feat(content): say what the row icons do, and what the media actually is - #189
Merged
Conversation
Two complaints with one cause: an icon-only control that never explains itself. **Action icons.** The three icons on an entry row carried no visible label, and the pin — the one nobody guesses — carried a hardcoded English `aria-label` saying "Pin to context", which names the mechanism rather than the effect. They now read "Attach entry to chat context", from the dictionary, with tooltip and `aria-label` resolved from one helper so the two cannot drift. Six hardcoded labels moved to the dictionary (the brief found four; `AssetCard` and the colour picker were also carrying them) and nine buttons that had no accessible name at all got one. Pin is a toggle, so it also gets `aria-pressed` and a label that names the direction it will go — "Remove from chat context" once attached. **Two accessibility bugs found on the way**, both from the same `opacity-0` that hides the icons until row hover: - On a touch device there is no hover, so edit / delete / attach were permanently invisible — while still taking taps. Worse than absent. - Tabbing to one left it invisible, focus ring included. The base hide moves to a `.reveal-on-hover` class in the components layer, where `@media (hover: none)` turns it back on and the existing `group-hover` utilities still outrank it, plus `focus-visible:opacity-100` on each button. Doing it in the cascade rather than as another utility means it does not depend on how Tailwind happens to sort variants. **Media fields.** An `IMAGE` row was a 32px thumbnail beside `8d2ed576-57e5-4cab-8f57-bfe52d56ddff.webp`. The thumbnail stays that size — list density is the reason it is small — and the full-size look now opens on hover, or on tap where there is no hover. A storage UUID renders as `WEBP · 8d2ed576`; a real filename is left alone. `video` and `file` were in the URL branch and printed as raw text. They are media now: video gets a tile and a preview, file gets a tile and no preview, because there is nothing to preview. And the three outcomes that used to look identical are now distinct: an asset that loads, a stored asset that 404s (an error — Studio put it there), and a value that was never a stored asset at all. The third is not broken, and is no longer fetched just to fail; it says so instead. New `atoms/Tooltip.vue` takes its trigger as a slot, which `InfoTooltip` could not — it owns its button. `InfoTooltip` and `ContentStatsBar` now delegate to it, retiring the two hand-rolled Radix stacks with their two different delays; `ContentStatsBar`'s hardcoded plurals went to the dictionary with them. `InfoTooltip`'s button had no accessible name either. Media string handling is in `app/utils/media-display.ts` — decisions about a string, better described by a table of inputs than by a mounted component.
ABB65
pushed a commit
that referenced
this pull request
Aug 13, 2026
T08b, the half of T08 held back until the list stopped rendering every row. #193 pages at 50, so this is now safe to land. `AtomsTooltip` carried its own `TooltipProvider`, which a paged list turns into 150 of them — 50 rows times three action icons. The count is the smaller problem. Radix's `skipDelayDuration` only applies within a single provider, so with one provider per tooltip it never applied at all: moving from the edit icon to the delete icon re-paid the full open delay every time. Scanning a row's icons is precisely what the tooltips were added for. Hoisted, the second and third open instantly. `TooltipRoot` throws without a provider and Radix exports no way to ask whether one is present, so the app root sets a flag. The flag and the provider live in one component — `AtomsTooltipScope` — because a flag provided without the provider it describes makes every tooltip in the app throw, and binding them together means a later edit cannot separate them. Outside that scope the atom still supplies its own, so a component mounted alone in a test keeps working. Delays move to `app/utils/tooltip-timing.ts`. There were three: 200ms in `InfoTooltip`, 300ms in `ContentStatsBar`, and Radix's own 700ms default. Which one you got depended on which component you hovered. Also verified, rather than assumed: `.reveal-on-hover` from #189 does what it claimed. In the built CSS the base hide sits at byte 10298 in the components layer, its `@media (hover: none)` override immediately after at 10332, and `group-hover/entry:opacity-60` at 184921 in utilities — so touch turns the icons on, hover still reveals them, and neither depends on how Tailwind sorts variants.
ABB65
added a commit
that referenced
this pull request
Aug 13, 2026
* feat(content): connect the search that was already built, and stop rendering a thousand rows at once Studio shipped a FlexSearch index, a worker handler, a `searchContent` function and an `@modelId query` parser — and nothing in `app/` ever called any of it. The palette's own help promised "search models, entries, vocabulary" and "@modelId to search within a model"; neither searched an entry. This wires the engine up rather than writing one. ## Two bugs in the engine, found on the way - **The model filter was applied after the limit.** The worker asked the index for `limit` hits and then dropped the ones from other models, so a search scoped to `articles` competed for those slots against every other model — and could come back empty while matching. - **Locale was stored and never filtered.** A Turkish list got English hits. Both are now decided before the cut, in `app/utils/search-results.ts`, where the order of operations can be stated and tested. Duplicate ids are dropped too: FlexSearch returns one set per indexed field, and the same entry was eating several slots. ## The list A search box that queries the index, so a match on page twenty is found without paging there. Results intersect with what this locale actually holds — the index is rebuilt per sync and can briefly name an entry the payload no longer has. Rendering is paged at 50. Every row is a `<details>` plus a stateful Radix dropdown plus three buttons; at 1000 articles that is the cost of opening a model, not the data. Virtualisation was the other option and was not taken: a new dependency, and rows change height when they expand. Paging leaves the row component untouched. "No matches" and "the index is not ready" are now different states. `searchContent` resolves to `[]` when the worker is absent, and reporting that as no results is a lie. ## The palette Entry hits are filled into their own ref and appended, because `buildResults` is synchronous and search is not. They go last, and the selection resets on the synchronous results rather than the final list, so an async hit landing does not yank the cursor from under someone already arrowing down. `SearchResult` carries no title, so each hit is read back and titled through the same resolver the list uses — without it the palette would list `f3a81c09d24e`. Selecting one navigates with `?entry=`, and the list pulls that entry to the front and opens it. Opening the model alone would have handed someone a thousand rows to find it in, which is what they searched to avoid. Also: the dictionary filter's placeholder said "Filter keys..." while the filter has always searched keys AND values. * perf(ui): one tooltip provider for the app, not one per tooltip T08b, the half of T08 held back until the list stopped rendering every row. #193 pages at 50, so this is now safe to land. `AtomsTooltip` carried its own `TooltipProvider`, which a paged list turns into 150 of them — 50 rows times three action icons. The count is the smaller problem. Radix's `skipDelayDuration` only applies within a single provider, so with one provider per tooltip it never applied at all: moving from the edit icon to the delete icon re-paid the full open delay every time. Scanning a row's icons is precisely what the tooltips were added for. Hoisted, the second and third open instantly. `TooltipRoot` throws without a provider and Radix exports no way to ask whether one is present, so the app root sets a flag. The flag and the provider live in one component — `AtomsTooltipScope` — because a flag provided without the provider it describes makes every tooltip in the app throw, and binding them together means a later edit cannot separate them. Outside that scope the atom still supplies its own, so a component mounted alone in a test keeps working. Delays move to `app/utils/tooltip-timing.ts`. There were three: 200ms in `InfoTooltip`, 300ms in `ContentStatsBar`, and Radix's own 700ms default. Which one you got depended on which component you hovered. Also verified, rather than assumed: `.reveal-on-hover` from #189 does what it claimed. In the built CSS the base hide sits at byte 10298 in the components layer, its `@media (hover: none)` override immediately after at 10332, and `group-hover/entry:opacity-60` at 184921 in utilities — so touch turns the icons on, hover still reveals them, and neither depends on how Tailwind sorts variants. --------- Co-authored-by: Contentrain <mcp@contentrain.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes T08a and T09. Two complaints, one cause: an icon-only control that never explains itself.
Action icons (T08a)
The three icons on an entry row had no visible label, and the pin — the one nobody guesses — carried a hardcoded English
aria-labelreading "Pin to context", which names the mechanism rather than the effect. It now reads "Attach entry to chat context", from the dictionary, with the tooltip and thearia-labelresolved from one helper so the two cannot drift.Pin is a toggle, so it also gets
aria-pressedand a label naming the direction it will go — "Remove from chat context" once attached.Inventory came out larger than the brief's:
aria-labelAssetCardand the colour picker were also carrying oneInfoTooltip's own button was one of themgrep -rn 'aria-label="[A-Z]' --include='*.vue' app/now returns nothing.Two accessibility bugs found on the way
Both from the
opacity-0that hides the icons until row hover:The base hide moved to
.reveal-on-hoverin the components layer, where@media (hover: none)turns it back on and the existinggroup-hoverutilities still outrank it, plusfocus-visible:opacity-100on each button. Doing it in the cascade rather than as another utility means it does not depend on how Tailwind happens to sort variants — which I did not want to bet on.Media fields (T09)
An
IMAGErow was a 32px thumbnail beside8d2ed576-57e5-4cab-8f57-bfe52d56ddff.webp.v-model:open+disableClosingTrigger).WEBP · 8d2ed576; a real filename is left alone, because a name someone chose is the best label available.videoandfilewere in the URL branch and printed as raw text. Video now gets a tile and a preview; file gets a tile and no preview, because there is nothing to preview.The three states that used to look identical
The third is the actual complaint — it is not broken, and rendering it as broken is what made editors report corrupt media for values Studio never held.
Tooltip atom
atoms/Tooltip.vuetakes its trigger as a slot, whichInfoTooltipcannot — it owns its button.InfoTooltipandContentStatsBarnow delegate to it, retiring the two hand-rolled Radix stacks and their two different delays (200ms / 300ms).The provider sits inside the atom:
TooltipRootthrows without one, so hoisting it to the app root would break any component test that mounts a tooltip-bearing component on its own. The cost is Radix's cross-tooltipskipDelayDuration— which neither ad-hoc stack had either, so nothing regresses; hoisting later is a one-line change.Also fixed, same files
ContentStatsBar's hardcoded plurals,ContentModelList'slocale/locales,ContentCollectionView'sentries, andContentFieldDisplay'sYes/Noall went to the dictionary. 14 new keys.Verified
pnpm lint— 0 errors (7 pre-existing warnings)pnpm typecheck— cleanpnpm test— 140 files / 1180 testsapp/utils/media-display.ts, 7 component tests on the media branches, 5 on the atom. The T11 tests already incontent-field-display.nuxt.test.tsare intact — I overwrote the file at one point and restored it; the suite now runs 15 there.Not verified — needs a browser
Everything below is a claim about rendering, and this PR does not prove any of it:
@media (hover: none), tap-to-preview) — no touch device in the test envOut of scope
T08b — per-row tooltips at list scale and touch discoverability inside the list — waits for T05's progressive rendering, per the split agreed in the plan.