@kolkrabbi/kol-component 0.224.0 → 0.226.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-component",
3
- "version": "0.224.0",
3
+ "version": "0.226.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",
@@ -35,7 +35,7 @@
35
35
  "react-dom": "^18.3.0 || ^19.0.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@kolkrabbi/kol-icons": "^0.27.1"
38
+ "@kolkrabbi/kol-icons": "^0.28.0"
39
39
  },
40
40
  "files": [
41
41
  "src",
@@ -40,6 +40,7 @@
40
40
  * title — passes through.
41
41
  */
42
42
  import TransparentX from '../utilities/TransparentX'
43
+ import { Tooltip } from '../utilities/Popover.jsx'
43
44
 
44
45
  const SIZE_CLASSES = {
45
46
  fill: 'w-full aspect-square',
@@ -122,11 +123,11 @@ export default function ColorSwatch({
122
123
 
123
124
  if (interactive) {
124
125
  return (
126
+ <Tooltip label={title} asChild>
125
127
  <button
126
128
  {...rest}
127
129
  type="button"
128
130
  onClick={onClick}
129
- title={title}
130
131
  aria-label={rest['aria-label'] ?? hex ?? 'transparent'}
131
132
  aria-pressed={selected}
132
133
  className={cls}
@@ -134,12 +135,15 @@ export default function ColorSwatch({
134
135
  >
135
136
  {inner}
136
137
  </button>
138
+ </Tooltip>
137
139
  )
138
140
  }
139
141
 
140
142
  return (
141
- <span {...rest} aria-hidden="true" title={title} className={cls} style={style}>
143
+ <Tooltip label={title} asChild>
144
+ <span {...rest} aria-hidden="true" className={cls} style={style}>
142
145
  {inner}
143
146
  </span>
147
+ </Tooltip>
144
148
  )
145
149
  }
@@ -1,5 +1,6 @@
1
1
  import { useContext } from 'react'
2
2
  import { CanvasZoomContext } from '../hooks/canvasZoom.js'
3
+ import { Tooltip } from '../utilities/Popover.jsx'
3
4
 
4
5
  /* taxonomy-ok: presentational transform-chrome overlay. It nests no KOL
5
6
  * component (pure inline-styled squares + label), so by the letter of the
@@ -110,9 +111,9 @@ export default function SelectionOverlay({
110
111
  * the box about its centre. Independent of the resize handles: a path
111
112
  * hides those (node-edit owns their geometry) and still rotates. */}
112
113
  {rotate && (
114
+ <Tooltip label={rotateTitle} asChild>
113
115
  <div
114
116
  data-handle="ROT"
115
- title={rotateTitle}
116
117
  style={{
117
118
  position: 'absolute',
118
119
  left: `calc(50% - ${size / 2}px)`,
@@ -126,6 +127,7 @@ export default function SelectionOverlay({
126
127
  pointerEvents: 'auto',
127
128
  }}
128
129
  />
130
+ </Tooltip>
129
131
  )}
130
132
  {showHandles && HANDLE_DIRS.map(({ dir, cursor, x: hx, y: hy }) => (
131
133
  <div
@@ -76,14 +76,14 @@ export default function XYPad({
76
76
  ref={ref}
77
77
  onPointerDown={onPointerDown}
78
78
  onPointerMove={onPointerMove}
79
- className="relative w-full aspect-square border border-fg-16 bg-fg-04 rounded cursor-crosshair touch-none"
79
+ className="relative w-full aspect-square border border-oq-16 bg-fg-04 rounded cursor-crosshair touch-none"
80
80
  >
81
81
  {/* crosshair guides */}
82
- <div className="absolute inset-x-0 top-1/2 border-t border-fg-08" />
83
- <div className="absolute inset-y-0 left-1/2 border-l border-fg-08" />
82
+ <div className="absolute inset-x-0 top-1/2 border-t border-oq-08" />
83
+ <div className="absolute inset-y-0 left-1/2 border-l border-oq-08" />
84
84
  {/* puck */}
85
85
  <div
86
- className="absolute w-3 h-3 -ml-1.5 -mt-1.5 rounded-full bg-fg-96 border border-fg-04 pointer-events-none"
86
+ className="absolute w-3 h-3 -ml-1.5 -mt-1.5 rounded-full bg-fg-96 border border-oq-04 pointer-events-none"
87
87
  style={{ left: `${puckX}%`, top: `${puckY}%` }}
88
88
  />
89
89
  </div>
package/src/index.js CHANGED
@@ -166,7 +166,7 @@ export { default as MediaLibrary, MediaLibraryProvider, useMediaLibrary, MediaPi
166
166
  /* the full-screen file viewer — public 2026-09-04 (user ruling): it already
167
167
  * rendered both mobile viewer views and was module-private, so every consumer
168
168
  * needing a phone viewer hand-rolled one. */
169
- export { MediaInspector } from './organisms/MediaLibraryPages.jsx'
169
+ export { MediaInspector, mediaSettingsSections, SETTINGS_BASE as MEDIA_SETTINGS_BASE } from './organisms/MediaLibraryPages.jsx'
170
170
  export { default as MediaLibraryExplorer } from './organisms/MediaLibraryExplorer.jsx'
171
171
  export { default as MediaTileGallery } from './organisms/MediaTileGallery.jsx'
172
172
  export { default as MediaViewer } from './organisms/MediaViewer.jsx'
@@ -2,7 +2,7 @@ import { useState } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
3
  import ColorSwatch from '../atoms/ColorSwatch'
4
4
  import Input from '../atoms/Input'
5
- import { usePopover, PopoverPanel } from '../utilities/Popover'
5
+ import { usePopover, PopoverPanel, Tooltip } from '../utilities/Popover'
6
6
 
7
7
  /**
8
8
  * ColorInputRow — swatch chip + `#` hex input row. The single merged form of
@@ -150,11 +150,11 @@ export default function ColorInputRow({
150
150
  {chip(24)}
151
151
  </button>
152
152
  ) : isLockToggle ? (
153
+ <Tooltip label={locked ? 'Unlock' : 'Lock'} asChild>
153
154
  <button
154
155
  type="button"
155
156
  onClick={onToggleLock}
156
157
  aria-pressed={locked}
157
- title={locked ? 'Unlock' : 'Lock'}
158
158
  className="group relative inline-flex h-6 w-6 shrink-0"
159
159
  >
160
160
  {chip('stretch')}
@@ -167,6 +167,7 @@ export default function ColorInputRow({
167
167
  <Icon name={locked ? 'lock' : 'unlock'} size={12} />
168
168
  </span>
169
169
  </button>
170
+ </Tooltip>
170
171
  ) : (
171
172
  chip(24)
172
173
  )
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useLayoutEffect, useRef, useState } from 'react'
2
2
  import { toneClass, TONE_VARS } from '../utilities/tone.js'
3
3
  import { Icon } from '@kolkrabbi/kol-icons'
4
- import { MenuDropdownItem } from './MenuItem.jsx'
4
+ import { MenuDropdownItem, MenuDropdownDivider } from './MenuItem.jsx'
5
5
  import { PopoverPanel, usePopover } from '../utilities/Popover.jsx'
6
6
  import { glyphSize, indicatorSize } from '../hooks/glyphLadders.js'
7
7
 
@@ -160,7 +160,11 @@ const Dropdown = ({
160
160
  onOptionHover?.(null)
161
161
  }, [isOpen]) // eslint-disable-line react-hooks/exhaustive-deps
162
162
 
163
- const currentOption = options.find((opt) => opt.value === value) || options[0]
163
+ /* `{ divider: true }` in `options` draws a hairline between groups (FoundationsTones, 2026-09-26 —
164
+ * user: none on top, the filled tones in order, outline and ghost apart). It is never a choice:
165
+ * the lookup, the fallback and the trigger's width all read the choices only. */
166
+ const choices = options.filter((opt) => !opt?.divider)
167
+ const currentOption = choices.find((opt) => opt.value === value) || choices[0]
164
168
 
165
169
  /* A clamped list (Popover caps the panel to the viewport) can open with the
166
170
  * checked row past the fold — scroll it into reach. Keyboard focus after
@@ -168,7 +172,7 @@ const Dropdown = ({
168
172
  const listRef = useRef(null)
169
173
  useEffect(() => {
170
174
  if (!isOpen) return
171
- const idx = options.findIndex((opt) => opt.value === currentOption?.value)
175
+ const idx = options.findIndex((opt) => !opt?.divider && opt.value === currentOption?.value)
172
176
  listRef.current?.children[idx]?.scrollIntoView({ block: 'nearest' })
173
177
  }, [isOpen]) // eslint-disable-line react-hooks/exhaustive-deps
174
178
 
@@ -215,7 +219,7 @@ const Dropdown = ({
215
219
  * trigger and list stay one piece at every selection */}
216
220
  <span className="kol-dd-label">
217
221
  <span>{currentOption?.label}</span>
218
- {options.map((option) => (
222
+ {choices.map((option) => (
219
223
  <span key={option.value} className="kol-dd-ghost" aria-hidden="true">
220
224
  {option.label}
221
225
  </span>
@@ -247,7 +251,8 @@ const Dropdown = ({
247
251
  {(resolvedVariant === 'primary' || resolvedVariant === 'grey') && <div className="kol-dd-div" />}
248
252
 
249
253
  <div ref={listRef} className="kol-dd-list" role="listbox">
250
- {options.map((option) => {
254
+ {options.map((option, i) => {
255
+ if (option?.divider) return <MenuDropdownDivider key={`divider-${i}`} />
251
256
  const isActive = option.value === currentOption?.value
252
257
  return (
253
258
  <MenuDropdownItem
@@ -148,7 +148,7 @@ export default function FieldRow({
148
148
  onClick={onPick}
149
149
  /* reference empty tile: FILLED, no border — a plain grey slab with the
150
150
  * + centred (frame 2026-08-09) */
151
- className="flex h-18 w-34 items-center justify-center rounded border-0 bg-fg-04 text-body transition-colors duration-150 enabled:cursor-pointer enabled:hover:text-emphasis disabled:opacity-60"
151
+ className="flex h-18 w-34 items-center justify-center rounded border-0 bg-fg-04 text-oq-64 transition-colors duration-150 enabled:cursor-pointer enabled:hover:text-emphasis disabled:opacity-60"
152
152
  >
153
153
  <Icon name="plus" size={16} />
154
154
  </button>
@@ -217,7 +217,7 @@ export default function FieldRow({
217
217
  type="button"
218
218
  aria-label={label}
219
219
  onClick={() => onChange(null)}
220
- className="inline-flex shrink-0 cursor-pointer border-0 bg-transparent p-0 text-body hover:text-emphasis"
220
+ className="inline-flex shrink-0 cursor-pointer border-0 bg-transparent p-0 text-oq-64 hover:text-emphasis"
221
221
  >
222
222
  <Icon name="x" size={12} />
223
223
  </button>
@@ -17,12 +17,23 @@
17
17
  * ponytail: a `SectionStack` parent could own this instead of each child
18
18
  * declaring it — that is the upgrade path if a consumer ever needs the stack
19
19
  * to vary the rule per-gap. One prop is a smaller API than a new component.
20
+ *
21
+ * @param {ReactNode} label the group's name
22
+ * @param {ReactNode} actions controls on the label's row, right-aligned
23
+ * @param {boolean} divided a hairline between divided siblings
24
+ * @param {string} className
20
25
  */
21
- export default function InspectorSection({ label, children, divided = false, className = '' }) {
26
+ export default function InspectorSection({ label, actions = null, children, divided = false, className = '' }) {
22
27
  return (
23
28
  <div className={`flex flex-col gap-2${divided ? ' kol-section--divided' : ''} ${className}`}>
24
- {label && (
25
- <p className="kol-helper-10 tracking-widest text-meta">{label}</p>
29
+ {/* `actions` — controls on the label's row, right-aligned (Figma's eye / plus on Fill and
30
+ * Stroke). From the design editor's own Section, retired into this (editor DS sync,
31
+ * 2026-09-27). */}
32
+ {(label || actions) && (
33
+ <div className="flex items-center gap-2 min-h-6">
34
+ {label && <p className="kol-helper-10 tracking-widest text-meta flex-1">{label}</p>}
35
+ {actions}
36
+ </div>
26
37
  )}
27
38
  {children}
28
39
  </div>
@@ -51,7 +51,7 @@ export function MenuItem({
51
51
  ref={popover.refs.setReference}
52
52
  {...popover.getReferenceProps()}
53
53
  type="button"
54
- className={`kol-helper-12 px-3 h-8 inline-flex items-center gap-2 rounded text-body hover:text-emphasis transition-colors ${buttonClassName}`}
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}`}
55
55
  >
56
56
  <span>{label}</span>
57
57
  {caret && (
@@ -125,7 +125,7 @@ export function MenuDropdownItem({ onClick, onPointerEnter, onPointerLeave, disa
125
125
  disabled={disabled}
126
126
  role="menuitem"
127
127
  style={height != null ? { height } : undefined}
128
- className={`w-full ${ROW_BY_SIZE[size] ?? ROW_BY_SIZE.sm} shrink-0 inline-flex items-center gap-2 text-body ${hover ? 'hover:text-emphasis' : ''} disabled:opacity-40 disabled:cursor-not-allowed text-left`}
128
+ className={`kol-menu-btn w-full ${ROW_BY_SIZE[size] ?? ROW_BY_SIZE.sm} shrink-0 inline-flex items-center gap-2 text-body ${hover ? 'hover:text-emphasis' : ''} disabled:opacity-40 disabled:cursor-not-allowed text-left`}
129
129
  >
130
130
  {prefix && <span className="shrink-0 inline-flex items-center">{prefix}</span>}
131
131
  {iconLeft && <span className="shrink-0 w-4 inline-flex items-center justify-center">{iconLeft}</span>}
@@ -140,7 +140,7 @@ export function MenuDropdownItem({ onClick, onPointerEnter, onPointerLeave, disa
140
140
  }
141
141
 
142
142
  export function MenuDropdownDivider() {
143
- return <div className="border-t border-fg-08 my-1" />
143
+ return <div className="border-t border-oq-08 my-1" />
144
144
  }
145
145
 
146
146
  /**
@@ -161,7 +161,7 @@ export function MenuDropdownNest({ prefix, iconLeft, label, children }) {
161
161
  type="button"
162
162
  onClick={() => setOpen((v) => !v)}
163
163
  aria-expanded={open}
164
- className="w-full kol-helper-12 px-3 h-8 inline-flex items-center gap-2 text-body hover:text-emphasis text-left"
164
+ className="kol-menu-btn w-full kol-helper-12 px-3 h-8 inline-flex items-center gap-2 text-body hover:text-emphasis text-left"
165
165
  >
166
166
  {prefix && <span className="shrink-0 inline-flex items-center">{prefix}</span>}
167
167
  {iconLeft && <span className="shrink-0 w-4 inline-flex items-center justify-center">{iconLeft}</span>}
@@ -175,7 +175,7 @@ export function MenuDropdownNest({ prefix, iconLeft, label, children }) {
175
175
  />
176
176
  </button>
177
177
  {open && (
178
- <div className="ml-3 border-l border-fg-08">
178
+ <div className="ml-3 border-l border-oq-08">
179
179
  {children}
180
180
  </div>
181
181
  )}
@@ -245,7 +245,7 @@ export default function SearchInput({
245
245
  /* preventDefault on mousedown keeps focus in the input across the clear */
246
246
  onMouseDown={(e) => e.preventDefault()}
247
247
  onClick={onClear}
248
- className="inline-flex items-center justify-center shrink-0 cursor-pointer text-fg-48 hover:text-fg-96 transition-colors"
248
+ className="inline-flex items-center justify-center shrink-0 cursor-pointer text-oq-48 hover:text-oq-96 transition-colors"
249
249
  >
250
250
  <Icon name="x" size={12} />
251
251
  </button>
@@ -101,6 +101,8 @@ export default function SectionText({
101
101
  const cls = (base, slot) => `${base} ${slotClass[slot] ?? (slot === 'eyebrow' ? slotClass.label : undefined) ?? ''}`.trim()
102
102
  const alignCls = align === 'center' ? 'items-center text-center' : 'items-start text-left'
103
103
  const inlineActions = actionsPlacement === 'inline' && !!actions && !!body
104
+ const inlineHead = actionsPlacement === 'inline' && !!actions && !body && !!headline
105
+ const headlineNode = headline && <Headline className={cls(`kol-section-text-headline ${headlineClass ?? (HEADLINE_ROLE[headlineSize] ?? HEADLINE_ROLE['heading-02'])}${headlineCase === 'upper' ? ' kol-section-text-caps' : ''}`, 'headline')} style={slotStyle.headline}>{headline}</Headline>
104
106
  const bodyNode = body && (typeof body === 'string'
105
107
  ? <p className={cls(bodyClass, 'body')} style={slotStyle.body}>{body}</p>
106
108
  : <div className={cls(bodyClass, 'body')} style={slotStyle.body}>{body}</div>)
@@ -112,18 +114,33 @@ export default function SectionText({
112
114
  {/* `kol-section-text-eyebrow` = uppercase by ROLE (kol-theme ≥0.55.0);
113
115
  * `labelClass` is the voice riding beside it */}
114
116
  {eb && <span className={cls(`kol-section-text-eyebrow ${ebClass}`, 'eyebrow')} style={slotStyle.eyebrow ?? slotStyle.label}>{eb}</span>}
115
- {headline && <Headline className={cls(`kol-section-text-headline ${headlineClass ?? (HEADLINE_ROLE[headlineSize] ?? HEADLINE_ROLE['heading-02'])}${headlineCase === 'upper' ? ' kol-section-text-caps' : ''}`, 'headline')} style={slotStyle.headline}>{headline}</Headline>}
117
+ {headlineNode && (inlineHead
118
+ ? (
119
+ /* INLINE WITH NO BODY → the cluster shares the HEADLINE's row (the tool frame, 2026-09-27 —
120
+ * a tool's masthead is its title with its controls on the right, apps/media's). This
121
+ * comment used to sit on the fallthrough below, which stacked the cluster under the
122
+ * headline instead: the docstring promised a row the code never drew. It WRAPS: on a
123
+ * phone a wide cluster drops under the title rather than pushing the page sideways. */
124
+ <div className="flex flex-wrap self-stretch items-baseline justify-between gap-x-6 gap-y-3">
125
+ {headlineNode}
126
+ <div className={cls(`${actionsClass} shrink-0`, 'actions')} style={slotStyle.actions}>{actions}</div>
127
+ </div>
128
+ )
129
+ : headlineNode)}
116
130
  {bodyNode && (inlineActions
117
131
  ? (
118
- <div className="flex items-baseline justify-between gap-6">
132
+ /* SELF-STRETCH (the Hub review, 2026-09-26 — user, on fxr's /settings: "there should
133
+ * have been space between … on the right edge"). The column is `items-start`, so this
134
+ * row shrank to its content and `justify-between` had no room to act: the cluster sat
135
+ * against the subtitle on every PageHeader with actions. */
136
+ <div className="flex self-stretch items-baseline justify-between gap-6">
119
137
  {bodyNode}
120
138
  {cluster}
121
139
  </div>
122
140
  )
123
141
  : bodyNode)}
124
142
  {children}
125
- {/* inline with no body → the cluster shares the HEADLINE's row instead */}
126
- {actions && !inlineActions && <div className={cls(actionsClass, 'actions')} style={slotStyle.actions}>{actions}</div>}
143
+ {actions && !inlineActions && !inlineHead && <div className={cls(actionsClass, 'actions')} style={slotStyle.actions}>{actions}</div>}
127
144
  </div>
128
145
  )
129
146
  }
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
3
  import Button from '../atoms/Button.jsx'
4
- import { PopoverPanel, usePopover } from '../utilities/Popover.jsx'
4
+ import { PopoverPanel, usePopover, Tooltip } from '../utilities/Popover.jsx'
5
5
  import { MenuDropdownItem } from './MenuItem.jsx'
6
6
 
7
7
  /**
@@ -44,15 +44,16 @@ const ShapeDropdown = ({ options = [], value, onChange, onAction, className = ''
44
44
  return (
45
45
  <div className={`inline-flex items-center ${className}`.trim()}>
46
46
  {current?.icon ? (
47
+ <Tooltip label={current.label}>
47
48
  <Button
48
49
  variant="ghost"
49
50
  size="sm"
50
51
  quiet
51
52
  iconOnly={current.icon}
52
53
  aria-label={current.label}
53
- title={current.label}
54
54
  onClick={() => onAction?.(current.id)}
55
55
  />
56
+ </Tooltip>
56
57
  ) : (
57
58
  <Button variant="ghost" size="sm" quiet onClick={() => onAction?.(current?.id)}>
58
59
  {current?.label}
@@ -1,6 +1,6 @@
1
1
  import { useState } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
- import { PopoverPanel, usePopover } from '../utilities/Popover.jsx'
3
+ import { PopoverPanel, usePopover, Tooltip } from '../utilities/Popover.jsx'
4
4
  import { glyphSize } from '../hooks/glyphLadders.js'
5
5
 
6
6
  /**
@@ -121,6 +121,7 @@ const SplitToolButton = ({
121
121
 
122
122
  return (
123
123
  <>
124
+ <Tooltip label={title} asChild>
124
125
  <button
125
126
  ref={popover.refs.setReference}
126
127
  {...popover.getReferenceProps({ onClick: handleTriggerClick })}
@@ -129,11 +130,11 @@ const SplitToolButton = ({
129
130
  disabled={disabled}
130
131
  aria-pressed={onTrigger ? undefined : active}
131
132
  aria-label={label}
132
- title={title}
133
133
  >
134
134
  {triggerVariant && <IconC name={triggerVariant.icon} size={glyphSize(size, true)} />}
135
135
  <FoldIndicator />
136
136
  </button>
137
+ </Tooltip>
137
138
  {/* w-max — floats size to content, the menu-family law (2026-08-09). */}
138
139
  <PopoverPanel
139
140
  popover={popover}
@@ -123,7 +123,7 @@ export default function Stepper({
123
123
  <button
124
124
  type="button"
125
125
  onClick={handleIncrement}
126
- className="flex items-center justify-center text-meta hover:text-emphasis transition-colors"
126
+ className="flex items-center justify-center text-oq-48 hover:text-emphasis transition-colors"
127
127
  aria-label="Increment"
128
128
  >
129
129
  <Icon name="chevron-up" size={chevronWidth} />
@@ -131,7 +131,7 @@ export default function Stepper({
131
131
  <button
132
132
  type="button"
133
133
  onClick={handleDecrement}
134
- className="flex items-center justify-center text-meta hover:text-emphasis transition-colors"
134
+ className="flex items-center justify-center text-oq-48 hover:text-emphasis transition-colors"
135
135
  aria-label="Decrement"
136
136
  >
137
137
  <Icon name="chevron-down" size={chevronWidth} />
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useState } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
3
  import ColorSwatch from '../atoms/ColorSwatch.jsx'
4
+ import { Tooltip } from '../utilities/Popover.jsx'
4
5
 
5
6
  /*
6
7
  * SwatchControls — the Photoshop-style top row of the colour panel, in two
@@ -107,16 +108,17 @@ export function EyedropPick({ sampleColor, onPick, disabled = false }) {
107
108
  return (
108
109
  <div className="flex items-start gap-1 shrink-0">
109
110
  {supported && (
111
+ <Tooltip label="Pick a color from the canvas" asChild>
110
112
  <button
111
113
  type="button"
112
114
  onClick={onPick}
113
115
  disabled={disabled}
114
116
  aria-label="Eyedropper"
115
- title="Pick a color from the canvas"
116
117
  className="inline-flex cursor-pointer text-fg hover:opacity-80 disabled:opacity-40 disabled:cursor-not-allowed"
117
118
  >
118
119
  <Icon name="eyedrop" size={24} />
119
120
  </button>
121
+ </Tooltip>
120
122
  )}
121
123
  <ColorSwatch
122
124
  hex={sampleColor}
@@ -71,7 +71,7 @@ export default function TabsRow({ tabs = [], value, onChange, onClose, onMinimis
71
71
  type="button"
72
72
  onClick={onMinimise}
73
73
  aria-label="Minimise"
74
- className="ml-auto text-meta hover:text-emphasis self-center"
74
+ className="ml-auto text-oq-48 hover:text-emphasis self-center"
75
75
  style={{ lineHeight: 0 }}
76
76
  >
77
77
  <Icon name="chevron-down" size={12} />
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react'
2
2
  import { CanvasZoomContext } from '../hooks/canvasZoom.js'
3
+ import { Tooltip } from '../utilities/Popover.jsx'
3
4
 
4
5
  /**
5
6
  * Canvas — the editor's aspect-ratio stage.
@@ -86,10 +87,15 @@ export function CanvasFrame({
86
87
  customRatio,
87
88
  aspects,
88
89
  bgColor,
89
- guideColor = CANVAS_DEFAULTS.guideColor,
90
+ guideColor,
90
91
  children,
91
92
  }) {
92
93
  const { ratio, label } = resolveAspect(aspect, customRatio, aspects)
94
+ /* NO guideColor → THEME INK (editor DS sync 2026-09-27, from the design editor's copy, which had
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. */
97
+ const borderColor = guideColor ? `color-mix(in srgb, ${guideColor} 24%, transparent)` : 'var(--kol-oq-24)'
98
+ const labelColor = guideColor ? `color-mix(in srgb, ${guideColor} 70%, transparent)` : 'var(--kol-fg-64)'
93
99
  const virtualH = CANVAS_VIRTUAL_W / ratio
94
100
 
95
101
  const rectRef = useRef(null)
@@ -126,7 +132,7 @@ export function CanvasFrame({
126
132
  className="absolute inset-0 pointer-events-none z-[2]"
127
133
  style={{
128
134
  border: '1px solid',
129
- borderColor: `color-mix(in srgb, ${guideColor} 24%, transparent)`,
135
+ borderColor,
130
136
  }}
131
137
  />
132
138
  <span
@@ -138,7 +144,7 @@ export function CanvasFrame({
138
144
  fontSize: 10,
139
145
  fontFamily: 'var(--kol-font-family-mono)',
140
146
  letterSpacing: '0.1em',
141
- color: `color-mix(in srgb, ${guideColor} 70%, transparent)`,
147
+ color: labelColor,
142
148
  pointerEvents: 'none',
143
149
  }}
144
150
  >
@@ -196,7 +202,7 @@ export default function Canvas({
196
202
  customRatio,
197
203
  aspects,
198
204
  bgColor,
199
- guideColor = CANVAS_DEFAULTS.guideColor,
205
+ guideColor,
200
206
  align = 'center',
201
207
  panEnabled = false,
202
208
  backdrop,
@@ -628,22 +634,24 @@ export function PanZoomViewport({
628
634
  {/* Zoom % + fps — matching chips. Zoom (click resets to 100% /
629
635
  centered) first, fps to its right, shown while `f` toggles it. */}
630
636
  <div className="absolute bottom-3 right-3 z-[3] flex items-center gap-2">
637
+ <Tooltip label="Reset zoom (⌘0)" asChild>
631
638
  <button
632
639
  type="button"
633
640
  onClick={() => setView({ zoom: 1, x: 0, y: 0 })}
634
641
  className="px-2 py-1 rounded border border-fg-08 bg-surface-secondary kol-mono-12 text-emphasis tabular-nums"
635
642
  style={{ opacity: atRest && !showFps ? 0.55 : 1 }}
636
- title="Reset zoom (⌘0)"
637
643
  >
638
644
  {Math.round(view.zoom * 100)}%
639
645
  </button>
646
+ </Tooltip>
640
647
  {showFps && (
648
+ <Tooltip label="Framerate — press F to hide" asChild>
641
649
  <span
642
650
  className="px-2 py-1 rounded border border-fg-08 bg-surface-secondary kol-mono-12 text-emphasis tabular-nums"
643
- title="Framerate — press F to hide"
644
651
  >
645
652
  {fps} fps
646
653
  </span>
654
+ </Tooltip>
647
655
  )}
648
656
  </div>
649
657
  </div>
@@ -817,7 +825,7 @@ export function CanvasRuler({ containerRef, virtualWidth = CANVAS_VIRTUAL_W, dis
817
825
  const tickColor = 'var(--kol-fg-48)'
818
826
  const textColor = 'var(--kol-fg-64)'
819
827
  const barBg = 'var(--kol-fg-08)'
820
- const borderColor = 'var(--kol-fg-16)'
828
+ const borderColor = 'var(--kol-oq-16)'
821
829
  const labelStyle = { fontFamily: 'var(--kol-font-family-mono)', fontSize: 9 }
822
830
 
823
831
  return (
@@ -41,6 +41,9 @@ import { readDraft, writeDraft, clearDraft } from '../utilities/localDrafts.js'
41
41
  * searchable list inside the editor inserts the pick at the caret
42
42
  * @param {Function} onPickAsset `() => Promise<{ name, url, contentType } | null>` — a consumer's own picker instead
43
43
  * @param {Function} onClose
44
+ * @param {boolean} inline render IN the page, filling its container, instead of over it (the notes
45
+ * page, media-shell 2026-09-26: the editor is the page's content there,
46
+ * not a window over a list). No scrim, no Escape-to-close; the X still closes
44
47
  */
45
48
  const TYPES = [
46
49
  { value: 'md', label: 'MD', kind: 'markdown' },
@@ -64,6 +67,7 @@ const DRAFT_PAUSE = 400 // browser memory is free — a short pause, not D1's wr
64
67
  export default function DocumentEditor({
65
68
  mode = 'edit', name: nameProp = '', kind: kindProp, text, savedAt = 0, draft,
66
69
  onSave, onCreate, folder = '', assets, onPickAsset, onClose,
70
+ inline = false,
67
71
  }) {
68
72
  const [picking, setPicking] = useState(false)
69
73
  const [pickQuery, setPickQuery] = useState('')
@@ -186,9 +190,8 @@ export default function DocumentEditor({
186
190
  ? <img src={`data:image/svg+xml;charset=utf-8,${encodeURIComponent(value)}`} alt="" className="max-w-full max-h-full object-contain m-auto" />
187
191
  : <KindPreview o={{ key: name || 'document' }} text={value} kind={kind} />
188
192
 
189
- return (
190
- <FullscreenOverlay open onClose={close} closeButton={false} scrim>
191
- <QuickLookFrame title={isNew ? `New document${folder ? ` in ${folder}` : ''}` : name} meta={status} onClose={close}
193
+ const frame = (
194
+ <QuickLookFrame className={inline ? 'kol-quicklook--inline' : ''} title={isNew ? `New document${folder ? ` in ${folder}` : ''}` : name} meta={status} onClose={close}
192
195
  actions={(
193
196
  <span className="flex items-center gap-2">
194
197
  <ViewToggle variant="text" viewMode={view} onViewChange={setView} options={views} />
@@ -198,7 +201,7 @@ export default function DocumentEditor({
198
201
  </span>
199
202
  )}>
200
203
  {/* the window's own caps, inline — Tailwind does not generate arbitrary values from package source */}
201
- <div className="kol-doc-editor flex flex-col" style={{ width: 'min(1200px, var(--kol-ql-max-w))', height: 'min(760px, var(--kol-ql-media-h))' }}>
204
+ <div className="kol-doc-editor flex flex-col" style={inline ? { width: '100%', height: '100%' } : { width: 'min(1200px, var(--kol-ql-max-w))', height: 'min(760px, var(--kol-ql-media-h))' }}>
202
205
  {isNew && (
203
206
  <div className="flex flex-wrap items-center gap-3 p-4 border-b" style={{ borderColor: 'var(--kol-oq-08)' }}>
204
207
  <Input variant="filled" size="sm" value={name} aria-label="File name" placeholder="File name"
@@ -240,8 +243,8 @@ export default function DocumentEditor({
240
243
  </div>
241
244
  </div>
242
245
  </QuickLookFrame>
243
- </FullscreenOverlay>
244
246
  )
247
+ return inline ? frame : <FullscreenOverlay open onClose={close} closeButton={false} scrim>{frame}</FullscreenOverlay>
245
248
  }
246
249
 
247
250
  /* THE FRONTMATTER AS FIELDS. One row per key; a list value (tags) is typed comma-separated. The
@@ -3,6 +3,7 @@ import Button from '../atoms/Button.jsx'
3
3
  import Dropdown from '../molecules/Dropdown.jsx'
4
4
  import LabeledControl from '../molecules/LabeledControl.jsx'
5
5
  import Slider from '../molecules/Slider.jsx'
6
+ import { Tooltip } from '../utilities/Popover.jsx'
6
7
 
7
8
  const deg = (r) => Math.round(((r || 0) * 180) / Math.PI)
8
9
  const rad = (d) => (d * Math.PI) / 180
@@ -41,7 +42,7 @@ export const DEFAULT_KEYFRAMES = [
41
42
  *
42
43
  * @param {Array<Object>} keyframes - The track; empty falls back to `DEFAULT_KEYFRAMES`
43
44
  * @param {Function} onChange - `(keyframes) => void` — the whole track, sorted
44
- * @param {number} t - The clock's current LAYER-LOCAL phase, 0..1 — what "Add @ playhead" stamps
45
+ * @param {number} t - The clock's current LAYER-LOCAL phase, 0..1 — what "Add @ playhead" stamps; or a function returning it, read at the click — a clock that runs without re-rendering (the design editor's transport) would otherwise stamp the phase of the last render
45
46
  * @param {Function} onSeek - `(t) => void` — a key was selected; seek the clock to its local `t`
46
47
  * @param {Function} [onPause] - Called before the seek, so the render holds on the pose
47
48
  * @param {Array<{value: string, label: string}>} [easeOptions] - The Ease menu (default `KEYFRAME_EASES`)
@@ -71,7 +72,7 @@ export default function KeyframeEditor({
71
72
  const onAdd = () => {
72
73
  const base = kfs[sel] || { rot: [0, 0, 0], pos: [0, 0, 0], scale: 1, ease: 'inout' }
73
74
  const nk = {
74
- t: Math.max(0, Math.min(1, t)),
75
+ t: Math.max(0, Math.min(1, typeof t === 'function' ? t() : t)),
75
76
  rot: [...(base.rot || [0, 0, 0])],
76
77
  pos: [...(base.pos || [0, 0, 0])],
77
78
  scale: base.scale ?? 1,
@@ -117,7 +118,7 @@ export default function KeyframeEditor({
117
118
  </div>
118
119
  <div className="flex gap-2">
119
120
  <Button variant="primary" size="sm" className="flex-1" onClick={onAdd}>Add @ playhead</Button>
120
- <Button variant="ghost" size="sm" title="Delete keyframe" onClick={onDelete} disabled={kfs.length <= 1}>Delete</Button>
121
+ <Tooltip label="Delete keyframe"><Button variant="ghost" size="sm" onClick={onDelete} disabled={kfs.length <= 1}>Delete</Button></Tooltip>
121
122
  </div>
122
123
 
123
124
  <span className="kol-helper-10 text-meta">Pose</span>