@kolkrabbi/kol-foundry 0.4.2 → 0.5.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-foundry",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
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",
@@ -11,9 +11,9 @@
11
11
  ".": "./src/index.js"
12
12
  },
13
13
  "dependencies": {
14
- "@kolkrabbi/kol-component": "0.10.1",
15
- "@kolkrabbi/kol-icons": "0.6.1",
16
- "@kolkrabbi/kol-theme": "0.7.6"
14
+ "@kolkrabbi/kol-icons": "0.7.0",
15
+ "@kolkrabbi/kol-component": "0.12.0",
16
+ "@kolkrabbi/kol-theme": "0.11.0"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "react": "^18.3.0 || ^19.0.0",
@@ -157,7 +157,7 @@ const FontPreviewSection = ({
157
157
  onStyleChange={setSelectedStyleVariant}
158
158
  showDropdown={showDropdown}
159
159
  badgeText={badgeText}
160
- icon="book-open"
160
+ icon="foundation"
161
161
  size="sm"
162
162
  selectedWeight={selectedWeight}
163
163
  onWeightChange={setSelectedWeight}
@@ -0,0 +1,51 @@
1
+ import React from 'react'
2
+
3
+ /**
4
+ * GlyphItem - Single glyph character display
5
+ *
6
+ * Foundry atom for displaying individual glyph characters
7
+ * Features bordered box with hover theme flip effect
8
+ *
9
+ * @param {Object} props
10
+ * @param {string} props.glyph - Single character to display
11
+ * @param {string} props.fontStyle - Font style: 'normal' or 'italic'
12
+ * @param {string} props.fontFamily - Font family name
13
+ * @param {boolean} props.isSelected - Whether this glyph is selected
14
+ * @param {Function} props.onClick - Callback fired on click
15
+ * @param {Function} props.onMouseEnter - Callback fired on mouse enter
16
+ * @param {string} props.className - Additional classes
17
+ */
18
+ const GlyphItem = ({
19
+ glyph,
20
+ fontStyle = 'normal',
21
+ fontFamily = 'TGMalromur',
22
+ isSelected = false,
23
+ onClick,
24
+ onMouseEnter,
25
+ className = ''
26
+ }) => {
27
+ const handleClick = () => {
28
+ onClick?.(glyph)
29
+ }
30
+
31
+ const handleMouseEnter = () => {
32
+ onMouseEnter?.(glyph)
33
+ }
34
+
35
+ return (
36
+ <div
37
+ onClick={handleClick}
38
+ onMouseEnter={handleMouseEnter}
39
+ className={`${isSelected ? 'bg-auto text-auto-inverse' : 'hoverFlipTheme'} aspect-square border rounded-lg flex items-center justify-center text-3xl leading-none transition-colors duration-300 w-16 h-16 cursor-pointer ${className}`.trim()}
40
+ style={{
41
+ fontFamily: fontFamily,
42
+ fontStyle: fontStyle,
43
+ borderColor: 'var(--kol-border-default)'
44
+ }}
45
+ >
46
+ {glyph}
47
+ </div>
48
+ )
49
+ }
50
+
51
+ export default GlyphItem
@@ -85,7 +85,7 @@ const GlyphMetricsSection = ({
85
85
  showDropdown={showDropdown || (showAxisDropdown && valueOptions.length > 0)}
86
86
  styleOptions={showDropdown ? italicOptions : valueOptions}
87
87
  badgeText={badgeText}
88
- icon="book-open"
88
+ icon="foundation"
89
89
  size="sm"
90
90
  showWeightDropdown={showAxisDropdown && valueOptions.length > 0}
91
91
  weightOptions={valueOptions}
@@ -114,7 +114,7 @@ const TypefaceStyleSection = ({
114
114
  styleOptions={styleOptions || undefined}
115
115
  showDropdown={showDropdown}
116
116
  badgeText={badgeText}
117
- icon="book-open"
117
+ icon="foundation"
118
118
  size="sm"
119
119
  />
120
120
 
@@ -107,7 +107,7 @@ const VariableFontSection = ({
107
107
  onStyleChange={setSelectedStyle}
108
108
  showDropdown={showDropdown}
109
109
  badgeText={badgeText}
110
- icon="book-open"
110
+ icon="foundation"
111
111
  size="sm"
112
112
  />
113
113
 
@@ -0,0 +1,333 @@
1
+ // =============================================================================
2
+ // core/FontInfo.js
3
+ // =============================================================================
4
+
5
+ import { AXIS_NAMES } from './Types.js';
6
+
7
+ const PREFERRED_LANGUAGE_CODES = ['en', 'en-US', 'en-GB', 'und', '0'];
8
+
9
+ export class FontInfoRenderer {
10
+ /**
11
+ * Renders font information into a specified container
12
+ * @param {HTMLElement} container - Container element to render into
13
+ * @param {Object} info - Font information object
14
+ */
15
+ static renderFontInfo(container, info) {
16
+ if (!container || !info) return;
17
+
18
+ container.innerHTML = `
19
+ <p><strong>Names</strong><br>
20
+ Font family &rarr; ${info.fontFamily}<br>
21
+ Font style &rarr; ${info.fontStyle}<br>
22
+
23
+ <p><strong>Font file</strong><br>
24
+ Filename &rarr; ${info.filename}<br>
25
+ File format &rarr; ${info.format}<br>
26
+ Variable font &rarr; ${info.axes.length > 0 ? 'Yes' : 'No'}<br>
27
+ Monospaced &rarr; ${info.isFixedPitch}<br>
28
+ Units per Em &rarr; ${info.unitsPerEm}<br>
29
+ Glyph count &rarr; ${info.glyphCount}</p>
30
+
31
+ ${info.axes.length ? `
32
+ <p><strong>Variable font axes</strong><br>
33
+ ${info.axes.map(axis =>
34
+ `${axis.name} (${axis.tag}) &rarr; ${axis.min} to ${axis.max}, default &rarr; ${axis.default}`
35
+ ).join('<br>')}</p>` : ''}
36
+
37
+ <p><strong>Version and dates</strong><br>
38
+ Version &rarr; ${info.version}<br>
39
+ Created &rarr; ${info.created}<br>
40
+ Modified &rarr; ${info.modified}</p>
41
+
42
+ <p><strong>Foundry</strong><br>
43
+ Manufacturer &rarr; ${info.manufacturer}<br>
44
+ ${info.manufacturerURL !== 'Unknown' ? `Manufacturer URL &rarr; ${info.manufacturerURL}<br>` : ''}
45
+ Designer &rarr; ${info.designer}<br>
46
+ ${info.designerURL !== 'Unknown' ? `Designer URL &rarr; ${info.designerURL}<br>` : ''}
47
+ Vendor ID &rarr; ${info.vendorID}</p>
48
+
49
+ <p><strong>Copyright</strong><br>
50
+ ${info.copyright}</p>
51
+
52
+ <p><strong>Licence</strong><br>
53
+ ${info.license}<br>
54
+ ${info.licenseURL !== 'Unknown' ? `Licence URL &rarr; ${info.licenseURL}</p>` : '</p>'}
55
+ `;
56
+ }
57
+
58
+ /**
59
+ * Renders glyph information into a specified container
60
+ * @param {HTMLElement} container - Container element to render into
61
+ * @param {Object} font - OpenType.js font object
62
+ * @param {string} glyph - Current glyph character
63
+ */
64
+ static renderGlyphInfo(container, font, glyph) {
65
+ if (!container) return;
66
+
67
+ if (!font || !glyph) {
68
+ container.innerHTML = '<p>No glyph selected</p>';
69
+ return;
70
+ }
71
+
72
+ const glyphIndex = font.charToGlyphIndex(glyph);
73
+ const glyphObj = font.glyphs.get(glyphIndex);
74
+
75
+ container.innerHTML = `
76
+ <div class="glyph-info-container">
77
+ <div class="info-column">
78
+ <p><strong>Glyph information</strong><br>
79
+ Character &rarr; ${glyph}<br>
80
+ Name &rarr; <span class="monospaced">${glyphObj.name}</span><br>
81
+ Unicode &rarr; <span class="monospaced">U+${glyphObj.unicode?.toString(16).toUpperCase().padStart(4, '0') || 'N/A'}</span><br>
82
+ Index &rarr; <span class="monospaced">${glyphIndex}</span><br>
83
+ Advance Width &rarr; <span class="monospaced">${glyphObj.advanceWidth}</span></p>
84
+ </div>
85
+
86
+ ${glyphObj.xMin !== undefined ? `
87
+ <div class="info-column">
88
+ <p><strong>Bounds</strong><br>
89
+ xMin &rarr; <span class="monospaced">${glyphObj.xMin}</span><br>
90
+ xMax &rarr; <span class="monospaced">${glyphObj.xMax}</span><br>
91
+ yMin &rarr; <span class="monospaced">${glyphObj.yMin}</span><br>
92
+ yMax &rarr; <span class="monospaced">${glyphObj.yMax}</span></p>
93
+ </div>
94
+ ` : ''}
95
+ </div>
96
+ `;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Extracts comprehensive information about a font
102
+ * @param {Object} font - OpenType.js font object
103
+ * @param {string} filename - Original font filename
104
+ * @returns {Object} Detailed font information
105
+ */
106
+ export function getFontInformation(font, filename) {
107
+ const names = font.names;
108
+ const os2 = font.tables.os2;
109
+ const head = font.tables.head;
110
+
111
+ const fontFamily = pickBestName([
112
+ names?.preferredFamily,
113
+ names?.fontFamily,
114
+ names?.windows?.preferredFamily,
115
+ names?.windows?.fontFamily
116
+ ]);
117
+
118
+ const fullName = pickBestName([
119
+ names?.fullName,
120
+ names?.windows?.fullName
121
+ ]);
122
+
123
+ const version = pickBestName([
124
+ names?.version,
125
+ names?.windows?.version
126
+ ]);
127
+
128
+ const copyright = pickBestName([
129
+ names?.copyright,
130
+ names?.windows?.copyright
131
+ ]);
132
+
133
+ const manufacturer = pickBestName([
134
+ names?.manufacturer,
135
+ names?.windows?.manufacturer
136
+ ]);
137
+
138
+ const manufacturerURL = pickBestName([
139
+ names?.manufacturerURL,
140
+ names?.windows?.manufacturerURL
141
+ ]);
142
+
143
+ const designer = pickBestName([
144
+ names?.designer,
145
+ names?.windows?.designer
146
+ ]);
147
+
148
+ const designerURL = pickBestName([
149
+ names?.designerURL,
150
+ names?.windows?.designerURL
151
+ ]);
152
+
153
+ const license = pickBestName([
154
+ names?.license,
155
+ names?.windows?.license
156
+ ]);
157
+
158
+ const licenseURL = pickBestName([
159
+ names?.licenseURL,
160
+ names?.windows?.licenseURL
161
+ ]);
162
+
163
+ return {
164
+ filename,
165
+ fontFamily,
166
+ fullName,
167
+ version,
168
+ copyright,
169
+ manufacturer,
170
+ manufacturerURL,
171
+ designer,
172
+ designerURL,
173
+ license,
174
+ licenseURL,
175
+ vendorID: os2?.achVendID || 'Unknown',
176
+ format: font.outlinesFormat,
177
+ unitsPerEm: head?.unitsPerEm || 'Unknown',
178
+ created: head?.created ? new Date(head.created * 1000).toLocaleDateString() : 'Unknown',
179
+ modified: head?.modified ? new Date(head.modified * 1000).toLocaleDateString() : 'Unknown',
180
+ glyphCount: font.glyphs.length,
181
+ isFixedPitch: font.tables.post?.isFixedPitch ? 'Yes' : 'No',
182
+ fontStyle: extractFontStyle(font),
183
+ features: extractOpenTypeFeatures(font),
184
+ axes: extractVariableAxes(font)
185
+ };
186
+ }
187
+
188
+ // Helper functions
189
+ /**
190
+ * Extracts a name from the OpenType names table using proper name ID structure
191
+ * @param {Object} names - The font.names object
192
+ * @param {number} nameId - OpenType name ID (1=family, 4=full name, 5=version, etc.)
193
+ * @returns {string} The extracted name or 'Unknown'
194
+ */
195
+ function extractNameByID(names, nameId) {
196
+ if (!names) return 'Unknown';
197
+
198
+ // Try different platforms in order of preference
199
+ const platforms = ['windows', 'macintosh', 'unicode'];
200
+
201
+ for (const platform of platforms) {
202
+ if (names[platform] && names[platform][nameId]) {
203
+ const nameEntry = names[platform][nameId];
204
+
205
+ // Try different language codes
206
+ const languageCodes = ['en', 'en-US', 'en-GB', 'und', '0', '1033']; // 1033 is Windows English
207
+
208
+ for (const code of languageCodes) {
209
+ if (nameEntry[code]) {
210
+ return nameEntry[code];
211
+ }
212
+ }
213
+
214
+ // If no language code matches, get the first available value
215
+ const availableKeys = Object.keys(nameEntry);
216
+ if (availableKeys.length > 0) {
217
+ return nameEntry[availableKeys[0]];
218
+ }
219
+ }
220
+ }
221
+
222
+ return 'Unknown';
223
+ }
224
+
225
+ /**
226
+ * Safely extracts a name value from font names table
227
+ * Tries multiple language codes and falls back to the first available value
228
+ * @param {Object} nameEntry - Name entry from font.names
229
+ * @returns {string} The extracted name or 'Unknown'
230
+ */
231
+ function extractFontName(nameEntry) {
232
+ if (!nameEntry) {
233
+ return 'Unknown';
234
+ }
235
+
236
+ for (const code of PREFERRED_LANGUAGE_CODES) {
237
+ if (nameEntry[code]) {
238
+ return nameEntry[code];
239
+ }
240
+ }
241
+
242
+ // If no standard language codes work, get the first available value
243
+ const availableKeys = Object.keys(nameEntry);
244
+
245
+ if (availableKeys.length > 0) {
246
+ const firstKey = availableKeys[0];
247
+ return nameEntry[firstKey];
248
+ }
249
+
250
+ return 'Unknown';
251
+ }
252
+
253
+ function pickBestName(entries = []) {
254
+ for (const entry of entries) {
255
+ const value = extractFontName(entry);
256
+ if (value && value !== 'Unknown') {
257
+ return value;
258
+ }
259
+ }
260
+ return 'Unknown';
261
+ }
262
+
263
+ /**
264
+ * Extracts font style (upright/italic/oblique) from OpenType tables
265
+ * @param {Object} font - OpenType.js font object
266
+ * @returns {string} Font style: 'Upright', 'Italic', or 'Oblique'
267
+ */
268
+ function extractFontStyle(font) {
269
+ // Check OS/2 table fsSelection field first (most reliable)
270
+ if (font.tables.os2 && font.tables.os2.fsSelection !== undefined) {
271
+ const fsSelection = font.tables.os2.fsSelection;
272
+
273
+ // Bit 9 (512): Oblique/Slanted
274
+ if (fsSelection & 512) {
275
+ return 'Oblique';
276
+ }
277
+
278
+ // Bit 0 (1): Italic
279
+ if (fsSelection & 1) {
280
+ return 'Italic';
281
+ }
282
+ }
283
+
284
+ // Fallback to head table macStyle field
285
+ if (font.tables.head && font.tables.head.macStyle !== undefined) {
286
+ const macStyle = font.tables.head.macStyle;
287
+
288
+ // Bit 1 (2): Italic
289
+ if (macStyle & 2) {
290
+ return 'Italic';
291
+ }
292
+ }
293
+
294
+ // If no italic/oblique flags are set, it's upright/roman
295
+ return 'Upright';
296
+ }
297
+
298
+ function extractOpenTypeFeatures(font) {
299
+ const features = [];
300
+ if (font.tables.gsub) {
301
+ font.tables.gsub.features.forEach(feature => {
302
+ features.push({
303
+ tag: feature.tag,
304
+ scripts: feature.feature.lookupListIndexes
305
+ });
306
+ });
307
+ }
308
+ return features;
309
+ }
310
+
311
+ function extractVariableAxes(font) {
312
+ if (!font.tables.fvar) {
313
+ console.log('No fvar table found in font');
314
+ return [];
315
+ }
316
+
317
+ console.log('Raw fvar axes:', font.tables.fvar.axes);
318
+
319
+ const axes = font.tables.fvar.axes.map(axis => {
320
+ const mappedAxis = {
321
+ tag: axis.tag,
322
+ name: AXIS_NAMES[axis.tag] || axis.tag,
323
+ min: axis.minValue,
324
+ max: axis.maxValue,
325
+ default: axis.defaultValue
326
+ };
327
+ console.log('Mapped axis:', mappedAxis);
328
+ return mappedAxis;
329
+ });
330
+
331
+ console.log('Final extracted axes:', axes);
332
+ return axes;
333
+ }
@@ -0,0 +1,76 @@
1
+ // =============================================================================
2
+ // core/FontLoader.js
3
+ // =============================================================================
4
+
5
+ import { getFontInformation } from './FontInfo.js';
6
+ // Namespace import for ESM/CJS interop: opentype.js ships a CJS-wrapped .mjs
7
+ // whose default export rollup (Vite 5) can't statically see. `parse` is a named
8
+ // export, so the namespace form resolves cleanly across bundlers.
9
+ import * as opentype from 'opentype.js';
10
+
11
+ export class FontLoader {
12
+ /**
13
+ * Creates a new FontLoader instance
14
+ * @param {Object} options - Configuration options
15
+ * @param {Function} options.onFontLoaded - Callback when font is loaded
16
+ * @param {Function} options.onError - Callback when error occurs
17
+ */
18
+ constructor(options = {}) {
19
+ this.currentFont = null;
20
+ this.callbacks = options;
21
+ }
22
+
23
+ /**
24
+ * Loads a font from an ArrayBuffer
25
+ * @param {ArrayBuffer} buffer - The font file buffer
26
+ * @param {string} filename - Original filename
27
+ * @returns {Promise<Object>} Font information and loaded font
28
+ */
29
+ async loadFont(buffer, filename) {
30
+ try {
31
+ // Parse the font using OpenType.js
32
+ const font = opentype.parse(buffer);
33
+ console.log('OpenType parsed font:', {
34
+ tables: Object.keys(font.tables),
35
+ hasFvar: !!font.tables.fvar,
36
+ axes: font.tables.fvar?.axes
37
+ });
38
+
39
+ // Create a unique name for this font instance
40
+ const uniqueFontName = `Font_${Date.now()}`;
41
+
42
+ // Create and load the font face
43
+ const fontFace = new FontFace(uniqueFontName, buffer);
44
+ await fontFace.load();
45
+ document.fonts.add(fontFace);
46
+
47
+ // Store current font and get info
48
+ this.currentFont = font;
49
+ const fontInfo = getFontInformation(font, filename);
50
+
51
+ console.log('Font info generated:', fontInfo);
52
+
53
+ this.callbacks.onFontLoaded?.({ font, fontInfo, fontFamily: uniqueFontName });
54
+ return { font, fontInfo, fontFamily: uniqueFontName };
55
+
56
+ } catch (error) {
57
+ console.error('Error loading font:', error);
58
+ this.callbacks.onError?.(error);
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Cleans up the current font
65
+ */
66
+ cleanup() {
67
+ if (this.currentFont) {
68
+ document.fonts.forEach(font => {
69
+ if (font.family.startsWith('Font_')) {
70
+ document.fonts.delete(font);
71
+ }
72
+ });
73
+ this.currentFont = null;
74
+ }
75
+ }
76
+ }