@ultimat3/ui 20.1.6 → 20.2.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.
Files changed (39) hide show
  1. package/CATALOG.md +100 -1
  2. package/CLAUDE.md +7 -3
  3. package/README.md +1 -0
  4. package/package.json +10 -6
  5. package/src/components/BarChart.module.scss +45 -0
  6. package/src/components/BarChart.tsx +78 -0
  7. package/src/components/CommandPalette.module.scss +110 -0
  8. package/src/components/CommandPalette.tsx +127 -0
  9. package/src/components/CopyButton.module.scss +40 -0
  10. package/src/components/CopyButton.tsx +62 -0
  11. package/src/components/Kbd.module.scss +15 -0
  12. package/src/components/Kbd.tsx +17 -0
  13. package/src/components/Meter.module.scss +18 -0
  14. package/src/components/Meter.tsx +58 -0
  15. package/src/components/QrCode.module.scss +41 -0
  16. package/src/components/QrCode.tsx +49 -0
  17. package/src/components/Sparkline.module.scss +20 -0
  18. package/src/components/Sparkline.tsx +40 -0
  19. package/src/components/StatTile.module.scss +86 -0
  20. package/src/components/StatTile.tsx +49 -0
  21. package/src/components/bar-chart-view.ts +55 -0
  22. package/src/components/command-palette-view.ts +61 -0
  23. package/src/components/meter-view.ts +13 -0
  24. package/src/components/qr-encode.ts +187 -0
  25. package/src/components/qr-matrix.ts +329 -0
  26. package/src/components/sparkline-view.ts +36 -0
  27. package/src/components/stat-delta.ts +30 -0
  28. package/src/error-registry.ts +23 -0
  29. package/src/errors.ts +21 -13
  30. package/src/i18n-keys.ts +4 -0
  31. package/src/index.ts +39 -1
  32. package/src/theme/ambient-slot.ts +20 -0
  33. package/src/theme/ambient.browser.ts +11 -0
  34. package/src/theme/ambient.ts +37 -0
  35. package/src/theme/context.ts +33 -38
  36. package/src/theme/inline-script.ts +5 -0
  37. package/src/theme/provider.tsx +5 -2
  38. package/src/tokens/_mixins.scss +30 -0
  39. package/src/tokens/reset.scss +7 -0
package/src/index.ts CHANGED
@@ -6,6 +6,12 @@
6
6
  // would be TS2307 there. The reference pulls the contract along with the entry.
7
7
  /// <reference path="./scss.d.ts" />
8
8
 
9
+ // Bare, and above every re-export: `theme/ambient.ts` registers the server's `useUi()` reader at
10
+ // import, and a bundler may drop a re-exported module whose binding nobody used. A browser build
11
+ // resolves this specifier to `theme/ambient.browser.ts` through `package.json`'s `browser` field,
12
+ // so an island never carries the i18n barrel for it (issue #490).
13
+ import './theme/ambient';
14
+
9
15
  export type {
10
16
  FocusTrap,
11
17
  LiveRegionAttrs,
@@ -57,20 +63,35 @@ export {
57
63
  } from './components/async-branch';
58
64
  export type { BadgeProps } from './components/Badge';
59
65
  export { Badge } from './components/Badge';
66
+ export type { BarChartProps } from './components/BarChart';
67
+ export { BarChart } from './components/BarChart';
60
68
  export type { BreadcrumbItem, BreadcrumbProps } from './components/Breadcrumb';
61
69
  export { Breadcrumb } from './components/Breadcrumb';
62
70
  export type { ButtonProps } from './components/Button';
63
71
  export { Button } from './components/Button';
72
+ export type { BarRect, ChartPoint } from './components/bar-chart-view';
73
+ export { BAR_CHART, barRects, GRID_STEPS, gridY, maxOf } from './components/bar-chart-view';
64
74
  export type { CardProps, Elevation } from './components/Card';
65
75
  export { Card } from './components/Card';
66
76
  export type { CheckboxProps } from './components/Checkbox';
67
77
  export { Checkbox } from './components/Checkbox';
68
78
  export type { ComboboxProps } from './components/Combobox';
69
79
  export { Combobox } from './components/Combobox';
80
+ export type { CommandPaletteLabels, CommandPaletteProps } from './components/CommandPalette';
81
+ export { CommandPalette } from './components/CommandPalette';
70
82
  export type { ContainerProps, ContainerSize } from './components/Container';
71
83
  export { Container } from './components/Container';
84
+ export type { CopyButtonProps } from './components/CopyButton';
85
+ export { COPIED_MS, CopyButton } from './components/CopyButton';
72
86
  export type { ComboboxOption } from './components/combobox-filter';
73
87
  export { COMBOBOX_LIMIT, filterOptions, normalizeQuery } from './components/combobox-filter';
88
+ export type { CommandPaletteItem, PaletteKeyAction } from './components/command-palette-view';
89
+ export {
90
+ filterItems,
91
+ keyAction,
92
+ settleActive,
93
+ stepActive,
94
+ } from './components/command-palette-view';
74
95
  export type { Column, DataTableProps } from './components/DataTable';
75
96
  export { DataTable } from './components/DataTable';
76
97
  export type { DateTimeProps } from './components/DateTime';
@@ -137,6 +158,8 @@ export type { ImageBox, ImageLoadingHints, ImageVariant } from './components/ima
137
158
  export { boxFor, loadingHints, ratioFor, srcsetFor } from './components/image-source';
138
159
  export type { LoadMoreInput, LoadMoreState } from './components/infinite-scroll-view';
139
160
  export { loadMoreState } from './components/infinite-scroll-view';
161
+ export type { KbdProps } from './components/Kbd';
162
+ export { Kbd } from './components/Kbd';
140
163
  export type { LinkProps } from './components/Link';
141
164
  export { Link } from './components/Link';
142
165
  export type { LocaleSwitcherProps } from './components/LocaleSwitcher';
@@ -145,8 +168,11 @@ export type { LinkTarget } from './components/link-target';
145
168
  export { linkTarget } from './components/link-target';
146
169
  export type { MenuItem, MenuProps } from './components/Menu';
147
170
  export { Menu } from './components/Menu';
171
+ export type { MeterProps } from './components/Meter';
172
+ export { Meter } from './components/Meter';
148
173
  export type { MoneyProps } from './components/Money';
149
174
  export { Money } from './components/Money';
175
+ export { meterShare, meterWidth } from './components/meter-view';
150
176
  export type { MoneyFormatter, MoneyInput, MoneyViewOptions } from './components/money-view';
151
177
  // --- formatting cores (pure, renderer-free) ----------------------------------
152
178
  export { moneyText, toMoney } from './components/money-view';
@@ -156,6 +182,10 @@ export type { PaginationProps } from './components/Pagination';
156
182
  export { Pagination } from './components/Pagination';
157
183
  export type { Placement, PopoverProps } from './components/Popover';
158
184
  export { Popover } from './components/Popover';
185
+ export type { QrCodeProps } from './components/QrCode';
186
+ export { QrCode } from './components/QrCode';
187
+ export type { QrMatrix } from './components/qr-matrix';
188
+ export { encodeQr } from './components/qr-matrix';
159
189
  export type { RadioOption, RadioProps } from './components/Radio';
160
190
  export { Radio } from './components/Radio';
161
191
  export type { RelativeTimeProps } from './components/RelativeTime';
@@ -168,14 +198,22 @@ export type { SelectOption, SelectProps } from './components/Select';
168
198
  export { Select } from './components/Select';
169
199
  export type { SkeletonProps } from './components/Skeleton';
170
200
  export { Skeleton } from './components/Skeleton';
201
+ export type { SparklineProps } from './components/Sparkline';
202
+ export { Sparkline } from './components/Sparkline';
171
203
  export type { SpinnerProps } from './components/Spinner';
172
204
  export { Spinner } from './components/Spinner';
173
205
  export type { StackProps } from './components/Stack';
174
206
  export { Stack } from './components/Stack';
207
+ export type { StatTileProps } from './components/StatTile';
208
+ export { StatTile } from './components/StatTile';
175
209
  export type { SwitchProps } from './components/Switch';
176
210
  export { Switch } from './components/Switch';
177
211
  export type { SortDirection, SortState } from './components/sort-state';
178
212
  export { ariaSortFor, nextSortState } from './components/sort-state';
213
+ export type { SparkPoint } from './components/sparkline-view';
214
+ export { SPARKLINE, sparklinePath, sparkPoints } from './components/sparkline-view';
215
+ export type { StatDelta, StatTrend } from './components/stat-delta';
216
+ export { DELTA_ARROW_PATH, deltaOf } from './components/stat-delta';
179
217
  export type { TableProps } from './components/Table';
180
218
  export { Table } from './components/Table';
181
219
  export type { TabItem, TabsProps } from './components/Tabs';
@@ -258,11 +296,11 @@ export {
258
296
  TAB_SELECTOR,
259
297
  tabStopIndex,
260
298
  } from './roving';
299
+ export { ambientUiContext } from './theme/ambient';
261
300
  export type { Brand, BrandInput, FontSlot } from './theme/brand';
262
301
  export { brandStyleCspSource, brandStyleTag, defineTheme, FONT_SLOTS } from './theme/brand';
263
302
  export type { Direction, UiContextValue } from './theme/context';
264
303
  export {
265
- ambientUiContext,
266
304
  defaultUiContext,
267
305
  fallbackTranslator,
268
306
  UI_DEFAULT_CURRENCY,
@@ -0,0 +1,20 @@
1
+ // The module-scope slot holding the reader `useUi()` falls back to when no Solid runtime is
2
+ // registered — a server render. Its own module for the same reason `runtime-slot.ts` is: the
3
+ // server's reader (`ambient.ts`) reaches `@ultimat3/i18n` and `@ultimat3/time`, and a slot that
4
+ // sat beside it would put the framework catalog in every browser chunk that calls `useUi()`.
5
+ // The slot itself imports nothing.
6
+
7
+ import type { UiContextValue } from './context';
8
+
9
+ export type AmbientUiReader = () => UiContextValue;
10
+
11
+ let reader: AmbientUiReader | null = null;
12
+
13
+ /** Called once, at import, by `ambient.ts`; a browser build never reaches that module. */
14
+ export function setAmbientUiReader(next: AmbientUiReader): void {
15
+ reader = next;
16
+ }
17
+
18
+ export function registeredAmbientUiReader(): AmbientUiReader | null {
19
+ return reader;
20
+ }
@@ -0,0 +1,11 @@
1
+ // What a browser bundler gets in place of `ambient.ts` — `package.json`'s `browser` field maps
2
+ // one file to the other. A browser has no request context: `useUi()` in a DOM reads the Solid
3
+ // context a `UiProvider` filled, and throws (`solid()`, X_UI_RUNTIME_MISSING) before it could ever
4
+ // fall back to an ambient reader. So nothing is registered here, and the same export answers the
5
+ // package defaults — which is exactly what the server's reader answers outside a request.
6
+
7
+ import { defaultUiContext, type UiContextValue } from './context';
8
+
9
+ export function ambientUiContext(): UiContextValue {
10
+ return defaultUiContext();
11
+ }
@@ -0,0 +1,37 @@
1
+ // The presentation context of a SERVER render, read from the request the framework already
2
+ // resolved — and the one module of the theme directory that reaches `@ultimat3/i18n` and
3
+ // `@ultimat3/time` for a value. `index.ts` imports it bare, so every server that imports the
4
+ // barrel has the reader registered before the first `useUi()`; `package.json`'s `browser` field
5
+ // maps this file to `ambient.browser.ts` for a browser bundler, which is what keeps the i18n
6
+ // barrel — and the framework catalog it installs at import — out of an island chunk (issue #490).
7
+
8
+ import { currentDirection, currentLocale, useI18n } from '@ultimat3/i18n';
9
+ import { currentTimeZone } from '@ultimat3/time';
10
+ import { setAmbientUiReader } from './ambient-slot';
11
+ import { UI_DEFAULT_CURRENCY, type UiContextValue } from './context';
12
+
13
+ /**
14
+ * `currentLocale()` and `currentTimeZone()` are the ambient answers `@ultimat3/i18n` and
15
+ * `@ultimat3/time` keep on the request context, and `useI18n()` is the translator built from the
16
+ * registered catalogs. No second ambient store, and no process-wide default — outside a request
17
+ * each of them returns its own configured fallback, which is where `defaultUiContext()`'s values
18
+ * come from in the first place.
19
+ *
20
+ * `theme` and `currency` have no ambient source and are not given one. The server cannot know the
21
+ * theme — `data-theme` is decided in the browser by the anti-flash script — and a default display
22
+ * currency is business convention: a `Money` carries its own, and an app that wants another for
23
+ * bare minor units wraps `<Money currency="EUR">` once (axiom 8).
24
+ */
25
+ export function ambientUiContext(): UiContextValue {
26
+ return {
27
+ theme: 'light',
28
+ locale: currentLocale(),
29
+ timeZone: currentTimeZone(),
30
+ currency: UI_DEFAULT_CURRENCY,
31
+ dir: currentDirection(),
32
+ t: useI18n(),
33
+ };
34
+ }
35
+
36
+ // At module scope, and that is the point: registration an importer can forget is not registration.
37
+ setAmbientUiReader(ambientUiContext);
@@ -1,19 +1,17 @@
1
1
  // The ambient presentation contract every component reads: theme, locale, tz, currency, direction,
2
2
  // translator. Formatting components take nothing from a process-wide default — it arrives from
3
3
  // `UiProvider` where a Solid runtime is registered, and from the request everywhere else.
4
+ //
5
+ // This module is what every component's `useUi()` retains, so it imports NO value from
6
+ // `@ultimat3/i18n` or `@ultimat3/time`: the i18n barrel installs the framework catalog at import,
7
+ // and the request readers live in `ambient.ts`, reached through `ambient-slot.ts` (issue #490).
4
8
 
5
- import type { Translator } from '@ultimat3/i18n';
6
- import {
7
- createTranslator,
8
- currentDirection,
9
- currentLocale,
10
- type Direction,
11
- directionOf,
12
- type Locale,
13
- useI18n,
14
- } from '@ultimat3/i18n';
15
- import { currentTimeZone, type TimeZone } from '@ultimat3/time';
9
+ import type { Direction } from '@ultimat3/core';
10
+ import { directionOf } from '@ultimat3/core';
11
+ import type { Locale, Translator } from '@ultimat3/i18n';
12
+ import type { TimeZone } from '@ultimat3/time';
16
13
  import type { Theme } from '../tokens/tokens';
14
+ import { registeredAmbientUiReader } from './ambient-slot';
17
15
  import { hasSolidRuntime } from './runtime-slot';
18
16
  import { type SolidContext, type SolidRuntime, solid } from './solid-adapter';
19
17
 
@@ -35,9 +33,23 @@ export const UI_DEFAULT_LOCALE: Locale = 'en';
35
33
  export const UI_DEFAULT_TIME_ZONE = 'UTC' as TimeZone;
36
34
  export const UI_DEFAULT_CURRENCY = 'USD';
37
35
 
38
- /** Loud-miss translator: a forgotten catalog key renders ⟦key⟧, never blank. */
36
+ /**
37
+ * Loud-miss translator: a forgotten catalog key renders ⟦key⟧, never blank.
38
+ *
39
+ * Written here rather than as `createTranslator({}, locale)`, and behaviourally the same call: an
40
+ * empty catalog has no key, so every lookup is a miss, `has` is false, `raw` is undefined and
41
+ * `keys` is empty — `context.test.ts` holds each of those against `@ultimat3/i18n`'s own. What
42
+ * the spelling buys is that a browser chunk with a `UiProvider` in it no longer reaches the i18n
43
+ * barrel for a translator that cannot translate.
44
+ */
39
45
  export function fallbackTranslator(locale: Locale = UI_DEFAULT_LOCALE): Translator {
40
- return createTranslator({}, locale);
46
+ const miss = (key: string): string => `⟦${key}⟧`;
47
+ return Object.assign(miss, {
48
+ has: (): boolean => false,
49
+ raw: (): string | undefined => undefined,
50
+ keys: (): string[] => [],
51
+ locale,
52
+ });
41
53
  }
42
54
 
43
55
  export function defaultUiContext(): UiContextValue {
@@ -51,30 +63,6 @@ export function defaultUiContext(): UiContextValue {
51
63
  };
52
64
  }
53
65
 
54
- /**
55
- * The presentation context of a server render, read from the request the framework already
56
- * resolved: `currentLocale()` and `currentTimeZone()` are the ambient answers `@ultimat3/i18n` and
57
- * `@ultimat3/time` keep on the request context, and `useI18n()` is the translator built from the
58
- * registered catalogs. No second ambient store, and no process-wide default — outside a request
59
- * each of them returns its own configured fallback, which is where `defaultUiContext()`'s values
60
- * come from in the first place.
61
- *
62
- * `theme` and `currency` have no ambient source and are not given one. The server cannot know the
63
- * theme — `data-theme` is decided in the browser by the anti-flash script — and a default display
64
- * currency is business convention: a `Money` carries its own, and an app that wants another for
65
- * bare minor units wraps `<Money currency="EUR">` once (axiom 8).
66
- */
67
- export function ambientUiContext(): UiContextValue {
68
- return {
69
- theme: 'light',
70
- locale: currentLocale(),
71
- timeZone: currentTimeZone(),
72
- currency: UI_DEFAULT_CURRENCY,
73
- dir: currentDirection(),
74
- t: useI18n(),
75
- };
76
- }
77
-
78
66
  let cached: {
79
67
  readonly runtime: SolidRuntime;
80
68
  readonly context: SolidContext<UiContextValue>;
@@ -99,8 +87,15 @@ export function uiContext(): SolidContext<UiContextValue> {
99
87
  * Solid context is provably empty — an inert tree is walked outside every owner, so `useContext`
100
88
  * returns the context's default value even with a real runtime registered — so reading the
101
89
  * request's own answers is strictly more true than reading a provider that provided nothing.
90
+ *
91
+ * Those answers come through the slot: `ambient.ts` registers `ambientUiContext` when the barrel
92
+ * is imported, and a browser build — where this branch is unreachable — never has it. The
93
+ * defaults are the fallback for a caller that reached this module without the barrel, which is
94
+ * what `ambientUiContext()` itself answers outside a request.
102
95
  */
103
96
  export function useUi(): UiContextValue {
104
97
  const runtime = solid();
105
- return hasSolidRuntime() ? runtime.useContext(uiContext()) : ambientUiContext();
98
+ if (hasSolidRuntime()) return runtime.useContext(uiContext());
99
+ const ambient = registeredAmbientUiReader();
100
+ return ambient === null ? defaultUiContext() : ambient();
106
101
  }
@@ -1,6 +1,11 @@
1
1
  // The blocking <head> snippet that kills the flash of wrong theme. Built from the
2
2
  // same constants as theme.ts so the two can never disagree. Server/build-only:
3
3
  // the hash is computed with Bun's hasher for the CSP header.
4
+ //
5
+ // DEPRECATED `As of 20.2.0`: the framework's boot inlines this script itself, from
6
+ // `@ultimat3/render`'s `themeScript({ fallback })` with `theme.defaultMode` in `app.config.ts` as
7
+ // the fallback, and admits it to the CSP — an app no longer writes either half. This module stays
8
+ // exported for one major so an app that inlined it by hand keeps building; it is removed in 21.
4
9
 
5
10
  import { runtimeMissingError } from '../errors';
6
11
  import { THEME_ATTRIBUTE, THEME_MEDIA_QUERY, THEME_STORAGE_KEY } from './theme';
@@ -1,7 +1,10 @@
1
1
  // UiProvider — the single place an app injects presentation context. Reactive
2
- // access goes through the Solid adapter, so this file imports only solid types.
2
+ // access goes through the Solid adapter, so this file imports only solid types — and only TYPES
3
+ // from `@ultimat3/i18n`: `directionOf` is core's, because a value import of the i18n barrel is
4
+ // the framework catalog in every island chunk with a provider (issue #490).
3
5
 
4
- import { directionOf, type Locale, type Translator } from '@ultimat3/i18n';
6
+ import { directionOf } from '@ultimat3/core';
7
+ import type { Locale, Translator } from '@ultimat3/i18n';
5
8
  import type { TimeZone } from '@ultimat3/time';
6
9
  import type { JSX } from 'solid-js';
7
10
  import { providerNeedsRuntimeError } from '../errors';
@@ -8,6 +8,8 @@
8
8
  @use 'colors';
9
9
  @use 'motion';
10
10
  @use 'radius';
11
+ @use 'space';
12
+ @use 'typography';
11
13
 
12
14
  /// Keyboard-only focus, drawn from the accent role so it themes automatically.
13
15
  @mixin focus-ring($role: 'accent', $width: 2px, $offset: 2px) {
@@ -191,6 +193,34 @@
191
193
  }
192
194
  }
193
195
 
196
+ /// The mono family with tabular figures, for slugs, counts, timestamps, ids and URLs — data
197
+ /// whose columns must not wobble as the value changes. Lifted from an app that had restated
198
+ /// these two lines in six stylesheets before the catalog carried them.
199
+ @mixin data-text {
200
+ font-family: var(--font-mono);
201
+ font-variant-numeric: tabular-nums;
202
+ }
203
+
204
+ /// The section-label voice: smallest text step, medium weight, wide tracking, uppercase. An
205
+ /// eyebrow over a hero, a stat tile's label, a table's column head.
206
+ @mixin label-caps {
207
+ font-size: typography.text('xs');
208
+ font-weight: typography.weight('medium');
209
+ letter-spacing: typography.tracking('wide');
210
+ text-transform: uppercase;
211
+ }
212
+
213
+ /// A dot grid faded toward the top-left, where the text sits, over the soft background — the
214
+ /// "quietly technical" ground a hero or a dashboard main sits on. Two layers: the fade (so the
215
+ /// dots never compete with the copy) and the grid itself, pitched on one spacing step. Every
216
+ /// colour is a role, so it themes with the page.
217
+ @mixin dot-grid($pitch: space.space(5), $alpha: 0.55) {
218
+ background:
219
+ radial-gradient(ellipse 70% 90% at 0% 0%, colors.role('bg') 35%, transparent 100%),
220
+ radial-gradient(colors.role('line', $alpha) 1px, transparent 1px) 0 0 / $pitch $pitch,
221
+ colors.role('bg-soft');
222
+ }
223
+
194
224
  /// Inline run of items on one baseline — a toolbar, a header, an icon beside a label.
195
225
  @mixin row($gap: var(--space-3), $align: center, $justify: flex-start) {
196
226
  display: flex;
@@ -15,6 +15,13 @@
15
15
  padding: 0;
16
16
  }
17
17
 
18
+ // The `*` above outranks the UA stylesheet's `dialog { margin: auto }`, which is the ONLY thing
19
+ // that centres a modal `<dialog>` in its viewport — so every `Dialog` and every app dialog opened
20
+ // pinned to the top-left corner until the app restated the margin itself. Restated once, here.
21
+ dialog {
22
+ margin: auto;
23
+ }
24
+
18
25
  html {
19
26
  -webkit-text-size-adjust: 100%;
20
27
  text-size-adjust: 100%;