@ultimat3/ui 6.0.0 → 7.0.0

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(await import('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(solidRuntime)`, from `import * as solidRuntime from 'solid-js'` |
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
 
@@ -220,19 +220,74 @@ import '../../shared/global'; // `shared/global.scss` is the app's one `@use '@u
220
220
  ```
221
221
 
222
222
  ```tsx
223
- // A client entry — an island, or a hydrated app shell. Both lines, in this order, once.
223
+ // A client entry — an island's `mount()`, or a hydrated app shell. In this order, once.
224
+ import { createTranslator } from '@ultimat3/i18n';
224
225
  import { setSolidRuntime, UiProvider } from '@ultimat3/ui';
226
+ import type { JSX } from 'solid-js';
227
+ import * as solidRuntime from 'solid-js';
228
+ import { render } from 'solid-js/web';
229
+
230
+ interface Props {
231
+ readonly locale: string;
232
+ readonly timeZone: string;
233
+ readonly currency: string;
234
+ /** The `ui.*` keys this tree renders, resolved on the server. A catalog crosses the seam as
235
+ * JSON; a `Translator` is a function and cannot. */
236
+ readonly strings: Readonly<Record<string, string>>;
237
+ readonly tree: JSX.Element;
238
+ }
239
+
240
+ export function mount(el: HTMLElement, props: Props): void {
241
+ // NOT `await import('solid-js')`: the chunk already carries Solid statically, so the await buys
242
+ // no bytes and makes `mount` async — and the hydration runtime calls it synchronously.
243
+ setSolidRuntime(solidRuntime);
244
+ el.textContent = ''; // Solid's `render` APPENDS; the server's shell would stay above this one.
245
+ render(
246
+ () => (
247
+ <UiProvider
248
+ locale={props.locale}
249
+ timeZone={props.timeZone}
250
+ currency={props.currency}
251
+ t={createTranslator(props.strings, props.locale)}
252
+ >
253
+ {props.tree}
254
+ </UiProvider>
255
+ ),
256
+ el,
257
+ );
258
+ }
259
+ ```
225
260
 
226
- setSolidRuntime(await import('solid-js'));
261
+ **The prop is `t`, it takes a `Translator`, and omitting it is not neutral.** `<UiProvider>` with
262
+ no `t` falls back to `fallbackTranslator(locale)` — `createTranslator({}, locale)`, an **empty**
263
+ catalog — so every built-in string in the tree renders its key: `<Dialog>`'s close button reads
264
+ `⟦ui.close⟧`, `<Field>`'s marker `⟦ui.required⟧`. The keys are `UI_KEYS`, and they live in the
265
+ framework catalog the SERVER has registered; a browser chunk has none, which is why
266
+ `translatorFor(locale)` on the client is the same empty answer wearing a better name. Send the
267
+ subset the island renders and build the translator from it. `t` itself — `@ultimat3/i18n`'s bare
268
+ exported function — is not a `Translator` and is `TS2739` in this position.
227
269
 
228
- <UiProvider locale="ar-EG" timeZone="Africa/Cairo" currency="EGP" t={t}>
229
- {tree}
230
- </UiProvider>
231
- ```
270
+ An island's own copy is a different thing and stays a plain prop: it arrives already translated,
271
+ as text, because `t()`'s catalog does not cross the seam and neither does a callback.
232
272
 
233
273
  `Field` owns the ids, so `aria-describedby` / `aria-invalid` can never drift from
234
- what is rendered. `UiProvider` sets `lang` + `dir` on `<html>`; nothing else is
235
- needed to make that form correct in Arabic.
274
+ what is rendered. `UiProvider` sets `lang` + `dir` on `<html>` from `locale`, so
275
+ `ar-EG` needs no second stylesheet and no second component.
276
+
277
+ **An island pays for the barrel, not for the component it named.** Measured through
278
+ `buildIslands` — minified, production Solid, `As of 2026-08-21`:
279
+
280
+ | An island that imports | Chunk |
281
+ |---|---|
282
+ | nothing | 52 B |
283
+ | `setSolidRuntime` alone | 5.7 kB |
284
+ | `<UiProvider>` + one `<Button>` | 49.0 kB, of which Solid's own runtime is 12.2 kB |
285
+ | `<UiProvider>` + `<Form>` + `<Input>` + `<Button>` | 54.8 kB |
286
+
287
+ There are no component subpath exports, so `import { Button }` reaches the whole index —
288
+ issue **#275**. `@ultimat3/ui/icons/*` is the one part of the package already shaped the
289
+ other way, and is what the components want. Budget an island against these numbers, not
290
+ against the component's own source.
236
291
 
237
292
  ## `<Text>` and `<Image>`
238
293
 
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@ultimat3/ui",
3
- "version": "6.0.0",
3
+ "version": "7.0.0",
4
4
  "description": "SolidJS design system: semantic design tokens, dark/RTL-ready SCSS modules, a11y primitives",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "sideEffects": [
8
+ "./src/errors.ts",
9
+ "**/*.scss"
10
+ ],
7
11
  "repository": {
8
12
  "type": "git",
9
13
  "url": "git+https://github.com/developerz-ai/ultimate.git",
@@ -39,10 +43,10 @@
39
43
  "icons": "bun run src/icons/build-icons.ts"
40
44
  },
41
45
  "dependencies": {
42
- "@ultimat3/core": "6.0.0",
43
- "@ultimat3/i18n": "6.0.0",
44
- "@ultimat3/money": "6.0.0",
45
- "@ultimat3/time": "6.0.0"
46
+ "@ultimat3/core": "7.0.0",
47
+ "@ultimat3/i18n": "7.0.0",
48
+ "@ultimat3/money": "7.0.0",
49
+ "@ultimat3/time": "7.0.0"
46
50
  },
47
51
  "peerDependencies": {
48
52
  "solid-js": "^1.9.0"
package/src/errors.ts CHANGED
@@ -91,18 +91,22 @@ export function runtimeMissingError(api: string, fix: string): UiError {
91
91
  }
92
92
 
93
93
  /**
94
- * `<UiProvider>` on a server render. No new code: a reactive runtime is absent, which is exactly
95
- * what X_UI_RUNTIME_MISSING already names — and a code is stable forever once shipped. The throw
96
- * is the point. A Provider in an inert tree reaches no descendant (they are walked outside every
97
- * owner), so rendering the children anyway would drop the locale, zone, currency and translator it
98
- * was handed while looking like it worked.
94
+ * `<UiProvider>` with no runtime registered. No new code: a reactive runtime is absent, which is
95
+ * exactly what X_UI_RUNTIME_MISSING already names — and a code is stable forever once shipped. The
96
+ * throw is the point. A Provider in an inert tree reaches no descendant (they are walked outside
97
+ * every owner), so rendering the children anyway would drop the locale, zone, currency and
98
+ * translator it was handed while looking like it worked.
99
+ *
100
+ * TWO callers reach here, not one, and the cause used to name only the first: a server render,
101
+ * which has no runtime by design, and an island whose `mount` never called `setSolidRuntime` —
102
+ * which is a real bug in a real browser, and was being told it was a server render.
99
103
  */
100
104
  export function providerNeedsRuntimeError(): UiError {
101
105
  return new UiError({
102
106
  code: UI_ERROR_CODES.runtimeMissing,
103
107
  cause:
104
- '<UiProvider> needs a registered Solid runtime; a server render has none, and its values would reach no component',
105
- fix: 'delete <UiProvider> from the server tree — useUi() already reads the request locale and time zone; keep the provider inside your *.island.tsx, under the mount() that called setSolidRuntime()',
108
+ '<UiProvider> was rendered with no Solid runtime registered, so its locale, time zone, currency and translator would reach no component',
109
+ 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",
106
110
  });
107
111
  }
108
112
 
@@ -9,10 +9,19 @@ import { INERT_SOLID_RUNTIME } from './inert-runtime';
9
9
  export type Accessor<T> = () => T;
10
10
  export type Setter<T> = (next: T) => void;
11
11
 
12
+ /**
13
+ * `children` is REQUIRED, and that one keyword is what makes `setSolidRuntime(solidRuntime)`
14
+ * compile at all. Solid's own `ContextProviderComponent` takes `FlowProps`, whose `children` is
15
+ * required, and a function taking `children?` is not assignable to one taking `children` — so
16
+ * `typeof import('solid-js')` did not satisfy `SolidRuntime` and the registration this package
17
+ * documents was a type error nobody had ever compiled: it had zero non-test callers (issue #246),
18
+ * and every test passed a hand-written fake that matched the declaration instead of the runtime.
19
+ * `JSX.Element` already includes `undefined`, so a provider with no children still type-checks.
20
+ */
12
21
  export interface SolidContext<T> {
13
22
  readonly id: symbol;
14
23
  readonly defaultValue: T;
15
- readonly Provider: (props: { value: T; children?: JSX.Element }) => JSX.Element;
24
+ readonly Provider: (props: { value: T; children: JSX.Element }) => JSX.Element;
16
25
  }
17
26
 
18
27
  /** The exact slice of solid-js the design system touches. */
@@ -56,9 +65,16 @@ function hasDom(): boolean {
56
65
  export function solid(): SolidRuntime {
57
66
  if (runtime !== null) return runtime;
58
67
  if (hasDom()) {
68
+ // Two lines to paste, and both are real: `x g resource` writes exactly them into the slice's
69
+ // `*-form.island.tsx`. The line this used to hand out was `setSolidRuntime(await
70
+ // import('solid-js'))`, which makes `mount` ASYNC for nothing — an island chunk already
71
+ // carries Solid statically, because the same file imports `render` from `solid-js/web`.
59
72
  throw runtimeMissingError(
60
73
  'a registered Solid runtime',
61
- "add setSolidRuntime(await import('solid-js')) at the top of your *.island.tsx's mount() — a server render needs none",
74
+ // ONE literal, never a concatenation: `fix-scan.ts` reads a single literal in this position
75
+ // and counts anything else `unreadable`, so a fix split across `+` is a fix the gate stops
76
+ // checking.
77
+ "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",
62
78
  );
63
79
  }
64
80
  return INERT_SOLID_RUNTIME;