@kolkrabbi/kol-component 0.169.0 → 0.171.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.169.0",
3
+ "version": "0.171.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",
package/src/index.js CHANGED
@@ -22,6 +22,7 @@ export { default as AssetPlaceholder } from './utilities/AssetPlaceholder.jsx'
22
22
  export { default as Avatar } from './atoms/Avatar.jsx'
23
23
  export { default as Badge } from './atoms/Badge.jsx'
24
24
  export { default as Button } from './atoms/Button.jsx'
25
+ export { default as CloseButton } from './utilities/CloseButton.jsx'
25
26
  export { default as ActionButton } from './atoms/ActionButton.jsx'
26
27
  export { default as SizeOrDownload } from './atoms/SizeOrDownload.jsx'
27
28
  export { default as SortHeader } from './atoms/SortHeader.jsx'
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useRef, useState } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
3
  import { Icon } from '@kolkrabbi/kol-icons'
4
- import Button from '../atoms/Button.jsx'
4
+ import CloseButton from '../utilities/CloseButton.jsx'
5
5
  import usePrefersReducedMotion from '../hooks/usePrefersReducedMotion.js'
6
6
 
7
7
  /* taxonomy-ok: nests kol-icons's Icon (a package import the relative-import
@@ -38,6 +38,7 @@ const FOCUSABLE =
38
38
  * @param {number|string} height panel height (px number or CSS length); omit for a content-sized sheet — `bottom` only
39
39
  * @param {ReactNode} header header-row content beside the close button (replaces the source's baked-in wordmark)
40
40
  * @param {boolean} backdrop render the dimming scrim (default true); false = panel alone, no darken/blur, close via × / Esc
41
+ * @param {'xs'|'sm'|'md'|'lg'} closeSize the close control's rung (default 'sm' — match the controls in the panel)
41
42
  * @param {ReactNode} children scrollable panel body
42
43
  * @param {string} className extra classes on the panel
43
44
  */
@@ -50,6 +51,14 @@ export default function ShellDrawer({
50
51
  header,
51
52
  closeSide = 'end',
52
53
  backdrop = true,
54
+ /* THE CLOSE SITS ON THE ROW'S RUNG (user 2026-09-03, on the settings drawer:
55
+ * *"does this button follow the size ladder?"*). It was pinned `md` (32) while
56
+ * every control in the panel below it — the switches, the dropdowns, the reset
57
+ * frame — is `sm` (26), so the one control that is not a setting was the
58
+ * largest thing on the surface. A size is a height and a row is one height;
59
+ * `sm` is the default because a drawer header sits over its own controls.
60
+ * Named `closeSize`, not `size` — `size` is already the panel's own box. */
61
+ closeSize = 'sm',
53
62
  children,
54
63
  className = '',
55
64
  /* SettingsPanelApproved (2026-08-27): the settings drawer has neither */
@@ -188,14 +197,7 @@ export default function ShellDrawer({
188
197
  {closeSide === 'start' && (
189
198
  /* bare glyph, no container (user 2026-08-09: "the X close icon
190
199
  * does not need a container") — body ink at rest, emphasis on hover */
191
- <button
192
- type="button"
193
- aria-label="Close"
194
- onClick={onClose}
195
- className="inline-flex shrink-0 cursor-pointer border-0 bg-transparent p-0 text-body transition-colors duration-150 hover:text-emphasis"
196
- >
197
- <Icon name="x" size={18} />
198
- </button>
200
+ <CloseButton onClick={onClose} size={closeSize} className="shrink-0" />
199
201
  )}
200
202
  {header != null && <div className="min-w-0 flex-1">{header}</div>}
201
203
  {/* A NORMAL ICON BUTTON (user 2026-09-02: *"it should just be like a
@@ -205,14 +207,7 @@ export default function ShellDrawer({
205
207
  * the md rung's 32px around a glyph six under it, and it read as an
206
208
  * oversized empty square. No override: the rung sets both. */}
207
209
  {closeSide !== 'start' && (
208
- <Button
209
- variant="nav"
210
- size="md"
211
- iconOnly="x"
212
- onClick={onClose}
213
- aria-label="Close"
214
- className="ml-auto shrink-0"
215
- />
210
+ <CloseButton onClick={onClose} size={closeSize} className="ml-auto shrink-0" />
216
211
  )}
217
212
  </div>
218
213
  <div className="flex-1 overflow-y-auto pr-1" style={{ overflowAnchor: 'none' }}>
@@ -1,4 +1,5 @@
1
1
  import { useRef } from 'react'
2
+ import CloseButton from '../utilities/CloseButton.jsx'
2
3
  import { Icon } from '@kolkrabbi/kol-icons'
3
4
 
4
5
  /* taxonomy-ok: nests kol-icons's Icon */
@@ -36,15 +37,9 @@ export default function TabsRow({ tabs = [], value, onChange, onClose, onMinimis
36
37
  return (
37
38
  <div className="flex items-stretch gap-4 h-10">
38
39
  {onClose && (
39
- <button
40
- type="button"
41
- onClick={onClose}
42
- aria-label="Close"
43
- className="text-meta hover:text-emphasis self-center"
44
- style={{ lineHeight: 0 }}
45
- >
46
- <Icon name="x" size={12} />
47
- </button>
40
+ /* the one close, at the rung whose glyph is the 12 this hand-rolled
41
+ * (2026-09-03) — xs SOLO is 12, so the drawing does not move */
42
+ <CloseButton onClick={onClose} size="xs" className="self-center" />
48
43
  )}
49
44
 
50
45
  <div ref={listRef} role="tablist" onKeyDown={onKeyDown} className="flex items-stretch gap-4">
@@ -0,0 +1,77 @@
1
+ import Button from '../atoms/Button.jsx'
2
+ import IconFrame from '../atoms/IconFrame.jsx'
3
+
4
+ /**
5
+ * CloseButton — the X that dismisses a thing. One component, so there is one.
6
+ *
7
+ * WHY IT EXISTS (user 2026-09-03: *"why are you FUCKING MAKING INDIVIDUAL
8
+ * CHANGES this isnt difficult, this is a button component yes or no"*). It is a
9
+ * Button, and that was the problem: "close" was never a component, it was four
10
+ * props retyped at four call sites, so the four drifted —
11
+ *
12
+ * ShellDrawer Button variant="nav" size="md" (+ a second,
13
+ * hand-rolled <button> with an 18px icon for closeSide="start")
14
+ * FullscreenOverlay Button variant="nav", Button's md default
15
+ * TabsRow a hand-rolled <button> with a 12px icon and its own hover
16
+ * ShellLayout Button variant="outline" quiet size="sm"
17
+ *
18
+ * — three sizes and two variants for one control, and `ShellLayout` was still
19
+ * shipping the boxed outline that the 2026-09-01 one-idiom ruling retired. The
20
+ * ruling was written down and then re-typed wrong three times, which is the
21
+ * `HEADER_ICON` lesson again: a number is advice, a component is the only thing
22
+ * that makes the box unwritable by hand.
23
+ *
24
+ * THE IDIOM, in one place: `variant="nav"` — bare glyph, no box, the wash on
25
+ * hover — at `sm` (26), the same rung as the controls a close usually sits
26
+ * beside. `size` is here because a close in a taller chrome row legitimately
27
+ * takes that row's rung; it is the ONLY thing a call site may vary, and it must
28
+ * be a rung, never an `iconSize`. The glyph follows from the rung.
29
+ *
30
+ * UTILITY, by the placement test (2026-08-09): an X on a canvas alone means
31
+ * nothing — it is only ever worn BY the thing it dismisses, the same row
32
+ * `FullscreenOverlay` is held on. It is also the only tier that works: a close
33
+ * is needed by a utility (`FullscreenOverlay`), a molecule (`ShellDrawer`,
34
+ * `TabsRow`) and a package shell alike, and utilities are the one folder every
35
+ * tier may import while importing only atoms itself.
36
+ *
37
+ * TWO BASES, ONE IDIOM (user 2026-09-03: *"sometimes you dont want states"*).
38
+ * `states` (default true) is a Button — hover wash, press, focus ring — which is
39
+ * what a close in reachable chrome should be. `states={false}` is `IconFrame`,
40
+ * which has NO interactive states by contract: for a close that is decoration
41
+ * over something already dismissible, or one inside a surface that owns the
42
+ * whole hit area, where a lighting-up X is noise. Same glyph, same variant, same
43
+ * rung either way — only the state machine differs, which is exactly the
44
+ * distinction IconFrame was promoted for (2026-08-01).
45
+ *
46
+ * @param {Function} onClick dismiss
47
+ * @param {boolean} states interactive states (default true → Button); false → IconFrame, no states
48
+ * @param {'xs'|'sm'|'md'|'lg'} size the rung (default 'sm'); match the row it sits in
49
+ * @param {string} label accessible name (default 'Close')
50
+ * @param {string} className extra classes — positioning, never chrome
51
+ */
52
+ export default function CloseButton({ onClick, size = 'sm', label = 'Close', states = true, className = '', ...rest }) {
53
+ if (!states) {
54
+ return (
55
+ <IconFrame
56
+ name="x"
57
+ variant="nav"
58
+ size={size}
59
+ onClick={onClick}
60
+ aria-label={label}
61
+ className={className}
62
+ {...rest}
63
+ />
64
+ )
65
+ }
66
+ return (
67
+ <Button
68
+ variant="nav"
69
+ size={size}
70
+ iconOnly="x"
71
+ onClick={onClick}
72
+ aria-label={label}
73
+ className={className}
74
+ {...rest}
75
+ />
76
+ )
77
+ }
@@ -1,13 +1,16 @@
1
1
  import { useEffect, useRef } from 'react'
2
- import Button from '../atoms/Button.jsx'
2
+ import CloseButton from './CloseButton.jsx'
3
3
 
4
4
  /**
5
5
  * FullscreenOverlay — the scrim + centred sheet every overlay in the repo
6
6
  * wears. Owns dismissal (Escape, backdrop, close button), scroll lock and
7
7
  * stacking; the consumer supplies the panel.
8
8
  *
9
- * The close control is the DS `Button` (quiet, icon-only, the `x` glyph) —
10
- * the same idiom as ShellLayout's. It was a hand-rolled <button> printing a
9
+ * The close control is the DS `Button` (icon-only, the `x` glyph) — the estate's
10
+ * ONE close idiom, and therefore ONE SIZE: `sm` (26), matching `ShellDrawer`'s
11
+ * (2026-09-03). It took Button's `md` default while the drawer's went to `sm`,
12
+ * which is two sizes for one idiom — the thing the single-idiom ruling exists to
13
+ * stop. It was a hand-rolled <button> printing a
11
14
  * literal `×` TEXT CHARACTER until 2026-08-01: no icon, no states, and a
12
15
  * typographic multiplication sign standing in for a glyph the icon set has
13
16
  * always shipped.
@@ -69,15 +72,9 @@ export default function FullscreenOverlay({ open, onClose, closeButton = true, c
69
72
  * user ruling): the estate's close X is the drawer trigger's bare
70
73
  * `nav` glyph — the boxed outline treatment was a second design one
71
74
  * tap away from the first, and had he kept a box it would have worn
72
- * `primary`, never `outline`. Same variant, same default square as
73
- * the trigger. */
74
- <Button
75
- variant="nav"
76
- iconOnly="x"
77
- className="kol-overlay-close"
78
- onClick={onClose}
79
- aria-label="Close"
80
- />
75
+ * `primary`, never `outline`. Same variant as the trigger; the SQUARE
76
+ * is `sm` since 2026-09-03, one size for the one idiom. */
77
+ <CloseButton className="kol-overlay-close" onClick={onClose} />
81
78
  )}
82
79
  {children}
83
80
  </div>