@happyvertical/smrt-ui 0.40.68 → 0.40.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/data/DataTable.svelte +1 -1
- package/dist/components/feedback/LoadingOverlay.svelte +1 -1
- package/dist/components/forms/Combobox.svelte +1 -1
- package/dist/components/forms/FilePicker.svelte +1 -1
- package/dist/components/forms/Input.svelte +1 -1
- package/dist/components/forms/InputGroup.svelte +1 -1
- package/dist/components/forms/Listbox.svelte +1 -1
- package/dist/components/forms/MultiSelect.svelte +1 -1
- package/dist/components/forms/RangeSlider.svelte +1 -1
- package/dist/components/forms/Select.svelte +1 -1
- package/dist/components/forms/Slider.svelte +1 -1
- package/dist/components/forms/TagsInput.svelte +1 -1
- package/dist/components/forms/Textarea.svelte +1 -1
- package/dist/components/roles/RoleSelector.svelte +10 -3
- package/dist/components/ui/Dropdown.svelte +11 -3
- package/dist/components/ui/Popover.svelte +1 -1
- package/dist/themes/ThemeProvider.svelte +40 -42
- package/dist/themes/ThemeProvider.svelte.d.ts +10 -0
- package/dist/themes/ThemeProvider.svelte.d.ts.map +1 -1
- package/dist/themes/__tests__/control-boundary-contrast.test.js +112 -0
- package/dist/themes/__tests__/css-generator.test.js +42 -0
- package/dist/themes/__tests__/styles-lockstep.test.js +32 -0
- package/dist/themes/__tests__/theme-provider.test.js +46 -8
- package/dist/themes/__tests__/theme-script.test.js +16 -11
- package/dist/themes/css-generator.d.ts +5 -0
- package/dist/themes/css-generator.d.ts.map +1 -1
- package/dist/themes/css-generator.js +5 -0
- package/dist/themes/glass/index.d.ts.map +1 -1
- package/dist/themes/glass/index.js +12 -4
- package/dist/themes/smrt/index.d.ts.map +1 -1
- package/dist/themes/smrt/index.js +8 -2
- package/dist/themes/studio/index.d.ts.map +1 -1
- package/dist/themes/studio/index.js +6 -2
- package/dist/themes/styles/glass.css +316 -84
- package/dist/themes/styles/material.css +355 -53
- package/dist/themes/styles/smrt.css +4 -4
- package/dist/themes/styles/studio.css +308 -72
- package/dist/themes/theme-script.d.ts +12 -4
- package/dist/themes/theme-script.d.ts.map +1 -1
- package/dist/themes/theme-script.js +14 -19
- package/package.json +3 -2
|
@@ -529,7 +529,7 @@ const sizeClasses = {
|
|
|
529
529
|
}
|
|
530
530
|
|
|
531
531
|
.data-table__cell--expand { width: 2.75rem; text-align: center; }
|
|
532
|
-
.data-table__expand-button { width: 1.75rem; height: 1.75rem; border: 1px solid var(--smrt-color-outline
|
|
532
|
+
.data-table__expand-button { width: 1.75rem; height: 1.75rem; border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-full); background: transparent; color: var(--smrt-color-on-surface); cursor: pointer; }
|
|
533
533
|
.data-table__expand-button:focus-visible { outline: 2px solid var(--smrt-color-primary); outline-offset: 2px; }
|
|
534
534
|
.data-table__cell--expanded { padding: var(--smrt-spacing-4); background: var(--smrt-color-surface-container-low); }
|
|
535
535
|
.data-table__footer { border-top: 2px solid var(--smrt-color-outline-variant); font-weight: var(--smrt-typography-weight-medium); }
|
|
@@ -259,7 +259,7 @@ function handleKeydown(e: KeyboardEvent) {
|
|
|
259
259
|
font-size: var(--smrt-typography-label-large-size, 0.875rem);
|
|
260
260
|
color: var(--smrt-color-on-surface-variant, #6b7280);
|
|
261
261
|
background: transparent;
|
|
262
|
-
border: 1px solid var(--smrt-color-outline
|
|
262
|
+
border: 1px solid var(--smrt-color-outline, #9ca3af);
|
|
263
263
|
border-radius: var(--smrt-radius-md, 8px);
|
|
264
264
|
cursor: pointer;
|
|
265
265
|
transition: all var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease);
|
|
@@ -165,7 +165,7 @@ useControlRegistration(() => {
|
|
|
165
165
|
</div>
|
|
166
166
|
<style>
|
|
167
167
|
.combobox { position: relative; display: grid; gap: var(--smrt-spacing-1); color: var(--smrt-color-on-surface); }
|
|
168
|
-
label { font: var(--smrt-typography-label-large-font); } input { width: 100%; padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border: 1px solid var(--smrt-color-outline
|
|
168
|
+
label { font: var(--smrt-typography-label-large-font); } input { width: 100%; padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface); color: inherit; }
|
|
169
169
|
input:focus { outline: 2px solid var(--smrt-color-primary); outline-offset: 1px; } .options { position: absolute; z-index: var(--smrt-z-index-dropdown); top: 100%; left: 0; right: 0; display: grid; padding: var(--smrt-spacing-1); border: 1px solid var(--smrt-color-outline-variant); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface-container); box-shadow: var(--smrt-elevation-2); }
|
|
170
170
|
.options button { padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border: 0; border-radius: var(--smrt-radius-extra-small); background: transparent; color: var(--smrt-color-on-surface); text-align: left; } .options button.active { background: var(--smrt-color-secondary-container); }
|
|
171
171
|
:global(.combobox[data-smrt-highlighted='true']) { outline: 3px solid var(--smrt-color-tertiary); outline-offset: 4px; }
|
|
@@ -106,7 +106,7 @@ useControlRegistration(() => {
|
|
|
106
106
|
<span class="label">{label}</span><span class="description">{files.length ? `${files.length} file${files.length === 1 ? '' : 's'} selected` : description}</span>
|
|
107
107
|
</label>
|
|
108
108
|
<style>
|
|
109
|
-
.file-picker { display: inline-flex; flex-direction: column; gap: var(--smrt-spacing-1); padding: var(--smrt-spacing-3) var(--smrt-spacing-4); border: 1px solid var(--smrt-color-outline
|
|
109
|
+
.file-picker { display: inline-flex; flex-direction: column; gap: var(--smrt-spacing-1); padding: var(--smrt-spacing-3) var(--smrt-spacing-4); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface); color: var(--smrt-color-on-surface); cursor: pointer; }
|
|
110
110
|
.file-picker.dropzone { width: 100%; min-height: 8rem; align-items: center; justify-content: center; border-style: dashed; text-align: center; }
|
|
111
111
|
.file-picker.dragging { border-color: var(--smrt-color-primary); background: var(--smrt-color-primary-container); }
|
|
112
112
|
.file-picker.disabled { opacity: .5; cursor: not-allowed; } input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
|
@@ -179,7 +179,7 @@ export function getElement(): HTMLInputElement | null {
|
|
|
179
179
|
line-height: var(--smrt-typography-body-medium-line-height, 1.5);
|
|
180
180
|
color: var(--smrt-color-on-surface, #1f2937);
|
|
181
181
|
background-color: var(--smrt-color-surface, #fff);
|
|
182
|
-
border: 1px solid var(--smrt-color-outline
|
|
182
|
+
border: 1px solid var(--smrt-color-outline, #6b7280);
|
|
183
183
|
border-radius: var(--smrt-radius-small, 0.375rem);
|
|
184
184
|
transition:
|
|
185
185
|
border-color var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease-in-out),
|
|
@@ -10,7 +10,7 @@ let { prefix, suffix, children, class: className = '' }: Props = $props();
|
|
|
10
10
|
</script>
|
|
11
11
|
<div class="input-group {className}">{#if prefix}<span class="adornment">{@render prefix()}</span>{/if}<div class="control">{@render children()}</div>{#if suffix}<span class="adornment">{@render suffix()}</span>{/if}</div>
|
|
12
12
|
<style>
|
|
13
|
-
.input-group { display: flex; align-items: stretch; width: 100%; border: 1px solid var(--smrt-color-outline
|
|
13
|
+
.input-group { display: flex; align-items: stretch; width: 100%; border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface); overflow: hidden; }
|
|
14
14
|
.adornment { display: inline-flex; align-items: center; padding: 0 var(--smrt-spacing-3); background: var(--smrt-color-surface-container-low); color: var(--smrt-color-on-surface-variant); }
|
|
15
15
|
.control { flex: 1; min-width: 0; } .control :global(input), .control :global(select), .control :global(textarea) { border: 0 !important; border-radius: 0 !important; box-shadow: none !important; }
|
|
16
16
|
.input-group:focus-within { outline: 2px solid var(--smrt-color-primary); outline-offset: 1px; }
|
|
@@ -90,7 +90,7 @@ useControlRegistration(() => {
|
|
|
90
90
|
onkeydown={(event) => move(event, index)} onclick={() => select(option.value)}>{option.label}</button>{/each}
|
|
91
91
|
</div>
|
|
92
92
|
<style>
|
|
93
|
-
.listbox { display: grid; padding: var(--smrt-spacing-1); border: 1px solid var(--smrt-color-outline
|
|
93
|
+
.listbox { display: grid; padding: var(--smrt-spacing-1); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface); }
|
|
94
94
|
button { padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border: 0; border-radius: var(--smrt-radius-extra-small); background: transparent; color: var(--smrt-color-on-surface); text-align: left; cursor: pointer; }
|
|
95
95
|
button[aria-selected='true'] { background: var(--smrt-color-secondary-container); color: var(--smrt-color-on-secondary-container); } button:focus-visible { outline: 2px solid var(--smrt-color-primary); }
|
|
96
96
|
button:disabled { opacity: .5; cursor: not-allowed; } :global(.listbox[data-smrt-highlighted='true']) { outline: 3px solid var(--smrt-color-tertiary); outline-offset: 4px; }
|
|
@@ -152,7 +152,7 @@ useControlRegistration(() => {
|
|
|
152
152
|
</div>
|
|
153
153
|
<style>
|
|
154
154
|
.multi-select { position: relative; display: grid; gap: var(--smrt-spacing-1); color: var(--smrt-color-on-surface); } .label { font: var(--smrt-typography-label-large-font); }
|
|
155
|
-
.trigger { width: 100%; min-height: 2.5rem; padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border: 1px solid var(--smrt-color-outline
|
|
155
|
+
.trigger { width: 100%; min-height: 2.5rem; padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface); color: inherit; text-align: left; }
|
|
156
156
|
.options { position: absolute; z-index: var(--smrt-z-index-dropdown); top: 100%; left: 0; right: 0; display: grid; padding: var(--smrt-spacing-1); border: 1px solid var(--smrt-color-outline-variant); background: var(--smrt-color-surface-container); box-shadow: var(--smrt-elevation-2); }
|
|
157
157
|
.options button { display: grid; grid-template-columns: 1.25rem 1fr; gap: var(--smrt-spacing-2); padding: var(--smrt-spacing-2); border: 0; background: transparent; color: inherit; text-align: left; } .options button[aria-selected='true'] { background: var(--smrt-color-secondary-container); }
|
|
158
158
|
:global(.multi-select[data-smrt-highlighted='true']) { outline: 3px solid var(--smrt-color-tertiary); outline-offset: 4px; }
|
|
@@ -131,7 +131,7 @@ useControlRegistration(() => {
|
|
|
131
131
|
.range-slider__track input::-moz-range-thumb { pointer-events: auto; }
|
|
132
132
|
.range-slider__inputs { display: flex; gap: var(--smrt-spacing-3); }
|
|
133
133
|
.range-slider__inputs label { display: grid; gap: var(--smrt-spacing-1); flex: 1; font: var(--smrt-typography-label-small-font); color: var(--smrt-color-on-surface-variant); }
|
|
134
|
-
.range-slider__inputs input { width: 100%; padding: var(--smrt-spacing-2); border: 1px solid var(--smrt-color-outline
|
|
134
|
+
.range-slider__inputs input { width: 100%; padding: var(--smrt-spacing-2); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface); color: var(--smrt-color-on-surface); }
|
|
135
135
|
input:focus-visible { outline: 2px solid var(--smrt-color-primary); outline-offset: 3px; }
|
|
136
136
|
:global(.range-slider[data-smrt-highlighted='true']) { outline: 3px solid var(--smrt-color-tertiary); outline-offset: 4px; border-radius: var(--smrt-radius-small); }
|
|
137
137
|
</style>
|
|
@@ -136,7 +136,7 @@ export function getElement(): HTMLSelectElement | null {
|
|
|
136
136
|
background-position: right 0.5rem center;
|
|
137
137
|
background-repeat: no-repeat;
|
|
138
138
|
background-size: 1.5em 1.5em;
|
|
139
|
-
border: 1px solid var(--smrt-color-outline
|
|
139
|
+
border: 1px solid var(--smrt-color-outline, #6b7280);
|
|
140
140
|
border-radius: 0.375rem;
|
|
141
141
|
appearance: none;
|
|
142
142
|
cursor: pointer;
|
|
@@ -125,7 +125,7 @@ useControlRegistration(() => {
|
|
|
125
125
|
.slider__header { justify-content: space-between; font: var(--smrt-typography-label-large-font); }
|
|
126
126
|
output { color: var(--smrt-color-primary); font-variant-numeric: tabular-nums; }
|
|
127
127
|
input[type='range'] { flex: 1; min-width: 8rem; accent-color: var(--smrt-color-primary); cursor: pointer; }
|
|
128
|
-
.slider__number { width: 5.5rem; padding: var(--smrt-spacing-2); border: 1px solid var(--smrt-color-outline
|
|
128
|
+
.slider__number { width: 5.5rem; padding: var(--smrt-spacing-2); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface); color: var(--smrt-color-on-surface); }
|
|
129
129
|
input:focus-visible { outline: 2px solid var(--smrt-color-primary); outline-offset: 3px; }
|
|
130
130
|
.slider[data-smrt-highlighted='true'] { outline: 3px solid var(--smrt-color-tertiary); outline-offset: 4px; border-radius: var(--smrt-radius-small); }
|
|
131
131
|
</style>
|
|
@@ -99,7 +99,7 @@ useControlRegistration(() => {
|
|
|
99
99
|
</div></div>
|
|
100
100
|
<style>
|
|
101
101
|
.tags-field { display: grid; gap: var(--smrt-spacing-1); color: var(--smrt-color-on-surface); } label { font: var(--smrt-typography-label-large-font); }
|
|
102
|
-
.tags { display: flex; flex-wrap: wrap; gap: var(--smrt-spacing-1); min-height: 2.5rem; padding: var(--smrt-spacing-1); border: 1px solid var(--smrt-color-outline
|
|
102
|
+
.tags { display: flex; flex-wrap: wrap; gap: var(--smrt-spacing-1); min-height: 2.5rem; padding: var(--smrt-spacing-1); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface); }
|
|
103
103
|
.tag { display: inline-flex; align-items: center; gap: var(--smrt-spacing-1); padding: var(--smrt-spacing-1) var(--smrt-spacing-2); border-radius: var(--smrt-radius-full); background: var(--smrt-color-secondary-container); color: var(--smrt-color-on-secondary-container); }
|
|
104
104
|
.tag button { border: 0; background: transparent; color: inherit; cursor: pointer; } .tags > input { flex: 1; min-width: 8rem; border: 0; outline: 0; background: transparent; color: inherit; }
|
|
105
105
|
:global(.tags[data-smrt-highlighted='true']) { outline: 3px solid var(--smrt-color-tertiary); outline-offset: 4px; }
|
|
@@ -134,7 +134,7 @@ export function getElement(): HTMLTextAreaElement | null {
|
|
|
134
134
|
line-height: var(--smrt-typography-body-medium-line-height, 1.5);
|
|
135
135
|
color: var(--smrt-color-on-surface, #1f2937);
|
|
136
136
|
background-color: var(--smrt-color-surface, #fff);
|
|
137
|
-
border: 1px solid var(--smrt-color-outline
|
|
137
|
+
border: 1px solid var(--smrt-color-outline, #6b7280);
|
|
138
138
|
border-radius: 0.375rem;
|
|
139
139
|
resize: vertical;
|
|
140
140
|
min-height: 80px;
|
|
@@ -199,7 +199,7 @@ $effect(() => {
|
|
|
199
199
|
width: 100%;
|
|
200
200
|
padding: 0.5rem 0.75rem;
|
|
201
201
|
background: var(--smrt-color-surface, white);
|
|
202
|
-
border: 1px solid var(--smrt-color-outline
|
|
202
|
+
border: 1px solid var(--smrt-color-outline, #6b7280);
|
|
203
203
|
border-radius: var(--smrt-radius-small, 0.375rem);
|
|
204
204
|
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
205
205
|
cursor: pointer;
|
|
@@ -270,10 +270,17 @@ $effect(() => {
|
|
|
270
270
|
text-align: left;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
.option:hover
|
|
273
|
+
.option:hover {
|
|
274
|
+
background: var(--smrt-color-surface-container-high, #f3f4f6);
|
|
275
|
+
}
|
|
276
|
+
/* Keyboard focus needs its own indicator: the hover background alone is a
|
|
277
|
+
~1.1:1 change against the list surface in every preset, which is invisible
|
|
278
|
+
and fails WCAG 2.4.11. Inset so it is not clipped by the list's overflow
|
|
279
|
+
(#2322). */
|
|
274
280
|
.option:focus-visible {
|
|
275
281
|
background: var(--smrt-color-surface-container-high, #f3f4f6);
|
|
276
|
-
outline:
|
|
282
|
+
outline: 2px solid var(--smrt-color-primary, #005ac1);
|
|
283
|
+
outline-offset: -2px;
|
|
277
284
|
}
|
|
278
285
|
|
|
279
286
|
.option.selected {
|
|
@@ -179,7 +179,7 @@ $effect(() => {
|
|
|
179
179
|
cursor: pointer;
|
|
180
180
|
padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
|
|
181
181
|
border-radius: var(--smrt-radius-medium, 8px);
|
|
182
|
-
border: 1px solid var(--smrt-color-outline
|
|
182
|
+
border: 1px solid var(--smrt-color-outline, #79747e);
|
|
183
183
|
background: var(--smrt-color-surface);
|
|
184
184
|
color: var(--smrt-color-on-surface);
|
|
185
185
|
}
|
|
@@ -236,10 +236,18 @@ $effect(() => {
|
|
|
236
236
|
color: var(--smrt-color-on-surface);
|
|
237
237
|
white-space: nowrap;
|
|
238
238
|
}
|
|
239
|
-
.dropdown__item:hover:not(:disabled)
|
|
239
|
+
.dropdown__item:hover:not(:disabled) {
|
|
240
|
+
background: var(--smrt-color-surface-container-high);
|
|
241
|
+
}
|
|
242
|
+
/* Keyboard focus needs its own indicator: the hover background alone is a
|
|
243
|
+
~1.1:1 change against the menu surface in every preset (1.00:1 in glass
|
|
244
|
+
light), which is invisible and fails WCAG 2.4.11. The ring is inset so it
|
|
245
|
+
sits inside the item's padding box rather than overlapping the neighbouring
|
|
246
|
+
item or the menu's edge (#2322). */
|
|
240
247
|
.dropdown__item:focus-visible {
|
|
241
248
|
background: var(--smrt-color-surface-container-high);
|
|
242
|
-
outline:
|
|
249
|
+
outline: 2px solid var(--smrt-color-primary);
|
|
250
|
+
outline-offset: -2px;
|
|
243
251
|
}
|
|
244
252
|
.dropdown__item:disabled {
|
|
245
253
|
opacity: 0.5;
|
|
@@ -91,7 +91,7 @@ $effect(() => {
|
|
|
91
91
|
|
|
92
92
|
<style>
|
|
93
93
|
.popover { position: relative; display: inline-flex; }
|
|
94
|
-
.popover__trigger { appearance: none; padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border: 1px solid var(--smrt-color-outline
|
|
94
|
+
.popover__trigger { appearance: none; padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-medium); background: var(--smrt-color-surface); color: var(--smrt-color-on-surface); font: inherit; cursor: pointer; }
|
|
95
95
|
.popover__trigger:focus-visible, .popover__panel:focus-visible { outline: 2px solid var(--smrt-color-primary); outline-offset: 2px; }
|
|
96
96
|
.popover__trigger:disabled { opacity: .5; cursor: not-allowed; }
|
|
97
97
|
.popover__panel { position: absolute; z-index: var(--smrt-z-index-popover, 1000); min-width: 16rem; padding: var(--smrt-spacing-4); border: 1px solid var(--smrt-color-outline-variant); border-radius: var(--smrt-radius-large); background: var(--smrt-color-surface-container); color: var(--smrt-color-on-surface); box-shadow: var(--smrt-elevation-3); }
|
|
@@ -3,7 +3,7 @@ import type { Snippet } from 'svelte';
|
|
|
3
3
|
import { onMount, untrack } from 'svelte';
|
|
4
4
|
import { setThemeContext, type ThemeContext } from './context.svelte.js';
|
|
5
5
|
import { generateThemeVariables } from './css-generator.js';
|
|
6
|
-
import { getTheme, isValidPreset } from './registry.js';
|
|
6
|
+
import { availablePresets, getTheme, isValidPreset } from './registry.js';
|
|
7
7
|
import type {
|
|
8
8
|
ColorScheme,
|
|
9
9
|
Theme,
|
|
@@ -34,6 +34,16 @@ interface Props {
|
|
|
34
34
|
persist?: boolean;
|
|
35
35
|
/** Storage key for persistence */
|
|
36
36
|
storageKey?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Force runtime generation of the full CSS variable set onto the wrapper's
|
|
39
|
+
* inline style (the legacy delivery path). Defaults to `true` for custom
|
|
40
|
+
* registered themes — they have no static stylesheet — and `false` for
|
|
41
|
+
* built-in presets, whose variables ship as static CSS
|
|
42
|
+
* (`themes/styles/<preset>.css`) selected by the `data-theme` /
|
|
43
|
+
* `data-color-scheme` attributes. Leave unset unless you cannot import the
|
|
44
|
+
* static stylesheet for a built-in preset.
|
|
45
|
+
*/
|
|
46
|
+
inlineVariables?: boolean;
|
|
37
47
|
/** Content */
|
|
38
48
|
children: Snippet;
|
|
39
49
|
}
|
|
@@ -47,6 +57,7 @@ let {
|
|
|
47
57
|
paintSurface = true,
|
|
48
58
|
persist = defaultThemeConfig.persist,
|
|
49
59
|
storageKey = defaultThemeConfig.storageKey,
|
|
60
|
+
inlineVariables,
|
|
50
61
|
children,
|
|
51
62
|
}: Props = $props();
|
|
52
63
|
|
|
@@ -85,39 +96,41 @@ const resolvedScheme = $derived<'light' | 'dark'>(
|
|
|
85
96
|
|
|
86
97
|
const isDark = $derived(resolvedScheme === 'dark');
|
|
87
98
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
// Static-first variable delivery: built-in presets ship their full variable
|
|
100
|
+
// set as static stylesheets (`themes/styles/<preset>.css`) selected by the
|
|
101
|
+
// data-theme/data-color-scheme attributes, so the wrapper carries no ~200-var
|
|
102
|
+
// inline style. Custom registered themes have no static file, so they keep the
|
|
103
|
+
// legacy runtime generation (opt built-ins back in with `inlineVariables`).
|
|
104
|
+
const isBuiltInPreset = $derived(
|
|
105
|
+
(availablePresets as string[]).includes(config.preset),
|
|
106
|
+
);
|
|
107
|
+
const useInlineVariables = $derived(inlineVariables ?? !isBuiltInPreset);
|
|
108
|
+
|
|
109
|
+
// Convert to style string — full variable set only on the runtime path;
|
|
110
|
+
// otherwise just the explicit accent/overrides so they layer over static CSS.
|
|
111
|
+
const styleString = $derived.by(() => {
|
|
112
|
+
const declarations: string[] = [];
|
|
113
|
+
|
|
114
|
+
if (useInlineVariables) {
|
|
115
|
+
for (const [key, value] of Object.entries(
|
|
116
|
+
generateThemeVariables(currentTheme, isDark),
|
|
117
|
+
)) {
|
|
118
|
+
declarations.push(`${key}: ${value}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
91
121
|
|
|
92
122
|
// Apply primary color override
|
|
93
123
|
if (config.primaryColor) {
|
|
94
|
-
|
|
124
|
+
declarations.push(`--smrt-color-primary: ${config.primaryColor}`);
|
|
95
125
|
// Recalculate dependent colors would go here in a full implementation
|
|
96
126
|
}
|
|
97
127
|
|
|
98
|
-
// Apply border radius override
|
|
99
|
-
if (config.borderRadius && config.borderRadius !== 'md') {
|
|
100
|
-
// The border radius values are already in CSS vars, but we could
|
|
101
|
-
// add logic here to override specific radius values
|
|
102
|
-
}
|
|
103
|
-
|
|
104
128
|
// Apply custom overrides
|
|
105
|
-
|
|
106
|
-
});
|
|
129
|
+
for (const [key, value] of Object.entries(config.overrides ?? {})) {
|
|
130
|
+
declarations.push(`${key}: ${value}`);
|
|
131
|
+
}
|
|
107
132
|
|
|
108
|
-
|
|
109
|
-
const styleString = $derived.by(() => {
|
|
110
|
-
return Object.entries(cssVariables)
|
|
111
|
-
.map(([key, value]) => {
|
|
112
|
-
const isExplicitOverride =
|
|
113
|
-
Object.hasOwn(config.overrides ?? {}, key) ||
|
|
114
|
-
(key === '--smrt-color-primary' && config.primaryColor);
|
|
115
|
-
const bootstrapKey = key.replace(/^--smrt-/, '--smrt-bootstrap-');
|
|
116
|
-
return `${key}: ${
|
|
117
|
-
isExplicitOverride ? value : `var(${bootstrapKey}, ${value})`
|
|
118
|
-
}`;
|
|
119
|
-
})
|
|
120
|
-
.join('; ');
|
|
133
|
+
return declarations.join('; ');
|
|
121
134
|
});
|
|
122
135
|
|
|
123
136
|
// Theme state for context
|
|
@@ -258,21 +271,6 @@ $effect(() => {
|
|
|
258
271
|
}
|
|
259
272
|
});
|
|
260
273
|
|
|
261
|
-
// The pre-paint aliases only bridge SSR to the hydrated provider. Remove them
|
|
262
|
-
// after the provider has applied the persisted config so later runtime theme
|
|
263
|
-
// changes use the component's reactive variables normally.
|
|
264
|
-
$effect(() => {
|
|
265
|
-
if (typeof document !== 'undefined' && mounted) {
|
|
266
|
-
const html = document.documentElement;
|
|
267
|
-
for (const property of Array.from(html.style)) {
|
|
268
|
-
if (property.startsWith('--smrt-bootstrap-')) {
|
|
269
|
-
html.style.removeProperty(property);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
html.removeAttribute('data-smrt-theme-bootstrap');
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
274
|
// Sync props to state — but only on actual prop changes. config is seeded
|
|
277
275
|
// from the initial props above and onMount applies persisted preferences, so
|
|
278
276
|
// re-asserting the initial prop values here would clobber the persisted
|
|
@@ -339,7 +337,7 @@ $effect(() => {
|
|
|
339
337
|
class:dark={isDark}
|
|
340
338
|
class:no-paint={!paintSurface}
|
|
341
339
|
class:smrt-theme-glass={config.preset === 'glass'}
|
|
342
|
-
style={styleString}
|
|
340
|
+
style={styleString || undefined}
|
|
343
341
|
data-theme={config.preset}
|
|
344
342
|
data-color-scheme={resolvedScheme}
|
|
345
343
|
>
|
|
@@ -21,6 +21,16 @@ interface Props {
|
|
|
21
21
|
persist?: boolean;
|
|
22
22
|
/** Storage key for persistence */
|
|
23
23
|
storageKey?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Force runtime generation of the full CSS variable set onto the wrapper's
|
|
26
|
+
* inline style (the legacy delivery path). Defaults to `true` for custom
|
|
27
|
+
* registered themes — they have no static stylesheet — and `false` for
|
|
28
|
+
* built-in presets, whose variables ship as static CSS
|
|
29
|
+
* (`themes/styles/<preset>.css`) selected by the `data-theme` /
|
|
30
|
+
* `data-color-scheme` attributes. Leave unset unless you cannot import the
|
|
31
|
+
* static stylesheet for a built-in preset.
|
|
32
|
+
*/
|
|
33
|
+
inlineVariables?: boolean;
|
|
24
34
|
/** Content */
|
|
25
35
|
children: Snippet;
|
|
26
36
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemeProvider.svelte.d.ts","sourceRoot":"","sources":["../../src/themes/ThemeProvider.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAKtC,OAAO,KAAK,EACV,WAAW,EAEX,WAAW,EACX,WAAW,EAEZ,MAAM,YAAY,CAAC;AAIpB,UAAU,KAAK;IACb,0BAA0B;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8BAA8B;IAC9B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+BAA+B;IAC/B,YAAY,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,CAAC;IAC3C,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB;
|
|
1
|
+
{"version":3,"file":"ThemeProvider.svelte.d.ts","sourceRoot":"","sources":["../../src/themes/ThemeProvider.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAKtC,OAAO,KAAK,EACV,WAAW,EAEX,WAAW,EACX,WAAW,EAEZ,MAAM,YAAY,CAAC;AAIpB,UAAU,KAAK;IACb,0BAA0B;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8BAA8B;IAC9B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+BAA+B;IAC/B,YAAY,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,CAAC;IAC3C,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB;AA+SD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control-boundary contrast floor, every preset (#2322).
|
|
3
|
+
*
|
|
4
|
+
* WCAG 2.2 SC 1.4.11 requires the visual boundary that identifies a UI
|
|
5
|
+
* component to clear 3:1 against its adjacent background. The form primitives
|
|
6
|
+
* used to draw their resting border with `outline-variant` — the divider role,
|
|
7
|
+
* which measures 1.2–1.7:1 in every preset — so an input's edge was effectively
|
|
8
|
+
* invisible. They now consume `outline`, and this test pins the other half of
|
|
9
|
+
* that contract: `outline` must actually be a boundary-strength value in every
|
|
10
|
+
* preset and scheme, on every surface a control can sit on.
|
|
11
|
+
*
|
|
12
|
+
* `outlineVariant` is deliberately NOT held to this floor. It is the quiet
|
|
13
|
+
* hairline for dividers, panel edges, and readout rows; raising it would erase
|
|
14
|
+
* the distinction the two tokens exist to draw.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, expect, it } from 'vitest';
|
|
17
|
+
import { themes } from '../registry.js';
|
|
18
|
+
/** Every surface role a control can be placed on. */
|
|
19
|
+
const SURFACES = [
|
|
20
|
+
'surface',
|
|
21
|
+
'surfaceVariant',
|
|
22
|
+
'surfaceContainer',
|
|
23
|
+
'surfaceContainerLow',
|
|
24
|
+
'surfaceContainerHigh',
|
|
25
|
+
'surfaceContainerHighest',
|
|
26
|
+
'surfaceContainerLowest',
|
|
27
|
+
'surfaceDim',
|
|
28
|
+
'surfaceBright',
|
|
29
|
+
'background',
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Parse `#rrggbb` or `rgba(r, g, b, a)` into a straight-alpha colour.
|
|
33
|
+
*
|
|
34
|
+
* The glass preset expresses its surfaces and outlines as `rgba(...)`, so a
|
|
35
|
+
* hex-only parser silently skips exactly the preset whose translucency makes
|
|
36
|
+
* contrast hardest to reason about — which is how glass sat at 1.42:1 unnoticed.
|
|
37
|
+
*/
|
|
38
|
+
function parseColor(value) {
|
|
39
|
+
const hex = value.trim().match(/^#([0-9a-f]{6})$/i);
|
|
40
|
+
if (hex) {
|
|
41
|
+
const n = Number.parseInt(hex[1], 16);
|
|
42
|
+
return { rgb: [(n >> 16) & 255, (n >> 8) & 255, n & 255], alpha: 1 };
|
|
43
|
+
}
|
|
44
|
+
const rgba = value
|
|
45
|
+
.trim()
|
|
46
|
+
.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.]+))?\s*\)$/i);
|
|
47
|
+
if (rgba) {
|
|
48
|
+
return {
|
|
49
|
+
rgb: [Number(rgba[1]), Number(rgba[2]), Number(rgba[3])],
|
|
50
|
+
alpha: rgba[4] === undefined ? 1 : Number(rgba[4]),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/** Composite `value` over an opaque backdrop, resolving any alpha. */
|
|
56
|
+
function flatten(value, backdrop) {
|
|
57
|
+
const parsed = parseColor(value);
|
|
58
|
+
if (!parsed)
|
|
59
|
+
return null;
|
|
60
|
+
const { rgb, alpha } = parsed;
|
|
61
|
+
return rgb.map((c, i) => Math.round(c * alpha + backdrop[i] * (1 - alpha)));
|
|
62
|
+
}
|
|
63
|
+
function relativeLuminance([r, g, b]) {
|
|
64
|
+
const [rr, gg, bb] = [r, g, b].map((n) => {
|
|
65
|
+
const s = n / 255;
|
|
66
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
67
|
+
});
|
|
68
|
+
return 0.2126 * rr + 0.7152 * gg + 0.0722 * bb;
|
|
69
|
+
}
|
|
70
|
+
function contrast(a, b) {
|
|
71
|
+
const [hi, lo] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
|
|
72
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
73
|
+
}
|
|
74
|
+
describe('control boundaries clear the 3:1 non-text floor (#2322)', () => {
|
|
75
|
+
for (const theme of Object.values(themes)) {
|
|
76
|
+
for (const scheme of ['light', 'dark']) {
|
|
77
|
+
const palette = theme[scheme];
|
|
78
|
+
it(`${theme.id} ${scheme}: outline is boundary-strength on every surface`, () => {
|
|
79
|
+
// Translucent surfaces resolve against the scheme's own background,
|
|
80
|
+
// which is the only backdrop the theme can promise.
|
|
81
|
+
const page = flatten(palette.background, [255, 255, 255]);
|
|
82
|
+
expect(page, `${theme.id} ${scheme} background must be parseable`).not.toBeNull();
|
|
83
|
+
const failures = [];
|
|
84
|
+
for (const surfaceName of SURFACES) {
|
|
85
|
+
// Assert each parse BEFORE it is used as the next one's backdrop.
|
|
86
|
+
// Folding these together would let an unparseable surface reach
|
|
87
|
+
// `flatten` as a null backdrop, and the test would die on a
|
|
88
|
+
// TypeError instead of naming the token that could not be read.
|
|
89
|
+
const surface = flatten(palette[surfaceName], page);
|
|
90
|
+
expect(surface, `${theme.id} ${scheme} ${surfaceName} must be parseable`).not.toBeNull();
|
|
91
|
+
const outline = flatten(palette.outline, surface);
|
|
92
|
+
expect(outline, `${theme.id} ${scheme} outline must be parseable`).not.toBeNull();
|
|
93
|
+
const ratio = contrast(outline, surface);
|
|
94
|
+
if (ratio < 3)
|
|
95
|
+
failures.push(`${surfaceName} ${ratio.toFixed(2)}:1`);
|
|
96
|
+
}
|
|
97
|
+
expect(failures, `${theme.id} ${scheme} outline below 3:1`).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
it('keeps outline and outlineVariant as distinct roles', () => {
|
|
102
|
+
// If a preset ever "fixes" its bezel by setting it equal to the boundary,
|
|
103
|
+
// dividers become as loud as control edges and the two-token system
|
|
104
|
+
// collapses. The hairline is allowed to be quiet — it just may not BE the
|
|
105
|
+
// boundary token.
|
|
106
|
+
for (const theme of Object.values(themes)) {
|
|
107
|
+
for (const scheme of ['light', 'dark']) {
|
|
108
|
+
expect(theme[scheme].outlineVariant, `${theme.id} ${scheme} outlineVariant must stay distinct from outline`).not.toBe(theme[scheme].outline);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -71,6 +71,48 @@ describe('theme CSS generator', () => {
|
|
|
71
71
|
// Deeper black shadows above level 1.
|
|
72
72
|
expect(dark['--smrt-elevation-5']).toContain('rgba(0, 0, 0, 0.25)');
|
|
73
73
|
});
|
|
74
|
+
it('matches every static preset CSS on colour values (drift guard)', () => {
|
|
75
|
+
// The elevation guard below caught shadows drifting; colours drifted too
|
|
76
|
+
// and nothing noticed — smrt.css carried `--smrt-theme-name: SMRT` long
|
|
77
|
+
// after the theme was renamed `s-m-r-t` (#2322). The alias test only
|
|
78
|
+
// checks that token NAMES appear in each stylesheet, so a stale VALUE
|
|
79
|
+
// sails through it. Compare the values themselves, for every preset.
|
|
80
|
+
const files = {
|
|
81
|
+
material: 'material.css',
|
|
82
|
+
glass: 'glass.css',
|
|
83
|
+
studio: 'studio.css',
|
|
84
|
+
smrt: 'smrt.css',
|
|
85
|
+
happyvertical: 'happyvertical.css',
|
|
86
|
+
};
|
|
87
|
+
const drift = [];
|
|
88
|
+
for (const [id, file] of Object.entries(files)) {
|
|
89
|
+
const css = readFileSync(join(process.cwd(), 'src/themes/styles', file), 'utf8');
|
|
90
|
+
for (const scheme of ['light', 'dark']) {
|
|
91
|
+
const selector = `[data-theme="${id}"][data-color-scheme="${scheme}"]`;
|
|
92
|
+
const start = css.indexOf(selector);
|
|
93
|
+
expect(start, `${file} must define ${selector}`).toBeGreaterThan(-1);
|
|
94
|
+
const open = css.indexOf('{', start);
|
|
95
|
+
const body = css.slice(open + 1, css.indexOf('\n}', open));
|
|
96
|
+
const defined = {};
|
|
97
|
+
for (const match of body.matchAll(/(--smrt-[a-z0-9-]+):\s*([^;]+);/gi)) {
|
|
98
|
+
defined[match[1]] = match[2].trim();
|
|
99
|
+
}
|
|
100
|
+
const generated = generateThemeVariables(getTheme(id), scheme === 'dark');
|
|
101
|
+
for (const [token, value] of Object.entries(generated)) {
|
|
102
|
+
if (!token.startsWith('--smrt-color-') &&
|
|
103
|
+
token !== '--smrt-theme-name')
|
|
104
|
+
continue;
|
|
105
|
+
// Only compare tokens the stylesheet actually declares; absence is
|
|
106
|
+
// the alias test's job, not this one's.
|
|
107
|
+
if (defined[token] !== undefined &&
|
|
108
|
+
defined[token] !== String(value)) {
|
|
109
|
+
drift.push(`${file} ${scheme} ${token}: css=${defined[token]} generator=${value}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
expect(drift).toEqual([]);
|
|
115
|
+
});
|
|
74
116
|
it('matches the static studio.css for both schemes (drift guard)', () => {
|
|
75
117
|
// The static-CSS delivery path and the JS generator must agree on VALUES,
|
|
76
118
|
// not just token names — this is what let the studio dark shadows diverge.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static stylesheet lockstep (#1586, all presets).
|
|
3
|
+
*
|
|
4
|
+
* Every built-in preset ships a static stylesheet whose two
|
|
5
|
+
* `[data-theme][data-color-scheme]` token blocks must be the verbatim output
|
|
6
|
+
* of the theme generator — ThemeProvider defaults built-in presets to this
|
|
7
|
+
* static delivery path, so drift between the theme definitions and the
|
|
8
|
+
* shipped CSS would silently change rendered values. Run
|
|
9
|
+
* `pnpm --filter @happyvertical/smrt-ui generate:themes` to regenerate after
|
|
10
|
+
* editing a preset definition.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { describe, expect, it } from 'vitest';
|
|
15
|
+
import { generateThemeCSS } from '../css-generator.js';
|
|
16
|
+
import { availablePresets, getTheme } from '../registry.js';
|
|
17
|
+
// Resolved from process.cwd() (NOT import.meta.url): the smrt-vitest plugin
|
|
18
|
+
// runs each package's suite with cwd === the package root.
|
|
19
|
+
describe('static stylesheets match the generator verbatim (#1586)', () => {
|
|
20
|
+
for (const preset of availablePresets) {
|
|
21
|
+
const theme = getTheme(preset);
|
|
22
|
+
const staticCss = readFileSync(join(process.cwd(), `src/themes/styles/${preset}.css`), 'utf8');
|
|
23
|
+
for (const [scheme, isDark] of [
|
|
24
|
+
['light', false],
|
|
25
|
+
['dark', true],
|
|
26
|
+
]) {
|
|
27
|
+
it(`${preset} ${scheme} block matches generateThemeCSS output`, () => {
|
|
28
|
+
expect(staticCss).toContain(generateThemeCSS(theme, isDark));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
@@ -92,18 +92,56 @@ describe('ThemeProvider persistence', () => {
|
|
|
92
92
|
expect(document.documentElement.getAttribute('data-color-scheme')).toBe('light');
|
|
93
93
|
});
|
|
94
94
|
});
|
|
95
|
-
it('
|
|
95
|
+
it('renders no inline variable payload for built-in presets (static-first)', () => {
|
|
96
96
|
mockSystemDark(false);
|
|
97
97
|
const { container } = render(ThemeProvider, {
|
|
98
98
|
props: { children: child() },
|
|
99
99
|
});
|
|
100
|
+
const root = container.querySelector('.smrt-theme-root');
|
|
101
|
+
const style = root?.getAttribute('style') ?? '';
|
|
102
|
+
// Built-in presets ship their full variable set as static stylesheets
|
|
103
|
+
// selected by the data attributes — the wrapper carries no ~200-var style.
|
|
104
|
+
expect(style).not.toContain('--smrt-color-background');
|
|
105
|
+
expect(root).toHaveAttribute('data-theme');
|
|
106
|
+
expect(root).toHaveAttribute('data-color-scheme');
|
|
107
|
+
});
|
|
108
|
+
it('applies only the explicit accent/overrides inline for built-in presets', () => {
|
|
109
|
+
mockSystemDark(false);
|
|
110
|
+
const { container } = render(ThemeProvider, {
|
|
111
|
+
props: {
|
|
112
|
+
primaryColor: '#123456',
|
|
113
|
+
overrides: { '--smrt-color-tertiary': '#654321' },
|
|
114
|
+
children: child(),
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const style = container.querySelector('.smrt-theme-root')?.getAttribute('style') ?? '';
|
|
118
|
+
expect(style).toContain('--smrt-color-primary: #123456');
|
|
119
|
+
expect(style).toContain('--smrt-color-tertiary: #654321');
|
|
120
|
+
expect(style).not.toContain('--smrt-color-background');
|
|
121
|
+
});
|
|
122
|
+
it('keeps runtime variable generation for custom registered themes', async () => {
|
|
123
|
+
const { createTheme, registerTheme } = await import('../create-theme.js');
|
|
124
|
+
registerTheme(createTheme({
|
|
125
|
+
id: 'provider-test-brand',
|
|
126
|
+
name: 'Provider Test Brand',
|
|
127
|
+
light: { primary: '#336699', background: '#fefefe' },
|
|
128
|
+
dark: { primary: '#99ccff', background: '#101820' },
|
|
129
|
+
}));
|
|
130
|
+
mockSystemDark(false);
|
|
131
|
+
const { container } = render(ThemeProvider, {
|
|
132
|
+
props: { preset: 'provider-test-brand', children: child() },
|
|
133
|
+
});
|
|
134
|
+
const style = container.querySelector('.smrt-theme-root')?.getAttribute('style') ?? '';
|
|
135
|
+
// No static stylesheet can exist for a registered custom theme, so the
|
|
136
|
+
// provider falls back to the legacy runtime-generated variable set.
|
|
137
|
+
expect(style).toContain('--smrt-color-background: #fefefe');
|
|
138
|
+
});
|
|
139
|
+
it('opts a built-in preset back into runtime generation via inlineVariables', () => {
|
|
140
|
+
mockSystemDark(false);
|
|
141
|
+
const { container } = render(ThemeProvider, {
|
|
142
|
+
props: { preset: 'material', inlineVariables: true, children: child() },
|
|
143
|
+
});
|
|
100
144
|
const style = container.querySelector('.smrt-theme-root')?.getAttribute('style') ?? '';
|
|
101
|
-
|
|
102
|
-
'--smrt',
|
|
103
|
-
'bootstrap',
|
|
104
|
-
'color',
|
|
105
|
-
'background',
|
|
106
|
-
].join('-');
|
|
107
|
-
expect(style).toContain(`--smrt-color-background: var(${bootstrapBackground},`);
|
|
145
|
+
expect(style).toContain('--smrt-color-background');
|
|
108
146
|
});
|
|
109
147
|
});
|