@ultimat3/ui 19.1.3 → 19.3.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.
package/README.md CHANGED
@@ -186,7 +186,7 @@ to render on the server.**
186
186
  | | Server render | Client render |
187
187
  |---|---|---|
188
188
  | The renderer | `@ultimat3/render`'s inert JSX factory — a component is a plain function, called once | Solid, with a reactive graph |
189
- | The runtime | `INERT_SOLID_RUNTIME`, handed out automatically: signals hold, memos recompute on read, effects never run | the real one, registered once: `setSolidRuntime(solidRuntime)`, from `import * as solidRuntime from 'solid-js'` |
189
+ | The runtime | `INERT_SOLID_RUNTIME`, handed out automatically: signals hold, memos recompute on read, effects never run | the real one, registered once: `setSolidRuntime({ createContext, useContext, createSignal, createMemo, createEffect, onCleanup })`, six named imports from `'solid-js'` — never `import * as`, which keeps every export of solid-js in the chunk (14.8 kB minified, measured) |
190
190
  | Where `useUi()` reads | the request — `currentLocale()`, `currentTimeZone()`, `useI18n()` | `<UiProvider>`, through Solid's context |
191
191
  | `<UiProvider>` | **throws** `X_UI_RUNTIME_MISSING` | the one injection point |
192
192
 
@@ -224,7 +224,14 @@ import '../../shared/global'; // `shared/global.scss` is the app's one `@use '@u
224
224
  import { createTranslator } from '@ultimat3/i18n';
225
225
  import { setSolidRuntime, UiProvider } from '@ultimat3/ui';
226
226
  import type { JSX } from 'solid-js';
227
- import * as solidRuntime from 'solid-js';
227
+ import {
228
+ createContext,
229
+ createEffect,
230
+ createMemo,
231
+ createSignal,
232
+ onCleanup,
233
+ useContext,
234
+ } from 'solid-js';
228
235
  import { render } from 'solid-js/web';
229
236
 
230
237
  interface Props {
@@ -240,7 +247,10 @@ interface Props {
240
247
  export function mount(el: HTMLElement, props: Props): void {
241
248
  // NOT `await import('solid-js')`: the chunk already carries Solid statically, so the await buys
242
249
  // no bytes and makes `mount` async — and the hydration runtime calls it synchronously.
243
- setSolidRuntime(solidRuntime);
250
+ // NOT `import * as solidRuntime` either: a namespace handed to a function cannot be shaken, and
251
+ // it costs 14.8 kB of solid-js per island chunk that nothing calls. The six picks are the whole
252
+ // contract.
253
+ setSolidRuntime({ createContext, useContext, createSignal, createMemo, createEffect, onCleanup });
244
254
  el.textContent = ''; // Solid's `render` APPENDS; the server's shell would stay above this one.
245
255
  render(
246
256
  () => (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/ui",
3
- "version": "19.1.3",
3
+ "version": "19.3.1",
4
4
  "description": "SolidJS design system: semantic design tokens, dark/RTL-ready SCSS modules, a11y primitives",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,10 +44,10 @@
44
44
  "icons": "bun run src/icons/build-icons.ts"
45
45
  },
46
46
  "dependencies": {
47
- "@ultimat3/core": "19.1.3",
48
- "@ultimat3/i18n": "19.1.3",
49
- "@ultimat3/money": "19.1.3",
50
- "@ultimat3/time": "19.1.3"
47
+ "@ultimat3/core": "19.3.1",
48
+ "@ultimat3/i18n": "19.3.1",
49
+ "@ultimat3/money": "19.3.1",
50
+ "@ultimat3/time": "19.3.1"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "solid-js": "^1.9.0"
package/src/a11y.ts CHANGED
@@ -197,6 +197,25 @@ export function createRovingTabindex(
197
197
 
198
198
  export type Politeness = 'polite' | 'assertive';
199
199
 
200
+ /**
201
+ * One region per politeness level — this plus `-polite` / `-assertive` is the id — and this on its
202
+ * own is the CLASS both of them wear, because they are hidden identically.
203
+ *
204
+ * A class, and not the `region.style.cssText` this wrote until 19.1.3: a style a script applies at
205
+ * runtime is not among the inline `<style>` bodies the framework's own CSP hashed at render time
206
+ * (`packages/cli/src/style-csp.ts`, sent by `dev-roles.ts`), so a clean browser logged `Applying
207
+ * inline style violates the following Content Security Policy directive 'style-src'` on every page
208
+ * load — and an app that promotes that report-only policy to enforcing would have the declaration
209
+ * dropped and the announcement painted on screen as visible page content. The rules live in
210
+ * `tokens/reset.scss`, through the same `visually-hidden` mixin every other off-screen label in
211
+ * this package uses.
212
+ *
213
+ * That the app loads this package's global stylesheet is not a new assumption — every rule this
214
+ * package emits reads a custom property from it, and `x verify` refuses a document defining none
215
+ * (`X_STYLES_GLOBAL_MISSING`). Being ANNOUNCED does not depend on it either way: `aria-live` is
216
+ * read off the accessibility tree, so a region with no stylesheet is still read out, and only the
217
+ * hiding is lost.
218
+ */
200
219
  const LIVE_REGION_ID = 'ultimate-live-region';
201
220
 
202
221
  /**
@@ -211,11 +230,10 @@ export function announce(message: string, politeness: Politeness = 'polite'): vo
211
230
  if (region === null) {
212
231
  region = document.createElement('div');
213
232
  region.id = id;
233
+ region.className = LIVE_REGION_ID;
214
234
  region.setAttribute('role', politeness === 'assertive' ? 'alert' : 'status');
215
235
  region.setAttribute('aria-live', politeness);
216
236
  region.setAttribute('aria-atomic', 'true');
217
- region.style.cssText =
218
- 'position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip-path:inset(50%)';
219
237
  document.body.appendChild(region);
220
238
  }
221
239
  region.textContent = '';
@@ -23,7 +23,7 @@
23
23
  color: t.role('fg-muted');
24
24
  font-size: t.text(xs);
25
25
  text-transform: uppercase;
26
- letter-spacing: var(--tracking-wide);
26
+ letter-spacing: t.tracking(wide);
27
27
 
28
28
  &::before,
29
29
  &::after {
package/src/errors.ts CHANGED
@@ -107,7 +107,7 @@ export function providerNeedsRuntimeError(): UiError {
107
107
  code: UI_ERROR_CODES.runtimeMissing,
108
108
  cause:
109
109
  '<UiProvider> was rendered with no Solid runtime registered, so its locale, time zone, currency and translator would reach no component',
110
- fix: "in an island, paste `import * as solidRuntime from 'solid-js';` at the top of the *.island.tsx and `setSolidRuntime(solidRuntime);` as the first line of its mount(), above the render() that builds <UiProvider>; on the server, delete <UiProvider> — useUi() already reads the request locale and time zone",
110
+ fix: "in an island, paste `import { createContext, createEffect, createMemo, createSignal, onCleanup, useContext } from 'solid-js';` at the top of the *.island.tsx and `setSolidRuntime({ createContext, useContext, createSignal, createMemo, createEffect, onCleanup });` as the first line of its mount(), above the render() that builds <UiProvider> — six named imports, never `import * as`, which keeps all of solid-js in the chunk; on the server, delete <UiProvider> — useUi() already reads the request locale and time zone",
111
111
  });
112
112
  }
113
113
 
@@ -10,7 +10,12 @@ import type { SolidRuntime } from './solid-adapter';
10
10
 
11
11
  let runtime: SolidRuntime | null = null;
12
12
 
13
- /** Register once, in the app entry, before the first render. */
13
+ /**
14
+ * Register once, in the app entry, before the first render. Hand it the six named imports —
15
+ * `{ createContext, useContext, createSignal, createMemo, createEffect, onCleanup }` — and not
16
+ * `import * as solidRuntime`: the namespace registers too, and costs 14.8 kB of unshaken solid-js
17
+ * per island chunk (`solid-adapter.ts` has the measurement).
18
+ */
14
19
  export function setSolidRuntime(next: SolidRuntime): void {
15
20
  runtime = next;
16
21
  }
@@ -29,7 +29,15 @@ export interface SolidContext<T> {
29
29
  readonly Provider: (props: { value: T; children: JSX.Element }) => JSX.Element;
30
30
  }
31
31
 
32
- /** The exact slice of solid-js the design system touches. */
32
+ /**
33
+ * The exact slice of solid-js the design system touches — six functions, and the registration is
34
+ * meant to be handed exactly those six as NAMED imports: `setSolidRuntime({ createContext,
35
+ * useContext, createSignal, createMemo, createEffect, onCleanup })`. A `typeof import('solid-js')`
36
+ * namespace still satisfies this shape and still registers, but a namespace object passed to a
37
+ * function is one the bundler cannot shake — every export of solid-js stays in the chunk. Measured
38
+ * with the island bundler's own settings (minified, production, no splitting): the namespace form
39
+ * is 28,556 B against 13,708 B for the six picks, 14.8 kB per island that nothing ever calls.
40
+ */
33
41
  export interface SolidRuntime {
34
42
  createContext<T>(defaultValue: T): SolidContext<T>;
35
43
  useContext<T>(context: SolidContext<T>): T;
@@ -64,7 +72,7 @@ export function solid(): SolidRuntime {
64
72
  // ONE literal, never a concatenation: `fix-scan.ts` reads a single literal in this position
65
73
  // and counts anything else `unreadable`, so a fix split across `+` is a fix the gate stops
66
74
  // checking.
67
- "paste `import * as solidRuntime from 'solid-js';` at the top of your *.island.tsx and `setSolidRuntime(solidRuntime);` as the first line of its mount(), above render() — a server render needs none",
75
+ "paste `import { createContext, createEffect, createMemo, createSignal, onCleanup, useContext } from 'solid-js';` at the top of your *.island.tsx and `setSolidRuntime({ createContext, useContext, createSignal, createMemo, createEffect, onCleanup });` as the first line of its mount(), above render() — six named imports, never `import * as`, which keeps all of solid-js in the chunk; a server render needs none",
68
76
  );
69
77
  }
70
78
  return INERT_SOLID_RUNTIME;
@@ -81,3 +81,7 @@ $letter-spacing: (
81
81
  @function leading($name) {
82
82
  @return var(--leading-#{$name});
83
83
  }
84
+
85
+ @function tracking($name) {
86
+ @return var(--tracking-#{$name});
87
+ }
@@ -1,5 +1,6 @@
1
1
  // Modern reset. Assumes `theme.scss` has already run — every value here is a
2
- // token, so the reset itself flips with the theme.
2
+ // token, so the reset itself flips with the theme. Plus the one GLOBAL class the package owns:
3
+ // the element wearing it is created by `announce()` at runtime, so no component module can hold it.
3
4
 
4
5
  @use 'index' as t;
5
6
 
@@ -41,7 +42,7 @@ h6 {
41
42
  color: t.role('fg-strong');
42
43
  font-weight: t.weight(semibold);
43
44
  line-height: t.leading(tight);
44
- letter-spacing: var(--tracking-tight);
45
+ letter-spacing: t.tracking(tight);
45
46
  text-wrap: balance;
46
47
  }
47
48
 
@@ -134,6 +135,15 @@ html:has(dialog:modal) {
134
135
  overflow: hidden;
135
136
  }
136
137
 
138
+ // The live region `announce()` (`../a11y.ts`) appends to <body>, hidden from here rather than
139
+ // through `element.style`: a style a script applies at runtime is not among the inline styles the
140
+ // CSP the framework itself sends has hashed, so it logged a `style-src` violation on every page
141
+ // load and would lose its styling outright under an enforcing policy — putting a screen-reader
142
+ // message on screen. Off-screen, never `display: none`, which would stop it being announced at all.
143
+ .ultimate-live-region {
144
+ @include t.visually-hidden;
145
+ }
146
+
137
147
  // Global motion guard. Placed last so it wins over every component rule.
138
148
  @media (prefers-reduced-motion: reduce) {
139
149
  html {