@kolkrabbi/kol-component 0.21.0 → 0.24.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 -2
- package/src/atoms/Button.jsx +9 -1
- package/src/atoms/IconFrame.jsx +47 -7
- package/src/hooks/useScrollSpy.js +8 -1
- package/src/index.js +8 -0
- package/src/molecules/CodeBlock.jsx +16 -2
- package/src/molecules/ShellDrawer.jsx +14 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "KOL design-system components — atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@floating-ui/react": "^0.27.19",
|
|
25
25
|
"embla-carousel-react": "^8.6.0",
|
|
26
26
|
"react-syntax-highlighter": "^16.1.1",
|
|
27
|
-
"@kolkrabbi/kol-icons": "0.
|
|
27
|
+
"@kolkrabbi/kol-icons": "0.10.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"framer-motion": "^12.0.0",
|
package/src/atoms/Button.jsx
CHANGED
|
@@ -11,7 +11,7 @@ import { glyphSize } from '../hooks/glyphLadders.js'
|
|
|
11
11
|
*
|
|
12
12
|
* @param {Object} props
|
|
13
13
|
* @param {ReactNode} props.children - Button content
|
|
14
|
-
* @param {'primary'|'secondary'|'accent'|'outline'|'ghost'|'danger'|'grey'|'control'} props.variant - Visual variant. `danger` is the destructive treatment (--ui-error fill); `control` is an alias for `ghost` (legacy call-sites).
|
|
14
|
+
* @param {'primary'|'secondary'|'accent'|'outline'|'ghost'|'nav'|'danger'|'grey'|'control'} props.variant - Visual variant. `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
15
|
* @param {'sm'|'md'|'lg'} props.size - Button size (default: 'md')
|
|
16
16
|
* @param {string} props.iconLeft - Icon name to display on the left
|
|
17
17
|
* @param {string} props.iconRight - Icon name to display on the right
|
|
@@ -82,6 +82,14 @@ const Button = ({
|
|
|
82
82
|
? 'kol-btn-danger'
|
|
83
83
|
: resolvedVariant === 'grey'
|
|
84
84
|
? 'kol-btn-grey'
|
|
85
|
+
/* `nav` — transparent box, oq-64 ink (2026-08-01). The class had lived in
|
|
86
|
+
* the theme since the shell landed with NO component able to emit it, so
|
|
87
|
+
* every consumer that wanted this exact weight hand-wrote the box instead:
|
|
88
|
+
* that orphan is the direct cause of the four-container header. It is the
|
|
89
|
+
* chrome rung — one step brighter than `ghost` at oq-48 — and it is what
|
|
90
|
+
* `text-fg-64` meant every time a call site typed it. */
|
|
91
|
+
: resolvedVariant === 'nav'
|
|
92
|
+
? 'kol-btn-nav'
|
|
85
93
|
: 'kol-btn-secondary'
|
|
86
94
|
|
|
87
95
|
// Add size class — pairs the padding rule with its mono type class.
|
package/src/atoms/IconFrame.jsx
CHANGED
|
@@ -36,8 +36,27 @@ import { SOLO } from '../hooks/glyphLadders.js'
|
|
|
36
36
|
* moves with it — that is the 2026-07-28 law, and it only means something if the
|
|
37
37
|
* two are separable.
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* IT TAKES A CLICK (2026-08-01 ruling) — *"you dont use a button… you use the
|
|
40
|
+
* ICON COMPONENT… it has no interactive states."* This file used to say the
|
|
41
|
+
* opposite, and called it "the entire point": no `onClick`, no `href`, and
|
|
42
|
+
* *"wanting any of those means wanting a Button with iconOnly, not this."*
|
|
43
|
+
*
|
|
44
|
+
* That was one sentence too strong. Two separate things had been welded
|
|
45
|
+
* together — **is it clickable** and **does it light up** — and only the second
|
|
46
|
+
* was ever the point. The shell header proved it: six chrome controls all
|
|
47
|
+
* needing a click, none wanting a hover wash, so every one of them hand-wrote
|
|
48
|
+
* its own box and the row ended up on four different containers.
|
|
49
|
+
*
|
|
50
|
+
* So `onClick` renders a `<button>`, `href` renders an `<a>`, and neither
|
|
51
|
+
* gains a state rule. The UA's own button chrome is reset in the theme
|
|
52
|
+
* (`button.kol-icon-frame, a.kol-icon-frame`) so "no states" is a property of
|
|
53
|
+
* the CLASS rather than of the tag — which is exactly the correction this
|
|
54
|
+
* component's own docstring already argued for when it refused to borrow
|
|
55
|
+
* `kol-btn-*` on a span.
|
|
56
|
+
*
|
|
57
|
+
* Still deliberately absent: `disabled` and `aria-pressed`. Both describe a
|
|
58
|
+
* control that CHANGES appearance with state, which is the line that stays.
|
|
59
|
+
* Want the wash, the pressed fill or the disabled dim? That is `Button`.
|
|
41
60
|
*
|
|
42
61
|
* @param {string} name icon name (kol-icons)
|
|
43
62
|
* @param {string} variant primary|secondary|accent|outline|ghost|nav|grey|danger
|
|
@@ -62,18 +81,39 @@ export default function IconFrame({
|
|
|
62
81
|
size = 'md',
|
|
63
82
|
radius = 'sm',
|
|
64
83
|
iconSize = null,
|
|
84
|
+
onClick,
|
|
85
|
+
href,
|
|
65
86
|
className = '',
|
|
66
87
|
...rest
|
|
67
88
|
}) {
|
|
68
89
|
if (!name) return null
|
|
69
90
|
const radiusCls = radius === 'full' ? ' kol-icon-frame-radius-full' : ''
|
|
70
91
|
const resolvedIconSize = iconSize ?? GLYPH[size] ?? GLYPH.md
|
|
92
|
+
const cls = `kol-icon-frame kol-icon-frame-${variant} kol-icon-frame-${size}${radiusCls} ${className}`.trim()
|
|
93
|
+
const glyph = <Icon name={name} size={resolvedIconSize} />
|
|
94
|
+
|
|
95
|
+
/* The element follows the affordance, and the CLASS is identical in all three
|
|
96
|
+
* branches — that is the whole contract. A frame that can be clicked must be
|
|
97
|
+
* a real button or a real link (keyboard, focus order, middle-click, screen
|
|
98
|
+
* readers); a frame that cannot must not be either, or it lands in the tab
|
|
99
|
+
* order announcing itself as something to press. */
|
|
100
|
+
if (href) {
|
|
101
|
+
return (
|
|
102
|
+
<a className={cls} href={href} {...rest}>
|
|
103
|
+
{glyph}
|
|
104
|
+
</a>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
if (onClick) {
|
|
108
|
+
return (
|
|
109
|
+
<button type="button" className={cls} onClick={onClick} {...rest}>
|
|
110
|
+
{glyph}
|
|
111
|
+
</button>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
71
114
|
return (
|
|
72
|
-
<span
|
|
73
|
-
|
|
74
|
-
{...rest}
|
|
75
|
-
>
|
|
76
|
-
<Icon name={name} size={resolvedIconSize} />
|
|
115
|
+
<span className={cls} {...rest}>
|
|
116
|
+
{glyph}
|
|
77
117
|
</span>
|
|
78
118
|
)
|
|
79
119
|
}
|
|
@@ -29,8 +29,15 @@ export default function useScrollSpy(ids, { rootMargin = '-30% 0px -60% 0px', ed
|
|
|
29
29
|
const atTop = top < edgeOffset
|
|
30
30
|
const atBottom = top + viewH >= fullH - edgeOffset * 0.8
|
|
31
31
|
if (atTop) {
|
|
32
|
+
/* THE FIRST HEADING, not null (user ruling 2026-08-01): *"at any given
|
|
33
|
+
* time you are at some place in the file, THAT LOCATION SHOULD
|
|
34
|
+
* HIGHLIGHT"*. The top lock used to clear the active id, so the rail
|
|
35
|
+
* highlighted nothing at rest — and a page opens at rest, which made
|
|
36
|
+
* "no active row" the state the reader saw first and most. The bottom
|
|
37
|
+
* lock has always activated the LAST id; this is that rule, both ends.
|
|
38
|
+
* You are at the top of the document, so you are in its first section. */
|
|
32
39
|
edgeLockRef.current = 'top'
|
|
33
|
-
setActiveId(
|
|
40
|
+
setActiveId(ids[0])
|
|
34
41
|
} else if (atBottom) {
|
|
35
42
|
edgeLockRef.current = 'bottom'
|
|
36
43
|
setActiveId(ids[ids.length - 1])
|
package/src/index.js
CHANGED
|
@@ -123,6 +123,14 @@ export { Icon } from '@kolkrabbi/kol-icons'
|
|
|
123
123
|
export { default as Graphic, GRAPHICS } from './graphics/Graphic.jsx'
|
|
124
124
|
export { GRAPHIC_RAW } from './graphics/graphicData.js'
|
|
125
125
|
|
|
126
|
+
/* The glyph ladders, exported 2026-08-01. They were internal, so anything
|
|
127
|
+
* OUTSIDE this package that pairs an icon with a label had to hardcode a
|
|
128
|
+
* number — the shell header's tabs took `size={14}`, foundry's section header
|
|
129
|
+
* took `20`, and neither could reference the rule it was meant to follow.
|
|
130
|
+
* Cross-package imports go through the `@kolkrabbi/*` specifier (ARCHITECTURE
|
|
131
|
+
* §3), so an export is the only way another package can obey the ladder. */
|
|
132
|
+
export { SOLO, ADJACENT, glyphSize } from './hooks/glyphLadders.js'
|
|
133
|
+
|
|
126
134
|
// hooks
|
|
127
135
|
export { default as usePrefersReducedMotion } from './hooks/usePrefersReducedMotion.js'
|
|
128
136
|
export { default as useReveal } from './hooks/useReveal.js'
|
|
@@ -19,6 +19,14 @@ import { Icon } from '@kolkrabbi/kol-icons'
|
|
|
19
19
|
* • Portable Text: `value={{ code, language, filename }}`
|
|
20
20
|
* • Direct props: `code` / `language` / `filename`
|
|
21
21
|
* • Children: `<CodeBlock language="js">{'…'}</CodeBlock>`
|
|
22
|
+
*
|
|
23
|
+
* `language` falls back to `'text'`, and a `'text'` block draws NO chip — so a
|
|
24
|
+
* fence that declares nothing renders as an unlabelled slab. That fallback is
|
|
25
|
+
* kept (a chip reading "text" is worse than none) and the fix is upstream:
|
|
26
|
+
* every fence declares a language, enforced by `pnpm validate:fences`.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} [size='md'] 'sm' | 'md' — the box and the type step together.
|
|
29
|
+
* @param {boolean} [bare] drop the FRAME; the host owns it. Not a size.
|
|
22
30
|
*/
|
|
23
31
|
|
|
24
32
|
const CheckMarkIcon = () => (
|
|
@@ -65,7 +73,13 @@ const syntaxTheme = (foregroundToken = 80) => ({
|
|
|
65
73
|
/* `bare` (2026-07-30): highlight + chip + copy WITHOUT the framed chrome — for
|
|
66
74
|
* hosts that already own the frame (PreviewCard's Code tab sat a full
|
|
67
75
|
* CodeBlock frame inside the kol-doc-figure border: frame-in-frame). */
|
|
68
|
-
|
|
76
|
+
/* `size` (2026-08-01, user ruling). The block had no size at all — its padding
|
|
77
|
+
* and type size sat in `.kol-codeblock` as unnamed constants, so *"its just
|
|
78
|
+
* whatever its defaulting to"* was literally true and no call site could ask
|
|
79
|
+
* for anything else. `md` is those exact values, named; `sm` is one step down
|
|
80
|
+
* on both axes. Size is INDEPENDENT of `bare`: bare removes the frame, size
|
|
81
|
+
* sets the box, and a bare block still has one. */
|
|
82
|
+
export default function CodeBlock({ children, code: codeProp, language: languageProp, filename: filenameProp, value, bare = false, size = 'md' }) {
|
|
69
83
|
const [copied, setCopied] = useState(false)
|
|
70
84
|
|
|
71
85
|
const code = String(value?.code ?? codeProp ?? children ?? '')
|
|
@@ -84,7 +98,7 @@ export default function CodeBlock({ children, code: codeProp, language: language
|
|
|
84
98
|
|
|
85
99
|
return (
|
|
86
100
|
<div className={bare ? '' : 'kol-codeblock-wrapper'}>
|
|
87
|
-
<div className={`kol-codeblock${bare ? ' kol-codeblock--bare' : ''}`}>
|
|
101
|
+
<div className={`kol-codeblock kol-codeblock--${size}${bare ? ' kol-codeblock--bare' : ''}`}>
|
|
88
102
|
{(filename || (language && language !== 'text')) && (
|
|
89
103
|
<div className="kol-codeblock-filename">{filename || language}</div>
|
|
90
104
|
)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
|
-
import
|
|
3
|
+
import Button from '../atoms/Button.jsx'
|
|
4
4
|
import usePrefersReducedMotion from '../hooks/usePrefersReducedMotion.js'
|
|
5
5
|
|
|
6
6
|
/* taxonomy-ok: nests kol-icons's Icon (a package import the relative-import
|
|
@@ -150,14 +150,21 @@ export default function ShellDrawer({
|
|
|
150
150
|
>
|
|
151
151
|
<div className="mb-6 flex items-center gap-4">
|
|
152
152
|
{header != null && <div className="min-w-0 flex-1">{header}</div>}
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
{/* The box has an owner (2026-08-01). This hand-wrote the icon-button
|
|
154
|
+
* square and its hover wash; `Button variant="nav"` IS that string.
|
|
155
|
+
* `iconSize` holds the glyph where it was — the ladder's md rung is
|
|
156
|
+
* heavier than a drawer close wants, and Button documents iconSize
|
|
157
|
+
* for exactly the cases the ladder cannot serve. The SQUARE is what
|
|
158
|
+
* needed an owner, and it now has one. */}
|
|
159
|
+
<Button
|
|
160
|
+
variant="nav"
|
|
161
|
+
size="md"
|
|
162
|
+
iconOnly="x"
|
|
163
|
+
iconSize={14}
|
|
155
164
|
onClick={onClose}
|
|
156
165
|
aria-label="Close"
|
|
157
|
-
className="ml-auto
|
|
158
|
-
|
|
159
|
-
<Icon name="x" size={14} />
|
|
160
|
-
</button>
|
|
166
|
+
className="ml-auto shrink-0"
|
|
167
|
+
/>
|
|
161
168
|
</div>
|
|
162
169
|
<div className="flex-1 overflow-y-auto pr-1" style={{ overflowAnchor: 'none' }}>
|
|
163
170
|
{children}
|