@sveltier/ui 0.0.0-stage → 0.1.0-alpha.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 +82 -2
- package/dist/components/Alert/Alert.svelte +88 -0
- package/dist/components/Alert/Alert.svelte.d.ts +4 -0
- package/dist/components/Alert/alert.css +78 -0
- package/dist/components/Alert/index.d.ts +2 -0
- package/dist/components/Alert/index.js +1 -0
- package/dist/components/Alert/types.d.ts +67 -0
- package/dist/components/Alert/types.js +1 -0
- package/dist/components/Avatar/Avatar.svelte +47 -0
- package/dist/components/Avatar/Avatar.svelte.d.ts +4 -0
- package/dist/components/Avatar/avatar.css +34 -0
- package/dist/components/Avatar/index.d.ts +2 -0
- package/dist/components/Avatar/index.js +1 -0
- package/dist/components/Avatar/types.d.ts +47 -0
- package/dist/components/Avatar/types.js +1 -0
- package/dist/components/Badge/Badge.svelte +53 -0
- package/dist/components/Badge/Badge.svelte.d.ts +4 -0
- package/dist/components/Badge/badge.css +57 -0
- package/dist/components/Badge/index.d.ts +2 -0
- package/dist/components/Badge/index.js +1 -0
- package/dist/components/Badge/types.d.ts +43 -0
- package/dist/components/Badge/types.js +1 -0
- package/dist/components/Button/Button.svelte +85 -0
- package/dist/components/Button/Button.svelte.d.ts +4 -0
- package/dist/components/Button/button.css +132 -0
- package/dist/components/Button/index.d.ts +2 -0
- package/dist/components/Button/index.js +1 -0
- package/dist/components/Button/types.d.ts +76 -0
- package/dist/components/Button/types.js +1 -0
- package/dist/components/ButtonGroup/ButtonGroup.svelte +61 -0
- package/dist/components/ButtonGroup/ButtonGroup.svelte.d.ts +4 -0
- package/dist/components/ButtonGroup/button-group.css +39 -0
- package/dist/components/ButtonGroup/index.d.ts +2 -0
- package/dist/components/ButtonGroup/index.js +1 -0
- package/dist/components/ButtonGroup/types.d.ts +48 -0
- package/dist/components/ButtonGroup/types.js +1 -0
- package/dist/components/Card/Card.svelte +53 -0
- package/dist/components/Card/Card.svelte.d.ts +4 -0
- package/dist/components/Card/card.css +62 -0
- package/dist/components/Card/index.d.ts +2 -0
- package/dist/components/Card/index.js +1 -0
- package/dist/components/Card/types.d.ts +56 -0
- package/dist/components/Card/types.js +1 -0
- package/dist/components/Checkbox/Checkbox.svelte +105 -0
- package/dist/components/Checkbox/Checkbox.svelte.d.ts +4 -0
- package/dist/components/Checkbox/checkbox.css +138 -0
- package/dist/components/Checkbox/index.d.ts +2 -0
- package/dist/components/Checkbox/index.js +1 -0
- package/dist/components/Checkbox/types.d.ts +76 -0
- package/dist/components/Checkbox/types.js +1 -0
- package/dist/components/Chip/Chip.svelte +109 -0
- package/dist/components/Chip/Chip.svelte.d.ts +4 -0
- package/dist/components/Chip/chip.css +101 -0
- package/dist/components/Chip/index.d.ts +2 -0
- package/dist/components/Chip/index.js +1 -0
- package/dist/components/Chip/types.d.ts +69 -0
- package/dist/components/Chip/types.js +1 -0
- package/dist/components/Choice/Choice.svelte +60 -0
- package/dist/components/Choice/Choice.svelte.d.ts +4 -0
- package/dist/components/Choice/choice.css +75 -0
- package/dist/components/Choice/types.d.ts +43 -0
- package/dist/components/Choice/types.js +1 -0
- package/dist/components/Divider/Divider.svelte +19 -0
- package/dist/components/Divider/Divider.svelte.d.ts +4 -0
- package/dist/components/Divider/divider.css +23 -0
- package/dist/components/Divider/index.d.ts +2 -0
- package/dist/components/Divider/index.js +1 -0
- package/dist/components/Divider/types.d.ts +10 -0
- package/dist/components/Divider/types.js +1 -0
- package/dist/components/Field/Field.svelte +95 -0
- package/dist/components/Field/Field.svelte.d.ts +4 -0
- package/dist/components/Field/field.css +210 -0
- package/dist/components/Field/index.d.ts +3 -0
- package/dist/components/Field/index.js +2 -0
- package/dist/components/Field/types.d.ts +109 -0
- package/dist/components/Field/types.js +2 -0
- package/dist/components/Form/Form.svelte +93 -0
- package/dist/components/Form/Form.svelte.d.ts +8 -0
- package/dist/components/Form/form.css +16 -0
- package/dist/components/Form/index.d.ts +2 -0
- package/dist/components/Form/index.js +1 -0
- package/dist/components/Form/types.d.ts +38 -0
- package/dist/components/Form/types.js +1 -0
- package/dist/components/Icon/Icon.svelte +38 -0
- package/dist/components/Icon/Icon.svelte.d.ts +4 -0
- package/dist/components/Icon/icon.css +33 -0
- package/dist/components/Icon/index.d.ts +2 -0
- package/dist/components/Icon/index.js +1 -0
- package/dist/components/Icon/types.d.ts +34 -0
- package/dist/components/Icon/types.js +1 -0
- package/dist/components/List/List.svelte +39 -0
- package/dist/components/List/List.svelte.d.ts +4 -0
- package/dist/components/List/ListItem.svelte +57 -0
- package/dist/components/List/ListItem.svelte.d.ts +4 -0
- package/dist/components/List/index.d.ts +3 -0
- package/dist/components/List/index.js +2 -0
- package/dist/components/List/list.css +127 -0
- package/dist/components/List/types.d.ts +67 -0
- package/dist/components/List/types.js +1 -0
- package/dist/components/NativeSelect/NativeSelect.svelte +110 -0
- package/dist/components/NativeSelect/NativeSelect.svelte.d.ts +25 -0
- package/dist/components/NativeSelect/index.d.ts +2 -0
- package/dist/components/NativeSelect/index.js +1 -0
- package/dist/components/NativeSelect/native-select.css +45 -0
- package/dist/components/NativeSelect/types.d.ts +58 -0
- package/dist/components/NativeSelect/types.js +1 -0
- package/dist/components/NumberField/NumberField.svelte +246 -0
- package/dist/components/NumberField/NumberField.svelte.d.ts +4 -0
- package/dist/components/NumberField/index.d.ts +2 -0
- package/dist/components/NumberField/index.js +1 -0
- package/dist/components/NumberField/number-field.css +74 -0
- package/dist/components/NumberField/types.d.ts +73 -0
- package/dist/components/NumberField/types.js +1 -0
- package/dist/components/Progress/ProgressCircular.svelte +61 -0
- package/dist/components/Progress/ProgressCircular.svelte.d.ts +4 -0
- package/dist/components/Progress/ProgressLinear.svelte +44 -0
- package/dist/components/Progress/ProgressLinear.svelte.d.ts +4 -0
- package/dist/components/Progress/index.d.ts +3 -0
- package/dist/components/Progress/index.js +2 -0
- package/dist/components/Progress/progress-circular.css +61 -0
- package/dist/components/Progress/progress-linear.css +67 -0
- package/dist/components/Progress/types.d.ts +53 -0
- package/dist/components/Progress/types.js +1 -0
- package/dist/components/RadioGroup/RadioGroup.svelte +149 -0
- package/dist/components/RadioGroup/RadioGroup.svelte.d.ts +25 -0
- package/dist/components/RadioGroup/index.d.ts +2 -0
- package/dist/components/RadioGroup/index.js +1 -0
- package/dist/components/RadioGroup/radio-group.css +139 -0
- package/dist/components/RadioGroup/types.d.ts +93 -0
- package/dist/components/RadioGroup/types.js +1 -0
- package/dist/components/SegmentedControl/SegmentedControl.svelte +70 -0
- package/dist/components/SegmentedControl/SegmentedControl.svelte.d.ts +25 -0
- package/dist/components/SegmentedControl/index.d.ts +2 -0
- package/dist/components/SegmentedControl/index.js +1 -0
- package/dist/components/SegmentedControl/segmented-control.css +68 -0
- package/dist/components/SegmentedControl/types.d.ts +60 -0
- package/dist/components/SegmentedControl/types.js +1 -0
- package/dist/components/Skeleton/Skeleton.svelte +33 -0
- package/dist/components/Skeleton/Skeleton.svelte.d.ts +4 -0
- package/dist/components/Skeleton/index.d.ts +2 -0
- package/dist/components/Skeleton/index.js +1 -0
- package/dist/components/Skeleton/skeleton.css +70 -0
- package/dist/components/Skeleton/types.d.ts +25 -0
- package/dist/components/Skeleton/types.js +1 -0
- package/dist/components/Slider/Slider.svelte +153 -0
- package/dist/components/Slider/Slider.svelte.d.ts +4 -0
- package/dist/components/Slider/index.d.ts +2 -0
- package/dist/components/Slider/index.js +1 -0
- package/dist/components/Slider/slider.css +206 -0
- package/dist/components/Slider/types.d.ts +102 -0
- package/dist/components/Slider/types.js +1 -0
- package/dist/components/Switch/Switch.svelte +94 -0
- package/dist/components/Switch/Switch.svelte.d.ts +4 -0
- package/dist/components/Switch/index.d.ts +2 -0
- package/dist/components/Switch/index.js +1 -0
- package/dist/components/Switch/switch.css +118 -0
- package/dist/components/Switch/types.d.ts +67 -0
- package/dist/components/Switch/types.js +1 -0
- package/dist/components/Tabs/Tabs.svelte +93 -0
- package/dist/components/Tabs/Tabs.svelte.d.ts +25 -0
- package/dist/components/Tabs/index.d.ts +2 -0
- package/dist/components/Tabs/index.js +1 -0
- package/dist/components/Tabs/tabs.css +73 -0
- package/dist/components/Tabs/types.d.ts +59 -0
- package/dist/components/Tabs/types.js +1 -0
- package/dist/components/TextField/TextField.svelte +115 -0
- package/dist/components/TextField/TextField.svelte.d.ts +4 -0
- package/dist/components/TextField/index.d.ts +2 -0
- package/dist/components/TextField/index.js +1 -0
- package/dist/components/TextField/types.d.ts +57 -0
- package/dist/components/TextField/types.js +1 -0
- package/dist/components/Textarea/Textarea.svelte +116 -0
- package/dist/components/Textarea/Textarea.svelte.d.ts +4 -0
- package/dist/components/Textarea/index.d.ts +2 -0
- package/dist/components/Textarea/index.js +1 -0
- package/dist/components/Textarea/textarea.css +35 -0
- package/dist/components/Textarea/types.d.ts +60 -0
- package/dist/components/Textarea/types.js +1 -0
- package/dist/core/defaults/DefaultsProvider.svelte +30 -0
- package/dist/core/defaults/DefaultsProvider.svelte.d.ts +13 -0
- package/dist/core/defaults/context.d.ts +45 -0
- package/dist/core/defaults/context.js +38 -0
- package/dist/core/display/display.d.ts +39 -0
- package/dist/core/display/display.js +53 -0
- package/dist/core/errors/codes.d.ts +112 -0
- package/dist/core/errors/codes.js +96 -0
- package/dist/core/errors/errors.d.ts +8 -0
- package/dist/core/errors/errors.js +7 -0
- package/dist/core/form/constraints.d.ts +15 -0
- package/dist/core/form/constraints.js +42 -0
- package/dist/core/form/context.d.ts +19 -0
- package/dist/core/form/context.js +4 -0
- package/dist/core/form/field.svelte.d.ts +34 -0
- package/dist/core/form/field.svelte.js +66 -0
- package/dist/core/form/number.d.ts +24 -0
- package/dist/core/form/number.js +71 -0
- package/dist/core/form/types.d.ts +22 -0
- package/dist/core/form/types.js +1 -0
- package/dist/core/locale/locale.d.ts +55 -0
- package/dist/core/locale/locale.js +80 -0
- package/dist/core/props/classes.d.ts +21 -0
- package/dist/core/props/classes.js +44 -0
- package/dist/core/props/element.d.ts +18 -0
- package/dist/core/props/element.js +1 -0
- package/dist/core/props/types.d.ts +47 -0
- package/dist/core/props/types.js +16 -0
- package/dist/core/provider/UIProvider.svelte +60 -0
- package/dist/core/provider/UIProvider.svelte.d.ts +29 -0
- package/dist/core/theme/ThemeProvider.svelte +178 -0
- package/dist/core/theme/ThemeProvider.svelte.d.ts +25 -0
- package/dist/core/theme/color.d.ts +23 -0
- package/dist/core/theme/color.js +54 -0
- package/dist/core/theme/context.d.ts +25 -0
- package/dist/core/theme/context.js +14 -0
- package/dist/core/theme/css.d.ts +25 -0
- package/dist/core/theme/css.js +69 -0
- package/dist/core/theme/define.d.ts +11 -0
- package/dist/core/theme/define.js +21 -0
- package/dist/core/theme/resolve.d.ts +6 -0
- package/dist/core/theme/resolve.js +18 -0
- package/dist/core/theme/theme-provider.css +13 -0
- package/dist/core/theme/types.d.ts +35 -0
- package/dist/core/theme/types.js +61 -0
- package/dist/dev/off.d.ts +4 -0
- package/dist/dev/off.js +3 -0
- package/dist/dev/on.d.ts +8 -0
- package/dist/dev/on.js +33 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +72 -0
- package/dist/primitives/anchored/anchored.d.ts +39 -0
- package/dist/primitives/anchored/anchored.js +104 -0
- package/dist/primitives/anchored/fallback.d.ts +31 -0
- package/dist/primitives/anchored/fallback.js +73 -0
- package/dist/primitives/focus/focus-trap.d.ts +25 -0
- package/dist/primitives/focus/focus-trap.js +86 -0
- package/dist/primitives/focus/roving-focus.d.ts +22 -0
- package/dist/primitives/focus/roving-focus.js +86 -0
- package/dist/primitives/focus/tabbable.d.ts +9 -0
- package/dist/primitives/focus/tabbable.js +75 -0
- package/dist/primitives/inert/inert-others.d.ts +12 -0
- package/dist/primitives/inert/inert-others.js +44 -0
- package/dist/primitives/keyboard/keymap.d.ts +17 -0
- package/dist/primitives/keyboard/keymap.js +39 -0
- package/dist/primitives/keyboard/typeahead.d.ts +18 -0
- package/dist/primitives/keyboard/typeahead.js +50 -0
- package/dist/primitives/layer/dismissable.d.ts +26 -0
- package/dist/primitives/layer/dismissable.js +84 -0
- package/dist/primitives/motion/easing.d.ts +9 -0
- package/dist/primitives/motion/easing.js +46 -0
- package/dist/primitives/motion/motion.d.ts +59 -0
- package/dist/primitives/motion/motion.js +80 -0
- package/dist/primitives/portal/Portal.svelte +37 -0
- package/dist/primitives/portal/Portal.svelte.d.ts +9 -0
- package/dist/primitives/portal/PortalContent.svelte +15 -0
- package/dist/primitives/portal/PortalContent.svelte.d.ts +9 -0
- package/dist/primitives/portal/portal.css +8 -0
- package/dist/primitives/scroll-lock/scroll-lock.d.ts +10 -0
- package/dist/primitives/scroll-lock/scroll-lock.js +33 -0
- package/dist/primitives/virtual/virtual-list.svelte.d.ts +65 -0
- package/dist/primitives/virtual/virtual-list.svelte.js +291 -0
- package/dist/styles/base.css +44 -0
- package/dist/styles/props.css +137 -0
- package/dist/styles/tokens.css +84 -0
- package/dist/styles/visually-hidden.css +16 -0
- package/dist/themes/aurora.d.ts +13 -0
- package/dist/themes/aurora.js +103 -0
- package/dist/themes/heritage.d.ts +13 -0
- package/dist/themes/heritage.js +102 -0
- package/dist/themes/maison.d.ts +13 -0
- package/dist/themes/maison.js +99 -0
- package/dist/themes/milano.d.ts +13 -0
- package/dist/themes/milano.js +101 -0
- package/dist/themes/monolith.d.ts +13 -0
- package/dist/themes/monolith.js +101 -0
- package/package.json +87 -5
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A validation rule: `true` when the value is valid, otherwise the message to show. Rules run
|
|
3
|
+
* in order after the control's HTML constraints, and the first message wins. They are
|
|
4
|
+
* synchronous: a check that needs the server belongs in the submit handler, whose answer comes
|
|
5
|
+
* back through the field's `error`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const rules = [(value: string) => value.endsWith('.com') || 'Use a .com address'];
|
|
9
|
+
*/
|
|
10
|
+
export type Rule<T> = (value: T) => true | string;
|
|
11
|
+
export declare const VALIDATE_ON: readonly ["blur", "input", "submit"];
|
|
12
|
+
/**
|
|
13
|
+
* When a field shows its message: `'blur'` once the user has changed the value and leaves the
|
|
14
|
+
* field, `'input'` from the first change, `'submit'` when the form is submitted. Once shown, the
|
|
15
|
+
* message follows every change, so fixing the value clears it at once.
|
|
16
|
+
*/
|
|
17
|
+
export type ValidateOn = (typeof VALIDATE_ON)[number];
|
|
18
|
+
/**
|
|
19
|
+
* An error from outside the field, such as the server's answer: a message, a list of messages
|
|
20
|
+
* of which the first is shown, or `true` for an invalid state described elsewhere.
|
|
21
|
+
*/
|
|
22
|
+
export type FieldError = string | readonly string[] | boolean | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VALIDATE_ON = ['blur', 'input', 'submit'];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Built-in English messages: the labels and announcements of components. */
|
|
2
|
+
export declare const MESSAGES: {
|
|
3
|
+
close: string;
|
|
4
|
+
loading: string;
|
|
5
|
+
progress: string;
|
|
6
|
+
required: string;
|
|
7
|
+
tooShort: string;
|
|
8
|
+
tooLong: string;
|
|
9
|
+
typeEmail: string;
|
|
10
|
+
typeUrl: string;
|
|
11
|
+
patternMismatch: string;
|
|
12
|
+
badInput: string;
|
|
13
|
+
rangeUnderflow: string;
|
|
14
|
+
rangeOverflow: string;
|
|
15
|
+
stepMismatch: string;
|
|
16
|
+
increase: string;
|
|
17
|
+
decrease: string;
|
|
18
|
+
};
|
|
19
|
+
export type MessageKey = keyof typeof MESSAGES;
|
|
20
|
+
export type Messages = Record<MessageKey, string>;
|
|
21
|
+
export interface LocaleOptions {
|
|
22
|
+
/** BCP 47 locale used for formatting, for example "en-GB". Defaults to "en-US". */
|
|
23
|
+
locale?: string;
|
|
24
|
+
/** ISO 4217 currency of formatCurrency(), for example "EUR". Defaults to "USD". */
|
|
25
|
+
currency?: string;
|
|
26
|
+
/** IANA time zone of formatDate(), for example "Europe/London". Defaults to the runtime's. */
|
|
27
|
+
timeZone?: string;
|
|
28
|
+
/** A translation, or single messages merged over the English ones. */
|
|
29
|
+
messages?: Partial<Messages>;
|
|
30
|
+
}
|
|
31
|
+
/** Messages and formatting, as seen by `useLocale()`. Settings are reactive. */
|
|
32
|
+
export interface LocaleInstance {
|
|
33
|
+
readonly locale: string;
|
|
34
|
+
readonly currency: string;
|
|
35
|
+
readonly timeZone: string | undefined;
|
|
36
|
+
/** The message `key`, with `{name}` placeholders replaced from `values`. */
|
|
37
|
+
t(key: MessageKey, values?: Record<string, string | number>): string;
|
|
38
|
+
formatNumber(value: number | bigint, options?: Intl.NumberFormatOptions): string;
|
|
39
|
+
/** Money in `currency` (default: the locale setting). Fraction digits follow the currency. */
|
|
40
|
+
formatCurrency(value: number | bigint, currency?: string, options?: Intl.NumberFormatOptions): string;
|
|
41
|
+
/** A ratio as a percentage: 0.0184 is "2%", or "1.84%" with minimumFractionDigits: 2. */
|
|
42
|
+
formatPercent(value: number, options?: Intl.NumberFormatOptions): string;
|
|
43
|
+
formatDate(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
|
|
44
|
+
/** Time from `now` in the largest unit that fits: "5 minutes ago", "tomorrow". */
|
|
45
|
+
formatRelative(value: Date | number, now?: Date | number): string;
|
|
46
|
+
}
|
|
47
|
+
/** Builds a locale whose settings are read through `options` on every call. */
|
|
48
|
+
export declare function createLocale(options: () => LocaleOptions): LocaleInstance;
|
|
49
|
+
/** Gives a subtree its locale, built from the root's options. */
|
|
50
|
+
export declare const setLocaleContext: (locale: LocaleInstance) => LocaleInstance;
|
|
51
|
+
/**
|
|
52
|
+
* Returns messages and formatters for the current locale. Call it during component
|
|
53
|
+
* initialisation. Without a provider it returns one shared instance with the defaults.
|
|
54
|
+
*/
|
|
55
|
+
export declare function useLocale(): LocaleInstance;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { getContext, hasContext, setContext } from 'svelte';
|
|
2
|
+
/** Built-in English messages: the labels and announcements of components. */
|
|
3
|
+
export const MESSAGES = {
|
|
4
|
+
close: 'Close',
|
|
5
|
+
loading: 'Loading',
|
|
6
|
+
progress: 'Progress',
|
|
7
|
+
// Validation: the HTML constraints of form fields.
|
|
8
|
+
required: 'Required',
|
|
9
|
+
tooShort: 'Enter at least {min} characters',
|
|
10
|
+
tooLong: 'Enter no more than {max} characters',
|
|
11
|
+
typeEmail: 'Enter a valid email address',
|
|
12
|
+
typeUrl: 'Enter a valid web address',
|
|
13
|
+
patternMismatch: 'Enter a value in the requested format',
|
|
14
|
+
badInput: 'Enter a number',
|
|
15
|
+
rangeUnderflow: 'Enter {min} or more',
|
|
16
|
+
rangeOverflow: 'Enter {max} or less',
|
|
17
|
+
stepMismatch: 'Enter a value in steps of {step}',
|
|
18
|
+
// The buttons of a number field.
|
|
19
|
+
increase: 'Increase',
|
|
20
|
+
decrease: 'Decrease'
|
|
21
|
+
};
|
|
22
|
+
// Building an Intl formatter is the expensive part, so each one is built once per locale and
|
|
23
|
+
// options. Keys hold no user data, so the cache is safe to share across server requests.
|
|
24
|
+
const cache = new Map();
|
|
25
|
+
function cached(Format, locale, options) {
|
|
26
|
+
const key = `${Format.name}|${locale}|${JSON.stringify(options)}`;
|
|
27
|
+
let formatter = cache.get(key);
|
|
28
|
+
if (!formatter)
|
|
29
|
+
cache.set(key, (formatter = new Format(locale, options)));
|
|
30
|
+
return formatter;
|
|
31
|
+
}
|
|
32
|
+
// Largest unit first, with its length in seconds.
|
|
33
|
+
const UNITS = [
|
|
34
|
+
['year', 31_536_000],
|
|
35
|
+
['month', 2_592_000],
|
|
36
|
+
['week', 604_800],
|
|
37
|
+
['day', 86_400],
|
|
38
|
+
['hour', 3_600],
|
|
39
|
+
['minute', 60],
|
|
40
|
+
['second', 1]
|
|
41
|
+
];
|
|
42
|
+
/** Builds a locale whose settings are read through `options` on every call. */
|
|
43
|
+
export function createLocale(options) {
|
|
44
|
+
const locale = () => options().locale ?? 'en-US';
|
|
45
|
+
const currency = () => options().currency ?? 'USD';
|
|
46
|
+
const timeZone = () => options().timeZone;
|
|
47
|
+
const number = (format) => cached(Intl.NumberFormat, locale(), format);
|
|
48
|
+
return {
|
|
49
|
+
get locale() {
|
|
50
|
+
return locale();
|
|
51
|
+
},
|
|
52
|
+
get currency() {
|
|
53
|
+
return currency();
|
|
54
|
+
},
|
|
55
|
+
get timeZone() {
|
|
56
|
+
return timeZone();
|
|
57
|
+
},
|
|
58
|
+
t: (key, values) => (options().messages?.[key] ?? MESSAGES[key]).replace(/\{(\w+)\}/g, (match, name) => String(values?.[name] ?? match)),
|
|
59
|
+
formatNumber: (value, format = {}) => number(format).format(value),
|
|
60
|
+
formatCurrency: (value, code = currency(), format = {}) => number({ ...format, style: 'currency', currency: code }).format(value),
|
|
61
|
+
formatPercent: (value, format = {}) => number({ ...format, style: 'percent' }).format(value),
|
|
62
|
+
formatDate: (value, format = {}) => cached(Intl.DateTimeFormat, locale(), { timeZone: timeZone(), ...format }).format(value),
|
|
63
|
+
formatRelative: (value, now = Date.now()) => {
|
|
64
|
+
const seconds = (+value - +now) / 1000;
|
|
65
|
+
const [unit, length] = UNITS.find(([, size]) => Math.abs(seconds) >= size) ?? ['second', 1];
|
|
66
|
+
return cached(Intl.RelativeTimeFormat, locale(), { numeric: 'auto' }).format(Math.round(seconds / length), unit);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const KEY = Symbol('sveltier.locale');
|
|
71
|
+
let shared;
|
|
72
|
+
/** Gives a subtree its locale, built from the root's options. */
|
|
73
|
+
export const setLocaleContext = (locale) => setContext(KEY, locale);
|
|
74
|
+
/**
|
|
75
|
+
* Returns messages and formatters for the current locale. Call it during component
|
|
76
|
+
* initialisation. Without a provider it returns one shared instance with the defaults.
|
|
77
|
+
*/
|
|
78
|
+
export function useLocale() {
|
|
79
|
+
return hasContext(KEY) ? getContext(KEY) : (shared ??= createLocale(() => ({})));
|
|
80
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type SharedProps, type Variant } from './types.js';
|
|
2
|
+
/** The shared props that map to classes. */
|
|
3
|
+
export type SharedClassProps = Pick<SharedProps, 'variant' | 'density' | 'size' | 'color' | 'rounded'>;
|
|
4
|
+
/**
|
|
5
|
+
* Classes for a component's shared props, for example
|
|
6
|
+
* `sv-variant-flat sv-density-default sv-size-md sv-color-primary sv-rounded`.
|
|
7
|
+
*
|
|
8
|
+
* The classes set custom properties (`--sv-color`, `--sv-variant-bg`, `--sv-size-height`,
|
|
9
|
+
* `--sv-density`, `--sv-rounded`, …) that the component reads. Custom properties inherit,
|
|
10
|
+
* so pass a value for every shared prop the component has, defaults included (the values
|
|
11
|
+
* `useDefaults()` resolves); an omitted one would follow the parent component.
|
|
12
|
+
*
|
|
13
|
+
* In development, a value outside its list warns with the valid ones, naming `component`.
|
|
14
|
+
* A component that takes only some variants passes them as `variants`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // In a component: resolve the defaults, then name the component for the warnings.
|
|
18
|
+
* const p = $derived(resolve({ variant, size, color }));
|
|
19
|
+
* const classes = $derived(['my-pill', sharedClasses(p, 'MyPill')]);
|
|
20
|
+
*/
|
|
21
|
+
export declare function sharedClasses({ variant, density, size, color, rounded }: SharedClassProps, component?: string, variants?: readonly Variant[]): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { checkValue, DEV } from '#dev';
|
|
2
|
+
import '../../styles/props.css';
|
|
3
|
+
import { INTENT_COLORS } from '../theme/types.js';
|
|
4
|
+
import { DENSITIES, ROUNDED, SIZES, VARIANTS } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Classes for a component's shared props, for example
|
|
7
|
+
* `sv-variant-flat sv-density-default sv-size-md sv-color-primary sv-rounded`.
|
|
8
|
+
*
|
|
9
|
+
* The classes set custom properties (`--sv-color`, `--sv-variant-bg`, `--sv-size-height`,
|
|
10
|
+
* `--sv-density`, `--sv-rounded`, …) that the component reads. Custom properties inherit,
|
|
11
|
+
* so pass a value for every shared prop the component has, defaults included (the values
|
|
12
|
+
* `useDefaults()` resolves); an omitted one would follow the parent component.
|
|
13
|
+
*
|
|
14
|
+
* In development, a value outside its list warns with the valid ones, naming `component`.
|
|
15
|
+
* A component that takes only some variants passes them as `variants`.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // In a component: resolve the defaults, then name the component for the warnings.
|
|
19
|
+
* const p = $derived(resolve({ variant, size, color }));
|
|
20
|
+
* const classes = $derived(['my-pill', sharedClasses(p, 'MyPill')]);
|
|
21
|
+
*/
|
|
22
|
+
export function sharedClasses({ variant, density, size, color, rounded }, component = 'component', variants = VARIANTS) {
|
|
23
|
+
if (DEV) {
|
|
24
|
+
// An intent given as the variant usually meant the colour.
|
|
25
|
+
const intent = INTENT_COLORS.includes(variant);
|
|
26
|
+
checkValue(component, 'variant', variant, variants, intent ? `To colour it, use color="${variant}".` : undefined);
|
|
27
|
+
checkValue(component, 'size', size, SIZES);
|
|
28
|
+
checkValue(component, 'density', density, DENSITIES);
|
|
29
|
+
checkValue(component, 'rounded', rounded, ROUNDED);
|
|
30
|
+
}
|
|
31
|
+
let classes = '';
|
|
32
|
+
if (variant)
|
|
33
|
+
classes += ` sv-variant-${variant}`;
|
|
34
|
+
if (density)
|
|
35
|
+
classes += ` sv-density-${density}`;
|
|
36
|
+
if (size)
|
|
37
|
+
classes += ` sv-size-${size}`;
|
|
38
|
+
if (color)
|
|
39
|
+
classes += ` sv-color-${color}`;
|
|
40
|
+
if (rounded !== undefined) {
|
|
41
|
+
classes += rounded === true ? ' sv-rounded' : ` sv-rounded-${rounded || 'none'}`;
|
|
42
|
+
}
|
|
43
|
+
return classes.slice(1);
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ClassValue } from 'svelte/elements';
|
|
2
|
+
/**
|
|
3
|
+
* The props of a component rendered as one root element: its own props `Own`, then every
|
|
4
|
+
* attribute of `Attributes` that `Own` does not redefine (spread onto the root), `class`
|
|
5
|
+
* (merged after the component's own classes) and a bindable `ref` to the root element.
|
|
6
|
+
*
|
|
7
|
+
* Own props win over same-named attributes, so a shared `size` or `color` never clashes
|
|
8
|
+
* with the HTML attribute of the same name.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* type PillProps = ElementProps<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>, { size?: Size }>;
|
|
12
|
+
*/
|
|
13
|
+
export type ElementProps<Element, Attributes, Own = object> = Own & Omit<Attributes, keyof Own | 'class'> & {
|
|
14
|
+
/** Classes added after the component's own: a string, an array or an object. */
|
|
15
|
+
class?: ClassValue | null;
|
|
16
|
+
/** The root element, bindable with `bind:ref`. */
|
|
17
|
+
ref?: Element | null;
|
|
18
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { IntentColor } from '../theme/types.js';
|
|
2
|
+
/** How tightly a component packs its content. `default` is the baseline. */
|
|
3
|
+
export declare const DENSITIES: readonly ["compact", "default", "comfortable"];
|
|
4
|
+
export type Density = (typeof DENSITIES)[number];
|
|
5
|
+
/** Overall scale of a component. `md` is the baseline. */
|
|
6
|
+
export declare const SIZES: readonly ["xs", "sm", "md", "lg", "xl"];
|
|
7
|
+
export type Size = (typeof SIZES)[number];
|
|
8
|
+
/**
|
|
9
|
+
* Visual weight: a raised surface, filled, tinted, outlined, text only, or no decoration at
|
|
10
|
+
* all.
|
|
11
|
+
*/
|
|
12
|
+
export declare const VARIANTS: readonly ["elevated", "flat", "tonal", "outlined", "text", "plain"];
|
|
13
|
+
export type Variant = (typeof VARIANTS)[number];
|
|
14
|
+
/**
|
|
15
|
+
* Corner rounding. `true` keeps the theme's radius, `false` / `'none'` removes it,
|
|
16
|
+
* `sm`–`xl` pick a theme radius, `'full'` makes a pill whatever the theme.
|
|
17
|
+
*/
|
|
18
|
+
export declare const ROUNDED: readonly [true, false, "none", "sm", "md", "lg", "xl", "full"];
|
|
19
|
+
export type Rounded = (typeof ROUNDED)[number];
|
|
20
|
+
/**
|
|
21
|
+
* Custom colours added with `defineTheme()`. Register them here by declaration merging to
|
|
22
|
+
* use them as a component's `color`:
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* declare module '@sveltier/ui' {
|
|
26
|
+
* interface CustomColors {
|
|
27
|
+
* brand: true;
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
*/
|
|
31
|
+
export interface CustomColors {
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A component's colour: an intent, `neutral` (the text colour, with the background as its
|
|
35
|
+
* on-colour), or a custom colour registered in `CustomColors`.
|
|
36
|
+
*/
|
|
37
|
+
export type Color = IntentColor | 'neutral' | Extract<keyof CustomColors, string> extends infer C ? C : never;
|
|
38
|
+
/** Props shared by every component that supports them. */
|
|
39
|
+
export interface SharedProps {
|
|
40
|
+
variant?: Variant;
|
|
41
|
+
density?: Density;
|
|
42
|
+
size?: Size;
|
|
43
|
+
color?: Color;
|
|
44
|
+
rounded?: Rounded;
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
loading?: boolean;
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The shared prop vocabulary. Every component that has one of these props uses exactly
|
|
2
|
+
// these names and these values.
|
|
3
|
+
/** How tightly a component packs its content. `default` is the baseline. */
|
|
4
|
+
export const DENSITIES = ['compact', 'default', 'comfortable'];
|
|
5
|
+
/** Overall scale of a component. `md` is the baseline. */
|
|
6
|
+
export const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'];
|
|
7
|
+
/**
|
|
8
|
+
* Visual weight: a raised surface, filled, tinted, outlined, text only, or no decoration at
|
|
9
|
+
* all.
|
|
10
|
+
*/
|
|
11
|
+
export const VARIANTS = ['elevated', 'flat', 'tonal', 'outlined', 'text', 'plain'];
|
|
12
|
+
/**
|
|
13
|
+
* Corner rounding. `true` keeps the theme's radius, `false` / `'none'` removes it,
|
|
14
|
+
* `sm`–`xl` pick a theme radius, `'full'` makes a pill whatever the theme.
|
|
15
|
+
*/
|
|
16
|
+
export const ROUNDED = [true, false, 'none', 'sm', 'md', 'lg', 'xl', 'full'];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { DefaultsConfig } from '../defaults/context.js';
|
|
4
|
+
import type { DisplayOptions } from '../display/display.js';
|
|
5
|
+
import type { LocaleOptions } from '../locale/locale.js';
|
|
6
|
+
import type { ThemeChoice, ThemeDefinition } from '../theme/types.js';
|
|
7
|
+
|
|
8
|
+
export interface UIProviderProps {
|
|
9
|
+
/** The registered themes, for example `monolith` or `{ ...monolith, ...maison }`. */
|
|
10
|
+
themes: Record<string, ThemeDefinition>;
|
|
11
|
+
/** "system" or a theme name. Bindable. */
|
|
12
|
+
theme?: ThemeChoice;
|
|
13
|
+
/** Theme used by "system" when the OS prefers light. Defaults to the first light theme. */
|
|
14
|
+
light?: string;
|
|
15
|
+
/** Theme used by "system" when the OS prefers dark. Defaults to the first dark theme. */
|
|
16
|
+
dark?: string;
|
|
17
|
+
/** Where the user's theme choice is remembered; `false` turns persistence off. */
|
|
18
|
+
storageKey?: string | false;
|
|
19
|
+
/** CSP nonce for the generated `<style>` and `<script>`. */
|
|
20
|
+
nonce?: string;
|
|
21
|
+
/** Default props for every component below. */
|
|
22
|
+
defaults?: DefaultsConfig;
|
|
23
|
+
/** Breakpoint thresholds, mobile breakpoint and server width. Read once. */
|
|
24
|
+
display?: DisplayOptions;
|
|
25
|
+
/** Locale, currency, time zone and messages. Changes apply at once. */
|
|
26
|
+
locale?: LocaleOptions;
|
|
27
|
+
children?: Snippet;
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<script lang="ts">
|
|
32
|
+
// The root of an app: theme, defaults, display and locale in one component.
|
|
33
|
+
import { untrack } from 'svelte';
|
|
34
|
+
import DefaultsProvider from '../defaults/DefaultsProvider.svelte';
|
|
35
|
+
import { createDisplay, setDisplayContext } from '../display/display.js';
|
|
36
|
+
import { createLocale, setLocaleContext } from '../locale/locale.js';
|
|
37
|
+
import ThemeProvider from '../theme/ThemeProvider.svelte';
|
|
38
|
+
|
|
39
|
+
let {
|
|
40
|
+
themes,
|
|
41
|
+
theme = $bindable(),
|
|
42
|
+
light,
|
|
43
|
+
dark,
|
|
44
|
+
storageKey,
|
|
45
|
+
nonce,
|
|
46
|
+
defaults,
|
|
47
|
+
display,
|
|
48
|
+
locale,
|
|
49
|
+
children
|
|
50
|
+
}: UIProviderProps = $props();
|
|
51
|
+
|
|
52
|
+
setDisplayContext(untrack(() => createDisplay(display)));
|
|
53
|
+
setLocaleContext(createLocale(() => locale ?? {}));
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<ThemeProvider {themes} bind:theme {light} {dark} {storageKey} {nonce}>
|
|
57
|
+
<DefaultsProvider defaults={defaults ?? {}}>
|
|
58
|
+
{@render children?.()}
|
|
59
|
+
</DefaultsProvider>
|
|
60
|
+
</ThemeProvider>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { DefaultsConfig } from '../defaults/context.js';
|
|
3
|
+
import type { DisplayOptions } from '../display/display.js';
|
|
4
|
+
import type { LocaleOptions } from '../locale/locale.js';
|
|
5
|
+
import type { ThemeChoice, ThemeDefinition } from '../theme/types.js';
|
|
6
|
+
export interface UIProviderProps {
|
|
7
|
+
/** The registered themes, for example `monolith` or `{ ...monolith, ...maison }`. */
|
|
8
|
+
themes: Record<string, ThemeDefinition>;
|
|
9
|
+
/** "system" or a theme name. Bindable. */
|
|
10
|
+
theme?: ThemeChoice;
|
|
11
|
+
/** Theme used by "system" when the OS prefers light. Defaults to the first light theme. */
|
|
12
|
+
light?: string;
|
|
13
|
+
/** Theme used by "system" when the OS prefers dark. Defaults to the first dark theme. */
|
|
14
|
+
dark?: string;
|
|
15
|
+
/** Where the user's theme choice is remembered; `false` turns persistence off. */
|
|
16
|
+
storageKey?: string | false;
|
|
17
|
+
/** CSP nonce for the generated `<style>` and `<script>`. */
|
|
18
|
+
nonce?: string;
|
|
19
|
+
/** Default props for every component below. */
|
|
20
|
+
defaults?: DefaultsConfig;
|
|
21
|
+
/** Breakpoint thresholds, mobile breakpoint and server width. Read once. */
|
|
22
|
+
display?: DisplayOptions;
|
|
23
|
+
/** Locale, currency, time zone and messages. Changes apply at once. */
|
|
24
|
+
locale?: LocaleOptions;
|
|
25
|
+
children?: Snippet;
|
|
26
|
+
}
|
|
27
|
+
declare const UIProvider: import("svelte").Component<UIProviderProps, {}, "theme">;
|
|
28
|
+
type UIProvider = ReturnType<typeof UIProvider>;
|
|
29
|
+
export default UIProvider;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import type { ThemeChoice, ThemeDefinition } from './types.js';
|
|
5
|
+
|
|
6
|
+
export interface ThemeProviderProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** "system" or a theme name. Nested providers inherit their parent's choice by default. */
|
|
8
|
+
theme?: ThemeChoice;
|
|
9
|
+
/** Root only, and required there. The registered themes, for example `monolith` or `{ ...monolith, ...maison }`. */
|
|
10
|
+
themes?: Record<string, ThemeDefinition>;
|
|
11
|
+
/** Root only. Theme used by "system" when the OS prefers light. Defaults to the first light theme. */
|
|
12
|
+
light?: string;
|
|
13
|
+
/** Root only. Theme used by "system" when the OS prefers dark. Defaults to the first dark theme. */
|
|
14
|
+
dark?: string;
|
|
15
|
+
/** Root only. Where the user's choice is remembered; `false` turns persistence off. */
|
|
16
|
+
storageKey?: string | false;
|
|
17
|
+
/** Root only. CSP nonce for the generated `<style>` and `<script>`. */
|
|
18
|
+
nonce?: string;
|
|
19
|
+
/** Nested only. Paint the scope's background. */
|
|
20
|
+
withBackground?: boolean;
|
|
21
|
+
/** Nested only. The wrapper element. */
|
|
22
|
+
ref?: HTMLDivElement | null;
|
|
23
|
+
children?: Snippet;
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<script lang="ts">
|
|
28
|
+
import { onMount, untrack } from 'svelte';
|
|
29
|
+
import { MediaQuery } from 'svelte/reactivity';
|
|
30
|
+
import '../../styles/tokens.css';
|
|
31
|
+
import '../../styles/base.css';
|
|
32
|
+
import './theme-provider.css';
|
|
33
|
+
import { sveltierError } from '../errors/errors.js';
|
|
34
|
+
import { hasThemeContext, setThemeContext, useTheme, type ThemeInstance } from './context.js';
|
|
35
|
+
import { THEME_ATTRIBUTE, themeCss, themeHead, themeScript } from './css.js';
|
|
36
|
+
|
|
37
|
+
const parent = hasThemeContext() ? useTheme() : undefined;
|
|
38
|
+
|
|
39
|
+
let {
|
|
40
|
+
theme = $bindable(),
|
|
41
|
+
themes,
|
|
42
|
+
light,
|
|
43
|
+
dark,
|
|
44
|
+
storageKey = 'sv-theme',
|
|
45
|
+
nonce,
|
|
46
|
+
withBackground = false,
|
|
47
|
+
ref = $bindable(null),
|
|
48
|
+
class: className,
|
|
49
|
+
children,
|
|
50
|
+
...rest
|
|
51
|
+
}: ThemeProviderProps = $props();
|
|
52
|
+
|
|
53
|
+
const registry = $derived.by(() => {
|
|
54
|
+
const registered = parent?.themes ?? themes;
|
|
55
|
+
if (!registered) {
|
|
56
|
+
throw sveltierError(
|
|
57
|
+
'SV1001',
|
|
58
|
+
"The root <ThemeProvider> has no themes. Pass one, for example themes={monolith} from '@sveltier/ui/themes/monolith'."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return registered;
|
|
62
|
+
});
|
|
63
|
+
const lightName = $derived(parent?.light ?? light ?? first(registry, false));
|
|
64
|
+
const darkName = $derived(parent?.dark ?? dark ?? first(registry, true));
|
|
65
|
+
const choice = $derived<ThemeChoice>(theme ?? parent?.choice ?? 'system');
|
|
66
|
+
|
|
67
|
+
const prefersDark = new MediaQuery('(prefers-color-scheme: dark)', false);
|
|
68
|
+
const name = $derived(
|
|
69
|
+
choice === 'system' ? (prefersDark.current ? darkName : lightName) : choice
|
|
70
|
+
);
|
|
71
|
+
const current = $derived(registry[name] ?? (registry[lightName] as ThemeDefinition));
|
|
72
|
+
const persist = $derived(!parent && storageKey !== false ? storageKey : undefined);
|
|
73
|
+
/** The first registered theme with the given darkness, or the first theme at all. */
|
|
74
|
+
function first(themes: Record<string, ThemeDefinition>, isDark: boolean): string {
|
|
75
|
+
const names = Object.keys(themes);
|
|
76
|
+
return names.find((n) => themes[n]?.dark === isDark) ?? (names[0] as string);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const isChoice = (value: unknown): value is ThemeChoice =>
|
|
80
|
+
value === 'system' || (typeof value === 'string' && Object.hasOwn(registry, value));
|
|
81
|
+
|
|
82
|
+
// The server cannot read a stored choice and the first client render must match the server
|
|
83
|
+
// HTML. The inline head script has already painted the stored theme, so <html> is only
|
|
84
|
+
// touched after mount, once the stored choice is known.
|
|
85
|
+
let mounted = $state(false);
|
|
86
|
+
const fallback = untrack(() => theme ?? 'system');
|
|
87
|
+
|
|
88
|
+
onMount(() => {
|
|
89
|
+
if (persist) {
|
|
90
|
+
try {
|
|
91
|
+
const stored = localStorage.getItem(persist);
|
|
92
|
+
if (isChoice(stored)) theme = stored;
|
|
93
|
+
} catch {
|
|
94
|
+
// Storage blocked (privacy mode, sandboxed iframe): keep the default.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
mounted = true;
|
|
98
|
+
if (!parent) return () => document.documentElement.removeAttribute(THEME_ATTRIBUTE);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
$effect(() => {
|
|
102
|
+
if (mounted && !parent) document.documentElement.setAttribute(THEME_ATTRIBUTE, choice);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const head = $derived(
|
|
106
|
+
parent
|
|
107
|
+
? ''
|
|
108
|
+
: themeHead(
|
|
109
|
+
themeCss({ themes: registry, light: lightName, dark: darkName, fallback }),
|
|
110
|
+
persist ? themeScript(persist, ['system', ...Object.keys(registry)]) : '',
|
|
111
|
+
nonce
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const instance: ThemeInstance = {
|
|
116
|
+
get choice() {
|
|
117
|
+
return choice;
|
|
118
|
+
},
|
|
119
|
+
get name() {
|
|
120
|
+
return name;
|
|
121
|
+
},
|
|
122
|
+
get current() {
|
|
123
|
+
return current;
|
|
124
|
+
},
|
|
125
|
+
get isDark() {
|
|
126
|
+
return current.dark;
|
|
127
|
+
},
|
|
128
|
+
get themes() {
|
|
129
|
+
return registry;
|
|
130
|
+
},
|
|
131
|
+
get light() {
|
|
132
|
+
return lightName;
|
|
133
|
+
},
|
|
134
|
+
get dark() {
|
|
135
|
+
return darkName;
|
|
136
|
+
},
|
|
137
|
+
set(next) {
|
|
138
|
+
if (!isChoice(next)) {
|
|
139
|
+
const names = Object.keys(registry).join(', ');
|
|
140
|
+
throw sveltierError(
|
|
141
|
+
'SV1002',
|
|
142
|
+
`Unknown theme "${next}". Registered: ${names}, or "system".`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
theme = next;
|
|
146
|
+
try {
|
|
147
|
+
if (persist) localStorage.setItem(persist, next);
|
|
148
|
+
} catch {
|
|
149
|
+
// Not remembered, but the choice still applies to this page view.
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
toggle() {
|
|
153
|
+
instance.set(current.dark ? lightName : darkName);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
setThemeContext(instance);
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
<svelte:head>
|
|
161
|
+
{#if head}
|
|
162
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- generated from validated theme values -->
|
|
163
|
+
{@html head}
|
|
164
|
+
{/if}
|
|
165
|
+
</svelte:head>
|
|
166
|
+
|
|
167
|
+
{#if parent}
|
|
168
|
+
<div
|
|
169
|
+
bind:this={ref}
|
|
170
|
+
{...rest}
|
|
171
|
+
class={['sv-theme-provider', withBackground && 'sv-theme-provider--background', className]}
|
|
172
|
+
data-sv-theme={choice}
|
|
173
|
+
>
|
|
174
|
+
{@render children?.()}
|
|
175
|
+
</div>
|
|
176
|
+
{:else}
|
|
177
|
+
{@render children?.()}
|
|
178
|
+
{/if}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import type { ThemeChoice, ThemeDefinition } from './types.js';
|
|
4
|
+
export interface ThemeProviderProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** "system" or a theme name. Nested providers inherit their parent's choice by default. */
|
|
6
|
+
theme?: ThemeChoice;
|
|
7
|
+
/** Root only, and required there. The registered themes, for example `monolith` or `{ ...monolith, ...maison }`. */
|
|
8
|
+
themes?: Record<string, ThemeDefinition>;
|
|
9
|
+
/** Root only. Theme used by "system" when the OS prefers light. Defaults to the first light theme. */
|
|
10
|
+
light?: string;
|
|
11
|
+
/** Root only. Theme used by "system" when the OS prefers dark. Defaults to the first dark theme. */
|
|
12
|
+
dark?: string;
|
|
13
|
+
/** Root only. Where the user's choice is remembered; `false` turns persistence off. */
|
|
14
|
+
storageKey?: string | false;
|
|
15
|
+
/** Root only. CSP nonce for the generated `<style>` and `<script>`. */
|
|
16
|
+
nonce?: string;
|
|
17
|
+
/** Nested only. Paint the scope's background. */
|
|
18
|
+
withBackground?: boolean;
|
|
19
|
+
/** Nested only. The wrapper element. */
|
|
20
|
+
ref?: HTMLDivElement | null;
|
|
21
|
+
children?: Snippet;
|
|
22
|
+
}
|
|
23
|
+
declare const ThemeProvider: import("svelte").Component<ThemeProviderProps, {}, "theme" | "ref">;
|
|
24
|
+
type ThemeProvider = ReturnType<typeof ThemeProvider>;
|
|
25
|
+
export default ThemeProvider;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface Oklch {
|
|
2
|
+
/** Lightness, 0–1. */
|
|
3
|
+
l: number;
|
|
4
|
+
/** Chroma, 0–0.4 in practice. */
|
|
5
|
+
c: number;
|
|
6
|
+
/** Hue in degrees. */
|
|
7
|
+
h: number;
|
|
8
|
+
/** Alpha, 0–1. */
|
|
9
|
+
alpha: number;
|
|
10
|
+
}
|
|
11
|
+
/** Parses `oklch(L C H)` or `oklch(L C H / A)` with numbers or percentages. */
|
|
12
|
+
export declare function parseOklch(value: string): Oklch | undefined;
|
|
13
|
+
/** Linear-light sRGB channels, clamped to the sRGB gamut. */
|
|
14
|
+
export declare function toLinearSrgb({ l, c, h }: Oklch): [number, number, number];
|
|
15
|
+
/** WCAG relative luminance of an opaque colour. */
|
|
16
|
+
export declare function luminance(color: Oklch): number;
|
|
17
|
+
/** WCAG 2 contrast ratio between two opaque oklch colours (1–21). */
|
|
18
|
+
export declare function contrast(a: Oklch, b: Oklch): number;
|
|
19
|
+
/**
|
|
20
|
+
* Picks a readable text colour for a filled background: a deep ink or a near-white, both
|
|
21
|
+
* tinted with the background's hue, whichever contrasts more.
|
|
22
|
+
*/
|
|
23
|
+
export declare function onColor(background: string): string | undefined;
|