@urbicon-ui/blocks 6.25.0 → 6.26.1

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.
@@ -147,7 +147,7 @@
147
147
  primary: 'text-text-on-primary',
148
148
  secondary: 'text-text-on-primary',
149
149
  success: 'text-text-on-primary',
150
- warning: 'text-text-on-surface',
150
+ warning: 'text-text-on-warning',
151
151
  danger: 'text-text-on-primary',
152
152
  neutral: 'text-text-on-primary'
153
153
  };
@@ -154,7 +154,7 @@ export const alertVariants = tv({
154
154
  {
155
155
  intent: 'warning',
156
156
  variant: 'filled',
157
- class: { base: 'bg-warning text-text-on-surface', dismissButton: 'text-text-on-surface' }
157
+ class: { base: 'bg-warning text-text-on-warning', dismissButton: 'text-text-on-warning' }
158
158
  },
159
159
  {
160
160
  intent: 'danger',
@@ -307,7 +307,7 @@ export const badgeVariants = tv({
307
307
  intent: 'warning',
308
308
  variant: 'filled',
309
309
  class: {
310
- base: 'bg-warning text-text-on-surface border-warning'
310
+ base: 'bg-warning text-text-on-warning border-warning'
311
311
  }
312
312
  },
313
313
  {
@@ -175,7 +175,7 @@ export const buttonVariants = tv({
175
175
  intent: 'warning',
176
176
  variant: 'filled',
177
177
  class: {
178
- base: 'bg-warning text-text-on-surface border-warning hover:bg-warning-hover active:bg-warning-active'
178
+ base: 'bg-warning text-text-on-warning border-warning hover:bg-warning-hover active:bg-warning-active'
179
179
  }
180
180
  },
181
181
  {
@@ -369,7 +369,7 @@ export const buttonVariants = tv({
369
369
  active: true,
370
370
  variant: 'outlined',
371
371
  intent: 'warning',
372
- class: { base: 'bg-warning text-text-on-surface border-warning hover:bg-warning-hover' }
372
+ class: { base: 'bg-warning text-text-on-warning border-warning hover:bg-warning-hover' }
373
373
  },
374
374
  {
375
375
  active: true,
@@ -146,7 +146,7 @@ export const checkboxVariants = tv({
146
146
  checked: true,
147
147
  intent: 'warning',
148
148
  class: {
149
- box: 'bg-warning border-warning text-text-on-surface group-hover:bg-warning-hover group-active:bg-warning-active'
149
+ box: 'bg-warning border-warning text-text-on-warning group-hover:bg-warning-hover group-active:bg-warning-active'
150
150
  }
151
151
  },
152
152
  {
@@ -189,7 +189,7 @@ export const checkboxVariants = tv({
189
189
  indeterminate: true,
190
190
  intent: 'warning',
191
191
  class: {
192
- box: 'bg-warning border-warning text-text-on-surface group-hover:bg-warning-hover group-active:bg-warning-active'
192
+ box: 'bg-warning border-warning text-text-on-warning group-hover:bg-warning-hover group-active:bg-warning-active'
193
193
  }
194
194
  },
195
195
  {
@@ -279,6 +279,10 @@
279
279
  easing: motion.easing
280
280
  }}
281
281
  ></div>
282
+ <!-- tabindex=-1: focus target of last resort — focusFirstElement falls back
283
+ to the panel when children contain nothing focusable (untitled dialog),
284
+ keeping the element-level ESC handler + Tab trap live. Negative, so it
285
+ never joins the Tab cycle. -->
282
286
  <div
283
287
  bind:this={panelEl}
284
288
  class={unstyled
@@ -286,6 +290,7 @@
286
290
  : styles.panel({ class: [slotClasses?.panel, className] })}
287
291
  data-intent={intent}
288
292
  role="document"
293
+ tabindex="-1"
289
294
  style:translate={dragX !== 0 || dragY !== 0 ? `${dragX}px ${dragY}px` : undefined}
290
295
  transition:scale={{
291
296
  duration: motion.enterDuration,
@@ -218,6 +218,10 @@
218
218
  easing: motion.easing
219
219
  }}
220
220
  ></div>
221
+ <!-- tabindex=-1: focus target of last resort — focusFirstElement falls back
222
+ to the panel when children contain nothing focusable (untitled drawer
223
+ with hideCloseButton), keeping the element-level ESC handler + Tab trap
224
+ live. Negative, so it never joins the Tab cycle. -->
221
225
  <div
222
226
  bind:this={panelElement}
223
227
  class={unstyled
@@ -225,6 +229,7 @@
225
229
  : styles.panel({ class: [slotClasses?.panel, className] })}
226
230
  data-intent={intent}
227
231
  role="document"
232
+ tabindex="-1"
228
233
  transition:fly={flyParams}
229
234
  onoutroend={handleOutroEnd}
230
235
  >
@@ -157,7 +157,7 @@ export const radioItemVariants = tv({
157
157
  {
158
158
  checked: true,
159
159
  intent: 'warning',
160
- class: { indicator: 'bg-warning border-warning', dot: 'bg-neutral-900' }
160
+ class: { indicator: 'bg-warning border-warning', dot: 'bg-text-on-warning' }
161
161
  },
162
162
  {
163
163
  checked: true,
@@ -122,7 +122,7 @@ export const stepperVariants = tv({
122
122
  label: 'text-danger'
123
123
  },
124
124
  warning: {
125
- indicator: 'border-warning bg-warning text-text-on-surface',
125
+ indicator: 'border-warning bg-warning text-text-on-warning',
126
126
  label: 'text-warning-emphasis'
127
127
  }
128
128
  },
@@ -53,7 +53,7 @@ export const tooltipVariants = tv({
53
53
  secondary: { base: 'bg-secondary text-text-on-primary' },
54
54
  info: { base: 'bg-info text-text-on-primary' },
55
55
  success: { base: 'bg-success text-text-on-primary' },
56
- warning: { base: 'bg-warning text-text-on-surface' },
56
+ warning: { base: 'bg-warning text-text-on-warning' },
57
57
  danger: { base: 'bg-danger text-text-on-primary' },
58
58
  neutral: { base: 'bg-surface-inverted text-text-inverted' }
59
59
  },
@@ -52,20 +52,20 @@
52
52
  --color-warm-neutral-900: oklch(0.2 0.008 45);
53
53
  --color-warm-neutral-950: oklch(0.12 0.006 45);
54
54
 
55
- /* Darkened *-500 / *-600 / *-700 so text-on-primary (white) passes WCAG
56
- AA (4.5:1) on the solid intent backgrounds **in light mode**, where
57
- --color-<intent> resolves to the -500/-600 shades. Previous values had
58
- L=0.65 which produced ~3:1 contrast — below AA. Warning keeps its
59
- yellow hue at L=0.75 because it pairs with text-on-surface (dark),
60
- not white.
61
-
62
- Scope, measured (contrast.test.ts, 2026-07-14): this holds for light
63
- mode only. In dark mode --color-<intent> resolves to the *lighter*
64
- -400/-300/-200 shades while --color-text-on-primary stays white, so
65
- every filled intent lands at 1.5–4.1:1. That is a semantic.css issue
66
- (text-on-primary is not mode-aware), not something these ramps can fix
67
- see the KNOWN SHORTFALLS block in contrast.test.ts. Do not "fix" it
68
- by lightening these ramps; that would break light mode instead. */
55
+ /* Darkened *-500 / *-600 / *-700 so text-on-primary (white in light mode)
56
+ passes WCAG AA (4.5:1) on the solid intent backgrounds **in light
57
+ mode**, where --color-<intent> resolves to the -500/-600 shades.
58
+ Previous values had L=0.65 which produced ~3:1 contrast — below AA.
59
+ Warning is the one inverted ramp: its fill stays light in both modes
60
+ (500 keeps its yellow at L=0.75) and pairs with the dark,
61
+ non-mode-aware `text-on-warning` (= warning-950), not white.
62
+
63
+ In dark mode --color-<intent> resolves to the *lighter* -400/-300/-200
64
+ shades and pairs with the dark branch of the mode-aware
65
+ `text-on-primary` (semantic.css, 2026-07-14). Both directions are
66
+ measured across all six themes by style/contrast.test.ts. Do not
67
+ lighten these ramps to "help" dark mode; light mode is the branch
68
+ these stops guarantee. */
69
69
  --color-success-50: oklch(0.95 0.03 140);
70
70
  --color-success-100: oklch(0.9 0.05 140);
71
71
  --color-success-200: oklch(0.82 0.08 140);
@@ -85,7 +85,14 @@
85
85
  --color-warning-400: oklch(0.66 0.14 80);
86
86
  --color-warning-500: oklch(0.75 0.15 80);
87
87
  --color-warning-600: oklch(0.65 0.15 80);
88
- --color-warning-700: oklch(0.55 0.13 80);
88
+ /* L=0.59, not the ramp's usual 0.55: warning-700 is the light-mode press
89
+ fill (--color-warning-active) under the dark `text-on-warning`
90
+ (warning-950). Warning's ramp is inverted — pressing moves TOWARD the
91
+ dark label — and at L=0.55 the press state measured 3.90–4.05:1, under
92
+ AA. 0.59 is the lowest 0.01 step at which every themed hue clears
93
+ 4.5:1 (0.58 still fails forest's hue 60 at 4.46:1). Mirrored by the
94
+ forest/sunset ramps; guarded by contrast.test.ts. */
95
+ --color-warning-700: oklch(0.59 0.13 80);
89
96
  --color-warning-800: oklch(0.45 0.11 80);
90
97
  --color-warning-900: oklch(0.25 0.08 80);
91
98
  --color-warning-950: oklch(0.15 0.05 80);
@@ -54,12 +54,13 @@
54
54
  --color-text-tertiary: light-dark(var(--color-neutral-600), var(--color-neutral-300));
55
55
  --color-text-quaternary: light-dark(var(--color-neutral-500), var(--color-neutral-400));
56
56
  --color-text-disabled: light-dark(var(--color-neutral-300), var(--color-neutral-600));
57
- /* The two on-colors below pair text with a *fill* rather than a surface.
58
- Their fills are dark in light mode and light in dark mode, so the label
59
- has to travel the opposite way white, then near-black. They are
60
- deliberately NOT aliased to one another (`text-on-primary: var(
61
- --color-text-on-dark)`), even though they carry the same value today —
62
- they answer to different fills and must stay independently overridable:
57
+ /* The on-colors below pair text with a *fill* rather than a surface.
58
+ text-on-dark and text-on-primary answer fills that are dark in light
59
+ mode and light in dark mode, so the label has to travel the opposite
60
+ way — white, then near-black. They are deliberately NOT aliased to one
61
+ another (`text-on-primary: var(--color-text-on-dark)`), even though
62
+ they carry the same value today — they answer to different fills and
63
+ must stay independently overridable:
63
64
 
64
65
  - text-on-dark is the partner of the avatar identity palette, whose 12
65
66
  slots hold ONE lightness per mode (0.45 / 0.72) by construction.
@@ -75,10 +76,21 @@
75
76
  Material-3 `onPrimary` pattern). White here was unconditional until
76
77
  2026-07-14, which put 125 of the 126 dark-mode intent fills under WCAG AA
77
78
  — down to 1.51:1, and getting *worse* on press. The mode-aware branch
78
- clears all 108 fills this token governs; `warning` is not one of them (it
79
- pairs with text-on-surface and is still under AA in dark mode see the
80
- KNOWN SHORTFALLS block in style/contrast.test.ts, which guards all of it). */
79
+ clears all 108 fills this token governs; `warning` is not one of them
80
+ its fill is light in BOTH modes, so it carries its own non-mode-aware
81
+ on-color below (`--color-text-on-warning`). */
81
82
  --color-text-on-primary: light-dark(var(--color-neutral-0), var(--color-neutral-900));
83
+ /* Warning's own on-color. Warning is the one intent whose fill stays a
84
+ light amber in BOTH modes (base/hover/active ride L 0.59–0.82), so unlike
85
+ text-on-primary this token is deliberately NOT mode-aware — the label is
86
+ a warm dark in both modes. It rides the warning ramp's own 950 stop
87
+ rather than a neutral, so theme re-hues (forest 60, sunset 92) flow
88
+ through and the label reads as part of the swatch instead of soot on top
89
+ of it. Until 2026-07-20 filled warning paired with `text-on-surface` — a
90
+ *surface* token that flips light in dark mode — which put all 18
91
+ dark-mode warning fills at 1.51–2.80:1. Measured across every theme ×
92
+ mode × state by style/contrast.test.ts. */
93
+ --color-text-on-warning: var(--color-warning-950);
82
94
  --color-text-on-surface: light-dark(var(--color-neutral-900), var(--color-neutral-100));
83
95
 
84
96
  /* === PRIMARY INTENT ===
@@ -86,7 +86,9 @@
86
86
  --color-warning-400: oklch(0.66 0.14 60);
87
87
  --color-warning-500: oklch(0.75 0.15 60);
88
88
  --color-warning-600: oklch(0.65 0.15 60);
89
- --color-warning-700: oklch(0.55 0.13 60);
89
+ /* 0.59 mirrors foundation.css: light-mode press fill under the dark
90
+ text-on-warning — hue 60 is the binding case (0.58 = 4.46:1, under AA). */
91
+ --color-warning-700: oklch(0.59 0.13 60);
90
92
  --color-warning-800: oklch(0.45 0.11 60);
91
93
  --color-warning-900: oklch(0.25 0.08 60);
92
94
  --color-warning-950: oklch(0.15 0.05 60);
@@ -73,7 +73,9 @@
73
73
  --color-warning-400: oklch(0.66 0.14 92);
74
74
  --color-warning-500: oklch(0.75 0.15 92);
75
75
  --color-warning-600: oklch(0.65 0.15 92);
76
- --color-warning-700: oklch(0.55 0.13 92);
76
+ /* 0.59 mirrors foundation.css: light-mode press fill under the dark
77
+ text-on-warning (AA floor across all themed hues). */
78
+ --color-warning-700: oklch(0.59 0.13 92);
77
79
  --color-warning-800: oklch(0.45 0.11 92);
78
80
  --color-warning-900: oklch(0.25 0.08 92);
79
81
  --color-warning-950: oklch(0.15 0.05 92);
@@ -99,7 +99,7 @@ const foundationColors = {
99
99
  '400': 'oklch(0.66 0.14 80)',
100
100
  '500': 'oklch(0.75 0.15 80)',
101
101
  '600': 'oklch(0.65 0.15 80)',
102
- '700': 'oklch(0.55 0.13 80)',
102
+ '700': 'oklch(0.59 0.13 80)',
103
103
  '800': 'oklch(0.45 0.11 80)',
104
104
  '900': 'oklch(0.25 0.08 80)',
105
105
  '950': 'oklch(0.15 0.05 80)'
@@ -155,6 +155,10 @@ const semanticTokens = {
155
155
  disabled: { value: '{color.neutral.300}', description: 'Disabled text' },
156
156
  'on-dark': { value: '{color.neutral.0}', description: 'Text on dark surfaces' },
157
157
  'on-primary': { value: '{color.neutral.0}', description: 'Text on primary color bg' },
158
+ 'on-warning': {
159
+ value: '{color.warning.950}',
160
+ description: 'Text on the warning fill (warm dark in both modes)'
161
+ },
158
162
  'on-surface': { value: '{color.neutral.900}', description: 'Text on tinted surfaces' }
159
163
  },
160
164
  border: {
@@ -18,8 +18,18 @@ export function trapFocus(event, container) {
18
18
  if (!isBrowser)
19
19
  return;
20
20
  const focusable = getFocusableElements(container);
21
- if (focusable.length === 0)
21
+ if (focusable.length === 0) {
22
+ // Nothing tabbable inside the overlay (untitled Dialog with static
23
+ // children, Drawer with hideCloseButton): park focus on the container —
24
+ // the panel that focusFirstElement's fallback focused via tabindex="-1" —
25
+ // instead of letting Tab walk out of the modal. Browsers with a real top
26
+ // layer get this from `showModal()` inertness; the guard keeps the trap's
27
+ // contract honest everywhere else. `contains` includes the container
28
+ // itself, so focus already parked there stays put.
29
+ if (container?.contains(document.activeElement))
30
+ event.preventDefault();
22
31
  return;
32
+ }
23
33
  const first = focusable[0];
24
34
  const last = focusable[focusable.length - 1];
25
35
  if (event.shiftKey) {
@@ -74,8 +84,20 @@ export function focusFirstElement(container) {
74
84
  return;
75
85
  tick().then(() => {
76
86
  const focusable = getFocusableElements(container);
77
- if (focusable.length > 0)
87
+ if (focusable.length > 0) {
78
88
  focusable[0].focus();
89
+ return;
90
+ }
91
+ // No focusable descendant — e.g. an untitled Dialog whose children are
92
+ // static text (no header, so no close button), or a Drawer with
93
+ // hideCloseButton. Fall back to the container itself, which the overlay
94
+ // panels make programmatically focusable via tabindex="-1" (excluded from
95
+ // getFocusableElements, so it never joins the Tab cycle). Without this,
96
+ // focus stays on <body>: the overlay's element-level keydown never fires,
97
+ // leaving ESC to the window fallback and the Tab trap inert until the
98
+ // user clicks in — a WCAG 2.1.2 problem. Mirrors the native <dialog>
99
+ // focusing steps, which likewise fall back to the dialog element.
100
+ container?.focus();
79
101
  });
80
102
  }
81
103
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/blocks",
3
- "version": "6.25.0",
3
+ "version": "6.26.1",
4
4
  "description": "Svelte 5 UI component library with Tailwind CSS 4, OKLCH design tokens and zero runtime dependencies",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,6 +34,7 @@
34
34
  "test:run": "vitest run"
35
35
  },
36
36
  "files": [
37
+ "LICENSE",
37
38
  "dist",
38
39
  "!dist/**/*.test.*",
39
40
  "!dist/**/*.spec.*",
@@ -81,18 +82,18 @@
81
82
  ],
82
83
  "peerDependencies": {
83
84
  "svelte": "^5.56.4",
84
- "@sveltejs/kit": "^2.69.2",
85
+ "@sveltejs/kit": "^2.70.1",
85
86
  "@urbicon-ui/i18n": "^6.0.0",
86
87
  "@urbicon-ui/shared-types": "^6.0.0"
87
88
  },
88
89
  "dependencies": {},
89
90
  "devDependencies": {
90
- "@sveltejs/kit": "^2.69.2",
91
+ "@sveltejs/kit": "^2.70.1",
91
92
  "@sveltejs/package": "^2.5.8",
92
93
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
93
94
  "@tailwindcss/vite": "^4.3.1",
94
- "@urbicon-ui/i18n": "6.25.0",
95
- "@urbicon-ui/shared-types": "6.25.0",
95
+ "@urbicon-ui/i18n": "6.26.1",
96
+ "@urbicon-ui/shared-types": "6.26.1",
96
97
  "prettier": "^3.8.4",
97
98
  "prettier-plugin-svelte": "^4.1.1",
98
99
  "prettier-plugin-tailwindcss": "^0.8.0",