@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 +2 -2
- package/src/atoms/ColorSwatch.jsx +6 -2
- package/src/atoms/SelectionOverlay.jsx +3 -1
- package/src/atoms/XYPad.jsx +4 -4
- package/src/index.js +1 -1
- package/src/molecules/ColorInputRow.jsx +3 -2
- package/src/molecules/Dropdown.jsx +10 -5
- package/src/molecules/FieldRow.jsx +2 -2
- package/src/molecules/InspectorSection.jsx +14 -3
- package/src/molecules/MenuItem.jsx +5 -5
- package/src/molecules/SearchInput.jsx +1 -1
- package/src/molecules/SectionText.jsx +21 -4
- package/src/molecules/ShapeDropdown.jsx +3 -2
- package/src/molecules/SplitToolButton.jsx +3 -2
- package/src/molecules/Stepper.jsx +2 -2
- package/src/molecules/SwatchControls.jsx +3 -1
- package/src/molecules/TabsRow.jsx +1 -1
- package/src/organisms/Canvas.jsx +15 -7
- package/src/organisms/DocumentEditor.jsx +8 -5
- package/src/organisms/KeyframeEditor.jsx +4 -3
- package/src/organisms/LayerStack.jsx +11 -6
- package/src/organisms/MediaLibrary.jsx +1 -1
- package/src/organisms/MediaLibraryExplorer.jsx +67 -3
- package/src/organisms/MediaLibraryPages.jsx +113 -62
- package/src/organisms/RecordManager.jsx +2 -2
- package/src/organisms/SettingsPanel.jsx +5 -1
- package/src/organisms/SettingsSections.jsx +2 -1
- package/src/organisms/SpectrumGrid.jsx +3 -2
- package/src/organisms/TimelineDock.jsx +7 -4
- package/src/organisms/ToolPalette.jsx +6 -3
- package/src/utilities/Popover.jsx +61 -11
- package/src/utilities/QuadrantSync.jsx +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "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.
|
|
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
|
-
<
|
|
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
|
package/src/atoms/XYPad.jsx
CHANGED
|
@@ -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-
|
|
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-
|
|
83
|
-
<div className="absolute inset-y-0 left-1/2 border-l border-
|
|
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-
|
|
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
|
-
|
|
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
|
-
{
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
{
|
|
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-
|
|
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-
|
|
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-
|
|
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} />
|
package/src/organisms/Canvas.jsx
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
|
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-
|
|
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
|
-
|
|
190
|
-
|
|
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"
|
|
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>
|