@ultimat3/ui 1.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/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +45 -0
- package/src/a11y.ts +187 -0
- package/src/components/Alert.module.scss +70 -0
- package/src/components/Alert.tsx +49 -0
- package/src/components/Avatar.module.scss +66 -0
- package/src/components/Avatar.tsx +56 -0
- package/src/components/Badge.module.scss +94 -0
- package/src/components/Badge.tsx +34 -0
- package/src/components/Breadcrumb.module.scss +45 -0
- package/src/components/Breadcrumb.tsx +48 -0
- package/src/components/Button.module.scss +146 -0
- package/src/components/Button.tsx +66 -0
- package/src/components/Card.module.scss +69 -0
- package/src/components/Card.tsx +40 -0
- package/src/components/Checkbox.module.scss +90 -0
- package/src/components/Checkbox.tsx +57 -0
- package/src/components/Container.module.scss +31 -0
- package/src/components/Container.tsx +29 -0
- package/src/components/DataTable.module.scss +33 -0
- package/src/components/DataTable.tsx +149 -0
- package/src/components/DateTime.module.scss +9 -0
- package/src/components/DateTime.tsx +45 -0
- package/src/components/Dialog.module.scss +83 -0
- package/src/components/Dialog.tsx +75 -0
- package/src/components/Divider.module.scss +39 -0
- package/src/components/Divider.tsx +38 -0
- package/src/components/Drawer.module.scss +89 -0
- package/src/components/Drawer.tsx +76 -0
- package/src/components/EmptyState.module.scss +34 -0
- package/src/components/EmptyState.tsx +35 -0
- package/src/components/ErrorState.module.scss +77 -0
- package/src/components/ErrorState.tsx +84 -0
- package/src/components/Field.module.scss +41 -0
- package/src/components/Field.tsx +86 -0
- package/src/components/Form.module.scss +21 -0
- package/src/components/Form.tsx +51 -0
- package/src/components/Grid.module.scss +9 -0
- package/src/components/Grid.tsx +40 -0
- package/src/components/IconButton.module.scss +99 -0
- package/src/components/IconButton.tsx +53 -0
- package/src/components/Image.module.scss +14 -0
- package/src/components/Image.tsx +59 -0
- package/src/components/Input.module.scss +55 -0
- package/src/components/Input.tsx +84 -0
- package/src/components/Link.module.scss +42 -0
- package/src/components/Link.tsx +46 -0
- package/src/components/LocaleSwitcher.module.scss +28 -0
- package/src/components/LocaleSwitcher.tsx +61 -0
- package/src/components/Menu.module.scss +63 -0
- package/src/components/Menu.tsx +115 -0
- package/src/components/Money.module.scss +11 -0
- package/src/components/Money.tsx +49 -0
- package/src/components/Pagination.module.scss +15 -0
- package/src/components/Pagination.tsx +77 -0
- package/src/components/Popover.module.scss +59 -0
- package/src/components/Popover.tsx +76 -0
- package/src/components/Radio.module.scss +87 -0
- package/src/components/Radio.tsx +64 -0
- package/src/components/RelativeTime.module.scss +7 -0
- package/src/components/RelativeTime.tsx +49 -0
- package/src/components/Select.module.scss +51 -0
- package/src/components/Select.tsx +63 -0
- package/src/components/Skeleton.module.scss +41 -0
- package/src/components/Skeleton.tsx +34 -0
- package/src/components/Spinner.module.scss +33 -0
- package/src/components/Spinner.tsx +35 -0
- package/src/components/Stack.module.scss +21 -0
- package/src/components/Stack.tsx +49 -0
- package/src/components/Switch.module.scss +66 -0
- package/src/components/Switch.tsx +44 -0
- package/src/components/Table.module.scss +77 -0
- package/src/components/Table.tsx +42 -0
- package/src/components/Tabs.module.scss +71 -0
- package/src/components/Tabs.tsx +88 -0
- package/src/components/Text.module.scss +37 -0
- package/src/components/Text.tsx +57 -0
- package/src/components/Textarea.module.scss +15 -0
- package/src/components/Textarea.tsx +48 -0
- package/src/components/ThemeToggle.module.scss +23 -0
- package/src/components/ThemeToggle.tsx +93 -0
- package/src/components/Toast.module.scss +92 -0
- package/src/components/Toast.tsx +74 -0
- package/src/components/Tooltip.module.scss +41 -0
- package/src/components/Tooltip.tsx +32 -0
- package/src/components/date-time-view.ts +68 -0
- package/src/components/image-source.ts +150 -0
- package/src/components/money-view.ts +50 -0
- package/src/components/relative-time-view.ts +45 -0
- package/src/components/sort-state.ts +28 -0
- package/src/components/variants.ts +17 -0
- package/src/cx.ts +42 -0
- package/src/errors.ts +72 -0
- package/src/global.scss +5 -0
- package/src/i18n-keys.ts +34 -0
- package/src/index.ts +199 -0
- package/src/scss.d.ts +7 -0
- package/src/theme/context.ts +65 -0
- package/src/theme/inline-script.ts +44 -0
- package/src/theme/provider.tsx +56 -0
- package/src/theme/solid-adapter.ts +51 -0
- package/src/theme/theme.ts +116 -0
- package/src/tokens/_breakpoints.scss +10 -0
- package/src/tokens/_colors.scss +95 -0
- package/src/tokens/_index.scss +13 -0
- package/src/tokens/_mixins.scss +186 -0
- package/src/tokens/_motion.scss +36 -0
- package/src/tokens/_radius.scss +23 -0
- package/src/tokens/_shadow.scss +49 -0
- package/src/tokens/_space.scss +28 -0
- package/src/tokens/_typography.scss +83 -0
- package/src/tokens/_z.scss +27 -0
- package/src/tokens/reset.scss +144 -0
- package/src/tokens/theme.scss +19 -0
- package/src/tokens/tokens.ts +222 -0
package/src/cx.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Class-name combiner. Deliberately tiny: SCSS modules mean class lists are
|
|
2
|
+
// short, so there is no need for a `clsx`-sized dependency.
|
|
3
|
+
|
|
4
|
+
export type ClassValue =
|
|
5
|
+
| string
|
|
6
|
+
| number
|
|
7
|
+
| false
|
|
8
|
+
| null
|
|
9
|
+
| undefined
|
|
10
|
+
| Record<string, boolean | null | undefined>
|
|
11
|
+
| readonly ClassValue[];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Join truthy class values. Object keys are emitted when their value is truthy,
|
|
15
|
+
* arrays are flattened, and duplicates collapse so `cx(a, a)` is stable.
|
|
16
|
+
*/
|
|
17
|
+
export function cx(...values: readonly ClassValue[]): string {
|
|
18
|
+
const out: string[] = [];
|
|
19
|
+
collect(values, out);
|
|
20
|
+
return out.join(' ');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function collect(values: readonly ClassValue[], out: string[]): void {
|
|
24
|
+
for (const value of values) {
|
|
25
|
+
if (value === null || value === undefined || value === false || value === '') continue;
|
|
26
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
27
|
+
push(String(value), out);
|
|
28
|
+
} else if (Array.isArray(value)) {
|
|
29
|
+
collect(value, out);
|
|
30
|
+
} else if (typeof value === 'object') {
|
|
31
|
+
for (const [key, enabled] of Object.entries(value)) {
|
|
32
|
+
if (enabled) push(key, out);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function push(raw: string, out: string[]): void {
|
|
39
|
+
for (const name of raw.split(/\s+/)) {
|
|
40
|
+
if (name && !out.includes(name)) out.push(name);
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// @ultimat3/ui error codes. Every throw carries a stable code, the cause, and
|
|
2
|
+
// the exact fix — identical in the terminal, the browser overlay, and `--json`.
|
|
3
|
+
|
|
4
|
+
import { registerErrorCodes, UltimateError } from '@ultimat3/core';
|
|
5
|
+
|
|
6
|
+
export const UI_ERROR_CODES = {
|
|
7
|
+
tokenUnknown: 'X_TOKEN_UNKNOWN',
|
|
8
|
+
themeInvalid: 'X_THEME_INVALID',
|
|
9
|
+
runtimeMissing: 'X_UI_RUNTIME_MISSING',
|
|
10
|
+
invalidValue: 'X_UI_INVALID_VALUE',
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export type UiErrorCode = (typeof UI_ERROR_CODES)[keyof typeof UI_ERROR_CODES];
|
|
14
|
+
|
|
15
|
+
// Unconditional like every other package: all four codes are ui's own, and a second package
|
|
16
|
+
// claiming one has to throw X_ERROR_CODE_DUPLICATE at import. Taking the process down there is the
|
|
17
|
+
// point — the alternative is two packages shipping two meanings for one code, decided by load order.
|
|
18
|
+
registerErrorCodes({
|
|
19
|
+
X_TOKEN_UNKNOWN: { title: 'design token role does not exist' },
|
|
20
|
+
X_THEME_INVALID: { title: 'theme is not "light" or "dark"' },
|
|
21
|
+
X_UI_RUNTIME_MISSING: { title: 'a host capability @ultimat3/ui needs is absent' },
|
|
22
|
+
X_UI_INVALID_VALUE: { title: 'a formatting component received an unrenderable value' },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export class UiError extends UltimateError {
|
|
26
|
+
override readonly name: string = 'UiError';
|
|
27
|
+
|
|
28
|
+
constructor(init: { code: UiErrorCode; cause: string; fix: string }) {
|
|
29
|
+
super({
|
|
30
|
+
code: init.code,
|
|
31
|
+
cause: init.cause,
|
|
32
|
+
fix: init.fix,
|
|
33
|
+
docs: `https://ultimate.dev/errors/${init.code}`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A component asked for a token role that the SCSS source does not define. */
|
|
39
|
+
export function unknownTokenError(kind: string, name: string, known: readonly string[]): UiError {
|
|
40
|
+
return new UiError({
|
|
41
|
+
code: UI_ERROR_CODES.tokenUnknown,
|
|
42
|
+
cause: `unknown ${kind} token "${name}"; known roles: ${known.join(', ')}`,
|
|
43
|
+
fix: `use one of the ${kind} roles above, or add "${name}" to packages/ui/src/tokens/_${kind}s.scss and mirror it in tokens.ts`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** `data-theme` or a stored preference held something other than light/dark. */
|
|
48
|
+
export function invalidThemeError(value: unknown): UiError {
|
|
49
|
+
return new UiError({
|
|
50
|
+
code: UI_ERROR_CODES.themeInvalid,
|
|
51
|
+
cause: `theme must be "light" or "dark", received ${JSON.stringify(value)}`,
|
|
52
|
+
fix: "call setTheme('light') or setTheme('dark'), or clearTheme() to follow the OS",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A helper reached for a host capability the current environment does not have. */
|
|
57
|
+
export function runtimeMissingError(api: string, fix: string): UiError {
|
|
58
|
+
return new UiError({
|
|
59
|
+
code: UI_ERROR_CODES.runtimeMissing,
|
|
60
|
+
cause: `@ultimat3/ui needed ${api} but it is not available in this environment`,
|
|
61
|
+
fix,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A formatting component was handed a value it cannot render. */
|
|
66
|
+
export function invalidValueError(kind: string, value: unknown, expected: string): UiError {
|
|
67
|
+
return new UiError({
|
|
68
|
+
code: UI_ERROR_CODES.invalidValue,
|
|
69
|
+
cause: `<${kind}> received ${JSON.stringify(value)}, which is not ${expected}`,
|
|
70
|
+
fix: `pass ${expected} — parse or validate the value in the loader, not in the component`,
|
|
71
|
+
});
|
|
72
|
+
}
|
package/src/global.scss
ADDED
package/src/i18n-keys.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Every string the design system needs but cannot receive as a prop. Components
|
|
2
|
+
// resolve these through the context translator, so an app can localise the whole
|
|
3
|
+
// system by adding one namespace to its catalog — and a miss is loud (⟦key⟧).
|
|
4
|
+
|
|
5
|
+
export const UI_KEYS = {
|
|
6
|
+
close: 'ui.close',
|
|
7
|
+
cancel: 'ui.cancel',
|
|
8
|
+
confirm: 'ui.confirm',
|
|
9
|
+
dismiss: 'ui.dismiss',
|
|
10
|
+
loading: 'ui.loading',
|
|
11
|
+
empty: 'ui.empty',
|
|
12
|
+
error: 'ui.error',
|
|
13
|
+
retry: 'ui.retry',
|
|
14
|
+
next: 'ui.next',
|
|
15
|
+
previous: 'ui.previous',
|
|
16
|
+
page: 'ui.page',
|
|
17
|
+
sortAscending: 'ui.sort.ascending',
|
|
18
|
+
sortDescending: 'ui.sort.descending',
|
|
19
|
+
breadcrumb: 'ui.breadcrumb',
|
|
20
|
+
menu: 'ui.menu',
|
|
21
|
+
more: 'ui.more',
|
|
22
|
+
required: 'ui.required',
|
|
23
|
+
optional: 'ui.optional',
|
|
24
|
+
theme: 'ui.theme',
|
|
25
|
+
themeLight: 'ui.theme.light',
|
|
26
|
+
themeDark: 'ui.theme.dark',
|
|
27
|
+
themeSystem: 'ui.theme.system',
|
|
28
|
+
language: 'ui.language',
|
|
29
|
+
errorCode: 'ui.error.code',
|
|
30
|
+
errorCause: 'ui.error.cause',
|
|
31
|
+
errorFix: 'ui.error.fix',
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
export type UiKey = (typeof UI_KEYS)[keyof typeof UI_KEYS];
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// The public surface of @ultimat3/ui. Explicit exports only — this list is the
|
|
2
|
+
// design system's contract, and anything not named here is internal.
|
|
3
|
+
|
|
4
|
+
// Ambient declarations are not reachable through imports, so a consuming app's
|
|
5
|
+
// program would never load them — every `*.module.scss` import in this package
|
|
6
|
+
// would be TS2307 there. The reference pulls the contract along with the entry.
|
|
7
|
+
/// <reference path="./scss.d.ts" />
|
|
8
|
+
|
|
9
|
+
export type { FocusTrap, Politeness, RovingOptions, RovingOrientation } from './a11y';
|
|
10
|
+
export {
|
|
11
|
+
announce,
|
|
12
|
+
ariaBool,
|
|
13
|
+
createFocusTrap,
|
|
14
|
+
createRovingTabindex,
|
|
15
|
+
FOCUSABLE_SELECTOR,
|
|
16
|
+
focusableWithin,
|
|
17
|
+
nextRovingIndex,
|
|
18
|
+
resetIdCounter,
|
|
19
|
+
useId,
|
|
20
|
+
} from './a11y';
|
|
21
|
+
export type { AlertProps } from './components/Alert';
|
|
22
|
+
// --- components --------------------------------------------------------------
|
|
23
|
+
export { Alert } from './components/Alert';
|
|
24
|
+
export type { AvatarProps } from './components/Avatar';
|
|
25
|
+
export { Avatar, initialsOf } from './components/Avatar';
|
|
26
|
+
export type { BadgeProps } from './components/Badge';
|
|
27
|
+
export { Badge } from './components/Badge';
|
|
28
|
+
export type { BreadcrumbItem, BreadcrumbProps } from './components/Breadcrumb';
|
|
29
|
+
export { Breadcrumb } from './components/Breadcrumb';
|
|
30
|
+
export type { ButtonProps } from './components/Button';
|
|
31
|
+
export { Button } from './components/Button';
|
|
32
|
+
export type { CardProps, Elevation } from './components/Card';
|
|
33
|
+
export { Card } from './components/Card';
|
|
34
|
+
export type { CheckboxProps } from './components/Checkbox';
|
|
35
|
+
export { Checkbox } from './components/Checkbox';
|
|
36
|
+
export type { ContainerProps, ContainerSize } from './components/Container';
|
|
37
|
+
export { Container } from './components/Container';
|
|
38
|
+
export type { Column, DataTableProps } from './components/DataTable';
|
|
39
|
+
export { DataTable } from './components/DataTable';
|
|
40
|
+
export type { DateTimeProps } from './components/DateTime';
|
|
41
|
+
export { DateTime } from './components/DateTime';
|
|
42
|
+
export type { DialogProps } from './components/Dialog';
|
|
43
|
+
export { Dialog } from './components/Dialog';
|
|
44
|
+
export type { DividerProps } from './components/Divider';
|
|
45
|
+
export { Divider } from './components/Divider';
|
|
46
|
+
export type { DrawerProps, DrawerSide } from './components/Drawer';
|
|
47
|
+
export { Drawer } from './components/Drawer';
|
|
48
|
+
export type {
|
|
49
|
+
DateStyle,
|
|
50
|
+
DateTimeFormatter,
|
|
51
|
+
DateTimeView,
|
|
52
|
+
DateTimeViewOptions,
|
|
53
|
+
TimeInput,
|
|
54
|
+
} from './components/date-time-view';
|
|
55
|
+
export { dateTimeView, toDate, toInstant, toIsoInstant } from './components/date-time-view';
|
|
56
|
+
export type { EmptyStateProps } from './components/EmptyState';
|
|
57
|
+
export { EmptyState } from './components/EmptyState';
|
|
58
|
+
export type { ErrorStateProps } from './components/ErrorState';
|
|
59
|
+
export { ErrorState, errorParts } from './components/ErrorState';
|
|
60
|
+
export type { FieldControl, FieldProps } from './components/Field';
|
|
61
|
+
export { Field } from './components/Field';
|
|
62
|
+
export type { FormProps } from './components/Form';
|
|
63
|
+
export { Form } from './components/Form';
|
|
64
|
+
export type { GridProps } from './components/Grid';
|
|
65
|
+
export { Grid } from './components/Grid';
|
|
66
|
+
export type { IconButtonProps } from './components/IconButton';
|
|
67
|
+
export { IconButton } from './components/IconButton';
|
|
68
|
+
export type { ImageProps } from './components/Image';
|
|
69
|
+
export { Image } from './components/Image';
|
|
70
|
+
export type { InputProps, InputType } from './components/Input';
|
|
71
|
+
export { Input } from './components/Input';
|
|
72
|
+
export type { ImageBox, ImageLoadingHints, ImageVariant } from './components/image-source';
|
|
73
|
+
export { boxFor, loadingHints, srcsetFor } from './components/image-source';
|
|
74
|
+
export type { LinkProps } from './components/Link';
|
|
75
|
+
export { Link } from './components/Link';
|
|
76
|
+
export type { LocaleSwitcherProps } from './components/LocaleSwitcher';
|
|
77
|
+
export { LocaleSwitcher, localeLabel } from './components/LocaleSwitcher';
|
|
78
|
+
export type { MenuItem, MenuProps } from './components/Menu';
|
|
79
|
+
export { Menu } from './components/Menu';
|
|
80
|
+
export type { MoneyProps } from './components/Money';
|
|
81
|
+
export { Money } from './components/Money';
|
|
82
|
+
export type { MoneyFormatter, MoneyInput, MoneyViewOptions } from './components/money-view';
|
|
83
|
+
// --- formatting cores (pure, renderer-free) ----------------------------------
|
|
84
|
+
export { moneyText, toMoney } from './components/money-view';
|
|
85
|
+
export type { PaginationProps } from './components/Pagination';
|
|
86
|
+
export { Pagination } from './components/Pagination';
|
|
87
|
+
export type { Placement, PopoverProps } from './components/Popover';
|
|
88
|
+
export { Popover } from './components/Popover';
|
|
89
|
+
export type { RadioOption, RadioProps } from './components/Radio';
|
|
90
|
+
export { Radio } from './components/Radio';
|
|
91
|
+
export type { RelativeTimeProps } from './components/RelativeTime';
|
|
92
|
+
export { RelativeTime } from './components/RelativeTime';
|
|
93
|
+
export type { RelativeTimeOptions } from './components/relative-time-view';
|
|
94
|
+
export { relativeTimeText } from './components/relative-time-view';
|
|
95
|
+
export type { SelectOption, SelectProps } from './components/Select';
|
|
96
|
+
export { Select } from './components/Select';
|
|
97
|
+
export type { SkeletonProps } from './components/Skeleton';
|
|
98
|
+
export { Skeleton } from './components/Skeleton';
|
|
99
|
+
export type { SpinnerProps } from './components/Spinner';
|
|
100
|
+
export { Spinner } from './components/Spinner';
|
|
101
|
+
export type { StackProps } from './components/Stack';
|
|
102
|
+
export { Stack } from './components/Stack';
|
|
103
|
+
export type { SwitchProps } from './components/Switch';
|
|
104
|
+
export { Switch } from './components/Switch';
|
|
105
|
+
export type { SortDirection, SortState } from './components/sort-state';
|
|
106
|
+
export { ariaSortFor, nextSortState } from './components/sort-state';
|
|
107
|
+
export type { TableProps } from './components/Table';
|
|
108
|
+
export { Table } from './components/Table';
|
|
109
|
+
export type { TabItem, TabsProps } from './components/Tabs';
|
|
110
|
+
export { Tabs } from './components/Tabs';
|
|
111
|
+
export type { TextProps, TextSize, TextTone, TextWeight } from './components/Text';
|
|
112
|
+
export { TEXT_TONES, Text } from './components/Text';
|
|
113
|
+
export type { TextareaProps } from './components/Textarea';
|
|
114
|
+
export { Textarea } from './components/Textarea';
|
|
115
|
+
export type { ThemeChoice, ThemeToggleProps } from './components/ThemeToggle';
|
|
116
|
+
export { ThemeToggle } from './components/ThemeToggle';
|
|
117
|
+
export type { ToastProps, ToastRegionProps } from './components/Toast';
|
|
118
|
+
export { Toast, ToastRegion } from './components/Toast';
|
|
119
|
+
export type { TooltipProps } from './components/Tooltip';
|
|
120
|
+
export { Tooltip } from './components/Tooltip';
|
|
121
|
+
export type { Align, ButtonVariant, Size, SpaceStep, Tone } from './components/variants';
|
|
122
|
+
// --- variants ----------------------------------------------------------------
|
|
123
|
+
export { BUTTON_VARIANTS, SIZES, TONES } from './components/variants';
|
|
124
|
+
export type { ClassValue } from './cx';
|
|
125
|
+
// --- helpers -----------------------------------------------------------------
|
|
126
|
+
export { cx } from './cx';
|
|
127
|
+
export type { UiErrorCode } from './errors';
|
|
128
|
+
export {
|
|
129
|
+
invalidThemeError,
|
|
130
|
+
invalidValueError,
|
|
131
|
+
runtimeMissingError,
|
|
132
|
+
UI_ERROR_CODES,
|
|
133
|
+
UiError,
|
|
134
|
+
unknownTokenError,
|
|
135
|
+
} from './errors';
|
|
136
|
+
export type { UiKey } from './i18n-keys';
|
|
137
|
+
export { UI_KEYS } from './i18n-keys';
|
|
138
|
+
export type { Direction, UiContextValue } from './theme/context';
|
|
139
|
+
export {
|
|
140
|
+
defaultUiContext,
|
|
141
|
+
fallbackTranslator,
|
|
142
|
+
UI_DEFAULT_CURRENCY,
|
|
143
|
+
UI_DEFAULT_LOCALE,
|
|
144
|
+
UI_DEFAULT_TIME_ZONE,
|
|
145
|
+
uiContext,
|
|
146
|
+
useUi,
|
|
147
|
+
} from './theme/context';
|
|
148
|
+
export {
|
|
149
|
+
THEME_INLINE_SCRIPT,
|
|
150
|
+
themeInlineScriptCspSource,
|
|
151
|
+
themeInlineScriptHash,
|
|
152
|
+
themeInlineScriptTag,
|
|
153
|
+
} from './theme/inline-script';
|
|
154
|
+
export type { UiProviderProps } from './theme/provider';
|
|
155
|
+
export { UiProvider } from './theme/provider';
|
|
156
|
+
export type { Accessor, Setter, SolidContext, SolidRuntime } from './theme/solid-adapter';
|
|
157
|
+
export {
|
|
158
|
+
clearSolidRuntime,
|
|
159
|
+
hasSolidRuntime,
|
|
160
|
+
setSolidRuntime,
|
|
161
|
+
solid,
|
|
162
|
+
} from './theme/solid-adapter';
|
|
163
|
+
export type { ThemeEnv } from './theme/theme';
|
|
164
|
+
// --- theme -------------------------------------------------------------------
|
|
165
|
+
export {
|
|
166
|
+
browserThemeEnv,
|
|
167
|
+
clearTheme,
|
|
168
|
+
initTheme,
|
|
169
|
+
isTheme,
|
|
170
|
+
osTheme,
|
|
171
|
+
resolveTheme,
|
|
172
|
+
setTheme,
|
|
173
|
+
storedTheme,
|
|
174
|
+
THEME_ATTRIBUTE,
|
|
175
|
+
THEME_MEDIA_QUERY,
|
|
176
|
+
THEME_STORAGE_KEY,
|
|
177
|
+
toggleTheme,
|
|
178
|
+
watchOsTheme,
|
|
179
|
+
} from './theme/theme';
|
|
180
|
+
export type { ColorRole, Theme } from './tokens/tokens';
|
|
181
|
+
// --- tokens ------------------------------------------------------------------
|
|
182
|
+
export {
|
|
183
|
+
assertColorRole,
|
|
184
|
+
breakpointTokens,
|
|
185
|
+
COLOR_ROLES,
|
|
186
|
+
color,
|
|
187
|
+
colorRgb,
|
|
188
|
+
colorTokens,
|
|
189
|
+
colorVar,
|
|
190
|
+
durationTokens,
|
|
191
|
+
easingTokens,
|
|
192
|
+
fontSizeTokens,
|
|
193
|
+
fontWeightTokens,
|
|
194
|
+
lineHeightTokens,
|
|
195
|
+
radiusTokens,
|
|
196
|
+
shadowTokens,
|
|
197
|
+
spaceTokens,
|
|
198
|
+
zTokens,
|
|
199
|
+
} from './tokens/tokens';
|
package/src/scss.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// SCSS modules resolve to a class-name map at build time. The default export is
|
|
2
|
+
// the bundler's contract, not ours — component code never re-exports it.
|
|
3
|
+
|
|
4
|
+
declare module '*.module.scss' {
|
|
5
|
+
const classes: Readonly<Record<string, string>>;
|
|
6
|
+
export default classes;
|
|
7
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// The ambient presentation contract every component reads: theme, locale, tz,
|
|
2
|
+
// currency, direction, translator. Formatting components take nothing from a
|
|
3
|
+
// process-wide default — everything they need arrives here, via UiProvider.
|
|
4
|
+
|
|
5
|
+
import type { Translator } from '@ultimat3/i18n';
|
|
6
|
+
import { createTranslator, type Direction, directionOf, type Locale } from '@ultimat3/i18n';
|
|
7
|
+
import type { TimeZone } from '@ultimat3/time';
|
|
8
|
+
import type { Theme } from '../tokens/tokens';
|
|
9
|
+
import { type SolidContext, type SolidRuntime, solid } from './solid-adapter';
|
|
10
|
+
|
|
11
|
+
export type { Direction };
|
|
12
|
+
|
|
13
|
+
export interface UiContextValue {
|
|
14
|
+
readonly theme: Theme;
|
|
15
|
+
/** BCP-47 tag. Drives every Intl call in the tree. */
|
|
16
|
+
readonly locale: Locale;
|
|
17
|
+
/** IANA zone. A date is never formatted without one. */
|
|
18
|
+
readonly timeZone: TimeZone;
|
|
19
|
+
/** ISO-4217 default for <Money> when the value is bare minor units. */
|
|
20
|
+
readonly currency: string;
|
|
21
|
+
readonly dir: Direction;
|
|
22
|
+
readonly t: Translator;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const UI_DEFAULT_LOCALE: Locale = 'en';
|
|
26
|
+
export const UI_DEFAULT_TIME_ZONE = 'UTC' as TimeZone;
|
|
27
|
+
export const UI_DEFAULT_CURRENCY = 'USD';
|
|
28
|
+
|
|
29
|
+
/** Loud-miss translator: a forgotten catalog key renders ⟦key⟧, never blank. */
|
|
30
|
+
export function fallbackTranslator(locale: Locale = UI_DEFAULT_LOCALE): Translator {
|
|
31
|
+
return createTranslator({}, locale);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function defaultUiContext(): UiContextValue {
|
|
35
|
+
return {
|
|
36
|
+
theme: 'light',
|
|
37
|
+
locale: UI_DEFAULT_LOCALE,
|
|
38
|
+
timeZone: UI_DEFAULT_TIME_ZONE,
|
|
39
|
+
currency: UI_DEFAULT_CURRENCY,
|
|
40
|
+
dir: directionOf(UI_DEFAULT_LOCALE),
|
|
41
|
+
t: fallbackTranslator(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let cached: {
|
|
46
|
+
readonly runtime: SolidRuntime;
|
|
47
|
+
readonly context: SolidContext<UiContextValue>;
|
|
48
|
+
} | null = null;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Created lazily so importing this module never needs a Solid runtime, and keyed on the runtime
|
|
52
|
+
* that built it: a context belongs to the reactive graph that created it, so handing a stale one
|
|
53
|
+
* to a replaced runtime reads as a working provider while every consumer sees the default value.
|
|
54
|
+
*/
|
|
55
|
+
export function uiContext(): SolidContext<UiContextValue> {
|
|
56
|
+
const runtime = solid();
|
|
57
|
+
if (cached === null || cached.runtime !== runtime) {
|
|
58
|
+
cached = { runtime, context: runtime.createContext(defaultUiContext()) };
|
|
59
|
+
}
|
|
60
|
+
return cached.context;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useUi(): UiContextValue {
|
|
64
|
+
return solid().useContext(uiContext());
|
|
65
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The blocking <head> snippet that kills the flash of wrong theme. Built from the
|
|
2
|
+
// same constants as theme.ts so the two can never disagree. Server/build-only:
|
|
3
|
+
// the hash is computed with Bun's hasher for the CSP header.
|
|
4
|
+
|
|
5
|
+
import { runtimeMissingError } from '../errors';
|
|
6
|
+
import { THEME_ATTRIBUTE, THEME_MEDIA_QUERY, THEME_STORAGE_KEY } from './theme';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Minified on purpose — this runs before first paint and is inlined into every
|
|
10
|
+
* document. Reads the explicit choice, falls back to the OS, and always stamps
|
|
11
|
+
* `data-theme` so screenshots and Playwright runs are deterministic.
|
|
12
|
+
*/
|
|
13
|
+
export const THEME_INLINE_SCRIPT =
|
|
14
|
+
`try{var d=document.documentElement,` +
|
|
15
|
+
`s=localStorage.getItem('${THEME_STORAGE_KEY}');` +
|
|
16
|
+
`d.setAttribute('${THEME_ATTRIBUTE}',` +
|
|
17
|
+
`s==='light'||s==='dark'?s:` +
|
|
18
|
+
`matchMedia('${THEME_MEDIA_QUERY}').matches?'dark':'light')}catch(e){}`;
|
|
19
|
+
|
|
20
|
+
/** The exact tag to inline. No attributes, so the CSP hash stays valid. */
|
|
21
|
+
export function themeInlineScriptTag(): string {
|
|
22
|
+
return `<script>${THEME_INLINE_SCRIPT}</script>`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let cachedHash: string | null = null;
|
|
26
|
+
|
|
27
|
+
/** `sha256-<base64>` for `script-src`. Stable for a given script text. */
|
|
28
|
+
export function themeInlineScriptHash(): string {
|
|
29
|
+
if (cachedHash !== null) return cachedHash;
|
|
30
|
+
if (typeof Bun === 'undefined') {
|
|
31
|
+
throw runtimeMissingError(
|
|
32
|
+
'Bun.CryptoHasher to hash the theme inline script',
|
|
33
|
+
'call themeInlineScriptHash() during build or SSR, never in browser code',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const digest = new Bun.CryptoHasher('sha256').update(THEME_INLINE_SCRIPT).digest('base64');
|
|
37
|
+
cachedHash = `sha256-${digest}`;
|
|
38
|
+
return cachedHash;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Quoted CSP source expression, ready to concatenate into a `script-src`. */
|
|
42
|
+
export function themeInlineScriptCspSource(): string {
|
|
43
|
+
return `'${themeInlineScriptHash()}'`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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.
|
|
3
|
+
|
|
4
|
+
import { directionOf, type Locale, type Translator } from '@ultimat3/i18n';
|
|
5
|
+
import type { TimeZone } from '@ultimat3/time';
|
|
6
|
+
import type { JSX } from 'solid-js';
|
|
7
|
+
import type { Theme } from '../tokens/tokens';
|
|
8
|
+
import {
|
|
9
|
+
fallbackTranslator,
|
|
10
|
+
UI_DEFAULT_CURRENCY,
|
|
11
|
+
UI_DEFAULT_LOCALE,
|
|
12
|
+
UI_DEFAULT_TIME_ZONE,
|
|
13
|
+
type UiContextValue,
|
|
14
|
+
uiContext,
|
|
15
|
+
} from './context';
|
|
16
|
+
import { solid } from './solid-adapter';
|
|
17
|
+
|
|
18
|
+
export interface UiProviderProps {
|
|
19
|
+
theme?: Theme | undefined;
|
|
20
|
+
locale?: Locale | undefined;
|
|
21
|
+
timeZone?: TimeZone | undefined;
|
|
22
|
+
currency?: string | undefined;
|
|
23
|
+
t?: Translator | undefined;
|
|
24
|
+
children?: JSX.Element | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Also reflects `lang` and `dir` onto <html>, so the document, native form
|
|
29
|
+
* controls, and every logical property agree with the injected locale.
|
|
30
|
+
*/
|
|
31
|
+
export function UiProvider(props: UiProviderProps): JSX.Element {
|
|
32
|
+
const rt = solid();
|
|
33
|
+
const value = rt.createMemo<UiContextValue>(() => {
|
|
34
|
+
const locale = props.locale ?? UI_DEFAULT_LOCALE;
|
|
35
|
+
return {
|
|
36
|
+
theme: props.theme ?? 'light',
|
|
37
|
+
locale,
|
|
38
|
+
timeZone: props.timeZone ?? UI_DEFAULT_TIME_ZONE,
|
|
39
|
+
currency: props.currency ?? UI_DEFAULT_CURRENCY,
|
|
40
|
+
dir: directionOf(locale),
|
|
41
|
+
t: props.t ?? fallbackTranslator(locale),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
rt.createEffect(() => {
|
|
46
|
+
if (typeof document === 'undefined') return;
|
|
47
|
+
const root = document.documentElement;
|
|
48
|
+
const next = value();
|
|
49
|
+
root.setAttribute('lang', next.locale);
|
|
50
|
+
root.setAttribute('dir', next.dir);
|
|
51
|
+
if (props.theme !== undefined) root.setAttribute('data-theme', next.theme);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const Ctx = uiContext();
|
|
55
|
+
return <Ctx.Provider value={value()}>{props.children}</Ctx.Provider>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Solid runtime adapter. @ultimat3/ui imports *types* from solid-js and nothing
|
|
2
|
+
// else, so the package installs, typechecks, and tests with no reactive runtime
|
|
3
|
+
// present. The app entry (or @ultimat3/render) registers the real one once.
|
|
4
|
+
|
|
5
|
+
import type { JSX } from 'solid-js';
|
|
6
|
+
import { runtimeMissingError } from '../errors';
|
|
7
|
+
|
|
8
|
+
export type Accessor<T> = () => T;
|
|
9
|
+
export type Setter<T> = (next: T) => void;
|
|
10
|
+
|
|
11
|
+
export interface SolidContext<T> {
|
|
12
|
+
readonly id: symbol;
|
|
13
|
+
readonly defaultValue: T;
|
|
14
|
+
readonly Provider: (props: { value: T; children?: JSX.Element }) => JSX.Element;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The exact slice of solid-js the design system touches. */
|
|
18
|
+
export interface SolidRuntime {
|
|
19
|
+
createContext<T>(defaultValue: T): SolidContext<T>;
|
|
20
|
+
useContext<T>(context: SolidContext<T>): T;
|
|
21
|
+
createSignal<T>(value: T): [Accessor<T>, Setter<T>];
|
|
22
|
+
createMemo<T>(fn: () => T): Accessor<T>;
|
|
23
|
+
createEffect(fn: () => void): void;
|
|
24
|
+
onCleanup(fn: () => void): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let runtime: SolidRuntime | null = null;
|
|
28
|
+
|
|
29
|
+
/** Register once, in the app entry, before the first render. */
|
|
30
|
+
export function setSolidRuntime(next: SolidRuntime): void {
|
|
31
|
+
runtime = next;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function hasSolidRuntime(): boolean {
|
|
35
|
+
return runtime !== null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** For tests: drop the registration so cases stay independent. */
|
|
39
|
+
export function clearSolidRuntime(): void {
|
|
40
|
+
runtime = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function solid(): SolidRuntime {
|
|
44
|
+
if (runtime === null) {
|
|
45
|
+
throw runtimeMissingError(
|
|
46
|
+
'a registered Solid runtime',
|
|
47
|
+
"add setSolidRuntime(await import('solid-js')) to the app entry before render",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return runtime;
|
|
51
|
+
}
|