@kolkrabbi/kol-component 0.7.0 → 0.9.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 -5
- package/src/atoms/RotaryDial.jsx +35 -21
- package/src/hooks/colorMath.js +214 -0
- package/src/hooks/useEyedropper.js +124 -0
- package/src/index.js +15 -18
- package/src/molecules/ButtonGroup.jsx +45 -0
- package/src/molecules/ColorInputRow.jsx +146 -126
- package/src/molecules/PaletteHarmonyWheel.jsx +192 -0
- 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/chess/ChessPiece.jsx +0 -191
- package/src/chess/apparatus/AlternativeControlsMock.jsx +0 -460
- package/src/chess/apparatus/ChessAnalysisLayout.jsx +0 -24
- package/src/chess/apparatus/ChessBoard.jsx +0 -210
- package/src/chess/apparatus/ChessBoardFullscreen.jsx +0 -49
- package/src/chess/apparatus/ChessBoardWithControls.jsx +0 -41
- package/src/chess/apparatus/ChessBoardWithSidebar.jsx +0 -122
- package/src/chess/apparatus/ChessSidebar.jsx +0 -131
- package/src/chess/apparatus/GameArchiveTable.jsx +0 -493
- package/src/chess/apparatus/GameSelector.jsx +0 -47
- package/src/chess/apparatus/NotationPanel.jsx +0 -70
- package/src/chess/apparatus/PlaybackControls.jsx +0 -52
- package/src/chess/apparatus/VariationTree.jsx +0 -137
- package/src/chess/assets/chess-extra-set/set-1/bishop-1.svg +0 -6
- package/src/chess/assets/chess-extra-set/set-1/bishop.svg +0 -6
- package/src/chess/assets/chess-extra-set/set-1/king-1.svg +0 -9
- package/src/chess/assets/chess-extra-set/set-1/king.svg +0 -7
- package/src/chess/assets/chess-extra-set/set-1/knight-1.svg +0 -8
- package/src/chess/assets/chess-extra-set/set-1/knight.svg +0 -7
- package/src/chess/assets/chess-extra-set/set-1/pawn-1.svg +0 -3
- package/src/chess/assets/chess-extra-set/set-1/pawn.svg +0 -3
- package/src/chess/assets/chess-extra-set/set-1/queen-1.svg +0 -11
- package/src/chess/assets/chess-extra-set/set-1/queen.svg +0 -6
- package/src/chess/assets/chess-extra-set/set-1/rook-1.svg +0 -7
- package/src/chess/assets/chess-extra-set/set-1/rook.svg +0 -11
- package/src/chess/assets/chess-extra-set/set-2/bishop-1.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/bishop.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/king-1.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/king.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/knight-1.svg +0 -5
- package/src/chess/assets/chess-extra-set/set-2/knight.svg +0 -5
- package/src/chess/assets/chess-extra-set/set-2/pawn-1.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/pawn.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/queen-1.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/queen.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/rook-1.svg +0 -5
- package/src/chess/assets/chess-extra-set/set-2/rook.svg +0 -5
- package/src/chess/assets/chess-extra-set/set-3/bishop-1.svg +0 -10
- package/src/chess/assets/chess-extra-set/set-3/bishop.svg +0 -3
- package/src/chess/assets/chess-extra-set/set-3/king-1.svg +0 -12
- package/src/chess/assets/chess-extra-set/set-3/king.svg +0 -3
- package/src/chess/assets/chess-extra-set/set-3/knight-1.svg +0 -3
- package/src/chess/assets/chess-extra-set/set-3/knight.svg +0 -10
- package/src/chess/assets/chess-extra-set/set-3/pawn-1.svg +0 -10
- package/src/chess/assets/chess-extra-set/set-3/pawn.svg +0 -10
- package/src/chess/assets/chess-extra-set/set-3/queen-1.svg +0 -10
- package/src/chess/assets/chess-extra-set/set-3/queen.svg +0 -3
- package/src/chess/assets/chess-extra-set/set-3/rook-1.svg +0 -3
- package/src/chess/assets/chess-extra-set/set-3/rook.svg +0 -11
- package/src/chess/assets/chess-vector-set/ABoard.svg +0 -164
- package/src/chess/assets/chess-vector-set/BBiskup.svg +0 -12
- package/src/chess/assets/chess-vector-set/BKing.svg +0 -15
- package/src/chess/assets/chess-vector-set/BKnight.svg +0 -13
- package/src/chess/assets/chess-vector-set/BPawn.svg +0 -7
- package/src/chess/assets/chess-vector-set/BQueen.svg +0 -6
- package/src/chess/assets/chess-vector-set/BRook.svg +0 -12
- package/src/chess/assets/chess-vector-set/WBiskup.svg +0 -14
- package/src/chess/assets/chess-vector-set/WKing.svg +0 -13
- package/src/chess/assets/chess-vector-set/WKnight.svg +0 -10
- package/src/chess/assets/chess-vector-set/WPawn.svg +0 -8
- package/src/chess/assets/chess-vector-set/WQueen.svg +0 -17
- package/src/chess/assets/chess-vector-set/WRook.svg +0 -13
- package/src/chess/context/ChessControlsContext.jsx +0 -379
- package/src/chess/dashboards/ChessHero.jsx +0 -303
- package/src/chess/index.js +0 -32
- package/src/chess/utils/parsePgnTree.js +0 -97
- package/src/dashboards/cards/DashAlertCard.jsx +0 -44
- package/src/dashboards/cards/DashChartCard.jsx +0 -44
- package/src/dashboards/cards/DashFeaturedCard.jsx +0 -42
- package/src/dashboards/cards/DashListCard.jsx +0 -87
- package/src/dashboards/cards/DashMetricCard.jsx +0 -59
- package/src/dashboards/cards/DashSlotCard.jsx +0 -45
- package/src/dashboards/cards/DashStackedBarCard.jsx +0 -70
- package/src/dashboards/cards/DashTableCard.jsx +0 -31
- package/src/dashboards/cards/_shared/CardHeader.jsx +0 -15
- package/src/dashboards/cards/_shared/LegendRow.jsx +0 -25
- package/src/dashboards/charts/Candlestick.jsx +0 -79
- package/src/dashboards/charts/DonutChart.jsx +0 -87
- package/src/dashboards/charts/Heatmap.jsx +0 -79
- package/src/dashboards/charts/Histogram.jsx +0 -56
- package/src/dashboards/charts/LineChart.jsx +0 -157
- package/src/dashboards/charts/ScatterPlot.jsx +0 -93
- package/src/dashboards/charts/Sparkline.jsx +0 -63
- package/src/dashboards/index.js +0 -28
- package/src/dashboards/layout/DashboardGrid.jsx +0 -30
- package/src/dashboards/layout/GridCard.jsx +0 -44
- package/src/dashboards/shared/DashTooltip.jsx +0 -6
- package/src/dashboards/shared/formatMetric.js +0 -26
- package/src/dashboards/shared/useChartTooltip.js +0 -25
- package/src/dashboards/shared/useCountUp.js +0 -107
- 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
|
@@ -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
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { HARMONIES, harmonyById, harmonyColors, normHue } from '../hooks/colorMath.js'
|
|
3
|
+
|
|
4
|
+
/* taxonomy-ok: a pure-canvas hue + harmony picker in the SpectrumControls
|
|
5
|
+
* color-picker family. It nests no KOL component — its only import is the
|
|
6
|
+
* colorMath support module in hooks/ — but belongs beside its siblings
|
|
7
|
+
* SpectrumControls / SwatchControls, not down in atoms. */
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* PaletteHarmonyWheel — a hue ring with a draggable base-hue handle and
|
|
11
|
+
* satellite markers at the active harmony's scheme hues. Dragging (or the
|
|
12
|
+
* arrow keys) rotates the base hue; every change emits the full set of
|
|
13
|
+
* harmony colors so the caller never re-derives them.
|
|
14
|
+
*
|
|
15
|
+
* The scheme table is DATA-INJECTED (`harmonies`, defaulting to the shared
|
|
16
|
+
* HARMONIES); saturation/lightness of the generated colors are injected too,
|
|
17
|
+
* so this stays a pure hue picker — the caller owns S/L.
|
|
18
|
+
*
|
|
19
|
+
* Controlled on `hue`; `onChange({ hue, colors })` fires on press, drag, and
|
|
20
|
+
* arrow keys. `colors` is one hex per role offset of the active harmony
|
|
21
|
+
* (see colorMath.harmonyColors), matching the satellite markers 1:1.
|
|
22
|
+
*
|
|
23
|
+
* The ring hues, marker outlines and handle halo are literal color math
|
|
24
|
+
* (hsl / #FFFFFF / rgba) on purpose — a spectrum is not themeable, and the
|
|
25
|
+
* markers sit on fully-saturated ring hues, not on the surface (same
|
|
26
|
+
* rationale as SpectrumControls). No token belongs here.
|
|
27
|
+
*
|
|
28
|
+
* @param {number} size px, square (default 248)
|
|
29
|
+
* @param {number} hue controlled base hue, 0–360
|
|
30
|
+
* @param {string|object} harmony active harmony id or object (default 'analogous')
|
|
31
|
+
* @param {number} saturation base saturation for emitted colors, 0–100 (default 100)
|
|
32
|
+
* @param {number} lightness base lightness for emitted colors, 0–100 (default 50)
|
|
33
|
+
* @param {Array} harmonies injectable scheme table (default HARMONIES)
|
|
34
|
+
* @param {Function} onChange ({ hue, colors }) => void
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/* Marker outline — white for contrast against the fully-saturated ring hues
|
|
38
|
+
* (theme-independent: the wheel's colours, not the surface, sit behind it). */
|
|
39
|
+
const MARKER_STROKE = '#FFFFFF'
|
|
40
|
+
|
|
41
|
+
export default function PaletteHarmonyWheel({
|
|
42
|
+
size = 248,
|
|
43
|
+
hue,
|
|
44
|
+
harmony = 'analogous',
|
|
45
|
+
saturation = 100,
|
|
46
|
+
lightness = 50,
|
|
47
|
+
harmonies = HARMONIES,
|
|
48
|
+
onChange,
|
|
49
|
+
}) {
|
|
50
|
+
const canvasRef = useRef(null)
|
|
51
|
+
const draggingRef = useRef(false)
|
|
52
|
+
const emitRef = useRef(null)
|
|
53
|
+
|
|
54
|
+
const active = harmonyById(
|
|
55
|
+
typeof harmony === 'string'
|
|
56
|
+
? (harmonies.find((h) => h.id === harmony) ?? harmonies[0])
|
|
57
|
+
: harmony,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
/* Emit next hue + its harmony colors. Held in a ref so the pointer/key
|
|
61
|
+
* handlers stay stable while always seeing the latest props. */
|
|
62
|
+
emitRef.current = (nextHue) => {
|
|
63
|
+
const h = normHue(nextHue)
|
|
64
|
+
onChange?.({ hue: h, colors: harmonyColors(h, active, { saturation, lightness }) })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const outerR = size / 2 - 8
|
|
68
|
+
const innerR = outerR - 22
|
|
69
|
+
const midR = (outerR + innerR) / 2
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const canvas = canvasRef.current
|
|
73
|
+
if (!canvas) return
|
|
74
|
+
const dpr = window.devicePixelRatio || 1
|
|
75
|
+
canvas.width = size * dpr
|
|
76
|
+
canvas.height = size * dpr
|
|
77
|
+
const ctx = canvas.getContext('2d')
|
|
78
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
79
|
+
ctx.clearRect(0, 0, size, size)
|
|
80
|
+
const cx = size / 2
|
|
81
|
+
const cy = size / 2
|
|
82
|
+
|
|
83
|
+
/* Angle mapping: hue 0 at 12 o'clock, clockwise. */
|
|
84
|
+
const angleFor = (h) => ((h - 90) * Math.PI) / 180
|
|
85
|
+
|
|
86
|
+
/* Hue ring — 360 thin annulus wedges (1.5° overlap hides seams). */
|
|
87
|
+
for (let a = 0; a < 360; a++) {
|
|
88
|
+
const a0 = angleFor(a)
|
|
89
|
+
const a1 = angleFor(a + 1.5)
|
|
90
|
+
ctx.beginPath()
|
|
91
|
+
ctx.arc(cx, cy, outerR, a0, a1)
|
|
92
|
+
ctx.arc(cx, cy, innerR, a1, a0, true)
|
|
93
|
+
ctx.closePath()
|
|
94
|
+
ctx.fillStyle = `hsl(${a}, 100%, 50%)`
|
|
95
|
+
ctx.fill()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const baseHue = normHue(hue)
|
|
99
|
+
const offsets = [...new Set((active?.roleOffsets ?? [0]).map(normHue))]
|
|
100
|
+
|
|
101
|
+
/* Spokes + satellite markers at the scheme hues. */
|
|
102
|
+
for (const off of offsets) {
|
|
103
|
+
const h = normHue(baseHue + off)
|
|
104
|
+
const a = angleFor(h)
|
|
105
|
+
ctx.beginPath()
|
|
106
|
+
ctx.moveTo(cx, cy)
|
|
107
|
+
ctx.lineTo(cx + Math.cos(a) * innerR, cy + Math.sin(a) * innerR)
|
|
108
|
+
ctx.strokeStyle = 'rgba(128, 128, 128, 0.4)'
|
|
109
|
+
ctx.lineWidth = 1
|
|
110
|
+
ctx.stroke()
|
|
111
|
+
if (off === 0) continue /* base hue gets the big handle below */
|
|
112
|
+
const x = cx + Math.cos(a) * midR
|
|
113
|
+
const y = cy + Math.sin(a) * midR
|
|
114
|
+
ctx.beginPath()
|
|
115
|
+
ctx.arc(x, y, 5, 0, Math.PI * 2)
|
|
116
|
+
ctx.fillStyle = `hsl(${h}, 100%, 50%)`
|
|
117
|
+
ctx.fill()
|
|
118
|
+
ctx.strokeStyle = MARKER_STROKE
|
|
119
|
+
ctx.lineWidth = 1.5
|
|
120
|
+
ctx.stroke()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Base-hue handle. */
|
|
124
|
+
const ba = angleFor(baseHue)
|
|
125
|
+
const bx = cx + Math.cos(ba) * midR
|
|
126
|
+
const by = cy + Math.sin(ba) * midR
|
|
127
|
+
ctx.beginPath()
|
|
128
|
+
ctx.arc(bx, by, 9, 0, Math.PI * 2)
|
|
129
|
+
ctx.fillStyle = `hsl(${baseHue}, 100%, 50%)`
|
|
130
|
+
ctx.fill()
|
|
131
|
+
ctx.strokeStyle = MARKER_STROKE
|
|
132
|
+
ctx.lineWidth = 2
|
|
133
|
+
ctx.stroke()
|
|
134
|
+
ctx.beginPath()
|
|
135
|
+
ctx.arc(bx, by, 10.5, 0, Math.PI * 2)
|
|
136
|
+
ctx.strokeStyle = 'rgba(0, 0, 0, 0.35)'
|
|
137
|
+
ctx.lineWidth = 1
|
|
138
|
+
ctx.stroke()
|
|
139
|
+
}, [size, hue, active, outerR, innerR, midR])
|
|
140
|
+
|
|
141
|
+
const pointFromEvent = (e) => {
|
|
142
|
+
const rect = canvasRef.current.getBoundingClientRect()
|
|
143
|
+
const dx = e.clientX - rect.left - rect.width / 2
|
|
144
|
+
const dy = e.clientY - rect.top - rect.height / 2
|
|
145
|
+
return { dist: Math.hypot(dx, dy), hue: normHue((Math.atan2(dy, dx) * 180) / Math.PI + 90) }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const onPointerDown = (e) => {
|
|
149
|
+
const { dist, hue: h } = pointFromEvent(e)
|
|
150
|
+
if (dist < innerR - 14 || dist > outerR + 10) return
|
|
151
|
+
draggingRef.current = true
|
|
152
|
+
e.currentTarget.setPointerCapture(e.pointerId)
|
|
153
|
+
emitRef.current?.(h)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const onPointerMove = (e) => {
|
|
157
|
+
if (!draggingRef.current) return
|
|
158
|
+
emitRef.current?.(pointFromEvent(e).hue)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const endDrag = () => { draggingRef.current = false }
|
|
162
|
+
|
|
163
|
+
const onKeyDown = (e) => {
|
|
164
|
+
const step = e.shiftKey ? 15 : 3
|
|
165
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
|
|
166
|
+
e.preventDefault()
|
|
167
|
+
emitRef.current?.(normHue(hue + step))
|
|
168
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
|
|
169
|
+
e.preventDefault()
|
|
170
|
+
emitRef.current?.(normHue(hue - step))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<canvas
|
|
176
|
+
ref={canvasRef}
|
|
177
|
+
role="slider"
|
|
178
|
+
aria-label="Base hue"
|
|
179
|
+
aria-valuemin={0}
|
|
180
|
+
aria-valuemax={359}
|
|
181
|
+
aria-valuenow={Math.round(normHue(hue))}
|
|
182
|
+
tabIndex={0}
|
|
183
|
+
className="cursor-pointer touch-none"
|
|
184
|
+
style={{ width: size, height: size }}
|
|
185
|
+
onPointerDown={onPointerDown}
|
|
186
|
+
onPointerMove={onPointerMove}
|
|
187
|
+
onPointerUp={endDrag}
|
|
188
|
+
onPointerCancel={endDrag}
|
|
189
|
+
onKeyDown={onKeyDown}
|
|
190
|
+
/>
|
|
191
|
+
)
|
|
192
|
+
}
|
package/src/molecules/Slider.jsx
CHANGED
|
@@ -2,28 +2,35 @@ import { useEffect, useMemo, useState } from 'react'
|
|
|
2
2
|
import Input from '../atoms/Input.jsx'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Slider — range slider with label and an editable value readout.
|
|
5
|
+
* Slider — range slider with label and an editable value readout. The LINEAR
|
|
6
|
+
* VARIANT of RotaryDial (atoms/RotaryDial.jsx): both implement the shared
|
|
7
|
+
* value-control contract — `value` / `min` / `max` / `step` /
|
|
8
|
+
* `onChange(next: number)` / `label` / `size` / `disabled` / `formatValue`.
|
|
9
|
+
* Controlled; onChange always fires with the plain number.
|
|
6
10
|
*
|
|
7
11
|
* One bare inline row: label · track · editable readout. (Bordered
|
|
8
12
|
* `default` and chip `subtle` variants retired 2026-07-08 — minimal is
|
|
9
13
|
* the only slider.)
|
|
10
14
|
*
|
|
11
15
|
* The value readout is an editable <Input> (type a value, commit on
|
|
12
|
-
* blur / Enter, revert on Escape)
|
|
13
|
-
*
|
|
14
|
-
*
|
|
16
|
+
* blur / Enter, revert on Escape) — a Slider-only extra; RotaryDial's
|
|
17
|
+
* readout is display-only (atoms nest no KOL component). Track color is
|
|
18
|
+
* exposed as the `--kol-slider-track` CSS variable on `.slider-black`;
|
|
19
|
+
* override per-instance via style={{ '--kol-slider-track': '...' }}.
|
|
15
20
|
*
|
|
16
21
|
* @param {Object} props
|
|
17
22
|
* @param {string} props.label - Slider label text
|
|
18
|
-
* @param {number} props.min - Minimum value
|
|
19
|
-
* @param {number} props.max - Maximum value
|
|
20
|
-
* @param {number} props.value -
|
|
21
|
-
* @param {Function} props.onChange -
|
|
23
|
+
* @param {number} props.min - Minimum value (default: 0)
|
|
24
|
+
* @param {number} props.max - Maximum value (default: 100)
|
|
25
|
+
* @param {number} props.value - Controlled value within min–max
|
|
26
|
+
* @param {Function} props.onChange - (next: number) => void change handler
|
|
27
|
+
* @param {number} props.step - Slider step increment (default: 1)
|
|
28
|
+
* @param {number} props.size - Track length in px (mirrors RotaryDial's dial px size); unset = fluid flex-1 track
|
|
29
|
+
* @param {boolean} props.disabled - Disables track + readout and dims the control (default: false)
|
|
30
|
+
* @param {Function} props.formatValue - Optional formatter for displayed value; RotaryDial's default readout is `${value}%`
|
|
22
31
|
* @param {string} props.className - Additional wrapper classes
|
|
23
32
|
* @param {number} props.displayWidth - Width of the value readout, in characters (default: 6)
|
|
24
33
|
* @param {string} props.fontSize - Font size for label/value (e.g., '11px')
|
|
25
|
-
* @param {number} props.step - Slider step increment (default: 1)
|
|
26
|
-
* @param {Function} props.formatValue - Optional formatter for displayed value
|
|
27
34
|
*/
|
|
28
35
|
const Slider = ({
|
|
29
36
|
label,
|
|
@@ -31,11 +38,13 @@ const Slider = ({
|
|
|
31
38
|
max = 100,
|
|
32
39
|
value = 0,
|
|
33
40
|
onChange,
|
|
41
|
+
step = 1,
|
|
42
|
+
size,
|
|
43
|
+
disabled = false,
|
|
44
|
+
formatValue,
|
|
34
45
|
className = '',
|
|
35
46
|
displayWidth = 6,
|
|
36
47
|
fontSize,
|
|
37
|
-
step = 1,
|
|
38
|
-
formatValue
|
|
39
48
|
}) => {
|
|
40
49
|
const handleChange = (e) => {
|
|
41
50
|
if (onChange) {
|
|
@@ -86,7 +95,10 @@ const Slider = ({
|
|
|
86
95
|
return (
|
|
87
96
|
<div className={`control-slider gap-3 shadow-none ${className}`}>
|
|
88
97
|
{label && (
|
|
89
|
-
<label
|
|
98
|
+
<label
|
|
99
|
+
className={`kol-helper-12 whitespace-nowrap shrink-0 w-fit ${disabled ? 'opacity-50' : ''}`}
|
|
100
|
+
style={fontSize ? { fontSize } : undefined}
|
|
101
|
+
>
|
|
90
102
|
{label}
|
|
91
103
|
</label>
|
|
92
104
|
)}
|
|
@@ -97,7 +109,9 @@ const Slider = ({
|
|
|
97
109
|
step={step}
|
|
98
110
|
value={value}
|
|
99
111
|
onChange={handleChange}
|
|
100
|
-
|
|
112
|
+
disabled={disabled}
|
|
113
|
+
className={`slider-black cursor-pointer disabled:cursor-default disabled:opacity-50 ${size ? 'flex-none' : 'flex-1 w-full'}`}
|
|
114
|
+
style={size ? { width: size } : undefined}
|
|
101
115
|
/>
|
|
102
116
|
<Input
|
|
103
117
|
type="text"
|
|
@@ -106,6 +120,7 @@ const Slider = ({
|
|
|
106
120
|
size="sm"
|
|
107
121
|
chars={displayWidth}
|
|
108
122
|
value={draft}
|
|
123
|
+
disabled={disabled}
|
|
109
124
|
onFocus={(e) => { setEditing(true); e.target.select() }}
|
|
110
125
|
onChange={(e) => setDraft(e.target.value)}
|
|
111
126
|
onBlur={commit}
|