Lab 13 / the biggest gap, closed
Every product this size ends up with a library of two hundred glyphs and no rule about when to use one. That happens because icons get treated as decoration for a control that already has a word on it.
This system needs far fewer, and the reason is a good one: it already encodes state in fill and colour. A status has a mark. A severity has an edge. A deadline has a border style. None of those want an icon on top, because a second encoding of the same fact is noise wearing a uniform. What is left is the small set of things a word cannot carry at a glance, and there are thirty-three of them.
They come from Lucide, which the wireframes already ship, brought onto our grid with a three property CSS override. The nav names below are the same ones lib/nav.ts imports today, so nothing has to be re-picked when this reaches the build.
01 / the rule
Filter, Views and Columns are words with no icon. The search field keeps a magnifier because it is an input, not a button, and a bare box with a placeholder is ambiguous in a way a labelled button is not. The rule never bent: an icon never duplicates a word, and the magnifier is not duplicating one, it is marking a field.
02 / construction
Lucide draws on a 24 unit grid with a 2 unit stroke, round caps and round joins. The round caps are the thing that would read as borrowed next to a hairline rule and a zero radius table. CSS beats the presentation attributes Lucide ships, so one rule fixes it everywhere, for every icon, including ones nobody has picked yet.
Lucide's own. Rendered at 15, 16 or 20 pixels and nothing between, so the geometry lands on whole pixels at every size we use.
Lucide ships 2. At our render sizes that sits heavier than the 12.5px body text beside it, so it comes down a notch: present next to a hairline rule, never shouting over a name.
The one real change. Lucide is round on both; the platform has zero radius everywhere else, and a rounded stroke end beside a square table cell is the thing that gives a borrowed set away.
Lucide has no filled variant, which happens to be exactly right here. Fill means settled on this platform, and an icon is never a state.
/* the whole override */ .tb-icon { stroke-linecap: square; stroke-linejoin: miter; stroke-width: 1.75; }
<Plane strokeLinecap="square" strokeLinejoin="miter" strokeWidth={1.75} size={16} /> works too, but a wrapper component that applies the three defaults once is better than three props on every call site. Either way there is no fork of the library and no drawing to maintain.
03 / the set
04 / what the set buys immediately
Same booking, same nine items, same 31 pixel rows. The Kind column carried "Flight · 24 pax", "Hotel · 7 nights", "Transfer · 24 pax" in ninety pixels of width. Now the shape carries the kind and the figure carries the quantity, in twenty-eight.
| Item | Kind | Dates | Confirmation | Sell | State |
|---|---|---|---|---|---|
| Tokyo Haneda to Osaka Itami ITM-2K9DQ4XW | Flight · 24 pax | 14 Sep | not ticketed | 38,900.00 | Awaiting issue |
| Park Hyatt Kyoto · 12 rooms ITM-7WB3N8LM | Hotel · 7 nights | 14 - 21 Sep | HTL-99204817 | 61,300.00 | Issued |
| Airport transfers · 4 vehicles ITM-3XN5V9TC | Transfer · 24 pax | 14 Sep | no driver assigned | 4,800.00 | Confirming |
| Item | Party | Dates | Confirmation | Net | Sell | Margin | State | |
|---|---|---|---|---|---|---|---|---|
| Tokyo Haneda to Osaka Itami ITM-2K9DQ4XW | 24 | 14 Sep | not ticketed | 31,880.00 | 38,900.00 | 18.0% | Awaiting issue | |
| Park Hyatt Kyoto · 12 rooms ITM-7WB3N8LM | 12 | 14 - 21 Sep | HTL-99204817 | 52,140.00 | 61,300.00 | 14.9% | Issued | |
| Airport transfers · 4 vehicles ITM-3XN5V9TC | 24 | 14 Sep | no driver assigned | 3,960.00 | 4,800.00 | 17.5% | Confirming | |
| Gala dinner · Kikunoi ITM-5JR9B3FS | 24 | 18 Sep | DIN-11938 | 7,100.00 | 9,400.00 | 24.5% | Issued | |
| Conference room · 3 days ITM-8DK2W6PY | 30 | 15 - 17 Sep | VEN-77320 | 9,600.00 | 12,400.00 | 22.6% | Issued |
One column of shapes bought back enough width for net cost and margin per item, which an agent previously had to open the Commercials card to see. This is the same trade the funnel made when it collapsed three count columns into one: an icon is only worth its pixels when it lets a fact onto the screen that could not fit before. If the width is not spent on something, the icon is decoration.
04b / the toolbar, settled after three passes
Three attempts, and the third is right. Icon beside word was duplication. Icon instead of word looked tidy in a specification and thin in the actual product, because a bookmark and three vertical bars do not tell somebody what they do while a word always does. What survives is the field.
Filter, Views and Columns are buttons, and a button's word is its name. A search field has no name, it has a placeholder, and a placeholder is content that disappears the moment somebody types. The magnifier is the only thing that still says what the box is once a person has typed into it. That is not a label duplicating a word, it is the affordance surviving the content.
The rule was tested from both sides in the same week. Adding an icon beside a word broke it. Removing the word to keep the icon technically satisfied it and made the product worse, which is the more dangerous failure, because a rule that can be satisfied while damaging the thing it protects is a rule stated at the wrong level. It is now stated at the right one: an icon may not replace a word that is doing work. The magnifier passes because no word was doing that work.
05 / motion
An operator opens this platform at nine and closes it at six. Anything that moves for pleasure is a thing they will watch four hundred times, and by the third week it is friction. So the rule is narrow: motion exists to tell a person their action landed, and to stop a surface appearing out of nowhere.
Hover the panel. Opacity and five pixels of rise. No scale, because a dialog that grows implies it came from somewhere it did not.
Hover the panel. From the edge it belongs to, at the distance it actually travels.
Hover the panel. The active mark grows from its own baseline. This is the only place a scale transform is allowed.
Numbers never count up, charts never draw in, table rows never stagger, skeletons never shimmer.
No counting up, no chart draw-in, no row stagger, no shimmer. A figure that animates is a figure a person cannot read yet, and a board opened from a link must be correct the instant it appears.
Filtering, sorting, searching and tab switching happen in the same frame as the input. Animating a filter makes a fast action feel slow and makes a slow one feel broken.
90 against 140. Arriving deserves a moment, leaving does not, and matching them makes dismissal feel sticky.
A sheet slides from its edge. A popover rises from its trigger. Nothing fades in from the middle of the screen, because that teaches a person nothing about where it came from or how to get back.
Not slower, not gentler. Under prefers-reduced-motion every duration is 0 and every transform is removed. The interface is fully usable with no motion at all, which is also the proof that none of it was load bearing.
Ruled
No custom drawing, no fork, no maintenance line.
Words for Filter, Views and Columns. A magnifier inside the search field and nowhere else. Export and Remind keep their words and take no icon. The rule was tested from both sides in one week and is now stated at the level that survives: an icon may not replace a word that is doing work. The magnifier passes because no word was doing that work; a placeholder disappears the moment somebody types.