@kolkrabbi/kol-component 0.191.0 → 0.193.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.191.0",
3
+ "version": "0.193.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",
@@ -77,6 +77,11 @@ const Dropdown = ({
77
77
  * while a row is hovered would otherwise leave the consumer previewing
78
78
  * forever. It never fires while closed — a closed dropdown has no rows. */
79
79
  onOptionHover,
80
+ /* OPTIONS may carry more than `{ value, label }`: `icon` puts a glyph in the
81
+ * row's line box (ADJACENT ladder — it sits beside a label), and `shortcut`
82
+ * prints on every row that is NOT the current one, the tick taking that slot
83
+ * when it is. Both were added for the tool-palette idiom and are general —
84
+ * a menu of tools and a menu of anything else want the same two columns. */
80
85
  /* ICON-ONLY TRIGGER — an icon name, or a pre-rendered node dropped in where
81
86
  * the glyph goes. The trigger becomes the pinned square (`kol-btn-icon`) at
82
87
  * the current size, with no label, no ghost widths and no caret; the panel
@@ -259,8 +264,25 @@ const Dropdown = ({
259
264
  onPointerEnter={onOptionHover ? () => reportHover(option.value) : undefined}
260
265
  onPointerLeave={onOptionHover ? () => reportHover(null) : undefined}
261
266
  onClick={() => handleSelect(option)}
262
- shortcut={isActive ? <Icon name="check" size={resolvedSize === 'xs' ? indicatorSize('xs') : 11} /> : undefined}
267
+ /* The active row's mark is the CHECK, always it is what says
268
+ * which value is current, and nothing may take that slot from
269
+ * it. An option's own `shortcut` shows on the rows that are
270
+ * not current, which is the tool-palette idiom: the keystroke
271
+ * that would arm this variant, replaced by the tick once it
272
+ * is armed (editor-set-is-behind-its-source, 2026-09-03). */
273
+ shortcut={
274
+ isActive
275
+ ? <Icon name="check" size={resolvedSize === 'xs' ? indicatorSize('xs') : 11} />
276
+ : option.shortcut
277
+ }
263
278
  >
279
+ {/* an option's leading glyph — the ADJACENT ladder, because it
280
+ * sits in a line box beside a label, never the solo rung */}
281
+ {option.icon && (
282
+ <span className="shrink-0 inline-flex items-center" style={{ marginRight: 'var(--kol-spacing-2)' }}>
283
+ <Icon name={option.icon} size={glyphSize(resolvedSize)} />
284
+ </span>
285
+ )}
264
286
  {option.label}
265
287
  </MenuDropdownItem>
266
288
  )
@@ -30,6 +30,22 @@ import { glyphSize } from '../hooks/glyphLadders.js'
30
30
  * For a text-trigger single-select use `Dropdown`; for the two-button
31
31
  * action-half + chevron-half split use `ShapeDropdown`.
32
32
  *
33
+ * WHY THIS IS STILL ITS OWN COMPONENT, after `Dropdown` grew the icon-only
34
+ * square trigger it was asked to (2026-09-03, `editor-set-is-behind-its-
35
+ * source`). The ticket's premise — *"it hand-rolls a `<button>` re-emitting
36
+ * Button's classes"* — is gone: since 0.182.0 this wears
37
+ * `kol-btn-icon kol-btn-{size}`, the same class output at the same rungs, so
38
+ * there is no second implementation of the box left to collapse. What remains
39
+ * is ONE behaviour Dropdown does not have and should not grow for one caller:
40
+ * a click on the trigger both ARMS the last-picked variant and opens the menu,
41
+ * so a tool is selected and re-pickable in one gesture. Dropdown is a
42
+ * controlled select — its trigger opens, it does not choose — and giving it an
43
+ * `onTriggerClick` seam to serve this would be a seam for exactly one consumer.
44
+ * Two components, one class output, one popover utility, one glyph ladder: the
45
+ * duplication the ticket named is closed, and the difference that is left is
46
+ * real. What DID move to Dropdown is the part that generalises — per-option
47
+ * `icon` and `shortcut` rows, which any menu wants.
48
+ *
33
49
  * @param {Object} props
34
50
  * @param {{id: string, label: string, icon: string, shortcut?: string}[]} props.variants - Variants: menu rows + trigger glyph
35
51
  * @param {string} props.value - Active variant id (controlled)
@@ -1,4 +1,5 @@
1
1
  import SectionText from '../molecules/SectionText.jsx'
2
+ import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
2
3
  import { FULL_BLEED } from './sectionBleed.js'
3
4
  import { surfaceClass } from '../utilities/sectionSurface.js'
4
5
  import useSectionTheme from '../hooks/useSectionTheme.js'
@@ -14,8 +15,19 @@ import { minHeightClass } from './sectionHeights.js'
14
15
  * 'right' media on the right, text on the left — the default
15
16
  * 'left' media first (the old `flip`)
16
17
  * 'center' ONE column: text centred, media below
18
+ * 'top' ONE column: media ABOVE the text, both centred
19
+ * 'bottom' ONE column: text above, media below — the explicit name for what
20
+ * `center` has always rendered, kept as its own value because
21
+ * "centred" says where the column sits and nothing about the order
17
22
  * One anatomy, one prop — not a third component.
18
23
  *
24
+ * THE VERTICAL PAIR (section-split-vertical-align, kol-client-hrafn
25
+ * 2026-09-03). The filer asked what `center` renders when `media` is passed and
26
+ * could not tell from the source: it STACKS, text first, media below, capped at
27
+ * 640. So `bottom` already existed under a name that did not say so, and only
28
+ * `top` was missing. Both are values a page can name now, and `center` keeps
29
+ * its behaviour exactly, so no existing call moves.
30
+ *
19
31
  * `meta` and `actions` are mutually exclusive by intent (pick one); `actions`
20
32
  * is conventionally a row of KOL Buttons. The media column renders only when
21
33
  * `media` is passed, and `caption` gates both the gradient veil and the
@@ -59,7 +71,8 @@ import { minHeightClass } from './sectionHeights.js'
59
71
  * than only in `fill`, because a prop that silently does nothing in the default form is a
60
72
  * seam wired to nothing. Unset, each form keeps exactly what it did: `fill` centres,
61
73
  * bounded follows `align` (centred when `align="center"`, else start).
62
- * @param {'right'|'left'|'center'} [align='right'] media side, or centred single column
74
+ * @param {'right'|'left'|'center'|'top'|'bottom'} [align='right'] media side, or a centred single column: `top` puts the media above the text, `center` and `bottom` below it
75
+ * @param {boolean|ReactNode} [placeholder=false] what stands in when `media` is absent — `true` renders the DS `AssetPlaceholder` at the frame's ratio, a node renders itself. OFF by default: a text-only `SectionSplit` is a real and common call, and injecting a visible box into every one of them estate-wide is not a fix (section-split-vertical-align, kol-client-hrafn 2026-09-03, which asked for the placeholder and gets it opt-in)
63
76
  * @param {'full'|'80'|'60'|string} [height='60'] min-height on the family's ladder — full = 100dvh,
64
77
  * 80 = 70svh / 80vh, 60 = 50svh / 60vh (default), 40 = 35svh / 40vh; the columns stay vertically centred inside it
65
78
  * @param {ReactNode} caption mono caption + gradient veil over the media
@@ -87,6 +100,7 @@ export default function SectionSplit({
87
100
  mediaHover = false,
88
101
  mediaClip = true,
89
102
  align = 'right',
103
+ placeholder = false,
90
104
  textAlign,
91
105
  fill = false,
92
106
  height = '60',
@@ -106,8 +120,13 @@ export default function SectionSplit({
106
120
  ? { backgroundImage: `url(${bgImage})`, backgroundSize: 'cover', backgroundPosition: 'center' }
107
121
  : undefined
108
122
  const bleed = fullBleed ? FULL_BLEED : ''
109
- const centred = align === 'center'
110
- const mediaFirst = align === 'left'
123
+ /* the three single-column forms — `center` and `bottom` are one layout under
124
+ * two names, `top` reverses the order */
125
+ const centred = align === 'center' || align === 'top' || align === 'bottom'
126
+ /* `order-*` and not DOM order: the grid is one column in every centred form
127
+ * and below 901px, and the order utilities are what lift the media above the
128
+ * text for `left` and `top` alike. */
129
+ const mediaFirst = align === 'left' || align === 'top'
111
130
  /* unset = each form's own prior behaviour, so nothing existing moves */
112
131
  const fillText = textAlign ?? 'center'
113
132
  const boundedText = textAlign ?? (centred ? 'center' : 'start')
@@ -206,7 +225,7 @@ export default function SectionSplit({
206
225
  </div>
207
226
  )}
208
227
  </SectionText>
209
- {media && (
228
+ {(media || placeholder) && (
210
229
  /* height = the rung minus the padding; width follows the ratio and
211
230
  * caps at the column — the media takes its size from the section,
212
231
  * never gives it */
@@ -235,7 +254,9 @@ export default function SectionSplit({
235
254
  className={`kol-section-split-visual relative w-full max-w-full justify-self-center rounded-[var(--kol-radius-sm)] min-[901px]:h-[calc(var(--kol-section-h,60vh)_-_2*var(--kol-section-py,4rem))] ${mediaClip ? 'overflow-hidden' : ''} ${mediaHover ? 'is-hoverable' : ''} ${mediaFirst ? 'order-1' : ''} ${centred ? 'max-w-[640px]' : ''}`.replace(/\s+/g, ' ').trim()}
236
255
  style={{ aspectRatio: ratio }}
237
256
  >
238
- {media}
257
+ {media ?? (placeholder === true
258
+ ? <AssetPlaceholder className="h-full w-full" />
259
+ : placeholder)}
239
260
  {caption && <div className="kol-section-split-visual-veil" aria-hidden="true" />}
240
261
  {caption && <span className="kol-section-split-visual-caption">{caption}</span>}
241
262
  </div>