typography-controller 1.1.0 → 1.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.
@@ -0,0 +1,113 @@
1
+ :host {
2
+ display: block;
3
+ font-family: system-ui, sans-serif;
4
+ max-width: 420px;
5
+ padding: 18px 20px;
6
+ border-radius: 18px;
7
+ background: rgba(255, 255, 255, 0.65);
8
+ border: 1px solid rgba(0, 0, 0, 0.08);
9
+ backdrop-filter: blur(14px);
10
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
11
+ color: #0f172a;
12
+ }
13
+
14
+ .header {
15
+ display: flex;
16
+ justify-content: space-between;
17
+ align-items: baseline;
18
+ margin-bottom: 14px;
19
+ }
20
+
21
+ .title {
22
+ font-size: 14px;
23
+ font-weight: 600;
24
+ letter-spacing: 0.08em;
25
+ text-transform: uppercase;
26
+ color: #475569;
27
+ }
28
+
29
+ .badge {
30
+ font-size: 11px;
31
+ padding: 3px 8px;
32
+ border-radius: 999px;
33
+ background: rgba(59, 130, 246, 0.12);
34
+ color: #1d4ed8;
35
+ border: 1px solid rgba(59, 130, 246, 0.35);
36
+ }
37
+
38
+ .group {
39
+ margin-bottom: 14px;
40
+ }
41
+
42
+ .label-row {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ align-items: center;
46
+ margin-bottom: 6px;
47
+ }
48
+
49
+ label {
50
+ font-size: 12px;
51
+ font-weight: 600;
52
+ color: #0f172a;
53
+ }
54
+
55
+ .value {
56
+ font-size: 11px;
57
+ color: #64748b;
58
+ }
59
+
60
+ input[type="range"] {
61
+ -webkit-appearance: none;
62
+ width: 100%;
63
+ height: 6px;
64
+ border-radius: 999px;
65
+ background: linear-gradient(90deg, #3b82f6, #60a5fa);
66
+ outline: none;
67
+ }
68
+
69
+ input[type="range"]::-webkit-slider-thumb {
70
+ -webkit-appearance: none;
71
+ height: 18px;
72
+ width: 18px;
73
+ border-radius: 50%;
74
+ background: white;
75
+ border: 1px solid rgba(0, 0, 0, 0.15);
76
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
77
+ cursor: pointer;
78
+ }
79
+
80
+ select {
81
+ width: 100%;
82
+ padding: 6px 8px;
83
+ border-radius: 8px;
84
+ border: 1px solid rgba(148, 163, 184, 0.7);
85
+ background: rgba(255, 255, 255, 0.9);
86
+ font-size: 12px;
87
+ color: #0f172a;
88
+ outline: none;
89
+ }
90
+
91
+ select:focus {
92
+ outline: none;
93
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
94
+ border-radius: 999px;
95
+ }
96
+
97
+ input[type="range"]:focus {
98
+ outline: none;
99
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
100
+ border-radius: 999px;
101
+ }
102
+
103
+ input[type="range"]:focus::-webkit-slider-thumb {
104
+ box-shadow:
105
+ 0 0 0 4px rgba(37, 99, 235, 0.35),
106
+ 0 4px 10px rgba(0, 0, 0, 0.25);
107
+ border-color: #2563eb;
108
+ }
109
+
110
+ input[type="range"]:hover::-webkit-slider-thumb {
111
+ transform: scale(1.05);
112
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
113
+ }
@@ -0,0 +1,69 @@
1
+ <div class="header">
2
+ <h2 class="title" id="controllerTitle">Typography controls</h2>
3
+ <span class="badge">v1.3.0</span>
4
+ </div>
5
+
6
+ <div aria-live="polite" class="sr-only" id="liveRegion"></div>
7
+
8
+ <div class="group" id="groupFontSize" role="group" aria-labelledby="labelFontSize">
9
+ <div class="label-row">
10
+ <label id="labelFontSize" for="fontSize">Font size</label>
11
+ <span class="value" id="fontSizeValue" aria-hidden="true"></span>
12
+ </div>
13
+ <input type="range" id="fontSize" min="12" max="60" value="24"
14
+ aria-valuemin="12" aria-valuemax="60" aria-valuenow="24"
15
+ aria-labelledby="labelFontSize">
16
+ </div>
17
+
18
+ <div class="group" id="groupLetterSpacing" role="group" aria-labelledby="labelLetterSpacing">
19
+ <div class="label-row">
20
+ <label id="labelLetterSpacing" for="letterSpacing">Letter spacing</label>
21
+ <span class="value" id="letterSpacingValue" aria-hidden="true"></span>
22
+ </div>
23
+ <input type="range" id="letterSpacing" min="0" max="20" value="0"
24
+ aria-valuemin="0" aria-valuemax="20" aria-valuenow="0"
25
+ aria-labelledby="labelLetterSpacing">
26
+ </div>
27
+
28
+ <div class="group" id="groupWordSpacing" role="group" aria-labelledby="labelWordSpacing">
29
+ <div class="label-row">
30
+ <label id="labelWordSpacing" for="wordSpacing">Word spacing</label>
31
+ <span class="value" id="wordSpacingValue" aria-hidden="true"></span>
32
+ </div>
33
+ <input type="range" id="wordSpacing" min="0" max="40" value="0"
34
+ aria-valuemin="0" aria-valuemax="40" aria-valuenow="0"
35
+ aria-labelledby="labelWordSpacing">
36
+ </div>
37
+
38
+ <div class="group" id="groupLineHeight" role="group" aria-labelledby="labelLineHeight">
39
+ <div class="label-row">
40
+ <label id="labelLineHeight" for="lineHeight">Line height</label>
41
+ <span class="value" id="lineHeightValue" aria-hidden="true"></span>
42
+ </div>
43
+ <input type="range" id="lineHeight" min="1" max="3" step="0.1" value="1.5"
44
+ aria-valuemin="1" aria-valuemax="3" aria-valuenow="1.5"
45
+ aria-labelledby="labelLineHeight">
46
+ </div>
47
+
48
+ <div class="group" id="groupContrast" role="group" aria-labelledby="labelContrast">
49
+ <div class="label-row">
50
+ <label id="labelContrast" for="contrast">Contrast</label>
51
+ <span class="value" id="contrastValue" aria-hidden="true"></span>
52
+ </div>
53
+ <input type="range" id="contrast" min="50" max="200" value="100"
54
+ aria-valuemin="50" aria-valuemax="200" aria-valuenow="100"
55
+ aria-labelledby="labelContrast">
56
+ </div>
57
+
58
+ <div class="group" id="groupFontFamily" role="group" aria-labelledby="labelFontFamily">
59
+ <div class="label-row">
60
+ <label id="labelFontFamily" for="fontFamily">Font family</label>
61
+ </div>
62
+ <select id="fontFamily" aria-labelledby="labelFontFamily">
63
+ <option value="system-ui, sans-serif">System</option>
64
+ <option value="Georgia, serif">Serif</option>
65
+ <option value="'Courier New', monospace">Monospace</option>
66
+ <option value="Verdana, sans-serif">Verdana</option>
67
+ <option value="inherit">Inherit</option>
68
+ </select>
69
+ </div>
@@ -1,286 +1,63 @@
1
- import { TypographyState } from "./TypographyState.js";
2
-
3
1
  class TypographyController extends HTMLElement {
4
2
  constructor() {
5
3
  super();
6
4
  this.attachShadow({ mode: "open" });
5
+ }
6
+
7
+ async connectedCallback() {
8
+ // 1. Load external HTML + CSS
9
+ const [html, css] = await Promise.all([
10
+ fetch("../src//typography-controller.html").then(r => r.text()),
11
+ fetch("../src/typography-controller.css").then(r => r.text())
12
+ ]);
7
13
 
14
+ // 2. Inject into shadow DOM
8
15
  this.shadowRoot.innerHTML = `
9
- <style>
10
- :host {
11
- display: block;
12
- font-family: system-ui, sans-serif;
13
- max-width: 420px;
14
- padding: 18px 20px;
15
- border-radius: 18px;
16
- background: rgba(255, 255, 255, 0.65);
17
- border: 1px solid rgba(0, 0, 0, 0.08);
18
- backdrop-filter: blur(14px);
19
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
20
- color: #0f172a;
21
- }
22
-
23
- .header {
24
- display: flex;
25
- justify-content: space-between;
26
- align-items: baseline;
27
- margin-bottom: 14px;
28
- }
29
-
30
- .title {
31
- font-size: 14px;
32
- font-weight: 600;
33
- letter-spacing: 0.08em;
34
- text-transform: uppercase;
35
- color: #475569;
36
- }
37
-
38
- .badge {
39
- font-size: 11px;
40
- padding: 3px 8px;
41
- border-radius: 999px;
42
- background: rgba(59, 130, 246, 0.12);
43
- color: #1d4ed8;
44
- border: 1px solid rgba(59, 130, 246, 0.35);
45
- }
46
-
47
- .group {
48
- margin-bottom: 14px;
49
- }
50
-
51
- .label-row {
52
- display: flex;
53
- justify-content: space-between;
54
- align-items: center;
55
- margin-bottom: 6px;
56
- }
57
-
58
- label {
59
- font-size: 12px;
60
- font-weight: 600;
61
- color: #0f172a;
62
- }
63
-
64
- .value {
65
- font-size: 11px;
66
- color: #64748b;
67
- }
68
-
69
- input[type="range"] {
70
- -webkit-appearance: none;
71
- width: 100%;
72
- height: 6px;
73
- border-radius: 999px;
74
- background: linear-gradient(90deg, #3b82f6, #60a5fa);
75
- outline: none;
76
- }
77
-
78
- input[type="range"]::-webkit-slider-thumb {
79
- -webkit-appearance: none;
80
- height: 18px;
81
- width: 18px;
82
- border-radius: 50%;
83
- background: white;
84
- border: 1px solid rgba(0, 0, 0, 0.15);
85
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
86
- cursor: pointer;
87
- }
88
-
89
- select {
90
- width: 100%;
91
- padding: 6px 8px;
92
- border-radius: 8px;
93
- border: 1px solid rgba(148, 163, 184, 0.7);
94
- background: rgba(255, 255, 255, 0.9);
95
- font-size: 12px;
96
- color: #0f172a;
97
- outline: none;
98
- }
99
-
100
- select:focus {
101
- outline: none;
102
- box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
103
- border-radius: 999px;
104
- }
105
-
106
- /* Highlight the track when focused */
107
- input[type="range"]:focus {
108
- outline: none;
109
- box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
110
- border-radius: 999px;
111
- }
112
-
113
- /* Glow effect on the thumb when focused */
114
- input[type="range"]:focus::-webkit-slider-thumb {
115
- box-shadow:
116
- 0 0 0 4px rgba(37, 99, 235, 0.35),
117
- 0 4px 10px rgba(0, 0, 0, 0.25);
118
- border-color: #2563eb;
119
- }
120
-
121
- /* Firefox thumb focus */
122
- input[type="range"]::-moz-range-thumb:focus {
123
- box-shadow:
124
- 0 0 0 4px rgba(37, 99, 235, 0.35),
125
- 0 4px 10px rgba(0, 0, 0, 0.25);
126
- border-color: #2563eb;
127
- }
128
- input[type="range"]:hover::-webkit-slider-thumb {
129
- transform: scale(1.05);
130
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
131
- }
132
- </style>
133
-
134
- <div class="header">
135
- <h2 class="title" id="controllerTitle">Typography Controls</h2>
136
- <span class="badge">v1.0.0</span>
137
- </div>
138
-
139
- <!-- Live region for announcements -->
140
- <div aria-live="polite" class="sr-only" id="liveRegion"></div>
141
-
142
- <div class="group" id="groupFontSize" role="group" aria-labelledby="labelFontSize">
143
- <div class="label-row">
144
- <label id="labelFontSize" for="fontSize">Font size</label>
145
- <span class="value" id="fontSizeValue" aria-hidden="true"></span>
146
- </div>
147
-
148
- <input
149
- type="range"
150
- id="fontSize"
151
- min="12"
152
- max="60"
153
- value="24"
154
- aria-valuemin="12"
155
- aria-valuemax="60"
156
- aria-valuenow="24"
157
- aria-labelledby="labelFontSize"
158
- >
159
- </div>
160
-
161
- <div class="group" id="groupLetterSpacing" role="group" aria-labelledby="labelLetterSpacing">
162
- <div class="label-row">
163
- <label id="labelLetterSpacing" for="letterSpacing">Letter spacing</label>
164
- <span class="value" id="letterSpacingValue" aria-hidden="true"></span>
165
- </div>
166
-
167
- <input
168
- type="range"
169
- id="letterSpacing"
170
- min="0"
171
- max="20"
172
- value="0"
173
- aria-valuemin="0"
174
- aria-valuemax="20"
175
- aria-valuenow="0"
176
- aria-labelledby="labelLetterSpacing"
177
- >
178
- </div>
179
-
180
- <div class="group" id="groupWordSpacing" role="group" aria-labelledby="labelWordSpacing">
181
- <div class="label-row">
182
- <label id="labelWordSpacing" for="wordSpacing">Word spacing</label>
183
- <span class="value" id="wordSpacingValue" aria-hidden="true"></span>
184
- </div>
185
-
186
- <input
187
- type="range"
188
- id="wordSpacing"
189
- min="0"
190
- max="40"
191
- value="0"
192
- aria-valuemin="0"
193
- aria-valuemax="40"
194
- aria-valuenow="0"
195
- aria-labelledby="labelWordSpacing"
196
- >
197
- </div>
198
-
199
- <div class="group" id="groupLineHeight" role="group" aria-labelledby="labelLineHeight">
200
- <div class="label-row">
201
- <label id="labelLineHeight" for="lineHeight">Line height</label>
202
- <span class="value" id="lineHeightValue" aria-hidden="true"></span>
203
- </div>
204
-
205
- <input
206
- type="range"
207
- id="lineHeight"
208
- min="1"
209
- max="3"
210
- step="0.1"
211
- value="1.5"
212
- aria-valuemin="1"
213
- aria-valuemax="3"
214
- aria-valuenow="1.5"
215
- aria-labelledby="labelLineHeight"
216
- >
217
- </div>
218
-
219
- <div class="group" id="groupContrast" role="group" aria-labelledby="labelContrast">
220
- <div class="label-row">
221
- <label id="labelContrast" for="contrast">Contrast</label>
222
- <span class="value" id="contrastValue" aria-hidden="true"></span>
223
- </div>
224
-
225
- <input
226
- type="range"
227
- id="contrast"
228
- min="50"
229
- max="200"
230
- value="100"
231
- aria-valuemin="50"
232
- aria-valuemax="200"
233
- aria-valuenow="100"
234
- aria-labelledby="labelContrast"
235
- >
236
- </div>
237
-
238
- <div class="group" id="groupFontFamily" role="group" aria-labelledby="labelFontFamily">
239
- <div class="label-row">
240
- <label id="labelFontFamily" for="fontFamily">Font family</label>
241
- </div>
242
-
243
- <select id="fontFamily" aria-labelledby="labelFontFamily">
244
- <option value="system-ui, sans-serif">System</option>
245
- <option value="Georgia, serif">Serif</option>
246
- <option value="'Courier New', monospace">Monospace</option>
247
- <option value="Verdana, sans-serif">Verdana</option>
248
- <option value="inherit">Inherit</option>
249
- </select>
250
- </div>
16
+ <style>${css}</style>
17
+ ${html}
251
18
  `;
252
- }
253
19
 
254
- connectedCallback() {
20
+ // 3. Now safe to query elements
255
21
  this.targetSelector = this.getAttribute("target");
256
22
 
257
- // Read attributes for initial hiding
23
+ // Feature toggles
258
24
  this.toggleGroup("#groupLetterSpacing", !this.hasAttribute("hide-letter-spacing"));
259
25
  this.toggleGroup("#groupWordSpacing", !this.hasAttribute("hide-word-spacing"));
260
26
  this.toggleGroup("#groupLineHeight", !this.hasAttribute("hide-line-height"));
261
27
  this.toggleGroup("#groupContrast", !this.hasAttribute("hide-contrast"));
28
+ this.toggleGroup("#groupFamily", !this.hasAttribute("hide-font-family"));
262
29
 
263
- // Query elements
30
+ // Query sliders
264
31
  this.fontSize = this.shadowRoot.querySelector("#fontSize");
265
32
  this.letterSpacing = this.shadowRoot.querySelector("#letterSpacing");
266
33
  this.wordSpacing = this.shadowRoot.querySelector("#wordSpacing");
267
34
  this.lineHeight = this.shadowRoot.querySelector("#lineHeight");
268
35
  this.contrast = this.shadowRoot.querySelector("#contrast");
269
36
  this.fontFamily = this.shadowRoot.querySelector("#fontFamily");
270
- // ⭐ Auto‑detect the user's current font family
37
+
38
+ // Auto-detect font family
271
39
  const target = this.targetElement;
272
40
  if (target) {
273
41
  const computed = getComputedStyle(target).fontFamily;
274
42
  this.fontFamily.value = computed;
275
43
  }
276
44
 
277
-
45
+ // Query value labels
278
46
  this.fontSizeValue = this.shadowRoot.querySelector("#fontSizeValue");
279
47
  this.letterSpacingValue = this.shadowRoot.querySelector("#letterSpacingValue");
280
48
  this.wordSpacingValue = this.shadowRoot.querySelector("#wordSpacingValue");
281
49
  this.lineHeightValue = this.shadowRoot.querySelector("#lineHeightValue");
282
50
  this.contrastValue = this.shadowRoot.querySelector("#contrastValue");
283
51
 
52
+ // 4. Bind value ranges
53
+ this.applyRange(this.fontSize, "font-size-min", "font-size-max");
54
+ this.applyRange(this.letterSpacing, "letter-spacing-min", "letter-spacing-max");
55
+ this.applyRange(this.wordSpacing, "word-spacing-min", "word-spacing-max");
56
+ this.applyRange(this.lineHeight, "line-height-min", "line-height-max");
57
+ this.applyRange(this.contrast, "contrast-min", "contrast-max");
58
+
59
+
60
+ // Event handler
284
61
  const updateAndEmit = () => {
285
62
  this.update();
286
63
  this.dispatchEvent(
@@ -292,6 +69,7 @@ class TypographyController extends HTMLElement {
292
69
  );
293
70
  };
294
71
 
72
+ // Bind events
295
73
  this.fontSize.addEventListener("input", updateAndEmit);
296
74
  this.letterSpacing.addEventListener("input", updateAndEmit);
297
75
  this.wordSpacing.addEventListener("input", updateAndEmit);
@@ -299,6 +77,14 @@ class TypographyController extends HTMLElement {
299
77
  this.contrast.addEventListener("input", updateAndEmit);
300
78
  this.fontFamily.addEventListener("change", updateAndEmit);
301
79
 
80
+ this.bindSlider(this.fontSize, this.fontSizeValue, "Font size");
81
+ this.bindSlider(this.letterSpacing, this.letterSpacingValue, "Letter spacing");
82
+ this.bindSlider(this.wordSpacing, this.wordSpacingValue, "Word spacing");
83
+ this.bindSlider(this.lineHeight, this.lineHeightValue, "Line height");
84
+ this.bindSlider(this.contrast, this.contrastValue, "Contrast");
85
+
86
+
87
+ // Initial update
302
88
  this.update();
303
89
  }
304
90
 
@@ -341,13 +127,14 @@ class TypographyController extends HTMLElement {
341
127
  }
342
128
 
343
129
  setValues(values = {}) {
344
-
345
130
  if (values.fontSize !== undefined) this.fontSize.value = values.fontSize;
346
131
  if (values.letterSpacing !== undefined) this.letterSpacing.value = values.letterSpacing;
347
132
  if (values.wordSpacing !== undefined) this.wordSpacing.value = values.wordSpacing;
348
133
  if (values.lineHeight !== undefined) this.lineHeight.value = values.lineHeight;
349
134
  if (values.contrast !== undefined) this.contrast.value = values.contrast;
135
+
350
136
  if (values.fontFamily !== undefined) {
137
+ const target = this.targetElement;
351
138
  if (values.fontFamily === "inherit" && target) {
352
139
  const computed = getComputedStyle(target).fontFamily;
353
140
  this.fontFamily.value = computed;
@@ -356,7 +143,6 @@ class TypographyController extends HTMLElement {
356
143
  }
357
144
  }
358
145
 
359
-
360
146
  this.update();
361
147
  }
362
148
 
@@ -388,7 +174,54 @@ class TypographyController extends HTMLElement {
388
174
  ? this.removeAttribute("hide-contrast")
389
175
  : this.setAttribute("hide-contrast", "");
390
176
  }
177
+ if (features.fontFamily !== undefined) {
178
+ this.toggleGroup("#groupFontFamily", features.fontFamily);
179
+ features.fontFamily
180
+ ? this.removeAttribute("hide-font-family")
181
+ : this.setAttribute("hide-font-family", "");
182
+ }
391
183
  }
184
+
185
+ bindSlider(slider, valueEl, labelText) {
186
+ // Initialize visible value
187
+ valueEl.textContent = slider.value;
188
+
189
+ slider.addEventListener("input", () => {
190
+ const val = slider.value;
191
+
192
+ // Update visible value
193
+ valueEl.textContent = val;
194
+
195
+ // Update ARIA so NVDA knows the value changed
196
+ slider.setAttribute("aria-valuenow", val);
197
+
198
+ // Emit your existing event
199
+ this.update();
200
+ this.dispatchEvent(
201
+ new CustomEvent("change", {
202
+ detail: this.getValues(),
203
+ bubbles: true,
204
+ composed: true
205
+ })
206
+ );
207
+ });
208
+ }
209
+
210
+ applyRange(slider, attrMin, attrMax) {
211
+ const min = this.getAttribute(attrMin);
212
+ const max = this.getAttribute(attrMax);
213
+
214
+ if (min !== null) {
215
+ slider.min = min;
216
+ slider.setAttribute("aria-valuemin", min);
217
+ }
218
+
219
+ if (max !== null) {
220
+ slider.max = max;
221
+ slider.setAttribute("aria-valuemax", max);
222
+ }
223
+ }
224
+
392
225
  }
393
226
 
394
- customElements.define("typography-controller", TypographyController);
227
+ customElements.define("typography-controller", TypographyController);