@kolkrabbi/kol-component 0.7.0 → 0.9.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/README.md +8 -0
- package/package.json +2 -5
- package/src/atoms/RotaryDial.jsx +35 -21
- package/src/hooks/colorMath.js +214 -0
- package/src/hooks/useEyedropper.js +124 -0
- package/src/index.js +15 -18
- package/src/molecules/ButtonGroup.jsx +45 -0
- package/src/molecules/ColorInputRow.jsx +146 -126
- package/src/molecules/PaletteHarmonyWheel.jsx +192 -0
- package/src/molecules/Slider.jsx +29 -14
- package/src/molecules/SplitToolButton.jsx +133 -0
- package/src/organisms/ContentFilters.jsx +2 -2
- package/src/organisms/GalleryCarousel.jsx +2 -1
- package/src/organisms/LoaderOverlay.jsx +11 -14
- package/src/organisms/MediaTileGallery.jsx +57 -0
- package/src/organisms/MediaViewer.jsx +89 -64
- package/src/atoms/PriceDisplay.jsx +0 -34
- package/src/atoms/TextPressure.jsx +0 -331
- package/src/atoms/TypeSample.jsx +0 -50
- package/src/atoms/TypeSpecCard.jsx +0 -42
- package/src/chess/ChessPiece.jsx +0 -191
- package/src/chess/apparatus/AlternativeControlsMock.jsx +0 -460
- package/src/chess/apparatus/ChessAnalysisLayout.jsx +0 -24
- package/src/chess/apparatus/ChessBoard.jsx +0 -210
- package/src/chess/apparatus/ChessBoardFullscreen.jsx +0 -49
- package/src/chess/apparatus/ChessBoardWithControls.jsx +0 -41
- package/src/chess/apparatus/ChessBoardWithSidebar.jsx +0 -122
- package/src/chess/apparatus/ChessSidebar.jsx +0 -131
- package/src/chess/apparatus/GameArchiveTable.jsx +0 -493
- package/src/chess/apparatus/GameSelector.jsx +0 -47
- package/src/chess/apparatus/NotationPanel.jsx +0 -70
- package/src/chess/apparatus/PlaybackControls.jsx +0 -52
- package/src/chess/apparatus/VariationTree.jsx +0 -137
- package/src/chess/assets/chess-extra-set/set-1/bishop-1.svg +0 -6
- package/src/chess/assets/chess-extra-set/set-1/bishop.svg +0 -6
- package/src/chess/assets/chess-extra-set/set-1/king-1.svg +0 -9
- package/src/chess/assets/chess-extra-set/set-1/king.svg +0 -7
- package/src/chess/assets/chess-extra-set/set-1/knight-1.svg +0 -8
- package/src/chess/assets/chess-extra-set/set-1/knight.svg +0 -7
- package/src/chess/assets/chess-extra-set/set-1/pawn-1.svg +0 -3
- package/src/chess/assets/chess-extra-set/set-1/pawn.svg +0 -3
- package/src/chess/assets/chess-extra-set/set-1/queen-1.svg +0 -11
- package/src/chess/assets/chess-extra-set/set-1/queen.svg +0 -6
- package/src/chess/assets/chess-extra-set/set-1/rook-1.svg +0 -7
- package/src/chess/assets/chess-extra-set/set-1/rook.svg +0 -11
- package/src/chess/assets/chess-extra-set/set-2/bishop-1.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/bishop.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/king-1.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/king.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/knight-1.svg +0 -5
- package/src/chess/assets/chess-extra-set/set-2/knight.svg +0 -5
- package/src/chess/assets/chess-extra-set/set-2/pawn-1.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/pawn.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/queen-1.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/queen.svg +0 -4
- package/src/chess/assets/chess-extra-set/set-2/rook-1.svg +0 -5
- package/src/chess/assets/chess-extra-set/set-2/rook.svg +0 -5
- package/src/chess/assets/chess-extra-set/set-3/bishop-1.svg +0 -10
- package/src/chess/assets/chess-extra-set/set-3/bishop.svg +0 -3
- package/src/chess/assets/chess-extra-set/set-3/king-1.svg +0 -12
- package/src/chess/assets/chess-extra-set/set-3/king.svg +0 -3
- package/src/chess/assets/chess-extra-set/set-3/knight-1.svg +0 -3
- package/src/chess/assets/chess-extra-set/set-3/knight.svg +0 -10
- package/src/chess/assets/chess-extra-set/set-3/pawn-1.svg +0 -10
- package/src/chess/assets/chess-extra-set/set-3/pawn.svg +0 -10
- package/src/chess/assets/chess-extra-set/set-3/queen-1.svg +0 -10
- package/src/chess/assets/chess-extra-set/set-3/queen.svg +0 -3
- package/src/chess/assets/chess-extra-set/set-3/rook-1.svg +0 -3
- package/src/chess/assets/chess-extra-set/set-3/rook.svg +0 -11
- package/src/chess/assets/chess-vector-set/ABoard.svg +0 -164
- package/src/chess/assets/chess-vector-set/BBiskup.svg +0 -12
- package/src/chess/assets/chess-vector-set/BKing.svg +0 -15
- package/src/chess/assets/chess-vector-set/BKnight.svg +0 -13
- package/src/chess/assets/chess-vector-set/BPawn.svg +0 -7
- package/src/chess/assets/chess-vector-set/BQueen.svg +0 -6
- package/src/chess/assets/chess-vector-set/BRook.svg +0 -12
- package/src/chess/assets/chess-vector-set/WBiskup.svg +0 -14
- package/src/chess/assets/chess-vector-set/WKing.svg +0 -13
- package/src/chess/assets/chess-vector-set/WKnight.svg +0 -10
- package/src/chess/assets/chess-vector-set/WPawn.svg +0 -8
- package/src/chess/assets/chess-vector-set/WQueen.svg +0 -17
- package/src/chess/assets/chess-vector-set/WRook.svg +0 -13
- package/src/chess/context/ChessControlsContext.jsx +0 -379
- package/src/chess/dashboards/ChessHero.jsx +0 -303
- package/src/chess/index.js +0 -32
- package/src/chess/utils/parsePgnTree.js +0 -97
- package/src/dashboards/cards/DashAlertCard.jsx +0 -44
- package/src/dashboards/cards/DashChartCard.jsx +0 -44
- package/src/dashboards/cards/DashFeaturedCard.jsx +0 -42
- package/src/dashboards/cards/DashListCard.jsx +0 -87
- package/src/dashboards/cards/DashMetricCard.jsx +0 -59
- package/src/dashboards/cards/DashSlotCard.jsx +0 -45
- package/src/dashboards/cards/DashStackedBarCard.jsx +0 -70
- package/src/dashboards/cards/DashTableCard.jsx +0 -31
- package/src/dashboards/cards/_shared/CardHeader.jsx +0 -15
- package/src/dashboards/cards/_shared/LegendRow.jsx +0 -25
- package/src/dashboards/charts/Candlestick.jsx +0 -79
- package/src/dashboards/charts/DonutChart.jsx +0 -87
- package/src/dashboards/charts/Heatmap.jsx +0 -79
- package/src/dashboards/charts/Histogram.jsx +0 -56
- package/src/dashboards/charts/LineChart.jsx +0 -157
- package/src/dashboards/charts/ScatterPlot.jsx +0 -93
- package/src/dashboards/charts/Sparkline.jsx +0 -63
- package/src/dashboards/index.js +0 -28
- package/src/dashboards/layout/DashboardGrid.jsx +0 -30
- package/src/dashboards/layout/GridCard.jsx +0 -44
- package/src/dashboards/shared/DashTooltip.jsx +0 -6
- package/src/dashboards/shared/formatMetric.js +0 -26
- package/src/dashboards/shared/useChartTooltip.js +0 -25
- package/src/dashboards/shared/useCountUp.js +0 -107
- package/src/molecules/ArticleCard.jsx +0 -178
- package/src/molecules/WorkListItem.jsx +0 -83
- package/src/molecules/foundry/SpecimenSectionHeader.jsx +0 -89
- package/src/organisms/ArticleHeader.jsx +0 -95
- package/src/organisms/ColorLoader.jsx +0 -155
- package/src/organisms/DiagonalMarqueeRiver.jsx +0 -138
- package/src/organisms/ParallaxShelf.jsx +0 -141
- package/src/organisms/PortableTextRenderer.jsx +0 -115
- package/src/organisms/ProductDetailLayout.jsx +0 -189
- package/src/organisms/ScrollDriftGallery.jsx +0 -214
- package/src/organisms/StackHero.jsx +0 -83
- package/src/organisms/WorkCard.jsx +0 -120
- package/src/organisms/WorkViewToggle.jsx +0 -170
- package/src/organisms/foundry/FontPreviewSection.jsx +0 -187
- package/src/organisms/foundry/FoundryCharacterSets.jsx +0 -113
- package/src/organisms/foundry/GlyphMetricsGrid.jsx +0 -335
- package/src/organisms/foundry/TypefaceHero.jsx +0 -107
- package/src/organisms/foundry/TypefaceStyleSection.jsx +0 -163
- package/src/organisms/foundry/VariableFontSection.jsx +0 -158
- package/src/organisms/foundry/glyphData.js +0 -30
- 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
|
-
}
|