srcdev-nuxt-components 9.3.1 → 9.3.3

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.
@@ -14,9 +14,9 @@ open/close state management.
14
14
  |---|---|---|---|
15
15
  | `dataDialogId` | `string` | — | **Required.** Unique identifier rendered as `data-dialog-id` attribute. |
16
16
  | `variant` | `'dialog' \| 'modal' \| 'confirm' \| 'alert' \| 'fullscreen'` | `'dialog'` | Controls panel sizing and ARIA role. |
17
- | `theme` | `SemanticTheme` | `undefined` | Optional header accent — `data-theme` on `.header` only; adds a coloured bottom border and tints the close icon. |
17
+ | `theme` | `SemanticTheme` | `undefined` | Optional header accent — `data-theme` on `.display-dialog-header` only; adds a coloured bottom border and tints the close icon. |
18
18
  | `v-model` | `boolean` | — | Controls open/closed state. |
19
- | `allowContentScroll` | `boolean` | `false` | Enables independent scroll on `.dialog-content`. |
19
+ | `allowContentScroll` | `boolean` | `false` | Enables independent scroll on `.display-dialog-content`. |
20
20
  | `lockViewport` | `boolean` | `true` | Adds/removes `lock` class on `<body>` on mount/close. |
21
21
  | `justifyDialog` | `'start' \| 'center' \| 'end'` | `'center'` | Horizontal position of panel within overlay. |
22
22
  | `alignDialog` | `'start' \| 'center' \| 'end'` | `'center'` | Vertical position of panel within overlay. |
@@ -27,12 +27,12 @@ open/close state management.
27
27
 
28
28
  | Slot | Renders in |
29
29
  |---|---|
30
- | `#dialogTitle` | `.col-left` in the header (hidden when slot is empty) |
31
- | `#dialogContent` | `.dialog-content` (hidden when slot is empty) |
32
- | `#actionButtonLeft` | Left side of `.footer` |
33
- | `#actionButtonRight` | Right side of `.footer` |
30
+ | `#dialogTitle` | `.display-dialog-col-left` in the header (hidden when slot is empty) |
31
+ | `#dialogContent` | `.display-dialog-content` (hidden when slot is empty) |
32
+ | `#actionButtonLeft` | Left side of `.display-dialog-footer` |
33
+ | `#actionButtonRight` | Right side of `.display-dialog-footer` |
34
34
 
35
- `.footer` is omitted entirely when neither action button slot is provided.
35
+ `.display-dialog-footer` is omitted entirely when neither action button slot is provided.
36
36
 
37
37
  ## Variants
38
38
 
@@ -1,5 +1,17 @@
1
1
  # DisplayDialog — Consumer Styling Guide
2
2
 
3
+ ## Structural class names (2026-08-22)
4
+
5
+ `DisplayDialog` renders `<dialog>` inline in the component tree, not via `<Teleport>` — so it's a
6
+ genuine descendant of wherever it's used in the page, and any ancestor's CSS selectors reach it.
7
+ Before 2026-08-22 its internal sections used bare generic class names (`.inner`, `.header`,
8
+ `.footer`, `.col-left`, `.col-right`, `.dialog-content`) — a consumer app with its own `.header`/
9
+ `.footer` layout classes (extremely common names) had its unlayered site CSS silently override the
10
+ dialog's own footer layout (button alignment, padding) purely by class-name collision, since
11
+ unlayered CSS always beats `@layer components` regardless of specificity. All six renamed to
12
+ `.display-dialog-*` prefixed variants (`.display-dialog-inner`, `.display-dialog-header`, etc.) —
13
+ update any consumer overrides written against the old names.
14
+
3
15
  ## Public token API
4
16
 
5
17
  All `--display-dialog-*` tokens are the stable override surface. Because dialogs are site-wide UI
@@ -75,11 +87,11 @@ on `:root`. This applies to every `DisplayDialog` across the site.
75
87
 
76
88
  The `fullscreen` variant bypasses `--_inner-border-radius`, `--_inner-border`, and
77
89
  `--_inner-outline` directly (setting them to `0`/`none`). To restyle the fullscreen panel,
78
- target the private tokens on `.inner.fullscreen`:
90
+ target the private tokens on `.display-dialog-inner.fullscreen`:
79
91
 
80
92
  ```css
81
93
  .display-dialog {
82
- .inner {
94
+ .display-dialog-inner {
83
95
  &.confirm {
84
96
  /* e.g. constrain confirm panel width further */
85
97
  max-width: 40rem;
@@ -101,19 +113,19 @@ target the private tokens on `.inner.fullscreen`:
101
113
 
102
114
  ## Section targeting
103
115
 
104
- Target `.header`, `.dialog-content`, and `.footer` directly to adjust layout within the panel:
116
+ Target `.display-dialog-header`, `.display-dialog-content`, and `.display-dialog-footer` directly to adjust layout within the panel:
105
117
 
106
118
  ```css
107
119
  .display-dialog {
108
- .header {
120
+ .display-dialog-header {
109
121
  border-bottom: 0.1rem solid var(--brand-border);
110
122
  }
111
123
 
112
- .dialog-content {
124
+ .display-dialog-content {
113
125
  /* content area uses --display-dialog-content-padding */
114
126
  }
115
127
 
116
- .footer {
128
+ .display-dialog-footer {
117
129
  border-top: 0.1rem solid var(--brand-border);
118
130
  justify-content: space-between; /* override default flex-end */
119
131
  }
@@ -134,7 +146,7 @@ you can scope overrides to a specific page without affecting the rest of the sit
134
146
  --display-dialog-inner-border-radius: 0;
135
147
  --display-dialog-backdrop-background: rgba(0, 0, 0, 0.8);
136
148
 
137
- .footer {
149
+ .display-dialog-footer {
138
150
  justify-content: stretch;
139
151
  }
140
152
  }
@@ -156,7 +168,7 @@ needs a different look, pass a modifier class and target it alongside `.display-
156
168
  .display-dialog.danger-dialog {
157
169
  --display-dialog-backdrop-background: rgba(180, 0, 0, 0.4);
158
170
 
159
- .inner {
171
+ .display-dialog-inner {
160
172
  --display-dialog-inner-border: 0.2rem solid var(--color-danger);
161
173
  }
162
174
  }
@@ -16,13 +16,13 @@
16
16
  :click-outside-deactivates="!isAlert"
17
17
  @deactivate="closeDialog()"
18
18
  >
19
- <div class="inner" :class="[resolved.variant]">
20
- <div class="header" :data-theme="resolved.theme">
21
- <div v-if="hasTitle" :id="dialogTitleId" class="col-left">
19
+ <div class="display-dialog-inner" :class="[resolved.variant]">
20
+ <div class="display-dialog-header" :data-theme="resolved.theme">
21
+ <div v-if="hasTitle" :id="dialogTitleId" class="display-dialog-col-left">
22
22
  <slot name="dialogTitle"></slot>
23
23
  </div>
24
24
 
25
- <div class="col-right">
25
+ <div class="display-dialog-col-right">
26
26
  <button
27
27
  data-test-id="display-dialog-header-close"
28
28
  class="display-dialog-close"
@@ -35,13 +35,13 @@
35
35
  </div>
36
36
  <div
37
37
  v-if="hasContent"
38
- class="dialog-content"
38
+ class="display-dialog-content"
39
39
  :class="[{ 'allow-content-scroll': resolved.allowContentScroll }]"
40
40
  :tabindex="resolved.allowContentScroll ? 0 : undefined"
41
41
  >
42
42
  <slot name="dialogContent"></slot>
43
43
  </div>
44
- <div v-if="hasFooter" class="footer">
44
+ <div v-if="hasFooter" class="display-dialog-footer">
45
45
  <slot name="actionButtonLeft"></slot>
46
46
  <slot name="actionButtonRight"></slot>
47
47
  </div>
@@ -216,7 +216,7 @@ onUnmounted(() => {
216
216
  align-items: flex-end;
217
217
  }
218
218
 
219
- .inner {
219
+ .display-dialog-inner {
220
220
  display: grid;
221
221
  grid-template-rows: auto 1fr auto;
222
222
  border-radius: var(--_inner-border-radius);
@@ -232,7 +232,11 @@ onUnmounted(() => {
232
232
  }
233
233
 
234
234
  &.confirm {
235
- width: initial;
235
+ /* Matches .alert's cap. Plain `width: initial` (== auto) makes this a shrink-to-fit flex
236
+ item with no wrapping constraint, so a long confirm sentence renders as one unbroken
237
+ line and drags the whole dialog (and its footer buttons) out to that width instead of
238
+ wrapping normally — visible once a real confirm message runs more than a few words. */
239
+ width: min(90%, 48rem);
236
240
  }
237
241
 
238
242
  &.dialog {
@@ -252,7 +256,7 @@ onUnmounted(() => {
252
256
  width: initial;
253
257
  }
254
258
 
255
- .header {
259
+ .display-dialog-header {
256
260
  display: flex;
257
261
  align-items: center;
258
262
  padding: var(--_header-padding);
@@ -265,11 +269,11 @@ onUnmounted(() => {
265
269
  }
266
270
  }
267
271
 
268
- .col-left {
272
+ .display-dialog-col-left {
269
273
  flex: 1;
270
274
  }
271
275
 
272
- .col-right {
276
+ .display-dialog-col-right {
273
277
  margin-inline-start: auto;
274
278
 
275
279
  .display-dialog-close {
@@ -303,7 +307,7 @@ onUnmounted(() => {
303
307
  }
304
308
  }
305
309
 
306
- .dialog-content {
310
+ .display-dialog-content {
307
311
  overflow: hidden;
308
312
  padding: var(--_content-padding);
309
313
 
@@ -315,7 +319,7 @@ onUnmounted(() => {
315
319
  }
316
320
  }
317
321
 
318
- .footer {
322
+ .display-dialog-footer {
319
323
  display: flex;
320
324
  gap: var(--_footer-gap);
321
325
  justify-content: flex-end;
@@ -116,81 +116,81 @@ describe("DisplayDialog", () => {
116
116
  expect(wrapper.attributes("role")).toBe("alertdialog");
117
117
  });
118
118
 
119
- // ─── Variant class on .inner ──────────────────────────────────────────────
119
+ // ─── Variant class on .display-dialog-inner ──────────────────────────────────────────────
120
120
 
121
- it("applies 'dialog' class to .inner by default", async () => {
121
+ it("applies 'dialog' class to .display-dialog-inner by default", async () => {
122
122
  const wrapper = await mountSuspended(DisplayDialog, {
123
123
  props: { dataDialogId: "test" },
124
124
  });
125
- expect(wrapper.find(".inner").classes()).toContain("dialog");
125
+ expect(wrapper.find(".display-dialog-inner").classes()).toContain("dialog");
126
126
  });
127
127
 
128
128
  it.each(["dialog", "modal", "confirm", "alert", "fullscreen"] as const)(
129
- "applies '%s' class to .inner",
129
+ "applies '%s' class to .display-dialog-inner",
130
130
  async (variant) => {
131
131
  const wrapper = await mountSuspended(DisplayDialog, {
132
132
  props: { dataDialogId: "test", variant },
133
133
  });
134
- expect(wrapper.find(".inner").classes()).toContain(variant);
134
+ expect(wrapper.find(".display-dialog-inner").classes()).toContain(variant);
135
135
  }
136
136
  );
137
137
 
138
138
  // ─── Slots ────────────────────────────────────────────────────────────────
139
139
 
140
- it("renders dialogTitle slot in .col-left", async () => {
140
+ it("renders dialogTitle slot in .display-dialog-col-left", async () => {
141
141
  const wrapper = await mountSuspended(DisplayDialog, {
142
142
  props: { dataDialogId: "test" },
143
143
  slots: { dialogTitle: "<p>My Title</p>" },
144
144
  });
145
- expect(wrapper.find(".col-left").text()).toBe("My Title");
145
+ expect(wrapper.find(".display-dialog-col-left").text()).toBe("My Title");
146
146
  });
147
147
 
148
- it("omits .col-left when dialogTitle slot is not provided", async () => {
148
+ it("omits .display-dialog-col-left when dialogTitle slot is not provided", async () => {
149
149
  const wrapper = await mountSuspended(DisplayDialog, {
150
150
  props: { dataDialogId: "test" },
151
151
  });
152
- expect(wrapper.find(".col-left").exists()).toBe(false);
152
+ expect(wrapper.find(".display-dialog-col-left").exists()).toBe(false);
153
153
  });
154
154
 
155
- it("renders dialogContent slot in .dialog-content", async () => {
155
+ it("renders dialogContent slot in .display-dialog-content", async () => {
156
156
  const wrapper = await mountSuspended(DisplayDialog, {
157
157
  props: { dataDialogId: "test" },
158
158
  slots: { dialogContent: "<p>Content</p>" },
159
159
  });
160
- expect(wrapper.find(".dialog-content").text()).toBe("Content");
160
+ expect(wrapper.find(".display-dialog-content").text()).toBe("Content");
161
161
  });
162
162
 
163
- it("omits .dialog-content when dialogContent slot is not provided", async () => {
163
+ it("omits .display-dialog-content when dialogContent slot is not provided", async () => {
164
164
  const wrapper = await mountSuspended(DisplayDialog, {
165
165
  props: { dataDialogId: "test" },
166
166
  });
167
- expect(wrapper.find(".dialog-content").exists()).toBe(false);
167
+ expect(wrapper.find(".display-dialog-content").exists()).toBe(false);
168
168
  });
169
169
 
170
- it("renders .footer when actionButtonLeft slot is provided", async () => {
170
+ it("renders .display-dialog-footer when actionButtonLeft slot is provided", async () => {
171
171
  const wrapper = await mountSuspended(DisplayDialog, {
172
172
  props: { dataDialogId: "test" },
173
173
  slots: { actionButtonLeft: "<button>Cancel</button>" },
174
174
  });
175
- expect(wrapper.find(".footer").exists()).toBe(true);
175
+ expect(wrapper.find(".display-dialog-footer").exists()).toBe(true);
176
176
  });
177
177
 
178
- it("renders .footer when actionButtonRight slot is provided", async () => {
178
+ it("renders .display-dialog-footer when actionButtonRight slot is provided", async () => {
179
179
  const wrapper = await mountSuspended(DisplayDialog, {
180
180
  props: { dataDialogId: "test" },
181
181
  slots: { actionButtonRight: "<button>Confirm</button>" },
182
182
  });
183
- expect(wrapper.find(".footer").exists()).toBe(true);
183
+ expect(wrapper.find(".display-dialog-footer").exists()).toBe(true);
184
184
  });
185
185
 
186
- it("omits .footer when neither action button slot is provided", async () => {
186
+ it("omits .display-dialog-footer when neither action button slot is provided", async () => {
187
187
  const wrapper = await mountSuspended(DisplayDialog, {
188
188
  props: { dataDialogId: "test" },
189
189
  });
190
- expect(wrapper.find(".footer").exists()).toBe(false);
190
+ expect(wrapper.find(".display-dialog-footer").exists()).toBe(false);
191
191
  });
192
192
 
193
- it("renders both action button slots inside .footer", async () => {
193
+ it("renders both action button slots inside .display-dialog-footer", async () => {
194
194
  const wrapper = await mountSuspended(DisplayDialog, {
195
195
  props: { dataDialogId: "test" },
196
196
  slots: {
@@ -198,8 +198,8 @@ describe("DisplayDialog", () => {
198
198
  actionButtonRight: "<button>Confirm</button>",
199
199
  },
200
200
  });
201
- expect(wrapper.find(".footer").text()).toContain("Cancel");
202
- expect(wrapper.find(".footer").text()).toContain("Confirm");
201
+ expect(wrapper.find(".display-dialog-footer").text()).toContain("Cancel");
202
+ expect(wrapper.find(".display-dialog-footer").text()).toContain("Confirm");
203
203
  });
204
204
 
205
205
  // ─── allowContentScroll ───────────────────────────────────────────────────
@@ -209,7 +209,7 @@ describe("DisplayDialog", () => {
209
209
  props: { dataDialogId: "test" },
210
210
  slots: { dialogContent: "<p>Content</p>" },
211
211
  });
212
- expect(wrapper.find(".dialog-content").classes()).not.toContain("allow-content-scroll");
212
+ expect(wrapper.find(".display-dialog-content").classes()).not.toContain("allow-content-scroll");
213
213
  });
214
214
 
215
215
  it("applies allow-content-scroll class when allowContentScroll=true", async () => {
@@ -217,7 +217,7 @@ describe("DisplayDialog", () => {
217
217
  props: { dataDialogId: "test", allowContentScroll: true },
218
218
  slots: { dialogContent: "<p>Content</p>" },
219
219
  });
220
- expect(wrapper.find(".dialog-content").classes()).toContain("allow-content-scroll");
220
+ expect(wrapper.find(".display-dialog-content").classes()).toContain("allow-content-scroll");
221
221
  });
222
222
 
223
223
  // ─── Close button ─────────────────────────────────────────────────────────
@@ -312,12 +312,12 @@ describe("DisplayDialog", () => {
312
312
  props: { dataDialogId: "test" },
313
313
  slots: { dialogContent: "<p>Content</p>" },
314
314
  });
315
- expect(wrapper.find(".inner").classes()).toContain("dialog");
315
+ expect(wrapper.find(".display-dialog-inner").classes()).toContain("dialog");
316
316
  expect(wrapper.attributes("align-dialog")).toBe("center");
317
317
  expect(wrapper.attributes("justify-dialog")).toBe("center");
318
318
  expect(document.body.classList.contains("lock")).toBe(true);
319
- expect(wrapper.find(".dialog-content").classes()).not.toContain("allow-content-scroll");
320
- expect(wrapper.find(".header").attributes("data-theme")).toBeUndefined();
319
+ expect(wrapper.find(".display-dialog-content").classes()).not.toContain("allow-content-scroll");
320
+ expect(wrapper.find(".display-dialog-header").attributes("data-theme")).toBeUndefined();
321
321
  expect(iconName(wrapper)).toContain("bitcoin-icons:cross-filled");
322
322
  });
323
323
 
@@ -340,12 +340,12 @@ describe("DisplayDialog", () => {
340
340
  props: { dataDialogId: "test" },
341
341
  slots: { dialogContent: "<p>Content</p>" },
342
342
  });
343
- expect(wrapper.find(".inner").classes()).toContain("modal");
343
+ expect(wrapper.find(".display-dialog-inner").classes()).toContain("modal");
344
344
  expect(wrapper.attributes("align-dialog")).toBe("end");
345
345
  expect(wrapper.attributes("justify-dialog")).toBe("start");
346
346
  expect(document.body.classList.contains("lock")).toBe(false);
347
- expect(wrapper.find(".dialog-content").classes()).toContain("allow-content-scroll");
348
- expect(wrapper.find(".header").attributes("data-theme")).toBe("info");
347
+ expect(wrapper.find(".display-dialog-content").classes()).toContain("allow-content-scroll");
348
+ expect(wrapper.find(".display-dialog-header").attributes("data-theme")).toBe("info");
349
349
  expect(iconName(wrapper)).toContain("heroicons:x-mark");
350
350
  });
351
351
 
@@ -377,12 +377,12 @@ describe("DisplayDialog", () => {
377
377
  },
378
378
  slots: { dialogContent: "<p>Content</p>" },
379
379
  });
380
- expect(wrapper.find(".inner").classes()).toContain("fullscreen");
380
+ expect(wrapper.find(".display-dialog-inner").classes()).toContain("fullscreen");
381
381
  expect(wrapper.attributes("align-dialog")).toBe("start");
382
382
  expect(wrapper.attributes("justify-dialog")).toBe("end");
383
383
  expect(document.body.classList.contains("lock")).toBe(true);
384
- expect(wrapper.find(".dialog-content").classes()).not.toContain("allow-content-scroll");
385
- expect(wrapper.find(".header").attributes("data-theme")).toBe("error");
384
+ expect(wrapper.find(".display-dialog-content").classes()).not.toContain("allow-content-scroll");
385
+ expect(wrapper.find(".display-dialog-header").attributes("data-theme")).toBe("error");
386
386
  expect(iconName(wrapper)).toContain("material-symbols:close");
387
387
  });
388
388
 
@@ -2,17 +2,17 @@
2
2
 
3
3
  exports[`DisplayDialog > renders correct HTML structure with all slots 1`] = `
4
4
  "<dialog class="display-dialog" aria-modal="true" aria-labelledby="v-0-0" align-dialog="center" justify-dialog="center" open="" data-dialog-id="test">
5
- <div class="inner dialog">
6
- <div class="header">
7
- <div id="v-0-0" class="col-left">
5
+ <div class="display-dialog-inner dialog">
6
+ <div class="display-dialog-header">
7
+ <div id="v-0-0" class="display-dialog-col-left">
8
8
  <p>Title</p>
9
9
  </div>
10
- <div class="col-right"><button data-test-id="display-dialog-header-close" class="display-dialog-close"><span class="iconify undefinedbitcoin-icons:cross-filled icon"></span><span class="sr-only">Close</span></button></div>
10
+ <div class="display-dialog-col-right"><button data-test-id="display-dialog-header-close" class="display-dialog-close"><span class="iconify undefinedbitcoin-icons:cross-filled icon"></span><span class="sr-only">Close</span></button></div>
11
11
  </div>
12
- <div class="dialog-content">
12
+ <div class="display-dialog-content">
13
13
  <p>Content</p>
14
14
  </div>
15
- <div class="footer"><button>Cancel</button><button>Confirm</button></div>
15
+ <div class="display-dialog-footer"><button>Cancel</button><button>Confirm</button></div>
16
16
  </div>
17
17
  </dialog>"
18
18
  `;
@@ -0,0 +1,42 @@
1
+ # InputSelectCore — Consumer Styling Guide
2
+
3
+ ## Two override paths
4
+
5
+ Same public/private split as `InputTextCore` (see that component's `CONSUMER-STYLING.md` for the
6
+ full rationale) — `.input-select-wrapper` now exposes local tokens, one indirection step below the
7
+ global `--theme-*` ones they default from:
8
+
9
+ ```css
10
+ --_input-select-surface: var(--theme-input-surface);
11
+ --_input-select-surface-hover: var(--theme-input-surface-hover);
12
+ --_input-select-border: var(--theme-border);
13
+ --_input-select-border-focus: var(--theme-border-focus);
14
+ --_input-select-outline-color: var(--_input-select-border-focus);
15
+ ```
16
+
17
+ - **Global override**: redeclare `--theme-input-surface` / `--theme-border` / `--theme-border-focus`
18
+ / `--theme-input-surface-hover` wherever you'd normally set theme tokens.
19
+ - **Local override** (this component only, guaranteed to win):
20
+
21
+ ```css
22
+ .input-select-wrapper {
23
+ --_input-select-border: var(--slate-06);
24
+ --_input-select-border-focus: var(--slate-04);
25
+ --_input-select-surface: var(--slate-01);
26
+ }
27
+ ```
28
+
29
+ ## Note on `--_input-select-outline-color`
30
+
31
+ Before 2026-08-22 this was referenced (on the open-picker outline) but never declared anywhere —
32
+ a dangling private token that silently resolved to the property's initial value (same failure mode
33
+ as the `--theme-button-surface` typo documented in the layer's `CLAUDE.md`, pitfall list). It's now
34
+ properly defined, defaulting to `--_input-select-border-focus`.
35
+
36
+ ---
37
+
38
+ ## Other tokens
39
+
40
+ Generic form-geometry tokens (`--input-padding-inline`, `--input-min-height`, `--input-font-size`,
41
+ `--form-element-border-width`, `--form-input-border-radius`, etc.) are shared across every
42
+ `05.forms` component and declared in `setup/04.elements/forms/*.css` — not component-local.
@@ -73,7 +73,17 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
73
73
  <style lang="css">
74
74
  @layer components {
75
75
  .input-select-wrapper {
76
- background-color: var(--theme-input-surface);
76
+ /* Local overrides, one indirection step below the global --theme-* tokens they default from —
77
+ a consumer can target `.input-select-wrapper { --_input-select-border: ...; }` directly for
78
+ a guaranteed-specific override, instead of relying solely on redefining the global token.
79
+ See CONSUMER-STYLING.md. */
80
+ --_input-select-surface: var(--theme-input-surface);
81
+ --_input-select-surface-hover: var(--theme-input-surface-hover);
82
+ --_input-select-border: var(--theme-border);
83
+ --_input-select-border-focus: var(--theme-border-focus);
84
+ --_input-select-outline-color: var(--_input-select-border-focus);
85
+
86
+ background-color: var(--_input-select-surface);
77
87
  overflow: hidden;
78
88
 
79
89
  z-index: 2;
@@ -81,31 +91,33 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
81
91
  transition: all var(--theme-form-transition-duration) ease-in-out;
82
92
 
83
93
  &.normal {
84
- border: var(--form-element-border-width) solid var(--theme-border);
94
+ border: var(--form-element-border-width) solid var(--_input-select-border);
85
95
  border-radius: var(--form-input-border-radius);
86
96
  outline: var(--form-element-outline-width) solid transparent;
87
97
  }
88
98
 
89
99
  &.underlined {
90
- border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--theme-border);
91
- background-color: var(--theme-input-surface);
100
+ border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_input-select-border);
101
+ background-color: var(--_input-select-surface);
92
102
  }
93
103
 
94
104
  &:has(select:focus-visible, :hover) {
95
- outline: var(--form-element-outline-width-focus) solid var(--theme-border-focus);
105
+ outline: var(--form-element-outline-width-focus) solid var(--_input-select-border-focus);
96
106
  outline-offset: var(--form-element-outline-offset-focus);
97
107
  }
98
108
 
99
109
  .input-select-core {
100
110
  appearance: none;
101
111
  background-color: transparent;
112
+ display: flex;
113
+ align-items: center;
102
114
 
103
115
  /* For legacy support - eg, Safari */
104
116
  /* &::after {
105
117
  content: '';
106
118
  width: 0.8em;
107
119
  height: 0.5em;
108
- background-color: var(--theme-border);
120
+ background-color: var(--_input-select-border);
109
121
  clip-path: polygon(100% 0%, 0 0%, 50% 100%);
110
122
  } */
111
123
 
@@ -128,7 +140,7 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
128
140
 
129
141
  &:open::picker(select) {
130
142
  opacity: 1;
131
- border: var(--form-element-border-width) solid var(--theme-border);
143
+ border: var(--form-element-border-width) solid var(--_input-select-border);
132
144
  outline: var(--form-element-outline-width) solid var(--_input-select-outline-color);
133
145
 
134
146
  @starting-style {
@@ -145,7 +157,7 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
145
157
 
146
158
  font-family: var(--font-family);
147
159
  font-size: var(--input-font-size);
148
- line-height: var(--input-element-line-height);
160
+ /* line-height: var(--input-element-line-height); */
149
161
  padding-block: var(--input-padding-block);
150
162
  padding-inline: var(--input-padding-inline);
151
163
  min-height: var(--input-min-height);
@@ -156,7 +168,7 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
156
168
  transition: all var(--theme-form-transition-duration) ease-in-out;
157
169
 
158
170
  &:hover {
159
- background-color: var(--theme-input-surface-hover);
171
+ background-color: var(--_input-select-surface-hover);
160
172
  }
161
173
 
162
174
  .input-select-core-option-decorator-icon {
@@ -0,0 +1,60 @@
1
+ # InputTextCore — Consumer Styling Guide
2
+
3
+ ## Two override paths
4
+
5
+ Before 2026-08-22, `.input-text-wrapper` read the global `--theme-*` tokens directly
6
+ (`--theme-input-surface`, `--theme-border`, `--theme-border-focus`, `--theme-input-surface-hover`).
7
+ Overriding those globally (e.g. in a consumer app's unlayered `:root` block) should always have
8
+ worked on pure CSS-cascade grounds — an unlayered `:root` declaration (specificity `0,1,0`) beats
9
+ the layer's own `:where(html, [data-theme], [data-invalid])` declaration (specificity `0`)
10
+ regardless of source/import order — but a consumer app hit a case where a global override wasn't
11
+ visibly landing and the root cause wasn't pinned down (see `nuxt-components`'s own git history
12
+ around the same date for a *different*, confirmed bug in a sibling app: wrapping consumer CSS in
13
+ `@layer consumer` broke in production because SSR-inlined per-chunk `<style>` tags fix layer
14
+ priority by first-reference document order, not the app's intended `@layer` statement order — sic
15
+ that isn't what was happening here, since the consumer app's CSS was already unlayered, but it's
16
+ the same class of "cascade behaves differently than a clean mental model predicts" issue).
17
+
18
+ Rather than leave global-token overrides as the only lever, `.input-text-wrapper` now also
19
+ exposes its own local tokens, one indirection step below the global ones:
20
+
21
+ ```css
22
+ --_input-text-surface: var(--theme-input-surface);
23
+ --_input-text-surface-hover: var(--theme-input-surface-hover);
24
+ --_input-text-border: var(--theme-border);
25
+ --_input-text-border-focus: var(--theme-border-focus);
26
+ ```
27
+
28
+ - **Global override** (affects every themed component, not just this one): redeclare
29
+ `--theme-input-surface` / `--theme-border` / `--theme-border-focus` / `--theme-input-surface-hover`
30
+ at whatever scope you'd normally set theme tokens (`:root`, `[data-theme]`, a page wrapper).
31
+ - **Local override** (this component only, guaranteed to win — it's the last declaration before
32
+ the property that consumes it, no global-token indirection to reason about):
33
+
34
+ ```css
35
+ /* Unscoped consumer CSS, no :deep() needed */
36
+ .input-text-wrapper {
37
+ --_input-text-border: var(--slate-06);
38
+ --_input-text-border-focus: var(--slate-04);
39
+ --_input-text-surface: var(--slate-01);
40
+ }
41
+ ```
42
+
43
+ Prefer the local override when you only want to change this one component's look, or when you've
44
+ hit a case (like the one above) where a global-token change isn't landing and you don't have time
45
+ to dig into why — the local path is a strictly simpler cascade to reason about.
46
+
47
+ ---
48
+
49
+ ## Other tokens
50
+
51
+ - `--input-icon-slot-gap`, `--input-padding-inline`, `--input-padding-block`, `--input-min-height`,
52
+ `--input-font-size`, `--form-element-border-width`, `--form-input-border-radius`,
53
+ `--form-element-border-bottom-width-underlined`, `--form-element-outline-width(-focus)`,
54
+ `--form-element-outline-offset-focus` — all generic form-geometry tokens shared across every
55
+ `05.forms` component, declared in `setup/04.elements/forms/*.css`. Not component-local, so
56
+ overriding one affects every form element, not just `InputTextCore`.
57
+ - `--theme-input-text-color-normal`, `--theme-input-placeholder`,
58
+ `--theme-input-placeholder-font-size` — still read directly from the global theme tokens, no
59
+ local indirection yet. Follow the same pattern above (`--_input-text-*`) if a future consumer
60
+ needs a local override for these.
@@ -127,10 +127,19 @@ onMounted(() => {
127
127
  <style lang="css">
128
128
  @layer components {
129
129
  .input-text-wrapper {
130
+ /* Local overrides, one indirection step below the global --theme-* tokens they default from
131
+ — a consumer can target `.input-text-wrapper { --_input-text-border: ...; }` directly for
132
+ a guaranteed-specific override, instead of relying solely on redefining the global token.
133
+ See CONSUMER-STYLING.md. */
134
+ --_input-text-surface: var(--theme-input-surface);
135
+ --_input-text-surface-hover: var(--theme-input-surface-hover);
136
+ --_input-text-border: var(--theme-border);
137
+ --_input-text-border-focus: var(--theme-border-focus);
138
+
130
139
  display: flex;
131
140
  align-items: center;
132
141
  gap: var(--input-icon-slot-gap);
133
- background-color: var(--theme-input-surface);
142
+ background-color: var(--_input-text-surface);
134
143
  overflow: hidden;
135
144
  transition: all var(--theme-form-transition-duration) ease-in-out;
136
145
 
@@ -138,25 +147,25 @@ onMounted(() => {
138
147
  position: relative;
139
148
 
140
149
  &.normal {
141
- border: var(--form-element-border-width) solid var(--theme-border);
150
+ border: var(--form-element-border-width) solid var(--_input-text-border);
142
151
  border-radius: var(--form-input-border-radius);
143
152
  outline: var(--form-element-outline-width) solid transparent;
144
153
 
145
154
  padding-inline: var(--input-padding-inline);
146
155
 
147
156
  &:has(input:is(:hover), button:is(:hover)) {
148
- outline: var(--form-element-outline-width-focus) solid var(--theme-border-focus);
157
+ outline: var(--form-element-outline-width-focus) solid var(--_input-text-border-focus);
149
158
  outline-offset: var(--form-element-outline-offset-focus);
150
159
  }
151
160
 
152
161
  &:has(input:focus-visible, button:focus-visible) {
153
- outline: var(--form-element-outline-width-focus) solid var(--theme-border-focus);
162
+ outline: var(--form-element-outline-width-focus) solid var(--_input-text-border-focus);
154
163
  outline-offset: var(--form-element-outline-offset-focus);
155
164
  }
156
165
  }
157
166
 
158
167
  &.underlined {
159
- border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--theme-border);
168
+ border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_input-text-border);
160
169
  padding-inline: var(--input-padding-inline);
161
170
  }
162
171
 
@@ -171,17 +180,17 @@ onMounted(() => {
171
180
  box-sizing: content-box;
172
181
 
173
182
  .input-button-core {
174
- background-color: var(--theme-input-surface);
183
+ background-color: var(--_input-text-surface);
175
184
  aspect-ratio: 1;
176
185
  border-radius: 0;
177
186
  width: var(--input-min-height);
178
187
 
179
188
  &:hover {
180
- background-color: var(--theme-input-surface-hover);
189
+ background-color: var(--_input-text-surface-hover);
181
190
  }
182
191
 
183
192
  &:is(:focus-visible) {
184
- outline: var(--form-element-outline-width-focus) solid var(--theme-border-focus);
193
+ outline: var(--form-element-outline-width-focus) solid var(--_input-text-border-focus);
185
194
  outline-offset: -4px;
186
195
  }
187
196
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.3.1",
4
+ "version": "9.3.3",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",