@kolkrabbi/kol-component 0.317.7 → 0.319.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 +2 -1
- package/src/atoms/Button.jsx +2 -2
- package/src/atoms/Input.jsx +2 -0
- package/src/index.js +2 -0
- package/src/molecules/TabsRow.jsx +9 -3
- package/src/organisms/GrowthLineStage.jsx +126 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.319.0",
|
|
4
4
|
"description": "The core component library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,6 +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.1.0",
|
|
29
30
|
"@kolkrabbi/kol-search": "^0.3.0",
|
|
30
31
|
"@kolkrabbi/kol-markdown": "^0.2.0"
|
|
31
32
|
},
|
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). `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'} 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` (2026-10-30, FloatingStyleAndSignInDemos — the floating style B) is the shape only: a 44 control (44×44 icon-only), an 18 radius; the ground stays the `tone`'s. 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
|
|
@@ -93,7 +93,7 @@ const Button = ({
|
|
|
93
93
|
warned.add(variant)
|
|
94
94
|
console.warn(`Button: variant="${variant}" is deprecated — use tone="${VARIANT_TONE[variant]}"`)
|
|
95
95
|
}
|
|
96
|
-
const KNOWN = ['primary', 'secondary', 'accent', 'outline', 'ghost', 'nav', 'tab', 'tab-filled', 'danger', 'grey']
|
|
96
|
+
const KNOWN = ['primary', 'secondary', 'accent', 'outline', 'ghost', 'nav', 'tab', 'tab-filled', 'danger', 'grey', 'float']
|
|
97
97
|
const known = !resolvedVariant || KNOWN.includes(resolvedVariant)
|
|
98
98
|
if (!known && import.meta.env.DEV) console.warn(`Button: unknown variant "${resolvedVariant}" — rendering the inherited tone (primary)`)
|
|
99
99
|
const variantClass = resolvedVariant && known ? `kol-btn-${resolvedVariant}` : ''
|
package/src/atoms/Input.jsx
CHANGED
|
@@ -11,6 +11,8 @@ 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 B (2026-10-30): 44 tall, an 18 radius, on the page's ground
|
|
15
|
+
* (surface-primary) inside a float box; AccountForm's box passes it
|
|
14
16
|
* tone="inverse" — the dark chip (`fg-ab-24`) for a washed plane
|
|
15
17
|
* (ControlToneInverse, 2026-08-27); same prop on ViewToggle · Dropdown
|
|
16
18
|
* size="xs" — the panel rung (ControlsXsRung, 2026-09-01):
|
package/src/index.js
CHANGED
|
@@ -195,6 +195,8 @@ export { default as ErrorBoundary } from './utilities/ErrorBoundary.jsx'
|
|
|
195
195
|
export { default as FeatureSplit } from './organisms/FeatureSplit.jsx'
|
|
196
196
|
export { default as FeaturedCarousel } from './organisms/FeaturedCarousel.jsx'
|
|
197
197
|
export { default as FeaturesCardSection } from './organisms/FeaturesCardSection.jsx'
|
|
198
|
+
/* the growing line on @kolkrabbi/kol-growth (GrowthLineStage, kol-atlas 2026-10-30) */
|
|
199
|
+
export { default as GrowthLineStage } from './organisms/GrowthLineStage.jsx'
|
|
198
200
|
export { default as SectionHero, default as FullBleedHero } from './organisms/SectionHero.jsx'
|
|
199
201
|
export { default as SectionSplit } from './organisms/SectionSplit.jsx'
|
|
200
202
|
export { default as SectionCards } from './organisms/SectionCards.jsx'
|
|
@@ -4,6 +4,7 @@ import CloseButton from '../atoms/CloseButton.jsx'
|
|
|
4
4
|
import Button from '../atoms/Button.jsx'
|
|
5
5
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
6
6
|
import { RAIL_ROW } from '../hooks/railRow.js'
|
|
7
|
+
import { Tooltip } from '../utilities/Popover.jsx'
|
|
7
8
|
|
|
8
9
|
/* taxonomy-ok: nests kol-icons's Icon */
|
|
9
10
|
|
|
@@ -59,7 +60,8 @@ import { RAIL_ROW } from '../hooks/railRow.js'
|
|
|
59
60
|
* that component shoudl probablyget its own variant"*): the pill on a DESK — shown from md up, hidden
|
|
60
61
|
* below (the phone has its own nav), placed at the content's bottom-left, 64 in from the rail's edge
|
|
61
62
|
* (`--kol-shell-rail-width`) and 64 up, not centred
|
|
62
|
-
* @param {boolean} [shadow=false]
|
|
63
|
+
* @param {boolean} [shadow=false] the KOL float shadow (`--kol-shadow-float`) — for a pill that floats over content (the transport,
|
|
64
|
+
* a nav over a clashing canvas). Every pill slot names itself on hover in the KOL `Tooltip`
|
|
63
65
|
* @param {'fill'|'circle'} [active='fill'] the lit slot: `fill` its whole 56 slot (today), `circle` a 44×44 1:1 circle
|
|
64
66
|
* @param {boolean} [foldable=false] a sideways swipe (> 40px) folds the pill into a 28×72 edge tab; a tap restores it
|
|
65
67
|
* @param {boolean} [folded] · {Function} [onFoldChange] the folded state, controlled (else the pill holds it)
|
|
@@ -207,7 +209,8 @@ function StackedTabs({ tabs, value, onChange, placement, shape = 'bar', collapse
|
|
|
207
209
|
background: 'var(--kol-surface-secondary)',
|
|
208
210
|
/* without the hairline the 1px it took stays, transparent, so the slots never move */
|
|
209
211
|
border: `1px solid ${border ? 'var(--kol-oq-08)' : 'transparent'}`,
|
|
210
|
-
|
|
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,
|
|
211
214
|
transition: `width ${COLLAPSE_MS}ms ease`,
|
|
212
215
|
...swipeProps.style,
|
|
213
216
|
} : phone ? {
|
|
@@ -224,7 +227,9 @@ function StackedTabs({ tabs, value, onChange, placement, shape = 'bar', collapse
|
|
|
224
227
|
{tabs.map((t) => {
|
|
225
228
|
const active = t.selected ?? t.id === value
|
|
226
229
|
const circle = pill && activeShape === 'circle'
|
|
227
|
-
|
|
230
|
+
/* THE NAME ON HOVER IS THE KOL TOOLTIP (AppFloatNav, kol-fxr 2026-10-30 — the Round 3 board), on the pill's far side
|
|
231
|
+
* from its edge; a thumb never hovers, so the phone is unchanged */
|
|
232
|
+
const slot = (
|
|
228
233
|
<button
|
|
229
234
|
key={t.id}
|
|
230
235
|
type="button"
|
|
@@ -246,6 +251,7 @@ function StackedTabs({ tabs, value, onChange, placement, shape = 'bar', collapse
|
|
|
246
251
|
{!phone && <span className="kol-helper-10 truncate max-w-full px-1">{t.label}</span>}
|
|
247
252
|
</button>
|
|
248
253
|
)
|
|
254
|
+
return pill ? <Tooltip key={t.id} label={t.label} placement={top ? 'bottom' : 'top'} offset={10} asChild>{slot}</Tooltip> : slot
|
|
249
255
|
})}
|
|
250
256
|
</div>
|
|
251
257
|
{phone && (
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { Growth, densityFrom, mulberry32, orbCanvas, pathCanvas } from '@kolkrabbi/kol-growth'
|
|
3
|
+
import usePrefersReducedMotion from '../hooks/usePrefersReducedMotion.js'
|
|
4
|
+
|
|
5
|
+
/* THE GROWING LINE AS A STAGE (GrowthLineStage, kol-atlas 2026-10-30) — kol-fxr's HomeStage sim loop on
|
|
6
|
+
* @kolkrabbi/kol-growth: one closed line grows into `shape`, packing tighter where it is dark. One <canvas> filling its
|
|
7
|
+
* box at dpr ≤ 2; the sim runs in CSS px, so the spacing reads the same on a phone as on a desk. It grows to its cap and
|
|
8
|
+
* then stops drawing (no rAF); a resize starts it again. Ink is the canvas's computed `color` (a `text-*` class), a
|
|
9
|
+
* `data-theme` flip repaints a finished line; the stage clears to transparent, no paper.
|
|
10
|
+
*
|
|
11
|
+
* THE POINTER IS A FORCE — a move pushes the line aside, a press blasts a crater it regrows into (engine `poke`);
|
|
12
|
+
* either wakes a finished line, which settles and sleeps again a second after the pointer leaves. Read on the window,
|
|
13
|
+
* so the stage can sit behind a page; a press on a control is the control's.
|
|
14
|
+
*
|
|
15
|
+
* REDUCED MOTION — the finished line is grown off screen and drawn once: no animation, no pointer force.
|
|
16
|
+
*
|
|
17
|
+
* `shape` — 'orb' · an SVG path `d` (centred at `fit` of the box) · `(w, h) => canvas` (dark on light). */
|
|
18
|
+
const ORB_PARAMS = { minGap: 6, maxGap: 24, contrast: 1.4, growth: 8 }
|
|
19
|
+
|
|
20
|
+
const shapeCanvas = (shape, w, h, fit) => {
|
|
21
|
+
if (typeof shape === 'function') return shape(w, h)
|
|
22
|
+
if (shape === 'orb' || !shape) return orbCanvas(w / h)
|
|
23
|
+
return pathCanvas(shape, w / h, fit)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function GrowthLineStage({ shape = 'orb', fit = 0.6, params = ORB_PARAMS, cap, pointer = true, seed, className = '' }) {
|
|
27
|
+
const ref = useRef(null)
|
|
28
|
+
const reduced = usePrefersReducedMotion()
|
|
29
|
+
/* the props are read at (re)start; the effect re-runs when one changes by value */
|
|
30
|
+
const key = JSON.stringify([typeof shape === 'function' ? null : shape, fit, params, cap, pointer, seed, reduced])
|
|
31
|
+
const shapeRef = useRef(shape)
|
|
32
|
+
shapeRef.current = shape
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const canvas = ref.current
|
|
36
|
+
if (!canvas) return undefined
|
|
37
|
+
let sim = null, raf = 0, w = 0, h = 0
|
|
38
|
+
let ptr = null, sm = null, idle = 0
|
|
39
|
+
const force = pointer && !reduced
|
|
40
|
+
const start = () => {
|
|
41
|
+
const r = canvas.getBoundingClientRect()
|
|
42
|
+
w = Math.max(1, r.width); h = Math.max(1, r.height)
|
|
43
|
+
const src = shapeCanvas(shapeRef.current, w, h, fit)
|
|
44
|
+
if (!src) return false
|
|
45
|
+
const dpr = Math.min(2, window.devicePixelRatio || 1)
|
|
46
|
+
canvas.width = Math.round(w * dpr); canvas.height = Math.round(h * dpr)
|
|
47
|
+
canvas.getContext('2d').setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
48
|
+
sim = new Growth({
|
|
49
|
+
w, h, density: densityFrom(src),
|
|
50
|
+
rand: seed == null ? Math.random : mulberry32(seed),
|
|
51
|
+
cap: cap ?? Math.min(14000, Math.round(w * h / 112)),
|
|
52
|
+
})
|
|
53
|
+
sim.setParams(params)
|
|
54
|
+
sim.seed()
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
const paint = () => {
|
|
58
|
+
const g = canvas.getContext('2d')
|
|
59
|
+
g.clearRect(0, 0, w, h)
|
|
60
|
+
sim.draw(g, w, h, { ink: getComputedStyle(canvas).color, width: 1 })
|
|
61
|
+
}
|
|
62
|
+
/* grows until the cap, then the loop stops — a finished line costs nothing until the pointer touches it */
|
|
63
|
+
const frame = () => {
|
|
64
|
+
const growing = sim.x.length < sim.cap
|
|
65
|
+
if (ptr) {
|
|
66
|
+
/* low-passed like the editor's stage pointer — the raw one twitches */
|
|
67
|
+
sm = 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 }
|
|
68
|
+
sim.poke(sm.x, sm.y, ptr.down)
|
|
69
|
+
idle = 60
|
|
70
|
+
} else sm = null
|
|
71
|
+
if (growing || idle > 0) { sim.step(); if (growing) sim.step() }
|
|
72
|
+
if (idle > 0) idle--
|
|
73
|
+
paint()
|
|
74
|
+
raf = growing || idle > 0 ? requestAnimationFrame(frame) : 0
|
|
75
|
+
}
|
|
76
|
+
const wake = () => { if (!raf && sim) raf = requestAnimationFrame(frame) }
|
|
77
|
+
const restart = () => {
|
|
78
|
+
cancelAnimationFrame(raf); raf = 0
|
|
79
|
+
if (!start()) return
|
|
80
|
+
/* reduced motion grows 4× the splits a step: nothing is watched growing, so it reaches the cap sooner */
|
|
81
|
+
if (reduced) sim.setParams({ growth: sim.growth * 4 })
|
|
82
|
+
raf = requestAnimationFrame(reduced ? settle : frame)
|
|
83
|
+
}
|
|
84
|
+
/* reduced motion: grown to the end off screen, ~12ms of steps a frame so the page never blocks, then drawn once */
|
|
85
|
+
const settle = () => {
|
|
86
|
+
const t = performance.now()
|
|
87
|
+
while (sim.x.length < sim.cap && performance.now() - t < 12) sim.step()
|
|
88
|
+
if (sim.x.length < sim.cap) { raf = requestAnimationFrame(settle); return }
|
|
89
|
+
raf = 0
|
|
90
|
+
paint()
|
|
91
|
+
}
|
|
92
|
+
restart()
|
|
93
|
+
const ro = new ResizeObserver(restart)
|
|
94
|
+
ro.observe(canvas)
|
|
95
|
+
const at = (e) => {
|
|
96
|
+
const r = canvas.getBoundingClientRect()
|
|
97
|
+
const x = e.clientX - r.left, y = e.clientY - r.top
|
|
98
|
+
return x >= 0 && y >= 0 && x <= r.width && y <= r.height ? { x, y } : null
|
|
99
|
+
}
|
|
100
|
+
const onMove = (e) => { const p = at(e); ptr = p && { ...p, down: ptr?.down ?? false }; if (ptr) wake() }
|
|
101
|
+
const onDown = (e) => { if (e.target.closest?.('button, a, input, textarea, select, [role="menu"]')) return; const p = at(e); if (p) { ptr = { ...p, down: true }; wake() } }
|
|
102
|
+
const onUp = () => { if (ptr) ptr = { ...ptr, down: false } }
|
|
103
|
+
const onLeave = () => { ptr = null }
|
|
104
|
+
if (force) {
|
|
105
|
+
window.addEventListener('pointermove', onMove)
|
|
106
|
+
window.addEventListener('pointerdown', onDown)
|
|
107
|
+
window.addEventListener('pointerup', onUp)
|
|
108
|
+
document.addEventListener('pointerleave', onLeave)
|
|
109
|
+
}
|
|
110
|
+
/* the ink follows a theme flip on a finished line too */
|
|
111
|
+
const mo = new MutationObserver(() => { if (!raf && sim) paint() })
|
|
112
|
+
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] })
|
|
113
|
+
return () => {
|
|
114
|
+
cancelAnimationFrame(raf); ro.disconnect(); mo.disconnect()
|
|
115
|
+
if (force) {
|
|
116
|
+
window.removeEventListener('pointermove', onMove)
|
|
117
|
+
window.removeEventListener('pointerdown', onDown)
|
|
118
|
+
window.removeEventListener('pointerup', onUp)
|
|
119
|
+
document.removeEventListener('pointerleave', onLeave)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
123
|
+
}, [key])
|
|
124
|
+
|
|
125
|
+
return <canvas ref={ref} aria-hidden="true" className={`block size-full ${className}`.trim()} />
|
|
126
|
+
}
|