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.
- package/.claude/skills/components/display-dialog.md +7 -7
- package/app/components/01.atoms/display-dialog/CONSUMER-STYLING.md +20 -8
- package/app/components/01.atoms/display-dialog/DisplayDialog.vue +17 -13
- package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +33 -33
- package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +6 -6
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +42 -0
- package/app/components/05.forms/input-select/InputSelectCore.vue +21 -9
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +60 -0
- package/app/components/05.forms/input-text/InputTextCore.vue +17 -8
- package/package.json +1 -1
|
@@ -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
|
|
package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap
CHANGED
|
@@ -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
|
-
|
|
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(--
|
|
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(--
|
|
91
|
-
background-color: var(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
193
|
+
outline: var(--form-element-outline-width-focus) solid var(--_input-text-border-focus);
|
|
185
194
|
outline-offset: -4px;
|
|
186
195
|
}
|
|
187
196
|
|