type-tester-tdf 2.0.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/dist/index.cjs ADDED
@@ -0,0 +1,609 @@
1
+ 'use strict';
2
+
3
+ // src/core/dom.ts
4
+ function el(tag, props = {}) {
5
+ const node = document.createElement(tag);
6
+ if (props.class) node.className = props.class;
7
+ if (props.text != null) node.textContent = props.text;
8
+ if (props.attrs) {
9
+ for (const [name, value] of Object.entries(props.attrs)) {
10
+ if (value == null || value === false) continue;
11
+ node.setAttribute(name, value === true ? "" : String(value));
12
+ }
13
+ }
14
+ if (props.children) {
15
+ for (const child of props.children) node.appendChild(child);
16
+ }
17
+ return node;
18
+ }
19
+ function clamp(value, min, max) {
20
+ return Math.min(max, Math.max(min, value));
21
+ }
22
+ function toNumber(value, fallback) {
23
+ if (value == null || value === "") return fallback;
24
+ const n = typeof value === "number" ? value : Number(value);
25
+ return Number.isFinite(n) ? n : fallback;
26
+ }
27
+
28
+ // src/core/fit.ts
29
+ var REFERENCE_SIZE = 100;
30
+ var MIRROR_PROPS = [
31
+ "fontFamily",
32
+ "fontWeight",
33
+ "fontStyle",
34
+ "fontStretch",
35
+ "letterSpacing",
36
+ "fontFeatureSettings",
37
+ "fontVariationSettings",
38
+ "textTransform"
39
+ ];
40
+ var Fitter = class {
41
+ /**
42
+ * @param target Element whose text is being fitted.
43
+ * @param min Minimum font-size in px.
44
+ * @param max Maximum font-size in px.
45
+ */
46
+ constructor(target, min, max) {
47
+ this.mirror = null;
48
+ this.observer = null;
49
+ this.frame = 0;
50
+ this.onFit = null;
51
+ this.destroyed = false;
52
+ // Last container width fitted against. Used to ignore ResizeObserver
53
+ // notifications caused purely by height changes (a fit grows the line
54
+ // height, which would otherwise re-trigger the observer in a loop).
55
+ this.lastWidth = -1;
56
+ this.target = target;
57
+ this.min = min;
58
+ this.max = max;
59
+ }
60
+ /**
61
+ * Starts observing the target's container and reports the fitted size via
62
+ * `onFit` whenever it should change. Re-fits once fonts have loaded.
63
+ */
64
+ start(onFit) {
65
+ this.onFit = onFit;
66
+ if (typeof ResizeObserver !== "undefined") {
67
+ const container = this.target.parentElement ?? this.target;
68
+ this.observer = new ResizeObserver(() => {
69
+ if (container.clientWidth !== this.lastWidth) this.schedule();
70
+ });
71
+ this.observer.observe(container);
72
+ }
73
+ const fonts = document.fonts;
74
+ if (fonts?.ready) {
75
+ fonts.ready.then(() => this.schedule()).catch(() => {
76
+ });
77
+ }
78
+ this.schedule();
79
+ }
80
+ /** Requests a fit on the next animation frame, coalescing rapid calls. */
81
+ schedule() {
82
+ if (this.destroyed) return;
83
+ if (typeof requestAnimationFrame === "undefined") {
84
+ this.fit();
85
+ return;
86
+ }
87
+ if (this.frame) return;
88
+ this.frame = requestAnimationFrame(() => {
89
+ this.frame = 0;
90
+ this.fit();
91
+ });
92
+ }
93
+ /** Measures the text and reports the fitted size. */
94
+ fit() {
95
+ if (this.destroyed || !this.onFit) return;
96
+ const container = this.target.parentElement ?? this.target;
97
+ const width = container.clientWidth;
98
+ if (width <= 0) return;
99
+ const mirror = this.ensureMirror();
100
+ const computed = getComputedStyle(this.target);
101
+ for (const prop of MIRROR_PROPS) {
102
+ mirror.style[prop] = computed[prop];
103
+ }
104
+ mirror.style.fontSize = `${REFERENCE_SIZE}px`;
105
+ mirror.textContent = this.target.textContent ?? "";
106
+ const measured = mirror.getBoundingClientRect().width;
107
+ if (measured <= 0) return;
108
+ this.lastWidth = width;
109
+ const size = clamp(REFERENCE_SIZE * width / measured, this.min, this.max);
110
+ this.onFit(Math.round(size * 100) / 100);
111
+ }
112
+ /** Lazily creates the shared offscreen measurement mirror. */
113
+ ensureMirror() {
114
+ if (this.mirror) return this.mirror;
115
+ const mirror = document.createElement("span");
116
+ mirror.setAttribute("aria-hidden", "true");
117
+ Object.assign(mirror.style, {
118
+ position: "absolute",
119
+ left: "-9999px",
120
+ top: "0",
121
+ visibility: "hidden",
122
+ whiteSpace: "nowrap",
123
+ pointerEvents: "none"
124
+ });
125
+ document.body.appendChild(mirror);
126
+ this.mirror = mirror;
127
+ return mirror;
128
+ }
129
+ /** Stops observing and removes the mirror. */
130
+ destroy() {
131
+ this.destroyed = true;
132
+ if (this.frame) cancelAnimationFrame(this.frame);
133
+ this.observer?.disconnect();
134
+ this.observer = null;
135
+ this.mirror?.remove();
136
+ this.mirror = null;
137
+ this.onFit = null;
138
+ }
139
+ };
140
+
141
+ // src/core/opentype.ts
142
+ var FEATURES = [
143
+ // Ligatures
144
+ { tag: "liga", label: "Standard Ligatures", group: "Ligatures" },
145
+ { tag: "dlig", label: "Discretionary Ligatures", group: "Ligatures" },
146
+ { tag: "hlig", label: "Historical Ligatures", group: "Ligatures" },
147
+ { tag: "clig", label: "Contextual Ligatures", group: "Ligatures" },
148
+ // Letter Case
149
+ { tag: "smcp", label: "Small Capitals", group: "Letter Case" },
150
+ { tag: "c2sc", label: "Capitals to Small Capitals", group: "Letter Case" },
151
+ { tag: "case", label: "Case-Sensitive Forms", group: "Letter Case" },
152
+ { tag: "cpsp", label: "Capital Spacing", group: "Letter Case" },
153
+ // Figures
154
+ { tag: "lnum", label: "Lining Figures", group: "Figures" },
155
+ { tag: "onum", label: "Oldstyle Figures", group: "Figures" },
156
+ { tag: "pnum", label: "Proportional Figures", group: "Figures" },
157
+ { tag: "tnum", label: "Tabular Figures", group: "Figures" },
158
+ { tag: "zero", label: "Slashed Zero", group: "Figures" },
159
+ { tag: "ordn", label: "Ordinals", group: "Figures" },
160
+ // Fractions
161
+ { tag: "frac", label: "Fractions", group: "Fractions" },
162
+ { tag: "afrc", label: "Alternative Fractions", group: "Fractions" },
163
+ // Alternates
164
+ { tag: "swsh", label: "Swash", group: "Alternates" },
165
+ { tag: "calt", label: "Contextual Alternates", group: "Alternates" },
166
+ { tag: "salt", label: "Stylistic Alternates", group: "Alternates" },
167
+ { tag: "hist", label: "Historical Forms", group: "Alternates" },
168
+ { tag: "nalt", label: "Alternate Annotation Forms", group: "Alternates" },
169
+ // Position
170
+ { tag: "sups", label: "Superscript", group: "Position" },
171
+ { tag: "subs", label: "Subscript", group: "Position" },
172
+ // Stylistic Sets ss01–ss20
173
+ ...Array.from({ length: 20 }, (_, i) => {
174
+ const n = i + 1;
175
+ const tag = `ss${String(n).padStart(2, "0")}`;
176
+ return { tag, label: `Stylistic Set ${n}`, group: "Stylistic Sets" };
177
+ })
178
+ ];
179
+ var FEATURE_BY_TAG = new Map(
180
+ FEATURES.map((f) => [f.tag, f])
181
+ );
182
+ function featureLabel(tag) {
183
+ return FEATURE_BY_TAG.get(tag)?.label ?? tag.toUpperCase();
184
+ }
185
+ function isKnownFeature(tag) {
186
+ return FEATURE_BY_TAG.has(tag);
187
+ }
188
+ function featureSettings(active) {
189
+ const tags = Array.from(active).filter(isKnownFeature);
190
+ if (tags.length === 0) return "normal";
191
+ return tags.map((tag) => `"${tag}" 1`).join(", ");
192
+ }
193
+
194
+ // src/core/typeTester.ts
195
+ var DEFAULT_RANGES = {
196
+ size: { min: 8, max: 300, step: 1 },
197
+ tracking: { min: -0.1, max: 0.5, step: 5e-3 },
198
+ weight: { min: 100, max: 900, step: 100 }
199
+ };
200
+ var ALIGNS = ["left", "center", "right"];
201
+ var idCounter = 0;
202
+ function nextId() {
203
+ return ++idCounter;
204
+ }
205
+ function resolveRange(value, fallback) {
206
+ if (!value) return null;
207
+ if (value === true) return { ...fallback };
208
+ return { min: value.min, max: value.max, step: value.step ?? fallback.step };
209
+ }
210
+ var TypeTester = class {
211
+ /**
212
+ * @param host Element to render the tester into.
213
+ * @param options Configuration; all fields optional.
214
+ */
215
+ constructor(host, options = {}) {
216
+ this.activeFeatures = /* @__PURE__ */ new Set();
217
+ this.cleanups = [];
218
+ this.sizeOutput = null;
219
+ this.fitter = null;
220
+ this.host = host;
221
+ this.options = options;
222
+ this.controls = options.controls ?? {};
223
+ const fit = options.size === "fit";
224
+ for (const tag of options.features ?? []) {
225
+ if (isKnownFeature(tag)) this.activeFeatures.add(tag);
226
+ }
227
+ this.state = {
228
+ text: options.text ?? "",
229
+ size: fit ? 0 : toNumber(options.size, 80),
230
+ fit,
231
+ tracking: toNumber(options.tracking, 0),
232
+ weight: toNumber(options.weight, 400),
233
+ italic: options.italic ?? false,
234
+ align: options.align ?? "left",
235
+ wrap: options.wrap ?? true,
236
+ features: Array.from(this.activeFeatures)
237
+ };
238
+ this.render();
239
+ }
240
+ /** Returns a snapshot of the current state. */
241
+ getState() {
242
+ return { ...this.state, features: Array.from(this.activeFeatures) };
243
+ }
244
+ /** Removes all DOM, listeners and observers created by this instance. */
245
+ destroy() {
246
+ this.fitter?.destroy();
247
+ this.fitter = null;
248
+ for (const off of this.cleanups.splice(0)) off();
249
+ this.host.replaceChildren();
250
+ }
251
+ // ---- rendering -------------------------------------------------------
252
+ render() {
253
+ this.host.classList.add("tt");
254
+ this.textEl = el("div", {
255
+ class: "tt__text",
256
+ text: this.state.text,
257
+ attrs: {
258
+ role: "textbox",
259
+ "aria-label": this.options.ariaLabel ?? "Sample text",
260
+ "aria-multiline": String(this.state.wrap),
261
+ contenteditable: this.options.editable !== false ? "true" : null,
262
+ spellcheck: "true",
263
+ "data-placeholder": this.options.placeholder ?? "Type to test\u2026"
264
+ }
265
+ });
266
+ this.typeEl = el("div", { class: "tt__type", children: [this.textEl] });
267
+ const stage = el("div", { class: "tt__stage", children: [this.typeEl] });
268
+ this.liveEl = el("div", {
269
+ class: "tt__sr-only",
270
+ attrs: { "aria-live": "polite", "aria-atomic": "true" }
271
+ });
272
+ const controls = this.buildControls();
273
+ const children = [stage];
274
+ if (controls) children.push(controls);
275
+ children.push(this.liveEl);
276
+ this.host.replaceChildren(...children);
277
+ if (this.options.editable !== false) this.wireEditable();
278
+ this.applyStyles();
279
+ this.setupFit();
280
+ }
281
+ buildControls() {
282
+ const items = [];
283
+ const sizeRange = resolveRange(this.controls.size, DEFAULT_RANGES.size);
284
+ if (sizeRange && !this.state.fit) items.push(this.buildSize(sizeRange));
285
+ const trackingRange = resolveRange(this.controls.tracking, DEFAULT_RANGES.tracking);
286
+ if (trackingRange) items.push(this.buildTracking(trackingRange));
287
+ const weightRange = resolveRange(
288
+ this.controls.weight,
289
+ this.options.variable?.wght ?? DEFAULT_RANGES.weight
290
+ );
291
+ if (weightRange) items.push(this.buildWeight(weightRange));
292
+ if (this.controls.italic) items.push(this.buildItalic());
293
+ if (this.controls.align) items.push(this.buildAlign());
294
+ if (this.controls.wrap) items.push(this.buildWrap());
295
+ if (this.controls.features) items.push(this.buildFeatures());
296
+ if (items.length === 0) return null;
297
+ return el("div", {
298
+ class: "tt__controls",
299
+ attrs: { role: "group", "aria-label": "Typography controls" },
300
+ children: items
301
+ });
302
+ }
303
+ // ---- generic control builders (no per-control copy-paste) -------------
304
+ buildSlider(key, label, range, value, unit, onInput) {
305
+ const output = el("output", { class: "tt__value", text: `${value}${unit}` });
306
+ const input = el("input", {
307
+ class: `tt__slider tt__slider--${key}`,
308
+ attrs: {
309
+ type: "range",
310
+ min: range.min,
311
+ max: range.max,
312
+ step: range.step ?? 1,
313
+ value,
314
+ "aria-label": label
315
+ }
316
+ });
317
+ const handler = () => {
318
+ const v = Number(input.value);
319
+ output.textContent = `${v}${unit}`;
320
+ onInput(v);
321
+ this.announce(`${label} ${v}${unit}`);
322
+ };
323
+ input.addEventListener("input", handler);
324
+ this.cleanups.push(() => input.removeEventListener("input", handler));
325
+ if (key === "size") this.sizeOutput = output;
326
+ return el("label", {
327
+ class: `tt__control tt__control--${key}`,
328
+ children: [el("span", { class: "tt__label", text: label }), input, output]
329
+ });
330
+ }
331
+ buildSize(range) {
332
+ this.state.size = clamp(this.state.size, range.min, range.max);
333
+ return this.buildSlider("size", "Size", range, this.state.size, "px", (v) => {
334
+ this.state.size = v;
335
+ this.applyStyles();
336
+ this.emitChange();
337
+ });
338
+ }
339
+ buildTracking(range) {
340
+ this.state.tracking = clamp(this.state.tracking, range.min, range.max);
341
+ return this.buildSlider(
342
+ "tracking",
343
+ "Tracking",
344
+ range,
345
+ this.state.tracking,
346
+ "em",
347
+ (v) => {
348
+ this.state.tracking = v;
349
+ this.applyStyles();
350
+ this.emitChange();
351
+ }
352
+ );
353
+ }
354
+ buildWeight(range) {
355
+ this.state.weight = clamp(this.state.weight, range.min, range.max);
356
+ return this.buildSlider("weight", "Weight", range, this.state.weight, "", (v) => {
357
+ this.state.weight = v;
358
+ this.applyStyles();
359
+ this.emitChange();
360
+ });
361
+ }
362
+ buildToggle(key, label, pressed, onToggle) {
363
+ const button = el("button", {
364
+ class: `tt__toggle tt__toggle--${key}`,
365
+ text: label,
366
+ // aria-pressed is a string-valued ARIA state and must always be present
367
+ // ("false"), unlike boolean HTML attributes which are omitted when off.
368
+ attrs: { type: "button", "aria-pressed": String(pressed) }
369
+ });
370
+ const handler = () => {
371
+ const next = button.getAttribute("aria-pressed") !== "true";
372
+ button.setAttribute("aria-pressed", String(next));
373
+ onToggle(next);
374
+ this.announce(`${label} ${next ? "on" : "off"}`);
375
+ };
376
+ button.addEventListener("click", handler);
377
+ this.cleanups.push(() => button.removeEventListener("click", handler));
378
+ return button;
379
+ }
380
+ buildItalic() {
381
+ const button = this.buildToggle("italic", "Italic", this.state.italic, (on) => {
382
+ this.state.italic = on;
383
+ this.applyStyles();
384
+ this.emitChange();
385
+ });
386
+ return el("div", { class: "tt__control", children: [button] });
387
+ }
388
+ buildWrap() {
389
+ const button = this.buildToggle("wrap", "Wrap", this.state.wrap, (on) => {
390
+ this.state.wrap = on;
391
+ this.textEl.setAttribute("aria-multiline", String(on));
392
+ this.applyStyles();
393
+ this.emitChange();
394
+ });
395
+ return el("div", { class: "tt__control", children: [button] });
396
+ }
397
+ buildAlign() {
398
+ const select = el("select", {
399
+ class: "tt__select tt__select--align",
400
+ attrs: { "aria-label": "Alignment" },
401
+ children: ALIGNS.map(
402
+ (a) => el("option", {
403
+ text: a.charAt(0).toUpperCase() + a.slice(1),
404
+ attrs: { value: a, selected: a === this.state.align }
405
+ })
406
+ )
407
+ });
408
+ const handler = () => {
409
+ const value = select.value;
410
+ if (ALIGNS.includes(value)) {
411
+ this.state.align = value;
412
+ this.applyStyles();
413
+ this.emitChange();
414
+ this.announce(`Alignment ${value}`);
415
+ }
416
+ };
417
+ select.addEventListener("change", handler);
418
+ this.cleanups.push(() => select.removeEventListener("change", handler));
419
+ return el("label", {
420
+ class: "tt__control tt__control--align",
421
+ children: [el("span", { class: "tt__label", text: "Align" }), select]
422
+ });
423
+ }
424
+ buildFeatures() {
425
+ const offered = Array.isArray(this.controls.features) ? this.controls.features.filter(isKnownFeature).map((tag) => FEATURE_BY_TAG.get(tag) ?? { tag, label: featureLabel(tag), group: "Alternates" }) : FEATURES;
426
+ const panelId = `tt-feat-${nextId()}`;
427
+ const toggle = el("button", {
428
+ class: "tt__toggle tt__toggle--features",
429
+ text: "Features",
430
+ attrs: {
431
+ type: "button",
432
+ "aria-haspopup": "true",
433
+ "aria-expanded": "false",
434
+ "aria-controls": panelId
435
+ }
436
+ });
437
+ const checks = offered.map((f) => {
438
+ const input = el("input", {
439
+ attrs: { type: "checkbox", value: f.tag, checked: this.activeFeatures.has(f.tag) }
440
+ });
441
+ const onChange = () => this.toggleFeature(f.tag, input.checked);
442
+ input.addEventListener("change", onChange);
443
+ this.cleanups.push(() => input.removeEventListener("change", onChange));
444
+ return el("label", {
445
+ class: "tt__feature",
446
+ children: [input, el("span", { text: f.label })]
447
+ });
448
+ });
449
+ const panel = el("div", {
450
+ class: "tt__panel",
451
+ attrs: { id: panelId, role: "group", "aria-label": "OpenType features", hidden: true },
452
+ children: checks
453
+ });
454
+ const wrapper = el("div", {
455
+ class: "tt__control tt__control--features",
456
+ children: [toggle, panel]
457
+ });
458
+ const setOpen = (open) => {
459
+ toggle.setAttribute("aria-expanded", String(open));
460
+ panel.toggleAttribute("hidden", !open);
461
+ if (open) {
462
+ const first = panel.querySelector("input");
463
+ first?.focus();
464
+ }
465
+ };
466
+ const onToggleClick = () => setOpen(toggle.getAttribute("aria-expanded") !== "true");
467
+ const onKeydown = (e) => {
468
+ if (e.key === "Escape" && toggle.getAttribute("aria-expanded") === "true") {
469
+ setOpen(false);
470
+ toggle.focus();
471
+ }
472
+ };
473
+ const onOutside = (e) => {
474
+ if (toggle.getAttribute("aria-expanded") !== "true") return;
475
+ if (!wrapper.contains(e.target)) setOpen(false);
476
+ };
477
+ toggle.addEventListener("click", onToggleClick);
478
+ document.addEventListener("keydown", onKeydown);
479
+ document.addEventListener("click", onOutside);
480
+ this.cleanups.push(() => {
481
+ toggle.removeEventListener("click", onToggleClick);
482
+ document.removeEventListener("keydown", onKeydown);
483
+ document.removeEventListener("click", onOutside);
484
+ });
485
+ return wrapper;
486
+ }
487
+ toggleFeature(tag, on) {
488
+ if (on) this.activeFeatures.add(tag);
489
+ else this.activeFeatures.delete(tag);
490
+ this.state.features = Array.from(this.activeFeatures);
491
+ this.applyStyles();
492
+ this.emitChange();
493
+ this.announce(`${featureLabel(tag)} ${on ? "on" : "off"}`);
494
+ }
495
+ // ---- behaviour -------------------------------------------------------
496
+ wireEditable() {
497
+ const handler = () => {
498
+ this.state.text = this.textEl.textContent ?? "";
499
+ if (this.state.fit) this.fitter?.schedule();
500
+ this.emitChange();
501
+ };
502
+ this.textEl.addEventListener("input", handler);
503
+ this.cleanups.push(() => this.textEl.removeEventListener("input", handler));
504
+ }
505
+ setupFit() {
506
+ if (!this.state.fit) return;
507
+ const range = resolveRange(this.controls.size, DEFAULT_RANGES.size) ?? DEFAULT_RANGES.size;
508
+ this.fitter = new Fitter(this.textEl, range.min, range.max);
509
+ this.fitter.start((size) => {
510
+ this.state.size = size;
511
+ this.typeEl.style.fontSize = `${size}px`;
512
+ if (this.sizeOutput) this.sizeOutput.textContent = `${size}px`;
513
+ });
514
+ }
515
+ /** Applies the full typographic state to the type element via element.style. */
516
+ applyStyles() {
517
+ const s = this.typeEl.style;
518
+ const family = this.options.fontFamily ? `"${this.options.fontFamily}"${this.options.fallback ? `, ${this.options.fallback}` : ", sans-serif"}` : this.options.fallback ?? "sans-serif";
519
+ s.fontFamily = family;
520
+ if (!this.state.fit) s.fontSize = `${this.state.size}px`;
521
+ s.letterSpacing = `${this.state.tracking}em`;
522
+ s.fontStyle = this.state.italic ? "italic" : "normal";
523
+ s.textAlign = this.state.align;
524
+ this.textEl.style.whiteSpace = this.state.wrap ? "normal" : "nowrap";
525
+ if (this.options.variable?.wght) {
526
+ s.fontVariationSettings = `"wght" ${this.state.weight}`;
527
+ }
528
+ s.fontWeight = String(this.state.weight);
529
+ const settings = featureSettings(this.activeFeatures);
530
+ s.fontFeatureSettings = settings;
531
+ s.setProperty("-webkit-font-feature-settings", settings);
532
+ }
533
+ announce(message) {
534
+ this.liveEl.textContent = message;
535
+ }
536
+ emitChange() {
537
+ this.options.onChange?.(this.getState());
538
+ }
539
+ };
540
+
541
+ // src/index.ts
542
+ function parseList(value) {
543
+ if (!value) return [];
544
+ return value.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
545
+ }
546
+ function parseControls(value) {
547
+ const keys = parseList(value);
548
+ const config = {};
549
+ for (const key of keys) {
550
+ if (key === "size") config.size = true;
551
+ else if (key === "tracking") config.tracking = true;
552
+ else if (key === "weight") config.weight = true;
553
+ else if (key === "italic") config.italic = true;
554
+ else if (key === "align") config.align = true;
555
+ else if (key === "wrap") config.wrap = true;
556
+ else if (key === "features") config.features = true;
557
+ }
558
+ return config;
559
+ }
560
+ function optionsFromDataset(host) {
561
+ const d = host.dataset;
562
+ const sizeAttr = d.size;
563
+ let size;
564
+ if (sizeAttr === "fit") size = "fit";
565
+ else if (sizeAttr != null && sizeAttr !== "") size = Number(sizeAttr);
566
+ const align = d.align;
567
+ const features = parseList(d.features ?? null).filter(
568
+ (t) => isKnownFeature(t)
569
+ );
570
+ return {
571
+ text: d.text ?? host.textContent?.trim() ?? "",
572
+ fontFamily: d.font,
573
+ fallback: d.fallback,
574
+ size,
575
+ tracking: d.tracking != null ? Number(d.tracking) : void 0,
576
+ weight: d.weight != null ? Number(d.weight) : void 0,
577
+ italic: d.italic != null ? d.italic !== "false" : void 0,
578
+ align: align && ["left", "center", "right"].includes(align) ? align : void 0,
579
+ wrap: d.wrap != null ? d.wrap !== "false" : void 0,
580
+ features: features.length ? features : void 0,
581
+ editable: d.editable !== "false",
582
+ placeholder: d.placeholder,
583
+ ariaLabel: d.ariaLabel,
584
+ controls: parseControls(d.controls ?? null)
585
+ };
586
+ }
587
+ function createFromElement(host) {
588
+ return new TypeTester(host, optionsFromDataset(host));
589
+ }
590
+ function autoInit(root = document) {
591
+ const hosts = Array.from(
592
+ root.querySelectorAll("[data-type-tester]")
593
+ ).filter((host) => host.dataset.ttReady !== "true");
594
+ return hosts.map((host) => {
595
+ host.dataset.ttReady = "true";
596
+ return createFromElement(host);
597
+ });
598
+ }
599
+
600
+ exports.FEATURES = FEATURES;
601
+ exports.FEATURE_BY_TAG = FEATURE_BY_TAG;
602
+ exports.TypeTester = TypeTester;
603
+ exports.autoInit = autoInit;
604
+ exports.createFromElement = createFromElement;
605
+ exports.featureLabel = featureLabel;
606
+ exports.featureSettings = featureSettings;
607
+ exports.isKnownFeature = isKnownFeature;
608
+ //# sourceMappingURL=index.cjs.map
609
+ //# sourceMappingURL=index.cjs.map