@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 +64 -9
- package/package.json +9 -5
- package/src/errors.ts +11 -7
- package/src/theme/solid-adapter.ts +18 -2
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
|
235
|
-
|
|
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": "
|
|
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": "
|
|
43
|
-
"@ultimat3/i18n": "
|
|
44
|
-
"@ultimat3/money": "
|
|
45
|
-
"@ultimat3/time": "
|
|
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>`
|
|
95
|
-
* what X_UI_RUNTIME_MISSING already names — and a code is stable forever once shipped. The
|
|
96
|
-
* is the point. A Provider in an inert tree reaches no descendant (they are walked outside
|
|
97
|
-
* owner), so rendering the children anyway would drop the locale, zone, currency and
|
|
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>
|
|
105
|
-
fix:
|
|
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
|
|
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
|
-
|
|
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;
|