srcdev-nuxt-components 9.6.9 → 9.6.11
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/.claude/rules/consumer/ids-from-useid.md +4 -2
- package/.claude/skills/component-compliance-checklist.md +24 -2
- package/.claude/skills/component-inline-action-button.md +5 -4
- package/.claude/skills/component-naming.md +8 -2
- package/.claude/skills/components/{alert-mask-core.md → alert-mask.md} +38 -23
- package/.claude/skills/components/alert-masked-content.md +3 -3
- package/.claude/skills/components/container-glow.md +1 -1
- package/.claude/skills/components/display-theme-switch.md +8 -8
- package/.claude/skills/components/input-button.md +26 -7
- package/.claude/skills/components/input-checkbox-radio.md +2 -0
- package/.claude/skills/components/input-description.md +5 -0
- package/.claude/skills/components/input-error.md +9 -2
- package/.claude/skills/components/input-label.md +8 -2
- package/.claude/skills/components/input-number.md +2 -0
- package/.claude/skills/components/input-otp.md +2 -0
- package/.claude/skills/components/input-range.md +2 -0
- package/.claude/skills/components/input-select.md +2 -0
- package/.claude/skills/components/input-text.md +2 -0
- package/.claude/skills/components/input-textarea.md +2 -0
- package/.claude/skills/components/pending-effect.md +5 -4
- package/.claude/skills/components/{triple-toggle-switch.md → segmented-control.md} +34 -19
- package/.claude/skills/components/services-card.md +1 -1
- package/.claude/skills/components/tabbed-content.md +1 -1
- package/.claude/skills/components/toggle-switch.md +2 -0
- package/.claude/skills/css-nesting-conventions.md +22 -1
- package/.claude/skills/index.md +5 -5
- package/.claude/skills/storybook-add-story.md +3 -1
- package/.claude/skills/theming-component-token-pattern.md +24 -0
- package/.claude/skills/theming-form-geometry-tokens.md +1 -1
- package/.claude/skills/theming-partial-override.md +1 -1
- package/.vscode/srcdev-component-alert-mask.code-snippets +41 -0
- package/.vscode/{srcdev-component-triple-toggle-switch.code-snippets → srcdev-component-segmented-control.code-snippets} +6 -7
- package/app/assets/styles/setup/03.theming/_theme-slots.css +1 -1
- package/app/assets/styles/setup/04.elements/forms/03.generic-input-geometry.css +4 -0
- package/app/components/01.atoms/alert-mask/AlertMask.vue +194 -0
- package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +57 -42
- package/app/components/01.atoms/alert-mask/stories/AlertMask.stories.ts +198 -0
- package/app/components/01.atoms/alert-mask/tests/AlertMask.spec.ts +233 -0
- package/app/components/02.molecules/alert-masked-content/AlertMaskedContent.vue +2 -2
- package/app/components/02.molecules/alert-masked-content/CONSUMER-STYLING.md +4 -4
- package/app/components/02.molecules/alert-masked-content/stories/AlertMaskedContent.stories.ts +60 -0
- package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +12 -12
- package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +3 -3
- package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +1 -1
- package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +1 -1
- package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +9 -9
- package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +16 -16
- package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +53 -1
- package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +8 -8
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +4 -4
- package/app/components/02.molecules/input-copy/InputCopy.vue +2 -2
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +2 -2
- package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +4 -4
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +2 -2
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +2 -2
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +6 -6
- package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/form-errors/InputError.vue +43 -37
- package/app/components/05.forms/form-errors/stories/InputError.stories.ts +79 -1
- package/app/components/05.forms/form-errors/tests/InputError.spec.ts +29 -1
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +80 -47
- package/app/components/05.forms/input-button/InputButton.vue +52 -100
- package/app/components/05.forms/input-button/playwright/input-button.playwright.ts +1 -1
- package/app/components/05.forms/input-button/stories/InputButton.stories.ts +145 -8
- package/app/components/05.forms/input-button/tests/InputButton.spec.ts +76 -46
- package/app/components/05.forms/input-button/tests/__snapshots__/InputButton.spec.ts.snap +17 -17
- package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +9 -1
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +8 -0
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +2 -2
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +14 -2
- package/app/components/05.forms/input-description/InputDescription.vue +2 -1
- package/app/components/05.forms/input-description/stories/InputDescription.stories.ts +70 -4
- package/app/components/05.forms/input-description/tests/InputDescription.spec.ts +2 -1
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +10 -2
- package/app/components/05.forms/input-label/InputLabel.vue +1 -0
- package/app/components/05.forms/input-label/stories/InputLabel.stories.ts +68 -2
- package/app/components/05.forms/input-label/tests/InputLabel.spec.ts +13 -3
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-number/InputNumber.vue +4 -5
- package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-otp/InputOtp.vue +2 -2
- package/app/components/05.forms/input-range/InputRange.vue +1 -1
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-select/InputSelect.vue +14 -5
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-text/InputText.vue +4 -4
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-textarea/InputTextarea.vue +2 -3
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +4 -4
- package/app/components/05.forms/pending-effect/CONSUMER-STYLING.md +7 -3
- package/app/components/05.forms/pending-effect/PendingEffect.vue +3 -3
- package/app/components/05.forms/pending-effect/tests/PendingEffect.spec.ts +1 -1
- package/app/components/05.forms/segmented-control/CONSUMER-STYLING.md +125 -0
- package/app/components/05.forms/segmented-control/SegmentedControl.vue +231 -0
- package/app/components/05.forms/segmented-control/stories/SegmentedControl.stories.ts +215 -0
- package/app/components/05.forms/segmented-control/tests/SegmentedControl.spec.ts +202 -0
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +7 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitch.vue +2 -0
- package/app/types/components/index.ts +1 -1
- package/package.json +1 -1
- package/.vscode/srcdev-component-alert-mask-core.code-snippets +0 -31
- package/app/components/01.atoms/alert-mask/AlertMaskCore.vue +0 -166
- package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +0 -105
- package/app/components/01.atoms/alert-mask/tests/AlertMaskCore.spec.ts +0 -342
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +0 -99
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +0 -251
- package/app/components/05.forms/triple-toggle-switch/stories/TripleToggleSwitch.stories.ts +0 -104
- package/app/components/05.forms/triple-toggle-switch/tests/TripleToggleSwitch.spec.ts +0 -138
- /package/app/types/components/{alert-mask-core.d.ts → alert-mask.d.ts} +0 -0
|
@@ -9,7 +9,6 @@ interface InputButtonInstance {
|
|
|
9
9
|
hasLeftSlot: () => boolean;
|
|
10
10
|
hasRightSlot: () => boolean;
|
|
11
11
|
hasIconOnlySlot: () => boolean;
|
|
12
|
-
buttonClasses: () => unknown[];
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
// --- Helpers ---
|
|
@@ -92,12 +91,12 @@ describe("InputButton", () => {
|
|
|
92
91
|
|
|
93
92
|
it("renders button text", async () => {
|
|
94
93
|
wrapper = await createWrapper({ buttonText: "Hello" });
|
|
95
|
-
expect(wrapper.find(".button-text").text()).toBe("Hello");
|
|
94
|
+
expect(wrapper.find(".input-button-text").text()).toBe("Hello");
|
|
96
95
|
});
|
|
97
96
|
|
|
98
97
|
it("handles empty buttonText gracefully", async () => {
|
|
99
98
|
wrapper = await createWrapper({ buttonText: "" });
|
|
100
|
-
const buttonText = wrapper.find(".button-text");
|
|
99
|
+
const buttonText = wrapper.find(".input-button-text");
|
|
101
100
|
expect(buttonText.exists()).toBe(true);
|
|
102
101
|
expect(buttonText.text()).toBe("");
|
|
103
102
|
});
|
|
@@ -107,31 +106,45 @@ describe("InputButton", () => {
|
|
|
107
106
|
// Props
|
|
108
107
|
// -------------------------
|
|
109
108
|
describe("Props", () => {
|
|
110
|
-
it("
|
|
111
|
-
for (const variant of ["primary", "secondary", "tertiary"] as const) {
|
|
109
|
+
it("reflects variant as data-variant", async () => {
|
|
110
|
+
for (const variant of ["primary", "secondary", "tertiary", "inline"] as const) {
|
|
112
111
|
const w = await mountSuspended(InputButton, {
|
|
113
112
|
props: { buttonText: "Test", variant },
|
|
114
113
|
});
|
|
115
|
-
expect(w.find("button").
|
|
114
|
+
expect(w.find("button").attributes("data-variant")).toBe(variant);
|
|
115
|
+
expect(w.find("button").classes()).not.toContain(variant);
|
|
116
116
|
w.unmount();
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
it("
|
|
120
|
+
it("sets data-pill when isPill is true", async () => {
|
|
121
121
|
wrapper = await createWrapper({ isPill: true });
|
|
122
|
-
expect(wrapper.find("button").
|
|
122
|
+
expect(wrapper.find("button").attributes("data-pill")).toBe("");
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
it("does not
|
|
125
|
+
it("does not set data-pill by default", async () => {
|
|
126
126
|
wrapper = await createWrapper();
|
|
127
|
-
expect(wrapper.find("button").
|
|
127
|
+
expect(wrapper.find("button").attributes("data-pill")).toBeUndefined();
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
it("
|
|
130
|
+
it("sets data-pending and data-pending-effect", async () => {
|
|
131
131
|
wrapper = await createWrapper({ isPending: true, hasPendingEffect: true });
|
|
132
|
+
const button = wrapper.find("button");
|
|
133
|
+
expect(button.attributes("data-pending")).toBe("");
|
|
134
|
+
expect(button.attributes("data-pending-effect")).toBe("");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("does not use collision-prone bare state classes", async () => {
|
|
138
|
+
wrapper = await createWrapper(
|
|
139
|
+
{ isPill: true, isPending: true, hasPendingEffect: true, variant: "secondary" },
|
|
140
|
+
{ iconOnly: "<span>⚡</span>" }
|
|
141
|
+
);
|
|
132
142
|
const classes = wrapper.find("button").classes();
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
for (const name of ["secondary", "pill", "is-pending", "pending-effect", "icon-only"]) {
|
|
144
|
+
expect(classes).not.toContain(name);
|
|
145
|
+
}
|
|
146
|
+
expect(wrapper.find(".btn-icon").exists()).toBe(false);
|
|
147
|
+
expect(wrapper.find(".button-text").exists()).toBe(false);
|
|
135
148
|
});
|
|
136
149
|
|
|
137
150
|
it("applies correct type attribute", async () => {
|
|
@@ -168,9 +181,9 @@ describe("InputButton", () => {
|
|
|
168
181
|
// Accessibility
|
|
169
182
|
// -------------------------
|
|
170
183
|
describe("Accessibility", () => {
|
|
171
|
-
it("
|
|
184
|
+
it("omits aria-disabled when not readonly", async () => {
|
|
172
185
|
wrapper = await createWrapper();
|
|
173
|
-
expect(wrapper.find("button").attributes("aria-disabled")).
|
|
186
|
+
expect(wrapper.find("button").attributes("aria-disabled")).toBeUndefined();
|
|
174
187
|
});
|
|
175
188
|
|
|
176
189
|
it("sets aria-disabled and readonly attr when readonly", async () => {
|
|
@@ -182,12 +195,12 @@ describe("InputButton", () => {
|
|
|
182
195
|
|
|
183
196
|
it("keeps button text visible (not sr-only) by default", async () => {
|
|
184
197
|
wrapper = await createWrapper();
|
|
185
|
-
expect(wrapper.find(".button-text").classes()).not.toContain("sr-only");
|
|
198
|
+
expect(wrapper.find(".input-button-text").classes()).not.toContain("sr-only");
|
|
186
199
|
});
|
|
187
200
|
|
|
188
201
|
it("makes button text sr-only with iconOnly slot, but keeps it in DOM for screen readers", async () => {
|
|
189
202
|
wrapper = await createWrapper({ buttonText: "Save document" }, { iconOnly: "<span>💾</span>" });
|
|
190
|
-
const buttonText = wrapper.find(".button-text");
|
|
203
|
+
const buttonText = wrapper.find(".input-button-text");
|
|
191
204
|
expect(buttonText.text()).toBe("Save document");
|
|
192
205
|
expect(buttonText.classes()).toContain("sr-only");
|
|
193
206
|
});
|
|
@@ -199,20 +212,20 @@ describe("InputButton", () => {
|
|
|
199
212
|
describe("Slots", () => {
|
|
200
213
|
it("renders left slot", async () => {
|
|
201
214
|
wrapper = await createWrapper({}, { left: '<span data-testid="left-icon">←</span>' });
|
|
202
|
-
expect(wrapper.find(
|
|
215
|
+
expect(wrapper.find('.input-button-icon[data-slot="left"]').exists()).toBe(true);
|
|
203
216
|
expect(wrapper.find('[data-testid="left-icon"]').exists()).toBe(true);
|
|
204
217
|
});
|
|
205
218
|
|
|
206
219
|
it("renders right slot", async () => {
|
|
207
220
|
wrapper = await createWrapper({}, { right: '<span data-testid="right-icon">→</span>' });
|
|
208
|
-
expect(wrapper.find(
|
|
221
|
+
expect(wrapper.find('.input-button-icon[data-slot="right"]').exists()).toBe(true);
|
|
209
222
|
expect(wrapper.find('[data-testid="right-icon"]').exists()).toBe(true);
|
|
210
223
|
});
|
|
211
224
|
|
|
212
|
-
it("renders iconOnly slot and
|
|
225
|
+
it("renders iconOnly slot and sets data-icon-only on the button", async () => {
|
|
213
226
|
wrapper = await createWrapper({}, { iconOnly: '<span data-testid="icon-only">⚡</span>' });
|
|
214
|
-
expect(wrapper.find(
|
|
215
|
-
expect(wrapper.find("button").
|
|
227
|
+
expect(wrapper.find('.input-button-icon[data-slot="icon-only"]').exists()).toBe(true);
|
|
228
|
+
expect(wrapper.find("button").attributes("data-icon-only")).toBe("");
|
|
216
229
|
});
|
|
217
230
|
|
|
218
231
|
it("iconOnly suppresses left and right slots", async () => {
|
|
@@ -224,9 +237,9 @@ describe("InputButton", () => {
|
|
|
224
237
|
iconOnly: '<span data-testid="icon">⚡</span>',
|
|
225
238
|
}
|
|
226
239
|
);
|
|
227
|
-
expect(wrapper.find(
|
|
228
|
-
expect(wrapper.find(
|
|
229
|
-
expect(wrapper.find(
|
|
240
|
+
expect(wrapper.find('.input-button-icon[data-slot="left"]').exists()).toBe(false);
|
|
241
|
+
expect(wrapper.find('.input-button-icon[data-slot="right"]').exists()).toBe(false);
|
|
242
|
+
expect(wrapper.find('.input-button-icon[data-slot="icon-only"]').exists()).toBe(true);
|
|
230
243
|
});
|
|
231
244
|
|
|
232
245
|
it("renders left and right slots together when no iconOnly", async () => {
|
|
@@ -237,9 +250,9 @@ describe("InputButton", () => {
|
|
|
237
250
|
right: '<span data-testid="right">→</span>',
|
|
238
251
|
}
|
|
239
252
|
);
|
|
240
|
-
expect(wrapper.find(
|
|
241
|
-
expect(wrapper.find(
|
|
242
|
-
expect(wrapper.find(
|
|
253
|
+
expect(wrapper.find('.input-button-icon[data-slot="left"]').exists()).toBe(true);
|
|
254
|
+
expect(wrapper.find('.input-button-icon[data-slot="right"]').exists()).toBe(true);
|
|
255
|
+
expect(wrapper.find('.input-button-icon[data-slot="icon-only"]').exists()).toBe(false);
|
|
243
256
|
});
|
|
244
257
|
});
|
|
245
258
|
|
|
@@ -265,9 +278,9 @@ describe("InputButton", () => {
|
|
|
265
278
|
expect(link.attributes("href")).toBe("/api/auth/github");
|
|
266
279
|
});
|
|
267
280
|
|
|
268
|
-
it("
|
|
281
|
+
it("sets data-link for a link", async () => {
|
|
269
282
|
wrapper = await createWrapper({ href: "/api/auth/github", external: true });
|
|
270
|
-
expect(wrapper.find("a").
|
|
283
|
+
expect(wrapper.find("a").attributes("data-link")).toBe("");
|
|
271
284
|
});
|
|
272
285
|
});
|
|
273
286
|
|
|
@@ -297,8 +310,19 @@ describe("InputButton", () => {
|
|
|
297
310
|
});
|
|
298
311
|
});
|
|
299
312
|
|
|
313
|
+
const looseMounts: { unmount: () => void }[] = [];
|
|
314
|
+
const track = <T extends { unmount: () => void }>(mounted: T) => {
|
|
315
|
+
looseMounts.push(mounted);
|
|
316
|
+
return mounted;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
afterEach(() => {
|
|
320
|
+
looseMounts.splice(0).forEach((mounted) => mounted.unmount());
|
|
321
|
+
vi.restoreAllMocks();
|
|
322
|
+
});
|
|
323
|
+
|
|
300
324
|
describe("InputButton slot toggling", () => {
|
|
301
|
-
it("adds and removes
|
|
325
|
+
it("adds and removes data-icon-only when the iconOnly slot changes after mount", async () => {
|
|
302
326
|
const Host = defineComponent({
|
|
303
327
|
components: { InputButton },
|
|
304
328
|
setup() {
|
|
@@ -310,23 +334,25 @@ describe("InputButton slot toggling", () => {
|
|
|
310
334
|
</InputButton>
|
|
311
335
|
`,
|
|
312
336
|
});
|
|
313
|
-
const wrapper = await mountSuspended(Host);
|
|
337
|
+
const wrapper = track(await mountSuspended(Host));
|
|
314
338
|
const button = () => wrapper.find(".input-button");
|
|
315
|
-
expect(button().
|
|
339
|
+
expect(button().attributes("data-icon-only")).toBeUndefined();
|
|
316
340
|
(wrapper.vm as unknown as { iconOnly: boolean }).iconOnly = true;
|
|
317
341
|
await nextTick();
|
|
318
|
-
expect(button().
|
|
319
|
-
expect(wrapper.find(".button-text").classes()).toContain("sr-only");
|
|
342
|
+
expect(button().attributes("data-icon-only")).toBe("");
|
|
343
|
+
expect(wrapper.find(".input-button-text").classes()).toContain("sr-only");
|
|
320
344
|
});
|
|
321
345
|
});
|
|
322
346
|
|
|
323
347
|
describe("InputButton readonly and reactivity", () => {
|
|
324
348
|
it("does not fire a consumer click handler while readonly (keyboard activation included)", async () => {
|
|
325
349
|
const onClick = vi.fn();
|
|
326
|
-
const wrapper =
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
350
|
+
const wrapper = track(
|
|
351
|
+
await mountSuspended(InputButton, {
|
|
352
|
+
props: { buttonText: "Save", readonly: true },
|
|
353
|
+
attrs: { onClick },
|
|
354
|
+
})
|
|
355
|
+
);
|
|
330
356
|
await wrapper.trigger("click");
|
|
331
357
|
expect(onClick).not.toHaveBeenCalled();
|
|
332
358
|
expect(wrapper.attributes("aria-disabled")).toBe("true");
|
|
@@ -334,18 +360,22 @@ describe("InputButton readonly and reactivity", () => {
|
|
|
334
360
|
|
|
335
361
|
it("fires the click handler when not readonly", async () => {
|
|
336
362
|
const onClick = vi.fn();
|
|
337
|
-
const wrapper =
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
363
|
+
const wrapper = track(
|
|
364
|
+
await mountSuspended(InputButton, {
|
|
365
|
+
props: { buttonText: "Save" },
|
|
366
|
+
attrs: { onClick },
|
|
367
|
+
})
|
|
368
|
+
);
|
|
341
369
|
await wrapper.trigger("click");
|
|
342
370
|
expect(onClick).toHaveBeenCalledTimes(1);
|
|
343
371
|
});
|
|
344
372
|
|
|
345
373
|
it("updates classes when styleClassPassthrough changes after mount", async () => {
|
|
346
|
-
const wrapper =
|
|
347
|
-
|
|
348
|
-
|
|
374
|
+
const wrapper = track(
|
|
375
|
+
await mountSuspended(InputButton, {
|
|
376
|
+
props: { buttonText: "Save", styleClassPassthrough: ["original"] },
|
|
377
|
+
})
|
|
378
|
+
);
|
|
349
379
|
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
350
380
|
expect(wrapper.classes()).not.toContain("original");
|
|
351
381
|
expect(wrapper.classes()).toContain("updated");
|
|
@@ -1,72 +1,72 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
3
|
exports[`InputButton > Snapshots > icon-only slot 1`] = `
|
|
4
|
-
"<button type="button"
|
|
4
|
+
"<button type="button" data-testid="input-button" data-theme="default" data-variant="primary" data-icon-only="" class="input-button">
|
|
5
5
|
<!--v-if-->
|
|
6
|
-
<!--v-if--><span class="button-text sr-only">Click me</span>
|
|
7
|
-
<!--v-if--><span class="
|
|
6
|
+
<!--v-if--><span class="input-button-text sr-only">Click me</span>
|
|
7
|
+
<!--v-if--><span class="input-button-icon" data-slot="icon-only"><span data-testid="icon-only">⚡</span></span>
|
|
8
8
|
</button>"
|
|
9
9
|
`;
|
|
10
10
|
|
|
11
11
|
exports[`InputButton > Snapshots > pending state 1`] = `
|
|
12
|
-
"<button type="button"
|
|
12
|
+
"<button type="button" data-testid="input-button" data-theme="default" data-variant="primary" data-pending="" data-pending-effect="" class="input-button"><svg class="pending-effect-container" data-theme="default" aria-hidden="true" focusable="false">
|
|
13
13
|
<rect pathLength="100" stroke-linecap="round" class="pending-effect-blur"></rect>
|
|
14
14
|
<rect pathLength="100" stroke-linecap="round" class="pending-effect-line"></rect>
|
|
15
15
|
</svg>
|
|
16
|
-
<!--v-if--><span class="button-text">Click me</span>
|
|
16
|
+
<!--v-if--><span class="input-button-text">Click me</span>
|
|
17
17
|
<!--v-if-->
|
|
18
18
|
<!--v-if-->
|
|
19
19
|
</button>"
|
|
20
20
|
`;
|
|
21
21
|
|
|
22
22
|
exports[`InputButton > Snapshots > pill 1`] = `
|
|
23
|
-
"<button type="button"
|
|
23
|
+
"<button type="button" data-testid="input-button" data-theme="default" data-variant="primary" data-pill="" class="input-button">
|
|
24
24
|
<!--v-if-->
|
|
25
|
-
<!--v-if--><span class="button-text">Click me</span>
|
|
25
|
+
<!--v-if--><span class="input-button-text">Click me</span>
|
|
26
26
|
<!--v-if-->
|
|
27
27
|
<!--v-if-->
|
|
28
28
|
</button>"
|
|
29
29
|
`;
|
|
30
30
|
|
|
31
31
|
exports[`InputButton > Snapshots > primary (default) 1`] = `
|
|
32
|
-
"<button type="button"
|
|
32
|
+
"<button type="button" data-testid="input-button" data-theme="default" data-variant="primary" class="input-button">
|
|
33
33
|
<!--v-if-->
|
|
34
|
-
<!--v-if--><span class="button-text">Click me</span>
|
|
34
|
+
<!--v-if--><span class="input-button-text">Click me</span>
|
|
35
35
|
<!--v-if-->
|
|
36
36
|
<!--v-if-->
|
|
37
37
|
</button>"
|
|
38
38
|
`;
|
|
39
39
|
|
|
40
40
|
exports[`InputButton > Snapshots > readonly 1`] = `
|
|
41
|
-
"<button type="button" readonly="" aria-disabled="true" data-testid="input-button" data-theme="default" class="input-button
|
|
41
|
+
"<button type="button" readonly="" aria-disabled="true" data-testid="input-button" data-theme="default" data-variant="primary" class="input-button">
|
|
42
42
|
<!--v-if-->
|
|
43
|
-
<!--v-if--><span class="button-text">Click me</span>
|
|
43
|
+
<!--v-if--><span class="input-button-text">Click me</span>
|
|
44
44
|
<!--v-if-->
|
|
45
45
|
<!--v-if-->
|
|
46
46
|
</button>"
|
|
47
47
|
`;
|
|
48
48
|
|
|
49
49
|
exports[`InputButton > Snapshots > secondary variant 1`] = `
|
|
50
|
-
"<button type="button"
|
|
50
|
+
"<button type="button" data-testid="input-button" data-theme="default" data-variant="secondary" class="input-button">
|
|
51
51
|
<!--v-if-->
|
|
52
|
-
<!--v-if--><span class="button-text">Click me</span>
|
|
52
|
+
<!--v-if--><span class="input-button-text">Click me</span>
|
|
53
53
|
<!--v-if-->
|
|
54
54
|
<!--v-if-->
|
|
55
55
|
</button>"
|
|
56
56
|
`;
|
|
57
57
|
|
|
58
58
|
exports[`InputButton > Snapshots > tertiary variant 1`] = `
|
|
59
|
-
"<button type="button"
|
|
59
|
+
"<button type="button" data-testid="input-button" data-theme="default" data-variant="tertiary" class="input-button">
|
|
60
60
|
<!--v-if-->
|
|
61
|
-
<!--v-if--><span class="button-text">Click me</span>
|
|
61
|
+
<!--v-if--><span class="input-button-text">Click me</span>
|
|
62
62
|
<!--v-if-->
|
|
63
63
|
<!--v-if-->
|
|
64
64
|
</button>"
|
|
65
65
|
`;
|
|
66
66
|
|
|
67
67
|
exports[`InputButton > Snapshots > with left and right slots 1`] = `
|
|
68
|
-
"<button type="button"
|
|
69
|
-
<!--v-if--><span class="
|
|
68
|
+
"<button type="button" data-testid="input-button" data-theme="default" data-variant="primary" class="input-button">
|
|
69
|
+
<!--v-if--><span class="input-button-icon" data-slot="left"><span data-testid="left-icon">←</span></span><span class="input-button-text">Click me</span><span class="input-button-icon" data-slot="right"><span data-testid="right-icon">→</span></span>
|
|
70
70
|
<!--v-if-->
|
|
71
71
|
</button>"
|
|
72
72
|
`;
|
|
@@ -25,6 +25,8 @@ declared in `04.elements/forms/03.generic-input-geometry.css`; see `theming-form
|
|
|
25
25
|
|---|---|---|
|
|
26
26
|
| `--input-checkbox-surface` | `var(--theme-checkbox-symbol-surface)` | Background |
|
|
27
27
|
| `--input-checkbox-border` | `var(--theme-border)` | Border colour |
|
|
28
|
+
| `--input-checkbox-border-hover` | the resolved border | 1px outline colour on hover |
|
|
29
|
+
| `--input-checkbox-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border (not inside the button style). Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
28
30
|
| `--input-checkbox-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible` (not in button mode, the button draws its own) |
|
|
29
31
|
| `--input-checkbox-icon-color` | `var(--theme-text)` | Checked icon colour |
|
|
30
32
|
| `--input-checkbox-transition-duration` | `var(--theme-form-transition-duration)` (`0.2s`) | Border/background and icon fade transitions, all three components |
|
|
@@ -50,7 +52,8 @@ Also reads `--input-checked-icon-gap` (control to label gap) and `--input-min-he
|
|
|
50
52
|
| `--input-checkbox-button-surface` | `var(--theme-input-surface)` | Background (resting) |
|
|
51
53
|
| `--input-checkbox-button-border` | `var(--theme-border)` | Border colour |
|
|
52
54
|
| `--input-checkbox-button-surface-hover` | `var(--theme-input-surface-hover)` | Background on hover |
|
|
53
|
-
| `--input-checkbox-button-ring-hover` | `var(--
|
|
55
|
+
| `--input-checkbox-button-ring-hover` | `var(--input-checkbox-button-border)` | 1px outline colour on hover, no offset |
|
|
56
|
+
| `--input-checkbox-button-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border. Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
54
57
|
| `--input-checkbox-button-surface-focus` | `var(--theme-surface-subtle)` | Background when the inner control has `:focus-visible` |
|
|
55
58
|
| `--input-checkbox-button-ring-focus` | `var(--theme-border-focus)` | Outline colour when the inner control has `:focus-visible` |
|
|
56
59
|
| `--input-checkbox-button-label-color` | `var(--theme-text)` | Option label colour |
|
|
@@ -90,6 +93,11 @@ transitions) and `--_icon-size` (the button shrinks the inner control's checked
|
|
|
90
93
|
|
|
91
94
|
---
|
|
92
95
|
|
|
96
|
+
> **Changed 2026-10-17**: bordered controls carry a light shadow ring (`box-shadow`, theme step 2,
|
|
97
|
+
> so red when invalid), and hover draws a 1px outline in the border colour with no offset, over the
|
|
98
|
+
> ring's inner edge. Hover used to draw a 2px outline at a 2px offset in the focus colour. Focus is
|
|
99
|
+
> unchanged and sits just outside the ring.
|
|
100
|
+
|
|
93
101
|
## State hooks
|
|
94
102
|
|
|
95
103
|
| Attribute | On | When |
|
|
@@ -112,6 +112,14 @@ watch(
|
|
|
112
112
|
border-radius: 100vw;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
&:not([data-button]) {
|
|
116
|
+
box-shadow: 0 0 0 var(--form-element-shadow-width) var(--input-checkbox-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
|
|
117
|
+
|
|
118
|
+
&:has(input:hover) {
|
|
119
|
+
outline-color: var(--input-checkbox-border-hover, var(--input-checkbox-border, var(--theme-border)));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
115
123
|
&:not([data-button]):has(input:focus-visible) {
|
|
116
124
|
outline: var(--form-element-outline-width-focus) solid var(--input-checkbox-border-focus, var(--theme-border-focus));
|
|
117
125
|
outline-offset: var(--form-element-outline-offset-focus);
|
|
@@ -114,6 +114,7 @@ watch(
|
|
|
114
114
|
border: var(--form-element-border-width) solid var(--input-checkbox-button-border, var(--theme-border));
|
|
115
115
|
border-radius: var(--input-checkbox-button-border-radius, 0.4rem);
|
|
116
116
|
outline: var(--form-element-outline-width) solid transparent;
|
|
117
|
+
box-shadow: 0 0 0 var(--form-element-shadow-width) var(--input-checkbox-button-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
|
|
117
118
|
|
|
118
119
|
transition: all var(--input-checkbox-transition-duration, var(--theme-form-transition-duration)) ease-in-out;
|
|
119
120
|
|
|
@@ -127,8 +128,7 @@ watch(
|
|
|
127
128
|
|
|
128
129
|
&:hover {
|
|
129
130
|
background-color: var(--input-checkbox-button-surface-hover, var(--theme-input-surface-hover));
|
|
130
|
-
outline-color: var(--input-checkbox-button-ring-hover, var(--theme-
|
|
131
|
-
outline-offset: var(--form-element-outline-offset-focus);
|
|
131
|
+
outline-color: var(--input-checkbox-button-ring-hover, var(--input-checkbox-button-border, var(--theme-border)));
|
|
132
132
|
cursor: pointer;
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -36,7 +36,7 @@ These match `FormFieldset`'s `--form-fieldset-*` panel tokens.
|
|
|
36
36
|
| `--input-description-slot-margin-block-end` | `0.8rem` | Space below each slot's wrapper |
|
|
37
37
|
| `--input-description-slot-margin-inline` | `0` | Inline margin of each slot's wrapper |
|
|
38
38
|
| `--input-description-font-size` | `var(--step-4)` | Font size of the `descriptionText` paragraph |
|
|
39
|
-
| `--input-description-line-height` | `
|
|
39
|
+
| `--input-description-line-height` | `1.5` | Line height of the `descriptionText` paragraph |
|
|
40
40
|
|
|
41
41
|
With default (unpadded) panel tokens, the slot margins collapse through the root, so they are what
|
|
42
42
|
spaces the description from the label and control. Beyond these the `descriptionHtml` wrapper
|
|
@@ -67,7 +67,7 @@ Every field's description at once, from an app-level stylesheet (e.g. `app/asset
|
|
|
67
67
|
```css
|
|
68
68
|
:where(html) {
|
|
69
69
|
--input-description-color: var(--theme-input-placeholder);
|
|
70
|
-
--input-description-
|
|
70
|
+
--input-description-font-size: 1.4rem;
|
|
71
71
|
}
|
|
72
72
|
```
|
|
73
73
|
|
|
@@ -175,3 +175,15 @@ show as uneven space inside the panel. Zero them and space the panel with the ro
|
|
|
175
175
|
`.input-description` element. It's only reachable when you render `InputDescription` directly in
|
|
176
176
|
a custom field wrapper: the library's own wrappers pass `input-text-description` (no styles
|
|
177
177
|
attached) and don't forward yours. Use a `class` on the wrapper instead (see **One field** above).
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Notes
|
|
182
|
+
|
|
183
|
+
- The root sets `overflow-wrap: anywhere`, so a long URL or unbroken word wraps inside a narrow
|
|
184
|
+
column. Copy arrives through slots, so there is no line-clamp token: clamp your own slot markup
|
|
185
|
+
if you need to.
|
|
186
|
+
|
|
187
|
+
> **Changed 2026-10-17**: `--input-description-line-height` defaults to `1.5` (was `var(--step-4)`,
|
|
188
|
+
> the same as the font size, so wrapped lines touched). If you relied on the tighter spacing, set
|
|
189
|
+
> it back with `--input-description-line-height: var(--step-4)`.
|
|
@@ -59,6 +59,7 @@ watch(
|
|
|
59
59
|
border-radius: var(--input-description-border-radius, 0);
|
|
60
60
|
outline: var(--input-description-outline, 0);
|
|
61
61
|
outline-offset: var(--input-description-outline-offset, 0);
|
|
62
|
+
overflow-wrap: anywhere;
|
|
62
63
|
|
|
63
64
|
.input-description-html,
|
|
64
65
|
.input-description-text {
|
|
@@ -69,7 +70,7 @@ watch(
|
|
|
69
70
|
|
|
70
71
|
.input-description-text {
|
|
71
72
|
font-size: var(--input-description-font-size, var(--step-4));
|
|
72
|
-
line-height: var(--input-description-line-height,
|
|
73
|
+
line-height: var(--input-description-line-height, 1.5);
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
-
import { computed } from "vue";
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
3
|
import StorybookComponent from "../InputDescription.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
4
5
|
import type { InputUiVariant } from "~/types/forms/types.forms";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
5
7
|
|
|
6
8
|
interface InputDescriptionStoryArgs {
|
|
7
9
|
descriptionId: string;
|
|
@@ -52,8 +54,43 @@ export default {
|
|
|
52
54
|
descriptionText: "We'll only use this to send your booking confirmation.",
|
|
53
55
|
descriptionHtml: "",
|
|
54
56
|
},
|
|
57
|
+
decorators: [
|
|
58
|
+
(story, context) => ({
|
|
59
|
+
components: { story, CanvasSwitcher },
|
|
60
|
+
setup() {
|
|
61
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
62
|
+
return { canvasName };
|
|
63
|
+
},
|
|
64
|
+
template: `
|
|
65
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
66
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
67
|
+
</div>
|
|
68
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
69
|
+
<story />
|
|
70
|
+
</div>
|
|
71
|
+
`,
|
|
72
|
+
}),
|
|
73
|
+
],
|
|
74
|
+
parameters: {
|
|
75
|
+
layout: "fullscreen",
|
|
76
|
+
docs: {
|
|
77
|
+
description: {
|
|
78
|
+
component:
|
|
79
|
+
"Help text for a form field. Copy goes in the `descriptionText` slot (a paragraph) and the `descriptionHtml` slot (rich markup, rendered first); with neither filled nothing renders. `description-id` becomes the root id, for the control's `aria-describedby`. `field-has-error` and `input-variant` are styling hooks only. Every labelled field renders it from its own description slots: see [InputTextField](?path=/story/components-forms-input-text-inputtextfield--with-description).",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
55
83
|
} as Meta<InputDescriptionStoryArgs>;
|
|
56
84
|
|
|
85
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
86
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
87
|
+
const fieldNote = `
|
|
88
|
+
<p style="${storyNoteStyle}">
|
|
89
|
+
Every labelled field renders this from its own description slots. See it in a real field in the
|
|
90
|
+
<a href="/?path=/story/components-forms-input-text-inputtextfield--with-description" target="_top">InputTextField</a> story.
|
|
91
|
+
</p>
|
|
92
|
+
`;
|
|
93
|
+
|
|
57
94
|
const Template: StoryFn<InputDescriptionStoryArgs> = (args) => ({
|
|
58
95
|
components: { StorybookComponent },
|
|
59
96
|
setup() {
|
|
@@ -64,7 +101,8 @@ const Template: StoryFn<InputDescriptionStoryArgs> = (args) => ({
|
|
|
64
101
|
return { args, componentArgs };
|
|
65
102
|
},
|
|
66
103
|
template: `
|
|
67
|
-
<div style="max-width: 48rem;
|
|
104
|
+
<div style="max-width: 48rem; font-size: 1.6rem;">
|
|
105
|
+
${fieldNote}
|
|
68
106
|
<label for="story-field" style="display: block; font-weight: 700;">Email address</label>
|
|
69
107
|
<StorybookComponent v-bind="componentArgs">
|
|
70
108
|
<template v-if="args.descriptionHtml" #descriptionHtml><span v-html="args.descriptionHtml"></span></template>
|
|
@@ -81,17 +119,26 @@ const Template: StoryFn<InputDescriptionStoryArgs> = (args) => ({
|
|
|
81
119
|
});
|
|
82
120
|
|
|
83
121
|
export const TextOnly = Template.bind({});
|
|
122
|
+
TextOnly.parameters = {
|
|
123
|
+
docs: { description: { story: "Plain help text in the `descriptionText` slot." } },
|
|
124
|
+
};
|
|
84
125
|
|
|
85
126
|
export const HtmlOnly = Template.bind({});
|
|
86
127
|
HtmlOnly.args = {
|
|
87
128
|
descriptionText: "",
|
|
88
129
|
descriptionHtml: "Password must contain:<ul><li>At least 12 characters</li><li>One number</li></ul>",
|
|
89
130
|
};
|
|
131
|
+
HtmlOnly.parameters = {
|
|
132
|
+
docs: { description: { story: "Rich markup, here a list, in the `descriptionHtml` slot." } },
|
|
133
|
+
};
|
|
90
134
|
|
|
91
135
|
export const TextAndHtml = Template.bind({});
|
|
92
136
|
TextAndHtml.args = {
|
|
93
137
|
descriptionHtml: "<strong>Tip:</strong> use your work address if you have one.",
|
|
94
138
|
};
|
|
139
|
+
TextAndHtml.parameters = {
|
|
140
|
+
docs: { description: { story: "Both slots: `descriptionHtml` renders above `descriptionText`." } },
|
|
141
|
+
};
|
|
95
142
|
|
|
96
143
|
export const Empty = Template.bind({});
|
|
97
144
|
Empty.args = { descriptionText: "", descriptionHtml: "" };
|
|
@@ -101,6 +148,9 @@ Empty.parameters = {
|
|
|
101
148
|
|
|
102
149
|
export const CustomTokens = Template.bind({});
|
|
103
150
|
CustomTokens.args = { styleClassPassthrough: "input-description-story-tokens" };
|
|
151
|
+
CustomTokens.parameters = {
|
|
152
|
+
docs: { description: { story: "Colour, size and slot spacing set as tokens through a passthrough class." } },
|
|
153
|
+
};
|
|
104
154
|
CustomTokens.decorators = [
|
|
105
155
|
() => ({
|
|
106
156
|
template: `
|
|
@@ -109,7 +159,6 @@ CustomTokens.decorators = [
|
|
|
109
159
|
.input-description-story-tokens {
|
|
110
160
|
--input-description-color: var(--theme-input-placeholder);
|
|
111
161
|
--input-description-font-size: 1.4rem;
|
|
112
|
-
--input-description-line-height: 1.5;
|
|
113
162
|
--input-description-slot-margin-block-start: 0.2rem;
|
|
114
163
|
--input-description-slot-margin-block-end: 1.2rem;
|
|
115
164
|
}
|
|
@@ -147,7 +196,6 @@ Panel.decorators = [
|
|
|
147
196
|
--input-description-padding-inline: 1.2rem;
|
|
148
197
|
--input-description-slot-margin-block-start: 0;
|
|
149
198
|
--input-description-slot-margin-block-end: 0;
|
|
150
|
-
--input-description-line-height: 1.5;
|
|
151
199
|
}
|
|
152
200
|
.input-description-story-panel .input-description-html + .input-description-text {
|
|
153
201
|
margin-block-start: 0.4rem;
|
|
@@ -158,3 +206,21 @@ Panel.decorators = [
|
|
|
158
206
|
`,
|
|
159
207
|
}),
|
|
160
208
|
];
|
|
209
|
+
|
|
210
|
+
export const StressTest = Template.bind({});
|
|
211
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
212
|
+
StressTest.args = {
|
|
213
|
+
descriptionText:
|
|
214
|
+
"📧 Wir verwenden Ihre Adresse ausschließlich für Rückfragen zu Ihrer Bestellung. Datenschutzgrundverordnungskonformitätsbestätigung https://example.com/a/very/long/unbroken/url/for/the/privacy/policy <b>not bold</b> يرجى إدخال بريدك الإلكتروني",
|
|
215
|
+
descriptionHtml:
|
|
216
|
+
"<strong>Hinweis:</strong> Elektronischepostadressefürdiebenachrichtigungüberbestellungen <a href=\"https://example.com/a/very/long/unbroken/url/for/the/terms\">https://example.com/a/very/long/unbroken/url/for/the/terms</a>",
|
|
217
|
+
};
|
|
218
|
+
StressTest.parameters = {
|
|
219
|
+
initialCanvas: "mobileCanvas",
|
|
220
|
+
docs: {
|
|
221
|
+
description: {
|
|
222
|
+
story:
|
|
223
|
+
"Long German copy, unbroken compound words, long URLs (one as a link), emoji, HTML-like and right-to-left text in both slots. Check at every canvas width: every line wraps inside the dashed outline and the wrapped lines don't touch. Copy arrives through slots, so there is no line-clamp token: clamp your own slot markup if you need to.",
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import { nextTick } from "vue";
|
|
4
4
|
import InputDescription from "../InputDescription.vue";
|
|
@@ -19,6 +19,7 @@ describe("InputDescription", () => {
|
|
|
19
19
|
afterEach(() => {
|
|
20
20
|
wrapper?.unmount();
|
|
21
21
|
wrapper = undefined;
|
|
22
|
+
vi.restoreAllMocks();
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
describe("rendering", () => {
|
|
@@ -130,5 +130,13 @@ ancestor of the label).
|
|
|
130
130
|
|
|
131
131
|
`:style-class-passthrough` (string or string array) adds classes to the `<label>`. It's only
|
|
132
132
|
reachable when you render `InputLabel` directly in a custom field wrapper: the library's wrappers
|
|
133
|
-
pass their own hook classes (`input-text-label`, `input-number-label`, `input-select-label`,
|
|
134
|
-
`input-textarea-label`, `
|
|
133
|
+
pass their own hook classes (`input-text-label`, `input-number-label`, `input-range-label`, `input-select-label`,
|
|
134
|
+
`input-textarea-label`, `toggle-switch-label`) and don't forward yours.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Notes
|
|
139
|
+
|
|
140
|
+
- The label sets `overflow-wrap: anywhere`, so a long URL or unbroken word (and an unbroken
|
|
141
|
+
marker text) wraps inside a narrow column. Label copy arrives through slots, so there is no
|
|
142
|
+
line-clamp token: clamp your own slot markup if you need to.
|
|
@@ -95,6 +95,7 @@ watch(
|
|
|
95
95
|
line-height: var(--input-label-line-height, 1.5);
|
|
96
96
|
margin-block: var(--input-label-margin-block, 0.8rem);
|
|
97
97
|
margin-inline: var(--input-label-margin-inline, 0);
|
|
98
|
+
overflow-wrap: anywhere;
|
|
98
99
|
|
|
99
100
|
.input-label-indicator {
|
|
100
101
|
--_indicator-color: var(--input-label-indicator-color, inherit);
|