@kolkrabbi/kol-component 0.228.0 → 0.230.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/package.json +4 -3
  2. package/src/atoms/ActionButton.jsx +1 -1
  3. package/src/atoms/Button.jsx +8 -2
  4. package/src/atoms/IconFrame.jsx +1 -1
  5. package/src/atoms/Input.jsx +3 -2
  6. package/src/atoms/SizeOrDownload.jsx +1 -1
  7. package/src/atoms/Tag.jsx +3 -3
  8. package/src/hooks/colorMath.js +1 -1
  9. package/src/index.js +7 -6
  10. package/src/molecules/ColorInputRow.jsx +1 -1
  11. package/src/molecules/ContentCard.jsx +5 -5
  12. package/src/molecules/ContentMedia.jsx +1 -1
  13. package/src/molecules/ContentRow.jsx +2 -2
  14. package/src/{utilities → molecules}/ContextMenu.jsx +1 -1
  15. package/src/molecules/DocsToc.jsx +1 -1
  16. package/src/molecules/LabeledControl.jsx +7 -4
  17. package/src/molecules/MenuItem.jsx +9 -1
  18. package/src/molecules/Modal.jsx +1 -1
  19. package/src/molecules/PageHeader.jsx +14 -7
  20. package/src/molecules/PaletteHarmonyWheel.jsx +4 -4
  21. package/src/molecules/ProfileCard.jsx +1 -1
  22. package/src/molecules/QuickLookFrame.jsx +1 -1
  23. package/src/molecules/SearchInput.jsx +7 -4
  24. package/src/molecules/ShellDrawer.jsx +1 -1
  25. package/src/molecules/SwatchControls.jsx +3 -3
  26. package/src/molecules/TabsRow.jsx +1 -1
  27. package/src/organisms/Canvas.jsx +1 -1
  28. package/src/organisms/ContentFilters.jsx +4 -11
  29. package/src/organisms/DocumentEditor.jsx +8 -7
  30. package/src/organisms/MediaLibrary.jsx +3 -5
  31. package/src/organisms/MediaLibraryExplorer.jsx +5 -1
  32. package/src/organisms/MediaLibraryPages.jsx +40 -17
  33. package/src/organisms/SectionCards.jsx +1 -1
  34. package/src/organisms/SectionCta.jsx +1 -1
  35. package/src/organisms/SectionFaq.jsx +1 -1
  36. package/src/organisms/SectionNewsletter.jsx +1 -1
  37. package/src/organisms/SettingsPanel.jsx +5 -2
  38. package/src/organisms/ShortcutsOverlay.jsx +8 -4
  39. package/src/organisms/sectionBleed.js +1 -1
  40. package/src/utilities/ErrorBoundary.jsx +1 -1
  41. package/src/utilities/FullscreenOverlay.jsx +1 -1
  42. package/src/utilities/LoaderOverlay.jsx +2 -2
  43. package/src/utilities/Popover.jsx +10 -1
  44. package/src/utilities/masthead.js +39 -0
  45. package/src/utilities/mediaSearch.js +41 -0
  46. package/src/utilities/searchItems.js +30 -0
  47. package/src/utilities/tone.js +1 -1
  48. /package/src/{utilities → atoms}/CloseButton.jsx +0 -0
  49. /package/src/{atoms → utilities}/CropOverlay.jsx +0 -0
  50. /package/src/{atoms → utilities}/CurveOverlay.jsx +0 -0
  51. /package/src/{atoms → utilities}/PathNodeOverlay.jsx +0 -0
  52. /package/src/{atoms → utilities}/SelectionOverlay.jsx +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-component",
3
- "version": "0.228.0",
3
+ "version": "0.230.0",
4
4
  "description": "KOL design-system components — atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,7 +25,8 @@
25
25
  "embla-carousel-react": "^8.6.0",
26
26
  "pdfjs-dist": "^6.3.289",
27
27
  "react-syntax-highlighter": "^16.1.1",
28
- "@kolkrabbi/kol-markdown": "^0.1.0"
28
+ "@kolkrabbi/kol-search": "^0.2.0",
29
+ "@kolkrabbi/kol-markdown": "^0.1.2"
29
30
  },
30
31
  "peerDependencies": {
31
32
  "@kolkrabbi/kol-icons": ">=0.22.0",
@@ -36,7 +37,7 @@
36
37
  "react-dom": "^18.3.0 || ^19.0.0"
37
38
  },
38
39
  "devDependencies": {
39
- "@kolkrabbi/kol-icons": "^0.29.0"
40
+ "@kolkrabbi/kol-icons": "^0.29.1"
40
41
  },
41
42
  "files": [
42
43
  "src",
@@ -146,7 +146,7 @@ export default function ActionButton({
146
146
  const rest = restRef.current
147
147
  const on = onRef.current
148
148
  /* no confirmIcon = ONE glyph, and the state is carried by the class alone
149
- * (a fill, a colour). Nothing to crossfade. */
149
+ * (a fill, a color). Nothing to crossfade. */
150
150
  if (!rest || !on) return undefined
151
151
 
152
152
  if (first.current) {
@@ -18,7 +18,7 @@ import { glyphSize } from '../hooks/glyphLadders.js'
18
18
  * @param {string} props.iconRight - Icon name to display on the right
19
19
  * @param {string} props.iconLeftHover - Icon to show on hover (left position)
20
20
  * @param {string} props.iconRightHover - Icon to show on hover (right position)
21
- * @param {'primary'|'secondary'|'inverted'|'outline'|'ghost'|'grey'|'sunken'} props.tone - the ground (`secondary` = the page surface, `inverted` = the text colour as fill — what `variant="secondary"` paints) (tone-is-the-ground-axis, 2026-09-03) — wins over `variant` on the same element; unset = inherit the wrapper's. `sunken` = the control set's dark well + fg-96 ink (ControlToneSunken); `inverse` aliased
21
+ * @param {'primary'|'secondary'|'inverted'|'outline'|'ghost'|'grey'|'sunken'} props.tone - the ground (`secondary` = the page surface, `inverted` = the text color as fill — what `variant="secondary"` paints) (tone-is-the-ground-axis, 2026-09-03) — wins over `variant` on the same element; unset = inherit the wrapper's. `sunken` = the control set's dark well + fg-96 ink (ControlToneSunken); `inverse` aliased
22
22
  * @param {string} props.iconOnly - Icon name for icon-only button
23
23
  * @param {string} props.iconOnlyHover - Icon to show on hover (icon-only)
24
24
  * @param {boolean} props.animateIcon - Disable default hover states to focus on icon animation
@@ -81,8 +81,14 @@ const Button = ({
81
81
  * grey) or its own bundle (nav · danger · accent); `nav` is ghost's chrome
82
82
  * rung, oq-80 ink + aria-current — it had lived in the theme with no
83
83
  * component able to emit it, the direct cause of the four-container header. */
84
+ /* An UNKNOWN variant is a typo, not a request for the inverted fill — it fell back to
85
+ * `kol-btn-secondary` (the text color as fill) until 2026-09-29, so a misspelt variant
86
+ * shipped inverted. It now stamps nothing, like an unset variant: the wrapper's tone,
87
+ * else primary — and says so in dev. */
84
88
  const KNOWN = ['primary', 'secondary', 'accent', 'outline', 'ghost', 'nav', 'danger', 'grey']
85
- const variantClass = !resolvedVariant ? '' : KNOWN.includes(resolvedVariant) ? `kol-btn-${resolvedVariant}` : 'kol-btn-secondary'
89
+ const known = !resolvedVariant || KNOWN.includes(resolvedVariant)
90
+ if (!known && import.meta.env.DEV) console.warn(`Button: unknown variant "${resolvedVariant}" — rendering the inherited tone (primary)`)
91
+ const variantClass = resolvedVariant && known ? `kol-btn-${resolvedVariant}` : ''
86
92
 
87
93
  // Add size class — pairs the padding rule with its mono type class.
88
94
  const sizeClass = size === 'sm'
@@ -22,7 +22,7 @@ import { toneClass } from '../utilities/tone.js'
22
22
  * frame's own classes declare the same background, foreground and geometry and
23
23
  * simply have no state rules to inherit.
24
24
  *
25
- * `variant` borrows the kol-btn COLOUR SET verbatim so the frame sits in the
25
+ * `variant` borrows the kol-btn COLOR SET verbatim so the frame sits in the
26
26
  * same visual system as real buttons; `primary` and `secondary` are inverse
27
27
  * pairs that flip with the theme, so light/dark comes free from the tokens with
28
28
  * no per-theme props.
@@ -158,8 +158,9 @@ export default function Input({
158
158
  * or <label> at the same kol-mono-N — Chromium computes input height
159
159
  * from the font's ascender+descender (font-metric), not strictly from
160
160
  * CSS line-height. Result: kol-control-sm ends up 26.5px instead of 26.
161
- * h-4 / h-[18px] / h-[22px] match the kol-mono-12 / -14 / -16 line-heights. */
162
- const heightCls = size === 'xs' ? 'h-3' : size === 'sm' ? 'h-4' : size === 'md' ? 'h-[18px]' : 'h-[22px]'
161
+ * h-4 / h-[18px] / h-[22px] match the kol-mono-12 / -14 / -16 line-heights; on touch every size is
162
+ * the 16 / 22 rung (D5, 2026-09-29 — see SearchInput) */
163
+ const heightCls = `${size === 'xs' ? 'h-3' : size === 'sm' ? 'h-4' : size === 'md' ? 'h-[18px]' : 'h-[22px]'} pointer-coarse:h-[22px]`
163
164
 
164
165
  const inputCls = [
165
166
  'min-w-0 bg-transparent border-none outline-none text-auto',
@@ -21,7 +21,7 @@ import { Icon } from '@kolkrabbi/kol-icons'
21
21
  * click, and this one is inside the link rather than being it.
22
22
  *
23
23
  * Both hover parts ink on the OPACITY scale (`text-oq-80`), not an `fg-*` role:
24
- * a stroke glyph on a flat fg colour reads wrong against the plate, and oq is
24
+ * a stroke glyph on a flat fg color reads wrong against the plate, and oq is
25
25
  * what the rest of the chrome uses.
26
26
  *
27
27
  * ONE type class throughout — kol-mono-12. helper-12 is line-height 1 against
package/src/atoms/Tag.jsx CHANGED
@@ -18,7 +18,7 @@ const ICON_SIZES = { xs: 8, sm: 10, md: 12, lg: 14 }
18
18
  * `:hover` rule between them, on `.tag-control`. Every other path rendered
19
19
  * dead.
20
20
  * - `color` was a SECOND axis that silently swapped the base class from
21
- * `tag-control` to `tag tag--{color}` — so passing a colour cost you the
21
+ * `tag-control` to `tag tag--{color}` — so passing a color cost you the
22
22
  * interaction state, invisibly. That is what shipped a solid blue pill you
23
23
  * could not hover.
24
24
  * - `variant="solid"` and a `solid` boolean did the same job.
@@ -28,8 +28,8 @@ const ICON_SIZES = { xs: 8, sm: 10, md: 12, lg: 14 }
28
28
  *
29
29
  * Now it is Pill's vocabulary — `primary` (filled) · `secondary` (outlined) ·
30
30
  * `inverse` — one size scale, ONE class scheme (`kol-tag--*`), and every
31
- * variant carries hover + active. Colour is not a prop: a chip's look is its
32
- * variant, exactly as it is on Pill and Button. Tag colour BY TAXONOMY returns
31
+ * variant carries hover + active. Color is not a prop: a chip's look is its
32
+ * variant, exactly as it is on Pill and Button. Tag color BY TAXONOMY returns
33
33
  * later as its own decision, on top of the variants rather than instead of them.
34
34
  *
35
35
  * @param {ReactNode} children label content
@@ -116,7 +116,7 @@ export function harmonyColors(hue, harmony, { saturation = 100, lightness = 50 }
116
116
  * package's `colors` payload was half-ignorable).
117
117
  *
118
118
  * A slot that is `locked`, empty, or has no hex is passed through untouched —
119
- * locking a colour is the one instruction a re-hue must not overrule.
119
+ * locking a color is the one instruction a re-hue must not overrule.
120
120
  *
121
121
  * @param {number} hue base hue, 0–360
122
122
  * @param {string|object} harmony harmony id or object
package/src/index.js CHANGED
@@ -22,14 +22,14 @@ export { default as AssetPlaceholder } from './utilities/AssetPlaceholder.jsx'
22
22
  export { default as Avatar } from './atoms/Avatar.jsx'
23
23
  export { default as Badge } from './atoms/Badge.jsx'
24
24
  export { default as Button } from './atoms/Button.jsx'
25
- export { default as CloseButton } from './utilities/CloseButton.jsx'
25
+ export { default as CloseButton } from './atoms/CloseButton.jsx'
26
26
  export { default as ActionButton } from './atoms/ActionButton.jsx'
27
27
  export { default as FileIcon } from './atoms/FileIcon.jsx'
28
28
  export { default as SizeOrDownload } from './atoms/SizeOrDownload.jsx'
29
29
  export { default as SortHeader } from './atoms/SortHeader.jsx'
30
30
  export { default as SortControls } from './molecules/SortControls.jsx'
31
31
  export { default as CopyButton } from './molecules/CopyButton.jsx'
32
- export { default as CurveOverlay } from './atoms/CurveOverlay.jsx'
32
+ export { default as CurveOverlay } from './utilities/CurveOverlay.jsx'
33
33
  export { default as Divider } from './atoms/Divider.jsx'
34
34
  export { default as DocsToc } from './molecules/DocsToc.jsx'
35
35
  export { default as DropdownTagFilter } from './molecules/DropdownTagFilter.jsx'
@@ -49,7 +49,7 @@ export { default as LabeledControl } from './molecules/LabeledControl.jsx'
49
49
  export { default as OverlayGlassPanel } from './utilities/OverlayGlassPanel.jsx'
50
50
  export { default as Pill } from './atoms/Pill.jsx'
51
51
  export { usePopover, PopoverPanel, Tooltip } from './utilities/Popover.jsx'
52
- export { default as ContextMenu, useContextMenu } from './utilities/ContextMenu.jsx'
52
+ export { default as ContextMenu, useContextMenu } from './molecules/ContextMenu.jsx'
53
53
  export { default as ProsePreview } from './utilities/ProsePreview.jsx'
54
54
  export { default as QuantityInput } from './molecules/QuantityInput.jsx'
55
55
  export { default as RotaryDial } from './atoms/RotaryDial.jsx'
@@ -73,8 +73,8 @@ export { default as InspectorRail } from './molecules/InspectorRail.jsx'
73
73
  /* SelectionOverlay's two siblings — same 1080-virtual contract, same zoom
74
74
  * division (editor-panels-the-held-specs B2). `pathMath` is their geometry,
75
75
  * exported because the editor engine re-exports it rather than keep a copy. */
76
- export { default as PathNodeOverlay } from './atoms/PathNodeOverlay.jsx'
77
- export { default as CropOverlay } from './atoms/CropOverlay.jsx'
76
+ export { default as PathNodeOverlay } from './utilities/PathNodeOverlay.jsx'
77
+ export { default as CropOverlay } from './utilities/CropOverlay.jsx'
78
78
  export { default as LayerStack, AddLayerButton, BLEND_MODES } from './organisms/LayerStack.jsx'
79
79
  export { TYPE_LABELS, BOOL_OP_LABELS, SHAPE_KIND_LABELS, labelForLayer, rowLabelForLayer, findLayerDeep } from './hooks/layerTree.js'
80
80
  export { default as TimelineDock, sampleTrack, TIMELINE_EASINGS } from './organisms/TimelineDock.jsx'
@@ -88,7 +88,7 @@ export { default as ButtonGroup } from './utilities/ButtonGroup.jsx'
88
88
  /* monorepo sets (P6–P10) — molecule members */
89
89
  export { default as AlignmentGrid } from './molecules/AlignmentGrid.jsx'
90
90
  export { default as ImageBlock } from './molecules/ImageBlock.jsx'
91
- export { default as SelectionOverlay } from './atoms/SelectionOverlay.jsx'
91
+ export { default as SelectionOverlay } from './utilities/SelectionOverlay.jsx'
92
92
  export { default as VideoBlock, getEmbedUrl } from './molecules/VideoBlock.jsx'
93
93
  export { default as SectionCardItem, default as CardFeatureItem } from './molecules/SectionCardItem.jsx'
94
94
  export { default as CodeBlock } from './molecules/CodeBlock.jsx'
@@ -152,6 +152,7 @@ export { default as EmblaNav } from './molecules/EmblaNav.jsx'
152
152
  export { default as MobileTabBar, TABBAR_H } from './molecules/MobileTabBar.jsx'
153
153
  export { default as ContentFilters } from './organisms/ContentFilters.jsx'
154
154
  export { default as PageHeader } from './molecules/PageHeader.jsx'
155
+ export { MastheadContext, MASTHEADS, useMasthead, mastheadTitleClass } from './utilities/masthead.js'
155
156
  export { default as ErrorBoundary } from './utilities/ErrorBoundary.jsx'
156
157
  export { default as FeatureSplit } from './organisms/FeatureSplit.jsx'
157
158
  export { default as FeaturedCarousel } from './organisms/FeaturedCarousel.jsx'
@@ -243,7 +243,7 @@ export default function ColorInputRow({
243
243
  )}
244
244
  {/* QUICK STATES. Theme (the auto value — a token that flips with
245
245
  light/dark) is offered only where the field HAS one; None is
246
- always available, because clearing a colour is not a palette
246
+ always available, because clearing a color is not a palette
247
247
  decision. Both were dropped in the first port, which is what left
248
248
  `value == null` renderable but unreachable. */}
249
249
  <div className="flex items-center gap-2">
@@ -47,7 +47,7 @@ const BOX = {
47
47
  /* SLIDE (slide-variant-and-shelf-preset, kol-client-olina 2026-09-03; user: "they are genuinely
48
48
  * different with 16:9 layout and those exposed properties"): file's stack — cover on top, the
49
49
  * plate below — on the PAGE'S surface, rest and hover (olina's /slide-deck, read off the render).
50
- * The plate is not a tone (user: "no just controls"); it is this kind's colour. */
50
+ * The plate is not a tone (user: "no just controls"); it is this kind's color. */
51
51
  slide: { layout: 'stack', border: null, bg: 'var(--kol-surface-primary)', pad: 'var(--kol-pad-card-sm)' },
52
52
  /* THE FRAME READS BACKWARDS (CatalogCardFrameAndZoom, kol-website 2026-08-28 — user, on a 212-tile
53
53
  * grid: no frame at rest; the old rest value is the hover): a wall of fg-04 frames is a grid of boxes,
@@ -94,7 +94,7 @@ const BOX = {
94
94
  * has no surface of its own to step, so it dims its title instead. */
95
95
  const HOVER = {
96
96
  file: 'var(--kol-oq-04)',
97
- slide: 'var(--kol-surface-primary)', /* the plate holds its colour on hover; the drawer control is the affordance */
97
+ slide: 'var(--kol-surface-primary)', /* the plate holds its color on hover; the drawer control is the affordance */
98
98
  catalog: 'var(--kol-surface-tertiary)',
99
99
  /* article and work take NO surface hover, and that is a decision not a gap:
100
100
  * article has no surface of its own (its media frame, when on, steps its
@@ -168,7 +168,7 @@ export default function ContentCard({
168
168
  * print draw it); `false` turns it off without an `!important` in a consumer sheet */
169
169
  plateRule,
170
170
  /* `bg` — the card's REST fill, overriding the variant's. It sets
171
- * `--kol-card-bg`, not a background, because the rest colours are custom
171
+ * `--kol-card-bg`, not a background, because the rest colors are custom
172
172
  * properties so the hover class can win; that is also why
173
173
  * `className="bg-oq-48"` does nothing here and a consumer reaching around the
174
174
  * component had to write `className="[--kol-card-bg:var(--kol-oq-48)]"`
@@ -437,7 +437,7 @@ export default function ContentCard({
437
437
  'data-tags': isHero && Array.isArray(text.tags) && text.tags.length ? text.tags.join(' ') : undefined,
438
438
  className: `kol-card group flex ${box.layout === 'canvas' && reveal != null ? 'has-reveal' : ''} ${expanded ? 'flex-col md:flex-row-reverse' : 'flex-col'} ${box.layout === 'drawer' ? 'relative overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${framed ? 'overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${box.border ? 'border' : ''} ${box.layout === 'canvas' ? 'relative' : ''} ${interactive ? 'cursor-pointer select-none' : ''} ${hoverBg && interactive ? 'kol-content-hover' : ''} ${interactive && box.frameHover ? 'kol-content-hover-frame' : ''} ${className}`.trim(),
439
439
  style: {
440
- /* same reason as ContentRow: rest colours are PROPERTIES, because an
440
+ /* same reason as ContentRow: rest colors are PROPERTIES, because an
441
441
  * inline background/borderColor outranks the hover class and the step
442
442
  * would never render. */
443
443
  '--kol-card-bg': bg ?? box.bg ?? undefined,
@@ -464,7 +464,7 @@ export default function ContentCard({
464
464
  onContextMenu={onContextMenu}
465
465
  onDoubleClick={onDoubleClick}
466
466
  /* SELECTED HAS TO SHOW (user 2026-09-23: a click in the grid "should select/highlight").
467
- * `selected` only flipped a border colour on the variants that HAVE a border, and the file
467
+ * `selected` only flipped a border color on the variants that HAVE a border, and the file
468
468
  * wall's cards have none — so the prop was true and the card looked untouched. The
469
469
  * attribute is the hook; the theme paints it, once, for every variant. */
470
470
  data-selected={selected || undefined}
@@ -57,7 +57,7 @@ import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
57
57
  * @param {boolean} frame tinted box + border UNDER the media
58
58
  * @param {boolean} border border only, no tint
59
59
  * @param {string} bg tint only, no border — a raw token value
60
- * @param {string} borderHover border colour on hover (article's fg-16 step)
60
+ * @param {string} borderHover border color on hover (article's fg-16 step)
61
61
  * @param {boolean} ring hairline border OVER the media, inset
62
62
  * @param {boolean|'hero'} zoom the artwork creeps up inside its frame on the
63
63
  * card's hover — 1.06; `'hero'` = the hero rung, 1.02
@@ -130,7 +130,7 @@ const BOX = {
130
130
  * its background and read as a different interaction from every other listing.
131
131
  *
132
132
  * The three passes are the lesson: the ask was "make it look like /work" and
133
- * each ticket named only the state someone had looked at — rest colours, then
133
+ * each ticket named only the state someone had looked at — rest colors, then
134
134
  * the derive's collateral, then this. When a user says make X look like Y,
135
135
  * diff EVERY state: rest, hover, selected, focus. */
136
136
  bg: 'var(--kol-surface-secondary)', frame: 'transparent', frameHover: 'var(--kol-fg-08)' },
@@ -171,7 +171,7 @@ export default function ContentRow({
171
171
  * card, `contentcard-bg-and-text-props` 2026-09-03; the pair ships together
172
172
  * and a consumer that re-grounds one hits the same wall on the other in the
173
173
  * same grid). It sets `--kol-row-bg`, not a background, because the rest
174
- * colours are custom properties so the hover class can win — which is why
174
+ * colors are custom properties so the hover class can win — which is why
175
175
  * `className="bg-oq-48"` does nothing here either. `selected` still wins:
176
176
  * a selected row is the list's state, not the consumer's ground. For the
177
177
  * row's INK, pass `text` — it falls through to ContentText. */
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useState } from 'react'
2
- import { usePopover, PopoverPanel } from './Popover.jsx'
2
+ import { usePopover, PopoverPanel } from '../utilities/Popover.jsx'
3
3
 
4
4
  /**
5
5
  * ContextMenu — a right-click menu, anchored at the pointer.
@@ -128,7 +128,7 @@ export default function DocsToc({
128
128
  * `block transition-colors focus-visible:ring-focus
129
129
  * hover:text-emphasis text-body` as utilities, so the same rung
130
130
  * rendered a different className here than in the left tree.
131
- * Layout, colour, hover and focus live in `.shell-nav-item` now;
131
+ * Layout, color, hover and focus live in `.shell-nav-item` now;
132
132
  * active is the shared `is-active` marker, not `text-emphasis`
133
133
  * typed at one call site. Matches RailRow exactly. */
134
134
  className={`shell-nav-item kol-mono-14${activeId === item.id ? ' is-active' : ''}`}
@@ -48,19 +48,22 @@ export default function LabeledControl({
48
48
  /* `'auto'`: the label is the yielding cell — flex-1, truncating — and the
49
49
  * control hugs. The control still carries min-w-0 + overflow-hidden so a
50
50
  * combo wider than the whole column CLIPS at the column edge rather than
51
- * painting over the neighbour (the invariant: no cell outside its column). */
51
+ * painting over the neighbour (the invariant: no cell outside its column).
52
+ * BELOW `sm` IT STACKS (apps review, 2026-09-29): the yielding label yielded to
53
+ * nothing on a phone — a sentence-long combo took the whole row and the label
54
+ * showed as "C" or not at all. Label on its line, the value under it. */
52
55
  const labelAuto = labelWidth === 'auto'
53
56
  return (
54
- <div className={`flex items-center gap-3 ${className}`}>
57
+ <div className={`flex ${labelAuto ? 'flex-col items-start gap-1 sm:flex-row sm:items-center sm:gap-3' : 'items-center gap-3'} ${className}`}>
55
58
  {showLabel && (
56
59
  <span
57
- className={`kol-helper-10 tracking-widest text-meta ${labelAuto ? 'flex-1 min-w-0 truncate' : 'shrink-0'}`}
60
+ className={`kol-helper-10 tracking-widest text-meta ${labelAuto ? 'max-w-full min-w-0 truncate sm:flex-1' : 'shrink-0'}`}
58
61
  style={labelAuto ? undefined : { width: labelWidth }}
59
62
  >
60
63
  {labelInner}
61
64
  </span>
62
65
  )}
63
- <div className={labelAuto ? 'min-w-0 overflow-hidden' : 'flex-1 min-w-0'}>{children}</div>
66
+ <div className={labelAuto ? 'min-w-0 max-w-full sm:overflow-hidden' : 'flex-1 min-w-0'}>{children}</div>
64
67
  </div>
65
68
  )
66
69
  }
@@ -19,6 +19,9 @@ import { PopoverPanel, usePopover } from '../utilities/Popover.jsx'
19
19
  * `Dropdown` molecule instead — MenuItem is for action menus / popovers
20
20
  * that hold arbitrary children.
21
21
  */
22
+ /* the ladder's tokens, so the touch rung (D5, 2026-09-29) grows the trigger with its row */
23
+ const TRIGGER_H = { xs: 'var(--kol-ctl-xs)', sm: 'var(--kol-ctl-sm)', md: 'var(--kol-ctl-md)', lg: 'var(--kol-ctl-lg)' }
24
+
22
25
  export function MenuItem({
23
26
  label,
24
27
  children,
@@ -33,6 +36,10 @@ export function MenuItem({
33
36
  * an affordance; neither reference draws one (ColumnBrowserMobileViews item
34
37
  * 15, kol-r2b2 2026-09-04). Default keeps every existing call-site. */
35
38
  caret = true,
39
+ /* THE TRIGGER IS ON THE LADDER (apps review 2026-09-29). It was a fixed `h-8` — 32px whatever sat
40
+ * beside it, so media's phone `···` stood 32 next to a 26 `sm` search field. `size` pins it to the
41
+ * control heights (01-foundations/09-sizes: 22 · 26 · 32 · 40); `md` is the 32 it always was. */
42
+ size = 'md',
36
43
  }) {
37
44
  const [open, setOpen] = useState(defaultOpen)
38
45
  const popover = usePopover({
@@ -51,7 +58,8 @@ export function MenuItem({
51
58
  ref={popover.refs.setReference}
52
59
  {...popover.getReferenceProps()}
53
60
  type="button"
54
- className={`kol-menu-btn kol-helper-12 px-3 h-8 inline-flex items-center gap-2 rounded text-body hover:text-emphasis transition-colors ${buttonClassName}`}
61
+ className={`kol-menu-btn kol-helper-12 px-3 inline-flex items-center gap-2 rounded text-body hover:text-emphasis transition-colors ${buttonClassName}`}
62
+ style={{ height: TRIGGER_H[size] ?? TRIGGER_H.md }}
55
63
  >
56
64
  <span>{label}</span>
57
65
  {caret && (
@@ -119,7 +119,7 @@ function ModalView({ state, closeWith }) {
119
119
  />
120
120
  )}
121
121
  <div className="flex gap-2 justify-end">
122
- {state.kind !== 'alert' && <Button variant="secondary" size="sm" onClick={cancel}>{state.cancelLabel ?? 'Cancel'}</Button>}
122
+ {state.kind !== 'alert' && <Button size="sm" onClick={cancel}>{state.cancelLabel ?? 'Cancel'}</Button>}
123
123
  <Button variant="primary" size="sm" onClick={submit}>{state.okLabel ?? 'OK'}</Button>
124
124
  </div>
125
125
  </div>
@@ -1,4 +1,5 @@
1
1
  import SectionText from './SectionText.jsx'
2
+ import { TITLE_ROLES, useMasthead } from '../utilities/masthead.js'
2
3
  /**
3
4
  * PageHeader — the page's masthead: an optional eyebrow, the title, and a
4
5
  * sub-line.
@@ -62,19 +63,25 @@ import SectionText from './SectionText.jsx'
62
63
  * with an `!important` on the margin — a DS text role re-implemented outside.
63
64
  * @param {string} subtitleMaxWidth the lede's measure (e.g. '800px' or '60ch'), a prop
64
65
  * instead of a consumer selector reaching inside
66
+ * @param {'display'|'mono'} masthead the APP's masthead (utilities/masthead.js, 2026-09-29): inside
67
+ * a Shell it comes from `AppShell masthead` and WINS over `voice`/`size`, and
68
+ * `display` drops the subtitle — one app, one header. Outside a Shell, unset =
69
+ * the props as given, so no site page moves.
65
70
  * The bottom rhythm is `--kol-page-header-mb` (default 40px): inline, as before, but through a
66
71
  * variable a consumer can re-point where an inline literal could only be `!important`-ed.
67
72
  */
68
73
  /* the title role per size and voice — the one thing PageHeader knows that the
69
- * base does not, because the base's ladder is the SECTION scale */
70
- const TITLE = {
71
- sans: { sm: 'kol-sans-heading-03', md: 'kol-sans-display-03', lg: 'kol-sans-display-02' },
72
- mono: { sm: 'kol-mono-heading-03', md: 'kol-mono-display-03', lg: 'kol-mono-display-02' },
73
- }
74
+ * base does not, because the base's ladder is the SECTION scale. Lives in
75
+ * utilities/masthead.js since 2026-09-29 so a tool's own title reads the same table. */
76
+ const TITLE = TITLE_ROLES
74
77
  /* the ONE thing the registers disagree about */
75
78
  const LEDE = { app: 'kol-mono-14', site: 'kol-sans-body-01' }
76
79
 
77
- export default function PageHeader({ eyebrow, title, subtitle, actions, subtitleMaxWidth, size = 'md', voice = 'sans', register = 'app', titleClass, className = '' }) {
80
+ export default function PageHeader({ eyebrow, title, subtitle: ownSubtitle, actions, subtitleMaxWidth, size: ownSize = 'md', voice: ownVoice = 'sans', register = 'app', titleClass, masthead, className = '' }) {
81
+ const m = useMasthead(masthead)
82
+ const voice = m ? m.voice : ownVoice
83
+ const size = m ? m.size : ownSize
84
+ const subtitle = m && !m.subtitle ? undefined : ownSubtitle
78
85
  const roles = TITLE[voice] ?? TITLE.sans
79
86
  return (
80
87
  /* the block owns its own rhythm — margins inline, never in a shared type
@@ -83,7 +90,7 @@ export default function PageHeader({ eyebrow, title, subtitle, actions, subtitle
83
90
  eyebrow={eyebrow}
84
91
  headline={title}
85
92
  headlineAs="h1"
86
- headlineClass={`text-fg-96 ${titleClass ?? roles[size] ?? roles.md}`}
93
+ headlineClass={`text-fg-96 ${titleClass ?? roles[size] ?? roles.md}${m?.upper ? ' uppercase' : ''}`}
87
94
  body={subtitle}
88
95
  bodyClass={`text-oq-64 ${LEDE[register] ?? LEDE.app} min-w-0`}
89
96
  actions={actions}
@@ -43,11 +43,11 @@ import { HARMONIES, harmonyById, harmonyColors, normHue, reHueSlots } from '../h
43
43
  * @param {Array} harmonies injectable scheme table (default HARMONIES)
44
44
  * @param {Array} slots the CURRENT palette in role order (`{hex, locked}` objects or plain hex strings). Given, `colors` re-hues these — each slot keeps its own S/L, locked and empty entries pass through — instead of generating flat ones
45
45
  * @param {Function} onChange ({ hue, colors }) => void
46
- * @param {Function} onHueChange (hue) => void — the payload-free seam, for a caller that derives its own colours
46
+ * @param {Function} onHueChange (hue) => void — the payload-free seam, for a caller that derives its own colors
47
47
  */
48
48
 
49
49
  /* Marker outline — white for contrast against the fully-saturated ring hues
50
- * (theme-independent: the wheel's colours, not the surface, sit behind it). */
50
+ * (theme-independent: the wheel's colors, not the surface, sit behind it). */
51
51
  const MARKER_STROKE = '#FFFFFF'
52
52
 
53
53
  export default function PaletteHarmonyWheel({
@@ -74,9 +74,9 @@ export default function PaletteHarmonyWheel({
74
74
  /* Emit next hue + its harmony colors. Held in a ref so the pointer/key
75
75
  * handlers stay stable while always seeing the latest props.
76
76
  *
77
- * With `slots`, the colours are the CALLER'S palette re-hued — each slot
77
+ * With `slots`, the colors are the CALLER'S palette re-hued — each slot
78
78
  * keeping its own saturation and lightness — rather than a fresh flat set.
79
- * Both fire, so a caller can take the hue and ignore the colours. */
79
+ * Both fire, so a caller can take the hue and ignore the colors. */
80
80
  emitRef.current = (nextHue) => {
81
81
  const h = normHue(nextHue)
82
82
  const colors = slots?.length
@@ -27,7 +27,7 @@ import { surfaceClass } from '../utilities/sectionSurface.js'
27
27
  * `inverse` with inverse ink, exactly as shipped. `controlVariant` goes straight
28
28
  * to the disclosure's `IconFrame`; `pad` is `ContentCard`'s — one step on
29
29
  * `--kol-pad-card-*`, overriding the size ramp's padding. The logo is a slot
30
- * and stays one: its ink follows the shelf, a coloured mark is the asset's.
30
+ * and stays one: its ink follows the shelf, a colored mark is the asset's.
31
31
  *
32
32
  * THE SHELF SIZES TO ITS CONTENT. The source held it in a fixed box
33
33
  * (`h-60`/`h-44`/`h-32`/`h-24`, `overflow-hidden`) and every vertical size
@@ -1,6 +1,6 @@
1
1
  import { useRef, useState } from 'react'
2
2
  import Button from '../atoms/Button.jsx'
3
- import CloseButton from '../utilities/CloseButton.jsx'
3
+ import CloseButton from '../atoms/CloseButton.jsx'
4
4
 
5
5
  /* taxonomy-ok: molecule — nests Button (atom) + CloseButton (utility). */
6
6
 
@@ -103,8 +103,9 @@ export default function SearchInput({
103
103
  if (expanding && isOpen) inputRef.current?.focus()
104
104
  }, [expanding, isOpen])
105
105
 
106
- /* the pinned squares, 01-foundations/09-sizes.md — 22 · 26 · 32 · 40 */
107
- const square = { sm: 28, md: 32, lg: 36 }[size] ?? 32
106
+ /* the pinned squares, 01-foundations/09-sizes.md — 22 · 26 · 32 · 40, read from the theme's
107
+ * `--kol-ctl-*` so the touch rung (D5, 2026-09-29) grows them with every other control */
108
+ const square = `var(--kol-ctl-${['xs', 'sm', 'md', 'lg'].includes(size) ? size : 'md'})`
108
109
  /* THE OPEN FIELD'S HEIGHT IS ITS OWN KNOB, defaulting to the square.
109
110
  *
110
111
  * /work's pill is `h-9` open AND closed, sitting level with a 36px toggle —
@@ -209,8 +210,10 @@ export default function SearchInput({
209
210
 
210
211
  /* Same height pin as Input: Chromium sizes an <input> from font metrics,
211
212
  * not CSS line-height, so without this the shell lands ~0.5px tall.
212
- * h-4 / h-[18px] match the kol-mono-12 / -14 line-heights. */
213
- const heightCls = size === 'sm' ? 'h-4' : 'h-[18px]'
213
+ * h-4 / h-[18px] match the kol-mono-12 / -14 line-heights. On TOUCH the input types 16 / 22 (the
214
+ * touch rung, D5 2026-09-29), and this utility out-ranked the theme's coarse rule — 16px text in a
215
+ * 16px line in a 26px box; `pointer-coarse:` carries the 22 so the box grows with its row. */
216
+ const heightCls = `${size === 'sm' ? 'h-4' : 'h-[18px]'} pointer-coarse:h-[22px]`
214
217
 
215
218
  const inputCls = [
216
219
  'min-w-0 flex-1 bg-transparent border-none outline-none text-auto',
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
2
2
  import { pushLayer, popLayer, isTopLayer } from '../utilities/layerStack.js'
3
3
  import { createPortal } from 'react-dom'
4
4
  import { Icon } from '@kolkrabbi/kol-icons'
5
- import CloseButton from '../utilities/CloseButton.jsx'
5
+ import CloseButton from '../atoms/CloseButton.jsx'
6
6
  import usePrefersReducedMotion from '../hooks/usePrefersReducedMotion.js'
7
7
 
8
8
  /* taxonomy-ok: nests kol-icons's Icon (a package import the relative-import
@@ -4,7 +4,7 @@ import ColorSwatch from '../atoms/ColorSwatch.jsx'
4
4
  import { Tooltip } from '../utilities/Popover.jsx'
5
5
 
6
6
  /*
7
- * SwatchControls — the Photoshop-style top row of the colour panel, in two
7
+ * SwatchControls — the Photoshop-style top row of the color panel, in two
8
8
  * hand-tuned pieces plus the composed row. Hand-tuned visuals (fixed pixel
9
9
  * slots, the overlap-by-DOM-order trick, the double-ring halo, the red-slash
10
10
  * "none" marker). DO NOT refactor pieces to atoms — the look is intentional
@@ -20,7 +20,7 @@ import { Tooltip } from '../utilities/Popover.jsx'
20
20
  * No z-index, no transform — DOM order alone stacks them.
21
21
  *
22
22
  * <EyedropPick sampleColor onPick disabled />
23
- * — eyedropper icon button + a small sample chip of the sampled colour.
23
+ * — eyedropper icon button + a small sample chip of the sampled color.
24
24
  * The button is feature-gated: it is hidden entirely when the browser
25
25
  * EyeDropper API is unavailable, and `disabled` dims it when supported
26
26
  * but unusable. The actual EyeDropper call + canvas sampling live at the
@@ -92,7 +92,7 @@ export function SwatchStack({
92
92
  }
93
93
 
94
94
  /**
95
- * EyedropPick — eyedropper icon button + a sample chip of the sampled colour.
95
+ * EyedropPick — eyedropper icon button + a sample chip of the sampled color.
96
96
  * The button is hidden when the browser EyeDropper API is unavailable (no
97
97
  * affordance for an action that can't run); when supported but unusable pass
98
98
  * `disabled` to dim it. `onPick` is the app seam where EyeDropper + canvas
@@ -1,5 +1,5 @@
1
1
  import { useRef } from 'react'
2
- import CloseButton from '../utilities/CloseButton.jsx'
2
+ import CloseButton from '../atoms/CloseButton.jsx'
3
3
  import { Icon } from '@kolkrabbi/kol-icons'
4
4
 
5
5
  /* taxonomy-ok: nests kol-icons's Icon */
@@ -93,7 +93,7 @@ export function CanvasFrame({
93
93
  const { ratio, label } = resolveAspect(aspect, customRatio, aspects)
94
94
  /* NO guideColor → THEME INK (editor DS sync 2026-09-27, from the design editor's copy, which had
95
95
  * it and this lift had not): the border and label follow the theme, so the frame reads on a light
96
- * page and a dark one. A guideColor (a type frame's own colour) still tints both. */
96
+ * page and a dark one. A guideColor (a type frame's own color) still tints both. */
97
97
  const borderColor = guideColor ? `color-mix(in srgb, ${guideColor} 24%, transparent)` : 'var(--kol-oq-24)'
98
98
  const labelColor = guideColor ? `color-mix(in srgb, ${guideColor} 70%, transparent)` : 'var(--kol-fg-64)'
99
99
  const virtualH = CANVAS_VIRTUAL_W / ratio
@@ -7,6 +7,7 @@ import { glyphSize } from '../hooks/glyphLadders.js'
7
7
  import SearchInput from '../molecules/SearchInput.jsx'
8
8
  import IconFrame from '../atoms/IconFrame.jsx'
9
9
  import { Tooltip } from '../utilities/Popover.jsx'
10
+ import { filterItems } from '../utilities/searchItems.js'
10
11
 
11
12
  /**
12
13
  * ContentFilters — universal filter component for content grids.
@@ -214,16 +215,8 @@ const ContentFilters = ({
214
215
  }
215
216
 
216
217
  const filteredItems = useMemo(() => {
217
- let result = items
218
- if (searchText) {
219
- const q = searchText.toLowerCase()
220
- result = result.filter((item) =>
221
- searchKeys.some((key) => {
222
- const val = item[key]
223
- return val && String(val).toLowerCase().includes(q)
224
- }),
225
- )
226
- }
218
+ /* the KOL engine, not a private substring match (apps review 2026-09-29) — utilities/searchItems */
219
+ let result = filterItems(items, searchText, { keys: searchKeys })
227
220
  if (activeFilters.size === 0) return result
228
221
  return result.filter((item) => {
229
222
  let matches = true
@@ -402,7 +395,7 @@ const ContentFilters = ({
402
395
  * 05-control-chrome.md:109 — "any icon-only control in chrome is
403
396
  * IconFrame; nothing hand-writes the square". This wore
404
397
  * `kol-btn-md kol-btn-icon` and then removed the background, the
405
- * border and the colour by inline style, which is the whole button
398
+ * border and the color by inline style, which is the whole button
406
399
  * paid for and thrown away — and it left the control with no
407
400
  * states at all while the search beside it had hover.
408
401
  *