@kolkrabbi/kol-component 0.229.0 → 0.231.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 (40) hide show
  1. package/package.json +4 -4
  2. package/src/atoms/ActionButton.jsx +1 -1
  3. package/src/atoms/Button.jsx +2 -2
  4. package/src/atoms/IconFrame.jsx +1 -1
  5. package/src/atoms/Kbd.jsx +33 -0
  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/PaletteHarmonyWheel.jsx +4 -4
  17. package/src/molecules/ProfileCard.jsx +1 -1
  18. package/src/molecules/QuickLookFrame.jsx +1 -1
  19. package/src/molecules/SearchInput.jsx +10 -9
  20. package/src/molecules/ShellDrawer.jsx +1 -1
  21. package/src/molecules/SwatchControls.jsx +3 -3
  22. package/src/molecules/TabsRow.jsx +1 -1
  23. package/src/organisms/Canvas.jsx +1 -1
  24. package/src/organisms/ContentFilters.jsx +1 -1
  25. package/src/organisms/MediaLibraryPages.jsx +1 -1
  26. package/src/organisms/SectionCards.jsx +1 -1
  27. package/src/organisms/SectionCta.jsx +1 -1
  28. package/src/organisms/SectionFaq.jsx +1 -1
  29. package/src/organisms/SectionNewsletter.jsx +1 -1
  30. package/src/organisms/ShellSearchOverlay.jsx +82 -38
  31. package/src/organisms/sectionBleed.js +1 -1
  32. package/src/utilities/FullscreenOverlay.jsx +1 -1
  33. package/src/utilities/LoaderOverlay.jsx +2 -2
  34. package/src/utilities/Popover.jsx +10 -1
  35. package/src/utilities/tone.js +1 -1
  36. /package/src/{utilities → atoms}/CloseButton.jsx +0 -0
  37. /package/src/{atoms → utilities}/CropOverlay.jsx +0 -0
  38. /package/src/{atoms → utilities}/CurveOverlay.jsx +0 -0
  39. /package/src/{atoms → utilities}/PathNodeOverlay.jsx +0 -0
  40. /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.229.0",
3
+ "version": "0.231.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,8 +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.1",
29
- "@kolkrabbi/kol-search": "^0.2.0"
28
+ "@kolkrabbi/kol-search": "^0.2.0",
29
+ "@kolkrabbi/kol-markdown": "^0.1.2"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@kolkrabbi/kol-icons": ">=0.22.0",
@@ -37,7 +37,7 @@
37
37
  "react-dom": "^18.3.0 || ^19.0.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@kolkrabbi/kol-icons": "^0.29.0"
40
+ "@kolkrabbi/kol-icons": "^0.31.0"
41
41
  },
42
42
  "files": [
43
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
@@ -82,7 +82,7 @@ const Button = ({
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
84
  /* An UNKNOWN variant is a typo, not a request for the inverted fill — it fell back to
85
- * `kol-btn-secondary` (the text colour as fill) until 2026-09-29, so a misspelt variant
85
+ * `kol-btn-secondary` (the text color as fill) until 2026-09-29, so a misspelt variant
86
86
  * shipped inverted. It now stamps nothing, like an unset variant: the wrapper's tone,
87
87
  * else primary — and says so in dev. */
88
88
  const KNOWN = ['primary', 'secondary', 'accent', 'outline', 'ghost', 'nav', 'danger', 'grey']
@@ -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.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Kbd — a key cap: one key or chord (↵, ⌘K, Esc) shown as an affordance.
3
+ * Replaces the two hand-rolled <kbd> chips (SearchInput's shortcut hint and
4
+ * the palette footer) that had drifted apart (2026-09-30). A plate, so it
5
+ * takes oq — never fg (icons-use-oq law).
6
+ *
7
+ * aria-hidden by default: a cap is a hint beside a label, not the label.
8
+ *
9
+ * @param {'sm'|'md'} size sm = inside a control (16px, helper-10) ·
10
+ * md = beside helper-12 text (20px)
11
+ * @param {string} icon kol-icons name drawn before the children — a key
12
+ * with a symbol (↵ `corner-down-left`, ⌘ `command`)
13
+ * is a GLYPH, never a typed character in the mono face
14
+ */
15
+ import { Icon } from '@kolkrabbi/kol-icons'
16
+
17
+ const SIZE = {
18
+ sm: 'h-4 min-w-4 px-1 kol-helper-10',
19
+ md: 'h-5 min-w-5 px-1.5 kol-helper-12',
20
+ }
21
+
22
+ export default function Kbd({ size = 'md', icon, className = '', children, ...rest }) {
23
+ return (
24
+ <kbd
25
+ aria-hidden="true"
26
+ className={`inline-flex items-center justify-center gap-0.5 shrink-0 rounded-[var(--kol-radius-xs)] bg-oq-08 text-oq-64 ${SIZE[size] ?? SIZE.md} ${className}`.trim()}
27
+ {...rest}
28
+ >
29
+ {icon && <Icon name={icon} size={12} />}
30
+ {children}
31
+ </kbd>
32
+ )
33
+ }
@@ -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'
@@ -59,6 +59,7 @@ export { default as SectionText } from './molecules/SectionText.jsx'
59
59
  export { default as SectionLabel } from './atoms/SectionLabel.jsx'
60
60
  export { default as SegmentedToggle } from './atoms/SegmentedToggle.jsx'
61
61
  export { default as Stepper } from './molecules/Stepper.jsx'
62
+ export { default as Kbd } from './atoms/Kbd.jsx'
62
63
  export { default as Tag } from './atoms/Tag.jsx'
63
64
  export { default as Textarea } from './atoms/Textarea.jsx'
64
65
  export { default as ToggleBracket } from './atoms/ToggleBracket.jsx'
@@ -73,8 +74,8 @@ export { default as InspectorRail } from './molecules/InspectorRail.jsx'
73
74
  /* SelectionOverlay's two siblings — same 1080-virtual contract, same zoom
74
75
  * division (editor-panels-the-held-specs B2). `pathMath` is their geometry,
75
76
  * 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'
77
+ export { default as PathNodeOverlay } from './utilities/PathNodeOverlay.jsx'
78
+ export { default as CropOverlay } from './utilities/CropOverlay.jsx'
78
79
  export { default as LayerStack, AddLayerButton, BLEND_MODES } from './organisms/LayerStack.jsx'
79
80
  export { TYPE_LABELS, BOOL_OP_LABELS, SHAPE_KIND_LABELS, labelForLayer, rowLabelForLayer, findLayerDeep } from './hooks/layerTree.js'
80
81
  export { default as TimelineDock, sampleTrack, TIMELINE_EASINGS } from './organisms/TimelineDock.jsx'
@@ -88,7 +89,7 @@ export { default as ButtonGroup } from './utilities/ButtonGroup.jsx'
88
89
  /* monorepo sets (P6–P10) — molecule members */
89
90
  export { default as AlignmentGrid } from './molecules/AlignmentGrid.jsx'
90
91
  export { default as ImageBlock } from './molecules/ImageBlock.jsx'
91
- export { default as SelectionOverlay } from './atoms/SelectionOverlay.jsx'
92
+ export { default as SelectionOverlay } from './utilities/SelectionOverlay.jsx'
92
93
  export { default as VideoBlock, getEmbedUrl } from './molecules/VideoBlock.jsx'
93
94
  export { default as SectionCardItem, default as CardFeatureItem } from './molecules/SectionCardItem.jsx'
94
95
  export { default as CodeBlock } from './molecules/CodeBlock.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' : ''}`}
@@ -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
 
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'
2
2
  import { toneClass } from '../utilities/tone.js'
3
3
  import { Icon } from '@kolkrabbi/kol-icons'
4
4
  import { glyphSize } from '../hooks/glyphLadders.js'
5
+ import Kbd from '../atoms/Kbd.jsx'
5
6
 
6
7
  /**
7
8
  * SearchInput — controlled search field on the .kol-control shell. The
@@ -200,8 +201,10 @@ export default function SearchInput({
200
201
  const shellCls = [
201
202
  bare
202
203
  /* kol-control--bare: zero-chrome marker so the theme's coarse-pointer
203
- * 16px floor covers this body plan too (OverlaySearchFieldZoomsIOS) */
204
- ? 'kol-control--bare flex w-full gap-2.5 px-4 py-3'
204
+ * 16px floor covers this body plan too (OverlaySearchFieldZoomsIOS).
205
+ * Chrome off, geometry on: the size's padding + a transparent 1px ring
206
+ * land it on the 22 · 26 · 32 · 40 ladder like the shell (09-sizes) */
207
+ ? `kol-control--bare kol-control-${size} flex w-full border border-transparent gap-2`
205
208
  : `kol-control${variant ? ` kol-control--${variant}` : ''} kol-control-${size} gap-2${toneClass(tone) ? ` ${toneClass(tone)}` : ''}`,
206
209
  'items-center cursor-text',
207
210
  SIZE_TYPE[size],
@@ -225,7 +228,7 @@ export default function SearchInput({
225
228
  <label className={shellCls}>
226
229
  <span
227
230
  aria-hidden="true"
228
- className={`flex items-center shrink-0 ${bare ? 'text-fg-48' : 'text-auto opacity-50'}`}
231
+ className={`flex items-center shrink-0 ${bare ? 'text-oq-48' : 'text-auto opacity-50'}`}
229
232
  >
230
233
  {/* ADJACENT — this glyph sits in the field's line box beside the query */}
231
234
  <Icon name="search" size={iconSize ?? glyphSize(size)} />
@@ -254,12 +257,10 @@ export default function SearchInput({
254
257
  </button>
255
258
  ) : shortcutHint ? (
256
259
  /* aria-hidden — affordance, not a label (same stance as Input's prefix/suffix) */
257
- <kbd
258
- aria-hidden="true"
259
- className="inline-flex items-center justify-center shrink-0 h-4 min-w-4 px-1 rounded-[var(--kol-radius-xs)] bg-fg-08 kol-helper-10 text-fg-48"
260
- >
261
- {shortcutHint}
262
- </kbd>
260
+ /* ⌘ is drawn, not typed — the mono face's ⌘ is the "drawn by a child" cap */
261
+ shortcutHint.startsWith('⌘')
262
+ ? <Kbd size="sm" icon="command">{shortcutHint.slice(1)}</Kbd>
263
+ : <Kbd size="sm">{shortcutHint}</Kbd>
263
264
  ) : null}
264
265
  </label>
265
266
  )
@@ -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
@@ -395,7 +395,7 @@ const ContentFilters = ({
395
395
  * 05-control-chrome.md:109 — "any icon-only control in chrome is
396
396
  * IconFrame; nothing hand-writes the square". This wore
397
397
  * `kol-btn-md kol-btn-icon` and then removed the background, the
398
- * 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
399
399
  * paid for and thrown away — and it left the control with no
400
400
  * states at all while the search beside it had hover.
401
401
  *
@@ -22,7 +22,7 @@ import { filterMedia, rankMedia } from '../utilities/mediaSearch.js'
22
22
  import { useMasthead, mastheadTitleClass } from '../utilities/masthead.js'
23
23
  import { MenuItem, MenuDropdownItem, MenuDropdownDivider } from '../molecules/MenuItem.jsx'
24
24
  import { Tooltip } from '../utilities/Popover.jsx'
25
- import ContextMenu, { useContextMenu } from '../utilities/ContextMenu.jsx'
25
+ import ContextMenu, { useContextMenu } from '../molecules/ContextMenu.jsx'
26
26
  import useMarquee from '../hooks/useMarquee.js'
27
27
  import useLongPress from '../hooks/useLongPress.js'
28
28
  import useMediaQuery from '../hooks/useMediaQuery.js'
@@ -36,7 +36,7 @@ import { minHeightClass } from './sectionHeights.js'
36
36
  * @param {boolean} [fullBleed=false] the FILL breaks the page gutter while the content keeps it —
37
37
  * the family's shared breakout (`sectionBleed.js`, SectionFamilyFullBleed, kol-website 2026-08-31).
38
38
  * Any member of this family can be a filled surface, and a filled surface inside `.kol-page` has its
39
- * colour clipped by the gutter on mobile. Viewport-relative, so unlike `.kol-full-bleed` it does not
39
+ * color clipped by the gutter on mobile. Viewport-relative, so unlike `.kol-full-bleed` it does not
40
40
  * over-bleed in a parent with no gutter of its own. The section's horizontal padding re-insets the
41
41
  * CONTENT, so only the fill moves. Default false — nothing renders differently until it is passed.
42
42
  * @param {string} sectionClassName · wrapperClassName · cardsWrapperClassName · actionsClassName · headerClassName · headerTextWidthClass layout seams
@@ -30,7 +30,7 @@ import { minHeightClass } from './sectionHeights.js'
30
30
  * @param {boolean} [fullBleed=false] the FILL breaks the page gutter while the content keeps it —
31
31
  * the family's shared breakout (`sectionBleed.js`, SectionFamilyFullBleed, kol-website 2026-08-31).
32
32
  * Any member of this family can be a filled surface, and a filled surface inside `.kol-page` has its
33
- * colour clipped by the gutter on mobile. Viewport-relative, so unlike `.kol-full-bleed` it does not
33
+ * color clipped by the gutter on mobile. Viewport-relative, so unlike `.kol-full-bleed` it does not
34
34
  * over-bleed in a parent with no gutter of its own. The section's horizontal padding re-insets the
35
35
  * CONTENT, so only the fill moves. Default false — nothing renders differently until it is passed.
36
36
  * @param {string} className extra classes on the section
@@ -21,7 +21,7 @@ import { minHeightClass } from './sectionHeights.js'
21
21
  * @param {boolean} [fullBleed=false] the FILL breaks the page gutter while the content keeps it —
22
22
  * the family's shared breakout (`sectionBleed.js`, SectionFamilyFullBleed, kol-website 2026-08-31).
23
23
  * Any member of this family can be a filled surface, and a filled surface inside `.kol-page` has its
24
- * colour clipped by the gutter on mobile. Viewport-relative, so unlike `.kol-full-bleed` it does not
24
+ * color clipped by the gutter on mobile. Viewport-relative, so unlike `.kol-full-bleed` it does not
25
25
  * over-bleed in a parent with no gutter of its own. The section's horizontal padding re-insets the
26
26
  * CONTENT, so only the fill moves. Default false — nothing renders differently until it is passed.
27
27
  * @param {string} className · innerClassName layout seams
@@ -60,7 +60,7 @@ import { minHeightClass } from './sectionHeights.js'
60
60
  * @param {boolean} [fullBleed=false] the FILL breaks the page gutter while the content keeps it
61
61
  * (SectionNewsletterFullBleed, kol-website 2026-08-31). This card is a filled surface inside
62
62
  * `.kol-page`, so the gutter clipped its background and left strips of page down both sides of the
63
- * colour. Fill and content padding are the same box, so a consumer could not bleed one without
63
+ * color. Fill and content padding are the same box, so a consumer could not bleed one without
64
64
  * dragging the other out with it. The breakout literal is SectionHero's, character for character —
65
65
  * two organisms in one family must not invent two ways to leave a gutter. The section's own
66
66
  * `px-5 sm:px-8` then re-insets the content, so only the fill moves.
@@ -1,6 +1,26 @@
1
1
  import { useEffect, useId, useRef, useState } from 'react'
2
+ import { Icon } from '@kolkrabbi/kol-icons'
2
3
  import SearchInput from '../molecules/SearchInput.jsx'
3
4
  import Tag from '../atoms/Tag.jsx'
5
+ import Kbd from '../atoms/Kbd.jsx'
6
+
7
+ /* ONE COLUMN (2026-09-30): rows, headings and footer sit on the field's own
8
+ * geometry — the p-2 inset, then the md control's 1px ring + 16px pad — so
9
+ * every glyph and word lines up under the field's icon and caret. */
10
+ import { glyphSize } from '../hooks/glyphLadders.js'
11
+
12
+ /* Rows bucketed by `group` in first-seen order, rank kept inside a group —
13
+ * one heading per group; arrows rove this order, not the engine's. */
14
+ function groupRows(rows) {
15
+ const order = []
16
+ const byGroup = new Map()
17
+ rows.forEach((r) => {
18
+ const g = r.group ?? ''
19
+ if (!byGroup.has(g)) { byGroup.set(g, []); order.push(g) }
20
+ byGroup.get(g).push(r)
21
+ })
22
+ return order.flatMap((g) => byGroup.get(g))
23
+ }
4
24
 
5
25
  /**
6
26
  * HighlightMatch — default row renderer: underlines the first
@@ -56,7 +76,9 @@ export function HighlightMatch({ label, query, ranges }) {
56
76
  *
57
77
  * @param {boolean} open mount/unmount the overlay
58
78
  * @param {Function} onClose () => void — backdrop click, Escape, post-select
59
- * @param {Array} results pre-filtered rows: { id, label, group?, hint? }
79
+ * @param {Array} results pre-filtered rows: { id, label, group?, hint?, icon? }
80
+ * @param {Array} [suggestions] rows shown while the query is empty (same shape) —
81
+ * the palette opens on somewhere to go, not a blank box
60
82
  * @param {string} query controlled query (drives the highlight slice)
61
83
  * @param {Function} onQueryChange (string) => void — input change
62
84
  * @param {Function} onSelect (item) => void — row click / Enter; consumer navigates
@@ -69,7 +91,8 @@ export function HighlightMatch({ label, query, ranges }) {
69
91
  export default function ShellSearchOverlay({
70
92
  open,
71
93
  onClose,
72
- results = [],
94
+ results: rawResults = [],
95
+ suggestions = [],
73
96
  /* EXPANDED — the palette's second state (user ruling 2026-08-01). Enter
74
97
  * commits the query and opens `children` as the results body; the palette
75
98
  * and the old tag overlay are one surface with two states, not two
@@ -92,6 +115,7 @@ export default function ShellSearchOverlay({
92
115
  /* Has the user actually chosen a row? See the Enter branch — without this,
93
116
  * index 0 counts as a selection and Enter navigates somewhere unasked. */
94
117
  const [navigated, setNavigated] = useState(false)
118
+ const results = groupRows(query ? rawResults : suggestions)
95
119
  const active = results.length > 0 ? Math.min(activeIndex, results.length - 1) : -1
96
120
 
97
121
  /* Focus in on open, restore the opener on close. querySelector instead of
@@ -110,7 +134,7 @@ export default function ShellSearchOverlay({
110
134
  /* Keep the active row visible inside the scrolling list. */
111
135
  useEffect(() => {
112
136
  if (active < 0) return
113
- listRef.current?.children[active]?.scrollIntoView({ block: 'nearest' })
137
+ listRef.current?.querySelectorAll('[role="option"]')[active]?.scrollIntoView({ block: 'nearest' })
114
138
  }, [active])
115
139
 
116
140
  if (!open) return null
@@ -141,7 +165,8 @@ export default function ShellSearchOverlay({
141
165
  * highlighted" is true from the first keystroke and testing `active >= 0`
142
166
  * made Enter navigate to whatever happened to be first. Committing a
143
167
  * query must never be a navigation you didn't choose. */
144
- if (navigated) select(results[active])
168
+ /* An empty query has nothing to commit — Enter goes to the top suggestion. */
169
+ if (active >= 0 && (navigated || !query)) select(results[active])
145
170
  else onExpand?.()
146
171
  } else if (e.key === 'Tab') {
147
172
  /* Focus trap — the input is the palette's only tab stop. */
@@ -182,8 +207,11 @@ export default function ShellSearchOverlay({
182
207
  ))}
183
208
  </div>
184
209
  )}
210
+ {/* A REAL FIELD, inset in the panel (2026-09-30, shadcn's palette as
211
+ * the aim) — the flush `bare` strip read as a hole, not a control. */}
212
+ <div className="kol-tone-grey p-2">
185
213
  <SearchInput
186
- bare
214
+ className="w-full"
187
215
  value={query}
188
216
  onChange={(e) => onQueryChange?.(e.target.value)}
189
217
  placeholder={chips.length > 0 ? 'Narrow these results…' : placeholder}
@@ -193,6 +221,7 @@ export default function ShellSearchOverlay({
193
221
  aria-controls={listId}
194
222
  aria-activedescendant={active >= 0 ? optionId(results[active]) : undefined}
195
223
  />
224
+ </div>
196
225
 
197
226
  {/* WHY THIS IS NOT `molecules/Dropdown` (asked 2026-08-01). Dropdown is
198
227
  * a SELECT: a trigger, a `value`, `onChange(value)`, and rows that are
@@ -204,7 +233,8 @@ export default function ShellSearchOverlay({
204
233
  *
205
234
  * THE ROW CONTRACT (was documented nowhere):
206
235
  * label the row's text, match-highlighted against the query
207
- * group right-aligned origin — 'Atoms', 'Documentation', 'Tags'
236
+ * group section heading the row files under — 'Atoms', 'Documentation', 'Tags'
237
+ * icon optional leading glyph (kol-icons name)
208
238
  * hint subtext shown when the LABEL was not what matched
209
239
  * href a destination; dismisses the palette
210
240
  * action a closure; runs and KEEPS the palette open (tag rows)
@@ -212,46 +242,60 @@ export default function ShellSearchOverlay({
212
242
  {expanded ? (
213
243
  <div className="border-t border-fg-08 max-h-[70vh] overflow-y-auto">{children}</div>
214
244
  ) : results.length > 0 && (
245
+ /* FIXED BODY HEIGHT — the panel holds still while the list narrows. */
215
246
  <ul
216
247
  ref={listRef}
217
248
  id={listId}
218
249
  role="listbox"
219
- className="border-t border-fg-08 max-h-80 overflow-y-auto py-1"
250
+ className="kol-tone-grey h-80 overflow-y-auto px-2 pb-2"
220
251
  >
221
- {results.map((item, i) => (
222
- <li
223
- key={item.id}
224
- id={optionId(item)}
225
- role="option"
226
- aria-selected={i === active}
227
- /* preventDefault keeps focus in the input through the click */
228
- onMouseDown={(e) => e.preventDefault()}
229
- onClick={() => select(item)}
230
- onMouseEnter={() => { setActiveIndex(i); setNavigated(true) }}
231
- className={`flex items-center gap-2 px-4 py-1.5 cursor-pointer kol-mono-14 transition-colors ${
232
- i === active ? 'bg-fg-08 text-fg' : 'text-fg-80'
233
- }`}
234
- >
235
- <span className="flex flex-col min-w-0">
236
- <span className="truncate">
237
- <HighlightMatch label={item.label} query={query} ranges={item.highlights} />
238
- </span>
239
- {item.hint && (
240
- <span className="kol-mono-12 text-fg-48 truncate">{item.hint}</span>
252
+ {results.map((item, i) => {
253
+ const heading = (item.group ?? '') !== (results[i - 1]?.group ?? '') || i === 0
254
+ return (
255
+ <li key={item.id} role="presentation">
256
+ {heading && item.group && (
257
+ <p className="kol-helper-12 text-fg-48 px-4 border-x border-transparent pt-3 pb-2">{item.group}</p>
241
258
  )}
242
- </span>
243
- {item.group && (
244
- <span className="ml-auto shrink-0 kol-helper-10 text-fg-48">{item.group}</span>
245
- )}
246
- </li>
247
- ))}
259
+ <div
260
+ id={optionId(item)}
261
+ role="option"
262
+ aria-selected={i === active}
263
+ /* preventDefault keeps focus in the input through the click */
264
+ onMouseDown={(e) => e.preventDefault()}
265
+ onClick={() => select(item)}
266
+ onMouseEnter={() => { setActiveIndex(i); setNavigated(true) }}
267
+ /* the md control's box: 1px ring + 6/16 pad + gap-2 → 32px, same as the field */
268
+ className={`flex items-center gap-2 px-4 py-1.5 border border-transparent min-h-[var(--kol-ctl-md)] rounded-[var(--kol-radius-sm)] cursor-pointer kol-mono-14 transition-colors ${
269
+ i === active ? 'bg-[var(--kol-tone-bg,var(--kol-surface-secondary))] text-fg' : 'text-fg-80'
270
+ }`}
271
+ >
272
+ {item.icon && (
273
+ <span aria-hidden="true" className="flex shrink-0 text-oq-48">
274
+ <Icon name={item.icon} size={glyphSize('md')} />
275
+ </span>
276
+ )}
277
+ <span className="flex flex-col min-w-0">
278
+ <span className="truncate">
279
+ <HighlightMatch label={item.label} query={query} ranges={item.highlights} />
280
+ </span>
281
+ {item.hint && (
282
+ <span className="kol-mono-12 text-fg-48 truncate">{item.hint}</span>
283
+ )}
284
+ </span>
285
+ </div>
286
+ </li>
287
+ )
288
+ })}
248
289
  </ul>
249
290
  )}
250
- {!expanded && enterLabel && query && (
251
- <p className="flex items-center gap-2 border-t border-fg-08 px-4 py-2 kol-helper-12 text-fg-64">
252
- <kbd className="kol-helper-12 rounded-[var(--kol-radius-sm)] bg-fg-08 px-1.5 py-0.5 text-fg">↵</kbd>
253
- {enterLabel}
254
- </p>
291
+ {/* THE FOOTER SAYS WHAT ENTER DOES — always, not only once typing. */}
292
+ {!expanded && ((!query && results.length > 0) || (query && enterLabel)) && (
293
+ <div className="border-t border-fg-08 py-2">
294
+ <p className="flex items-center gap-2 kol-helper-12 text-fg-48 mx-2 px-4 border-x border-transparent">
295
+ <Kbd icon="corner-down-left" />
296
+ {query ? enterLabel : 'Go to page'}
297
+ </p>
298
+ </div>
255
299
  )}
256
300
  </div>
257
301
  </div>
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * WHY IT IS A FILLED-SECTION PROBLEM, not a newsletter one. Any member of the
9
9
  * family can be a filled surface, and a filled surface inside `.kol-page` has its
10
- * colour clipped by the page gutter on mobile — strips of page down both sides of
10
+ * color clipped by the page gutter on mobile — strips of page down both sides of
11
11
  * the fill. Reported once per organism until the prop is shared.
12
12
  *
13
13
  * NOT `.kol-full-bleed`: that escape is CONTAINER-relative, so on an organism
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useRef } from 'react'
2
2
  import { pushLayer, popLayer, isTopLayer } from './layerStack.js'
3
- import CloseButton from './CloseButton.jsx'
3
+ import CloseButton from '../atoms/CloseButton.jsx'
4
4
 
5
5
  /**
6
6
  * FullscreenOverlay — the scrim + centred sheet every overlay in the repo
@@ -8,7 +8,7 @@ import FullscreenOverlay from './FullscreenOverlay.jsx'
8
8
  * `loader` slot — as the overlay content. Being mounted IS being visible;
9
9
  * the parent removes this element to dismiss.
10
10
  *
11
- * The loader is a SLOT, not a built-in: inject a curtain (e.g. `<ColorLoader/>`
11
+ * The loader is a SLOT, not a built-in: inject a curtain (e.g. `<IntroLoader/>`
12
12
  * from `@kolkrabbi/kol-foundry`) via `loader` and wire its completion callback
13
13
  * yourself. The slot is mounted in a `fixed inset-0` box that escapes
14
14
  * FullscreenOverlay's centered, `--kol-container-max`-width sheet so the loader
@@ -16,7 +16,7 @@ import FullscreenOverlay from './FullscreenOverlay.jsx'
16
16
  * With neither, the loader slot renders nothing (the overlay still works).
17
17
  *
18
18
  * @param {ReactNode} children overlay content, centered (takes precedence)
19
- * @param {ReactNode} loader full-screen loading curtain, e.g. foundry's ColorLoader
19
+ * @param {ReactNode} loader full-screen loading curtain, e.g. foundry's IntroLoader
20
20
  */
21
21
  export default function LoaderOverlay({ children, loader }) {
22
22
  return (
@@ -7,6 +7,7 @@ import {
7
7
  flip as flipMw,
8
8
  shift as shiftMw,
9
9
  size as sizeMw,
10
+ hide as hideMw,
10
11
  FloatingPortal,
11
12
  FloatingFocusManager,
12
13
  useClick,
@@ -101,6 +102,7 @@ export function usePopover({
101
102
  * external DOM node (e.g. a parent container ref) instead of wiring
102
103
  * `setReference` onto the trigger. Used by TypeBlockToolbar to anchor
103
104
  * to its TypeFrame parent. */
105
+ middleware.push(hideMw({ strategy: 'referenceHidden' })) // last — it reads the final position
104
106
  const data = useFloating({
105
107
  open,
106
108
  onOpenChange,
@@ -110,6 +112,13 @@ export function usePopover({
110
112
  elements: referenceElement ? { reference: referenceElement } : undefined,
111
113
  })
112
114
 
115
+ /* A PANEL FOLLOWS ITS TRIGGER OUT OF SIGHT (2026-09-30 — docs/menus: an open menu painted over
116
+ * the sticky header when its trigger scrolled under it, and floated at the page's foot when it
117
+ * scrolled away). The panel is portalled to <body>, so no scroll container can clip it; `hide`
118
+ * reports when the trigger is clipped, and the panel hides with it. */
119
+ const hidden = data.middlewareData.hide?.referenceHidden
120
+ const floatingStyles = hidden ? { ...data.floatingStyles, visibility: 'hidden' } : data.floatingStyles
121
+
113
122
  const interactions = useInteractions([
114
123
  useClick(data.context, { enabled: click }),
115
124
  useHover(data.context, { enabled: hover, delay: hoverDelay, move: false }),
@@ -118,7 +127,7 @@ export function usePopover({
118
127
  useRole(data.context, { role }),
119
128
  ])
120
129
 
121
- return { ...data, ...interactions, open }
130
+ return { ...data, floatingStyles, ...interactions, open }
122
131
  }
123
132
 
124
133
  /**
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * primary · secondary · inverted · outline · ghost · grey · sunken
6
6
  *
7
- * `secondary` paints the PAGE SURFACE and `inverted` the text colour as fill
7
+ * `secondary` paints the PAGE SURFACE and `inverted` the text color as fill
8
8
  * (tone-secondary-is-inverse, 2026-09-03; user: "that tone should be called
9
9
  * secondary. What is currently secondary should be called inverted") — 0.134.0
10
10
  * had lifted `secondary` from Button's variant, which was already an inverse.
File without changes
File without changes
File without changes
File without changes
File without changes