@kolkrabbi/kol-foundry 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/GlyphMetricsSection.jsx +107 -0
- package/src/TypefaceLibraryGrid.jsx +75 -0
- package/src/TypefaceLibraryGridWithVariables.jsx +236 -0
- package/src/TypefaceLibraryItem.jsx +190 -0
- package/src/TypefaceSpecimenPage.jsx +223 -0
- package/src/TypefaceVariablePreview.jsx +160 -0
- package/src/index.js +18 -9
- package/src/typefaceConfig.js +227 -0
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-foundry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "KOL
|
|
5
|
+
"description": "KOL foundry component set — the type-specimen apparatus: typeface hero, variable-font axis playground, parsed-metric glyph inspector, character-set browser, font-preview, and the typeface-catalog grid. Every component renders, inspects, or manipulates a live font. Typeface data is consumer-injected. Sits above @kolkrabbi/kol-{theme,icons,component}.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.js",
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
".": "./src/index.js"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"@kolkrabbi/kol-theme": "0.7.1",
|
|
14
15
|
"@kolkrabbi/kol-component": "0.7.0",
|
|
15
|
-
"@kolkrabbi/kol-icons": "0.5.0"
|
|
16
|
-
"@kolkrabbi/kol-theme": "0.7.1"
|
|
16
|
+
"@kolkrabbi/kol-icons": "0.5.0"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"react": "^18.3.0 || ^19.0.0",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import SpecimenSectionHeader from './SpecimenSectionHeader.jsx'
|
|
3
|
+
import GlyphMetricsGrid from './GlyphMetricsGrid.jsx'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GlyphMetricsSection — the "Glyph Metrics" specimen section: a section header
|
|
7
|
+
* (Roman/Italic or Weight/Width axis dropdowns) over the parsed-metric
|
|
8
|
+
* GlyphMetricsGrid. Owns the style/axis selection state and feeds the grid the
|
|
9
|
+
* chosen font URL + `variationSettings`.
|
|
10
|
+
*
|
|
11
|
+
* Composes the package's existing canonical leaves — SpecimenSectionHeader
|
|
12
|
+
* (promoted from the monorepo's `FoundrySection`) and GlyphMetricsGrid (which
|
|
13
|
+
* already handles font I/O with opentype.js as an optional peer).
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} props
|
|
16
|
+
* @param {string} props.fontFamily - CSS family fallback until the parsed face injects.
|
|
17
|
+
* @param {string} props.fontUrlRoman - Roman/upright font URL.
|
|
18
|
+
* @param {string} props.fontUrlItalic - Italic font URL.
|
|
19
|
+
* @param {'normal'|'italic'} props.fontStyle - Initial style variant.
|
|
20
|
+
* @param {string} props.badgeText - Section title text.
|
|
21
|
+
* @param {boolean} props.showDropdown - Show the Roman/Italic style dropdown.
|
|
22
|
+
* @param {boolean} props.hasWeight - Font has a weight axis.
|
|
23
|
+
* @param {boolean} props.hasWidth - Font has a width axis.
|
|
24
|
+
* @param {Array<{label,weight}>} props.weights - Weight axis options.
|
|
25
|
+
* @param {Array<{label,width}>} props.widths - Width axis options.
|
|
26
|
+
*/
|
|
27
|
+
const GlyphMetricsSection = ({
|
|
28
|
+
fontFamily = 'TGMalromur',
|
|
29
|
+
fontUrlRoman,
|
|
30
|
+
fontUrlItalic,
|
|
31
|
+
fontStyle = 'normal',
|
|
32
|
+
badgeText = 'Málrómur',
|
|
33
|
+
showDropdown = true,
|
|
34
|
+
// Variable font axis support
|
|
35
|
+
hasWeight = false,
|
|
36
|
+
hasWidth = false,
|
|
37
|
+
weights = [],
|
|
38
|
+
widths = []
|
|
39
|
+
}) => {
|
|
40
|
+
const [selectedStyleVariant, setSelectedStyleVariant] = useState(
|
|
41
|
+
fontStyle === 'italic' ? 'italic' : 'roman'
|
|
42
|
+
)
|
|
43
|
+
const [selectedAxis] = useState('weight')
|
|
44
|
+
const [selectedWeight, setSelectedWeight] = useState(400)
|
|
45
|
+
const [selectedWidth, setSelectedWidth] = useState(100)
|
|
46
|
+
|
|
47
|
+
const isItalic = selectedStyleVariant === 'italic'
|
|
48
|
+
|
|
49
|
+
// Select the correct font URL based on the dropdown
|
|
50
|
+
const currentFontUrl = isItalic ? fontUrlItalic : fontUrlRoman
|
|
51
|
+
|
|
52
|
+
// Build axis options for dropdown
|
|
53
|
+
const axisOptions = []
|
|
54
|
+
if (hasWeight) axisOptions.push({ label: 'Weight', value: 'weight' })
|
|
55
|
+
if (hasWidth) axisOptions.push({ label: 'Width', value: 'width' })
|
|
56
|
+
|
|
57
|
+
// Build value options based on selected axis
|
|
58
|
+
const valueOptions = selectedAxis === 'weight'
|
|
59
|
+
? weights.map(w => ({ label: w.label, value: w.weight }))
|
|
60
|
+
: widths.map(w => ({ label: w.label, value: w.width }))
|
|
61
|
+
|
|
62
|
+
const selectedValue = selectedAxis === 'weight' ? selectedWeight : selectedWidth
|
|
63
|
+
const onValueChange = selectedAxis === 'weight' ? setSelectedWeight : setSelectedWidth
|
|
64
|
+
|
|
65
|
+
// Show axis dropdown if font has variable axes
|
|
66
|
+
const showAxisDropdown = axisOptions.length > 0
|
|
67
|
+
|
|
68
|
+
// Get variation settings for the glyph display
|
|
69
|
+
const variationSettings = {}
|
|
70
|
+
if (hasWeight) variationSettings.wght = selectedWeight
|
|
71
|
+
if (hasWidth) variationSettings.wdth = selectedWidth
|
|
72
|
+
|
|
73
|
+
// Style dropdown: roman/italic when font has italic
|
|
74
|
+
const italicOptions = [
|
|
75
|
+
{ label: 'Roman', value: 'roman' },
|
|
76
|
+
{ label: 'Italic', value: 'italic' }
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<section className="w-full py-12 lg:py-16">
|
|
81
|
+
<div className="max-w-[1400px] mx-auto flex flex-col gap-8">
|
|
82
|
+
<SpecimenSectionHeader
|
|
83
|
+
selectedStyle={showDropdown ? selectedStyleVariant : showAxisDropdown ? selectedValue : undefined}
|
|
84
|
+
onStyleChange={showDropdown ? setSelectedStyleVariant : showAxisDropdown ? onValueChange : undefined}
|
|
85
|
+
showDropdown={showDropdown || (showAxisDropdown && valueOptions.length > 0)}
|
|
86
|
+
styleOptions={showDropdown ? italicOptions : valueOptions}
|
|
87
|
+
badgeText={badgeText}
|
|
88
|
+
icon="foundation"
|
|
89
|
+
size="sm"
|
|
90
|
+
showWeightDropdown={showAxisDropdown && valueOptions.length > 0}
|
|
91
|
+
weightOptions={valueOptions}
|
|
92
|
+
selectedWeight={selectedValue}
|
|
93
|
+
onWeightChange={onValueChange}
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<GlyphMetricsGrid
|
|
97
|
+
fontUrl={currentFontUrl}
|
|
98
|
+
fontFamily={fontFamily}
|
|
99
|
+
fontStyle="normal"
|
|
100
|
+
variationSettings={variationSettings}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
</section>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default GlyphMetricsSection
|
|
@@ -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
|
|
@@ -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,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @kol
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
//
|
|
7
|
+
// section scaffold
|
|
10
8
|
export { default as SpecimenSectionHeader } from './SpecimenSectionHeader.jsx'
|
|
11
9
|
|
|
12
|
-
//
|
|
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
|
+
// reference composition (severed page — data via props, no router/SEO/data-fetch)
|
|
26
|
+
export { default as TypefaceSpecimenPage } from './TypefaceSpecimenPage.jsx'
|
|
27
|
+
|
|
20
28
|
// data
|
|
21
29
|
export { glyphSets, glyphCategories, SPECIMEN_SAMPLE_TEXT } from './glyphData.js'
|
|
30
|
+
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
|
+
}
|