@kolkrabbi/kol-component 0.319.1 → 0.320.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 +3 -2
- package/src/atoms/Input.jsx +3 -2
- package/src/atoms/SegmentedToggle.jsx +9 -3
- package/src/index.js +2 -1
- package/src/molecules/TabsRow.jsx +170 -82
- package/src/organisms/GrowthLineStage.jsx +10 -8
- package/src/utilities/Popover.jsx +3 -1
- package/src/utilities/tone.js +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.320.0",
|
|
4
4
|
"description": "The core component library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,7 +26,7 @@
|
|
|
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.
|
|
29
|
+
"@kolkrabbi/kol-growth": "^0.3.0",
|
|
30
30
|
"@kolkrabbi/kol-search": "^0.3.0",
|
|
31
31
|
"@kolkrabbi/kol-markdown": "^0.2.0"
|
|
32
32
|
},
|
|
@@ -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.54.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` (
|
|
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).
|
|
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
|
|
@@ -69,7 +69,8 @@ const Button = ({
|
|
|
69
69
|
// button is a solo glyph in a pinned square, so it takes SOLO (16/20/24) —
|
|
70
70
|
// it took the text-adjacent ladder until component 0.20.0, which put a 14px
|
|
71
71
|
// glyph in a 28px square at `sm`. `iconSize` still overrides either.
|
|
72
|
-
|
|
72
|
+
/* a float glyph follows the pointer (18, 20 on touch — `--kol-float-ic`), so its size is the token, not a number */
|
|
73
|
+
const resolvedIconSize = iconSize ?? (variant === 'float' ? 'var(--kol-float-ic)' : glyphSize(size, Boolean(iconOnly)))
|
|
73
74
|
|
|
74
75
|
// `control` is a legacy alias for `ghost` (kept so web call-sites passing
|
|
75
76
|
// variant="control" keep working post-migration).
|
package/src/atoms/Input.jsx
CHANGED
|
@@ -11,8 +11,9 @@ import { glyphSize } from '../hooks/glyphLadders.js'
|
|
|
11
11
|
* variant="outline" — bordered, transparent bg — THE secondary
|
|
12
12
|
* treatment (2026-07-08 chrome law: one
|
|
13
13
|
* secondary, always subordinate to filled)
|
|
14
|
-
* variant="float" — the floating style
|
|
15
|
-
* (
|
|
14
|
+
* variant="float" — the floating style (round 3, W16 2026-10-31): a capsule on the float's well
|
|
15
|
+
* (`--kol-float-well`), 16 in, sm · md · lg 28 · 36 · 44 (touch 36 · 44 · 52) with
|
|
16
|
+
* 12 · 13 · 14 (13 · 15 · 16) type; AccountForm's box passes it
|
|
16
17
|
* tone="inverse" — the dark chip (`fg-ab-24`) for a washed plane
|
|
17
18
|
* (ControlToneInverse, 2026-08-27); same prop on ViewToggle · Dropdown
|
|
18
19
|
* size="xs" — the panel rung (ControlsXsRung, 2026-09-01):
|
|
@@ -44,7 +44,9 @@
|
|
|
44
44
|
* outline shell; the inset ring marks ONLY the selected cell) |
|
|
45
45
|
* 'tonal' (filled tiles, but the clicked cell marks itself by
|
|
46
46
|
* TONE — surface-tertiary fill, no ring; 2026-08-12).
|
|
47
|
-
*
|
|
47
|
+
* 'float' (W16, round 3 — 2026-10-31): a capsule track 6 in on its `surface`, pills 4 apart at the
|
|
48
|
+
* float heights (28/36/44, touch 36/44/52), the selected pill filled with the ink.
|
|
49
|
+
* Every other variant renders at the SAME pinned button-ladder
|
|
48
50
|
* height (22/26/32/40) — icon or text, the box never moves.
|
|
49
51
|
* size — mirrors Button exactly: 'sm' (26px, mono-12, 4/12 pad) |
|
|
50
52
|
* 'md' (default, 32px, mono-14, 6/16 pad) | 'lg' (40px,
|
|
@@ -57,13 +59,16 @@
|
|
|
57
59
|
* selected cell on `--kol-surface-sunken` / `fg-96`, rest cells as the default paints
|
|
58
60
|
* them; `filled` and `tonal` keep their own selected law. Through `toneClass`, so a
|
|
59
61
|
* `kol-tone-sunken` wrapper reaches it too. Unset = inherit.
|
|
62
|
+
* surface — `variant="float"` only: 'solid' (default) | 'glass-dark' | 'glass-light' — the track's material
|
|
63
|
+
* (`floatSurfaceClass`, kol-theme `.kol-float-*`)
|
|
60
64
|
* ariaLabel — accessible name for the group
|
|
61
65
|
* className — additional classes on the outer shell
|
|
62
66
|
*/
|
|
63
|
-
import { toneClass } from '../utilities/tone.js'
|
|
67
|
+
import { toneClass, floatSurfaceClass } from '../utilities/tone.js'
|
|
64
68
|
import { Tooltip } from '../utilities/Popover.jsx'
|
|
65
69
|
|
|
66
|
-
export default function SegmentedToggle({ value, onChange, options = [], variant = 'default', size = 'md', tone = 'default', ariaLabel, className = '' }) {
|
|
70
|
+
export default function SegmentedToggle({ value, onChange, options = [], variant = 'default', size = 'md', tone = 'default', surface = 'solid', ariaLabel, className = '' }) {
|
|
71
|
+
const float = variant === 'float'
|
|
67
72
|
const cellType = { xs: 'kol-mono-8', sm: 'kol-mono-12', md: 'kol-mono-14', lg: 'kol-mono-16' }[size]
|
|
68
73
|
const stateless = value == null
|
|
69
74
|
const focusIdx = Math.max(0, options.findIndex((opt) => opt.value === value))
|
|
@@ -95,6 +100,7 @@ export default function SegmentedToggle({ value, onChange, options = [], variant
|
|
|
95
100
|
(variant === 'filled' || variant === 'tonal') && 'kol-seg--filled',
|
|
96
101
|
variant === 'tonal' && 'kol-seg--tonal',
|
|
97
102
|
size !== 'md' && `kol-seg--${size}`,
|
|
103
|
+
float && `kol-seg--float ${floatSurfaceClass(surface)}`,
|
|
98
104
|
toneClass(resolvedTone),
|
|
99
105
|
className,
|
|
100
106
|
].filter(Boolean).join(' ')}
|
package/src/index.js
CHANGED
|
@@ -54,6 +54,7 @@ export { default as LabeledControl } from './molecules/LabeledControl.jsx'
|
|
|
54
54
|
export { default as OverlayGlassPanel } from './utilities/OverlayGlassPanel.jsx'
|
|
55
55
|
export { default as Pill } from './atoms/Pill.jsx'
|
|
56
56
|
export { usePopover, PopoverPanel, Tooltip } from './utilities/Popover.jsx'
|
|
57
|
+
export { floatSurfaceClass, FLOAT_SURFACES } from './utilities/tone.js'
|
|
57
58
|
/* armLongPress — the touch hold the panel value controls report (moved from kol-hardware 2026-10-01) */
|
|
58
59
|
export { armLongPress } from './utilities/armLongPress.js'
|
|
59
60
|
export { default as ContextMenu, useContextMenu } from './molecules/ContextMenu.jsx'
|
|
@@ -149,7 +150,7 @@ export { default as SplitToolButton } from './molecules/SplitToolButton.jsx'
|
|
|
149
150
|
export { default as ToolPalette } from './organisms/ToolPalette.jsx'
|
|
150
151
|
export { default as DocumentEditor } from './organisms/DocumentEditor.jsx'
|
|
151
152
|
export { default as SwatchControls, SwatchStack, EyedropPick } from './molecules/SwatchControls.jsx'
|
|
152
|
-
export { default as TabsRow, TABBAR_H, TABBAR_PILL_INSET, TABBAR_PILL_TAB, tabbarPillWidth } from './molecules/TabsRow.jsx'
|
|
153
|
+
export { default as TabsRow, TABBAR_H, TABBAR_PILL_H, TABBAR_PILL_INSET, TABBAR_PILL_TAB, TABBAR_PILL_GAP, TABBAR_PILL_LABEL_TAB, TABBAR_PILL_SPLIT_GAP, tabbarPillWidth, tabbarPillSlot } from './molecules/TabsRow.jsx'
|
|
153
154
|
/** @deprecated 2026-10-17 — alias of `TabsRow variant="chip"` */
|
|
154
155
|
export { default as TabChips } from './molecules/TabChips.jsx'
|
|
155
156
|
|
|
@@ -5,6 +5,7 @@ import Button from '../atoms/Button.jsx'
|
|
|
5
5
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
6
6
|
import { RAIL_ROW } from '../hooks/railRow.js'
|
|
7
7
|
import { Tooltip } from '../utilities/Popover.jsx'
|
|
8
|
+
import { floatSurfaceClass } from '../utilities/tone.js'
|
|
8
9
|
|
|
9
10
|
/* taxonomy-ok: nests kol-icons's Icon */
|
|
10
11
|
|
|
@@ -33,7 +34,8 @@ import { Tooltip } from '../utilities/Popover.jsx'
|
|
|
33
34
|
* keeps one line that scrolls sideways with no scrollbar, `.kol-scroll-fade--x` fading the clipped edge(s), and the
|
|
34
35
|
* active tab scrolled into view when it changes — arrow keys included; `wrap` breaks the tabs onto a second line.
|
|
35
36
|
* Every variant but `stacked`, which has its phone bar
|
|
36
|
-
* @param {'underline'|'chip'|'text'|'stacked'} [variant='underline']
|
|
37
|
+
* @param {'underline'|'chip'|'text'|'stacked'|'float'} [variant='underline'] `float` (W16, round 3): `chip`'s row as the floating
|
|
38
|
+
* style's capsules — float heights (28/36/44, touch 36/44/52), 4 apart, the selected one oq-16
|
|
37
39
|
* @param {string} [look] @deprecated 2026-10-30 — the old spelling of `variant`; `variant` wins when both are passed
|
|
38
40
|
* @param {'mono'|'sans'|'mono-light'} [type='mono'] `underline` only: the tab's type — `mono-light` is `sans`'s row on JetBrains Mono at 300 (his word, 2026-10-30)
|
|
39
41
|
* @param {boolean} [filled=false] - `chip` only: every chip shows at rest (Button `tab-filled`), so the row reads as choices
|
|
@@ -62,7 +64,15 @@ import { Tooltip } from '../utilities/Popover.jsx'
|
|
|
62
64
|
* (`--kol-shell-rail-width`) and 64 up, not centred
|
|
63
65
|
* @param {boolean} [shadow=false] the KOL float shadow (`--kol-shadow-float`) — for a pill that floats over content (the transport,
|
|
64
66
|
* a nav over a clashing canvas). Every pill slot names itself on hover in the KOL `Tooltip`
|
|
65
|
-
* @param {'fill'|'circle'} [active='fill'] the lit slot
|
|
67
|
+
* @param {'fill'|'circle'} [active='fill'] the lit slot — since W16 (round 3, 2026-10-31) both are the whole 56 slot as a circle,
|
|
68
|
+
* oq-16; the prop stays so call sites keep working
|
|
69
|
+
* THE FLOAT (W16, round 3 — 2026-10-31, accepted: *"accept the style, both glass styles"*). The pill is fxr's live nav: 66 tall, 56
|
|
70
|
+
* buttons 4 apart, 5 in, 28 glyphs — the same on mouse and touch. Its name on hover is the float Tooltip.
|
|
71
|
+
* @param {'solid'|'glass-dark'|'glass-light'} [surface='solid'] the pill's material (`floatSurfaceClass`); a glass has no
|
|
72
|
+
* hairline (its rim is the edge) and always its own shadow; `solid` keeps `border` and `shadow`
|
|
73
|
+
* @param {boolean} [split=false] the LAST tab (AppFloatNav's ···) stands apart as its own 66 circle, 8 off the bar (iOS 27's
|
|
74
|
+
* grouped toolbar); while `collapsed` the bar fades and the circle is the ×
|
|
75
|
+
* @param {boolean} [labels=false] the tab bar with labels: 66 tall, items 58 tall and 84 wide, 4 in, a 24 glyph over an 11 label
|
|
66
76
|
* @param {boolean} [foldable=false] a sideways swipe (> 40px) folds the pill into a 28×72 edge tab; a tap restores it
|
|
67
77
|
* @param {boolean} [folded] · {Function} [onFoldChange] the folded state, controlled (else the pill holds it)
|
|
68
78
|
* @param {'left'|'right'} [foldSide='left'] the screen edge the tab sits on
|
|
@@ -70,21 +80,38 @@ import { Tooltip } from '../utilities/Popover.jsx'
|
|
|
70
80
|
* lights it whatever `value` is — action slots (the transport) light more than one.
|
|
71
81
|
*/
|
|
72
82
|
export const TABBAR_H = 56
|
|
73
|
-
/* the pill's
|
|
83
|
+
/* the pill's height (W16, round 3 — fxr's live nav: was TABBAR_H) — a page under it owes TABBAR_PILL_H + TABBAR_PILL_INSET */
|
|
84
|
+
export const TABBAR_PILL_H = 66
|
|
85
|
+
/* the pill's air off the bottom edge */
|
|
74
86
|
export const TABBAR_PILL_INSET = 16
|
|
75
|
-
/* the pill's geometry, shared with the
|
|
87
|
+
/* the pill's geometry, shared with the sheets that align to it: 56 per tab, 4 between, 5 in (4 + the 1px hairline) each side;
|
|
88
|
+
* with labels 84 per tab, none between, 4 in; a split last tab is a TABBAR_PILL_H circle 8 off the bar */
|
|
76
89
|
export const TABBAR_PILL_TAB = 56
|
|
77
|
-
export const
|
|
90
|
+
export const TABBAR_PILL_GAP = 4
|
|
91
|
+
export const TABBAR_PILL_LABEL_TAB = 84
|
|
92
|
+
export const TABBAR_PILL_SPLIT_GAP = 8
|
|
93
|
+
const pillBar = (n, labels) => (labels ? n * TABBAR_PILL_LABEL_TAB + 8 : n * TABBAR_PILL_TAB + Math.max(0, n - 1) * TABBAR_PILL_GAP + 10)
|
|
94
|
+
/* the whole pill's width — the bar, plus the split circle when `split` */
|
|
95
|
+
export const tabbarPillWidth = (count, { labels = false, split = false } = {}) => (split
|
|
96
|
+
? pillBar(count - 1, labels) + TABBAR_PILL_SPLIT_GAP + TABBAR_PILL_H
|
|
97
|
+
: pillBar(count, labels))
|
|
98
|
+
/* a slot's centre, from the pill's left edge (a split last slot is the circle's) */
|
|
99
|
+
export const tabbarPillSlot = (at, { count, labels = false, split = false } = {}) => {
|
|
100
|
+
if (split && at === count - 1) return pillBar(count - 1, labels) + TABBAR_PILL_SPLIT_GAP + TABBAR_PILL_H / 2
|
|
101
|
+
return labels ? 4 + at * TABBAR_PILL_LABEL_TAB + TABBAR_PILL_LABEL_TAB / 2 : 5 + at * (TABBAR_PILL_TAB + TABBAR_PILL_GAP) + TABBAR_PILL_TAB / 2
|
|
102
|
+
}
|
|
78
103
|
/* the collapse's motion — one duration for the width and the glyph swap (300, user 2026-10-23: 180 read as abrupt) */
|
|
79
104
|
const COLLAPSE_MS = 300
|
|
80
105
|
|
|
81
|
-
export default function TabsRow({ tabs: raw = [], value, onChange, onClose, onMinimise, variant, look: lookLegacy, type = 'mono', filled = false, placement = 'inline', shape = 'bar', collapsed = false, onExpand, edge, border, shadow, dock, active, foldable, folded, onFoldChange, foldSide, size, tracked = false, overflow = 'scroll', ariaLabel, className, style }) {
|
|
106
|
+
export default function TabsRow({ tabs: raw = [], value, onChange, onClose, onMinimise, variant, look: lookLegacy, type = 'mono', filled = false, placement = 'inline', shape = 'bar', collapsed = false, onExpand, edge, border, shadow, dock, active, foldable, folded, onFoldChange, foldSide, surface, split, labels, size, tracked = false, overflow = 'scroll', ariaLabel, className, style }) {
|
|
82
107
|
const tabs = raw.map((t) => ({ ...t, id: t.id ?? t.value }))
|
|
83
108
|
const look = variant ?? lookLegacy ?? 'underline'
|
|
84
109
|
const wrap = overflow === 'wrap'
|
|
110
|
+
/* `float` (W16, round 3): the chip row as capsules — float heights, pills 4 apart, the selected one oq-16 */
|
|
111
|
+
if (look === 'float') return <Overflow mode={overflow} value={value}><ChipTabs tabs={tabs} value={value} onChange={onChange} size={size ?? 'md'} float ariaLabel={ariaLabel} className={`${wrap ? 'flex-wrap ' : ''}${className ?? ''}`} /></Overflow>
|
|
85
112
|
if (look === 'chip') return <Overflow mode={overflow} value={value}><ChipTabs tabs={tabs} value={value} onChange={onChange} size={size ?? 'sm'} filled={filled} ariaLabel={ariaLabel} className={`${wrap ? 'flex-wrap ' : ''}${className ?? ''}`} /></Overflow>
|
|
86
113
|
if (look === 'text') return <Overflow mode={overflow} value={value}><TextTabs tabs={tabs} value={value} onChange={onChange} size={size ?? 14} tracked={tracked} className={`${wrap ? 'flex-wrap gap-y-2 ' : ''}${className ?? 'gap-6'}`} style={style} /></Overflow>
|
|
87
|
-
if (look === 'stacked') return <StackedTabs tabs={tabs} value={value} onChange={onChange} placement={placement} shape={shape} collapsed={collapsed} onExpand={onExpand} edge={edge} border={border} shadow={shadow} dock={dock} active={active} foldable={foldable} folded={folded} onFoldChange={onFoldChange} foldSide={foldSide} ariaLabel={ariaLabel} className={className ?? ''} />
|
|
114
|
+
if (look === 'stacked') return <StackedTabs tabs={tabs} value={value} onChange={onChange} placement={placement} shape={shape} collapsed={collapsed} onExpand={onExpand} edge={edge} border={border} shadow={shadow} dock={dock} active={active} foldable={foldable} folded={folded} onFoldChange={onFoldChange} foldSide={foldSide} surface={surface} split={split} labels={labels} ariaLabel={ariaLabel} className={className ?? ''} />
|
|
88
115
|
return <Overflow mode={overflow} value={value}><UnderlineTabs tabs={tabs} value={value} onChange={onChange} onClose={onClose} onMinimise={onMinimise} wrap={wrap} type={type} /></Overflow>
|
|
89
116
|
}
|
|
90
117
|
|
|
@@ -108,14 +135,14 @@ function Overflow({ mode, value, children }) {
|
|
|
108
135
|
}
|
|
109
136
|
|
|
110
137
|
/* chip — TabChips verbatim */
|
|
111
|
-
function ChipTabs({ tabs, value, onChange, size, filled, ariaLabel, className }) {
|
|
138
|
+
function ChipTabs({ tabs, value, onChange, size, filled, float = false, ariaLabel, className }) {
|
|
112
139
|
return (
|
|
113
|
-
<div className={`flex items-center ${filled ? 'gap-2' : 'gap-1'} ${className}`.trim()} role="tablist" aria-label={ariaLabel}>
|
|
140
|
+
<div className={`flex items-center ${filled ? 'gap-2' : 'gap-1'}${float ? ' kol-tabs-float' : ''} ${className}`.trim()} role="tablist" aria-label={ariaLabel}>
|
|
114
141
|
{tabs.map((t) => {
|
|
115
142
|
const active = t.id === value
|
|
116
143
|
return (
|
|
117
144
|
/* a tab is selected, not pressed — the role's own attribute replaces Button's aria-pressed */
|
|
118
|
-
<Button key={t.id} variant={filled ? 'tab-filled' : 'tab'} size={size} pressed={active} role="tab" aria-selected={active} aria-pressed={undefined} data-active={active || undefined} onClick={() => onChange?.(t.id)}>
|
|
145
|
+
<Button key={t.id} variant={float ? 'float' : filled ? 'tab-filled' : 'tab'} size={size} pressed={active} role="tab" aria-selected={active} aria-pressed={undefined} data-active={active || undefined} onClick={() => onChange?.(t.id)}>
|
|
119
146
|
{t.label}
|
|
120
147
|
</Button>
|
|
121
148
|
)
|
|
@@ -147,17 +174,24 @@ function TextTabs({ tabs, value, onChange, size, tracked, className, style }) {
|
|
|
147
174
|
/* the fold: a sideways swipe past this folds the pill, as frame E's prototype measures it */
|
|
148
175
|
const FOLD_SWIPE = 40
|
|
149
176
|
|
|
150
|
-
function StackedTabs({ tabs, value, onChange, placement, shape = 'bar', collapsed = false, onExpand, edge = 'bottom', border = true, shadow = false, dock = false, active: activeShape = 'fill', foldable = false, folded: foldedProp, onFoldChange, foldSide = 'left', ariaLabel = 'Sections', className }) {
|
|
177
|
+
function StackedTabs({ tabs, value, onChange, placement, shape = 'bar', collapsed = false, onExpand, edge = 'bottom', border = true, shadow = false, dock = false, active: activeShape = 'fill', foldable = false, folded: foldedProp, onFoldChange, foldSide = 'left', surface = 'solid', split: splitProp = false, labels = false, ariaLabel = 'Sections', className }) {
|
|
151
178
|
const [foldedOwn, setFoldedOwn] = useState(false)
|
|
152
179
|
const swipe = useRef({ x: null, swiped: false })
|
|
153
180
|
if (tabs.length === 0) return null
|
|
154
181
|
const phone = placement === 'phone'
|
|
155
182
|
const pill = phone && shape === 'pill'
|
|
156
183
|
const isCollapsed = phone && collapsed
|
|
157
|
-
const
|
|
184
|
+
const split = pill && splitProp && tabs.length > 1
|
|
185
|
+
const W = tabbarPillWidth(tabs.length, { labels, split })
|
|
158
186
|
const top = pill && edge === 'top'
|
|
159
187
|
const isFolded = pill && foldable && (foldedProp ?? foldedOwn)
|
|
160
188
|
const setFolded = (v) => { if (foldedProp === undefined) setFoldedOwn(v); onFoldChange?.(v) }
|
|
189
|
+
/* THE MATERIAL (W16): solid keeps the pill's ground, hairline and shadow props; a glass is the theme's class, no hairline */
|
|
190
|
+
const glass = surface === 'glass-dark' || surface === 'glass-light'
|
|
191
|
+
const material = glass ? floatSurfaceClass(surface) : ''
|
|
192
|
+
const solidLook = glass ? {} : { background: 'var(--kol-surface-secondary)', boxShadow: shadow ? 'var(--kol-shadow-float)' : undefined }
|
|
193
|
+
/* without the hairline the 1px it took stays, transparent, so the slots never move */
|
|
194
|
+
const edgeLine = `1px solid ${border && !glass ? 'var(--kol-oq-08)' : 'transparent'}`
|
|
161
195
|
/* the edge offset: the pill's inset from the safe area on its edge */
|
|
162
196
|
const inset = `calc(env(safe-area-inset-${top ? 'top' : 'bottom'}, 0px) + ${TABBAR_PILL_INSET}px)`
|
|
163
197
|
const swipeProps = pill && foldable ? {
|
|
@@ -178,101 +212,155 @@ function StackedTabs({ tabs, value, onChange, placement, shape = 'bar', collapse
|
|
|
178
212
|
type="button"
|
|
179
213
|
aria-label={`Show ${ariaLabel.toLowerCase()}`}
|
|
180
214
|
onClick={() => setFolded(false)}
|
|
181
|
-
className=
|
|
215
|
+
className={`kol-mobile-tabbar-tab md:hidden fixed z-[calc(var(--kol-z-sticky)+1)] flex items-center justify-center cursor-pointer ${material}`.trim()}
|
|
182
216
|
style={{
|
|
183
217
|
[foldSide]: 0,
|
|
184
218
|
[top ? 'top' : 'bottom']: `calc(env(safe-area-inset-${top ? 'top' : 'bottom'}, 0px) + ${TABBAR_PILL_INSET + 8}px)`,
|
|
185
219
|
width: 28, height: 72, padding: 0, border: 'none',
|
|
186
220
|
borderRadius: foldSide === 'left' ? '0 16px 16px 0' : '16px 0 0 16px',
|
|
187
|
-
background: 'var(--kol-surface-secondary)', color: RAIL_ROW.rest,
|
|
221
|
+
...(glass ? {} : { background: 'var(--kol-surface-secondary)' }), color: RAIL_ROW.rest,
|
|
188
222
|
}}
|
|
189
223
|
>
|
|
190
224
|
<Icon name={foldSide === 'left' ? 'chevron-right' : 'chevron-left'} size={18} />
|
|
191
225
|
</button>
|
|
192
226
|
)
|
|
193
227
|
}
|
|
228
|
+
const inBar = split ? tabs.slice(0, -1) : tabs
|
|
229
|
+
const last = split ? tabs[tabs.length - 1] : null
|
|
230
|
+
const slotW = labels ? TABBAR_PILL_LABEL_TAB : TABBAR_PILL_TAB
|
|
231
|
+
const slotH = labels ? 58 : TABBAR_PILL_TAB
|
|
232
|
+
const ink = (on) => (phone ? (on ? RAIL_ROW.active : RAIL_ROW.rest) : (on ? 'var(--kol-fg-default)' : 'var(--kol-fg-48)'))
|
|
233
|
+
const tip = (t, el) => <Tooltip key={t.id} label={t.label} placement={top ? 'bottom' : 'top'} offset={10} variant="float" asChild>{el}</Tooltip>
|
|
234
|
+
const slot = (t) => {
|
|
235
|
+
const active = t.selected ?? t.id === value
|
|
236
|
+
if (pill) {
|
|
237
|
+
/* round 3: the lit slot is the whole slot as a circle (a capsule with labels), oq-16 */
|
|
238
|
+
return tip(t, (
|
|
239
|
+
<button
|
|
240
|
+
key={t.id}
|
|
241
|
+
type="button"
|
|
242
|
+
onClick={() => onChange?.(t.id)}
|
|
243
|
+
aria-current={active ? 'page' : undefined}
|
|
244
|
+
aria-label={t.label}
|
|
245
|
+
className={`flex flex-col items-center justify-center rounded-full cursor-pointer flex-none${labels ? ' gap-0.5' : ''}`}
|
|
246
|
+
style={{ width: slotW, height: slotH, padding: 0, border: 'none', background: active ? 'var(--kol-oq-16)' : 'transparent', color: ink(active) }}
|
|
247
|
+
>
|
|
248
|
+
{t.icon && <Icon name={t.icon} size={labels ? 24 : 28} />}
|
|
249
|
+
{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>}
|
|
250
|
+
</button>
|
|
251
|
+
))
|
|
252
|
+
}
|
|
253
|
+
return (
|
|
254
|
+
<button
|
|
255
|
+
key={t.id}
|
|
256
|
+
type="button"
|
|
257
|
+
onClick={() => onChange?.(t.id)}
|
|
258
|
+
aria-current={active ? 'page' : undefined}
|
|
259
|
+
/* the PHONE bar is icons alone (user 2026-10-23: *"skip the title and show just the icon bigger"*) — the
|
|
260
|
+
* label is the button's accessible name */
|
|
261
|
+
aria-label={phone ? t.label : undefined}
|
|
262
|
+
className="flex-1 min-w-0 flex flex-col items-center justify-center gap-1 cursor-pointer"
|
|
263
|
+
/* the PHONE bar wears the rail's row type and ink (review 2, § 7.1 — RAIL_ROW, the one constant the rail reads);
|
|
264
|
+
* an inline stacked row keeps MobileTabBar's own */
|
|
265
|
+
style={{ background: 'transparent', border: 'none', color: ink(active) }}
|
|
266
|
+
>
|
|
267
|
+
{t.icon && <Icon name={t.icon} size={phone ? 24 : 18} />}
|
|
268
|
+
{/* helper is single-line chrome, which a tab label always is */}
|
|
269
|
+
{!phone && <span className="kol-helper-10 truncate max-w-full px-1">{t.label}</span>}
|
|
270
|
+
</button>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
/* the isCollapsed × — the bar's More slot, or (a whole pill, no split) the pill shrunk to one circle; present always so it cross-fades */
|
|
274
|
+
const closeX = phone && !split && (
|
|
275
|
+
<button
|
|
276
|
+
type="button"
|
|
277
|
+
aria-label="Close"
|
|
278
|
+
aria-hidden={!isCollapsed}
|
|
279
|
+
tabIndex={isCollapsed ? 0 : -1}
|
|
280
|
+
onClick={onExpand}
|
|
281
|
+
className="absolute flex items-center justify-center rounded-full cursor-pointer"
|
|
282
|
+
style={{
|
|
283
|
+
/* no fill — the × is the only glyph, a filled circle inside the circle read as two rings (user 2026-10-23) */
|
|
284
|
+
...(pill ? { right: 4, top: 4, width: TABBAR_PILL_TAB, height: TABBAR_PILL_TAB } : { right: 0, top: 0, bottom: 0, width: `${100 / tabs.length}%` }),
|
|
285
|
+
background: 'transparent', border: 'none', color: RAIL_ROW.active,
|
|
286
|
+
opacity: isCollapsed ? 1 : 0, pointerEvents: isCollapsed ? 'auto' : 'none', transition: `opacity ${COLLAPSE_MS}ms ease`,
|
|
287
|
+
}}
|
|
288
|
+
>
|
|
289
|
+
<Icon name="x" size={pill ? 28 : 24} />
|
|
290
|
+
</button>
|
|
291
|
+
)
|
|
292
|
+
const row = (
|
|
293
|
+
<div className={`flex items-stretch ${pill ? 'flex-none' : 'flex-1 min-w-0'}`} style={{ gap: pill && !labels ? TABBAR_PILL_GAP : undefined, opacity: isCollapsed ? 0 : 1, pointerEvents: isCollapsed ? 'none' : 'auto', transition: `opacity ${COLLAPSE_MS}ms ease` }}>
|
|
294
|
+
{inBar.map(slot)}
|
|
295
|
+
</div>
|
|
296
|
+
)
|
|
297
|
+
if (!pill) {
|
|
298
|
+
return (
|
|
299
|
+
<nav
|
|
300
|
+
className={`${phone ? 'kol-mobile-tabbar md:hidden fixed inset-x-0 bottom-0 z-[var(--kol-z-sticky)] ' : ''}flex items-stretch ${className}`.trim()}
|
|
301
|
+
style={phone ? {
|
|
302
|
+
height: TABBAR_H,
|
|
303
|
+
/* the pill's own ground, not the page's — it floats OVER content, so a
|
|
304
|
+
* transparent bar would show rows sliding through the labels */
|
|
305
|
+
background: 'var(--kol-surface-primary)',
|
|
306
|
+
borderTop: '1px solid var(--kol-oq-08)',
|
|
307
|
+
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
|
|
308
|
+
} : { height: TABBAR_H }}
|
|
309
|
+
aria-label={ariaLabel}
|
|
310
|
+
>
|
|
311
|
+
{row}
|
|
312
|
+
{closeX}
|
|
313
|
+
</nav>
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
const barW = W - (split ? TABBAR_PILL_SPLIT_GAP + TABBAR_PILL_H : 0)
|
|
194
317
|
return (
|
|
195
318
|
<nav
|
|
196
319
|
{...swipeProps}
|
|
197
|
-
className={
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
height: TABBAR_H,
|
|
205
|
-
width: isCollapsed ? TABBAR_H : W,
|
|
320
|
+
className={`kol-mobile-tabbar kol-mobile-tabbar-pill ${dock ? 'hidden md:flex' : 'md:hidden'} fixed z-[calc(var(--kol-z-sticky)+1)] flex items-stretch ${className}`.trim()}
|
|
321
|
+
style={{
|
|
322
|
+
/* THE PILL (user 2026-10-23, iOS Files beside the full-width bar; round 3's size, W16): floats off the bottom edge,
|
|
323
|
+
* hugs its tabs. Anchored by its RIGHT edge at the centre line, so the collapse shrinks it toward where More was
|
|
324
|
+
* and the × lands under the same thumb. */
|
|
325
|
+
height: TABBAR_PILL_H,
|
|
326
|
+
gap: TABBAR_PILL_SPLIT_GAP,
|
|
206
327
|
...(dock
|
|
207
328
|
? { left: 'calc(var(--kol-shell-rail-width, 48px) + 64px)', bottom: 64 }
|
|
208
329
|
: { right: `calc(50% - ${W / 2}px)`, [top ? 'top' : 'bottom']: inset }),
|
|
209
|
-
background: 'var(--kol-surface-secondary)',
|
|
210
|
-
/* without the hairline the 1px it took stays, transparent, so the slots never move */
|
|
211
|
-
border: `1px solid ${border ? 'var(--kol-oq-08)' : 'transparent'}`,
|
|
212
|
-
/* the KOL float shadow (kol-theme `--kol-shadow-float`, FloatingStyleAndSignInDemos 2026-10-30) — was a local 40px blur */
|
|
213
|
-
boxShadow: shadow ? 'var(--kol-shadow-float)' : undefined,
|
|
214
|
-
transition: `width ${COLLAPSE_MS}ms ease`,
|
|
215
330
|
...swipeProps.style,
|
|
216
|
-
}
|
|
217
|
-
height: TABBAR_H,
|
|
218
|
-
/* the pill's own ground, not the page's — it floats OVER content, so a
|
|
219
|
-
* transparent bar would show rows sliding through the labels */
|
|
220
|
-
background: 'var(--kol-surface-primary)',
|
|
221
|
-
borderTop: '1px solid var(--kol-oq-08)',
|
|
222
|
-
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
|
|
223
|
-
} : { height: TABBAR_H }}
|
|
331
|
+
}}
|
|
224
332
|
aria-label={ariaLabel}
|
|
225
333
|
>
|
|
226
|
-
<div
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
className={pill ? 'flex items-center justify-center rounded-full cursor-pointer' : 'flex-1 min-w-0 flex flex-col items-center justify-center gap-1 cursor-pointer'}
|
|
242
|
-
/* the PHONE bar wears the rail's row type and ink (review 2, § 7.1 — RAIL_ROW, the one constant the rail reads);
|
|
243
|
-
* an inline stacked row keeps MobileTabBar's own */
|
|
244
|
-
style={{ background: pill && active && !circle ? 'var(--kol-oq-08)' : 'transparent', border: 'none', width: pill ? 56 : undefined, color: phone ? (active ? RAIL_ROW.active : RAIL_ROW.rest) : (active ? 'var(--kol-fg-default)' : 'var(--kol-fg-48)') }}
|
|
245
|
-
>
|
|
246
|
-
{/* the circle: 44×44, 1:1 inside the 56 slot (frame E — the fill stretched to the slot's width) */}
|
|
247
|
-
{t.icon && (circle
|
|
248
|
-
? <span className="inline-flex items-center justify-center rounded-full" style={{ width: 44, height: 44, background: active ? 'var(--kol-oq-08)' : 'transparent' }}><Icon name={t.icon} size={28} /></span>
|
|
249
|
-
: <Icon name={t.icon} size={pill ? 28 : phone ? 24 : 18} />)}
|
|
250
|
-
{/* helper is single-line chrome, which a tab label always is */}
|
|
251
|
-
{!phone && <span className="kol-helper-10 truncate max-w-full px-1">{t.label}</span>}
|
|
252
|
-
</button>
|
|
253
|
-
)
|
|
254
|
-
return pill ? <Tooltip key={t.id} label={t.label} placement={top ? 'bottom' : 'top'} offset={10} asChild>{slot}</Tooltip> : slot
|
|
255
|
-
})}
|
|
334
|
+
<div
|
|
335
|
+
className={`relative flex items-center rounded-full overflow-hidden ${material}`.trim()}
|
|
336
|
+
style={{
|
|
337
|
+
height: TABBAR_PILL_H,
|
|
338
|
+
width: isCollapsed && !split ? TABBAR_PILL_H : barW,
|
|
339
|
+
padding: labels ? 3 : 4,
|
|
340
|
+
border: edgeLine,
|
|
341
|
+
...solidLook,
|
|
342
|
+
opacity: isCollapsed && split ? 0 : 1,
|
|
343
|
+
pointerEvents: isCollapsed && split ? 'none' : undefined,
|
|
344
|
+
transition: `width ${COLLAPSE_MS}ms ease, opacity ${COLLAPSE_MS}ms ease`,
|
|
345
|
+
}}
|
|
346
|
+
>
|
|
347
|
+
{row}
|
|
348
|
+
{closeX}
|
|
256
349
|
</div>
|
|
257
|
-
{
|
|
258
|
-
/* the
|
|
350
|
+
{split && tip(isCollapsed ? { id: last.id, label: 'Close' } : last, (
|
|
351
|
+
/* the split circle (round 3, iOS 27's grouped toolbar): ··· on its own, 8 off the bar; the × while collapsed */
|
|
259
352
|
<button
|
|
353
|
+
key={last.id}
|
|
260
354
|
type="button"
|
|
261
|
-
|
|
262
|
-
aria-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
style={{
|
|
267
|
-
/* no fill — the × is the only glyph, a filled circle inside the circle read as two rings (user 2026-10-23) */
|
|
268
|
-
...(pill ? { right: 6, top: 6, width: TABBAR_H - 14, height: TABBAR_H - 14 } : { right: 0, top: 0, bottom: 0, width: `${100 / tabs.length}%` }),
|
|
269
|
-
background: 'transparent', border: 'none', color: RAIL_ROW.active,
|
|
270
|
-
opacity: isCollapsed ? 1 : 0, pointerEvents: isCollapsed ? 'auto' : 'none', transition: `opacity ${COLLAPSE_MS}ms ease`,
|
|
271
|
-
}}
|
|
355
|
+
onClick={() => (isCollapsed ? onExpand?.() : onChange?.(last.id))}
|
|
356
|
+
aria-current={!isCollapsed && (last.selected ?? last.id === value) ? 'page' : undefined}
|
|
357
|
+
aria-label={isCollapsed ? 'Close' : last.label}
|
|
358
|
+
className={`flex items-center justify-center rounded-full cursor-pointer flex-none ${material}`.trim()}
|
|
359
|
+
style={{ width: TABBAR_PILL_H, height: TABBAR_PILL_H, padding: 0, border: edgeLine, ...solidLook, color: ink(isCollapsed || (last.selected ?? last.id === value)) }}
|
|
272
360
|
>
|
|
273
|
-
<Icon name=
|
|
361
|
+
<Icon name={isCollapsed ? 'x' : last.icon} size={labels ? 24 : 28} />
|
|
274
362
|
</button>
|
|
275
|
-
)}
|
|
363
|
+
))}
|
|
276
364
|
</nav>
|
|
277
365
|
)
|
|
278
366
|
}
|
|
@@ -10,8 +10,9 @@ import usePrefersReducedMotion from '../hooks/usePrefersReducedMotion.js'
|
|
|
10
10
|
*
|
|
11
11
|
* THE POINTER IS A FORCE — a move pushes the line aside, a press blasts a crater it regrows into (engine `poke`);
|
|
12
12
|
* either wakes a finished line, which settles and sleeps again a second after the pointer stops. A resting pointer is
|
|
13
|
-
* not a force
|
|
14
|
-
* `bleed`), frozen there. Read on
|
|
13
|
+
* not a force. THE LINE RESTS (engine `settle`, kol-fxr 2026-10-30): the packed interior cools and stops, so only the
|
|
14
|
+
* growing front and what the pointer touches move; it runs 48px past the edges (engine `bleed`), frozen there. Read on
|
|
15
|
+
* the window,
|
|
15
16
|
* so the stage can sit behind a page; a press on a control is the control's.
|
|
16
17
|
*
|
|
17
18
|
* REDUCED MOTION — the finished line is grown off screen and drawn once: no animation, no pointer force.
|
|
@@ -37,7 +38,7 @@ export default function GrowthLineStage({ shape = 'orb', fit = 0.6, params = ORB
|
|
|
37
38
|
const canvas = ref.current
|
|
38
39
|
if (!canvas) return undefined
|
|
39
40
|
let sim = null, raf = 0, w = 0, h = 0
|
|
40
|
-
let ptr = null, sm = null,
|
|
41
|
+
let ptr = null, sm = null, idle = 0, full = false
|
|
41
42
|
const force = pointer && !reduced
|
|
42
43
|
const start = () => {
|
|
43
44
|
const r = canvas.getBoundingClientRect()
|
|
@@ -52,8 +53,9 @@ export default function GrowthLineStage({ shape = 'orb', fit = 0.6, params = ORB
|
|
|
52
53
|
rand: seed == null ? Math.random : mulberry32(seed),
|
|
53
54
|
cap: cap ?? Math.min(14000, Math.round(w * h / 112)),
|
|
54
55
|
})
|
|
55
|
-
/* the line runs past the edges and freezes there instead of piling into them
|
|
56
|
-
|
|
56
|
+
/* the line runs past the edges and freezes there instead of piling into them, and rests where it is done; one
|
|
57
|
+
* step a frame keeps 0.2.0's pace now that only the front grows */
|
|
58
|
+
sim.setParams({ bleed: 48, settle: 120, ...params })
|
|
57
59
|
sim.seed()
|
|
58
60
|
full = false
|
|
59
61
|
return true
|
|
@@ -73,11 +75,11 @@ export default function GrowthLineStage({ shape = 'orb', fit = 0.6, params = ORB
|
|
|
73
75
|
/* low-passed like the editor's stage pointer — the raw one twitches */
|
|
74
76
|
const to = sm ? { x: sm.x + (ptr.x - sm.x) * 0.1, y: sm.y + (ptr.y - sm.y) * 0.1 } : { x: ptr.x, y: ptr.y }
|
|
75
77
|
/* a RESTING pointer is not a force — poked every frame, the line under it churned forever */
|
|
76
|
-
if (ptr.down || !sm || Math.hypot(to.x - sm.x, to.y - sm.y) > 0.3) { sim.poke(to.x, to.y, ptr.down);
|
|
78
|
+
if (ptr.down || !sm || Math.hypot(to.x - sm.x, to.y - sm.y) > 0.3) { sim.poke(to.x, to.y, ptr.down); idle = 60 }
|
|
77
79
|
sm = to
|
|
78
80
|
} else sm = null
|
|
79
|
-
/* a
|
|
80
|
-
if (growing
|
|
81
|
+
/* settled stretches rest, so a step moves only the growing front and the pointer's wake */
|
|
82
|
+
if (growing || idle > 0) sim.step()
|
|
81
83
|
if (idle > 0) idle--
|
|
82
84
|
paint()
|
|
83
85
|
raf = growing || idle > 0 ? requestAnimationFrame(frame) : 0
|
|
@@ -177,6 +177,8 @@ function TooltipOn({
|
|
|
177
177
|
triggerClassName = 'inline-flex',
|
|
178
178
|
asChild = false,
|
|
179
179
|
disabled = false,
|
|
180
|
+
/* `float` (W16, round 3 — 2026-10-31): the floating style's tooltip, an inverted pill 24 (28 on touch) tall, 11 (12) type */
|
|
181
|
+
variant,
|
|
180
182
|
}) {
|
|
181
183
|
const [open, setOpen] = useState(false)
|
|
182
184
|
const [covered, setCovered] = useState(false)
|
|
@@ -238,7 +240,7 @@ function TooltipOn({
|
|
|
238
240
|
popover={popover}
|
|
239
241
|
focus={false}
|
|
240
242
|
panel={false}
|
|
241
|
-
className=
|
|
243
|
+
className={variant === 'float' ? 'kol-tooltip kol-tooltip-float' : 'kol-tooltip'}
|
|
242
244
|
style={ambient ?? undefined}
|
|
243
245
|
>
|
|
244
246
|
<span>{label}</span>
|
package/src/utilities/tone.js
CHANGED
|
@@ -41,3 +41,16 @@ export const TONE_VARS = [
|
|
|
41
41
|
'pressed-bg', 'pressed-image', 'pressed-fg',
|
|
42
42
|
'panel-bg', 'panel-border-w', 'panel-border',
|
|
43
43
|
].map((k) => `--kol-tone-${k}`)
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* floatSurfaceClass — what a float is made of (W16, round 3 — 2026-10-31: *"accept the style, both glass styles"*).
|
|
47
|
+
* `surface="solid|glass-dark|glass-light"` on the float components resolves here to kol-theme's classes, never inline styles:
|
|
48
|
+
* `solid` (surface-secondary + the float shadow), `glass-dark` (Apple's dark glass, plus `.dark` — a dark material in either
|
|
49
|
+
* theme, so what sits on it takes dark-theme ink), `glass-light` (the light tint). `medium` marks a menu or a box (the
|
|
50
|
+
* deeper blur and shadow); a bar, a button or a track is small. Unknown → solid.
|
|
51
|
+
*/
|
|
52
|
+
export const FLOAT_SURFACES = ['solid', 'glass-dark', 'glass-light']
|
|
53
|
+
export const floatSurfaceClass = (surface = 'solid', medium = false) => {
|
|
54
|
+
const s = FLOAT_SURFACES.includes(surface) ? surface : 'solid'
|
|
55
|
+
return `kol-float-${s}${medium ? ' kol-float-m' : ''}${s === 'glass-dark' ? ' dark' : ''}`
|
|
56
|
+
}
|