@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,223 @@
1
+ import { Button } from '@kolkrabbi/kol-component'
2
+
3
+ import TypefaceStyleSection from './TypefaceStyleSection.jsx'
4
+ import FontPreviewSection from './FontPreviewSection.jsx'
5
+ import VariableFontSection from './VariableFontSection.jsx'
6
+ import GlyphMetricsSection from './GlyphMetricsSection.jsx'
7
+
8
+ /**
9
+ * TypefaceSpecimenPage — the full data-driven typeface specimen composition:
10
+ * hero → styles → preview → variable axes → glyph metrics, interleaved with
11
+ * editorial photos. Drives every section generically from one `typeface` config
12
+ * object (see the bundled `typefaceConfig` / `getTypefaceConfig`).
13
+ *
14
+ * SEVERED from the monorepo route (`TypefacePage`), which is why this is a pure
15
+ * composition, not a route:
16
+ * - No router: navigation is an injected `linkComponent` prop (receives `to`),
17
+ * defaulting to a plain `<a href>`.
18
+ * - No app theme control: the route's `applyTheme`/`getInitialTheme` effect and
19
+ * `toggleTheme` were dropped — theme is the consuming app's concern.
20
+ * - No app-shell hero: the route's `FullBleedHero` (consumer chrome) is now an
21
+ * injectable `HeroComponent` prop. A minimal built-in `DefaultHero` renders the
22
+ * background photo when none is injected; pass the real hero to restore it.
23
+ * - No SEO / data-fetch wrapper: data arrives via the `typeface` prop.
24
+ *
25
+ * Text casing: displayName, category, description and labels render verbatim.
26
+ *
27
+ * @param {Object} props
28
+ * @param {Object} props.typeface - Typeface config object (see typefaceConfig).
29
+ * @param {string} props.titleClassName - Optional hero title className (kept for API parity).
30
+ * @param {React.ElementType} props.linkComponent - Optional link wrapper (receives `to`); defaults to `<a href>`.
31
+ * @param {React.ElementType} props.HeroComponent - Optional hero slot; defaults to a minimal built-in hero.
32
+ */
33
+
34
+ /** Minimal built-in hero used when no HeroComponent is injected — background
35
+ * photo with the specimen intro overlaid. Ignores FullBleedHero-only props. */
36
+ const DefaultHero = ({ image, srcSet, alt, children }) => (
37
+ <div className="relative w-full min-h-[70vh] flex items-center justify-center overflow-hidden rounded-[2px] bg-surface-secondary">
38
+ {image && (
39
+ <img
40
+ src={image}
41
+ srcSet={srcSet || undefined}
42
+ alt={alt}
43
+ className="absolute inset-0 w-full h-full object-cover"
44
+ loading="eager"
45
+ />
46
+ )}
47
+ <div className="relative z-10">{children}</div>
48
+ </div>
49
+ )
50
+
51
+ const TypefaceSpecimenPage = ({
52
+ typeface,
53
+ titleClassName = 'text-8xl',
54
+ linkComponent,
55
+ HeroComponent,
56
+ }) => {
57
+ const LinkEl = linkComponent || 'a'
58
+ const linkPropsFor = (dest) => (linkComponent ? { to: dest } : { href: dest })
59
+ const Hero = HeroComponent || DefaultHero
60
+
61
+ const {
62
+ displayName,
63
+ fontFamily,
64
+ fontUrl,
65
+ fontUrlRoman,
66
+ fontUrlItalic,
67
+ fontStyle,
68
+ badgeText,
69
+ category,
70
+ description,
71
+ styles,
72
+ photos = []
73
+ } = typeface
74
+
75
+ // Determine if variable font sections should be shown
76
+ const showVariableSection = styles.hasWeight || styles.hasWidth
77
+
78
+ // Get photo at index (all typefaces have CDN photos configured)
79
+ const getPhoto = (index) => photos[index]
80
+
81
+ // Generate srcSet from a photo URL by replacing the size
82
+ // Pattern: {path}-{size}.jpg → {path}-400.jpg 400w, {path}-800.jpg 800w, etc.
83
+ // Capped at 1600 as not all images have 2560 available
84
+ const getSrcSet = (photoUrl) => {
85
+ if (!photoUrl || !photoUrl.includes('-1200.jpg')) return null
86
+ const basePath = photoUrl.replace('-1200.jpg', '')
87
+ return `${basePath}-400.jpg 400w, ${basePath}-800.jpg 800w, ${basePath}-1200.jpg 1200w, ${basePath}-1600.jpg 1600w`
88
+ }
89
+
90
+ return (
91
+ <div className="min-h-screen mb-16 bg-surface-primary">
92
+ <main className="w-full">
93
+ {/* Hero (injectable slot) */}
94
+ <div className="mt-14 md:mt-16">
95
+ <Hero
96
+ image={getPhoto(0)}
97
+ srcSet={getSrcSet(photos[0])}
98
+ alt={`${displayName} showcase`}
99
+ imageOpacity={100}
100
+ >
101
+ <div className="flex flex-col items-center text-center gap-4 md:gap-6 px-4 md:px-6 py-6 md:py-8 rounded-[2px]" style={{ backgroundColor: 'color-mix(in srgb, var(--kol-surface-primary) 80%, transparent)', backdropFilter: 'blur(1px)' }}>
102
+ <span
103
+ className={`${(displayName === 'Málrómur' || displayName === 'Tröllatunga') ? 'text-[48px] sm:text-[64px] md:text-[88px] lg:text-[120px]' : 'text-[56px] sm:text-[80px] md:text-[110px] lg:text-[144px]'} block text-auto leading-none ${fontStyle === 'italic' ? 'italic' : ''}`.trim()}
104
+ style={{ fontFamily, fontStyle: fontStyle || 'normal', fontWeight: 400 }}
105
+ >
106
+ {displayName}
107
+ </span>
108
+ <span className="kol-mono-xs text-fg-64">{category}</span>
109
+ <p className="kol-mono-xs text-auto max-w-[480px] md:max-w-[600px]">{description}</p>
110
+ <LinkEl {...linkPropsFor('/foundry/licensing')}>
111
+ <Button variant="primary" size="sm">Download Font</Button>
112
+ </LinkEl>
113
+ </div>
114
+ </Hero>
115
+ </div>
116
+
117
+ <div className="breakpoint-padding">
118
+ {/* Section 1: Styles */}
119
+ <TypefaceStyleSection typeface={typeface} />
120
+
121
+ {/* Image Section 2 */}
122
+ <section className="w-full overflow-hidden py-16">
123
+ <div className="max-w-[1400px] mx-auto aspect-[2/1]">
124
+ <div className="w-full h-full bg-surface-secondary rounded border border-fg-08">
125
+ <img
126
+ src={getPhoto(1)}
127
+ srcSet={getSrcSet(photos[1])}
128
+ sizes="(max-width: 1400px) 100vw, 1400px"
129
+ alt={`${displayName} showcase`}
130
+ className="w-full h-full object-cover rounded-[4px]"
131
+ loading="lazy"
132
+ />
133
+ </div>
134
+ </div>
135
+ </section>
136
+
137
+ {/* Section 2: Font Preview */}
138
+ <FontPreviewSection
139
+ fontFamily={fontFamily}
140
+ badgeText={badgeText}
141
+ showDropdown={styles.hasItalic}
142
+ availableWeights={(styles.weights || []).map(w => w.label)}
143
+ initialWeight={(styles.weights || [])[0]?.label || 'Regular'}
144
+ />
145
+
146
+ {/* Image Section 3 */}
147
+ <section className="w-full overflow-hidden py-16">
148
+ <div className="max-w-[1400px] mx-auto aspect-[2/1]">
149
+ <div className="w-full h-full bg-surface-secondary rounded border border-fg-08">
150
+ <img
151
+ src={getPhoto(2)}
152
+ srcSet={getSrcSet(photos[2])}
153
+ sizes="(max-width: 1400px) 100vw, 1400px"
154
+ alt={`${displayName} showcase`}
155
+ className="w-full h-full object-cover rounded-[4px]"
156
+ loading="lazy"
157
+ />
158
+ </div>
159
+ </div>
160
+ </section>
161
+
162
+ {/* Section 3: Variable Font (only for variable fonts) */}
163
+ {showVariableSection && (
164
+ <VariableFontSection
165
+ fontFamily={fontFamily}
166
+ badgeText={badgeText}
167
+ showDropdown={styles.hasItalic}
168
+ />
169
+ )}
170
+
171
+ {/* Section 4: Glyph Metrics Grid */}
172
+ <GlyphMetricsSection
173
+ fontUrlRoman={fontUrlRoman || fontUrl}
174
+ fontUrlItalic={fontUrlItalic || fontUrl}
175
+ fontFamily={fontFamily}
176
+ fontStyle={fontStyle}
177
+ badgeText={badgeText}
178
+ showDropdown={styles.hasItalic}
179
+ hasWeight={styles.hasWeight}
180
+ hasWidth={styles.hasWidth}
181
+ weights={styles.weights || []}
182
+ widths={styles.widths || []}
183
+ />
184
+
185
+ {/* Image Section 4 */}
186
+ <section className="w-full mt-12 py-16 overflow-hidden">
187
+ <div className="max-w-[1400px] mx-auto aspect-[2/1]">
188
+ <div className="w-full h-full bg-surface-secondary rounded border border-fg-08">
189
+ <img
190
+ src={getPhoto(3)}
191
+ srcSet={getSrcSet(photos[3])}
192
+ sizes="(max-width: 1400px) 100vw, 1400px"
193
+ alt={`${displayName} showcase`}
194
+ className="w-full h-full object-cover rounded-[4px]"
195
+ loading="lazy"
196
+ />
197
+ </div>
198
+ </div>
199
+ </section>
200
+
201
+ {/* Image Section 5 */}
202
+ <section className="w-full py-16 overflow-hidden">
203
+ <div className="max-w-[1400px] mx-auto aspect-[2/1]">
204
+ <div className="w-full h-full bg-surface-secondary rounded border border-fg-08">
205
+ <img
206
+ src={getPhoto(4)}
207
+ srcSet={getSrcSet(photos[4])}
208
+ sizes="(max-width: 1400px) 100vw, 1400px"
209
+ alt={`${displayName} showcase`}
210
+ className="w-full h-full object-cover rounded-[4px]"
211
+ loading="lazy"
212
+ />
213
+ </div>
214
+ </div>
215
+ </section>
216
+
217
+ </div>
218
+ </main>
219
+ </div>
220
+ )
221
+ }
222
+
223
+ export default TypefaceSpecimenPage
@@ -0,0 +1,160 @@
1
+ import { useState } from 'react'
2
+ import { Slider } from '@kolkrabbi/kol-component'
3
+
4
+ /**
5
+ * TypefaceVariablePreview — interactive preview for a single typeface weight.
6
+ * List view: editable specimen text with Size / Leading / Spacing sliders. Card
7
+ * view: static metrics readout. Used in the "By Typeface" library filter to
8
+ * compare weights side by side.
9
+ *
10
+ * @param {Object} props
11
+ * @param {Object} props.typeface - Typeface data ({ name, styles, classification, status, year }).
12
+ * @param {string} props.weight - Weight label (e.g. 'Medium', 'Bold').
13
+ * @param {number} props.weightValue - Numeric weight value (200–900).
14
+ * @param {'list'|'card'} props.variant - Display variant (default 'list').
15
+ */
16
+ const TypefaceVariablePreview = ({
17
+ typeface,
18
+ weight = 'Medium',
19
+ weightValue = 400,
20
+ variant = 'list'
21
+ }) => {
22
+ // Font family mapping
23
+ const fontFamilyMap = {
24
+ 'TG Málrómur': 'TGMalromur',
25
+ 'TG Rót': 'TGRoot',
26
+ 'TG Gullhamrar': 'TGGullhamrar',
27
+ 'TG Tröllatunga': 'TGTrollatunga',
28
+ 'TG Dylgjur': 'TGDylgjur'
29
+ }
30
+
31
+ const fontFamily = fontFamilyMap[typeface.name] || 'TGMalromur'
32
+ const isItalic = typeface.name === 'TG Málrómur' || typeface.name === 'TG Dylgjur'
33
+
34
+ // State for interactive controls
35
+ const [size, setSize] = useState(64)
36
+ const [leading, setLeading] = useState(10) // 0-50 range (maps to 90-140%)
37
+ const [spacing, setSpacing] = useState(0)
38
+ const [previewText, setPreviewText] = useState('Rennimjúkt eðal flauel, duft slæðist, silkislaufa')
39
+
40
+ // Card View - Static information display
41
+ if (variant === 'card') {
42
+ return (
43
+ <div className="bg-surface-primary border border-fg-08 rounded-sm p-6 h-full">
44
+ <div className="space-y-4">
45
+ {/* Header */}
46
+ <div>
47
+ <h3 className="kol-heading-sm mb-1">{typeface.name} — {weight}</h3>
48
+ <p className="kol-mono-xs text-fg-64">{typeface.styles}</p>
49
+ </div>
50
+
51
+ {/* Metrics Grid */}
52
+ <div className="grid grid-cols-2 gap-3 pt-4">
53
+ <div>
54
+ <span className="kol-mono-xs text-fg-64">Classification</span>
55
+ <p className="kol-mono-sm text-auto">{typeface.classification}</p>
56
+ </div>
57
+ <div>
58
+ <span className="kol-mono-xs text-fg-64">Weight</span>
59
+ <p className="kol-mono-sm text-auto">{weight} ({weightValue})</p>
60
+ </div>
61
+ <div>
62
+ <span className="kol-mono-xs text-fg-64">Status</span>
63
+ <p className="kol-mono-sm text-auto">{typeface.status}</p>
64
+ </div>
65
+ <div>
66
+ <span className="kol-mono-xs text-fg-64">Year</span>
67
+ <p className="kol-mono-sm text-auto">{typeface.year}</p>
68
+ </div>
69
+ </div>
70
+
71
+ {/* Typography Metrics - Placeholder for future implementation */}
72
+ <div className="pt-4 border-t border-fg-08">
73
+ <h4 className="kol-mono-xs text-fg-64 mb-2">Typography Metrics</h4>
74
+ <div className="grid grid-cols-2 gap-2 kol-mono-xs text-fg-64">
75
+ <div>Baseline: —</div>
76
+ <div>x-height: —</div>
77
+ <div>Cap height: —</div>
78
+ <div>Ascender: —</div>
79
+ <div>Descender: —</div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ )
85
+ }
86
+
87
+ // List View - Interactive preview with controls
88
+ return (
89
+ <div className="self-stretch min-h-40 p-6 rounded border border-fg-16 flex flex-col justify-start items-start gap-6 overflow-hidden bg-surface-primary">
90
+ {/* Header Row with Controls */}
91
+ <div className="self-stretch flex justify-between items-center">
92
+ {/* Left: Typeface Name & Info */}
93
+ <div className="w-64 flex justify-start items-start gap-6">
94
+ <div className="flex-1 flex flex-col justify-start items-start gap-3">
95
+ <div className="self-stretch flex flex-col justify-start items-start gap-2">
96
+ <div className="self-stretch kol-mono-sm uppercase">
97
+ {typeface.name} — {weight}
98
+ </div>
99
+ <div className="self-stretch kol-mono-xs text-fg-64">
100
+ {typeface.styles}
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ {/* Right: Three Sliders Horizontal */}
107
+ <div className="w-[960px] flex justify-start items-start gap-8">
108
+ <Slider
109
+ label="Size"
110
+ min={12}
111
+ max={200}
112
+ value={size}
113
+ onChange={setSize}
114
+ variant="minimal"
115
+ className="flex-1"
116
+ />
117
+ <Slider
118
+ label="Leading"
119
+ min={0}
120
+ max={50}
121
+ value={leading}
122
+ onChange={setLeading}
123
+ variant="minimal"
124
+ className="flex-1"
125
+ formatValue={(val) => 90 + val}
126
+ />
127
+ <Slider
128
+ label="Spacing"
129
+ min={-50}
130
+ max={50}
131
+ value={spacing}
132
+ onChange={setSpacing}
133
+ variant="minimal"
134
+ className="flex-1"
135
+ />
136
+ </div>
137
+ </div>
138
+
139
+ {/* Preview Text Row */}
140
+ <div className="self-stretch rounded flex justify-start items-center">
141
+ <input
142
+ type="text"
143
+ value={previewText}
144
+ onChange={(e) => setPreviewText(e.target.value)}
145
+ className="flex-1 self-stretch justify-start bg-transparent text-auto font-black leading-[52px] outline-none border-none placeholder:text-auto"
146
+ style={{
147
+ fontFamily: fontFamily,
148
+ fontStyle: isItalic ? 'italic' : 'normal',
149
+ fontWeight: weightValue,
150
+ fontSize: `${size}px`,
151
+ lineHeight: `${90 + leading}%`,
152
+ letterSpacing: `${spacing}px`
153
+ }}
154
+ />
155
+ </div>
156
+ </div>
157
+ )
158
+ }
159
+
160
+ export default TypefaceVariablePreview
package/src/index.js CHANGED
@@ -1,21 +1,38 @@
1
1
  /**
2
- * @kol/component foundry sub-barrel — the type-specimen layer (the DS answer to
3
- * the monorepo's `@kol/ui/foundry`). Exposed as the `./foundry` package subpath
4
- * so the showcase specimen set can import these without touching the root
5
- * src/index.js barrel. The root barrel export lines are documented in the p9
6
- * wiring notes for a central merge.
2
+ * @kolkrabbi/kol-foundry — the type-specimen apparatus. Every export renders,
3
+ * inspects, or manipulates a live font (see COMPONENTS.md for the membership
4
+ * test). Data is consumer-injected; shared primitives stay in @kolkrabbi/kol-component.
7
5
  */
8
6
 
9
- // molecule (shared section header)
7
+ // section scaffold
10
8
  export { default as SpecimenSectionHeader } from './SpecimenSectionHeader.jsx'
11
9
 
12
- // organisms (specimen sections)
10
+ // specimen tools — one typeface, inspected
13
11
  export { default as TypefaceHero } from './TypefaceHero.jsx'
14
- export { default as VariableFontSection } from './VariableFontSection.jsx'
15
- export { default as GlyphMetricsGrid } from './GlyphMetricsGrid.jsx'
16
12
  export { default as TypefaceStyleSection } from './TypefaceStyleSection.jsx'
17
13
  export { default as FontPreviewSection } from './FontPreviewSection.jsx'
14
+ export { default as VariableFontSection } from './VariableFontSection.jsx'
15
+ export { default as GlyphMetricsGrid } from './GlyphMetricsGrid.jsx'
16
+ export { default as GlyphMetricsSection } from './GlyphMetricsSection.jsx'
18
17
  export { default as FoundryCharacterSets } from './FoundryCharacterSets.jsx'
19
18
 
19
+ // catalog — the specimen collection
20
+ export { default as TypefaceLibraryGrid } from './TypefaceLibraryGrid.jsx'
21
+ export { default as TypefaceLibraryGridWithVariables } from './TypefaceLibraryGridWithVariables.jsx'
22
+ export { default as TypefaceLibraryItem } from './TypefaceLibraryItem.jsx'
23
+ export { default as TypefaceVariablePreview } from './TypefaceVariablePreview.jsx'
24
+
25
+ // type-specimen kit — prop-driven specimen blocks (moved from @kolkrabbi/kol-component 2026-07-09)
26
+ export { default as TypeSample } from './TypeSample.jsx'
27
+ export { default as TypeSpecCard } from './TypeSpecCard.jsx'
28
+
29
+ // live-font effects — variable-font axes, animated
30
+ export { default as TextPressure } from './TextPressure.jsx'
31
+ export { default as ColorLoader } from './ColorLoader.jsx'
32
+
33
+ // reference composition (severed page — data via props, no router/SEO/data-fetch)
34
+ export { default as TypefaceSpecimenPage } from './TypefaceSpecimenPage.jsx'
35
+
20
36
  // data
21
37
  export { glyphSets, glyphCategories, SPECIMEN_SAMPLE_TEXT } from './glyphData.js'
38
+ export { typefaceConfig, getTypefaceConfig, getAllTypefaceIds, getAllTypefaces } from './typefaceConfig.js'
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Typeface Configuration — bundled default fixture for the foundry specimen
3
+ * layer. Ported verbatim from the monorepo `apps/web` foundry data so the DS
4
+ * package carries a ready-to-render default set (Málrómur, Rót, Dylgjur,
5
+ * Gullhamrar, Tröllatunga). Consumers can ignore this and inject their own
6
+ * typeface objects via props — nothing here is load-bearing beyond a default.
7
+ *
8
+ * Font URLs are relative (`/fonts/…`); a consumer serves the actual files.
9
+ */
10
+
11
+ const cdnBase = 'https://f005.backblazeb2.com/file/kolkrabbi/website/asset-library/foundry'
12
+
13
+ export const typefaceConfig = {
14
+ malromur: {
15
+ id: 'malromur',
16
+ name: 'Málrómur',
17
+ displayName: 'Málrómur',
18
+ fontFamily: 'TGMalromur',
19
+ fontUrl: '/fonts/TGMalromurItalicVF.ttf',
20
+ fontUrlRoman: '/fonts/TGMalromurRomanVF.ttf',
21
+ fontUrlItalic: '/fonts/TGMalromurItalicVF.ttf',
22
+ fontStyle: 'italic',
23
+ category: 'Variable Font',
24
+ description: 'A contemporary italic variable font for editorial design',
25
+ badgeText: 'Málrómur',
26
+ specimenLink: '/foundry/specimen/malromur',
27
+
28
+ photos: [
29
+ `${cdnBase}/foundry-typefaces/01-malromur/typefaces-malromur/01-typefaces-hero/typefaces-hero-1200.jpg`,
30
+ `${cdnBase}/foundry-typefaces/01-malromur/typefaces-malromur/02-typefaces-image/typefaces-image-1200.jpg`,
31
+ `${cdnBase}/foundry-typefaces/01-malromur/typefaces-malromur/03-typefaces-image/typefaces-image-1200.jpg`,
32
+ `${cdnBase}/foundry-typefaces/01-malromur/typefaces-malromur/04-typefaces-image/typefaces-image-1200.jpg`,
33
+ `${cdnBase}/foundry-typefaces/01-malromur/typefaces-malromur/05-typefaces-image/typefaces-image-1200.jpg`
34
+ ],
35
+
36
+ // Style section config
37
+ styles: {
38
+ hasWeight: true,
39
+ hasWidth: false,
40
+ hasItalic: true,
41
+ defaultStyle: 'italic',
42
+ weights: [
43
+ { label: 'Thin', weight: 100 },
44
+ { label: 'Extralight', weight: 200 },
45
+ { label: 'Light', weight: 300 },
46
+ { label: 'Regular', weight: 400 },
47
+ { label: 'Medium', weight: 500 },
48
+ { label: 'Semibold', weight: 600 },
49
+ { label: 'Bold', weight: 700 },
50
+ { label: 'Extrabold', weight: 800 }
51
+ ]
52
+ }
53
+ },
54
+
55
+ rot: {
56
+ id: 'rot',
57
+ name: 'Rót',
58
+ displayName: 'Rót',
59
+ fontFamily: 'TGRoot',
60
+ fontUrl: '/fonts/TGRotVF.ttf',
61
+ fontStyle: 'normal',
62
+ category: 'Variable Font',
63
+ description: 'A precise geometric sans serif with variable weight and width axes',
64
+ badgeText: 'Rót Aa',
65
+ specimenLink: '/foundry/specimen/rot',
66
+
67
+ photos: [
68
+ `${cdnBase}/foundry-typefaces/02-raetur/typefaces-raetur/01-typefaces-hero/typefaces-hero-1200.jpg`,
69
+ `${cdnBase}/foundry-typefaces/02-raetur/typefaces-raetur/02-typefaces-image/typefaces-image-1200.jpg`,
70
+ `${cdnBase}/foundry-typefaces/02-raetur/typefaces-raetur/03-typefaces-image/typefaces-image-1200.jpg`,
71
+ `${cdnBase}/foundry-typefaces/02-raetur/typefaces-raetur/04-typefaces-image/typefaces-image-1200.jpg`,
72
+ `${cdnBase}/foundry-typefaces/02-raetur/typefaces-raetur/05-typefaces-image/typefaces-image-1200.jpg`
73
+ ],
74
+
75
+ styles: {
76
+ hasWeight: true,
77
+ hasWidth: true,
78
+ hasItalic: false,
79
+ defaultStyle: 'weight',
80
+ weights: [
81
+ { label: 'Thin', weight: 100 },
82
+ { label: 'Extralight', weight: 200 },
83
+ { label: 'Light', weight: 300 },
84
+ { label: 'Regular', weight: 400 },
85
+ { label: 'Medium', weight: 500 },
86
+ { label: 'Semibold', weight: 600 },
87
+ { label: 'Bold', weight: 700 },
88
+ { label: 'Extrabold', weight: 800 },
89
+ { label: 'Black', weight: 900 }
90
+ ],
91
+ widths: [
92
+ { label: 'Narrow', width: 100 },
93
+ { label: 'Semi-Narrow', width: 175 },
94
+ { label: 'Normal', width: 250 },
95
+ { label: 'Semi-Extended', width: 325 },
96
+ { label: 'Extended', width: 400 }
97
+ ]
98
+ }
99
+ },
100
+
101
+ dylgjur: {
102
+ id: 'dylgjur',
103
+ name: 'Dylgjur',
104
+ displayName: 'Dylgjur',
105
+ fontFamily: 'TGDylgjur',
106
+ fontUrl: '/fonts/TGDylgjur-Regular.otf',
107
+ fontStyle: 'normal',
108
+ category: 'Display Font',
109
+ description: 'Sharp angles and pointed character for critical discourse',
110
+ badgeText: 'Dylgjur Aa',
111
+ specimenLink: '/foundry/specimen/dylgjur',
112
+
113
+ photos: [
114
+ `${cdnBase}/foundry-typefaces/03-dylgjur/typefaces-dylgjur/01-typefaces-hero/typefaces-hero-1200.jpg`,
115
+ `${cdnBase}/foundry-typefaces/03-dylgjur/typefaces-dylgjur/02-typefaces-image/typefaces-image-1200.jpg`,
116
+ `${cdnBase}/foundry-typefaces/03-dylgjur/typefaces-dylgjur/03-typefaces-image/typefaces-image-1200.jpg`,
117
+ `${cdnBase}/foundry-typefaces/03-dylgjur/typefaces-dylgjur/04-typefaces-image/typefaces-image-1200.jpg`,
118
+ `${cdnBase}/foundry-typefaces/03-dylgjur/typefaces-dylgjur/05-typefaces-image/typefaces-image-1200.jpg`
119
+ ],
120
+
121
+ styles: {
122
+ hasWeight: false,
123
+ hasWidth: false,
124
+ hasItalic: false,
125
+ weights: [
126
+ { label: 'Regular', weight: 400 }
127
+ ]
128
+ }
129
+ },
130
+
131
+ gullhamrar: {
132
+ id: 'gullhamrar',
133
+ name: 'Gullhamrar',
134
+ displayName: 'Gullhamrar',
135
+ fontFamily: 'TGGullhamrar',
136
+ fontUrl: '/fonts/TGGullhamrarVF.ttf',
137
+ fontStyle: 'normal',
138
+ category: 'Variable Font',
139
+ description: 'Variable weight typeface with warm, graceful forms',
140
+ badgeText: 'Gullhamrar Aa',
141
+ specimenLink: '/foundry/specimen/gullhamrar',
142
+
143
+ photos: [
144
+ `${cdnBase}/foundry-typefaces/04-gullhamrar/typefaces-gullhamrar/01-typefaces-hero/typefaces-hero-1200.jpg`,
145
+ `${cdnBase}/foundry-typefaces/04-gullhamrar/typefaces-gullhamrar/02-typefaces-image/typefaces-image-1200.jpg`,
146
+ `${cdnBase}/foundry-typefaces/04-gullhamrar/typefaces-gullhamrar/03-typefaces-image/typefaces-image-1200.jpg`,
147
+ `${cdnBase}/foundry-typefaces/04-gullhamrar/typefaces-gullhamrar/04-typefaces-image/typefaces-image-1200.jpg`,
148
+ `${cdnBase}/foundry-typefaces/04-gullhamrar/typefaces-gullhamrar/05-typefaces-image/typefaces-image-1200.jpg`
149
+ ],
150
+
151
+ styles: {
152
+ hasWeight: true,
153
+ hasWidth: false,
154
+ hasItalic: false,
155
+ weights: [
156
+ { label: 'Thin', weight: 100 },
157
+ { label: 'Extralight', weight: 200 },
158
+ { label: 'Light', weight: 300 },
159
+ { label: 'Regular', weight: 400 },
160
+ { label: 'Medium', weight: 500 },
161
+ { label: 'Semibold', weight: 600 },
162
+ { label: 'Bold', weight: 700 },
163
+ { label: 'Extrabold', weight: 800 }
164
+ ]
165
+ }
166
+ },
167
+
168
+ trollatunga: {
169
+ id: 'trollatunga',
170
+ name: 'Tröllatunga',
171
+ displayName: 'Tröllatunga',
172
+ fontFamily: 'TGTrollatunga',
173
+ fontUrl: '/fonts/TGTrollatunga-Regular.otf',
174
+ fontStyle: 'normal',
175
+ category: 'Display Font',
176
+ description: 'Bold expressive display font for impactful messaging',
177
+ badgeText: 'Tröllatunga Aa',
178
+ specimenLink: '/foundry/specimen/trollatunga',
179
+
180
+ photos: [
181
+ `${cdnBase}/foundry-typefaces/05-trollatunga/typefaces-trollatunga/01-typefaces-hero/typefaces-hero-1200.jpg`,
182
+ `${cdnBase}/foundry-typefaces/05-trollatunga/typefaces-trollatunga/02-typefaces-image/typefaces-image-1200.jpg`,
183
+ `${cdnBase}/foundry-typefaces/05-trollatunga/typefaces-trollatunga/03-typefaces-image/typefaces-image-1200.jpg`,
184
+ `${cdnBase}/foundry-typefaces/05-trollatunga/typefaces-trollatunga/04-typefaces-image/typefaces-image-1200.jpg`,
185
+ `${cdnBase}/foundry-typefaces/05-trollatunga/typefaces-trollatunga/05-typefaces-image/typefaces-image-1200.jpg`
186
+ ],
187
+
188
+ styles: {
189
+ hasWeight: false,
190
+ hasWidth: false,
191
+ hasItalic: false,
192
+ weights: [
193
+ { label: 'Regular', weight: 400 }
194
+ ]
195
+ }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Helper to get typeface config by ID
201
+ * @param {string} id - Typeface ID (malromur, rot, dylgjur, etc.)
202
+ * @returns {object} Typeface configuration object
203
+ */
204
+ export function getTypefaceConfig(id) {
205
+ const config = typefaceConfig[id]
206
+ if (!config) {
207
+ console.warn(`Typeface config not found for: ${id}`)
208
+ return typefaceConfig.malromur // fallback
209
+ }
210
+ return config
211
+ }
212
+
213
+ /**
214
+ * Get all typeface IDs
215
+ * @returns {string[]} Array of typeface IDs
216
+ */
217
+ export function getAllTypefaceIds() {
218
+ return Object.keys(typefaceConfig)
219
+ }
220
+
221
+ /**
222
+ * Get all typefaces
223
+ * @returns {object[]} Array of typeface configuration objects
224
+ */
225
+ export function getAllTypefaces() {
226
+ return Object.values(typefaceConfig)
227
+ }