| 1 | # π€ Icons β one system across web and native |
| 2 | |
| 3 | Three kinds of mark, three sources, one rule about emoji. |
| 4 | |
| 5 | ## The three kinds |
| 6 | |
| 7 | 1. **UI chrome** (carets, the kebab, message actions, app marks for |
| 8 | first-party apps): **Lucide** on web (https://lucide.dev, ISC β vendored, |
| 9 | never a CDN), **SF Symbols** on iOS/Mac (built into Apple platforms; |
| 10 | their license keeps them off the web, which is why one literal library |
| 11 | everywhere isn't possible). The table below keeps the two in step. |
| 12 | 2. **Brand marks** (the integration apps): real logos, vendored monochrome in |
| 13 | `web/static/logos.js` β Simple Icons (CC0) where available, an official |
| 14 | asset otherwise (DNSimple's mask icon β note it needs |
| 15 | `fill-rule="evenodd"` or the knockout fills solid). Brands have no SF |
| 16 | Symbols; native gets the same SVGs as template images in the asset |
| 17 | catalogs, tinted like SF. |
| 18 | 3. **Emoji** β **content, never chrome.** An emoji is right where a person or |
| 19 | a model *chose* it: a member bot's title ("RTE News Hourly πΊ"), a member |
| 20 | app's glyph, a view's glyph, reactions. Interface furniture never uses |
| 21 | emoji. Every first-party app still declares an emoji `glyph` β it is the |
| 22 | cross-surface fallback (native command palette, string contexts) β but |
| 23 | web surfaces render its `logo`. |
| 24 | |
| 25 | ## How it's wired (web) |
| 26 | |
| 27 | - `web/static/icons.js` β every chrome icon as a static in-code SVG string |
| 28 | (24Γ24 viewBox, `currentColor`, stroke idiom). Only ever injected via |
| 29 | `innerHTML` with these constants β never user content. |
| 30 | - `web/static/logos.js` β the brand marks, same shape. |
| 31 | - Apps carry `logo:` beside `glyph:` in `defineApp`; `appGlyphInto(el, app)` |
| 32 | (apps.js) renders logo-else-glyph everywhere app identity draws. |
| 33 | - The Views button's caret is the one inline SVG in `web/pages/index.html` |
| 34 | (static markup; same chevron as `ICON_CHEVRON_DOWN`). |
| 35 | |
| 36 | **Adding an icon** = vendor the Lucide SVG into `icons.js` (keep the |
| 37 | `(Lucide: <slug>)` comment), add a row here with its SF Symbol, use it. |
| 38 | |
| 39 | ## The mapping |
| 40 | |
| 41 | | Meaning | icons.js (Lucide slug) | SF Symbol (native) | |
| 42 | |---|---|---| |
| 43 | | Reply | `ICON_REPLY` (hand-drawn, pre-Lucide) | `arrowshape.turn.up.left` | |
| 44 | | Reply in thread | `ICON_THREAD` (messages-square) | `bubble.left.and.bubble.right` | |
| 45 | | Threads list | `ICON_THREAD` (messages-square) | `bubble.left.and.bubble.right` (iOS kebab; the Mac kebab's rows are text-only β see below) | |
| 46 | | Convert to thread | `ICON_FOLD_IN` (fold-vertical) | `arrow.down.right.and.arrow.up.left` | |
| 47 | | Close / dismiss | `ICON_X` | `xmark` | |
| 48 | | Edit | `ICON_EDIT` | `pencil` | |
| 49 | | React | `ICON_REACT` | `face.smiling` | |
| 50 | | Delete | `ICON_TRASH` | `trash` | |
| 51 | | Play | `ICON_PLAY` | `play.fill` | |
| 52 | | Pinned chat | `ICON_PIN` | `pin.fill` | |
| 53 | | Muted chat | `ICON_MUTE` | `bell.slash` | |
| 54 | | Report | `ICON_REPORT` | `flag` | |
| 55 | | Copy | `ICON_COPY` | `doc.on.doc` | |
| 56 | | Dropdown caret | `ICON_CHEVRON_DOWN` (chevron-down) | `chevron.down` | |
| 57 | | Back | `ICON_CHEVRON_LEFT` (chevron-left) | `chevron.left` | |
| 58 | | Kebab | `ICON_MORE_VERTICAL` (ellipsis-vertical) | `ellipsis` | |
| 59 | | Memory | `ICON_BRAIN` (brain) | `brain` | |
| 60 | | To-do | `ICON_SQUARE_CHECK` (square-check) | `checkmark.square` | |
| 61 | | Reminders | `ICON_ALARM_CLOCK` (alarm-clock) | `alarm` | |
| 62 | | Bookmarks | `ICON_BOOKMARK` (bookmark) | `bookmark` | |
| 63 | | Poll | `ICON_CHART_COLUMN` (chart-column) | `chart.bar` | |
| 64 | | Board | `ICON_PRESENTATION` (presentation) | `rectangle.on.rectangle` | |
| 65 | | Vault | `ICON_LOCK` (lock) | `lock` | |
| 66 | | Emoji picker | `ICON_SMILE` (smile) | `face.smiling` | |
| 67 | | GIF | `ICON_FILM` (film) | `film` | |
| 68 | | Admin | `ICON_WRENCH` (wrench) | `wrench.adjustable` | |
| 69 | | App Studio | `ICON_PUZZLE` (puzzle) | `puzzlepiece` | |
| 70 | |
| 71 | Brand marks (`logos.js`): GitHub, DNSimple, Stripe, HubSpot, Linear |
| 72 | (awaiting the linear app), plus Lucide's `plug` for the Integrations app |
| 73 | itself (a capability, not a brand). |
| 74 | |
| 75 | ## Native status |
| 76 | |
| 77 | Built (`ios/LChatCore/Sources/LChatCore/AppIdentity.swift`): the appId β |
| 78 | SF-Symbol map above plus the brand marks as vendored template PNGs in |
| 79 | `LChatCore/Marks/` (the same SVGs as `logos.js`, rasterized |
| 80 | black-on-transparent at 96px, tinted at render β regenerate with resvg, |
| 81 | remembering the vendored strings need an `xmlns` added). Wired into both |
| 82 | platforms' avatar ladders (photo β mark β symbol β glyph β initials): the |
| 83 | iOS thread list/search/share rows, the Mac sidebar, rail, feed header, |
| 84 | search, βK palette, and both New-chat pickers. The old "β¦" placeholder is |
| 85 | gone β unmapped apps get `square.grid.2x2`. Device verify + the release |
| 86 | changelog ride the next native release mint. |
| 87 | |
| 88 | One row of the table is ahead of native: **Reply in thread**. The web strip |
| 89 | and menus now draw `ICON_THREAD`, but the native message menus still label a |
| 90 | second `arrowshape.turn.up.left` (`StockChatFeed.swift`, `MessageBubble.swift`) |
| 91 | β tolerable there because those rows carry text, whereas the web hover strip is |
| 92 | icon-only and two arrows read as one control drawn twice. Swap them to |
| 93 | `bubble.left.and.bubble.right` on the next native touch. |
| 94 | |
| 95 | The **Threads list** follows the same platform split as every other kebab |
| 96 | row: the web's β€ menu and the iOS β― menu draw the icon (iOS menu rows are |
| 97 | `Label`s on one icon grid), while the Mac header kebab is text-only rows by |
| 98 | construction β nothing else in it carries a glyph either. |
| 99 | |
| 100 | **Convert to thread** carries a glyph on exactly two of its four surfaces: the |
| 101 | web's picker (icon-only rows, so it needs one) and the iOS long-press sheet. |
| 102 | The iOS context menu and the Mac `NSMenu` are text-only rows by construction, |
| 103 | so there is nothing to map there. |
| 104 | |