@kolkrabbi/kol-component 0.7.0 → 0.8.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.
Files changed (128) hide show
  1. package/README.md +8 -0
  2. package/package.json +2 -5
  3. package/src/atoms/RotaryDial.jsx +35 -21
  4. package/src/index.js +6 -18
  5. package/src/molecules/ButtonGroup.jsx +45 -0
  6. package/src/molecules/ColorInputRow.jsx +146 -126
  7. package/src/molecules/Slider.jsx +29 -14
  8. package/src/molecules/SplitToolButton.jsx +133 -0
  9. package/src/organisms/ContentFilters.jsx +2 -2
  10. package/src/organisms/GalleryCarousel.jsx +2 -1
  11. package/src/organisms/LoaderOverlay.jsx +11 -14
  12. package/src/organisms/MediaTileGallery.jsx +57 -0
  13. package/src/organisms/MediaViewer.jsx +89 -64
  14. package/src/atoms/PriceDisplay.jsx +0 -34
  15. package/src/atoms/TextPressure.jsx +0 -331
  16. package/src/atoms/TypeSample.jsx +0 -50
  17. package/src/atoms/TypeSpecCard.jsx +0 -42
  18. package/src/chess/ChessPiece.jsx +0 -191
  19. package/src/chess/apparatus/AlternativeControlsMock.jsx +0 -460
  20. package/src/chess/apparatus/ChessAnalysisLayout.jsx +0 -24
  21. package/src/chess/apparatus/ChessBoard.jsx +0 -210
  22. package/src/chess/apparatus/ChessBoardFullscreen.jsx +0 -49
  23. package/src/chess/apparatus/ChessBoardWithControls.jsx +0 -41
  24. package/src/chess/apparatus/ChessBoardWithSidebar.jsx +0 -122
  25. package/src/chess/apparatus/ChessSidebar.jsx +0 -131
  26. package/src/chess/apparatus/GameArchiveTable.jsx +0 -493
  27. package/src/chess/apparatus/GameSelector.jsx +0 -47
  28. package/src/chess/apparatus/NotationPanel.jsx +0 -70
  29. package/src/chess/apparatus/PlaybackControls.jsx +0 -52
  30. package/src/chess/apparatus/VariationTree.jsx +0 -137
  31. package/src/chess/assets/chess-extra-set/set-1/bishop-1.svg +0 -6
  32. package/src/chess/assets/chess-extra-set/set-1/bishop.svg +0 -6
  33. package/src/chess/assets/chess-extra-set/set-1/king-1.svg +0 -9
  34. package/src/chess/assets/chess-extra-set/set-1/king.svg +0 -7
  35. package/src/chess/assets/chess-extra-set/set-1/knight-1.svg +0 -8
  36. package/src/chess/assets/chess-extra-set/set-1/knight.svg +0 -7
  37. package/src/chess/assets/chess-extra-set/set-1/pawn-1.svg +0 -3
  38. package/src/chess/assets/chess-extra-set/set-1/pawn.svg +0 -3
  39. package/src/chess/assets/chess-extra-set/set-1/queen-1.svg +0 -11
  40. package/src/chess/assets/chess-extra-set/set-1/queen.svg +0 -6
  41. package/src/chess/assets/chess-extra-set/set-1/rook-1.svg +0 -7
  42. package/src/chess/assets/chess-extra-set/set-1/rook.svg +0 -11
  43. package/src/chess/assets/chess-extra-set/set-2/bishop-1.svg +0 -4
  44. package/src/chess/assets/chess-extra-set/set-2/bishop.svg +0 -4
  45. package/src/chess/assets/chess-extra-set/set-2/king-1.svg +0 -4
  46. package/src/chess/assets/chess-extra-set/set-2/king.svg +0 -4
  47. package/src/chess/assets/chess-extra-set/set-2/knight-1.svg +0 -5
  48. package/src/chess/assets/chess-extra-set/set-2/knight.svg +0 -5
  49. package/src/chess/assets/chess-extra-set/set-2/pawn-1.svg +0 -4
  50. package/src/chess/assets/chess-extra-set/set-2/pawn.svg +0 -4
  51. package/src/chess/assets/chess-extra-set/set-2/queen-1.svg +0 -4
  52. package/src/chess/assets/chess-extra-set/set-2/queen.svg +0 -4
  53. package/src/chess/assets/chess-extra-set/set-2/rook-1.svg +0 -5
  54. package/src/chess/assets/chess-extra-set/set-2/rook.svg +0 -5
  55. package/src/chess/assets/chess-extra-set/set-3/bishop-1.svg +0 -10
  56. package/src/chess/assets/chess-extra-set/set-3/bishop.svg +0 -3
  57. package/src/chess/assets/chess-extra-set/set-3/king-1.svg +0 -12
  58. package/src/chess/assets/chess-extra-set/set-3/king.svg +0 -3
  59. package/src/chess/assets/chess-extra-set/set-3/knight-1.svg +0 -3
  60. package/src/chess/assets/chess-extra-set/set-3/knight.svg +0 -10
  61. package/src/chess/assets/chess-extra-set/set-3/pawn-1.svg +0 -10
  62. package/src/chess/assets/chess-extra-set/set-3/pawn.svg +0 -10
  63. package/src/chess/assets/chess-extra-set/set-3/queen-1.svg +0 -10
  64. package/src/chess/assets/chess-extra-set/set-3/queen.svg +0 -3
  65. package/src/chess/assets/chess-extra-set/set-3/rook-1.svg +0 -3
  66. package/src/chess/assets/chess-extra-set/set-3/rook.svg +0 -11
  67. package/src/chess/assets/chess-vector-set/ABoard.svg +0 -164
  68. package/src/chess/assets/chess-vector-set/BBiskup.svg +0 -12
  69. package/src/chess/assets/chess-vector-set/BKing.svg +0 -15
  70. package/src/chess/assets/chess-vector-set/BKnight.svg +0 -13
  71. package/src/chess/assets/chess-vector-set/BPawn.svg +0 -7
  72. package/src/chess/assets/chess-vector-set/BQueen.svg +0 -6
  73. package/src/chess/assets/chess-vector-set/BRook.svg +0 -12
  74. package/src/chess/assets/chess-vector-set/WBiskup.svg +0 -14
  75. package/src/chess/assets/chess-vector-set/WKing.svg +0 -13
  76. package/src/chess/assets/chess-vector-set/WKnight.svg +0 -10
  77. package/src/chess/assets/chess-vector-set/WPawn.svg +0 -8
  78. package/src/chess/assets/chess-vector-set/WQueen.svg +0 -17
  79. package/src/chess/assets/chess-vector-set/WRook.svg +0 -13
  80. package/src/chess/context/ChessControlsContext.jsx +0 -379
  81. package/src/chess/dashboards/ChessHero.jsx +0 -303
  82. package/src/chess/index.js +0 -32
  83. package/src/chess/utils/parsePgnTree.js +0 -97
  84. package/src/dashboards/cards/DashAlertCard.jsx +0 -44
  85. package/src/dashboards/cards/DashChartCard.jsx +0 -44
  86. package/src/dashboards/cards/DashFeaturedCard.jsx +0 -42
  87. package/src/dashboards/cards/DashListCard.jsx +0 -87
  88. package/src/dashboards/cards/DashMetricCard.jsx +0 -59
  89. package/src/dashboards/cards/DashSlotCard.jsx +0 -45
  90. package/src/dashboards/cards/DashStackedBarCard.jsx +0 -70
  91. package/src/dashboards/cards/DashTableCard.jsx +0 -31
  92. package/src/dashboards/cards/_shared/CardHeader.jsx +0 -15
  93. package/src/dashboards/cards/_shared/LegendRow.jsx +0 -25
  94. package/src/dashboards/charts/Candlestick.jsx +0 -79
  95. package/src/dashboards/charts/DonutChart.jsx +0 -87
  96. package/src/dashboards/charts/Heatmap.jsx +0 -79
  97. package/src/dashboards/charts/Histogram.jsx +0 -56
  98. package/src/dashboards/charts/LineChart.jsx +0 -157
  99. package/src/dashboards/charts/ScatterPlot.jsx +0 -93
  100. package/src/dashboards/charts/Sparkline.jsx +0 -63
  101. package/src/dashboards/index.js +0 -28
  102. package/src/dashboards/layout/DashboardGrid.jsx +0 -30
  103. package/src/dashboards/layout/GridCard.jsx +0 -44
  104. package/src/dashboards/shared/DashTooltip.jsx +0 -6
  105. package/src/dashboards/shared/formatMetric.js +0 -26
  106. package/src/dashboards/shared/useChartTooltip.js +0 -25
  107. package/src/dashboards/shared/useCountUp.js +0 -107
  108. package/src/molecules/ArticleCard.jsx +0 -178
  109. package/src/molecules/WorkListItem.jsx +0 -83
  110. package/src/molecules/foundry/SpecimenSectionHeader.jsx +0 -89
  111. package/src/organisms/ArticleHeader.jsx +0 -95
  112. package/src/organisms/ColorLoader.jsx +0 -155
  113. package/src/organisms/DiagonalMarqueeRiver.jsx +0 -138
  114. package/src/organisms/ParallaxShelf.jsx +0 -141
  115. package/src/organisms/PortableTextRenderer.jsx +0 -115
  116. package/src/organisms/ProductDetailLayout.jsx +0 -189
  117. package/src/organisms/ScrollDriftGallery.jsx +0 -214
  118. package/src/organisms/StackHero.jsx +0 -83
  119. package/src/organisms/WorkCard.jsx +0 -120
  120. package/src/organisms/WorkViewToggle.jsx +0 -170
  121. package/src/organisms/foundry/FontPreviewSection.jsx +0 -187
  122. package/src/organisms/foundry/FoundryCharacterSets.jsx +0 -113
  123. package/src/organisms/foundry/GlyphMetricsGrid.jsx +0 -335
  124. package/src/organisms/foundry/TypefaceHero.jsx +0 -107
  125. package/src/organisms/foundry/TypefaceStyleSection.jsx +0 -163
  126. package/src/organisms/foundry/VariableFontSection.jsx +0 -158
  127. package/src/organisms/foundry/glyphData.js +0 -30
  128. package/src/organisms/foundry/index.js +0 -21
@@ -1,25 +0,0 @@
1
- import { useState, useCallback, useRef } from 'react'
2
-
3
- const useChartTooltip = () => {
4
- const [activeIndex, setActiveIndex] = useState(null)
5
- const containerRef = useRef(null)
6
-
7
- const containerHandlers = {
8
- onMouseMove: (e) => {
9
- if (!containerRef.current) return
10
- const rect = containerRef.current.getBoundingClientRect()
11
- containerRef.current.style.setProperty('--tx', `${e.clientX - rect.left}px`)
12
- containerRef.current.style.setProperty('--ty', `${e.clientY - rect.top}px`)
13
- },
14
- onMouseLeave: () => setActiveIndex(null)
15
- }
16
-
17
- const handlers = useCallback((index) => ({
18
- onMouseEnter: () => setActiveIndex(index),
19
- onMouseLeave: () => setActiveIndex(null)
20
- }), [])
21
-
22
- return { activeIndex, handlers, containerHandlers, containerRef }
23
- }
24
-
25
- export default useChartTooltip
@@ -1,107 +0,0 @@
1
- import { useState, useEffect, useRef } from 'react'
2
-
3
- const parseNumeric = (value) => {
4
- const str = String(value)
5
- const match = str.match(/^([^0-9]*?)([\d,.]+)(.*?)$/)
6
- if (!match) return null
7
- const prefix = match[1]
8
- const numStr = match[2]
9
- const suffix = match[3]
10
- const num = parseFloat(numStr.replace(/,/g, ''))
11
- if (isNaN(num)) return null
12
- const hasCommas = numStr.includes(',')
13
- const decimalMatch = numStr.match(/\.(\d+)/)
14
- const decimals = decimalMatch ? decimalMatch[1].length : 0
15
- return { prefix, num, suffix, hasCommas, decimals }
16
- }
17
-
18
- const formatNumber = (n, hasCommas, decimals) => {
19
- const fixed = n.toFixed(decimals)
20
- if (!hasCommas) return fixed
21
- const [intPart, decPart] = fixed.split('.')
22
- const withCommas = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
23
- return decPart ? `${withCommas}.${decPart}` : withCommas
24
- }
25
-
26
- const useCountUp = (targetValue, { duration = 600, enabled = true } = {}) => {
27
- const [displayValue, setDisplayValue] = useState(targetValue)
28
- const ref = useRef(null)
29
- const hasEnteredRef = useRef(false)
30
- const prevTargetRef = useRef(targetValue)
31
- const rafRef = useRef(null)
32
- const elRef = useRef(null)
33
-
34
- useEffect(() => {
35
- if (!enabled) {
36
- setDisplayValue(targetValue)
37
- return
38
- }
39
-
40
- const animate = (from, to) => {
41
- const parsed = parseNumeric(to)
42
- if (!parsed) {
43
- setDisplayValue(to)
44
- return
45
- }
46
-
47
- const parsedFrom = parseNumeric(from)
48
- const startNum = parsedFrom ? parsedFrom.num : 0
49
- const endNum = parsed.num
50
- const startTime = performance.now()
51
-
52
- const step = (now) => {
53
- const elapsed = now - startTime
54
- const t = Math.min(elapsed / duration, 1)
55
- const eased = t * (2 - t) // ease-out quadratic
56
- const current = startNum + (endNum - startNum) * eased
57
- const formatted = `${parsed.prefix}${formatNumber(current, parsed.hasCommas, parsed.decimals)}${parsed.suffix}`
58
- setDisplayValue(formatted)
59
- if (t < 1) {
60
- rafRef.current = requestAnimationFrame(step)
61
- }
62
- }
63
-
64
- if (rafRef.current) cancelAnimationFrame(rafRef.current)
65
- rafRef.current = requestAnimationFrame(step)
66
- }
67
-
68
- // On target change while already visible
69
- if (hasEnteredRef.current && prevTargetRef.current !== targetValue) {
70
- animate(prevTargetRef.current, targetValue)
71
- prevTargetRef.current = targetValue
72
- return
73
- }
74
-
75
- prevTargetRef.current = targetValue
76
-
77
- // IntersectionObserver for first entry
78
- if (hasEnteredRef.current) return
79
-
80
- const el = elRef.current
81
- if (!el) {
82
- setDisplayValue(targetValue)
83
- return
84
- }
85
-
86
- const observer = new IntersectionObserver(
87
- ([entry]) => {
88
- if (entry.isIntersecting && !hasEnteredRef.current) {
89
- hasEnteredRef.current = true
90
- animate('0', targetValue)
91
- observer.disconnect()
92
- }
93
- },
94
- { threshold: 0.1 }
95
- )
96
- observer.observe(el)
97
-
98
- return () => {
99
- observer.disconnect()
100
- if (rafRef.current) cancelAnimationFrame(rafRef.current)
101
- }
102
- }, [targetValue, duration, enabled])
103
-
104
- return { displayValue, ref: elRef }
105
- }
106
-
107
- export default useCountUp
@@ -1,178 +0,0 @@
1
- import Pill from '../atoms/Pill.jsx'
2
- import Image from './Image.jsx'
3
-
4
- /**
5
- * Optional link wrapper — the one place routing lives. `http*`/`mailto` hrefs
6
- * open in a new tab; any other href renders a plain same-tab anchor with an
7
- * `onNavigate(event)` seam an SPA consumer intercepts (preventDefault + its
8
- * router). No `href` → the bare content, no anchor.
9
- */
10
- function CardLink({ href, onNavigate, className, children }) {
11
- if (!href) return <div className={className}>{children}</div>
12
- const isExternal = href.startsWith('http') || href.startsWith('mailto')
13
- return (
14
- <a
15
- href={href}
16
- onClick={isExternal ? undefined : onNavigate}
17
- target={isExternal ? '_blank' : undefined}
18
- rel={isExternal ? 'noreferrer noopener' : undefined}
19
- className={className}
20
- >
21
- {children}
22
- </a>
23
- )
24
- }
25
-
26
- /** Cover-fit thumbnail (or fg-token placeholder box when no src). */
27
- function Thumb({ src, alt }) {
28
- if (!src) return null
29
- return (
30
- <Image
31
- src={src}
32
- alt={alt}
33
- className="object-cover"
34
- style={{ width: '100%', height: '100%', objectFit: 'cover' }}
35
- />
36
- )
37
- }
38
-
39
- /**
40
- * ArticleCard — one blog-card family with three sizes (`default` / `hero` /
41
- * `mini`), collapsing what were three byte-identical duplicate components. All
42
- * three share the flat view-model (title, excerpt, thumbnail, tags, meta) and
43
- * the link seam; `size` picks the layout.
44
- *
45
- * - **default** — grid tile: thumbnail (landscape/portrait) over tag Pills, a
46
- * mono title, an excerpt, and a `date • readingTime` meta Pill.
47
- * - **hero** — index masthead: an optional header row (`label` + `meta`),
48
- * a big 16/9 image that zooms on hover, then kicker → title → summary.
49
- * - **mini** — sidebar row: a fixed 120×120 thumbnail beside title → summary →
50
- * meta.
51
- *
52
- * De-Sanitized flat props, no router import. Placeholder thumbnails use
53
- * fg-opacity tokens (theme-aware — no `.dark` style injection). Every string
54
- * is authored at the call site in its final case — no `text-transform`, no JS
55
- * casing (the kicker/meta `uppercase` are presentational classes only).
56
- *
57
- * @param {'default'|'hero'|'mini'} size layout preset (default 'default')
58
- * @param {string} title card title
59
- * @param {string} excerpt summary/excerpt paragraph (alias: `summary`)
60
- * @param {string} summary alias for `excerpt` (hero/mini call it this)
61
- * @param {string} kicker hero-only eyebrow above the title
62
- * @param {string} label hero header label (was the hardcoded "Featured")
63
- * @param {string[]} meta hero header chips / mini meta line (joined ` • `)
64
- * @param {string} date default meta — left of the `•`
65
- * @param {string} readingTime default meta — right of the `•`
66
- * @param {string[]} tags default: rendered as Pills; hero/mini: `data-tags` only
67
- * @param {string} thumbnail image src (omit → fg-token placeholder)
68
- * @param {'landscape'|'portrait'} aspect default thumbnail ratio (default 'landscape')
69
- * @param {boolean} showHeader hero: show the label/meta header row (default true)
70
- * @param {string} href link target; `http*`/`mailto` → new tab, else same-tab seam
71
- * @param {Function} onNavigate (event) => void — same-tab click seam (SPA intercept)
72
- * @param {string} className extra classes on the root
73
- */
74
- export default function ArticleCard({
75
- size = 'default',
76
- title,
77
- excerpt,
78
- summary,
79
- kicker,
80
- label,
81
- meta,
82
- date,
83
- readingTime,
84
- tags = [],
85
- thumbnail,
86
- aspect = 'landscape',
87
- showHeader = true,
88
- href,
89
- onNavigate,
90
- className = '',
91
- }) {
92
- const body = excerpt ?? summary
93
- const dataTags = tags?.length ? tags.join(' ') : undefined
94
-
95
- if (size === 'hero') {
96
- return (
97
- <CardLink href={href} onNavigate={onNavigate} className={`block group ${className}`.trim()}>
98
- <article className="w-full" data-tags={dataTags}>
99
- {showHeader && (label || meta?.length) && (
100
- <div className="flex justify-between items-center mb-4">
101
- {label && <div className="kol-helper-14 text-fg-64">{label}</div>}
102
- {meta?.length > 0 && (
103
- <div className="flex gap-3 kol-helper-12 text-fg-48">
104
- {meta.map((item, i) => <span key={i}>{item}</span>)}
105
- </div>
106
- )}
107
- </div>
108
- )}
109
- <div className="aspect-[16/9] mb-4 overflow-hidden w-full bg-fg-04 border border-fg-08 hover:border-fg-16 rounded">
110
- {thumbnail && (
111
- <Image
112
- src={thumbnail}
113
- alt={title}
114
- className="object-cover transition-transform duration-300 group-hover:scale-105"
115
- style={{ width: '100%', height: '100%', objectFit: 'cover' }}
116
- />
117
- )}
118
- </div>
119
- <div className="space-y-3">
120
- {kicker && (
121
- <div className="kol-helper-16 uppercase tracking-wide text-fg-64">{kicker}</div>
122
- )}
123
- <h2 className="kol-sans-heading-03 transition-opacity duration-200 group-hover:opacity-70 line-clamp-2">
124
- {title}
125
- </h2>
126
- {body && <p className="kol-mono-14 text-fg-48 line-clamp-3">{body}</p>}
127
- </div>
128
- </article>
129
- </CardLink>
130
- )
131
- }
132
-
133
- if (size === 'mini') {
134
- const metaText = Array.isArray(meta) ? meta.join(' • ') : meta
135
- return (
136
- <CardLink
137
- href={href}
138
- onNavigate={onNavigate}
139
- className={`group flex gap-6 items-start transition-opacity hover:opacity-80 ${className}`.trim()}
140
- >
141
- <div className="flex-shrink-0 w-[120px] h-[120px] overflow-hidden rounded bg-fg-12" data-tags={dataTags}>
142
- <Thumb src={thumbnail} alt={title} />
143
- </div>
144
- <div className="flex-1 min-w-0 flex flex-col gap-2.5">
145
- <h4 className="kol-mono-14 line-clamp-2 transition-opacity group-hover:opacity-80">{title}</h4>
146
- {body && <p className="kol-mono-14 text-fg-64 line-clamp-2">{body}</p>}
147
- {metaText && <div className="kol-helper-12 text-fg-80 uppercase">{metaText}</div>}
148
- </div>
149
- </CardLink>
150
- )
151
- }
152
-
153
- // size === 'default'
154
- return (
155
- <CardLink href={href} onNavigate={onNavigate} className={`block group cursor-pointer w-full max-w-full ${className}`.trim()}>
156
- <article className="w-full max-w-full" data-tags={dataTags}>
157
- <div
158
- className="mb-4 overflow-hidden w-full rounded bg-fg-04 border border-fg-08"
159
- style={{ aspectRatio: aspect === 'portrait' ? '3/4' : '16/9' }}
160
- >
161
- <Thumb src={thumbnail} alt={title} />
162
- </div>
163
- {tags.length > 0 && (
164
- <div className="flex flex-wrap gap-2 mb-2">
165
- {tags.map((tag, i) => (
166
- <Pill key={i} variant="inverse" size="sm">{tag}</Pill>
167
- ))}
168
- </div>
169
- )}
170
- <h3 className="mb-2 kol-mono-20 transition-opacity duration-200 group-hover:opacity-70">{title}</h3>
171
- {body && <p className="mb-2 kol-mono-14 text-fg-64">{body}</p>}
172
- {(date || readingTime) && (
173
- <Pill variant="subtle" size="sm">{[date, readingTime].filter(Boolean).join(' • ')}</Pill>
174
- )}
175
- </article>
176
- </CardLink>
177
- )
178
- }
@@ -1,83 +0,0 @@
1
- import Image from './Image.jsx'
2
- import Tag from '../atoms/Tag.jsx'
3
-
4
- /**
5
- * WorkListItem — the list/row view of a `/work` project: the row twin of
6
- * WorkCard. A horizontal card — a small square thumbnail on the left, then a
7
- * content column split into a header row (title + tag chips on the left, type +
8
- * year right-aligned) above a large display preview line (the project
9
- * description). Hover raises a border; `active` persists that treatment for the
10
- * last-hovered row so a parent list can mark one active.
11
- *
12
- * Shares WorkCard's flat project bag so a `/work` view can toggle grid ⇄ list
13
- * as a render swap, not a data reshape. The whole row is an anchor (`href`) —
14
- * the source row was not a link (its parent list owned navigation); as the
15
- * list-view counterpart to WorkCard it is the click target itself. Thumbnail
16
- * routes through DS Image (graceful missing-asset fallback); tags render as DS
17
- * Tag chips. Router-agnostic via `onNavigate(href, event)`.
18
- *
19
- * Text casing: no text-transform (KOL rule) — the source's `uppercase` title
20
- * and `capitalize` type are dropped; strings render verbatim as authored.
21
- *
22
- * @param {string} title header title
23
- * @param {string} thumbnail thumbnail URL; the block is omitted when absent
24
- * @param {string[]} tags rendered as Tag chips under the title
25
- * @param {string} type right-column label
26
- * @param {string|number} year right-column, below type
27
- * @param {string} description large display preview line
28
- * @param {string} href row navigation target
29
- * @param {boolean} active persist the hover treatment (last-hovered row)
30
- * @param {Function} onMouseEnter hover callback (parent tracks the active row)
31
- * @param {Function} onNavigate (href, event) => void — anchor click seam for SPA routing
32
- */
33
- export default function WorkListItem({
34
- title,
35
- thumbnail,
36
- tags,
37
- type,
38
- year,
39
- description,
40
- href,
41
- active = false,
42
- onMouseEnter,
43
- onNavigate,
44
- }) {
45
- return (
46
- <a
47
- href={href}
48
- onClick={onNavigate ? (e) => onNavigate(href, e) : undefined}
49
- onMouseEnter={onMouseEnter}
50
- className={`self-stretch min-h-24 md:min-h-40 p-4 md:p-6 rounded flex items-stretch gap-4 md:gap-6 mb-4 md:mb-6 overflow-hidden cursor-pointer transition-all duration-300 bg-surface-secondary border ${active ? 'border-fg-16' : 'border-transparent hover:border-fg-16'}`}
51
- >
52
- {thumbnail && (
53
- <div className="shrink-0 w-16 h-16 md:w-28 md:h-28 rounded-[2px] overflow-hidden border border-fg-08">
54
- <Image src={thumbnail} alt={title} category="work" name={title} className="w-full h-full object-cover" />
55
- </div>
56
- )}
57
-
58
- <div className="flex flex-col justify-between min-w-0 flex-1 gap-3 md:gap-4">
59
- <div className="flex justify-between items-start md:items-center gap-4">
60
- <div className="flex flex-col gap-1 md:gap-2 min-w-0">
61
- <div className="kol-mono-14 truncate">{title}</div>
62
- {tags?.length > 0 && (
63
- <div className="flex flex-wrap gap-1.5">
64
- {tags.map((t) => (
65
- <Tag key={t} size="sm" variant="naked" hash={false}>{t}</Tag>
66
- ))}
67
- </div>
68
- )}
69
- </div>
70
-
71
- <div className="flex flex-col items-end gap-1 md:gap-2 shrink-0">
72
- <span className="kol-mono-12 md:kol-mono-14">{type}</span>
73
- <span className="kol-mono-12 text-fg-64">{year}</span>
74
- </div>
75
- </div>
76
-
77
- <p className="kol-sans-heading-03 text-auto leading-tight whitespace-nowrap overflow-hidden text-ellipsis">
78
- {description}
79
- </p>
80
- </div>
81
- </a>
82
- )
83
- }
@@ -1,89 +0,0 @@
1
- import { Icon } from '@kolkrabbi/kol-icons'
2
- import Dropdown from '../Dropdown.jsx'
3
- import Divider from '../../atoms/Divider.jsx'
4
-
5
- /* taxonomy-ok: nests DS Dropdown + Divider (relative imports) plus kol-icons's
6
- * Icon (a package import the relative-import check can't see). */
7
-
8
- /**
9
- * SpecimenSectionHeader — the shared header every type-specimen section mounts
10
- * under. A title (with an optional inline icon) on the left, up to two
11
- * controlled dropdowns (weight + style/axis) on the right, and a horizontal
12
- * divider beneath. It is the connective tissue that makes the foundry sections
13
- * read as one system, so it is built before the sections that consume it.
14
- *
15
- * Fully controlled and data-free: it holds no font data, only label/badge text,
16
- * an icon name, and the two dropdowns' value/onChange pairs supplied by the
17
- * parent section. Promoted from the monorepo's route-local `FoundrySection`;
18
- * the `Foundry*` name was a route artifact, this name describes the role.
19
- *
20
- * Title source forks: `label` wins, else `badgeText`. `size` swaps the title
21
- * type stop (`sm` → kol-mono-12, else kol-mono-16). Each dropdown is
22
- * independently gated; the weight dropdown also requires a non-empty
23
- * `weightOptions`.
24
- *
25
- * Text casing: title and option labels render verbatim as authored — no
26
- * text-transform. Author "Roman" / "Italic" / "Character Set" in intended case.
27
- *
28
- * @param {Object} props
29
- * @param {string} props.label - Title text (takes precedence over badgeText).
30
- * @param {string} props.badgeText - Fallback title when label is unset.
31
- * @param {string} props.icon - KOL Icon name shown after the title; omit to hide.
32
- * @param {'sm'|'lg'} props.size - Title type stop (default 'lg').
33
- * @param {string} props.selectedStyle - Controlled value of the style/axis dropdown.
34
- * @param {Function} props.onStyleChange - Style dropdown change handler.
35
- * @param {boolean} props.showDropdown - Render the style/axis dropdown (default true).
36
- * @param {Array<{label,value}>} props.styleOptions - Style dropdown options (default Roman/Italic).
37
- * @param {string} props.selectedWeight - Controlled value of the weight dropdown.
38
- * @param {Function} props.onWeightChange - Weight dropdown change handler.
39
- * @param {boolean} props.showWeightDropdown - Gate the weight dropdown (default true; also needs options).
40
- * @param {Array<{label,value}>} props.weightOptions - Weight dropdown options (default []).
41
- */
42
- const SpecimenSectionHeader = ({
43
- label,
44
- badgeText,
45
- icon,
46
- size = 'lg',
47
- selectedStyle,
48
- onStyleChange,
49
- showDropdown = true,
50
- styleOptions = [
51
- { label: 'Roman', value: 'roman' },
52
- { label: 'Italic', value: 'italic' },
53
- ],
54
- selectedWeight,
55
- onWeightChange,
56
- showWeightDropdown = true,
57
- weightOptions = [],
58
- }) => {
59
- const title = label || badgeText
60
- // kol-mono-text-lg / kol-mono-sm-regular don't exist in the DS theme; the
61
- // nearest mono stops are kol-mono-16 (lg) and kol-mono-12 (sm).
62
- const titleClass = size === 'sm' ? 'kol-mono-12' : 'kol-mono-16'
63
-
64
- return (
65
- <div className="flex flex-col gap-[13px]">
66
- <div className="w-full flex flex-row justify-between items-end gap-4">
67
- {/* Left: title + optional icon */}
68
- <div className="flex items-center gap-3 md:gap-4">
69
- <span className={titleClass}>{title}</span>
70
- {icon && <Icon name={icon} size={20} />}
71
- </div>
72
-
73
- {/* Right: controlled dropdowns */}
74
- <div className="flex items-center gap-4">
75
- {showWeightDropdown && weightOptions.length > 0 && (
76
- <Dropdown options={weightOptions} value={selectedWeight} onChange={onWeightChange} />
77
- )}
78
- {showDropdown && (
79
- <Dropdown options={styleOptions} value={selectedStyle} onChange={onStyleChange} />
80
- )}
81
- </div>
82
- </div>
83
-
84
- <Divider variant="horizontal" />
85
- </div>
86
- )
87
- }
88
-
89
- export default SpecimenSectionHeader
@@ -1,95 +0,0 @@
1
- import Pill from '../atoms/Pill.jsx'
2
- import Avatar from '../atoms/Avatar.jsx'
3
- import Image from '../molecules/Image.jsx'
4
-
5
- /**
6
- * ArticleHeader — long-form article masthead: a row of tag Pills, a display
7
- * title, an author cluster (DS Avatar + name + role), a `date • readingTime`
8
- * meta line, an optional excerpt, and an optional hero image. Pure
9
- * presentational composition of a flat prop bag.
10
- *
11
- * De-Sanitized: the four embedded Sanity image-URL builders and the
12
- * `resolveImageUrl` unpacker are gone — `heroImage` is a plain resolved src
13
- * string handed straight to DS Image (which owns its own missing-asset
14
- * fallback). The author avatar is the DS Avatar atom (initials), not a
15
- * hand-rolled `<img>` — so `authorInitial` replaces the Sanity `authorImage`.
16
- * The app `reveal` entrance utility is dropped.
17
- *
18
- * Name and meta render uppercase via a presentational class only — strings are
19
- * authored as-is at the call site, no JS casing.
20
- *
21
- * @param {string[]} tags Pill labels; row hidden when empty
22
- * @param {string} title display title
23
- * @param {string} authorName author name (rendered uppercase via class)
24
- * @param {string} authorTitle author role/title line
25
- * @param {string} authorInitial avatar glyph; defaults to authorName's first char
26
- * @param {string} date publication date text
27
- * @param {string} readingTime reading-time text (prefixed `• ` in the meta line)
28
- * @param {string} excerpt lede paragraph; block hidden when falsy
29
- * @param {string} heroImage resolved hero image src (omit → no hero block)
30
- * @param {string} className extra classes on the <header>
31
- */
32
- export default function ArticleHeader({
33
- tags = [],
34
- title,
35
- authorName,
36
- authorTitle,
37
- authorInitial,
38
- date,
39
- readingTime,
40
- excerpt,
41
- heroImage,
42
- className = '',
43
- }) {
44
- const initial = authorInitial ?? (authorName ? authorName.trim().charAt(0) : '')
45
-
46
- return (
47
- <header className={`pb-12 ${className}`.trim()}>
48
- <div className="max-w-[1400px] mx-auto flex flex-col">
49
- {tags.length > 0 && (
50
- <div className="flex flex-wrap items-center gap-2 pb-6">
51
- {tags.map((tag, i) => (
52
- <Pill key={i} variant="inverse" size="md">{tag}</Pill>
53
- ))}
54
- </div>
55
- )}
56
-
57
- <div className="flex flex-col gap-6">
58
- <h1 className="kol-prose-title text-balance">{title}</h1>
59
-
60
- <div className="flex flex-wrap items-center gap-6 text-fg-64 pb-3">
61
- <div className="flex items-center gap-4">
62
- <Avatar initial={initial} size="lg" />
63
- <div className="flex flex-col">
64
- <span className="kol-helper-12 uppercase text-fg-48">{authorName}</span>
65
- <span className="kol-mono-12 text-fg-64">{authorTitle}</span>
66
- </div>
67
- </div>
68
- <div className="flex items-center gap-3 kol-helper-12 uppercase text-fg-48">
69
- <span>{date}</span>
70
- {readingTime && <span>• {readingTime}</span>}
71
- </div>
72
- </div>
73
-
74
- {excerpt && (
75
- <div className="pb-4 w-full lg:w-[80%]">
76
- <p className="kol-mono-14 text-fg-64">{excerpt}</p>
77
- </div>
78
- )}
79
-
80
- {heroImage && (
81
- <div className="rounded overflow-hidden border border-fg-08">
82
- <Image
83
- src={heroImage}
84
- alt=""
85
- loading="eager"
86
- className="object-cover"
87
- style={{ width: '100%', aspectRatio: '4/2', objectFit: 'cover' }}
88
- />
89
- </div>
90
- )}
91
- </div>
92
- </div>
93
- </header>
94
- )
95
- }