font-switcher 0.1.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,429 @@
1
+ "use strict";
2
+ "use client";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/component.tsx
22
+ var component_exports = {};
23
+ __export(component_exports, {
24
+ FontSwitcher: () => FontSwitcher
25
+ });
26
+ module.exports = __toCommonJS(component_exports);
27
+ var import_react = require("react");
28
+ var import_jsx_runtime = require("react/jsx-runtime");
29
+ var PANEL_STORAGE_KEY = "font-switcher-ui";
30
+ var DEFAULT_RENDER = {
31
+ textRendering: "auto",
32
+ webkitSmoothing: "auto",
33
+ mozSmoothing: "auto"
34
+ };
35
+ var POSITIONS = {
36
+ "bottom-right": { bottom: "16px", right: "16px" },
37
+ "bottom-left": { bottom: "16px", left: "16px" },
38
+ "top-right": { top: "16px", right: "16px" },
39
+ "top-left": { top: "16px", left: "16px" }
40
+ };
41
+ function buildFeatureMeta(fonts, detected) {
42
+ const result = {};
43
+ for (const font of fonts) {
44
+ const fromConfig = font.features ?? [];
45
+ const fromDetected = detected[font.key] ?? [];
46
+ const configTags = new Set(fromConfig.map((f) => f.tag));
47
+ const merged = [...fromConfig, ...fromDetected.filter((f) => !configTags.has(f.tag))];
48
+ if (merged.length) result[font.key] = merged;
49
+ }
50
+ return result;
51
+ }
52
+ function applyRuntimeStyles(activeKey, featureEnabled, render) {
53
+ let el = document.getElementById("font-switcher-rt");
54
+ if (!el) {
55
+ el = document.createElement("style");
56
+ el.id = "font-switcher-rt";
57
+ document.head.appendChild(el);
58
+ }
59
+ const fontTags = featureEnabled[activeKey];
60
+ const featureStr = fontTags && Object.keys(fontTags).length ? Object.entries(fontTags).map(([tag, on]) => `"${tag}" ${on ? 1 : 0}`).join(", ") : "normal";
61
+ el.textContent = [
62
+ "html {",
63
+ ` font-feature-settings: ${featureStr};`,
64
+ ` text-rendering: ${render.textRendering};`,
65
+ ` -webkit-font-smoothing: ${render.webkitSmoothing};`,
66
+ ` -moz-osx-font-smoothing: ${render.mozSmoothing};`,
67
+ "}"
68
+ ].join("\n");
69
+ }
70
+ function FontSwitcher({ config, position = "bottom-right" }) {
71
+ const {
72
+ fonts,
73
+ defaultFont,
74
+ storageKey = "font-switcher-active",
75
+ revealParam = "fonts",
76
+ revealShortcut = "ctrl+shift+f",
77
+ finalFont
78
+ } = config;
79
+ const [visible, setVisible] = (0, import_react.useState)(false);
80
+ const [active, setActive] = (0, import_react.useState)(defaultFont);
81
+ const [featureMeta, setFeatureMeta] = (0, import_react.useState)({});
82
+ const [featureEnabled, setFeatureEnabled] = (0, import_react.useState)({});
83
+ const [renderSettings, setRenderSettings] = (0, import_react.useState)(DEFAULT_RENDER);
84
+ (0, import_react.useEffect)(() => {
85
+ if (finalFont) return;
86
+ try {
87
+ const stored = localStorage.getItem(storageKey);
88
+ if (stored && fonts.some((f) => f.key === stored)) setActive(stored);
89
+ } catch {
90
+ }
91
+ const params = new URLSearchParams(window.location.search);
92
+ if (params.has(revealParam)) {
93
+ setVisible(true);
94
+ try {
95
+ localStorage.setItem(PANEL_STORAGE_KEY, "on");
96
+ } catch {
97
+ }
98
+ } else {
99
+ try {
100
+ if (localStorage.getItem(PANEL_STORAGE_KEY) === "on") setVisible(true);
101
+ } catch {
102
+ }
103
+ }
104
+ const parts = revealShortcut.toLowerCase().split("+");
105
+ const targetKey = parts[parts.length - 1];
106
+ const mods = new Set(parts.slice(0, -1));
107
+ const onKey = (e) => {
108
+ if (e.key.toLowerCase() !== targetKey) return;
109
+ if (mods.has("ctrl") !== e.ctrlKey) return;
110
+ if (mods.has("shift") !== e.shiftKey) return;
111
+ if (mods.has("alt") !== e.altKey) return;
112
+ if (mods.has("meta") !== e.metaKey) return;
113
+ e.preventDefault();
114
+ setVisible((v) => {
115
+ const next = !v;
116
+ try {
117
+ localStorage.setItem(PANEL_STORAGE_KEY, next ? "on" : "off");
118
+ } catch {
119
+ }
120
+ return next;
121
+ });
122
+ };
123
+ window.addEventListener("keydown", onKey);
124
+ const detected = window.__fontSwitcherFeatures ?? {};
125
+ const meta = buildFeatureMeta(fonts, detected);
126
+ setFeatureMeta(meta);
127
+ const defaults = Object.fromEntries(
128
+ Object.entries(meta).map(([key, features]) => [
129
+ key,
130
+ Object.fromEntries(features.map((f) => [f.tag, f.enabled]))
131
+ ])
132
+ );
133
+ try {
134
+ const stored = localStorage.getItem(`${storageKey}-features`);
135
+ if (stored) {
136
+ const parsed = JSON.parse(stored);
137
+ for (const [fontKey, tags] of Object.entries(parsed)) {
138
+ if (defaults[fontKey]) Object.assign(defaults[fontKey], tags);
139
+ }
140
+ }
141
+ } catch {
142
+ }
143
+ setFeatureEnabled(defaults);
144
+ try {
145
+ const stored = localStorage.getItem(`${storageKey}-render`);
146
+ if (stored) setRenderSettings((r) => ({ ...r, ...JSON.parse(stored) }));
147
+ } catch {
148
+ }
149
+ return () => window.removeEventListener("keydown", onKey);
150
+ }, []);
151
+ (0, import_react.useEffect)(() => {
152
+ if (finalFont) return;
153
+ applyRuntimeStyles(active, featureEnabled, renderSettings);
154
+ }, [active, featureEnabled, renderSettings, finalFont]);
155
+ const switchFont = (0, import_react.useCallback)(
156
+ (key) => {
157
+ document.documentElement.dataset.font = key;
158
+ try {
159
+ localStorage.setItem(storageKey, key);
160
+ } catch {
161
+ }
162
+ setActive(key);
163
+ },
164
+ [storageKey]
165
+ );
166
+ const reset = (0, import_react.useCallback)(() => {
167
+ delete document.documentElement.dataset.font;
168
+ try {
169
+ localStorage.removeItem(storageKey);
170
+ } catch {
171
+ }
172
+ setActive(defaultFont);
173
+ }, [defaultFont, storageKey]);
174
+ const toggleFeature = (0, import_react.useCallback)(
175
+ (fontKey, tag) => {
176
+ setFeatureEnabled((prev) => {
177
+ const next = {
178
+ ...prev,
179
+ [fontKey]: { ...prev[fontKey], [tag]: !prev[fontKey]?.[tag] }
180
+ };
181
+ try {
182
+ localStorage.setItem(`${storageKey}-features`, JSON.stringify(next));
183
+ } catch {
184
+ }
185
+ return next;
186
+ });
187
+ },
188
+ [storageKey]
189
+ );
190
+ const updateRender = (0, import_react.useCallback)(
191
+ (key, value) => {
192
+ setRenderSettings((prev) => {
193
+ const next = { ...prev, [key]: value };
194
+ try {
195
+ localStorage.setItem(`${storageKey}-render`, JSON.stringify(next));
196
+ } catch {
197
+ }
198
+ return next;
199
+ });
200
+ },
201
+ [storageKey]
202
+ );
203
+ if (finalFont || !visible) return null;
204
+ const activeFeatures = featureMeta[active] ?? [];
205
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
206
+ "div",
207
+ {
208
+ style: {
209
+ position: "fixed",
210
+ zIndex: 9999,
211
+ background: "#fff",
212
+ border: "1px solid #e0e0e0",
213
+ padding: "6px",
214
+ minWidth: "200px",
215
+ maxWidth: "280px",
216
+ maxHeight: "80vh",
217
+ overflowY: "auto",
218
+ boxShadow: "0 2px 8px rgba(0,0,0,0.12)",
219
+ fontFamily: "system-ui, sans-serif",
220
+ ...POSITIONS[position]
221
+ },
222
+ children: [
223
+ fonts.map((font) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
224
+ "button",
225
+ {
226
+ onClick: () => switchFont(font.key),
227
+ style: {
228
+ display: "flex",
229
+ alignItems: "center",
230
+ justifyContent: "space-between",
231
+ width: "100%",
232
+ padding: "5px 8px",
233
+ border: "none",
234
+ background: active === font.key ? "#f5f5f5" : "transparent",
235
+ cursor: "pointer",
236
+ fontFamily: font.cssFamily,
237
+ fontSize: "13px",
238
+ letterSpacing: 0,
239
+ textAlign: "left"
240
+ },
241
+ children: [
242
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: font.label }),
243
+ active === font.key && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "8px", color: "#999", marginLeft: "8px" }, children: "\u25CF" })
244
+ ]
245
+ },
246
+ font.key
247
+ )),
248
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { borderTop: "1px solid #e8e8e8", marginTop: "4px", paddingTop: "4px" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
249
+ "button",
250
+ {
251
+ onClick: reset,
252
+ style: {
253
+ display: "block",
254
+ width: "100%",
255
+ padding: "4px 8px",
256
+ border: "none",
257
+ background: "transparent",
258
+ cursor: "pointer",
259
+ fontSize: "11px",
260
+ color: "#aaa",
261
+ textAlign: "left",
262
+ letterSpacing: 0
263
+ },
264
+ children: "Reset"
265
+ }
266
+ ) }),
267
+ activeFeatures.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { borderTop: "1px solid #e8e8e8", marginTop: "4px", paddingTop: "4px" }, children: [
268
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
269
+ "div",
270
+ {
271
+ style: {
272
+ padding: "2px 8px 4px",
273
+ fontSize: "10px",
274
+ color: "#bbb",
275
+ textTransform: "uppercase",
276
+ letterSpacing: "0.06em"
277
+ },
278
+ children: "Features"
279
+ }
280
+ ),
281
+ activeFeatures.map((feat) => {
282
+ const on = featureEnabled[active]?.[feat.tag] ?? feat.enabled;
283
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
284
+ "button",
285
+ {
286
+ onClick: () => toggleFeature(active, feat.tag),
287
+ title: `font-feature-settings: "${feat.tag}" ${on ? 1 : 0}`,
288
+ style: {
289
+ display: "flex",
290
+ alignItems: "center",
291
+ justifyContent: "space-between",
292
+ width: "100%",
293
+ padding: "4px 8px",
294
+ border: "none",
295
+ background: "transparent",
296
+ cursor: "pointer",
297
+ fontSize: "12px",
298
+ textAlign: "left",
299
+ color: on ? "#111" : "#ccc",
300
+ fontFamily: "system-ui, sans-serif"
301
+ },
302
+ children: [
303
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: feat.label }),
304
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
305
+ "span",
306
+ {
307
+ style: {
308
+ fontSize: "9px",
309
+ fontFamily: "monospace",
310
+ color: on ? "#bbb" : "#ddd",
311
+ marginLeft: "8px",
312
+ flexShrink: 0
313
+ },
314
+ children: feat.tag
315
+ }
316
+ )
317
+ ]
318
+ },
319
+ feat.tag
320
+ );
321
+ })
322
+ ] }),
323
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { borderTop: "1px solid #e8e8e8", marginTop: "4px", paddingTop: "4px" }, children: [
324
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
325
+ "div",
326
+ {
327
+ style: {
328
+ padding: "2px 8px 4px",
329
+ fontSize: "10px",
330
+ color: "#bbb",
331
+ textTransform: "uppercase",
332
+ letterSpacing: "0.06em"
333
+ },
334
+ children: "Rendering"
335
+ }
336
+ ),
337
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
338
+ RenderSelect,
339
+ {
340
+ label: "text-rendering",
341
+ title: "text-rendering",
342
+ value: renderSettings.textRendering,
343
+ options: ["auto", "optimizeLegibility", "optimizeSpeed", "geometricPrecision"],
344
+ onChange: (v) => updateRender("textRendering", v)
345
+ }
346
+ ),
347
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
348
+ RenderSelect,
349
+ {
350
+ label: "webkit-smoothing",
351
+ title: "-webkit-font-smoothing",
352
+ value: renderSettings.webkitSmoothing,
353
+ options: ["auto", "none", "antialiased", "subpixel-antialiased"],
354
+ onChange: (v) => updateRender("webkitSmoothing", v)
355
+ }
356
+ ),
357
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
358
+ RenderSelect,
359
+ {
360
+ label: "moz-osx-smoothing",
361
+ title: "-moz-osx-font-smoothing",
362
+ value: renderSettings.mozSmoothing,
363
+ options: ["auto", "grayscale"],
364
+ onChange: (v) => updateRender("mozSmoothing", v)
365
+ }
366
+ )
367
+ ] })
368
+ ]
369
+ }
370
+ );
371
+ }
372
+ function RenderSelect({
373
+ label,
374
+ title,
375
+ value,
376
+ options,
377
+ onChange
378
+ }) {
379
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
380
+ "div",
381
+ {
382
+ style: {
383
+ display: "flex",
384
+ alignItems: "center",
385
+ justifyContent: "space-between",
386
+ padding: "3px 8px",
387
+ gap: "8px"
388
+ },
389
+ children: [
390
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
391
+ "span",
392
+ {
393
+ title,
394
+ style: {
395
+ fontSize: "11px",
396
+ color: "#888",
397
+ fontFamily: "monospace",
398
+ flexShrink: 0,
399
+ cursor: "default"
400
+ },
401
+ children: label
402
+ }
403
+ ),
404
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
405
+ "select",
406
+ {
407
+ value,
408
+ onChange: (e) => onChange(e.target.value),
409
+ style: {
410
+ fontSize: "11px",
411
+ border: "1px solid #e0e0e0",
412
+ background: "#fafafa",
413
+ padding: "2px 4px",
414
+ cursor: "pointer",
415
+ minWidth: 0,
416
+ maxWidth: "130px",
417
+ color: "#444"
418
+ },
419
+ children: options.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: opt, children: opt }, opt))
420
+ }
421
+ )
422
+ ]
423
+ }
424
+ );
425
+ }
426
+ // Annotate the CommonJS export names for ESM import in node:
427
+ 0 && (module.exports = {
428
+ FontSwitcher
429
+ });
@@ -0,0 +1,51 @@
1
+ import * as react from 'react';
2
+
3
+ interface FontFeature {
4
+ tag: string;
5
+ label: string;
6
+ enabled: boolean;
7
+ }
8
+ interface FontMetrics {
9
+ capHeight: number;
10
+ ascent: number;
11
+ descent: number;
12
+ lineGap: number;
13
+ unitsPerEm: number;
14
+ xHeight: number;
15
+ }
16
+ interface FontConfig {
17
+ key: string;
18
+ label: string;
19
+ /** Value for CSS font-family — can be a var() ref or a literal stack */
20
+ cssFamily: string;
21
+ /** Raw font metrics. If @capsizecss/core is installed, trims are computed from these. */
22
+ metrics?: FontMetrics;
23
+ /** OpenType feature settings. If omitted, auto-detected from font file by the Vite plugin. */
24
+ features?: FontFeature[];
25
+ }
26
+ interface FontSwitcherConfig {
27
+ fonts: FontConfig[];
28
+ defaultFont: string;
29
+ /** Path to CSS file scanned for @font-face and --font-* declarations. Default: 'src/styles/index.css' */
30
+ cssFile?: string;
31
+ /** localStorage key for the active font. Default: 'font-switcher-active' */
32
+ storageKey?: string;
33
+ /** URL param that reveals the panel. Default: 'fonts' */
34
+ revealParam?: string;
35
+ /** Keyboard shortcut that toggles the panel. Default: 'ctrl+shift+f' */
36
+ revealShortcut?: string;
37
+ /** CSS selector for capsize elements. Set false to disable trim injection. Default: '.capsize' */
38
+ capsizeSelector?: string | false;
39
+ /** Font size in px used for capsize trim computation. Default: 16 */
40
+ fontSize?: number;
41
+ /** When set: locks the font, disables all switcher output. Set this when client decides, then remove the package. */
42
+ finalFont?: string;
43
+ }
44
+
45
+ interface Props {
46
+ config: FontSwitcherConfig;
47
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
48
+ }
49
+ declare function FontSwitcher({ config, position }: Props): react.JSX.Element | null;
50
+
51
+ export { FontSwitcher };
@@ -0,0 +1,51 @@
1
+ import * as react from 'react';
2
+
3
+ interface FontFeature {
4
+ tag: string;
5
+ label: string;
6
+ enabled: boolean;
7
+ }
8
+ interface FontMetrics {
9
+ capHeight: number;
10
+ ascent: number;
11
+ descent: number;
12
+ lineGap: number;
13
+ unitsPerEm: number;
14
+ xHeight: number;
15
+ }
16
+ interface FontConfig {
17
+ key: string;
18
+ label: string;
19
+ /** Value for CSS font-family — can be a var() ref or a literal stack */
20
+ cssFamily: string;
21
+ /** Raw font metrics. If @capsizecss/core is installed, trims are computed from these. */
22
+ metrics?: FontMetrics;
23
+ /** OpenType feature settings. If omitted, auto-detected from font file by the Vite plugin. */
24
+ features?: FontFeature[];
25
+ }
26
+ interface FontSwitcherConfig {
27
+ fonts: FontConfig[];
28
+ defaultFont: string;
29
+ /** Path to CSS file scanned for @font-face and --font-* declarations. Default: 'src/styles/index.css' */
30
+ cssFile?: string;
31
+ /** localStorage key for the active font. Default: 'font-switcher-active' */
32
+ storageKey?: string;
33
+ /** URL param that reveals the panel. Default: 'fonts' */
34
+ revealParam?: string;
35
+ /** Keyboard shortcut that toggles the panel. Default: 'ctrl+shift+f' */
36
+ revealShortcut?: string;
37
+ /** CSS selector for capsize elements. Set false to disable trim injection. Default: '.capsize' */
38
+ capsizeSelector?: string | false;
39
+ /** Font size in px used for capsize trim computation. Default: 16 */
40
+ fontSize?: number;
41
+ /** When set: locks the font, disables all switcher output. Set this when client decides, then remove the package. */
42
+ finalFont?: string;
43
+ }
44
+
45
+ interface Props {
46
+ config: FontSwitcherConfig;
47
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
48
+ }
49
+ declare function FontSwitcher({ config, position }: Props): react.JSX.Element | null;
50
+
51
+ export { FontSwitcher };