@kolkrabbi/kol-component 0.103.0 → 0.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.104.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",
|
package/src/index.js
CHANGED
|
@@ -137,7 +137,7 @@ export { default as LoaderOverlay } from './utilities/LoaderOverlay.jsx'
|
|
|
137
137
|
export { default as MediaLibrary, MediaLibraryProvider, useMediaLibrary, MediaPicker, MediaBrowser } from './organisms/MediaLibrary.jsx'
|
|
138
138
|
export { default as MediaTileGallery } from './organisms/MediaTileGallery.jsx'
|
|
139
139
|
export { default as MediaViewer } from './organisms/MediaViewer.jsx'
|
|
140
|
-
export { default as SettingsPanel, SettingsSection, SettingsRow, SettingsSwitch, SettingsChoice, SettingsChipRow, SettingsFooter } from './organisms/SettingsPanel.jsx'
|
|
140
|
+
export { default as SettingsPanel, SettingsSection, SettingsRow, SettingsSwitch, SettingsChoice, SettingsMulti, SettingsChipRow, SettingsFooter } from './organisms/SettingsPanel.jsx'
|
|
141
141
|
export { default as SectionNewsletter } from './organisms/SectionNewsletter.jsx'
|
|
142
142
|
export { default as NewsletterBand } from './organisms/NewsletterBand.jsx'
|
|
143
143
|
export { default as ColumnBrowser } from './organisms/ColumnBrowser.jsx'
|
|
@@ -43,6 +43,9 @@ export default function ShellDrawer({
|
|
|
43
43
|
backdrop = true,
|
|
44
44
|
children,
|
|
45
45
|
className = '',
|
|
46
|
+
/* SettingsPanelApproved (2026-08-27): the settings drawer has neither */
|
|
47
|
+
edge = true,
|
|
48
|
+
shadow = true,
|
|
46
49
|
}) {
|
|
47
50
|
const reduced = usePrefersReducedMotion()
|
|
48
51
|
const panelRef = useRef(null)
|
|
@@ -149,8 +152,10 @@ export default function ShellDrawer({
|
|
|
149
152
|
role="dialog"
|
|
150
153
|
aria-modal="true"
|
|
151
154
|
tabIndex={-1}
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
/* `edge` / `shadow` (SettingsPanelApproved, 2026-08-27): the approved settings
|
|
156
|
+
* drawer has neither — the sheet meets the page flat */
|
|
157
|
+
className={`fixed inset-y-0 z-[200] flex max-w-full flex-col bg-surface-primary px-4 py-4 outline-none md:px-5 lg:px-6 ${backdrop && shadow ? 'shadow-2xl' : ''} ${
|
|
158
|
+
side === 'right' ? `right-0 ${edge ? 'border-l' : ''}` : `left-0 ${edge ? 'border-r' : ''}`
|
|
154
159
|
} border-oq-08 ${width == null ? 'w-full' : ''} ${motionPanel} ${className}`}
|
|
155
160
|
style={width != null ? { width: typeof width === 'number' ? `${width}px` : width } : undefined}
|
|
156
161
|
>
|
|
@@ -163,7 +163,17 @@ export default function ColumnBrowser({
|
|
|
163
163
|
* a Finder-style breadcrumb — fired whenever it changes; a folder pick or an
|
|
164
164
|
* outside prefix change fires null */
|
|
165
165
|
const pick = (o) => { setPicked(o); onPick?.(o) }
|
|
166
|
-
|
|
166
|
+
/* picking a FILE in a column that has an open folder closes that folder
|
|
167
|
+
* (Finder: one highlight per column) — the prefix collapses to the file's
|
|
168
|
+
* level, and the cursor STAYS on the file instead of re-seeding
|
|
169
|
+
* (SettingsPanelApproved §4, kol-r2b2 2026-08-27) */
|
|
170
|
+
const keepCursor = useRef(false)
|
|
171
|
+
const pickFile = (level, o) => {
|
|
172
|
+
pick(o)
|
|
173
|
+
if (prefix !== level) { keepCursor.current = true; pickedByCollapse.current = true; onPrefix(level) }
|
|
174
|
+
}
|
|
175
|
+
const pickedByCollapse = useRef(false)
|
|
176
|
+
useEffect(() => { if (pickedByCollapse.current) { pickedByCollapse.current = false; return } if (picked) pick(null) }, [prefix]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
167
177
|
const levelsOf = (pfx) => {
|
|
168
178
|
const out = ['']
|
|
169
179
|
if (pfx) {
|
|
@@ -190,7 +200,10 @@ export default function ColumnBrowser({
|
|
|
190
200
|
return { col, idx: Math.max(0, idx) }
|
|
191
201
|
}
|
|
192
202
|
const [cursor, setCursor] = useState(() => seedCursor(prefix))
|
|
193
|
-
useEffect(() => {
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (keepCursor.current) { keepCursor.current = false; return }
|
|
205
|
+
setCursor(seedCursor(prefix))
|
|
206
|
+
}, [prefix, objects.length]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
194
207
|
/* the cursor is NOT drawn until the keyboard is used (ColumnBrowserCursorStart,
|
|
195
208
|
* kol-r2b2 2026-08-27): at rest it sat on row 0 beside the open folder and
|
|
196
209
|
* read as a second selection. Arrows arm it; a click seeds it. */
|
|
@@ -200,7 +213,7 @@ export default function ColumnBrowser({
|
|
|
200
213
|
const land = (level, item) => {
|
|
201
214
|
if (!item) return
|
|
202
215
|
if (item.type === 'folder') { pick(null); onPrefix(level + item.name) }
|
|
203
|
-
else
|
|
216
|
+
else pickFile(level, item.o)
|
|
204
217
|
}
|
|
205
218
|
|
|
206
219
|
const onKeyDown = (e) => {
|
|
@@ -309,7 +322,7 @@ export default function ColumnBrowser({
|
|
|
309
322
|
muted={shown?.key !== o.key}
|
|
310
323
|
active={shown?.key === o.key}
|
|
311
324
|
cursor={cursorActive && cursor.col === k && cursor.idx === folders.length + i}
|
|
312
|
-
onClick={() => { setCursor({ col: k, idx: folders.length + i }); setCursorActive(true); rootRef.current?.focus();
|
|
325
|
+
onClick={() => { setCursor({ col: k, idx: folders.length + i }); setCursorActive(true); rootRef.current?.focus(); pickFile(level, o) }}
|
|
313
326
|
/>
|
|
314
327
|
))}
|
|
315
328
|
{folders.length === 0 && files.length === 0 && (
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Button from '../atoms/Button.jsx'
|
|
2
2
|
import Dropdown from '../molecules/Dropdown.jsx'
|
|
3
|
-
import
|
|
3
|
+
import Divider from '../atoms/Divider.jsx'
|
|
4
|
+
import IconFrame from '../atoms/IconFrame.jsx'
|
|
5
|
+
import LabeledControl from '../molecules/LabeledControl.jsx'
|
|
4
6
|
import ToggleSwitch from '../atoms/ToggleSwitch.jsx'
|
|
5
7
|
import ShellDrawer from '../molecules/ShellDrawer.jsx'
|
|
6
8
|
import FullscreenOverlay from '../utilities/FullscreenOverlay.jsx'
|
|
@@ -35,8 +37,8 @@ import FullscreenOverlay from '../utilities/FullscreenOverlay.jsx'
|
|
|
35
37
|
* @param {'drawer'|'overlay'} variant presentation
|
|
36
38
|
* @param {number|string} width panel width (default 380)
|
|
37
39
|
* @param {string} title header line 1
|
|
38
|
-
* @param {string} subtitle header line 2
|
|
39
|
-
* @param {string} intro
|
|
40
|
+
* @param {string} subtitle header line 2 (the approved drawer passes none)
|
|
41
|
+
* @param {string} intro a note under the header (the approved drawer passes none)
|
|
40
42
|
* @param {Function} onClose × · scrim · Escape
|
|
41
43
|
* @param {ReactNode} footer the foot slot — `SettingsFooter` is the default shape
|
|
42
44
|
* @param {ReactNode} children the sections
|
|
@@ -90,55 +92,53 @@ export default function SettingsPanel({
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
return (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
/* THE APPROVED DRAWER (SettingsPanelApproved, kol-r2b2 2026-08-27 — user: "LOCK
|
|
96
|
+
THIS"): no edge, no shadow, a Divider under the header, the sections. */
|
|
97
|
+
<ShellDrawer open={open} onClose={onClose} side="right" width={width} header={header} className={className} edge={false} shadow={false}>
|
|
98
|
+
<div className="flex flex-col gap-4"><Divider />{body}</div>
|
|
95
99
|
</ShellDrawer>
|
|
96
100
|
)
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
/**
|
|
100
|
-
* SettingsRow —
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
104
|
+
* SettingsRow — a `LabeledControl inline` (SettingsPanelApproved, 2026-08-27):
|
|
105
|
+
* uppercase label (`kol-helper-10` tracked, meta ink) in a 160px column, the
|
|
106
|
+
* control fills the rest. A switch sits at the far right (`align="end"`, the
|
|
107
|
+
* default); a dropdown fills the row (`align="fill"`). No hint sentences on the
|
|
108
|
+
* page — `hint` rides the control's `title`.
|
|
105
109
|
*/
|
|
106
|
-
export function SettingsRow({ label, hint, children }) {
|
|
110
|
+
export function SettingsRow({ label, hint, align = 'end', labelWidth = 160, children }) {
|
|
107
111
|
return (
|
|
108
|
-
<
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
{hint && <span className="kol-mono-12 text-meta">{hint}</span>}
|
|
112
|
-
</div>
|
|
113
|
-
<div className="shrink-0">{children}</div>
|
|
114
|
-
</div>
|
|
112
|
+
<LabeledControl inline label={typeof label === 'string' ? label.toUpperCase() : label} labelWidth={labelWidth}>
|
|
113
|
+
<span title={hint} className={`inline-flex w-full ${align === 'fill' ? '' : 'justify-end'}`.trim()}>{children}</span>
|
|
114
|
+
</LabeledControl>
|
|
115
115
|
)
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/** SettingsSwitch — the row's on/off control: the DS ToggleSwitch, bare, sm.
|
|
119
119
|
* `disabledHint` rides `title` so a switch that cannot act says why. */
|
|
120
|
-
export function SettingsSwitch({ on = false, onChange, disabled = false, disabledHint, label }) {
|
|
120
|
+
export function SettingsSwitch({ on = false, onChange, disabled = false, disabledHint, label, title }) {
|
|
121
121
|
return (
|
|
122
122
|
<ToggleSwitch
|
|
123
123
|
size="sm"
|
|
124
124
|
checked={on}
|
|
125
125
|
onChange={onChange}
|
|
126
126
|
disabled={disabled}
|
|
127
|
-
title={disabled ? disabledHint : undefined}
|
|
127
|
+
title={title ?? (disabled ? disabledHint : undefined)}
|
|
128
128
|
aria-label={label}
|
|
129
129
|
/>
|
|
130
130
|
)
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
/** SettingsSection — a section of the panel
|
|
134
|
-
* `
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
export function SettingsSection({ label, divided = false, children, className = '' }) {
|
|
133
|
+
/** SettingsSection — a section of the panel (SettingsPanelApproved, 2026-08-27):
|
|
134
|
+
* the EYEBROW (`kol-eyebrow text-fg-80`) standing apart from the rows (gap-3),
|
|
135
|
+
* the rows in their own stack — `rowGap` 1 for switch rows (24 tall already),
|
|
136
|
+
* 2 for dropdown rows. `divided` = a hairline above (between sections). */
|
|
137
|
+
export function SettingsSection({ label, divided = false, rowGap = 2, children, className = '' }) {
|
|
138
138
|
return (
|
|
139
139
|
<div className={`flex flex-col gap-3 ${divided ? 'kol-section--divided' : ''} ${className}`.replace(/\s+/g, ' ').trim()}>
|
|
140
|
-
{label && <
|
|
141
|
-
{children}
|
|
140
|
+
{label && <p className="kol-eyebrow text-fg-80">{label}</p>}
|
|
141
|
+
<div className={`flex flex-col ${rowGap === 1 ? 'gap-1' : 'gap-2'}`}>{children}</div>
|
|
142
142
|
</div>
|
|
143
143
|
)
|
|
144
144
|
}
|
|
@@ -149,7 +149,7 @@ export function SettingsSection({ label, divided = false, children, className =
|
|
|
149
149
|
* width is the call site's (`className="w-40"`). */
|
|
150
150
|
export function SettingsChoice({ options = [], value, onChange, ariaLabel, className = '' }) {
|
|
151
151
|
const opts = options.map((o) => (o != null && typeof o === 'object' ? o : { value: o, label: String(o) }))
|
|
152
|
-
return <Dropdown size="sm" variant="primary" value={value} onChange={onChange} options={opts} className={className} aria-label={ariaLabel} />
|
|
152
|
+
return <Dropdown size="sm" variant="primary" value={value} onChange={onChange} options={opts} className={`w-full ${className}`.trim()} aria-label={ariaLabel} />
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
/* THE CONTROL CHIP (SettingsPanelCompliance, user 2026-08-27: "WROOOONG" on the
|
|
@@ -160,6 +160,24 @@ export function SettingsChoice({ options = [], value, onChange, ariaLabel, class
|
|
|
160
160
|
export const CHIP_CLS = 'kol-control kol-control-sm kol-mono-12'
|
|
161
161
|
export const chipCls = (on) => `${CHIP_CLS} ${on ? 'kol-control--filled' : 'text-meta hover:text-emphasis'}`
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* SettingsMulti — a many-of-N control as ONE Dropdown (SettingsPanelApproved,
|
|
165
|
+
* 2026-08-27 — the kinds row): the trigger reads `N of M <noun>`, every entry
|
|
166
|
+
* toggles and `✓` marks the ones that are on. No grouping.
|
|
167
|
+
* @param {Array} options [{ value, label }]
|
|
168
|
+
* @param {Array|Set} selected
|
|
169
|
+
* @param {Function} onToggle (value) => void
|
|
170
|
+
* @param {string} noun the trigger's noun (default 'kinds')
|
|
171
|
+
*/
|
|
172
|
+
export function SettingsMulti({ options = [], selected = [], onToggle, noun = 'kinds', className = '' }) {
|
|
173
|
+
const on = selected instanceof Set ? selected : new Set(selected)
|
|
174
|
+
const opts = [
|
|
175
|
+
{ value: '__summary', label: `${on.size} of ${options.length} ${noun}` },
|
|
176
|
+
...options.map((o) => ({ value: o.value, label: `${on.has(o.value) ? '✓ ' : ''}${o.label ?? String(o.value)}` })),
|
|
177
|
+
]
|
|
178
|
+
return <Dropdown size="sm" variant="primary" value="__summary" options={opts} onChange={(v) => (v === '__summary' ? null : onToggle?.(v))} className={`w-full ${className}`.trim()} />
|
|
179
|
+
}
|
|
180
|
+
|
|
163
181
|
/**
|
|
164
182
|
* SettingsChipRow — a wrap of toggle chips with optional counts (an
|
|
165
183
|
* allow-list: every chip sets a default, never a gate). `allChip` puts an
|
|
@@ -198,12 +216,16 @@ export function SettingsChipRow({ options = [], selected = [], onToggle, allChip
|
|
|
198
216
|
)
|
|
199
217
|
}
|
|
200
218
|
|
|
201
|
-
/** SettingsFooter —
|
|
202
|
-
|
|
219
|
+
/** SettingsFooter — a Divider, then one `IconFrame refresh` (primary · sm) at the
|
|
220
|
+
* right = reset to defaults (SettingsPanelApproved, 2026-08-27). No status word,
|
|
221
|
+
* no text button. `customised` is accepted for compatibility and unused. */
|
|
222
|
+
export function SettingsFooter({ onReset, resetLabel = 'Reset to defaults' }) {
|
|
203
223
|
return (
|
|
204
|
-
<div className="flex
|
|
205
|
-
<
|
|
206
|
-
|
|
224
|
+
<div className="flex flex-col gap-4">
|
|
225
|
+
<Divider />
|
|
226
|
+
<div className="flex justify-end">
|
|
227
|
+
{onReset && <IconFrame name="refresh" variant="primary" size="sm" onClick={onReset} title={resetLabel} aria-label={resetLabel} />}
|
|
228
|
+
</div>
|
|
207
229
|
</div>
|
|
208
230
|
)
|
|
209
231
|
}
|