@kolkrabbi/kol-foundry 0.1.0 → 0.3.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.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * TypeSample — a single labeled type-specimen block: an optional mono caption
3
+ * over one paragraph whose typography is driven entirely by props via inline
4
+ * style. The atomic unit of the type-specimen kit — stack several to show a
5
+ * scale, a weight range, or a family; adjacent samples get a hairline
6
+ * separator (`.kol-type-sample + .kol-type-sample`).
7
+ *
8
+ * Presentational — arbitrary px values are the point, so props map to inline
9
+ * style rather than fixed kol-* size stops. Label and children render
10
+ * verbatim as authored.
11
+ *
12
+ * @param {string} family font family name (wrapped as `"family", sans-serif`); defaults to the KOL sans token
13
+ * @param {number} weight font-weight
14
+ * @param {boolean} italic italic sample
15
+ * @param {number} size font-size in px
16
+ * @param {number} lineHeight line-height in px; unitless 1.2 when unset
17
+ * @param {string} label mono caption above the sample (omit to hide)
18
+ * @param {ReactNode} children the specimen text itself
19
+ */
20
+ export default function TypeSample({
21
+ family,
22
+ weight = 400,
23
+ italic = false,
24
+ size = 32,
25
+ lineHeight,
26
+ label,
27
+ children,
28
+ }) {
29
+ return (
30
+ <div className="kol-type-sample py-6">
31
+ {label && (
32
+ <p className="kol-helper-12 tracking-wider text-meta m-0 mb-3">
33
+ {label}
34
+ </p>
35
+ )}
36
+ <p
37
+ className="kol-type-sample-body m-0 text-auto"
38
+ style={{
39
+ fontFamily: family ? `"${family}", sans-serif` : 'var(--kol-font-family-sans)',
40
+ fontWeight: weight,
41
+ fontStyle: italic ? 'italic' : 'normal',
42
+ fontSize: `${size}px`,
43
+ lineHeight: lineHeight ? `${lineHeight}px` : '1.2',
44
+ }}
45
+ >
46
+ {children}
47
+ </p>
48
+ </div>
49
+ )
50
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * TypeSpecCard — a two-column type-spec row: a left meta panel of key/value
3
+ * pairs (font metrics) beside a live sample slot on the right, with an
4
+ * optional corner label. The "data-sheet" member of the type-specimen kit —
5
+ * pairs the numeric spec of a type style with a rendered example of it
6
+ * (often a TypeSample passed as children, composed at the call site).
7
+ *
8
+ * Single column on mobile; `240px + fluid` two-column at lg. `meta` is
9
+ * arbitrary tuples — no fixed metric schema. Sample paragraphs get 16px
10
+ * rhythm via `.kol-type-spec-sample p` (Preflight zeroes <p> margins).
11
+ *
12
+ * @param {string} label corner caption, absolute top-left (omit to hide)
13
+ * @param {Array<[string, ReactNode]>} meta key/value rows in the left panel
14
+ * @param {ReactNode} children the live type sample on the right
15
+ */
16
+ export default function TypeSpecCard({ label, meta = [], children }) {
17
+ return (
18
+ <div className="kol-type-spec relative py-12 border-t border-fg-08">
19
+ {label && (
20
+ <span className="kol-type-spec-label kol-helper-12 tracking-widest text-meta absolute top-4 left-0">
21
+ {label}
22
+ </span>
23
+ )}
24
+ <div className="kol-type-spec-row grid grid-cols-1 gap-6 lg:grid-cols-[240px_minmax(0,1fr)] lg:gap-12 items-start pt-6">
25
+ <div className="kol-type-spec-meta flex flex-col">
26
+ {meta.map(([key, value]) => (
27
+ <div
28
+ key={key}
29
+ className="kol-type-spec-meta-row grid grid-cols-[auto_minmax(0,1fr)] gap-4 items-baseline py-2 border-b border-[var(--kol-fg-04)] last:border-b-0"
30
+ >
31
+ <span className="kol-helper-10 text-meta">{key}</span>
32
+ <span className="kol-helper-10 text-strong text-right [overflow-wrap:anywhere]">{value}</span>
33
+ </div>
34
+ ))}
35
+ </div>
36
+ <div className="kol-type-spec-sample min-w-0">
37
+ {children}
38
+ </div>
39
+ </div>
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,75 @@
1
+ import { ContentFilters } from '@kolkrabbi/kol-component'
2
+
3
+ /**
4
+ * TypefaceLibraryGrid — filtered typeface library grid. Thin wrapper around the
5
+ * DS `ContentFilters` configured for typefaces (Classification + Styles filter
6
+ * groups, card/list view toggle). The parent supplies `renderItem` so it can
7
+ * wrap each entry in its own navigation.
8
+ *
9
+ * @param {Object} props
10
+ * @param {Array} props.typefaces - Typeface objects.
11
+ * @param {Function} props.renderItem - (typeface, viewMode) => node, wraps each item.
12
+ * @param {number} props.totalCount - Total count of all typefaces.
13
+ */
14
+ const TypefaceLibraryGrid = ({
15
+ typefaces,
16
+ renderItem,
17
+ totalCount
18
+ }) => {
19
+ // Extract unique values for filter groups
20
+ const classifications = [...new Set(typefaces.map((t) => t.classification))].sort()
21
+ const styles = [...new Set(typefaces.map((t) => t.styles))].sort()
22
+
23
+ const filterGroups = [
24
+ {
25
+ label: 'Classification',
26
+ key: 'classification',
27
+ values: classifications
28
+ },
29
+ {
30
+ label: 'Styles',
31
+ key: 'styles',
32
+ values: styles
33
+ }
34
+ ]
35
+
36
+ const viewModeOptions = [
37
+ { value: 'card', label: 'Card view' },
38
+ { value: 'list', label: 'List view' }
39
+ ]
40
+
41
+ // Render items in grid or list based on view mode
42
+ const renderItemsWithLayout = (items, viewMode) => {
43
+ if (viewMode === 'card') {
44
+ return (
45
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
46
+ {items.map((typeface) => renderItem(typeface, 'card'))}
47
+ </div>
48
+ )
49
+ }
50
+
51
+ return (
52
+ <div className="space-y-3">
53
+ {items.map((typeface) => renderItem(typeface, 'list'))}
54
+ </div>
55
+ )
56
+ }
57
+
58
+ return (
59
+ <section className="w-full py-16">
60
+ <div className="max-w-[1400px] mx-auto">
61
+ <ContentFilters
62
+ items={typefaces}
63
+ title="All Typefaces"
64
+ totalCount={totalCount}
65
+ filterGroups={filterGroups}
66
+ renderItem={(items, viewMode) => renderItemsWithLayout(items, viewMode)}
67
+ viewModeOptions={viewModeOptions}
68
+ defaultViewMode="list"
69
+ />
70
+ </div>
71
+ </section>
72
+ )
73
+ }
74
+
75
+ export default TypefaceLibraryGrid
@@ -0,0 +1,236 @@
1
+ import { useState, useMemo, useEffect, useRef } from 'react'
2
+ import { ContentFilters } from '@kolkrabbi/kol-component'
3
+ import TypefaceLibraryItem from './TypefaceLibraryItem.jsx'
4
+ import TypefaceVariablePreview from './TypefaceVariablePreview.jsx'
5
+
6
+ /**
7
+ * TypefaceLibraryGridWithVariables — the library grid with a "By Typeface"
8
+ * filter mode. Default mode shows standard TypefaceLibraryItem cards; selecting
9
+ * a single typeface swaps to TypefaceVariablePreview per weight variant, with an
10
+ * optional Axes filter for multi-axis families.
11
+ *
12
+ * Router-severed: pass an injected `linkComponent` (e.g. your router's `Link`)
13
+ * to wrap each item — it receives a `to` prop. When omitted, items render as a
14
+ * plain `<a href>`. Report: replaced the monorepo's `react-router-dom` `Link`.
15
+ *
16
+ * @param {Object} props
17
+ * @param {Array} props.typefaces - Typeface objects.
18
+ * @param {Object} props.typefaceWeights - Map of typeface name → weight variant array.
19
+ * @param {number} props.totalCount - Total count of all typefaces.
20
+ * @param {React.ElementType} props.linkComponent - Optional link wrapper (receives `to`); defaults to `<a href>`.
21
+ */
22
+ const TypefaceLibraryGridWithVariables = ({
23
+ typefaces,
24
+ typefaceWeights = {},
25
+ totalCount,
26
+ linkComponent
27
+ }) => {
28
+ const [activeFilters, setActiveFilters] = useState(new Set())
29
+ const [viewMode, setViewMode] = useState('list')
30
+ const [activeIndex, setActiveIndex] = useState(null)
31
+ const prevModeRef = useRef(null)
32
+
33
+ const LinkEl = linkComponent || 'a'
34
+ const linkPropsFor = (dest) => (linkComponent ? { to: dest } : { href: dest })
35
+
36
+ // Reset active index when filters change
37
+ useEffect(() => {
38
+ setActiveIndex(null)
39
+ }, [activeFilters])
40
+
41
+ // Handle filter changes with mutual exclusivity for typeface selection
42
+ const handleFilterChange = (newFilters) => {
43
+ // Check if a typeface (name) filter was added
44
+ const newTypefaceFilter = Array.from(newFilters).find(f => f.startsWith('name:'))
45
+ const oldTypefaceFilter = Array.from(activeFilters).find(f => f.startsWith('name:'))
46
+
47
+ // If a new typeface is selected and it's different from the old one,
48
+ // remove the old typeface filter to enforce mutual exclusivity
49
+ if (newTypefaceFilter && oldTypefaceFilter && newTypefaceFilter !== oldTypefaceFilter) {
50
+ const updatedFilters = new Set(newFilters)
51
+ updatedFilters.delete(oldTypefaceFilter)
52
+ setActiveFilters(updatedFilters)
53
+ } else {
54
+ setActiveFilters(newFilters)
55
+ }
56
+ }
57
+
58
+ // Extract unique values for filter groups
59
+ const classifications = [...new Set(typefaces.map((t) => t.classification))].sort()
60
+ const styles = [...new Set(typefaces.map((t) => t.styles))].sort()
61
+ const typefaceNames = typefaces.map((t) => t.name).sort()
62
+
63
+ // Check if a specific typeface is selected
64
+ const selectedTypeface = useMemo(() => {
65
+ const typefaceFilter = Array.from(activeFilters).find(f => f.startsWith('name:'))
66
+ return typefaceFilter ? typefaceFilter.split(':')[1] : null
67
+ }, [activeFilters])
68
+
69
+ // Get available axes for selected typeface
70
+ const availableAxes = useMemo(() => {
71
+ if (!selectedTypeface || !typefaceWeights[selectedTypeface]) return []
72
+ const weights = typefaceWeights[selectedTypeface]
73
+ const axes = [...new Set(weights.map(w => w.axis).filter(Boolean))]
74
+ return axes
75
+ }, [selectedTypeface, typefaceWeights])
76
+
77
+ // Filter groups - dynamically add Axes filter if multi-axis typeface selected
78
+ const filterGroups = useMemo(() => {
79
+ const baseGroups = [
80
+ { label: 'Classification', key: 'classification', values: classifications },
81
+ { label: 'Styles', key: 'styles', values: styles },
82
+ { label: 'Typefaces', key: 'name', values: typefaceNames }
83
+ ]
84
+
85
+ // Add Axes filter if selected typeface has multiple axes
86
+ if (selectedTypeface && availableAxes.length > 1) {
87
+ baseGroups.push({
88
+ label: 'Axes',
89
+ key: 'axis',
90
+ values: availableAxes
91
+ })
92
+ }
93
+
94
+ return baseGroups
95
+ }, [classifications, styles, typefaceNames, selectedTypeface, availableAxes])
96
+
97
+ // Filtered items
98
+ const filteredItems = useMemo(() => {
99
+ if (activeFilters.size === 0) return typefaces
100
+
101
+ return typefaces.filter((typeface) => {
102
+ let matches = true
103
+ activeFilters.forEach((filter) => {
104
+ const [filterType, value] = filter.split(':')
105
+ if (filterType === 'typeface' && typeface.name !== value) {
106
+ matches = false
107
+ }
108
+ if (filterType === 'classification' && typeface.classification !== value) {
109
+ matches = false
110
+ }
111
+ if (filterType === 'styles' && typeface.styles !== value) {
112
+ matches = false
113
+ }
114
+ })
115
+ return matches
116
+ })
117
+ }, [typefaces, activeFilters])
118
+
119
+ // View mode options
120
+ const viewModeOptions = [
121
+ { value: 'card', label: 'Card view' },
122
+ { value: 'list', label: 'List view' }
123
+ ]
124
+
125
+ // Render items based on filter mode
126
+ const renderItems = (items, mode) => {
127
+ // Reset active index when view mode changes
128
+ if (prevModeRef.current !== null && prevModeRef.current !== mode) {
129
+ setActiveIndex(null)
130
+ }
131
+ prevModeRef.current = mode
132
+
133
+ // If a specific typeface is selected, show weight variants
134
+ if (selectedTypeface && typefaceWeights[selectedTypeface]) {
135
+ const typeface = items.find(t => t.name === selectedTypeface)
136
+ if (!typeface) return null
137
+
138
+ let weights = typefaceWeights[selectedTypeface]
139
+
140
+ // Filter by selected axes if any axis filters are active
141
+ const selectedAxes = Array.from(activeFilters)
142
+ .filter(f => f.startsWith('axis:'))
143
+ .map(f => f.split(':')[1])
144
+
145
+ if (selectedAxes.length > 0) {
146
+ weights = weights.filter(w => selectedAxes.includes(w.axis))
147
+ }
148
+
149
+ if (mode === 'card') {
150
+ return (
151
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
152
+ {weights.map((w) => (
153
+ <TypefaceVariablePreview
154
+ key={`${typeface.name}-${w.weight}`}
155
+ typeface={typeface}
156
+ weight={w.weight}
157
+ weightValue={w.value}
158
+ variant="card"
159
+ />
160
+ ))}
161
+ </div>
162
+ )
163
+ }
164
+
165
+ // List view
166
+ return (
167
+ <div className="space-y-6">
168
+ {weights.map((w) => (
169
+ <TypefaceVariablePreview
170
+ key={`${typeface.name}-${w.weight}`}
171
+ typeface={typeface}
172
+ weight={w.weight}
173
+ weightValue={w.value}
174
+ variant="list"
175
+ />
176
+ ))}
177
+ </div>
178
+ )
179
+ }
180
+
181
+ // Default mode: show standard typeface cards
182
+ if (mode === 'card') {
183
+ return (
184
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
185
+ {items.map((typeface, index) => (
186
+ <LinkEl key={typeface.link} {...linkPropsFor(typeface.link)}>
187
+ <TypefaceLibraryItem
188
+ typeface={typeface}
189
+ variant="card"
190
+ isActive={activeIndex === index}
191
+ onMouseEnter={() => setActiveIndex(index)}
192
+ />
193
+ </LinkEl>
194
+ ))}
195
+ </div>
196
+ )
197
+ }
198
+
199
+ // List view
200
+ return (
201
+ <div className="space-y-6">
202
+ {items.map((typeface, index) => (
203
+ <LinkEl key={typeface.link} {...linkPropsFor(typeface.link)}>
204
+ <TypefaceLibraryItem
205
+ typeface={typeface}
206
+ variant="list"
207
+ isActive={activeIndex === index}
208
+ onMouseEnter={() => setActiveIndex(index)}
209
+ />
210
+ </LinkEl>
211
+ ))}
212
+ </div>
213
+ )
214
+ }
215
+
216
+ return (
217
+ <section className="w-full py-16">
218
+ <div className="max-w-[1400px] mx-auto">
219
+ <ContentFilters
220
+ items={typefaces}
221
+ title="All Typefaces (With Variable Preview)"
222
+ totalCount={totalCount}
223
+ filterGroups={filterGroups}
224
+ renderItem={(items, mode) => renderItems(items, mode)}
225
+ viewModeOptions={viewModeOptions}
226
+ defaultViewMode="list"
227
+ onFilterChange={handleFilterChange}
228
+ mutuallyExclusiveFilters={['name']}
229
+ customFilterKeys={['axis']}
230
+ />
231
+ </div>
232
+ </section>
233
+ )
234
+ }
235
+
236
+ export default TypefaceLibraryGridWithVariables
@@ -0,0 +1,190 @@
1
+ import { useState, useRef, useLayoutEffect } from 'react'
2
+
3
+ /**
4
+ * TypefaceLibraryItem — a single typeface entry in the library, in either card
5
+ * or list layout. Card: vertical layout with a large "Ðð" preview that swaps to
6
+ * a pangram on hover. List: horizontal layout with a width-clipped alphabet
7
+ * preview (binary-search clipping via ResizeObserver, mirroring DisplaySpecimen).
8
+ *
9
+ * Meant to be wrapped by the parent for navigation (see TypefaceLibraryGrid*).
10
+ *
11
+ * @param {Object} props
12
+ * @param {Object} props.typeface - Typeface data ({ name, styles, classification, year, ... }).
13
+ * @param {'card'|'list'} props.variant - Display variant (default 'card').
14
+ * @param {boolean} props.isActive - Whether this item is the last-hovered/active one.
15
+ * @param {Function} props.onMouseEnter - Callback fired on mouse enter.
16
+ */
17
+ const TypefaceLibraryItem = ({ typeface, variant = 'card', isActive = false, onMouseEnter }) => {
18
+ const containerRef = useRef(null)
19
+ const textRef = useRef(null)
20
+ const [visibleText, setVisibleText] = useState('Aa Bb Cc Dd Ee Ff Gg Hh Ii Jj Kk Ll Mm Nn Oo Pp Qq Rr Ss Tt Uu Vv Ww Xx Yy Zz')
21
+ // Map typeface name to font family
22
+ const fontFamily = typeface.name === 'TG Rót' ? 'TGRoot' :
23
+ typeface.name === 'TG Tröllatunga' ? 'TGTrollatunga' :
24
+ typeface.name === 'TG Dylgjur' ? 'TGDylgjur' :
25
+ typeface.name === 'TG Gullhamrar' ? 'TGGullhamrar' :
26
+ 'TGMalromur'
27
+
28
+ const fontStyle = typeface.name === 'TG Málrómur' ? 'italic' : 'normal'
29
+
30
+ const fullAlphabet = 'Aa Bb Cc Dd Ee Ff Gg Hh Ii Jj Kk Ll Mm Nn Oo Pp Qq Rr Ss Tt Uu Vv Ww Xx Yy Zz'
31
+
32
+ // TEXT CLIPPING LOGIC - similar to DisplaySpecimen but for horizontal width
33
+ useLayoutEffect(() => {
34
+ if (variant !== 'list') return
35
+
36
+ const container = containerRef.current
37
+ const textElement = textRef.current
38
+ if (!container || !textElement) return
39
+
40
+ const calculateClipping = () => {
41
+ if (container.clientWidth === 0) return
42
+ const availableWidth = container.clientWidth - 40
43
+ const chars = fullAlphabet.split('')
44
+ let clippedText = ''
45
+ let low = 0
46
+ let high = chars.length
47
+
48
+ while (low <= high) {
49
+ const mid = Math.floor((low + high) / 2)
50
+ const testText = chars.slice(0, mid).join('')
51
+ textElement.textContent = testText
52
+ if (textElement.scrollWidth <= availableWidth) {
53
+ clippedText = testText
54
+ low = mid + 1
55
+ } else {
56
+ high = mid - 1
57
+ }
58
+ }
59
+
60
+ setVisibleText(clippedText)
61
+ }
62
+
63
+ // Use ResizeObserver so it fires when container actually has dimensions
64
+ const ro = new ResizeObserver(calculateClipping)
65
+ ro.observe(container)
66
+
67
+ window.addEventListener('resize', calculateClipping)
68
+
69
+ return () => {
70
+ ro.disconnect()
71
+ window.removeEventListener('resize', calculateClipping)
72
+ }
73
+ }, [variant, fontFamily, fontStyle])
74
+
75
+ // Card variant
76
+ if (variant === 'card') {
77
+ return (
78
+ <div
79
+ className={`group bg-surface-primary hover:bg-surface-inverse rounded transition-all duration-300 h-[500px] relative border border-fg-08 cursor-pointer ${
80
+ isActive ? 'bg-surface-inverse' : ''
81
+ }`}
82
+ onMouseEnter={onMouseEnter}
83
+ >
84
+ {/* Details at Top */}
85
+ <div className={`p-6 space-y-2 group-hover:opacity-0 transition-opacity duration-300 relative z-10 ${isActive ? 'opacity-0' : ''}`}>
86
+ <div className="flex items-center gap-2 mb-1 flex-wrap">
87
+ <h3 className={`kol-helper-lg group-hover:text-auto-inverse transition-colors ${isActive ? 'text-auto-inverse' : 'text-auto'}`}>
88
+ {typeface.name}
89
+ </h3>
90
+ </div>
91
+ <p className={`kol-helper-s group-hover:text-fg-inverse-64 transition-colors ${isActive ? 'text-fg-inverse-64' : 'text-fg-64'}`}>
92
+ {typeface.styles}
93
+ </p>
94
+ </div>
95
+
96
+ {/* Preview Area */}
97
+ <div className="absolute bottom-0 left-0 right-0 top-0 flex items-end justify-start p-8">
98
+ {/* Default: Ðð */}
99
+ <div
100
+ className={`text-[140px] lg:text-[160px] leading-none group-hover:text-auto-inverse group-hover:opacity-0 transition-all duration-300 relative z-10 ${
101
+ isActive ? 'text-auto-inverse opacity-0' : 'text-auto'
102
+ }`}
103
+ style={{
104
+ fontFamily,
105
+ fontStyle,
106
+ fontWeight: 400
107
+ }}
108
+ >
109
+ Ðð
110
+ </div>
111
+ </div>
112
+
113
+ {/* Hover: Sentence - Covers entire card */}
114
+ <div className={`absolute inset-0 flex items-center justify-center p-8 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none ${
115
+ isActive ? 'opacity-100' : 'opacity-0'
116
+ }`}>
117
+ <p
118
+ className="text-auto-inverse text-4xl lg:text-5xl leading-tight text-center"
119
+ style={{
120
+ fontFamily,
121
+ fontStyle,
122
+ fontWeight: 400
123
+ }}
124
+ >
125
+ The quick brown fox jumps over the lazy dog
126
+ </p>
127
+ </div>
128
+ </div>
129
+ )
130
+ }
131
+
132
+ // List variant - compact horizontal style matching TypefaceVariablePreview
133
+ return (
134
+ <div
135
+ ref={containerRef}
136
+ className={`self-stretch min-h-40 p-6 rounded flex flex-col justify-start items-start gap-6 mb-6 overflow-hidden cursor-pointer transition-all duration-300 ${
137
+ isActive
138
+ ? 'bg-[color-mix(in_srgb,var(--kol-surface-on-primary)_1%,transparent)] border border-[color-mix(in_srgb,var(--kol-surface-on-primary)_24%,transparent)]'
139
+ : 'bg-transparent border border-fg-08 hover:bg-[color-mix(in_srgb,var(--kol-surface-on-primary)_1%,transparent)] hover:border-[color-mix(in_srgb,var(--kol-surface-on-primary)_24%,transparent)]'
140
+ }`}
141
+ onMouseEnter={onMouseEnter}
142
+ >
143
+ {/* Header Row */}
144
+ <div className="self-stretch flex justify-between items-center">
145
+ {/* Left: Typeface Name & Info */}
146
+ <div className="w-64 flex justify-start items-start gap-6">
147
+ <div className="flex-1 flex flex-col justify-start items-start gap-3">
148
+ <div className="self-stretch flex flex-col justify-start items-start gap-2">
149
+ <div className="self-stretch kol-mono-sm uppercase">
150
+ {typeface.name}
151
+ </div>
152
+ <div className="self-stretch kol-mono-xs text-fg-64">
153
+ {typeface.styles}
154
+ </div>
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ {/* Right: Status/Year info */}
160
+ <div className="flex flex-col items-end gap-2">
161
+ <span className="kol-mono-sm">
162
+ {typeface.classification}
163
+ </span>
164
+ <span className="kol-mono-xs text-fg-64">
165
+ {typeface.year}
166
+ </span>
167
+ </div>
168
+ </div>
169
+
170
+ {/* Preview Text Row */}
171
+ <div className="self-stretch rounded flex justify-start items-center">
172
+ <div
173
+ ref={textRef}
174
+ className="flex-1 self-stretch justify-start text-auto font-black leading-[52px] whitespace-nowrap"
175
+ style={{
176
+ fontFamily,
177
+ fontStyle,
178
+ fontWeight: 400,
179
+ fontSize: '48px',
180
+ letterSpacing: '0'
181
+ }}
182
+ >
183
+ {visibleText}
184
+ </div>
185
+ </div>
186
+ </div>
187
+ )
188
+ }
189
+
190
+ export default TypefaceLibraryItem