@kolkrabbi/kol-component 0.98.0 → 0.100.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.98.0",
3
+ "version": "0.100.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",
@@ -24,13 +24,13 @@
24
24
  "framer-motion": "^12.0.0",
25
25
  "gsap": "^3.13.0",
26
26
  "hls.js": "^1.6.0",
27
- "opentype.js": "^1.3.4",
27
+ "opentype.js": "^1.3.4 || ^2.0.0",
28
28
  "react": "^18.3.0 || ^19.0.0",
29
29
  "react-dom": "^18.3.0 || ^19.0.0",
30
30
  "react-router-dom": "^6.0.0 || ^7.0.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@kolkrabbi/kol-icons": "^0.19.0"
33
+ "@kolkrabbi/kol-icons": "^0.20.0"
34
34
  },
35
35
  "files": [
36
36
  "src",
package/src/atoms/Tag.jsx CHANGED
@@ -75,13 +75,12 @@ export default function Tag({
75
75
  * SINGLE-LINE chrome — that is exactly the kol-helper-* ramp, whose defining
76
76
  * property is `line-height: 1`. The sizes match the scale 1:1 (10/12/14), so
77
77
  * this is adopting the existing system, not restating it. */
78
- const TYPE = { sm: 'kol-helper-10', md: 'kol-helper-12', lg: 'kol-helper-14' }
78
+ /* the type rides `.kol-tag--{size}` in kol-theme since 0.69.0 (WorkListingRowsAndFilters) */
79
79
 
80
80
  const classes = [
81
81
  'kol-tag',
82
82
  VARIANTS[variant] ?? VARIANTS.primary,
83
83
  `kol-tag--${size}`,
84
- TYPE[size] ?? TYPE.sm,
85
84
  active ? 'is-active' : '',
86
85
  className
87
86
  ].filter(Boolean).join(' ')
@@ -44,7 +44,8 @@ const BOX = {
44
44
  * like I've said that before" — the ListingCardThumbBorder ruling): the selected
45
45
  * state reads from the checked ToggleCheckbox, not a fg-64 border */
46
46
  default: { layout: 'stack', border: null, bg: 'var(--kol-fg-02)', pad: 'var(--kol-pad-card-sm)' },
47
- catalog: { layout: 'fill-card', border: 'var(--kol-fg-04)', bg: 'var(--kol-fg-04)', pad: 'var(--kol-pad-card-sm) var(--kol-pad-card-md)', plateTop: true, plateBg: 'var(--kol-surface-primary)' },
47
+ /* frameHover: the frame steps fg-04 fg-16 on hover (ShellHomeSystem, 2026-08-27 the tertiary fill alone drowned the fg-04 border) */
48
+ catalog: { layout: 'fill-card', border: 'var(--kol-fg-04)', bg: 'var(--kol-fg-04)', pad: 'var(--kol-pad-card-sm) var(--kol-pad-card-md)', plateTop: true, plateBg: 'var(--kol-surface-primary)', frameHover: 'var(--kol-fg-16)' },
48
49
  /* flip: PrintGridCard's 3D turn on `isFlipped` → `selected` (ContentRowsAndPrintCard, 2026-08-27) */
49
50
  print: { layout: 'fill-card', border: null, bg: 'var(--kol-surface-secondary)', pad: 'var(--kol-pad-card-sm) var(--kol-pad-card-md)', plateTop: true, flip: true },
50
51
  article: { layout: 'stack', border: null, bg: null, pad: '0', mediaGap: 'var(--kol-spacing-4)' },
@@ -196,7 +197,8 @@ export default function ContentCard({
196
197
  frame: frame ?? MEDIA[variant]?.frame ?? false,
197
198
  ring: ring ?? MEDIA[variant]?.ring ?? false,
198
199
  borderHover: MEDIA[variant]?.borderHover ?? false,
199
- zoom: zoom ?? (isHero ? 'hero' : MEDIA[variant]?.zoom ?? false),
200
+ /* a catalog card with REAL media is image-led: it zooms (ShellHomeSystem) */
201
+ zoom: zoom ?? (isHero ? 'hero' : variant === 'catalog' ? media != null : MEDIA[variant]?.zoom ?? false),
200
202
  fade: MEDIA[variant]?.fade ?? false,
201
203
  }
202
204
 
@@ -301,7 +303,7 @@ export default function ContentCard({
301
303
 
302
304
  const common = {
303
305
  'data-tags': isHero && Array.isArray(text.tags) && text.tags.length ? text.tags.join(' ') : undefined,
304
- className: `kol-card group flex ${box.layout === 'canvas' && reveal != null ? 'has-reveal' : ''} ${expanded ? 'flex-row-reverse' : 'flex-col'} ${box.layout === 'drawer' ? 'relative overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${framed ? 'overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${box.border ? 'border' : ''} ${box.layout === 'canvas' ? 'relative' : ''} ${interactive ? 'cursor-pointer select-none' : ''} ${hoverBg && interactive ? 'kol-content-hover' : ''} ${className}`.trim(),
306
+ className: `kol-card group flex ${box.layout === 'canvas' && reveal != null ? 'has-reveal' : ''} ${expanded ? 'flex-row-reverse' : 'flex-col'} ${box.layout === 'drawer' ? 'relative overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${framed ? 'overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${box.border ? 'border' : ''} ${box.layout === 'canvas' ? 'relative' : ''} ${interactive ? 'cursor-pointer select-none' : ''} ${hoverBg && interactive ? 'kol-content-hover' : ''} ${interactive && box.frameHover ? 'kol-content-hover-frame' : ''} ${className}`.trim(),
305
307
  style: {
306
308
  /* same reason as ContentRow: rest colours are PROPERTIES, because an
307
309
  * inline background/borderColor outranks the hover class and the step
@@ -309,6 +311,7 @@ export default function ContentCard({
309
311
  '--kol-card-bg': box.bg ?? undefined,
310
312
  '--kol-card-border': box.border ? (selected ? 'var(--kol-fg-64)' : box.border) : undefined,
311
313
  '--kol-content-hover-bg': hoverBg ?? undefined,
314
+ '--kol-content-hover-border': box.frameHover ?? undefined,
312
315
  aspectRatio: box.height || expanded ? undefined : (box.layout !== 'stack' ? r : undefined),
313
316
  height: box.height,
314
317
  gridColumn: expanded ? 'span 2' : undefined,
@@ -12,7 +12,8 @@ import ContentText from './ContentText.jsx'
12
12
  *
13
13
  * @param {string} variant default | catalog | print | article | work | typeface
14
14
  * @param {ReactNode} media thumb content (omit → placeholder)
15
- * @param {number} thumb thumb edge px — overrides the ruled default; 0 hides
15
+ * @param {number|'fill'} thumb thumb edge px — overrides the ruled default; 0 hides; `'fill'` = a square
16
+ * the height of the row's content, whatever the rung (WorkListingRowsAndFilters)
16
17
  * @param {string} ratio thumb aspect-ratio — overrides the ruled default
17
18
  * @param {number} paddingY vertical padding px — overrides the ruled default
18
19
  * @param {boolean} selected
@@ -38,7 +39,12 @@ const BOX = {
38
39
  article: { thumb: 120, ratio: '1 / 1', pad: '0', gap: S6, align: 'items-start', thumbBg: 'var(--kol-fg-12)', hover: 'var(--kol-oq-02)' },
39
40
  /* work and typeface step UP at md — the shipped rows both do, and a work row
40
41
  * at a fixed 96 cannot hold the display-03 line it was ruled to carry. */
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 },
42
+ /* RULED ON /work (WorkListingRowsAndFilters, 2026-08-27): min-height 168 ("div
43
+ * 8"), 16 padding at every width ("16px padding, that's fine"), the thumb FILLS
44
+ * the content height (136 in a 168 row) with no hairline, the frame steps
45
+ * transparent → fg-08 on hover ("0 → 16 is a big jump"). No md step: the rows
46
+ * were approved at their base values. */
47
+ work: { thumb: 'fill', ratio: '1 / 1', pad: S4, gap: S4, frame: 'transparent', frameHover: 'var(--kol-fg-08)', bg: 'var(--kol-surface-secondary)', minH: 168, align: 'items-stretch', thumbRadius: 'var(--kol-radius-xs)', thumbBorder: false },
42
48
  /* typeface's row is a COLUMN, not a line: a header (name/styles left,
43
49
  * classification/year right) with a full-width specimen band under it. The
44
50
  * shipped item is `flex-col gap-6`, and forcing it into the horizontal
@@ -63,6 +69,7 @@ export default function ContentRow({
63
69
  }) {
64
70
  const box = BOX[variant] ?? BOX.default
65
71
  const thumbPx = thumb ?? box.thumb
72
+ const padY = (paddingY != null ? `${paddingY}px` : String(box.pad)).trim().split(/\s+/)[0]
66
73
 
67
74
  /* The md: STEP is a custom property, not a Tailwind variant. Tailwind cannot
68
75
  * generate `md:min-h-40` from package source (the SegmentedToggle rule), and
@@ -76,7 +83,10 @@ export default function ContentRow({
76
83
  '--kol-row-gap-md': box.gapMd,
77
84
  '--kol-row-min-h': box.minH != null ? `${box.minH}px` : undefined,
78
85
  '--kol-row-min-h-md': box.minHMd != null ? `${box.minHMd}px` : undefined,
79
- '--kol-row-thumb': `${thumbPx}px`,
86
+ /* `fill` = the rung minus the vertical padding — a definite number, not a
87
+ * stretch (a min-height row has no definite cross size, so a stretched
88
+ * aspect-ratio square resolved to the image's intrinsic width) */
89
+ '--kol-row-thumb': thumbPx === 'fill' ? `calc(${box.minH ?? 96}px - 2 * ${padY})` : `${thumbPx}px`,
80
90
  '--kol-row-thumb-md': box.thumbMd != null ? `${box.thumbMd}px` : undefined,
81
91
  '--kol-content-hover-bg': box.hover,
82
92
  '--kol-content-hover-border': box.frameHover,
@@ -103,7 +113,7 @@ export default function ContentRow({
103
113
  </>
104
114
  ) : (
105
115
  <>
106
- {thumbPx > 0 && (
116
+ {(thumbPx === 'fill' || thumbPx > 0) && (
107
117
  /* THE THUMB IS A FIXED SQUARE BOX (ContentRowsAndPrintCard, user
108
118
  * 2026-08-27: "the image should not control height, image should fit
109
119
  * the row image placeholder") — `--kol-row-thumb` wide, square, at the
@@ -111,7 +121,7 @@ export default function ContentRow({
111
121
  * media object-covers into it; row height = max(thumb, text). This
112
122
  * retires the 2026-08-15 fill-the-height ruling and the `thumbSquare`
113
123
  * exception it needed. Rows never zoom their thumb — cards keep theirs. */
114
- <div className="kol-row-thumb shrink-0 self-start">
124
+ <div className={`kol-row-thumb shrink-0 self-start ${thumbPx === 'fill' ? 'is-fill' : ''}`.trim()}>
115
125
  <ContentMedia
116
126
  ratio={null}
117
127
  border={box.thumbBorder ?? false}
@@ -52,7 +52,9 @@ const RAMP = {
52
52
  row: { title: 'kol-sans-heading-05 text-emphasis truncate', date: 'kol-mono-12 text-meta', size: 'kol-mono-12 text-meta' },
53
53
  },
54
54
  catalog: {
55
- card: { title: 'kol-mono-14 text-emphasis', detail: 'kol-mono-10 text-meta' },
55
+ /* detail truncates (ShellHomeSystem, 2026-08-27): a wrapping blurb set three
56
+ * cards' plates at three heights and misaligned the media */
57
+ card: { title: 'kol-mono-14 text-emphasis', detail: 'kol-mono-10 text-meta truncate' },
56
58
  row: { title: 'kol-mono-12 text-emphasis', detail: 'kol-mono-10 text-meta' },
57
59
  },
58
60
  print: {
@@ -85,7 +87,10 @@ const RAMP = {
85
87
  * `kol-mono-12 text-fg-64` · description `kol-sans-heading-03 text-auto`.
86
88
  * Title and type are the pair — same rung, same full ink; only the year
87
89
  * steps down. */
88
- 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' },
90
+ /* RULED ON /work beside the local WorkListItem (WorkListingRowsAndFilters,
91
+ * 2026-08-27): the typeface row's voices — title + type kol-mono-14 full
92
+ * ink, the year kol-mono-12 fg-64. Body unchanged (the site passes its face). */
93
+ row: { title: 'kol-mono-14 uppercase text-emphasis truncate', body: 'kol-sans-heading-03 leading-tight text-emphasis truncate', meta: 'kol-mono-14 text-emphasis', date: 'kol-mono-12 text-fg-64', tags: 'flex flex-wrap items-center gap-1.5' },
89
94
  },
90
95
  typeface: {
91
96
  /* RULED ON SCREEN (TypefaceCardAndRow, kol-website 2026-08-27): name and
@@ -85,7 +85,7 @@ export default function SectionText({
85
85
  {/* `kol-section-text-eyebrow` = uppercase by ROLE (kol-theme ≥0.55.0);
86
86
  * `labelClass` is the voice riding beside it */}
87
87
  {eb && <span className={cls(`kol-section-text-eyebrow ${ebClass}`, 'eyebrow')} style={slotStyle.eyebrow ?? slotStyle.label}>{eb}</span>}
88
- {headline && <Headline className={cls(`${headlineClass ?? (HEADLINE_ROLE[headlineSize] ?? HEADLINE_ROLE['heading-02'])}${headlineCase === 'upper' ? ' kol-section-text-caps' : ''}`, 'headline')} style={slotStyle.headline}>{headline}</Headline>}
88
+ {headline && <Headline className={cls(`kol-section-text-headline ${headlineClass ?? (HEADLINE_ROLE[headlineSize] ?? HEADLINE_ROLE['heading-02'])}${headlineCase === 'upper' ? ' kol-section-text-caps' : ''}`, 'headline')} style={slotStyle.headline}>{headline}</Headline>}
89
89
  {body && (typeof body === 'string' ? <p className={cls(bodyClass, 'body')} style={slotStyle.body}>{body}</p> : <div className={cls(bodyClass, 'body')} style={slotStyle.body}>{body}</div>)}
90
90
  {children}
91
91
  {actions && <div className={cls(actionsClass, 'actions')} style={slotStyle.actions}>{actions}</div>}
@@ -98,6 +98,10 @@ export default function ContentCollection({
98
98
  key={form}
99
99
  className={`m-0 list-none p-0 ${colsCls} ${className}`.replace(/\s+/g, ' ').trim()}
100
100
  style={{
101
+ /* the ROW's md step is a container query (kol-theme .kol-row) and nothing
102
+ * declared a container until 2026-08-27 (WorkListingRowsAndFilters) — the
103
+ * wall is one, so a row inside it steps on the wall's own width */
104
+ containerType: 'inline-size',
101
105
  display: 'grid',
102
106
  /* with `cols` the classes carry the tracks — an inline template would outrank them */
103
107
  gridTemplateColumns: colsCls
@@ -42,6 +42,8 @@ import IconFrame from '../atoms/IconFrame.jsx'
42
42
  * @param {boolean} props.titleUppercase — cases the title (default false)
43
43
  * @param {string} props.labelClassName — filter-group label type/ink
44
44
  * @param {boolean} props.labelUppercase — cases the group label (default true)
45
+ * groups: `{ label, key, values, stack?, className?, wrapClassName? }` — a group with ≤ 6 values stacks by
46
+ * default (`stack: false` opts out); `className` / `wrapClassName` are per-group seams
45
47
  * @param {string} props.tagVariant — filter chip variant ('primary' grey fill)
46
48
  * @param {string} props.tagSize — filter chip size
47
49
  * @param {string} props.tagActiveClassName — chip ink, selected
@@ -240,8 +242,13 @@ const ContentFilters = ({
240
242
  </div>
241
243
  ) : null
242
244
 
243
- const renderFilterGroup = (group) => (
244
- <div key={group.key} className={`flex flex-col gap-3 ${group.stack ? 'shrink-0' : 'min-w-0 flex-1'}`}>
245
+ const renderFilterGroup = (group) => {
246
+ /* a SHORT group (≤ 6 values) stacks by default (WorkListingRowsAndFilters, 2026-08-27
247
+ * user: "why do I have to say it every time? why do you default to 50/50?");
248
+ * `stack: false` opts out, `stack: true` forces */
249
+ const stacked = group.stack ?? (group.values?.length ?? 0) <= 6
250
+ return (
251
+ <div key={group.key} className={`flex flex-col gap-3 ${stacked ? 'shrink-0' : 'min-w-0 flex-1'} ${group.className ?? ''}`.trim()}>
245
252
  {/* THE CATEGORY LABEL IS THE ACTIVE INK — `kol-helper-12` at `text-fg-96`,
246
253
  * the same full opacity a SELECTED layout item wears (user ruling
247
254
  * 2026-08-15: "TAGS and other categories are ACTIVE state full opacity").
@@ -255,7 +262,9 @@ const ContentFilters = ({
255
262
  <h4 className={labelClassName} style={labelUppercase ? { textTransform: 'uppercase' } : undefined}>
256
263
  {group.label}
257
264
  </h4>
258
- <div className={group.stack ? 'flex flex-col items-start gap-2' : 'flex flex-wrap gap-2'}>
265
+ {/* the fluid group keeps 48px of room on its right (it ran flush to the
266
+ * panel's edge); `group.wrapClassName` is the per-group seam */}
267
+ <div className={`${stacked ? 'flex flex-col items-start gap-2' : 'flex flex-wrap gap-2 pr-12'} ${group.wrapClassName ?? ''}`.trim()}>
259
268
  {group.values.map((value) => (
260
269
  <Tag
261
270
  key={value}
@@ -272,6 +281,7 @@ const ContentFilters = ({
272
281
  </div>
273
282
  </div>
274
283
  )
284
+ }
275
285
 
276
286
  return (
277
287
  /* minHeight 0 on both this root and the body below: without it a flex
@@ -466,7 +476,8 @@ const ContentFilters = ({
466
476
  )}
467
477
 
468
478
  <div style={{ marginTop: 'var(--kol-spacing-6)', display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
469
- {renderItem(filteredItems, viewMode, layout)}
479
+ {/* the items panel is a CONTAINER (WorkListingRowsAndFilters, 2026-08-27) — rows step on its width */}
480
+ <div style={{ containerType: 'inline-size' }}>{renderItem(filteredItems, viewMode, layout)}</div>
470
481
  </div>
471
482
  </div>
472
483
  )