@kolkrabbi/kol-component 0.322.0 → 0.324.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 +3 -3
- package/src/atoms/Button.jsx +7 -1
- package/src/index.js +2 -1
- package/src/molecules/Dropdown.jsx +6 -4
- package/src/molecules/Menu.jsx +198 -0
- package/src/molecules/MenuItem.jsx +13 -5
- package/src/molecules/Modal.jsx +30 -17
- package/src/molecules/SectionText.jsx +12 -4
- package/src/molecules/TabsRow.jsx +5 -2
- package/src/organisms/SettingsPanel.jsx +5 -2
- package/src/organisms/SettingsSections.jsx +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.324.0",
|
|
4
4
|
"description": "The core component library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"embla-carousel-react": "^8.6.0",
|
|
27
27
|
"pdfjs-dist": "^6.3.289",
|
|
28
28
|
"react-syntax-highlighter": "^16.1.1",
|
|
29
|
-
"@kolkrabbi/kol-growth": "^0.4.0",
|
|
30
29
|
"@kolkrabbi/kol-search": "^0.3.0",
|
|
30
|
+
"@kolkrabbi/kol-growth": "^0.4.0",
|
|
31
31
|
"@kolkrabbi/kol-markdown": "^0.2.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@kolkrabbi/kol-icons": "^0.
|
|
42
|
+
"@kolkrabbi/kol-icons": "^0.56.0"
|
|
43
43
|
},
|
|
44
44
|
"files": [
|
|
45
45
|
"src",
|
package/src/atoms/Button.jsx
CHANGED
|
@@ -12,7 +12,7 @@ import { glyphSize } from '../hooks/glyphLadders.js'
|
|
|
12
12
|
*
|
|
13
13
|
* @param {Object} props
|
|
14
14
|
* @param {ReactNode} props.children - Button content
|
|
15
|
-
* @param {'accent'|'danger'|'nav'|'tab'|'tab-filled'} props.variant - The intent; the GROUND is `tone`. `tab` is the tab chip (2026-10-02): quiet text at rest, a soft filled chip while `pressed`. `tab-filled` (2026-10-29) shows its chip at rest (oq-04), brighter on hover (oq-08) and brightest selected (oq-16). `float` (FloatingStyleAndSignInDemos; round 3's values, W16 2026-10-31) is the shape only: a capsule whose height and type step together — sm · md · lg 28 · 36 · 44 with 12 · 13 · 14 type and an 18 glyph, on `pointer: coarse` 36 · 44 · 52, 15 · 16 type and a 20 glyph; icon-only is the square; the ground stays the `tone`'s, the surface is the float it sits on. A composed float component (AppFloatNav, AccountForm's box) passes it, a consumer never sets it by hand. `primary · secondary · outline · ghost · grey · control` are deprecated aliases of tones (2026-10-01, on the retirement ledger): pass `tone` — `secondary` is `tone="inverted"`, `control` is `tone="ghost"`, the rest keep their name. Visual variant. UNSET (the default) inherits the tone of the nearest `kol-tone-*` wrapper, else renders primary; the five ground variants are aliases of `tone`. `danger` is the destructive treatment (--ui-error fill); `nav` is the chrome rung — transparent, oq-64 ink, one step brighter than `ghost`; `control` is an alias for `ghost` (legacy call-sites).
|
|
15
|
+
* @param {'accent'|'danger'|'nav'|'tab'|'tab-filled'|'bare'} props.variant - `bare` is a pressable surface with no button chrome (a tile, a bar row — the caller draws it). The intent; the GROUND is `tone`. `tab` is the tab chip (2026-10-02): quiet text at rest, a soft filled chip while `pressed`. `tab-filled` (2026-10-29) shows its chip at rest (oq-04), brighter on hover (oq-08) and brightest selected (oq-16). `float` (FloatingStyleAndSignInDemos; round 3's values, W16 2026-10-31) is the shape only: a capsule whose height and type step together — sm · md · lg 28 · 36 · 44 with 12 · 13 · 14 type and an 18 glyph, on `pointer: coarse` 36 · 44 · 52, 15 · 16 type and a 20 glyph; icon-only is the square; the ground stays the `tone`'s, the surface is the float it sits on. A composed float component (AppFloatNav, AccountForm's box) passes it, a consumer never sets it by hand. `primary · secondary · outline · ghost · grey · control` are deprecated aliases of tones (2026-10-01, on the retirement ledger): pass `tone` — `secondary` is `tone="inverted"`, `control` is `tone="ghost"`, the rest keep their name. Visual variant. UNSET (the default) inherits the tone of the nearest `kol-tone-*` wrapper, else renders primary; the five ground variants are aliases of `tone`. `danger` is the destructive treatment (--ui-error fill); `nav` is the chrome rung — transparent, oq-64 ink, one step brighter than `ghost`; `control` is an alias for `ghost` (legacy call-sites).
|
|
16
16
|
* @param {'xs'|'sm'|'md'|'lg'} props.size - Button size (default: 'md'); xs is the panel rung (ControlsXsRung, 2026-09-01) — kol-mono-8 in a 22px shell (20 icon-only), opt-in by prop
|
|
17
17
|
* @param {string} props.iconLeft - Icon name to display on the left
|
|
18
18
|
* @param {string} props.iconRight - Icon name to display on the right
|
|
@@ -65,6 +65,12 @@ const Button = ({
|
|
|
65
65
|
tone = 'default',
|
|
66
66
|
...props
|
|
67
67
|
}) => {
|
|
68
|
+
/* `variant="bare"` (MetricRebuild, 2026-10-31): a pressable SURFACE — a stat tile, a bar row, a column head — that
|
|
69
|
+
* draws itself. No kol-btn chrome (ground, size, type, radius); the button keeps its type, the KOL focus ring
|
|
70
|
+
* (`kol-menu-btn`, as MenuItem's rows) and `disabled`. For a control that looks like a button, use any other variant. */
|
|
71
|
+
if (variant === 'bare') {
|
|
72
|
+
return <button type={type} onClick={onClick} disabled={disabled} className={`kol-menu-btn ${className}`.trim()} style={style} {...props}>{children}</button>
|
|
73
|
+
}
|
|
68
74
|
// Two ladders, split on whether a label sits beside the glyph. An icon-only
|
|
69
75
|
// button is a solo glyph in a pinned square, so it takes SOLO (16/20/24) —
|
|
70
76
|
// it took the text-adjacent ladder until component 0.20.0, which put a 14px
|
package/src/index.js
CHANGED
|
@@ -127,7 +127,8 @@ export { default as ContentCard } from './molecules/ContentCard.jsx'
|
|
|
127
127
|
export { default as ContentRow } from './molecules/ContentRow.jsx'
|
|
128
128
|
export { default as ContentItem } from './molecules/ContentItem.jsx'
|
|
129
129
|
export { default as ContentCollection } from './organisms/ContentCollection.jsx'
|
|
130
|
-
export { MenuItem, MenuDropdownItem, MenuDropdownDivider, MenuDropdownNest } from './molecules/MenuItem.jsx'
|
|
130
|
+
export { MenuItem, MenuRow, MenuDivider, MenuFlyout, MenuDropdownItem, MenuDropdownDivider, MenuDropdownNest } from './molecules/MenuItem.jsx'
|
|
131
|
+
export { Menu, MenuTrigger, MenuPanel, MenuLabel, MenuFilter, MenuDatePicker, MenuMore } from './molecules/Menu.jsx'
|
|
131
132
|
export { MenuPopover } from './molecules/MenuPopover.jsx'
|
|
132
133
|
export { ModalProvider, useModal, Dialog } from './molecules/Modal.jsx'
|
|
133
134
|
export { default as Combobox } from './molecules/Combobox.jsx'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
2
2
|
import { toneClass, TONE_VARS } from '../utilities/tone.js'
|
|
3
3
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
4
|
-
import {
|
|
4
|
+
import { MenuRow, MenuDivider } from './MenuItem.jsx'
|
|
5
5
|
import { PopoverPanel, usePopover } from '../utilities/Popover.jsx'
|
|
6
6
|
import { glyphSize, indicatorSize } from '../hooks/glyphLadders.js'
|
|
7
7
|
|
|
@@ -32,6 +32,8 @@ import { glyphSize, indicatorSize } from '../hooks/glyphLadders.js'
|
|
|
32
32
|
* explicit `size` prop (user law 2026-07-28 — desktop is sm, always, and
|
|
33
33
|
* smaller viewports never resolve larger than desktop). The old auto-ramp
|
|
34
34
|
* (lg ≥1024 / md ≥768 / sm below) is gone.
|
|
35
|
+
*
|
|
36
|
+
* An action menu (rows as data under a trigger) is `Menu` (the Menu* family, 2026-10-31), not this.
|
|
35
37
|
*/
|
|
36
38
|
|
|
37
39
|
/* xs (ControlsXsRung, 2026-09-01): the panel rung, opt-in by prop — the default stays sm (2026-07-28 law) */
|
|
@@ -297,11 +299,11 @@ const Dropdown = ({
|
|
|
297
299
|
|
|
298
300
|
<div ref={listRef} className="kol-dd-list" role="listbox" style={scale !== 1 ? { zoom: scale } : undefined}>
|
|
299
301
|
{options.map((option, i) => {
|
|
300
|
-
if (option?.divider) return <
|
|
302
|
+
if (option?.divider) return <MenuDivider key={`divider-${i}`} />
|
|
301
303
|
if (option?.heading) return <div key={`heading-${i}`} className={`${resolvedSize === 'xs' ? 'kol-mono-8' : 'kol-helper-12'} ${ROW_H[resolvedSize] ?? ROW_H.sm} ${ROW_PAD[resolvedSize] ?? ROW_PAD.sm} flex shrink-0 items-center text-meta`}>{option.heading}</div>
|
|
302
304
|
const isActive = option.value === currentOption?.value
|
|
303
305
|
return (
|
|
304
|
-
<
|
|
306
|
+
<MenuRow
|
|
305
307
|
key={option.value}
|
|
306
308
|
/* NO HOVER STATE ANYWHERE ON A DROPDOWN (user 2026-08-28,
|
|
307
309
|
* "delete any hover state on dropdowns"): the trigger was
|
|
@@ -336,7 +338,7 @@ const Dropdown = ({
|
|
|
336
338
|
</span>
|
|
337
339
|
)}
|
|
338
340
|
{option.label}
|
|
339
|
-
</
|
|
341
|
+
</MenuRow>
|
|
340
342
|
)
|
|
341
343
|
})}
|
|
342
344
|
</div>
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Icon } from '@kolkrabbi/kol-icons'
|
|
3
|
+
import Button from '../atoms/Button.jsx'
|
|
4
|
+
import Kbd from '../atoms/Kbd.jsx'
|
|
5
|
+
import SearchInput from './SearchInput.jsx'
|
|
6
|
+
import { MenuRow, MenuDivider, MenuFlyout } from './MenuItem.jsx'
|
|
7
|
+
import { PopoverPanel, usePopover, Tooltip } from '../utilities/Popover.jsx'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The Menu* family (metric Round 4, 2026-10-31) — one family, one prefix.
|
|
11
|
+
*
|
|
12
|
+
* MenuTrigger the chip: `Button variant="tab" size="sm"`, `pressed` while open; `iconOnly` is the ⋮
|
|
13
|
+
* MenuPanel the faint raised panel (kol-theme `.kol-menu-panel`)
|
|
14
|
+
* MenuRow · MenuDivider · MenuLabel · MenuFlyout — the parts (MenuRow / MenuFlyout / MenuDivider live in MenuItem.jsx)
|
|
15
|
+
* Menu the base: `trigger` (render prop `({ open, props })`) + `items` (rows as data)
|
|
16
|
+
* MenuFilter · MenuDatePicker · MenuMore — ready-made menus, data and callbacks only
|
|
17
|
+
*
|
|
18
|
+
* <Menu align="end" minWidth={224} trigger={({ open, props }) => <MenuTrigger open={open} icon="filter-lines" {...props}>Filter</MenuTrigger>} items={[
|
|
19
|
+
* { heading: 'Graph interval' }, // a MenuLabel row
|
|
20
|
+
* { label: 'Days', current: true, checked: true, onClick },
|
|
21
|
+
* { divider: true },
|
|
22
|
+
* { label: 'Segment', icon: 'star', disabled: true, tooltip: 'No segments saved' },
|
|
23
|
+
* { label: 'Browser', icon: 'layout', items: [{ label: 'Browser', onClick }] }, // a flyout
|
|
24
|
+
* { label: 'Last 7 days', kbd: 'W', onClick },
|
|
25
|
+
* { label: 'Export stats', trailing: 'download', onClick },
|
|
26
|
+
* ]} />
|
|
27
|
+
*
|
|
28
|
+
* row keys: `icon` (kol-icons name or node) · `trailing` (+ `trailingSize`) · `current` fills the row · `checked` ✓
|
|
29
|
+
* (before the `kbd` cap) · `kbd` · `disabled` + `tooltip` (why) · `items` (a flyout of the same rows).
|
|
30
|
+
* `search` ({ placeholder, empty }) puts a search field above the rows and filters them by label.
|
|
31
|
+
* `children` sit above the rows.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/* the ⋮ — kol-icons `more-vertical` (user 2026-10-31: *"just make a vertical version"* — was `more` turned 90°) */
|
|
35
|
+
const MORE = <Icon name="more-vertical" size={16} />
|
|
36
|
+
|
|
37
|
+
export function MenuTrigger({ open = false, iconOnly, icon, iconComponent, children, ...rest }) {
|
|
38
|
+
if (iconOnly) {
|
|
39
|
+
return <Button variant="tab" size="sm" pressed={open} iconOnly={iconOnly === true ? 'more-vertical' : iconOnly} iconComponent={iconOnly === true ? MORE : iconComponent} {...rest} className={`kol-menu-trigger ${rest.className ?? ''}`.trim()} />
|
|
40
|
+
}
|
|
41
|
+
/* a labelled trigger has NO hover wash — its ground shows only while its menu is open (user 2026-10-31, on Plausible:
|
|
42
|
+
* *"its not supposed to be solid, only bg on hover … no hover just click … only [...] has hover"*); the ⋮ keeps it */
|
|
43
|
+
return <Button variant="tab" size="sm" pressed={open} iconLeft={icon} iconComponent={iconComponent} {...rest} className={`kol-menu-trigger kol-menu-trigger--quiet ${rest.className ?? ''}`.trim()}>{children}</Button>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const MenuPanel = ({ className = '', children, ...rest }) => (
|
|
47
|
+
<div className={`kol-menu-panel ${className}`.trim()} {...rest}>{children}</div>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
export const MenuLabel = ({ children }) => <div className="kol-menu-label kol-helper-12">{children}</div>
|
|
51
|
+
|
|
52
|
+
const glyph = (icon, size) => (typeof icon === 'string' ? <Icon name={icon} size={size} /> : icon)
|
|
53
|
+
const Check = () => <Icon name="check" size={14} className="text-oq-88" />
|
|
54
|
+
|
|
55
|
+
function trailingOf(it) {
|
|
56
|
+
if (it.kbd != null) {
|
|
57
|
+
return (
|
|
58
|
+
<span className="inline-flex items-center gap-2">
|
|
59
|
+
{it.checked && <Check />}
|
|
60
|
+
<Kbd size="md" className="kol-menu-kbd">{it.kbd}</Kbd>
|
|
61
|
+
</span>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
if (it.checked) return <Check />
|
|
65
|
+
return it.trailing ? <span className="text-oq-88 inline-flex">{glyph(it.trailing, it.trailingSize ?? 14)}</span> : undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Rows({ items }) {
|
|
69
|
+
return items.map((it, i) => {
|
|
70
|
+
if (it.divider) return <MenuDivider key={`d${i}`} className="kol-menu-rule" />
|
|
71
|
+
if (it.heading) return <MenuLabel key={`h${i}`}>{it.heading}</MenuLabel>
|
|
72
|
+
const icon = it.icon && <span className="text-oq-88 inline-flex">{glyph(it.icon, 16)}</span>
|
|
73
|
+
const cls = `kol-menu-row kol-helper-12${it.current ? ' kol-menu-row--current' : ''}`
|
|
74
|
+
const row = it.items?.length && !it.disabled ? (
|
|
75
|
+
<MenuFlyout flyout iconLeft={icon} label={it.label} rowClass={cls} panelClassName="kol-menu-panel min-w-[200px]" panelStyle={{ marginLeft: 4 }}>
|
|
76
|
+
<Rows items={it.items} />
|
|
77
|
+
</MenuFlyout>
|
|
78
|
+
) : (
|
|
79
|
+
<MenuRow disabled={it.disabled} rowClass={cls} onClick={it.onClick} iconLeft={icon} shortcut={trailingOf(it)}>
|
|
80
|
+
{it.label}
|
|
81
|
+
</MenuRow>
|
|
82
|
+
)
|
|
83
|
+
const key = it.key ?? `r${i}`
|
|
84
|
+
return it.tooltip
|
|
85
|
+
? <Tooltip key={key} label={it.tooltip} placement="left" triggerClassName="block w-full">{row}</Tooltip>
|
|
86
|
+
: <div key={key}>{row}</div>
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function Menu({ items = [], trigger, align = 'start', minWidth, offset = 4, defaultOpen = false, stayOpen = false, search, className = '', children }) {
|
|
91
|
+
const [open, setOpen] = useState(defaultOpen)
|
|
92
|
+
const [q, setQ] = useState('')
|
|
93
|
+
const popover = usePopover({
|
|
94
|
+
open,
|
|
95
|
+
onOpenChange: setOpen,
|
|
96
|
+
placement: align === 'end' ? 'bottom-end' : 'bottom-start',
|
|
97
|
+
offset,
|
|
98
|
+
role: 'menu',
|
|
99
|
+
dismiss: stayOpen ? { outsidePress: false } : true,
|
|
100
|
+
})
|
|
101
|
+
let rows = items
|
|
102
|
+
if (search) {
|
|
103
|
+
const s = q.toLowerCase()
|
|
104
|
+
rows = items.filter((it) => it.label == null || String(it.label).toLowerCase().includes(s))
|
|
105
|
+
if (!rows.some((it) => it.label != null)) rows = [...rows, { key: 'empty', label: search.empty || 'Nothing matches', disabled: true }]
|
|
106
|
+
}
|
|
107
|
+
/* the wrapper takes the ref (a Button need not forward one); the trigger takes the handlers and the aria —
|
|
108
|
+
* `aria-pressed` cleared, an open menu is `aria-expanded`, its fill is the trigger's `pressed` */
|
|
109
|
+
return (
|
|
110
|
+
<>
|
|
111
|
+
<span ref={popover.refs.setReference} className={`inline-flex align-middle ${className}`.trim()}>
|
|
112
|
+
{trigger({ open, props: { ...popover.getReferenceProps(), 'aria-pressed': undefined } })}
|
|
113
|
+
</span>
|
|
114
|
+
<PopoverPanel popover={popover} panel={false} focus={false} className="kol-menu-panel w-max" style={minWidth ? { minWidth } : undefined}>
|
|
115
|
+
{/* a row's click closes the menu (it bubbles from `[data-menu-item]`); a flyout row only toggles */}
|
|
116
|
+
<div onClick={(e) => { if (e.target.closest('[data-menu-item]')) setOpen(false) }}>
|
|
117
|
+
{search && <div className="pb-1"><SearchInput variant="outline" size="sm" value={q} onChange={(e) => setQ(e.target?.value ?? e)} onClear={() => setQ('')} placeholder={search.placeholder || 'Search'} /></div>}
|
|
118
|
+
{children}
|
|
119
|
+
<Rows items={rows} />
|
|
120
|
+
</div>
|
|
121
|
+
</PopoverPanel>
|
|
122
|
+
</>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ── the ready-made menus ── */
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* MenuFilter — category rows (`groups`: `{ key, label, icon, fields: [{ key, label }], more, disabled: 'why' }` or
|
|
130
|
+
* `{ divider: true }`). A group with one field is a plain row that opens it; a `more` group with 2+ fields opens a flyout
|
|
131
|
+
* of them; a `more` group with none is disabled with ›; a disabled group shows its reason. `onPick({ group, field? })`.
|
|
132
|
+
* `active` (`[{ key, label, group, field? }]`) — metric Round 6 (user: *"dont put this tag there, it breaks the layout"*):
|
|
133
|
+
* the active filters live INSIDE the menu, an "Active" section on top (a row's click is `onPick` to edit it, its × is
|
|
134
|
+
* `onRemove(f)`, "Clear all" at 2+ is `onClear`), and the trigger holds its pressed ground while any is set. No count.
|
|
135
|
+
*/
|
|
136
|
+
export function MenuFilter({ groups = [], onPick, active = [], onRemove, onClear, label = 'Filter', icon = 'filter-lines', align = 'end', minWidth = 224 }) {
|
|
137
|
+
const items = groups.map((g) => {
|
|
138
|
+
if (g.divider) return g
|
|
139
|
+
const one = g.fields?.length === 1 && !g.disabled
|
|
140
|
+
return {
|
|
141
|
+
key: g.key, label: g.label, icon: g.icon, disabled: !!g.disabled, tooltip: g.disabled || undefined,
|
|
142
|
+
trailing: g.more && g.disabled ? 'chevron-right' : undefined, trailingSize: 12,
|
|
143
|
+
items: g.more && !g.disabled && !one ? g.fields.map((f) => ({ key: f.key, label: f.label, onClick: () => onPick?.({ group: g.key, field: f.key }) })) : undefined,
|
|
144
|
+
onClick: () => onPick?.({ group: g.key }),
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
const rowCls = 'kol-menu-row kol-helper-12'
|
|
148
|
+
return (
|
|
149
|
+
<Menu align={align} minWidth={minWidth} offset={8} items={items}
|
|
150
|
+
trigger={({ open, props }) => <MenuTrigger open={open || active.length > 0} icon={icon} {...props}>{label}</MenuTrigger>}>
|
|
151
|
+
{active.length > 0 && (
|
|
152
|
+
<>
|
|
153
|
+
<MenuLabel>Active</MenuLabel>
|
|
154
|
+
{active.map((f) => (
|
|
155
|
+
<div key={f.key} className="flex items-center">
|
|
156
|
+
<div className="flex-1 min-w-0">
|
|
157
|
+
<MenuRow rowClass={rowCls} onClick={() => onPick?.({ group: f.group, field: f.field })}>{f.label}</MenuRow>
|
|
158
|
+
</div>
|
|
159
|
+
<Button tone="ghost" size="xs" iconOnly="x" aria-label={`Remove filter: ${f.label}`} onClick={() => onRemove?.(f)} />
|
|
160
|
+
</div>
|
|
161
|
+
))}
|
|
162
|
+
{active.length > 1 && onClear && <MenuRow rowClass={rowCls} onClick={onClear}>Clear all</MenuRow>}
|
|
163
|
+
<MenuDivider className="kol-menu-rule" />
|
|
164
|
+
</>
|
|
165
|
+
)}
|
|
166
|
+
</Menu>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* MenuDatePicker — range `groups` (`[[{ key, label, kbd }]]`, a rule between groups), `value` the current key (filled,
|
|
172
|
+
* ✓), `onChange(key)` (a custom-range key is the caller's to open its dialog). `compare` (`{ label, kbd, onClick }`)
|
|
173
|
+
* adds the compare row under a rule. The trigger shows `label` beside the calendar glyph (`icon` / `iconComponent`).
|
|
174
|
+
* metric Round 6 (user: *"relation ship between tab and dropdown is much better at reference"* — plausible.io): the
|
|
175
|
+
* trigger holds a value, so it wears its chip at rest — the tab's pressed oq-08, the panel's own ground, one family —
|
|
176
|
+
* and the panel opens right-aligned 8px under it (plausible.io measured: 8px, right edges flush).
|
|
177
|
+
*/
|
|
178
|
+
export function MenuDatePicker({ groups = [], value, onChange, label, icon = 'calendar', iconComponent, compare, align = 'end', minWidth = 224 }) {
|
|
179
|
+
const items = [
|
|
180
|
+
...groups.flatMap((g, i) => [
|
|
181
|
+
...(i > 0 ? [{ divider: true }] : []),
|
|
182
|
+
...g.map((r) => ({ key: r.key, label: r.label, current: r.key === value, checked: r.key === value, kbd: r.kbd, onClick: () => onChange?.(r.key) })),
|
|
183
|
+
]),
|
|
184
|
+
...(compare ? [{ divider: true }, { key: 'compare', label: compare.label, kbd: compare.kbd, onClick: compare.onClick }] : []),
|
|
185
|
+
]
|
|
186
|
+
return (
|
|
187
|
+
<Menu align={align} minWidth={minWidth} offset={8} items={items}
|
|
188
|
+
trigger={({ open, props }) => <MenuTrigger open={open} icon={icon} iconComponent={iconComponent} {...props}>{label}</MenuTrigger>} />
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** MenuMore — the ⋮ chip and its rows (`items`, as `Menu`'s); `label` names it for a screen reader. */
|
|
193
|
+
export function MenuMore({ label, items = [], align = 'end', minWidth = 224, defaultOpen, stayOpen }) {
|
|
194
|
+
return (
|
|
195
|
+
<Menu align={align} minWidth={minWidth} offset={8} items={items} defaultOpen={defaultOpen} stayOpen={stayOpen}
|
|
196
|
+
trigger={({ open, props }) => <MenuTrigger open={open} iconOnly aria-label={label} {...props} />} />
|
|
197
|
+
)
|
|
198
|
+
}
|
|
@@ -99,7 +99,7 @@ export function MenuItem({
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
*
|
|
102
|
+
* MenuRow — action row inside a menu panel (`Menu`, `MenuItem`). Was `MenuDropdownItem` (alias kept, 2026-10-31).
|
|
103
103
|
*
|
|
104
104
|
* `height` (default none — the `h-8` rung) — an inline row height, for a panel
|
|
105
105
|
* in a chrome whose own rows are shorter (DropdownGhostWidthAndListHeight,
|
|
@@ -134,7 +134,7 @@ const ROW_BY_SIZE = { xs: 'kol-mono-8 px-2 h-5', sm: 'kol-helper-12 px-2 h-8', m
|
|
|
134
134
|
/* `flex`, not `inline-flex` (MenuDropdownItemBlockRow, kol-fxr 2026-10-13): as bare children of a
|
|
135
135
|
* content-sized (`w-max`) panel, inline rows measured as ONE line — five starters ≈ 795px. A block
|
|
136
136
|
* row still fills a fixed panel through `w-full` and lets a content-sized one hug its longest label. */
|
|
137
|
-
export function
|
|
137
|
+
export function MenuRow({ onClick, onPointerEnter, onPointerLeave, disabled, prefix, iconLeft, shortcut, hover = true, height, size = 'sm', rowClass, children }) {
|
|
138
138
|
return (
|
|
139
139
|
<button
|
|
140
140
|
type="button"
|
|
@@ -161,12 +161,13 @@ export function MenuDropdownItem({ onClick, onPointerEnter, onPointerLeave, disa
|
|
|
161
161
|
|
|
162
162
|
/* `className` — the rule's colour and spacing, whole (MetricDashboardUpstream, kol-website 2026-10-31): on a filled
|
|
163
163
|
* panel the default `border-oq-08` vanishes. */
|
|
164
|
-
|
|
164
|
+
/* MenuDivider — a rule between a menu's groups. Was `MenuDropdownDivider` (alias kept, 2026-10-31). */
|
|
165
|
+
export function MenuDivider({ className = 'border-oq-08 my-1' }) {
|
|
165
166
|
return <div className={`border-t ${className}`} />
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
/**
|
|
169
|
-
*
|
|
170
|
+
* MenuFlyout — a menu row that holds a submenu. Was `MenuDropdownNest` (alias kept, 2026-10-31).
|
|
170
171
|
*
|
|
171
172
|
* Default — ACCORDION: click the row to expand/collapse its children inline
|
|
172
173
|
* (below the row, in the same panel); the trailing chevron rotates 90°.
|
|
@@ -193,7 +194,7 @@ export function MenuDropdownDivider({ className = 'border-oq-08 my-1' }) {
|
|
|
193
194
|
* Either way, a leaf MenuDropdownItem inside still closes the whole menu via the
|
|
194
195
|
* standard data-menu-item bubble; the nest row itself only toggles.
|
|
195
196
|
*/
|
|
196
|
-
export function
|
|
197
|
+
export function MenuFlyout({ prefix, iconLeft, label, disabled, flyout = false, scroll = false, rowClass, panelClassName, panelStyle, children }) {
|
|
197
198
|
if (flyout) return <MenuFlyoutNest prefix={prefix} iconLeft={iconLeft} label={label} disabled={disabled} scroll={scroll} rowClass={rowClass} panelClassName={panelClassName} panelStyle={panelStyle}>{children}</MenuFlyoutNest>
|
|
198
199
|
return <MenuAccordionNest prefix={prefix} iconLeft={iconLeft} label={label} disabled={disabled}>{children}</MenuAccordionNest>
|
|
199
200
|
}
|
|
@@ -296,3 +297,10 @@ function MenuFlyoutNest({ prefix, iconLeft, label, disabled, scroll, rowClass, p
|
|
|
296
297
|
</div>
|
|
297
298
|
)
|
|
298
299
|
}
|
|
300
|
+
|
|
301
|
+
/** @deprecated 2026-10-31 → MenuRow (the Menu* family, metric Round 4) */
|
|
302
|
+
export const MenuDropdownItem = MenuRow
|
|
303
|
+
/** @deprecated 2026-10-31 → MenuDivider */
|
|
304
|
+
export const MenuDropdownDivider = MenuDivider
|
|
305
|
+
/** @deprecated 2026-10-31 → MenuFlyout */
|
|
306
|
+
export const MenuDropdownNest = MenuFlyout
|
package/src/molecules/Modal.jsx
CHANGED
|
@@ -64,7 +64,16 @@ import ToggleCheckbox from '../atoms/ToggleCheckbox.jsx'
|
|
|
64
64
|
|
|
65
65
|
const ModalCtx = (globalThis[Symbol.for('@kolkrabbi/kol-component/ModalCtx')] ??= createContext(null))
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
/* THE MODAL VARIANTS (user 2026-10-31: *"do we not use variants for different styles?"*). `lift` — the overlay panel
|
|
68
|
+
* every KOL overlay wears (oq-04, no border, the scrim is the edge); `bordered` — the page surface behind an fg-08
|
|
69
|
+
* hairline. Each modal keeps its own default (prompts bordered, Dialog lift) until he picks one. */
|
|
70
|
+
const MODAL_VARIANTS = {
|
|
71
|
+
lift: { className: 'kol-overlay-panel', style: {} },
|
|
72
|
+
bordered: { className: 'border border-fg-08', style: { background: 'var(--kol-surface-primary)', color: 'var(--kol-surface-on-primary)' } },
|
|
73
|
+
}
|
|
74
|
+
const modalVariant = (v, fallback) => MODAL_VARIANTS[v] ?? MODAL_VARIANTS[fallback]
|
|
75
|
+
|
|
76
|
+
export function ModalProvider({ children, variant = 'bordered' }) {
|
|
68
77
|
const [state, setState] = useState(null)
|
|
69
78
|
|
|
70
79
|
const closeWith = useCallback((value) => {
|
|
@@ -74,34 +83,35 @@ export function ModalProvider({ children }) {
|
|
|
74
83
|
})
|
|
75
84
|
}, [])
|
|
76
85
|
|
|
77
|
-
const prompt = useCallback((title, defaultValue = '', { okLabel, cancelLabel, type = 'text', placeholder } = {}) =>
|
|
78
|
-
new Promise((resolve) => setState({ kind: 'prompt', title, defaultValue, okLabel, cancelLabel, type, placeholder, resolve })),
|
|
86
|
+
const prompt = useCallback((title, defaultValue = '', { okLabel, cancelLabel, type = 'text', placeholder, variant } = {}) =>
|
|
87
|
+
new Promise((resolve) => setState({ kind: 'prompt', title, defaultValue, okLabel, cancelLabel, type, placeholder, variant, resolve })),
|
|
79
88
|
[])
|
|
80
89
|
|
|
81
|
-
const confirm = useCallback((title, { okLabel, cancelLabel, options } = {}) =>
|
|
82
|
-
new Promise((resolve) => setState({ kind: 'confirm', title, okLabel, cancelLabel, options: options?.length ? options : null, resolve })),
|
|
90
|
+
const confirm = useCallback((title, { okLabel, cancelLabel, options, variant } = {}) =>
|
|
91
|
+
new Promise((resolve) => setState({ kind: 'confirm', title, okLabel, cancelLabel, options: options?.length ? options : null, variant, resolve })),
|
|
83
92
|
[])
|
|
84
93
|
|
|
85
|
-
const alert = useCallback((title, { okLabel } = {}) =>
|
|
86
|
-
new Promise((resolve) => setState({ kind: 'alert', title, okLabel, resolve })),
|
|
94
|
+
const alert = useCallback((title, { okLabel, variant } = {}) =>
|
|
95
|
+
new Promise((resolve) => setState({ kind: 'alert', title, okLabel, variant, resolve })),
|
|
87
96
|
[])
|
|
88
97
|
|
|
89
|
-
const form = useCallback((title, fields = [], { okLabel, cancelLabel } = {}) =>
|
|
90
|
-
new Promise((resolve) => setState({ kind: 'form', title, fields, okLabel, cancelLabel, resolve })),
|
|
98
|
+
const form = useCallback((title, fields = [], { okLabel, cancelLabel, variant } = {}) =>
|
|
99
|
+
new Promise((resolve) => setState({ kind: 'form', title, fields, okLabel, cancelLabel, variant, resolve })),
|
|
91
100
|
[])
|
|
92
101
|
|
|
93
102
|
return (
|
|
94
103
|
<ModalCtx.Provider value={{ prompt, confirm, alert, form }}>
|
|
95
104
|
{children}
|
|
96
105
|
{state && typeof document !== 'undefined' && createPortal(
|
|
97
|
-
<ModalView state={state} closeWith={closeWith} />,
|
|
106
|
+
<ModalView state={state} closeWith={closeWith} variant={state.variant ?? variant} />,
|
|
98
107
|
document.body,
|
|
99
108
|
)}
|
|
100
109
|
</ModalCtx.Provider>
|
|
101
110
|
)
|
|
102
111
|
}
|
|
103
112
|
|
|
104
|
-
function ModalView({ state, closeWith }) {
|
|
113
|
+
function ModalView({ state, closeWith, variant }) {
|
|
114
|
+
const look = modalVariant(variant, 'bordered')
|
|
105
115
|
const [val, setVal] = useState(state.defaultValue ?? '')
|
|
106
116
|
const [values, setValues] = useState(() => Object.fromEntries((state.options ?? []).map((o) => [o.id, !!o.defaultValue])))
|
|
107
117
|
const [fieldVals, setFieldVals] = useState(() => Object.fromEntries((state.fields ?? []).map((f) => [f.id, f.defaultValue ?? ''])))
|
|
@@ -184,10 +194,9 @@ function ModalView({ state, closeWith }) {
|
|
|
184
194
|
aria-modal="true"
|
|
185
195
|
aria-labelledby="kol-modal-title"
|
|
186
196
|
onMouseDown={(e) => e.stopPropagation()}
|
|
187
|
-
className=
|
|
197
|
+
className={`kol-modal flex flex-col gap-4 p-5 rounded ${look.className}`}
|
|
188
198
|
style={{
|
|
189
|
-
|
|
190
|
-
color: 'var(--kol-surface-on-primary)',
|
|
199
|
+
...look.style,
|
|
191
200
|
minWidth: 320,
|
|
192
201
|
maxWidth: '90vw',
|
|
193
202
|
}}
|
|
@@ -288,7 +297,8 @@ export function useModal() {
|
|
|
288
297
|
*
|
|
289
298
|
* <Dialog open={open} onClose={() => setOpen(false)} title="Filter by page" width={560}>…</Dialog>
|
|
290
299
|
*/
|
|
291
|
-
export function Dialog({ open, onClose, title, width = 560, children }) {
|
|
300
|
+
export function Dialog({ open, onClose, title, width = 560, variant = 'lift', children }) {
|
|
301
|
+
const look = modalVariant(variant, 'lift')
|
|
292
302
|
useEffect(() => {
|
|
293
303
|
if (!open) return undefined
|
|
294
304
|
const onKey = (e) => { if (e.key === 'Escape' && !document.querySelector('.kol-popover-float')) onClose() }
|
|
@@ -308,8 +318,11 @@ export function Dialog({ open, onClose, title, width = 560, children }) {
|
|
|
308
318
|
aria-label={title}
|
|
309
319
|
data-dialog
|
|
310
320
|
onMouseDown={(e) => e.stopPropagation()}
|
|
311
|
-
|
|
312
|
-
|
|
321
|
+
/* THE OVERLAY PANEL (MetricRebuild, 2026-10-31 — user: the filter dialog read as "a borderless box floating
|
|
322
|
+
* mid-page"): it painted the page's own surface behind an fg-08 hairline, so on a dark page it was the page.
|
|
323
|
+
* `.kol-overlay-panel` is the lift every KOL overlay wears (oq-04, no border — the scrim is the edge). */
|
|
324
|
+
className={`kol-modal flex flex-col rounded max-h-[85vh] ${look.className}`}
|
|
325
|
+
style={{ ...look.style, width: `min(${width}px, calc(100vw - 32px))` }}
|
|
313
326
|
>
|
|
314
327
|
{children}
|
|
315
328
|
</div>
|
|
@@ -37,13 +37,15 @@
|
|
|
37
37
|
* stamps `reveal` on each part; nothing animates in the DS)
|
|
38
38
|
* @param {object} slotStyle inline style per slot — `{ headline: { '--reveal-delay': '0.1s' } }`
|
|
39
39
|
* @param {string} className extra classes on the block
|
|
40
|
-
* @param {'page'|'app'|'section'|'content'} [
|
|
40
|
+
* @param {'page'|'app'|'section'|'content'|'document'} [variant] THE HEADER VARIANTS (was `level` until 2026-10-31 — user: `variant`
|
|
41
|
+
* is the only word for a variant; `level` stays a deprecated alias on the retirement ledger) (2026-10-22, user: *"rename PageHeader to
|
|
41
42
|
* something that describes section text elements"* — PageHeader is now an alias of this). A
|
|
42
43
|
* level fills the role defaults; any explicit prop still wins. page = h1 display-01 over
|
|
43
44
|
* `.kol-header-lede` — the hero; app = h1 display-01 at 46/60 over `.kol-header-lede-app`;
|
|
44
45
|
* section = h2 display-03 over `.kol-header-lede-section`; content = h3 heading-04 under a
|
|
45
46
|
* `kol-eyebrow`, over body-01 fg-64 at 64ch — a section of a page's CONTENT (a dev tab's
|
|
46
|
-
* sections, DevSectionHeader, kol-fxr).
|
|
47
|
+
* sections, DevSectionHeader, kol-fxr); document = h1 36/1.2 bold display face over a 15/1.6 body-face lede
|
|
48
|
+
* to 724px — a document's own page (DocumentIndex). A level sets actions inline, and the bottom rhythm
|
|
47
49
|
* to `--kol-page-header-mb` (40px; content 16px). Unset = the section family as before.
|
|
48
50
|
* @param {ReactNode} media a mark above the eyebrow (a logo, an SVG) — with a `level` only
|
|
49
51
|
*/
|
|
@@ -60,7 +62,7 @@ export const HEADLINE_ROLE = {
|
|
|
60
62
|
'heading-05': 'kol-sans-heading-05',
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
/* ONE HEADER,
|
|
65
|
+
/* ONE HEADER, FIVE LEVELS (Round 11 + PageHeaderAppLevel, carried from PageHeader whole). `title` is the
|
|
64
66
|
* headline's type, `lede` the body's; the rest default the block for that level. */
|
|
65
67
|
export const SECTION_TEXT_LEVELS = {
|
|
66
68
|
page: { title: 'kol-sans-display-01', lede: 'kol-header-lede' },
|
|
@@ -69,6 +71,9 @@ export const SECTION_TEXT_LEVELS = {
|
|
|
69
71
|
/* content (DevSectionHeader, kol-fxr 2026-10-22 — the user's approved look: *"maybe title and description is
|
|
70
72
|
* one step bigger?"*; never `kol-helper-*` for a paragraph) */
|
|
71
73
|
content: { title: 'kol-sans-heading-04', lede: 'kol-sans-body-01 text-fg-64', eyebrow: 'kol-eyebrow text-fg-48', as: 'h3', gap: 'gap-2', mb: '16px', measure: '64ch' },
|
|
74
|
+
/* document (DocumentIndexFigma, kol-atlas 2026-10-31) — the Research page header, the workspace sheet's: 36/1.2 bold
|
|
75
|
+
* display face at −0.005em, 12 under it; the lede 15/1.6 in the body face to a 724 edge */
|
|
76
|
+
document: { title: 'font-[family-name:var(--kol-font-family-sans)] text-[36px] font-bold leading-[1.2] tracking-[-0.005em]', lede: 'm-0 font-[family-name:var(--kol-font-family-sans-neutral)] text-[15px] leading-[1.6] text-fg-96', measure: '724px' },
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
export default function SectionText({
|
|
@@ -115,11 +120,14 @@ export default function SectionText({
|
|
|
115
120
|
(PageHeader's `--kol-page-header-mb`). `slotStyle` reaches the slots; this
|
|
116
121
|
was the one box it could not reach. */
|
|
117
122
|
style,
|
|
123
|
+
variant: variantProp,
|
|
124
|
+
/* deprecated alias of `variant` (2026-10-31) */
|
|
118
125
|
level,
|
|
119
126
|
media,
|
|
120
127
|
}) {
|
|
121
128
|
/* a LEVEL fills the header defaults; an explicit prop always wins */
|
|
122
|
-
const
|
|
129
|
+
const variant = variantProp ?? level
|
|
130
|
+
const L = variant ? SECTION_TEXT_LEVELS[variant] ?? SECTION_TEXT_LEVELS.page : null
|
|
123
131
|
const Headline = headlineAs ?? L?.as ?? (L ? 'h1' : 'h2')
|
|
124
132
|
if (L) {
|
|
125
133
|
headlineClass = `text-fg-96 ${headlineClass ?? L.title}`
|
|
@@ -242,8 +242,11 @@ function StackedTabs({ tabs, value, onChange, placement, shape = 'bar', collapse
|
|
|
242
242
|
onClick={() => onChange?.(t.id)}
|
|
243
243
|
aria-current={active ? 'page' : undefined}
|
|
244
244
|
aria-label={t.label}
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
/* a DOCK slot lights on hover like a menu row — the nav row's oq-04 ground and on-primary ink (HomeDockRound4,
|
|
246
|
+
* kol-fxr 2026-10-31; was forced from AppFloatNav with `!` over the inline ground). The rest ground and ink ride
|
|
247
|
+
* variables so the hover class can win without `!`. */
|
|
248
|
+
className={`flex flex-col items-center justify-center rounded-full cursor-pointer flex-none bg-[var(--slot-bg)] text-[var(--slot-ink)]${dock && !active ? ' hover:bg-[var(--kol-oq-04)] hover:text-[var(--kol-surface-on-primary)]' : ''}${labels ? ' gap-0.5' : ''}`}
|
|
249
|
+
style={{ width: slotW, height: slotH, padding: 0, border: 'none', '--slot-bg': active ? 'var(--kol-oq-16)' : 'transparent', '--slot-ink': ink(active) }}
|
|
247
250
|
>
|
|
248
251
|
{t.icon && <Icon name={t.icon} size={labels ? 24 : 28} />}
|
|
249
252
|
{labels && <span className="truncate max-w-full px-1.5" style={{ fontFamily: 'var(--kol-font-family-mono)', fontSize: 11, lineHeight: 1.2, fontWeight: 600 }}>{t.label}</span>}
|
|
@@ -143,10 +143,13 @@ export default function SettingsPanel({
|
|
|
143
143
|
* row living in a column narrower than the 160px label default
|
|
144
144
|
* (SettingsShortcutsComboOverflow, kol-monitor 2026-09-01). `text` is a row whose value is text — the same label,
|
|
145
145
|
* on the baseline, as tall as its text (LabeledControl's `text`; SettingsRowMonoBaseline, kol-monitor 2026-10-18).
|
|
146
|
+
* `stack` puts the label ABOVE the value, both at the row's start — a dated entry read as a bulletin (metric's
|
|
147
|
+
* Deploys tab, Round 3 2026-10-31: *"date above the text"*); `labelWidth` and `align` do nothing there.
|
|
146
148
|
*/
|
|
147
|
-
export function SettingsRow({ label, hint, align = 'end', labelWidth = 160, text = false, children }) {
|
|
149
|
+
export function SettingsRow({ label, hint, align = 'end', labelWidth = 160, text = false, stack = false, children }) {
|
|
150
|
+
if (stack) align = 'fill'
|
|
148
151
|
return (
|
|
149
|
-
<LabeledControl inline text={text} label={typeof label === 'string' ? label.toUpperCase() : label} labelWidth={labelWidth}>
|
|
152
|
+
<LabeledControl inline={!stack} text={text} label={typeof label === 'string' ? label.toUpperCase() : label} labelWidth={labelWidth}>
|
|
150
153
|
{hint
|
|
151
154
|
? <Tooltip label={hint} triggerClassName={`flex min-h-6 items-center w-full ${align === 'fill' ? '' : 'justify-end'}`.trim()}>{children}</Tooltip>
|
|
152
155
|
/* `flex`, not `inline-flex`: an inline box rode the label's text baseline and sat 5px low (2026-10-31) */
|
|
@@ -67,7 +67,7 @@ const rowAlign = (row, fallback) => row.align ?? (row.type === 'choice' || row.t
|
|
|
67
67
|
* first included: the rule is `.kol-section--divided + .kol-section--divided`,
|
|
68
68
|
* so the first draws nothing and skipping it just costs the next its line.
|
|
69
69
|
*
|
|
70
|
-
* @param {Array<{label?: string, note?: string|string[], rows?: Array<{label: ReactNode, type?: 'choice'|'switch'|'slider', options?: Array, value?: ReactNode, on?: boolean, onChange?: Function, min?: number, max?: number, step?: number, formatValue?: Function, defaultValue?: number, displayWidth?: number, render?: Function, hint?: string, align?: string, labelWidth?: number|'auto', text?: boolean, id?: string}>, rowGap?: number}>} sections - The settings body, in order (`hint` → SettingsRow's hint; `note` a sentence or two under the eyebrow; a string `value` is drawn in the DS's mono-12 and sits on the baseline; `type: 'choice'` / `'switch'` / `'slider'` rows are drawn here — a slider is the DS Slider across the value column, fluid track + the Input readout at `displayWidth` chars (3, `preset="rail"`), its label column 80 unless set; `min` · `max` · `step` · `value` · `onChange`)
|
|
70
|
+
* @param {Array<{label?: string, note?: string|string[], rows?: Array<{label: ReactNode, type?: 'choice'|'switch'|'slider', options?: Array, value?: ReactNode, on?: boolean, onChange?: Function, min?: number, max?: number, step?: number, formatValue?: Function, defaultValue?: number, displayWidth?: number, render?: Function, hint?: string, align?: string, labelWidth?: number|'auto', text?: boolean, stack?: boolean, id?: string}>, rowGap?: number}>} sections - The settings body, in order (`hint` → SettingsRow's hint; `stack` → SettingsRow's stack, the label over the value; `note` a sentence or two under the eyebrow; a string `value` is drawn in the DS's mono-12 and sits on the baseline; `type: 'choice'` / `'switch'` / `'slider'` rows are drawn here — a slider is the DS Slider across the value column, fluid track + the Input readout at `displayWidth` chars (3, `preset="rail"`), its label column 80 unless set; `min` · `max` · `step` · `value` · `onChange`)
|
|
71
71
|
* @param {boolean} [divided=false] - Hairline above each section
|
|
72
72
|
* @param {number|'auto'} [labelWidth] - Default label column for every row — `'auto'` makes the label yield and the control hug, which is what a narrow column or a nowrap value needs
|
|
73
73
|
* @param {string} [align] - Default row alignment passed to `SettingsRow`
|
|
@@ -87,6 +87,7 @@ export default function SettingsSections({ sections = [], divided = false, label
|
|
|
87
87
|
align={rowAlign(row, align)}
|
|
88
88
|
labelWidth={row.labelWidth ?? labelWidth ?? (row.type === 'slider' ? 80 : undefined)}
|
|
89
89
|
text={row.text ?? (!row.render && !row.type && isText(row.value))}
|
|
90
|
+
stack={row.stack}
|
|
90
91
|
>
|
|
91
92
|
{control(row)}
|
|
92
93
|
</SettingsRow>
|