@kolkrabbi/kol-component 0.19.0 → 0.20.1

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.19.0",
3
+ "version": "0.20.1",
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",
@@ -1,5 +1,6 @@
1
1
  import { isValidElement } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
+ import { glyphSize } from '../hooks/glyphLadders.js'
3
4
 
4
5
  /**
5
6
  * Button — canonical KOL button. Emits kol-btn* classes (CSS in @kol/theme).
@@ -20,7 +21,8 @@ import { Icon } from '@kolkrabbi/kol-icons'
20
21
  * @param {string} props.iconOnlyHover - Icon to show on hover (icon-only)
21
22
  * @param {boolean} props.animateIcon - Disable default hover states to focus on icon animation
22
23
  * @param {boolean} props.quiet - Dimmed at rest, brightens on hover; stays dimmed when disabled. For secondary icon-only chrome.
23
- * @param {number} props.iconSize - Size of the icon in pixels (default: auto by size)
24
+ * @param {number} props.iconSize - Size of the icon in pixels (default: auto by size — SOLO when iconOnly, ADJACENT otherwise)
25
+ * @param {'sm'|'full'} props.radius - sm (default, the system's radius token) | full (a full round). Two values, nothing between — mirrors IconFrame, and a round control is its own chrome idiom (edge-straddling controls, avatars), the only sanctioned exception to the hard radius invariant.
24
26
  * @param {number} props.iconGap - Gap between icon and text (default: 8)
25
27
  * @param {string} props.href - Link destination (makes it an <a>)
26
28
  * @param {Function} props.onClick - Click handler (makes it a <button>)
@@ -42,6 +44,7 @@ const Button = ({
42
44
  iconRightHover,
43
45
  iconOnly,
44
46
  iconOnlyHover,
47
+ radius = 'sm',
45
48
  animateIcon = false,
46
49
  quiet = false,
47
50
  iconSize,
@@ -57,7 +60,11 @@ const Button = ({
57
60
  pressed,
58
61
  ...props
59
62
  }) => {
60
- const resolvedIconSize = iconSize ?? (size === 'sm' ? 14 : size === 'lg' ? 18 : 16)
63
+ // Two ladders, split on whether a label sits beside the glyph. An icon-only
64
+ // button is a solo glyph in a pinned square, so it takes SOLO (16/20/24) —
65
+ // it took the text-adjacent ladder until component 0.20.0, which put a 14px
66
+ // glyph in a 28px square at `sm`. `iconSize` still overrides either.
67
+ const resolvedIconSize = iconSize ?? glyphSize(size, Boolean(iconOnly))
61
68
 
62
69
  // `control` is a legacy alias for `ghost` (kept so web call-sites passing
63
70
  // variant="control" keep working post-migration).
@@ -97,7 +104,12 @@ const Button = ({
97
104
  // .kol-btn-icon replaces the old iconOnly INLINE style — inline display
98
105
  // beat every consumer utility (lg:hidden could never hide the button).
99
106
  const iconOnlyClass = iconOnly ? 'kol-btn-icon' : ''
100
- const combinedClass = `kol-btn ${variantClass} ${sizeClass} ${animateClass} ${quietClass} ${pressedClass} ${iconOnlyClass} ${className}`.trim().replace(/\s+/g, ' ')
107
+
108
+ // `sm` is the default and carries no class — it's on .kol-btn itself.
109
+ // The rule for `full` is shared with .kol-icon-frame-radius-full, one
110
+ // selector, so the two controls can never disagree on the value.
111
+ const radiusClass = radius === 'full' ? 'kol-btn-radius-full' : ''
112
+ const combinedClass = `kol-btn ${variantClass} ${sizeClass} ${animateClass} ${quietClass} ${pressedClass} ${iconOnlyClass} ${radiusClass} ${className}`.trim().replace(/\s+/g, ' ')
101
113
 
102
114
  // Render icon with optional hover state
103
115
  const renderIcon = (iconName, iconHoverName) => {
@@ -1,4 +1,5 @@
1
1
  import { Icon } from '@kolkrabbi/kol-icons'
2
+ import { SOLO } from '../hooks/glyphLadders.js'
2
3
 
3
4
  /**
4
5
  * IconFrame — a STATIC square frame holding one icon.
@@ -51,7 +52,9 @@ import { Icon } from '@kolkrabbi/kol-icons'
51
52
  * square is unaffected; only the centred glyph moves.
52
53
  * @param {string} className escape hatch
53
54
  */
54
- const GLYPH = { sm: 16, md: 20, lg: 24 }
55
+ // The solo-glyph ladder, from its one source. This was a local transcription
56
+ // until component 0.20.0 — see hooks/glyphLadders.js for why that mattered.
57
+ const GLYPH = SOLO
55
58
 
56
59
  export default function IconFrame({
57
60
  name,
@@ -1,4 +1,5 @@
1
1
  import { Icon } from '@kolkrabbi/kol-icons'
2
+ import { glyphSize } from '../hooks/glyphLadders.js'
2
3
 
3
4
  /**
4
5
  * Input — single-input atom built on the .kol-control shell.
@@ -31,7 +32,6 @@ import { Icon } from '@kolkrabbi/kol-icons'
31
32
  */
32
33
 
33
34
  const SIZE_TYPE = { sm: 'kol-mono-12', md: 'kol-mono-14', lg: 'kol-mono-16' }
34
- const ICON_SIZE = { sm: 14, md: 14, lg: 18 }
35
35
 
36
36
  export default function Input({
37
37
  type = 'text',
@@ -53,7 +53,12 @@ export default function Input({
53
53
  }) {
54
54
  const isNumber = type === 'number'
55
55
  const fixedChars = typeof chars === 'number'
56
- const resolvedIconSize = iconSize ?? ICON_SIZE[size] ?? 14
56
+ // The text-adjacent ladder an Input's icon sits in the rung's line box
57
+ // beside the value, exactly like a labelled Button's. `.kol-control-*` and
58
+ // `.kol-btn-*` carry identical padding + type per rung, so the glyph is the
59
+ // same too. This was a local ICON_SIZE with `md: 14` until 0.20.1, the last
60
+ // of the four transcriptions (sm and lg had always agreed).
61
+ const resolvedIconSize = iconSize ?? glyphSize(size)
57
62
 
58
63
  // ghost folds into outline (2026-07-08 chrome law): one secondary treatment.
59
64
  const resolvedVariant = variant === 'ghost' ? 'outline' : variant
@@ -0,0 +1,39 @@
1
+ /**
2
+ * THE two glyph ladders. One source for the sizes every icon-bearing control
3
+ * resolves against.
4
+ *
5
+ * Plain constants, not hooks — they live here because src/hooks is the
6
+ * taxonomy's only non-component folder (same reason as cssVar.js).
7
+ *
8
+ * The ladders split on ONE question: is there a label beside the glyph?
9
+ *
10
+ * SOLO an icon alone in a pinned square (28/32/36) — takes the room
11
+ * ADJACENT an icon inside a rung's line box, beside a label
12
+ *
13
+ * Stated in the DS before this file existed — ThemeToggle.jsx:40 ("solo
14
+ * 16/20/24, the pinned-square pairing") and IconFrame.jsx ("the solo-glyph
15
+ * ladder against the pinned squares 28/32/36"). This file is where they stop
16
+ * being transcribed.
17
+ *
18
+ * Why it exists: the same two ladders were re-typed in four places
19
+ * (Button, IconFrame, Input, Tag) and had already drifted. Button's icon-only
20
+ * branch took the ADJACENT ladder — a 14px glyph in a 28px square where the
21
+ * law says 16 — which is exactly what independent transcriptions produce.
22
+ * framework 0.10.3 fixed the mirror of this bug (hop-bare took SOLO while
23
+ * carrying a label) and nothing connected the two.
24
+ */
25
+
26
+ /** Icon alone in a pinned square. Pairs with squares 28 · 32 · 36. */
27
+ export const SOLO = { sm: 16, md: 20, lg: 24 }
28
+
29
+ /** Icon beside a label, inside the rung's line box. */
30
+ export const ADJACENT = { sm: 14, md: 16, lg: 18 }
31
+
32
+ /**
33
+ * Resolve a glyph size. `solo` picks the ladder; `size` indexes it.
34
+ * Falls back to the md rung so an unknown size never yields undefined.
35
+ */
36
+ export function glyphSize(size, solo = false) {
37
+ const ladder = solo ? SOLO : ADJACENT
38
+ return ladder[size] ?? ladder.md
39
+ }