@kolkrabbi/kol-component 0.66.0 → 0.68.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.66.0",
3
+ "version": "0.68.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",
@@ -57,6 +57,10 @@ const HOVER = {
57
57
  default: 'var(--kol-oq-04)',
58
58
  catalog: 'var(--kol-surface-tertiary)',
59
59
  print: 'var(--kol-oq-04)',
60
+ /* article and work take NO surface hover, and that is a decision not a gap:
61
+ * article has no surface of its own (its media frame steps its border
62
+ * instead), and work's whole hover IS the drawer rising. A second wash under
63
+ * either would be two answers to one question. */
60
64
  article: null,
61
65
  work: null,
62
66
  typeface: 'var(--kol-surface-inverse)',
@@ -55,6 +55,12 @@ import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
55
55
  * @param {boolean} ring hairline border OVER the media, inset
56
56
  * @param {boolean} zoom the artwork creeps up inside its frame on the
57
57
  * CARD's hover — image-led variants only
58
+ * @param {boolean} fillHeight size from the HEIGHT instead of the width: the
59
+ * media fills its parent's height and its width
60
+ * follows the ratio. What every ROW wants — a
61
+ * thumb sized off the row's own height keeps the
62
+ * row's rhythm, where a fixed width leaves it
63
+ * floating in a tall row.
58
64
  * @param {ReactNode} children the real media
59
65
  */
60
66
  const FIT = {
@@ -73,6 +79,7 @@ export default function ContentMedia({
73
79
  borderHover,
74
80
  ring = false,
75
81
  zoom = false,
82
+ fillHeight = false,
76
83
  children,
77
84
  className = '',
78
85
  }) {
@@ -82,7 +89,7 @@ export default function ContentMedia({
82
89
  const round = radius ? 'rounded-[var(--kol-radius-sm)]' : ''
83
90
  return (
84
91
  <div
85
- className={`relative w-full overflow-hidden ${round} ${FIT[fit] ?? FIT.cover} ${frame ? 'bg-fg-04 border border-fg-08' : ''} ${border ? 'border border-fg-08' : ''} ${zoom ? 'kol-media-zoom' : ''} ${borderHover ? 'transition-colors hover:border-fg-16' : ''} ${ratio == null ? 'h-full' : ''} ${className}`.trim()}
92
+ className={`relative ${fillHeight ? 'h-full w-auto' : 'w-full'} overflow-hidden ${round} ${FIT[fit] ?? FIT.cover} ${frame ? 'bg-fg-04 border border-fg-08' : ''} ${border ? 'border border-fg-08' : ''} ${zoom ? 'kol-media-zoom' : ''} ${borderHover ? 'transition-colors hover:border-fg-16' : ''} ${ratio == null ? 'h-full' : ''} ${className}`.trim()}
86
93
  style={{ ...(ratio != null ? { aspectRatio: ratio } : null), background: bg }}
87
94
  >
88
95
  {children}
@@ -31,10 +31,14 @@ const BOX = {
31
31
  catalog: { thumb: 0, ratio: '1 / 1', pad: `0 ${S3}`, gap: S3, frame: 'var(--kol-fg-04)', bg: 'var(--kol-surface-tertiary)', minH: 36, align: 'items-center', hover: 'var(--kol-oq-04)' },
32
32
  print: { thumb: 0, ratio: '1 / 1', pad: `0 ${S3}`, gap: S3, frame: 'var(--kol-fg-04)', bg: 'var(--kol-surface-tertiary)', minH: 36, align: 'items-center', hover: 'var(--kol-oq-04)' },
33
33
  /* ListingCard's row thumb is `bg-fg-12` bare — a tint, no border. */
34
- article: { thumb: 120, ratio: '1 / 1', pad: '0', gap: S6, align: 'items-start', thumbBg: 'var(--kol-fg-12)' },
34
+ /* article had NO hover of any kind the only row in the family you could
35
+ * point at and get nothing back. It has no surface of its own, so it takes
36
+ * the lightest step there is. Its thumb zooms: on an article row the image
37
+ * IS the subject. */
38
+ article: { thumb: 120, ratio: '1 / 1', pad: '0', gap: S6, align: 'items-start', thumbBg: 'var(--kol-fg-12)', hover: 'var(--kol-oq-02)', thumbZoom: true },
35
39
  /* work and typeface step UP at md — the shipped rows both do, and a work row
36
40
  * at a fixed 96 cannot hold the display-03 line it was ruled to carry. */
37
- work: { thumb: 64, thumbMd: 112, ratio: '1 / 1', pad: S4, padMd: S6, gap: S4, gapMd: S6, frame: 'transparent', frameHover: 'var(--kol-fg-16)', bg: 'var(--kol-surface-secondary)', minH: 96, minHMd: 160, align: 'items-stretch', thumbRadius: 'var(--kol-radius-xs)', thumbBorder: true },
41
+ work: { thumb: 64, thumbMd: 112, ratio: '1 / 1', pad: S4, padMd: S6, gap: S4, gapMd: S6, frame: 'transparent', frameHover: 'var(--kol-fg-16)', bg: 'var(--kol-surface-secondary)', minH: 96, minHMd: 160, align: 'items-stretch', thumbRadius: 'var(--kol-radius-xs)', thumbBorder: true, thumbZoom: true },
38
42
  /* typeface's row is a COLUMN, not a line: a header (name/styles left,
39
43
  * classification/year right) with a full-width specimen band under it. The
40
44
  * shipped item is `flex-col gap-6`, and forcing it into the horizontal
@@ -100,11 +104,22 @@ export default function ContentRow({
100
104
  ) : (
101
105
  <>
102
106
  {thumbPx > 0 && (
103
- <div className="kol-row-thumb shrink-0">
107
+ /* THE THUMB FILLS THE ROW'S HEIGHT (user ruling 2026-08-15).
108
+ *
109
+ * WIDTH stays pinned to `--kol-row-thumb` — that is what stops it
110
+ * running away. Only the height stretches, and `ratio={null}` puts
111
+ * ContentMedia on `h-full` so the image object-covers into whatever
112
+ * height the row is. An earlier cut freed the width instead and let
113
+ * `aspect-ratio` fall back to the image's intrinsic size, which is how
114
+ * one card ate the page. */
115
+ <div className="kol-row-thumb shrink-0 self-stretch">
104
116
  <ContentMedia
105
- ratio={ratio ?? box.ratio}
117
+ ratio={null}
106
118
  border={box.thumbBorder ?? false}
107
119
  bg={box.thumbBg}
120
+ /* zoom where the thumb IS the subject (article · work), never on a
121
+ * 48px file chip or a between-header with no media at all */
122
+ zoom={box.thumbZoom ?? false}
108
123
  className={box.thumbRadius ? 'rounded-[var(--kol-radius-xs)]' : ''}
109
124
  >
110
125
  {media}
@@ -27,6 +27,23 @@
27
27
  * dateClass, sizeClass, metaClass — full class overrides
28
28
  */
29
29
 
30
+ /* THE ROW INK LADDER — three rungs, and only three (2026-08-15).
31
+ *
32
+ * A work row was rendering its small title, its big description AND its type
33
+ * label all at full emphasis, with only the year stepped down: four fields
34
+ * wearing two values, so nothing led. The ladder is the same one the header
35
+ * strips already use, which is why it reads as one system rather than a
36
+ * per-variant guess:
37
+ *
38
+ * emphasis THE THING YOU CAME FOR — one per row, never two
39
+ * body what identifies it — the title, the type
40
+ * meta metadata — the year, the count, the file size
41
+ *
42
+ * The ROLES, not raw `fg-*` opacities and not raw `oq-*` rungs. A role is the
43
+ * only thing that survives a theme flip and a consumer's own palette; the three
44
+ * of them are exactly the three the family needs, which is why there are three
45
+ * and not fourteen. */
46
+
30
47
  /* [variant][form][slot] → 'type-class ink-role', verbatim from the ruled table */
31
48
  const RAMP = {
32
49
  default: {
@@ -38,28 +55,37 @@ const RAMP = {
38
55
  row: { title: 'kol-mono-12 text-emphasis', detail: 'kol-mono-10 text-meta' },
39
56
  },
40
57
  print: {
41
- card: { title: 'kol-mono-14 text-body', detail: 'kol-mono-10 text-meta' },
42
- row: { title: 'kol-mono-10 text-body', detail: 'kol-mono-10 text-meta' },
58
+ card: { title: 'kol-mono-14 text-emphasis', detail: 'kol-mono-10 text-meta' },
59
+ /* mono-12 + emphasis, matching catalog — print's row IS catalog's row, so
60
+ * the two must not disagree about their own type. */
61
+ row: { title: 'kol-mono-12 text-emphasis', detail: 'kol-mono-10 text-meta' },
43
62
  },
44
63
  article: {
45
- card: { kicker: 'kol-mono-12 text-body', title: 'kol-sans-heading-03 text-emphasis', body: 'kol-mono-14 text-body', date: 'kol-mono-12 text-meta', size: 'kol-mono-12 text-body', tags: 'flex flex-wrap gap-2' },
46
- row: { kicker: 'kol-mono-12 text-body', title: 'kol-sans-heading-04 text-emphasis', body: 'kol-mono-12 text-body', date: 'kol-mono-12 text-meta', size: 'kol-mono-12 text-body', tags: 'flex flex-wrap gap-2' },
64
+ card: { kicker: 'kol-mono-12 text-meta', title: 'kol-sans-heading-03 text-emphasis', body: 'kol-mono-14 text-body', date: 'kol-mono-12 text-meta', size: 'kol-mono-12 text-meta', tags: 'flex flex-wrap gap-2' },
65
+ row: { kicker: 'kol-mono-12 text-meta', title: 'kol-sans-heading-04 text-emphasis', body: 'kol-mono-12 text-body', date: 'kol-mono-12 text-meta', size: 'kol-mono-12 text-meta', tags: 'flex flex-wrap gap-2' },
47
66
  },
48
67
  work: {
49
68
  /* INVERSE ink — the card's plate is the drawer, `surface-inverse`. Leaving
50
69
  * these on `text-emphasis` painted light type on a light plate and the
51
70
  * caption disappeared. */
52
- card: { title: 'kol-sans-display-03 text-fg-inverse', meta: 'kol-mono-12 text-fg-inverse-48', body: 'kol-mono-14 text-fg-inverse', date: 'kol-mono-12 text-fg-inverse-48', tags: 'flex flex-wrap gap-2' },
71
+ /* the drawer is an INVERSE surface, so the roles do not apply but the
72
+ * ladder does: one full ink, the rest stepped. */
73
+ card: { title: 'kol-sans-display-03 text-fg-inverse', meta: 'kol-mono-12 text-fg-inverse-64', body: 'kol-mono-14 text-fg-inverse-64', date: 'kol-mono-12 text-fg-inverse-48', tags: 'flex flex-wrap gap-2' },
53
74
  /* verbatim from WorkListItem: title `kol-mono-14` truncated · type
54
75
  * `kol-mono-12 md:kol-mono-14` at FULL ink, no opacity step · year
55
76
  * `kol-mono-12 text-fg-64` · description `kol-sans-heading-03 text-auto`.
56
77
  * Title and type are the pair — same rung, same full ink; only the year
57
78
  * steps down. */
58
- row: { title: 'kol-mono-12 text-emphasis uppercase truncate', body: 'kol-sans-heading-03 leading-tight text-emphasis truncate', meta: 'kol-mono-12 text-emphasis', date: 'kol-mono-12 text-fg-64', tags: 'flex flex-wrap items-center gap-1.5' },
79
+ row: { title: 'kol-mono-12 text-body uppercase truncate', body: 'kol-sans-heading-03 leading-tight text-emphasis truncate', meta: 'kol-mono-12 text-body', date: 'kol-mono-12 text-meta', tags: 'flex flex-wrap items-center gap-1.5' },
59
80
  },
60
81
  typeface: {
61
- card: { title: 'kol-mono-16 text-emphasis', body: 'kol-mono-14 text-fg-64', date: 'kol-mono-12 text-fg-64' },
62
- row: { title: 'kol-mono-14 uppercase text-emphasis', body: 'kol-mono-12 text-fg-64', detail: 'kol-mono-14 text-emphasis', date: 'kol-mono-12 text-fg-64' },
82
+ /* same as the row: the SPECIMEN carries the emphasis on a typeface card,
83
+ * so the name steps down. The glyph is what you came to look at. */
84
+ card: { title: 'kol-mono-16 text-body', body: 'kol-mono-12 text-meta', date: 'kol-mono-12 text-meta' },
85
+ /* the name steps DOWN to `body`: on a typeface row the SPECIMEN is the
86
+ * thing you came for and it carries the one emphasis. Two full inks in one
87
+ * block is the ladder broken, and the measurement caught it. */
88
+ row: { title: 'kol-mono-14 uppercase text-body', body: 'kol-mono-12 text-meta', detail: 'kol-mono-14 text-body', date: 'kol-mono-12 text-meta' },
63
89
  },
64
90
  }
65
91
 
@@ -162,14 +188,18 @@ export default function ContentText({
162
188
  * needs exactly that: classification over year at the right edge, which a
163
189
  * flat slot list cannot express and which was previously collapsed into one
164
190
  * `date` slot that lost a value. */
165
- const render = (entry, i) => {
191
+ const render = (entry, i, trailing = false) => {
166
192
  if (typeof entry === 'string') return line(entry)
167
193
  const [kind, ...slots] = entry
168
- const parts = slots.map((s, j) => render(s, j)).filter(Boolean)
194
+ const parts = slots.map((s, j) => render(s, j, kind === 'between' && j === slots.length - 1)).filter(Boolean)
169
195
  if (!parts.length) return null
170
196
  if (kind === 'stack') {
197
+ /* A TRAILING column right-aligns (user ruling 2026-08-15) — it sits at
198
+ * the row's right edge, so its own right edge is the one that lines up
199
+ * down the list. `items-end` only applies where the stack is trailing;
200
+ * a leading stack still reads from the left. */
171
201
  return (
172
- <div key={`stack-${i}`} className="flex w-full min-w-0 flex-col" style={{ gap: STACK[variant] ?? 'var(--kol-spacing-1)' }}>
202
+ <div key={`stack-${i}`} className={`flex w-full min-w-0 flex-col ${trailing ? 'items-end text-right' : 'items-start text-left'}`} style={{ gap: STACK[variant] ?? 'var(--kol-spacing-1)' }}>
173
203
  {parts}
174
204
  </div>
175
205
  )
@@ -190,7 +220,7 @@ export default function ContentText({
190
220
  * empty space sat between the two columns. */}
191
221
  {kind === 'between' && parts.length > 1
192
222
  ? parts.map((p, j) => (
193
- <div key={j} className={j === 0 ? 'min-w-0 flex-1' : 'shrink-0'}>{p}</div>
223
+ <div key={j} className={j === 0 ? 'min-w-0 flex-1' : 'shrink-0 text-right'}>{p}</div>
194
224
  ))
195
225
  : parts}
196
226
  </div>