@kolkrabbi/kol-component 0.6.0 → 0.8.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/README.md +8 -0
- package/package.json +2 -3
- package/src/atoms/RotaryDial.jsx +35 -21
- package/src/index.js +6 -18
- package/src/molecules/ButtonGroup.jsx +45 -0
- package/src/molecules/ColorInputRow.jsx +146 -126
- package/src/molecules/Slider.jsx +29 -14
- package/src/molecules/SplitToolButton.jsx +133 -0
- package/src/organisms/ContentFilters.jsx +2 -2
- package/src/organisms/GalleryCarousel.jsx +2 -1
- package/src/organisms/LoaderOverlay.jsx +11 -14
- package/src/organisms/MediaTileGallery.jsx +57 -0
- package/src/organisms/MediaViewer.jsx +89 -64
- package/src/atoms/PriceDisplay.jsx +0 -34
- package/src/atoms/TextPressure.jsx +0 -331
- package/src/atoms/TypeSample.jsx +0 -50
- package/src/atoms/TypeSpecCard.jsx +0 -42
- package/src/molecules/ArticleCard.jsx +0 -178
- package/src/molecules/WorkListItem.jsx +0 -83
- package/src/molecules/foundry/SpecimenSectionHeader.jsx +0 -89
- package/src/organisms/ArticleHeader.jsx +0 -95
- package/src/organisms/ColorLoader.jsx +0 -155
- package/src/organisms/DiagonalMarqueeRiver.jsx +0 -138
- package/src/organisms/ParallaxShelf.jsx +0 -141
- package/src/organisms/PortableTextRenderer.jsx +0 -115
- package/src/organisms/ProductDetailLayout.jsx +0 -189
- package/src/organisms/ScrollDriftGallery.jsx +0 -214
- package/src/organisms/StackHero.jsx +0 -83
- package/src/organisms/WorkCard.jsx +0 -120
- package/src/organisms/WorkViewToggle.jsx +0 -170
- package/src/organisms/foundry/FontPreviewSection.jsx +0 -187
- package/src/organisms/foundry/FoundryCharacterSets.jsx +0 -113
- package/src/organisms/foundry/GlyphMetricsGrid.jsx +0 -335
- package/src/organisms/foundry/TypefaceHero.jsx +0 -107
- package/src/organisms/foundry/TypefaceStyleSection.jsx +0 -163
- package/src/organisms/foundry/VariableFontSection.jsx +0 -158
- package/src/organisms/foundry/glyphData.js +0 -30
- package/src/organisms/foundry/index.js +0 -21
package/README.md
CHANGED
|
@@ -11,6 +11,14 @@ npm i @kolkrabbi/kol-component @kolkrabbi/kol-theme
|
|
|
11
11
|
|
|
12
12
|
Requires a **Vite + Tailwind v4** app and the theme CSS imported (see the theme package).
|
|
13
13
|
|
|
14
|
+
## Tailwind v4 consumers
|
|
15
|
+
|
|
16
|
+
Tailwind skips `node_modules` when scanning, so point it at this package's source — otherwise the utility classes inside the components never generate and layouts collapse:
|
|
17
|
+
|
|
18
|
+
```css
|
|
19
|
+
@source "../node_modules/@kolkrabbi/kol-component/src";
|
|
20
|
+
```
|
|
21
|
+
|
|
14
22
|
## Use
|
|
15
23
|
|
|
16
24
|
```jsx
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
7
7
|
"main": "./src/index.js",
|
|
8
8
|
"module": "./src/index.js",
|
|
9
9
|
"exports": {
|
|
10
|
-
".": "./src/index.js"
|
|
11
|
-
"./foundry": "./src/organisms/foundry/index.js"
|
|
10
|
+
".": "./src/index.js"
|
|
12
11
|
},
|
|
13
12
|
"files": [
|
|
14
13
|
"src",
|
package/src/atoms/RotaryDial.jsx
CHANGED
|
@@ -7,22 +7,29 @@ const snapTo = (v, min, max, step) => {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* RotaryDial — drag-to-set rotary knob numeric input.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* value fires on release. Focus + arrow keys step the value
|
|
16
|
-
* (role="slider" with aria-valuemin/max/now). The label and `%` readout
|
|
17
|
-
* rows render only when `label` is set; label text renders as authored.
|
|
10
|
+
* RotaryDial — drag-to-set rotary knob numeric input. The KNOB VARIANT of
|
|
11
|
+
* Slider (molecules/Slider.jsx): both implement the shared value-control
|
|
12
|
+
* contract — `value` / `min` / `max` / `step` / `onChange(next: number)` /
|
|
13
|
+
* `label` / `size` / `disabled` / `formatValue`. Controlled; onChange always
|
|
14
|
+
* fires with the plain number.
|
|
18
15
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
16
|
+
* Vertical pointer drag rotates the whole dial over a 270° sweep (−135° to
|
|
17
|
+
* +135°) mapped onto min–max; a full sweep is ~200px of drag, movement
|
|
18
|
+
* measured absolute from drag start. A local visual buffer keeps rotation
|
|
19
|
+
* smooth while parent updates are RAF-throttled; the final value fires on
|
|
20
|
+
* release. Focus + arrow keys step the value (role="slider" with
|
|
21
|
+
* aria-valuemin/max/now). The label and readout rows render only when
|
|
22
|
+
* `label` is set; label text renders as authored.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} label text under the dial; also gates the label + readout rows
|
|
25
|
+
* @param {number} value controlled value within min–max
|
|
26
|
+
* @param {Function} onChange (next: number) => void — RAF-throttled while dragging, final on release
|
|
27
|
+
* @param {number} min minimum value (default 0)
|
|
28
|
+
* @param {number} max maximum value (default 100)
|
|
29
|
+
* @param {number} step drag snap / arrow-key increment (default 1)
|
|
30
|
+
* @param {number} size dial px size (default 80); derives ring + disc radii
|
|
31
|
+
* @param {boolean} disabled blocks drag + keyboard and dims the control (default false)
|
|
32
|
+
* @param {Function} formatValue optional readout formatter (value: number) => string; default `${value}%`
|
|
26
33
|
*/
|
|
27
34
|
export default function RotaryDial({
|
|
28
35
|
label,
|
|
@@ -32,6 +39,8 @@ export default function RotaryDial({
|
|
|
32
39
|
max = 100,
|
|
33
40
|
step = 1,
|
|
34
41
|
size = 80,
|
|
42
|
+
disabled = false,
|
|
43
|
+
formatValue,
|
|
35
44
|
}) {
|
|
36
45
|
const [isDragging, setIsDragging] = useState(false)
|
|
37
46
|
const [localValue, setLocalValue] = useState(value) // visual buffer — updates every move
|
|
@@ -99,23 +108,24 @@ export default function RotaryDial({
|
|
|
99
108
|
const angle = -135 + ((localValue - min) / (max - min || 1)) * 270
|
|
100
109
|
|
|
101
110
|
return (
|
|
102
|
-
<div className=
|
|
111
|
+
<div className={`flex flex-col items-center gap-2 ${disabled ? 'opacity-50' : ''}`}>
|
|
103
112
|
<div
|
|
104
113
|
role="slider"
|
|
105
|
-
tabIndex={0}
|
|
114
|
+
tabIndex={disabled ? -1 : 0}
|
|
106
115
|
aria-label={label}
|
|
107
116
|
aria-valuemin={min}
|
|
108
117
|
aria-valuemax={max}
|
|
109
118
|
aria-valuenow={localValue}
|
|
110
|
-
|
|
119
|
+
aria-disabled={disabled || undefined}
|
|
120
|
+
className={`relative select-none touch-none ${disabled ? 'cursor-default' : 'cursor-pointer'}`}
|
|
111
121
|
style={{
|
|
112
122
|
width: size,
|
|
113
123
|
height: size,
|
|
114
124
|
transform: `rotate(${angle}deg)`,
|
|
115
125
|
willChange: isDragging ? 'transform' : 'auto',
|
|
116
126
|
}}
|
|
117
|
-
onPointerDown={handlePointerDown}
|
|
118
|
-
onKeyDown={handleKeyDown}
|
|
127
|
+
onPointerDown={disabled ? undefined : handlePointerDown}
|
|
128
|
+
onKeyDown={disabled ? undefined : handleKeyDown}
|
|
119
129
|
>
|
|
120
130
|
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ overflow: 'visible' }}>
|
|
121
131
|
{/* outer dashed tick ring */}
|
|
@@ -144,7 +154,11 @@ export default function RotaryDial({
|
|
|
144
154
|
</svg>
|
|
145
155
|
</div>
|
|
146
156
|
{label && <div className="kol-helper-12 text-fg-64">{label}</div>}
|
|
147
|
-
{label &&
|
|
157
|
+
{label && (
|
|
158
|
+
<div className="kol-helper-12 text-fg-96">
|
|
159
|
+
{formatValue ? String(formatValue(value)) : `${value}%`}
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
148
162
|
</div>
|
|
149
163
|
)
|
|
150
164
|
}
|
package/src/index.js
CHANGED
|
@@ -38,7 +38,6 @@ export { default as LabeledControl } from './atoms/LabeledControl.jsx'
|
|
|
38
38
|
export { default as OverlayGlassPanel } from './atoms/OverlayGlassPanel.jsx'
|
|
39
39
|
export { default as Pill } from './atoms/Pill.jsx'
|
|
40
40
|
export { usePopover, PopoverPanel, Tooltip } from './atoms/Popover.jsx'
|
|
41
|
-
export { default as PriceDisplay } from './atoms/PriceDisplay.jsx'
|
|
42
41
|
export { default as ProsePreview } from './atoms/ProsePreview.jsx'
|
|
43
42
|
export { default as QuantityInput } from './atoms/QuantityInput.jsx'
|
|
44
43
|
export { default as RotaryDial } from './atoms/RotaryDial.jsx'
|
|
@@ -52,22 +51,18 @@ export { default as Textarea } from './atoms/Textarea.jsx'
|
|
|
52
51
|
export { default as ToggleBracket } from './atoms/ToggleBracket.jsx'
|
|
53
52
|
export { default as ToggleCheckbox } from './atoms/ToggleCheckbox.jsx'
|
|
54
53
|
export { default as ToggleSwitch } from './atoms/ToggleSwitch.jsx'
|
|
55
|
-
export { default as TextPressure } from './atoms/TextPressure.jsx'
|
|
56
54
|
export { default as TiltCard } from './atoms/TiltCard.jsx'
|
|
57
55
|
export { default as TransparentX } from './atoms/TransparentX.jsx'
|
|
58
|
-
export { default as TypeSample } from './atoms/TypeSample.jsx'
|
|
59
|
-
export { default as TypeSpecCard } from './atoms/TypeSpecCard.jsx'
|
|
60
56
|
export { default as ViewToggle } from './atoms/ViewToggle.jsx'
|
|
61
57
|
|
|
62
58
|
// molecules
|
|
63
59
|
export { Accordion, AccordionPanel } from './molecules/Accordion.jsx'
|
|
60
|
+
export { default as ButtonGroup } from './molecules/ButtonGroup.jsx'
|
|
64
61
|
/* monorepo sets (P6–P10) — molecule members */
|
|
65
62
|
export { default as AlignmentGrid } from './molecules/AlignmentGrid.jsx'
|
|
66
|
-
export { default as ArticleCard } from './molecules/ArticleCard.jsx'
|
|
67
63
|
export { default as ImageBlock } from './molecules/ImageBlock.jsx'
|
|
68
64
|
export { default as SelectionOverlay } from './molecules/SelectionOverlay.jsx'
|
|
69
65
|
export { default as VideoBlock, getEmbedUrl } from './molecules/VideoBlock.jsx'
|
|
70
|
-
export { default as WorkListItem } from './molecules/WorkListItem.jsx'
|
|
71
66
|
export { default as CardFeatureItem } from './molecules/CardFeatureItem.jsx'
|
|
72
67
|
export { default as CodeBlock } from './molecules/CodeBlock.jsx'
|
|
73
68
|
export { default as ColorInputRow } from './molecules/ColorInputRow.jsx'
|
|
@@ -88,28 +83,20 @@ export { default as ShellSearchOverlay } from './molecules/ShellSearchOverlay.js
|
|
|
88
83
|
export { default as Slider } from './molecules/Slider.jsx'
|
|
89
84
|
export { default as SpecList } from './molecules/SpecList.jsx'
|
|
90
85
|
export { default as SpectrumControls, HueStrip, SBSquare, WheelTriangle } from './molecules/SpectrumControls.jsx'
|
|
86
|
+
export { default as SplitToolButton } from './molecules/SplitToolButton.jsx'
|
|
91
87
|
export { default as SwatchControls, SwatchStack, EyedropPick } from './molecules/SwatchControls.jsx'
|
|
92
88
|
export { default as TabsRow } from './molecules/TabsRow.jsx'
|
|
93
89
|
|
|
94
90
|
// organisms
|
|
95
|
-
/* monorepo sets (P6–P10) — organism members. Foundry members
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
/* monorepo sets (P6–P10) — organism members. Foundry members live in the
|
|
92
|
+
standalone @kolkrabbi/kol-foundry package (with the type-specimen kit +
|
|
93
|
+
live-font effects moved there 2026-07-09) — never re-exported here. */
|
|
98
94
|
export { default as Canvas, CanvasFrame, PanViewport, CANVAS_VIRTUAL_W, DEFAULT_ASPECTS, CANVAS_DEFAULTS } from './organisms/Canvas.jsx'
|
|
99
|
-
export { default as DiagonalMarqueeRiver } from './organisms/DiagonalMarqueeRiver.jsx'
|
|
100
95
|
export { default as EditorShell } from './organisms/EditorShell.jsx'
|
|
101
96
|
export { default as GalleryCarousel } from './organisms/GalleryCarousel.jsx'
|
|
102
|
-
export { default as ParallaxShelf } from './organisms/ParallaxShelf.jsx'
|
|
103
|
-
export { default as PortableTextRenderer, slugify } from './organisms/PortableTextRenderer.jsx'
|
|
104
|
-
export { default as ProductDetailLayout } from './organisms/ProductDetailLayout.jsx'
|
|
105
|
-
export { default as ScrollDriftGallery } from './organisms/ScrollDriftGallery.jsx'
|
|
106
|
-
export { default as StackHero } from './organisms/StackHero.jsx'
|
|
107
|
-
export { default as WorkCard } from './organisms/WorkCard.jsx'
|
|
108
|
-
export { default as WorkViewToggle } from './organisms/WorkViewToggle.jsx'
|
|
109
97
|
export { default as AsciiCursor } from './organisms/AsciiCursor.jsx'
|
|
110
98
|
export { default as BentoCard } from './organisms/BentoCard.jsx'
|
|
111
99
|
export { default as Carousel } from './organisms/Carousel.jsx'
|
|
112
|
-
export { default as ColorLoader } from './organisms/ColorLoader.jsx'
|
|
113
100
|
export { default as ContentFilters } from './organisms/ContentFilters.jsx'
|
|
114
101
|
export { default as CtaGlobal } from './organisms/CtaGlobal.jsx'
|
|
115
102
|
export { default as ErrorBoundary } from './organisms/ErrorBoundary.jsx'
|
|
@@ -118,6 +105,7 @@ export { default as FeaturedCarousel } from './organisms/FeaturedCarousel.jsx'
|
|
|
118
105
|
export { default as FeaturesCardSection } from './organisms/FeaturesCardSection.jsx'
|
|
119
106
|
export { default as FullBleedHero } from './organisms/FullBleedHero.jsx'
|
|
120
107
|
export { default as LoaderOverlay } from './organisms/LoaderOverlay.jsx'
|
|
108
|
+
export { default as MediaTileGallery } from './organisms/MediaTileGallery.jsx'
|
|
121
109
|
export { default as MediaViewer } from './organisms/MediaViewer.jsx'
|
|
122
110
|
export { default as NewsletterBand } from './organisms/NewsletterBand.jsx'
|
|
123
111
|
export { default as SpectrumGrid } from './organisms/SpectrumGrid.jsx'
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* taxonomy-ok: composes consumer-passed Buttons via children — nesting happens at the call site */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ButtonGroup — responsive layout wrapper for a group of Buttons: stacked
|
|
5
|
+
* full-width on mobile, horizontal row from `sm` up, aligned left / center /
|
|
6
|
+
* right. Pure layout — children compose at the call site and render as-is
|
|
7
|
+
* (nothing is cloned, no props injected); all button behavior lives on each
|
|
8
|
+
* Button. `center` (default) hugs its content and self-centers; `left` /
|
|
9
|
+
* `right` stay full-width block. Alignment classes come from static lookup
|
|
10
|
+
* maps of complete literals — never interpolated — so Tailwind's scanner
|
|
11
|
+
* sees them.
|
|
12
|
+
*
|
|
13
|
+
* @param {'left'|'center'|'right'} align horizontal alignment + container layout mode (default 'center')
|
|
14
|
+
* @param {string} title optional heading rendered above the group, authored at the call site
|
|
15
|
+
* @param {string} className extra container classes (appended)
|
|
16
|
+
* @param {ReactNode} children the Buttons
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const CONTAINER = {
|
|
20
|
+
left: 'flex flex-col w-full',
|
|
21
|
+
center: 'flex sm:inline-flex flex-col w-full sm:w-auto sm:mx-auto',
|
|
22
|
+
right: 'flex flex-col w-full',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const JUSTIFY = {
|
|
26
|
+
left: 'sm:justify-start',
|
|
27
|
+
center: 'sm:justify-center',
|
|
28
|
+
right: 'sm:justify-end',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default function ButtonGroup({ align = 'center', title, className = '', children }) {
|
|
32
|
+
return (
|
|
33
|
+
<div className={[CONTAINER[align] || CONTAINER.center, className].filter(Boolean).join(' ')}>
|
|
34
|
+
{title && <h3 className="kol-sans-heading-05 text-emphasis mb-6">{title}</h3>}
|
|
35
|
+
<div
|
|
36
|
+
className={[
|
|
37
|
+
'flex flex-col gap-4 sm:flex-row sm:items-center',
|
|
38
|
+
JUSTIFY[align] || JUSTIFY.center,
|
|
39
|
+
].join(' ')}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -2,153 +2,172 @@ import { useState } from 'react'
|
|
|
2
2
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
3
3
|
import ColorSwatch from './ColorSwatch'
|
|
4
4
|
import Input from '../atoms/Input'
|
|
5
|
-
import LabeledControl from '../atoms/LabeledControl'
|
|
6
5
|
import { usePopover, PopoverPanel } from '../atoms/Popover'
|
|
7
6
|
|
|
8
|
-
const HEX6 = /^[0-9A-F]{6}$/
|
|
9
|
-
|
|
10
|
-
/** '#f0a' / 'F0A' / '#ff00aa' → 'FF00AA'; anything unparseable → null. */
|
|
11
|
-
function normalizeDigits(raw) {
|
|
12
|
-
if (typeof raw !== 'string') return null
|
|
13
|
-
let d = raw.replace(/^#/, '').trim().toUpperCase()
|
|
14
|
-
if (/^[0-9A-F]{3}$/.test(d)) d = d.replace(/./g, (c) => c + c)
|
|
15
|
-
return HEX6.test(d) ? d : null
|
|
16
|
-
}
|
|
17
|
-
|
|
18
7
|
/**
|
|
19
8
|
* ColorInputRow — swatch chip + `#` hex input row. The single merged form of
|
|
20
9
|
* the brand editor's ColorField (layer color assignment: palette-ref popover
|
|
21
|
-
* behind the swatch) and SwatchRow (palette-slot editing:
|
|
22
|
-
* one core
|
|
23
|
-
* molecules.
|
|
10
|
+
* behind the swatch) and SwatchRow (palette-slot editing: lock toggle +
|
|
11
|
+
* token-name column) — one core with additive slots instead of two
|
|
12
|
+
* overlapping molecules. Supersedes both lobby briefs (lobby/ColorField.md,
|
|
13
|
+
* lobby/SwatchRow.md).
|
|
24
14
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
15
|
+
* Modes (additive on the core swatch + hex input):
|
|
16
|
+
* refs — the swatch becomes a popover trigger over a 6-column grid
|
|
17
|
+
* of palette entries. Entries arrive PRE-RESOLVED
|
|
18
|
+
* ({ value, label, hex }) — no `palette:` string convention,
|
|
19
|
+
* no resolver seam; the app keeps its own resolution logic.
|
|
20
|
+
* Picking a cell fires onChange(entry.value) and closes.
|
|
21
|
+
* lock — `onToggleLock` makes the swatch the lock toggle: a sibling
|
|
22
|
+
* absolute overlay span (NOT inside the swatch — its
|
|
23
|
+
* overflow-hidden would clip it) shows the lock/unlock glyph,
|
|
24
|
+
* pinned visible when locked, on hover otherwise. Ignored
|
|
25
|
+
* when `refs` is set — the swatch has one click affordance.
|
|
26
|
+
* tokenName — adds the resolved-token readout column and switches the
|
|
27
|
+
* row from a flex row to the fixed 4-column grid
|
|
28
|
+
* (24px / label / token / input).
|
|
30
29
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* `transparentTone` is an explicit prop,
|
|
30
|
+
* The hex input is normalized per SwatchRow: value renders as bare UPPER
|
|
31
|
+
* digits behind the `#` prefix, and every keystroke emits
|
|
32
|
+
* onChange('#' + digits.toUpperCase()) — the `.toUpperCase()` is value
|
|
33
|
+
* normalization, not copy casing. SwatchRow's dead `edited` prop is dropped;
|
|
34
|
+
* `transparentTone` is an explicit prop, never inferred from the label.
|
|
35
|
+
* Composition only — no color math, no store coupling.
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
37
|
+
* Props:
|
|
38
|
+
* value — current color: '#RRGGBB', a refs entry's `value`, or
|
|
39
|
+
* null (renders the transparent "None" swatch)
|
|
40
|
+
* onChange — (next: string) => void — '#'+UPPER hex on typing, or
|
|
41
|
+
* entry.value on a palette pick
|
|
42
|
+
* label — row label (kol-helper-12); also prefixes aria-labels
|
|
43
|
+
* hideLabel — suppress the visible label (aria keeps it)
|
|
44
|
+
* refs — [{ value, label, hex }] pre-resolved palette entries →
|
|
45
|
+
* popover mode
|
|
46
|
+
* locked — lock overlay pinned visible, aria-pressed on the swatch
|
|
47
|
+
* onToggleLock — () => void — swatch click toggles the lock
|
|
48
|
+
* tokenName — resolved token readout (kol-helper-10) → grid mode
|
|
49
|
+
* disabled — opacity-30 + pointer-events-none + aria-disabled
|
|
50
|
+
* unused — muted slot: transparent swatch, label/token drop to
|
|
51
|
+
* text-meta/text-subtle, input dims
|
|
52
|
+
* transparentTone — TransparentX stroke tone for the null/unused swatch:
|
|
53
|
+
* 'warning' (default) | 'error' | 'info' | 'success'
|
|
47
54
|
*/
|
|
48
55
|
export default function ColorInputRow({
|
|
49
56
|
value,
|
|
50
57
|
onChange,
|
|
51
58
|
label,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
inputVariant = 'filled',
|
|
59
|
+
hideLabel = false,
|
|
60
|
+
refs,
|
|
61
|
+
locked = false,
|
|
62
|
+
onToggleLock,
|
|
63
|
+
tokenName,
|
|
58
64
|
disabled = false,
|
|
65
|
+
unused = false,
|
|
66
|
+
transparentTone = 'warning',
|
|
59
67
|
className = '',
|
|
60
68
|
}) {
|
|
61
|
-
const
|
|
62
|
-
const
|
|
69
|
+
const hasRefs = Array.isArray(refs) && refs.length > 0
|
|
70
|
+
const isLockToggle = !hasRefs && typeof onToggleLock === 'function'
|
|
71
|
+
const isGrid = tokenName != null
|
|
72
|
+
const labelVisible = label != null && !hideLabel
|
|
63
73
|
|
|
64
|
-
/*
|
|
65
|
-
*
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
74
|
+
/* Display resolution: when the current value is a refs entry, the swatch and
|
|
75
|
+
* input show the entry's pre-resolved hex; otherwise value IS the hex. */
|
|
76
|
+
const activeRef = hasRefs ? refs.find((r) => r.value === value) : undefined
|
|
77
|
+
const displayHex = activeRef?.hex ?? value ?? null
|
|
78
|
+
const digits = (displayHex ?? '').replace(/^#/, '').toUpperCase()
|
|
79
|
+
const showTransparent = unused || displayHex == null
|
|
80
|
+
const subtitle = displayHex == null ? 'None' : (activeRef?.label ?? '#' + digits)
|
|
72
81
|
|
|
73
82
|
const [open, setOpen] = useState(false)
|
|
74
83
|
const popover = usePopover({ open, onOpenChange: setOpen, placement: 'bottom-start', offset: 4 })
|
|
75
84
|
|
|
76
|
-
const
|
|
77
|
-
const next = normalizeDigits(draft)
|
|
78
|
-
if (next == null) {
|
|
79
|
-
setDraft(digits ?? '') // invalid or empty → revert, never emit
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
setDraft(next)
|
|
83
|
-
if (next !== digits) onChange?.('#' + next)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const pick = (ref) => {
|
|
87
|
-
onChange?.('#' + (normalizeDigits(ref.value) ?? ''))
|
|
88
|
-
setOpen(false)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/* Swatch subtitle for the trigger's aria-label: the matching ref's label
|
|
92
|
-
* when the value is a palette color, else the hex, else 'None'. */
|
|
93
|
-
const refLabel = hasRefs
|
|
94
|
-
? paletteRefs.find((r) => normalizeDigits(r.value) === digits)?.label
|
|
95
|
-
: undefined
|
|
96
|
-
const subtitle = digits == null ? 'None' : (refLabel ?? '#' + digits)
|
|
97
|
-
|
|
98
|
-
const swatch = (
|
|
85
|
+
const chip = (size) => (
|
|
99
86
|
<ColorSwatch
|
|
100
|
-
hex={
|
|
101
|
-
size={
|
|
102
|
-
showTransparent={
|
|
87
|
+
hex={showTransparent ? null : displayHex}
|
|
88
|
+
size={size}
|
|
89
|
+
showTransparent={showTransparent}
|
|
103
90
|
transparentTone={transparentTone}
|
|
104
91
|
hoverable={false}
|
|
105
92
|
/>
|
|
106
93
|
)
|
|
107
94
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
95
|
+
/* Swatch cell — popover trigger (refs), lock toggle (onToggleLock), or a
|
|
96
|
+
* plain preview chip. The lock overlay is a SIBLING of the swatch: inside
|
|
97
|
+
* it, ColorSwatch's overflow-hidden radius clip would cut the glyph off. */
|
|
98
|
+
const swatchCell = hasRefs ? (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
ref={popover.refs.setReference}
|
|
102
|
+
{...popover.getReferenceProps()}
|
|
103
|
+
aria-label={`${label ?? 'Color'}: ${subtitle}`}
|
|
104
|
+
className="inline-flex items-center shrink-0"
|
|
112
105
|
>
|
|
113
|
-
{
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
106
|
+
{chip(24)}
|
|
107
|
+
</button>
|
|
108
|
+
) : isLockToggle ? (
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={onToggleLock}
|
|
112
|
+
aria-pressed={locked}
|
|
113
|
+
title={locked ? 'Unlock' : 'Lock'}
|
|
114
|
+
className="group relative inline-flex h-6 w-6 shrink-0"
|
|
115
|
+
>
|
|
116
|
+
{chip('stretch')}
|
|
117
|
+
<span
|
|
118
|
+
aria-hidden="true"
|
|
119
|
+
className={`absolute inset-0 inline-flex items-center justify-center rounded-[2px] transition-opacity bg-fg-absolute-48 text-white pointer-events-none ${
|
|
120
|
+
locked ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
|
121
|
+
}`}
|
|
122
|
+
>
|
|
123
|
+
<Icon name={locked ? 'lock' : 'unlock'} size={12} />
|
|
124
|
+
</span>
|
|
125
|
+
</button>
|
|
126
|
+
) : (
|
|
127
|
+
chip(24)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const labelCls = `kol-helper-12 truncate ${unused ? 'text-meta' : 'text-emphasis'}`
|
|
131
|
+
|
|
132
|
+
const hexInput = (
|
|
133
|
+
<Input
|
|
134
|
+
variant="filled"
|
|
135
|
+
size="sm"
|
|
136
|
+
prefix="#"
|
|
137
|
+
chars={6}
|
|
138
|
+
maxLength={6}
|
|
139
|
+
uppercase
|
|
140
|
+
value={digits}
|
|
141
|
+
onChange={(e) => onChange?.('#' + e.target.value.replace(/^#/, '').toUpperCase())}
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
aria-label={label ? `${label} hex` : 'Hex color'}
|
|
144
|
+
className={unused ? 'opacity-50' : ''}
|
|
145
|
+
/>
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const stateCls = `items-center gap-2 ${disabled ? 'opacity-30 pointer-events-none' : ''} ${className}`
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
{isGrid ? (
|
|
153
|
+
<div
|
|
154
|
+
className={`grid ${stateCls}`}
|
|
155
|
+
style={{ gridTemplateColumns: '24px 1fr 1fr 1fr' }}
|
|
156
|
+
aria-disabled={disabled || undefined}
|
|
121
157
|
>
|
|
122
|
-
{
|
|
123
|
-
|
|
158
|
+
{swatchCell}
|
|
159
|
+
<span className={labelCls}>{labelVisible ? label : ''}</span>
|
|
160
|
+
<span className={`kol-helper-10 truncate ${unused ? 'text-subtle' : 'text-meta'}`}>
|
|
161
|
+
{tokenName}
|
|
162
|
+
</span>
|
|
163
|
+
{hexInput}
|
|
164
|
+
</div>
|
|
124
165
|
) : (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
prefix="#"
|
|
131
|
-
chars={6}
|
|
132
|
-
maxLength={6}
|
|
133
|
-
uppercase
|
|
134
|
-
value={draft}
|
|
135
|
-
onChange={(e) => setDraft(e.target.value.replace(/[^0-9a-fA-F]/g, '').toUpperCase().slice(0, 6))}
|
|
136
|
-
onBlur={commit}
|
|
137
|
-
onKeyDown={(e) => { if (e.key === 'Enter') commit() }}
|
|
138
|
-
disabled={disabled}
|
|
139
|
-
aria-label={label ? `${label} hex` : 'Hex color'}
|
|
140
|
-
/>
|
|
141
|
-
{trailing}
|
|
142
|
-
{onRemove && (
|
|
143
|
-
<button
|
|
144
|
-
type="button"
|
|
145
|
-
onClick={onRemove}
|
|
146
|
-
aria-label="Remove"
|
|
147
|
-
title="Remove"
|
|
148
|
-
className="inline-flex items-center justify-center w-6 h-6 shrink-0 rounded text-meta hover:text-emphasis hover:bg-fg-08 transition-colors"
|
|
149
|
-
>
|
|
150
|
-
<Icon name="x" size={12} />
|
|
151
|
-
</button>
|
|
166
|
+
<div className={`flex ${stateCls}`} aria-disabled={disabled || undefined}>
|
|
167
|
+
{swatchCell}
|
|
168
|
+
{labelVisible && <span className={`${labelCls} flex-1 min-w-0`}>{label}</span>}
|
|
169
|
+
{hexInput}
|
|
170
|
+
</div>
|
|
152
171
|
)}
|
|
153
172
|
{hasRefs && (
|
|
154
173
|
<PopoverPanel
|
|
@@ -159,21 +178,22 @@ export default function ColorInputRow({
|
|
|
159
178
|
style={{ minWidth: 200 }}
|
|
160
179
|
>
|
|
161
180
|
<div className="grid grid-cols-6 gap-1">
|
|
162
|
-
{
|
|
181
|
+
{refs.map((entry) => (
|
|
163
182
|
<ColorSwatch
|
|
164
|
-
key={
|
|
165
|
-
hex={
|
|
183
|
+
key={entry.value}
|
|
184
|
+
hex={entry.hex}
|
|
166
185
|
size="fill"
|
|
167
|
-
selected={
|
|
168
|
-
title={
|
|
169
|
-
onClick={() =>
|
|
186
|
+
selected={entry.value === value}
|
|
187
|
+
title={entry.label}
|
|
188
|
+
onClick={() => {
|
|
189
|
+
onChange?.(entry.value)
|
|
190
|
+
setOpen(false)
|
|
191
|
+
}}
|
|
170
192
|
/>
|
|
171
193
|
))}
|
|
172
194
|
</div>
|
|
173
195
|
</PopoverPanel>
|
|
174
196
|
)}
|
|
175
|
-
|
|
197
|
+
</>
|
|
176
198
|
)
|
|
177
|
-
|
|
178
|
-
return label ? <LabeledControl label={label}>{row}</LabeledControl> : row
|
|
179
199
|
}
|