@kolkrabbi/kol-component 0.181.0 → 0.183.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.181.0",
3
+ "version": "0.183.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",
@@ -167,6 +167,16 @@ export default function ContentCard({
167
167
  /* plateRule (CatalogCardFrameAndZoom): the plate's top hairline — default the variant's (catalog and
168
168
  * print draw it); `false` turns it off without an `!important` in a consumer sheet */
169
169
  plateRule,
170
+ /* `bg` — the card's REST fill, overriding the variant's. It sets
171
+ * `--kol-card-bg`, not a background, because the rest colours are custom
172
+ * properties so the hover class can win; that is also why
173
+ * `className="bg-oq-48"` does nothing here and a consumer reaching around the
174
+ * component had to write `className="[--kol-card-bg:var(--kol-oq-48)]"`
175
+ * (kol-client-hrafn, `contentcard-bg-and-text-props` 2026-09-03). The hover
176
+ * step is `--kol-content-hover-bg` and is set separately, so the two do not
177
+ * move together. For the card's INK, pass `text` — it falls through to
178
+ * ContentText with the rest of the slots. */
179
+ bg,
170
180
  control,
171
181
  controlStart,
172
182
  reveal,
@@ -242,7 +252,7 @@ export default function ContentCard({
242
252
  zIndex: box.layout === 'canvas' ? 1 : undefined,
243
253
  }}
244
254
  >
245
- {hasText && <ContentText variant={variant} form={isHero ? 'hero' : 'card'} tagVariant={tagVariant ?? (/surface-/.test(box.bg ?? '') ? 'tertiary' : 'primary')} {...textSlots} />}
255
+ {hasText && <ContentText variant={variant} form={isHero ? 'hero' : 'card'} tagVariant={tagVariant ?? (/surface-/.test(bg ?? box.bg ?? '') ? 'tertiary' : 'primary')} {...textSlots} />}
246
256
  {/* ABSOLUTE, not a flex sibling: the plate's height moves with the title
247
257
  * and the meta, so a laid-out stack would stretch or drift with it. The
248
258
  * inset reads the SAME pad token the plate uses, so the icons sit the
@@ -390,7 +400,7 @@ export default function ContentCard({
390
400
  /* same reason as ContentRow: rest colours are PROPERTIES, because an
391
401
  * inline background/borderColor outranks the hover class and the step
392
402
  * would never render. */
393
- '--kol-card-bg': box.bg ?? undefined,
403
+ '--kol-card-bg': bg ?? box.bg ?? undefined,
394
404
  '--kol-card-border': box.border ? (selected ? 'var(--kol-fg-64)' : box.border) : undefined,
395
405
  '--kol-content-hover-bg': hoverBg ?? undefined,
396
406
  '--kol-content-hover-border': box.frameHover ?? undefined,
@@ -160,6 +160,15 @@ export default function ContentRow({
160
160
  href,
161
161
  onNavigate,
162
162
  tagVariant,
163
+ /* `bg` — the row's REST fill, the twin of ContentCard's (filed against the
164
+ * card, `contentcard-bg-and-text-props` 2026-09-03; the pair ships together
165
+ * and a consumer that re-grounds one hits the same wall on the other in the
166
+ * same grid). It sets `--kol-row-bg`, not a background, because the rest
167
+ * colours are custom properties so the hover class can win — which is why
168
+ * `className="bg-oq-48"` does nothing here either. `selected` still wins:
169
+ * a selected row is the list's state, not the consumer's ground. For the
170
+ * row's INK, pass `text` — it falls through to ContentText. */
171
+ bg,
163
172
  className = '',
164
173
  ...text
165
174
  }) {
@@ -216,7 +225,7 @@ export default function ContentRow({
216
225
  /* rest values are PROPERTIES, not inline declarations — an inline
217
226
  * `background`/`borderColor` outranks every class, so the hover rules in
218
227
  * kol-theme could never win and no row hover fired at all. */
219
- '--kol-row-bg': selected ? 'var(--kol-fg-04)' : box.bg,
228
+ '--kol-row-bg': selected ? 'var(--kol-fg-04)' : bg ?? box.bg,
220
229
  '--kol-row-border': box.frame || undefined,
221
230
  }
222
231
 
@@ -231,7 +240,7 @@ export default function ContentRow({
231
240
  * (a rendered alphabet, a waveform, a sparkline) is never the family's. */
232
241
  const inner = box.column ? (
233
242
  <>
234
- <ContentText variant={variant} form="row" className="w-full" tagVariant={tagVariant ?? (/surface-/.test(box.bg ?? '') ? 'tertiary' : 'primary')} {...text} />
243
+ <ContentText variant={variant} form="row" className="w-full" tagVariant={tagVariant ?? (/surface-/.test(bg ?? box.bg ?? '') ? 'tertiary' : 'primary')} {...text} />
235
244
  {footer}
236
245
  </>
237
246
  ) : (
@@ -259,7 +268,7 @@ export default function ContentRow({
259
268
  * ruled it on screen): the two lines stack to 34 inside a 40 content box,
260
269
  * and pushing them fully apart puts the ascenders hard against the thumb's
261
270
  * top and bottom edges. It is the ROW's ruling, so the row passes it. */}
262
- <ContentText variant={variant} form="row" className={`flex-1 ${box.height != null ? 'py-[2px]' : ''}`.trim()} tagVariant={tagVariant ?? (/surface-/.test(box.bg ?? '') ? 'tertiary' : 'primary')} {...text} />
271
+ <ContentText variant={variant} form="row" className={`flex-1 ${box.height != null ? 'py-[2px]' : ''}`.trim()} tagVariant={tagVariant ?? (/surface-/.test(bg ?? box.bg ?? '') ? 'tertiary' : 'primary')} {...text} />
263
272
  {/* `specs` rides the trailing edge on EVERY variant — year · material ·
264
273
  * edition is a content difference, not a geometry one, and minting a
265
274
  * seventh page-named box for it is the exact mistake §1 of the ticket is
@@ -30,6 +30,7 @@
30
30
  * solid surface fill — the /work row the variant was minted for — and `primary` otherwise
31
31
  * @param {string} titleClass … eyebrowClass (alias `kickerClass`), bodyClass, detailClass,
32
32
  * dateClass, sizeClass, metaClass — full class overrides
33
+ * @param {'subtle'|'meta'|'body'|'lede'|'strong'|'shout'|'scream'|'emphasis'} text - ONE ink role for every slot, replacing the ink each ramp hardcodes (`text-emphasis` on a title, `text-meta` on a date). It also re-inks a `<slot>Class` override, which is the point: an override replaces the ramp string WHOLE, so `kickerClass="kol-sans-body-01"` silently drops the `text-meta` the ramp carried and the kicker jumps to inherited ink (kol-client-hrafn, `contentcard-bg-and-text-props` 2026-09-03). `text` sets the level once instead of re-specifying eight ramps. It FLATTENS the hierarchy by design — every slot lands on the same role — so a card that wants its own relative steps still passes per-slot classes
33
34
  */
34
35
 
35
36
  import Tag from '../atoms/Tag'
@@ -219,6 +220,7 @@ export default function ContentText({
219
220
  title, body, eyebrow, kicker, detail, date, size, meta, tags,
220
221
  gap, clamp, tagVariant = 'primary',
221
222
  titleClass, bodyClass, eyebrowClass, kickerClass, detailClass, dateClass, sizeClass, metaClass, tagsClass,
223
+ text,
222
224
  className = '',
223
225
  }) {
224
226
  /* `kicker` / `kickerClass` = aliases of `eyebrow` / `eyebrowClass` (2026-08-27) */
@@ -264,9 +266,24 @@ export default function ContentText({
264
266
  return v.map((item, i) => <span key={i}>{item}</span>)
265
267
  }
266
268
  const spread = (slot) => (Array.isArray(values[slot]) && slot !== 'tags' ? ' flex flex-wrap gap-2' : '')
269
+
270
+ /* `text` — one ink role across the slots. The ramps HARDCODE their ink, so
271
+ * neither inheritance nor a wrapper class can reach them; the only way in is
272
+ * to take the ink class out of the resolved string and put the role back.
273
+ * Applied after the override for the reason the prop exists: an override
274
+ * replaces the ramp whole and takes its ink with it.
275
+ *
276
+ * The alternation is closed on purpose — `text-right` / `text-center` are
277
+ * alignment, not ink, and must survive. `text-fg-NN` goes too: the hero ramps
278
+ * reach for stops (`text-fg-64`, `text-fg-48`) rather than roles, and leaving
279
+ * those behind would let two inks sit in one class string. */
280
+ const INK = /\btext-(?:subtle|meta|body|lede|strong|shout|scream|emphasis|fg-\d+)\b/g
281
+ const ink = (cls) =>
282
+ text ? `${cls.replace(INK, '').replace(/\s+/g, ' ').trim()} text-${text}` : cls
283
+
267
284
  const line = (slot) =>
268
285
  values[slot] == null ? null : (
269
- <div key={slot} className={`${overrides[slot] ?? ramp[slot] ?? ''}${extra(slot)}${hook(slot)}${spread(slot)}`.trim()}>{content(slot)}</div>
286
+ <div key={slot} className={`${ink(overrides[slot] ?? ramp[slot] ?? '')}${extra(slot)}${hook(slot)}${spread(slot)}`.trim()}>{content(slot)}</div>
270
287
  )
271
288
 
272
289
  /* RECURSIVE (2026-08-15) — an entry inside a line/between/group may itself be
@@ -1,10 +1,11 @@
1
1
  import { useState } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
3
  import { PopoverPanel, usePopover } from '../utilities/Popover.jsx'
4
+ import { glyphSize } from '../hooks/glyphLadders.js'
4
5
 
5
6
  /**
6
7
  * SplitToolButton — single-trigger split tool button + variant menu (the
7
- * tool-palette idiom: Select · Text · [Shape ◢] · Pattern). A 28×28
8
+ * tool-palette idiom: Select · Text · [Shape ◢] · Pattern). A pinned-square
8
9
  * quiet/pressed trigger shows the current variant while the group is `active`
9
10
  * (else the `lastPicked` variant) plus a corner fold indicator; ONE click both
10
11
  * arms that variant (`onChange`) and opens the variant menu (floating-ui's own
@@ -14,9 +15,17 @@ import { PopoverPanel, usePopover } from '../utilities/Popover.jsx'
14
15
  * The trigger is a plain `kol-btn` element, not DS Button: Button doesn't
15
16
  * forward refs (the popover must anchor on the real button), can't host the
16
17
  * corner fold inside its own box, and `blurOnClick` must blur the button
17
- * itself. The class output — kol-btn kol-btn-ghost + kol-btn-quiet /
18
- * kol-btn-pressed — is exactly what `<Button variant="ghost" quiet pressed>`
19
- * emits, so the visual contract stays Button's.
18
+ * itself. The class output — kol-btn kol-btn-ghost kol-btn-icon kol-btn-{size}
19
+ * + kol-btn-quiet / kol-btn-pressed — is exactly what
20
+ * `<Button variant="ghost" quiet pressed iconOnly>` emits, so the box and the
21
+ * visual contract stay Button's.
22
+ *
23
+ * `size` was a raw px number (28) with a transcribed 14px glyph until 0.182.0:
24
+ * off the 22 · 26 · 32 · 40 ladder in a set whose whole point is that one row
25
+ * is one box, and the glyph pinned where the SOLO ladder should have decided
26
+ * it. kol-fxr's tool rail hand-rolls this trigger at 36/22 rather than
27
+ * importing it (design-editor-set-is-a-half-port, 2026-09-03) — 36 is not a
28
+ * rung either, and the DS answer to a bespoke 36 is `size="lg"`.
20
29
  *
21
30
  * For a text-trigger single-select use `Dropdown`; for the two-button
22
31
  * action-half + chevron-half split use `ShapeDropdown`.
@@ -27,7 +36,7 @@ import { PopoverPanel, usePopover } from '../utilities/Popover.jsx'
27
36
  * @param {Function} props.onChange - Fires with a variant id — on menu pick, and on trigger click while inactive (arming)
28
37
  * @param {string} props.lastPicked - Variant id the trigger shows while the group is inactive (default: variants[0])
29
38
  * @param {boolean} props.active - Whether this tool group is the active tool — lit trigger, `aria-pressed`
30
- * @param {number} props.size - Trigger box in px (default: 28)
39
+ * @param {'xs'|'sm'|'md'|'lg'} props.size - Trigger box on the pinned-square ladder — 22 · 26 · 32 · 40 (default: 'md'). The glyph follows the SOLO ladder; it is never set at the call site
31
40
  * @param {boolean} props.blurOnClick - Blur the trigger after click so a canvas can reclaim focus and refresh its cursor (default: false)
32
41
  * @param {string} props.aria-label - Trigger label fallback when no variant resolves
33
42
  * @param {string} props.className - Additional classes on the trigger
@@ -54,7 +63,7 @@ const SplitToolButton = ({
54
63
  onChange,
55
64
  lastPicked,
56
65
  active = false,
57
- size = 28,
66
+ size = 'md',
58
67
  blurOnClick = false,
59
68
  className = '',
60
69
  'aria-label': ariaLabel,
@@ -91,13 +100,12 @@ const SplitToolButton = ({
91
100
  ref={popover.refs.setReference}
92
101
  {...popover.getReferenceProps({ onClick: handleTriggerClick })}
93
102
  type="button"
94
- className={`relative kol-btn kol-btn-ghost ${active ? 'kol-btn-pressed' : 'kol-btn-quiet'} ${className}`.trim()}
95
- style={{ width: size, height: size, padding: 6 }}
103
+ className={`relative kol-btn kol-btn-ghost kol-btn-icon kol-btn-${size} ${active ? 'kol-btn-pressed' : 'kol-btn-quiet'} ${className}`.trim()}
96
104
  aria-pressed={active}
97
105
  aria-label={label}
98
106
  title={title}
99
107
  >
100
- {triggerVariant && <Icon name={triggerVariant.icon} size={14} />}
108
+ {triggerVariant && <Icon name={triggerVariant.icon} size={glyphSize(size, true)} />}
101
109
  <FoldIndicator />
102
110
  </button>
103
111
  {/* w-max — floats size to content, the menu-family law (2026-08-09). */}