@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
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,16 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
12
|
-
"./dashboards": "./src/dashboards/index.js",
|
|
13
|
-
"./chess": "./src/chess/index.js"
|
|
10
|
+
".": "./src/index.js"
|
|
14
11
|
},
|
|
15
12
|
"files": [
|
|
16
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
|
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* colorMath — HSL/hex conversion + harmony generation for the KOL color
|
|
3
|
+
* pickers (SpectrumControls, PaletteHarmonyWheel).
|
|
4
|
+
*
|
|
5
|
+
* Plain functions, not a hook — it lives in src/hooks/ because that is the
|
|
6
|
+
* taxonomy's only non-component folder (same rationale as cssVar.js).
|
|
7
|
+
*
|
|
8
|
+
* Two harmony paths:
|
|
9
|
+
* - Deterministic — `harmonyColors(hue, harmony, {saturation, lightness})`
|
|
10
|
+
* / `generateHarmony(baseHex, harmony)`: role-offset hues off a base,
|
|
11
|
+
* no randomness, so dragging the harmony wheel is smooth + repeatable.
|
|
12
|
+
* - Seeded/jittered — `seedHarmony(baseHex, mode)`: slot 0 = the base,
|
|
13
|
+
* slots 1–5 derived via a mode with small random jitter, so consecutive
|
|
14
|
+
* "randomize" clicks produce related-but-different variants around a seed.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/* ── HSL / hex conversion ─────────────────────────────────────────────── */
|
|
18
|
+
|
|
19
|
+
/** '#RRGGBB' → { h: 0–360, s: 0–100, l: 0–100 }. */
|
|
20
|
+
export function hexToHsl(hex) {
|
|
21
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255
|
|
22
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255
|
|
23
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255
|
|
24
|
+
const max = Math.max(r, g, b)
|
|
25
|
+
const min = Math.min(r, g, b)
|
|
26
|
+
let h = 0, s = 0
|
|
27
|
+
const l = (max + min) / 2
|
|
28
|
+
if (max !== min) {
|
|
29
|
+
const d = max - min
|
|
30
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
|
31
|
+
switch (max) {
|
|
32
|
+
case r: h = (g - b) / d + (g < b ? 6 : 0); break
|
|
33
|
+
case g: h = (b - r) / d + 2; break
|
|
34
|
+
case b: h = (r - g) / d + 4; break
|
|
35
|
+
}
|
|
36
|
+
h *= 60
|
|
37
|
+
}
|
|
38
|
+
return { h, s: s * 100, l: l * 100 }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** (h: 0–360, s: 0–100, l: 0–100) → uppercase '#RRGGBB'. */
|
|
42
|
+
export function hslToHex(h, s, l) {
|
|
43
|
+
h = normHue(h)
|
|
44
|
+
s = Math.max(0, Math.min(100, s)) / 100
|
|
45
|
+
l = Math.max(0, Math.min(100, l)) / 100
|
|
46
|
+
const c = (1 - Math.abs(2 * l - 1)) * s
|
|
47
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
|
|
48
|
+
const m = l - c / 2
|
|
49
|
+
let r = 0, g = 0, b = 0
|
|
50
|
+
if (h < 60) { r = c; g = x; b = 0 }
|
|
51
|
+
else if (h < 120) { r = x; g = c; b = 0 }
|
|
52
|
+
else if (h < 180) { r = 0; g = c; b = x }
|
|
53
|
+
else if (h < 240) { r = 0; g = x; b = c }
|
|
54
|
+
else if (h < 300) { r = x; g = 0; b = c }
|
|
55
|
+
else { r = c; g = 0; b = x }
|
|
56
|
+
const toHex = (v) => Math.round((v + m) * 255).toString(16).padStart(2, '0')
|
|
57
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** RGB components (0–255) → uppercase '#RRGGBB'. Shared with the eyedropper. */
|
|
61
|
+
export function rgbToHex(r, g, b) {
|
|
62
|
+
return '#' + [r, g, b]
|
|
63
|
+
.map((n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, '0'))
|
|
64
|
+
.join('')
|
|
65
|
+
.toUpperCase()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Wrap any hue into [0, 360). */
|
|
69
|
+
export const normHue = (h) => ((h % 360) + 360) % 360
|
|
70
|
+
|
|
71
|
+
/* ── Harmony schemes ──────────────────────────────────────────────────── */
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Hue offsets for the five palette roles (Primary / Secondary / Light /
|
|
75
|
+
* Dark / Accent) relative to the base hue. The wheel draws a satellite
|
|
76
|
+
* marker per unique offset; the generators emit one color per role.
|
|
77
|
+
*/
|
|
78
|
+
export const HARMONIES = [
|
|
79
|
+
{ id: 'analogous', label: 'Analogous', roleOffsets: [0, -30, -15, 15, 30] },
|
|
80
|
+
{ id: 'complementary', label: 'Complementary', roleOffsets: [0, 0, 0, 180, 180] },
|
|
81
|
+
{ id: 'split', label: 'Split complementary', roleOffsets: [0, 150, 210, 0, 150] },
|
|
82
|
+
{ id: 'triadic', label: 'Triadic', roleOffsets: [0, 120, 240, 0, 120] },
|
|
83
|
+
{ id: 'tetradic', label: 'Tetradic', roleOffsets: [0, 90, 180, 270, 0] },
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
/** Resolve a harmony id string OR a harmony object to the object. */
|
|
87
|
+
export const harmonyById = (harmony) => {
|
|
88
|
+
if (harmony && typeof harmony === 'object') return harmony
|
|
89
|
+
return HARMONIES.find((h) => h.id === harmony) ?? HARMONIES[0]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Deterministic role colors for a harmony, driven by a hue + fixed S/L.
|
|
94
|
+
* Preserves saturation/lightness across roles (only hue rotates), so the
|
|
95
|
+
* output tracks the wheel's satellite markers exactly.
|
|
96
|
+
*
|
|
97
|
+
* @param {number} hue base hue, 0–360
|
|
98
|
+
* @param {string|object} harmony harmony id or object
|
|
99
|
+
* @param {{saturation?: number, lightness?: number}} opts base S/L (0–100)
|
|
100
|
+
* @returns {string[]} one '#RRGGBB' per role offset
|
|
101
|
+
*/
|
|
102
|
+
export function harmonyColors(hue, harmony, { saturation = 100, lightness = 50 } = {}) {
|
|
103
|
+
const { roleOffsets } = harmonyById(harmony)
|
|
104
|
+
return roleOffsets.map((off) => hslToHex(normHue(hue + off), saturation, lightness))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Deterministic role colors derived from a base hex (S/L taken from the hex).
|
|
109
|
+
* Convenience wrapper over `harmonyColors` for callers holding a color, not
|
|
110
|
+
* a hue.
|
|
111
|
+
*/
|
|
112
|
+
export function generateHarmony(baseHex, harmony) {
|
|
113
|
+
const { h, s, l } = hexToHsl(baseHex)
|
|
114
|
+
return harmonyColors(h, harmony, { saturation: s, lightness: l })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* ── Seeded (jittered) modes ──────────────────────────────────────────── */
|
|
118
|
+
|
|
119
|
+
const jitter = (range) => (Math.random() - 0.5) * 2 * range
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Seed-based generators — slot 0 = the base, slots 1–5 derived via the mode.
|
|
123
|
+
* Each call jitters parameters so repeated "randomize" produces visibly
|
|
124
|
+
* different but harmonically-related variants around the same seed.
|
|
125
|
+
*/
|
|
126
|
+
const SEED_MODES = {
|
|
127
|
+
random: (base) => {
|
|
128
|
+
const { h, s, l } = hexToHsl(base)
|
|
129
|
+
return [
|
|
130
|
+
base,
|
|
131
|
+
hslToHex(normHue(h + Math.random() * 360), s, l),
|
|
132
|
+
hslToHex(normHue(h + Math.random() * 360), s, l),
|
|
133
|
+
hslToHex(h, s, Math.max(10, l - 25 + jitter(10))),
|
|
134
|
+
hslToHex(h, s, Math.min(90, l + 25 + jitter(10))),
|
|
135
|
+
hslToHex(h, s * (0.4 + Math.random() * 0.4), l),
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
monochromatic: (base) => {
|
|
140
|
+
const { h, s, l } = hexToHsl(base)
|
|
141
|
+
const j = 8
|
|
142
|
+
return [
|
|
143
|
+
base,
|
|
144
|
+
hslToHex(h, s, Math.max(10, l - 30 + jitter(j))),
|
|
145
|
+
hslToHex(h, s, Math.max(20, l - 15 + jitter(j))),
|
|
146
|
+
hslToHex(h, s, Math.min(85, l + 15 + jitter(j))),
|
|
147
|
+
hslToHex(h, s, Math.min(95, l + 30 + jitter(j))),
|
|
148
|
+
hslToHex(h, s, Math.min(98, l + 45 + jitter(j))),
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
analogous: (base) => {
|
|
153
|
+
const { h, s, l } = hexToHsl(base)
|
|
154
|
+
return [
|
|
155
|
+
base,
|
|
156
|
+
hslToHex(normHue(h - 30 + jitter(8)), s, l),
|
|
157
|
+
hslToHex(normHue(h - 15 + jitter(8)), s, l),
|
|
158
|
+
hslToHex(normHue(h + 15 + jitter(8)), s, l),
|
|
159
|
+
hslToHex(normHue(h + 30 + jitter(8)), s, l),
|
|
160
|
+
hslToHex(normHue(h + 45 + jitter(8)), s, l),
|
|
161
|
+
]
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
complementary: (base) => {
|
|
165
|
+
const { h, s, l } = hexToHsl(base)
|
|
166
|
+
return [
|
|
167
|
+
base,
|
|
168
|
+
hslToHex(h, s, Math.max(15, l - 25 + jitter(8))),
|
|
169
|
+
hslToHex(h, s, Math.min(85, l + 25 + jitter(8))),
|
|
170
|
+
hslToHex(normHue(h + 180 + jitter(10)), s, l),
|
|
171
|
+
hslToHex(normHue(h + 180 + jitter(10)), s, Math.max(15, l - 15 + jitter(8))),
|
|
172
|
+
hslToHex(normHue(h + 180 + jitter(10)), s, Math.min(85, l + 15 + jitter(8))),
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
triadic: (base) => {
|
|
177
|
+
const { h, s, l } = hexToHsl(base)
|
|
178
|
+
return [
|
|
179
|
+
base,
|
|
180
|
+
hslToHex(normHue(h + 120 + jitter(10)), s, l),
|
|
181
|
+
hslToHex(normHue(h + 240 + jitter(10)), s, l),
|
|
182
|
+
hslToHex(h, s, Math.min(85, l + 20 + jitter(8))),
|
|
183
|
+
hslToHex(normHue(h + 120 + jitter(10)), s, Math.max(15, l - 20 + jitter(8))),
|
|
184
|
+
hslToHex(normHue(h + 240 + jitter(10)), s, Math.min(85, l + 20 + jitter(8))),
|
|
185
|
+
]
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
doubleComplementary: (base) => {
|
|
189
|
+
const { h, s, l } = hexToHsl(base)
|
|
190
|
+
return [
|
|
191
|
+
base,
|
|
192
|
+
hslToHex(normHue(h + 90 + jitter(10)), s, l),
|
|
193
|
+
hslToHex(normHue(h + 180 + jitter(10)), s, l),
|
|
194
|
+
hslToHex(normHue(h + 270 + jitter(10)), s, l),
|
|
195
|
+
hslToHex(h, s, Math.min(85, l + 20 + jitter(8))),
|
|
196
|
+
hslToHex(normHue(h + 180 + jitter(10)), s, Math.max(15, l - 20 + jitter(8))),
|
|
197
|
+
]
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The available seed-mode ids (for a consumer's mode selector). */
|
|
202
|
+
export const SEED_MODE_IDS = Object.keys(SEED_MODES)
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Generate a jittered 6-color palette from a seed hex.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} baseHex the seed color, occupies slot 0
|
|
208
|
+
* @param {string} mode one of SEED_MODE_IDS (falls back to 'random')
|
|
209
|
+
* @returns {string[]} six '#RRGGBB' values
|
|
210
|
+
*/
|
|
211
|
+
export function seedHarmony(baseHex, mode = 'random') {
|
|
212
|
+
const gen = SEED_MODES[mode] ?? SEED_MODES.random
|
|
213
|
+
return gen(baseHex)
|
|
214
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useEyedropper — cross-browser color sampling.
|
|
3
|
+
*
|
|
4
|
+
* Primary path is the native `window.EyeDropper` API (Chromium): one call,
|
|
5
|
+
* samples any pixel on screen, returns an sRGB hex. Where it is unavailable
|
|
6
|
+
* (Firefox, Safari) the hook falls back to an injected `fallback` sampler —
|
|
7
|
+
* e.g. `pickFromCanvasElement`, which samples a pixel from a <canvas> the
|
|
8
|
+
* consumer renders. The DS stays decoupled: it never reaches into an app's
|
|
9
|
+
* canvas/scene — the consumer supplies the fallback.
|
|
10
|
+
*
|
|
11
|
+
* Pairs with the EyedropPick affordance in SwatchControls: gate the button on
|
|
12
|
+
* `supported`, wire the button's `onPick` to `pick`.
|
|
13
|
+
*
|
|
14
|
+
* const { supported, pick } = useEyedropper({ fallback })
|
|
15
|
+
* // supported → render the eyedropper button
|
|
16
|
+
* const hex = await pick() // '#RRGGBB' or null (cancelled / unavailable)
|
|
17
|
+
*/
|
|
18
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
19
|
+
import { rgbToHex } from './colorMath.js'
|
|
20
|
+
|
|
21
|
+
/* Native support probe — read at call time so it is correct even before the
|
|
22
|
+
* post-mount state (below) has settled. */
|
|
23
|
+
function hasNativeEyeDropper() {
|
|
24
|
+
return typeof window !== 'undefined' && 'EyeDropper' in window
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generic canvas-region sampler — the reusable core of a canvas eyedropper.
|
|
29
|
+
* Sets a crosshair cursor on `canvas`, awaits one pointer press on it, reads
|
|
30
|
+
* the pixel under the cursor via getImageData, and resolves the hex. Escape,
|
|
31
|
+
* a press outside the canvas bounds, or an aborted `signal` resolve to null.
|
|
32
|
+
*
|
|
33
|
+
* Backing-store aware: scales client coords by canvas.width/height ÷ CSS box,
|
|
34
|
+
* so a HiDPI or CSS-scaled canvas still samples the right pixel.
|
|
35
|
+
*
|
|
36
|
+
* @param {HTMLCanvasElement} canvas the canvas to sample from
|
|
37
|
+
* @param {{signal?: AbortSignal}} [opts]
|
|
38
|
+
* @returns {Promise<string|null>} uppercase '#RRGGBB' or null
|
|
39
|
+
*/
|
|
40
|
+
export async function pickFromCanvasElement(canvas, { signal } = {}) {
|
|
41
|
+
if (!canvas || typeof canvas.getContext !== 'function') return null
|
|
42
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true })
|
|
43
|
+
if (!ctx) return null
|
|
44
|
+
if (signal?.aborted) return null
|
|
45
|
+
|
|
46
|
+
const prevCursor = canvas.style.cursor
|
|
47
|
+
canvas.style.cursor = 'crosshair'
|
|
48
|
+
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
const cleanup = () => {
|
|
51
|
+
canvas.removeEventListener('pointerdown', onDown, true)
|
|
52
|
+
document.removeEventListener('keydown', onKey, true)
|
|
53
|
+
signal?.removeEventListener('abort', onAbort)
|
|
54
|
+
canvas.style.cursor = prevCursor
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const onDown = (e) => {
|
|
58
|
+
e.preventDefault()
|
|
59
|
+
e.stopPropagation()
|
|
60
|
+
const rect = canvas.getBoundingClientRect()
|
|
61
|
+
const sx = rect.width ? canvas.width / rect.width : 1
|
|
62
|
+
const sy = rect.height ? canvas.height / rect.height : 1
|
|
63
|
+
const vx = Math.floor((e.clientX - rect.left) * sx)
|
|
64
|
+
const vy = Math.floor((e.clientY - rect.top) * sy)
|
|
65
|
+
if (vx < 0 || vy < 0 || vx >= canvas.width || vy >= canvas.height) {
|
|
66
|
+
cleanup(); resolve(null); return
|
|
67
|
+
}
|
|
68
|
+
let data
|
|
69
|
+
try { data = ctx.getImageData(vx, vy, 1, 1).data }
|
|
70
|
+
catch { cleanup(); resolve(null); return } /* tainted canvas */
|
|
71
|
+
cleanup()
|
|
72
|
+
resolve(rgbToHex(data[0], data[1], data[2]))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const onKey = (e) => {
|
|
76
|
+
if (e.key === 'Escape') { e.preventDefault(); cleanup(); resolve(null) }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const onAbort = () => { cleanup(); resolve(null) }
|
|
80
|
+
|
|
81
|
+
/* Capture phase so we beat the consumer's own canvas handlers. */
|
|
82
|
+
canvas.addEventListener('pointerdown', onDown, true)
|
|
83
|
+
document.addEventListener('keydown', onKey, true)
|
|
84
|
+
signal?.addEventListener('abort', onAbort)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {{fallback?: (opts?: {signal?: AbortSignal}) => Promise<string|null>}} [options]
|
|
90
|
+
* fallback — sampler used when the native EyeDropper API is absent. Return
|
|
91
|
+
* a hex or null. Without a fallback, unsupported browsers report
|
|
92
|
+
* `supported: false` and `pick` resolves null.
|
|
93
|
+
* @returns {{supported: boolean, nativeSupported: boolean,
|
|
94
|
+
* pick: (opts?: {signal?: AbortSignal}) => Promise<string|null>}}
|
|
95
|
+
*/
|
|
96
|
+
export function useEyedropper({ fallback } = {}) {
|
|
97
|
+
/* Resolved after mount so SSR / first paint stays stable (starts false,
|
|
98
|
+
* flips true only where the native API exists). */
|
|
99
|
+
const [nativeSupported, setNativeSupported] = useState(false)
|
|
100
|
+
useEffect(() => { setNativeSupported(hasNativeEyeDropper()) }, [])
|
|
101
|
+
|
|
102
|
+
const fallbackRef = useRef(fallback)
|
|
103
|
+
fallbackRef.current = fallback
|
|
104
|
+
|
|
105
|
+
const supported = nativeSupported || !!fallback
|
|
106
|
+
|
|
107
|
+
const pick = useCallback(async ({ signal } = {}) => {
|
|
108
|
+
if (hasNativeEyeDropper()) {
|
|
109
|
+
try {
|
|
110
|
+
const res = await new window.EyeDropper().open(signal ? { signal } : undefined)
|
|
111
|
+
return res?.sRGBHex ? res.sRGBHex.toUpperCase() : null
|
|
112
|
+
} catch {
|
|
113
|
+
return null /* user cancelled (Escape) rejects the promise */
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (fallbackRef.current) return (await fallbackRef.current({ signal })) ?? null
|
|
117
|
+
return null
|
|
118
|
+
}, [])
|
|
119
|
+
|
|
120
|
+
return useMemo(
|
|
121
|
+
() => ({ supported, nativeSupported, pick }),
|
|
122
|
+
[supported, nativeSupported, pick],
|
|
123
|
+
)
|
|
124
|
+
}
|
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'
|
|
@@ -81,6 +76,7 @@ export { default as MediaRow } from './molecules/MediaRow.jsx'
|
|
|
81
76
|
export { MenuItem, MenuDropdownItem, MenuDropdownDivider, MenuDropdownNest } from './molecules/MenuItem.jsx'
|
|
82
77
|
export { MenuPopover } from './molecules/MenuPopover.jsx'
|
|
83
78
|
export { ModalProvider, useModal } from './molecules/Modal.jsx'
|
|
79
|
+
export { default as PaletteHarmonyWheel } from './molecules/PaletteHarmonyWheel.jsx'
|
|
84
80
|
export { default as PropertyInput } from './molecules/PropertyInput.jsx'
|
|
85
81
|
export { default as ShapeDropdown } from './molecules/ShapeDropdown.jsx'
|
|
86
82
|
export { default as ShellDrawer } from './molecules/ShellDrawer.jsx'
|
|
@@ -88,28 +84,20 @@ export { default as ShellSearchOverlay } from './molecules/ShellSearchOverlay.js
|
|
|
88
84
|
export { default as Slider } from './molecules/Slider.jsx'
|
|
89
85
|
export { default as SpecList } from './molecules/SpecList.jsx'
|
|
90
86
|
export { default as SpectrumControls, HueStrip, SBSquare, WheelTriangle } from './molecules/SpectrumControls.jsx'
|
|
87
|
+
export { default as SplitToolButton } from './molecules/SplitToolButton.jsx'
|
|
91
88
|
export { default as SwatchControls, SwatchStack, EyedropPick } from './molecules/SwatchControls.jsx'
|
|
92
89
|
export { default as TabsRow } from './molecules/TabsRow.jsx'
|
|
93
90
|
|
|
94
91
|
// organisms
|
|
95
|
-
/* monorepo sets (P6–P10) — organism members. Foundry members
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
/* monorepo sets (P6–P10) — organism members. Foundry members live in the
|
|
93
|
+
standalone @kolkrabbi/kol-foundry package (with the type-specimen kit +
|
|
94
|
+
live-font effects moved there 2026-07-09) — never re-exported here. */
|
|
98
95
|
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
96
|
export { default as EditorShell } from './organisms/EditorShell.jsx'
|
|
101
97
|
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
98
|
export { default as AsciiCursor } from './organisms/AsciiCursor.jsx'
|
|
110
99
|
export { default as BentoCard } from './organisms/BentoCard.jsx'
|
|
111
100
|
export { default as Carousel } from './organisms/Carousel.jsx'
|
|
112
|
-
export { default as ColorLoader } from './organisms/ColorLoader.jsx'
|
|
113
101
|
export { default as ContentFilters } from './organisms/ContentFilters.jsx'
|
|
114
102
|
export { default as CtaGlobal } from './organisms/CtaGlobal.jsx'
|
|
115
103
|
export { default as ErrorBoundary } from './organisms/ErrorBoundary.jsx'
|
|
@@ -118,6 +106,7 @@ export { default as FeaturedCarousel } from './organisms/FeaturedCarousel.jsx'
|
|
|
118
106
|
export { default as FeaturesCardSection } from './organisms/FeaturesCardSection.jsx'
|
|
119
107
|
export { default as FullBleedHero } from './organisms/FullBleedHero.jsx'
|
|
120
108
|
export { default as LoaderOverlay } from './organisms/LoaderOverlay.jsx'
|
|
109
|
+
export { default as MediaTileGallery } from './organisms/MediaTileGallery.jsx'
|
|
121
110
|
export { default as MediaViewer } from './organisms/MediaViewer.jsx'
|
|
122
111
|
export { default as NewsletterBand } from './organisms/NewsletterBand.jsx'
|
|
123
112
|
export { default as SpectrumGrid } from './organisms/SpectrumGrid.jsx'
|
|
@@ -135,4 +124,12 @@ export { default as useReveal } from './hooks/useReveal.js'
|
|
|
135
124
|
export { default as useScrollSpy } from './hooks/useScrollSpy.js'
|
|
136
125
|
export { default as useTilt } from './hooks/useTilt.js'
|
|
137
126
|
export { default as useAxisAnimation } from './hooks/useAxisAnimation.js'
|
|
127
|
+
export { useEyedropper, pickFromCanvasElement } from './hooks/useEyedropper.js'
|
|
138
128
|
export { resolveCssVar, resolveCssColor, isLight } from './hooks/cssVar.js'
|
|
129
|
+
|
|
130
|
+
// color math (support module — HSL/hex conversion + harmony generation)
|
|
131
|
+
export {
|
|
132
|
+
hexToHsl, hslToHex, rgbToHex, normHue,
|
|
133
|
+
HARMONIES, harmonyById, harmonyColors, generateHarmony,
|
|
134
|
+
SEED_MODE_IDS, seedHarmony,
|
|
135
|
+
} from './hooks/colorMath.js'
|
|
@@ -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
|
+
}
|