@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-component",
3
- "version": "0.21.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.8.11"
27
+ "@kolkrabbi/kol-icons": "0.10.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "framer-motion": "^12.0.0",
@@ -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.
@@ -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
- * Deliberately absent: `onClick`, `href`, `disabled`, `aria-pressed`, `title`.
40
- * Wanting any of those means wanting a `Button` with `iconOnly`, not this.
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
- className={`kol-icon-frame kol-icon-frame-${variant} kol-icon-frame-${size}${radiusCls} ${className}`.trim()}
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(null)
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
- export default function CodeBlock({ children, code: codeProp, language: languageProp, filename: filenameProp, value, bare = false }) {
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 { Icon } from '@kolkrabbi/kol-icons'
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
- <button
154
- type="button"
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 flex h-8 w-8 shrink-0 items-center justify-center rounded-md border-0 bg-transparent cursor-pointer text-fg-64 transition-colors hover:bg-fg-08 hover:text-emphasis"
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}