entasis 0.13.0 → 0.13.2
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/dist/components/Theme/index.d.ts +1 -1
- package/dist/components/Theme/index.js +1 -1
- package/dist/components/Theme/theme.mcp.d.ts +1 -1
- package/dist/components/Theme/theme.mcp.js +4 -1
- package/dist/components/Theme/theme.props.d.ts +6 -3
- package/dist/components/Theme/theme.state.svelte.d.ts +2 -2
- package/dist/components/Theme/themeTransition.d.ts +14 -1
- package/dist/components/Theme/themeTransition.js +17 -2
- package/dist/generated/componentContract.d.ts +2 -2
- package/dist/generated/componentContract.js +11 -1
- package/dist/i18n/I18n.svelte +37 -5
- package/dist/i18n/I18n.svelte.d.ts +8 -2
- package/dist/i18n/i18n.mcp.d.ts +1 -1
- package/dist/i18n/i18n.mcp.js +1 -1
- package/dist/i18n/index.d.ts +7 -0
- package/dist/i18n/index.js +7 -0
- package/dist/i18n/localeCatalogs.d.ts +6 -0
- package/dist/i18n/localeCatalogs.js +28 -0
- package/dist/i18n/locales.d.ts +5 -1
- package/dist/i18n/locales.js +6 -2
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@ export { default as Theme } from './Theme.svelte';
|
|
|
2
2
|
export { ThemeState, useDefaultColor, useTheme } from './theme.state.svelte.js';
|
|
3
3
|
export type { ThemeProps } from './theme.props.js';
|
|
4
4
|
export { defaultThemeSpacingScale, typeScalePresets, type ThemeDesignTokenMap, type ThemeDesignTokens, type ThemeRadius, type ThemeSpacing, type ThemeSpacingScale, type ThemeSpacingStep, type TypeScaleOptions, type TypeScalePreset, type TypeScaleRatio } from './theme.designTokens.js';
|
|
5
|
-
export { themeTransitions, type ThemeTransition } from './themeTransition.js';
|
|
5
|
+
export { resolveThemeTransition, themeTransitions, type ThemeTransition, type ThemeTransitionInput, type ThemeTransitionOptions } from './themeTransition.js';
|
|
6
6
|
export { focusRing, selectedSoft, selectedSolid } from './theme.recipes.js';
|
|
7
7
|
export { themePresetNames, themePresets, type ThemePreset, type ThemePresetName, type ThemePresetTokens } from './theme.presets.js';
|
|
8
8
|
export { breakpoints, containerBreakpoints, resolveContainerBreakpoint, resolveResponsive, responsiveContainerClasses, responsiveVariables, type ContainerBreakpoint } from './responsive.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { default as Theme } from './Theme.svelte';
|
|
2
2
|
export { ThemeState, useDefaultColor, useTheme } from './theme.state.svelte.js';
|
|
3
3
|
export { defaultThemeSpacingScale, typeScalePresets } from './theme.designTokens.js';
|
|
4
|
-
export { themeTransitions } from './themeTransition.js';
|
|
4
|
+
export { resolveThemeTransition, themeTransitions } from './themeTransition.js';
|
|
5
5
|
export { focusRing, selectedSoft, selectedSolid } from './theme.recipes.js';
|
|
6
6
|
export { themePresetNames, themePresets } from './theme.presets.js';
|
|
7
7
|
export { breakpoints, containerBreakpoints, resolveContainerBreakpoint, resolveResponsive, responsiveContainerClasses, responsiveVariables } from './responsive.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const themeDescription = "\n# Theme\n\n`Theme` owns global theme selection, runtime design tokens, shared overlay state, and theme\ntransitions. Wrap the application once and use the `ThemeState` received by the children snippet.\nComponents below it read the same state with `useTheme()` from `entasis/theme`, called during\ncomponent initialisation; it throws when no `Theme` is above. For a palette computed at runtime,\nsee `generateColorPalette` in `entasis/color-palette`.\n\n## Runtime design tokens\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme, type ThemeDesignTokenMap } from 'entasis/theme';\n\n\tlet spacing = $state<'small' | 'normal' | 'large'>('normal');\n\tconst designTokens = $derived({\n\t\tlight: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'normal',\n\t\t\ttypeScale: 'default',\n\t\t\traisedWithBorder: true,\n\t\t\tdefaultColor: 'neutral'\n\t\t},\n\t\tdark: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'small',\n\t\t\ttypeScale: 'compact',\n\t\t\traisedWithBorder: false,\n\t\t\tdefaultColor: 'neutral'\n\t\t}\n\t} satisfies ThemeDesignTokenMap<readonly ['light', 'dark']>);\n</script>\n\n<Theme {designTokens} transition=\"radial-top-right\">\n\t{#snippet children(theme)}\n\t\t<button onclick={() => (spacing = spacing === 'small' ? 'large' : 'small')}>\n\t\t\tChange density\n\t\t</button>\n\t\t<button onclick={() => (theme.theme = theme.resolvedTheme === 'dark' ? 'light' : 'dark')}>\n\t\t\tToggle color scheme\n\t\t</button>\n\t{/snippet}\n</Theme>\n```\n\n`designTokens` is keyed by logical theme name and respects the `attribute` and `value` props.\nEleven presets ship as `themePresets` (`dense`, `compact`, `balanced`, `comfortable`, `spacious`,\n`sharp`, `rounded`, `display`, `editorial`, `glass`, `terminal`): `designTokens={{ light: themePresets.glass.tokens, dark: themePresets.glass.tokens }}`.\nChanging the controlled object updates already-rendered Tailwind utilities without rebuilding CSS.\n\n### ThemeDesignTokens\n\n- `spacing`: `'small' | 'normal' | 'large' | number`. Globally scales density.\n- `spacingScale`: partial overrides for the strictly increasing `xs`, `sm`, `md`, `lg`,\n and `xl` spacing multipliers. Defaults to 1/1.5/2/3/4.\n- `radius`: `'none' | 'subtile' | 'small' | 'normal' | 'large' | 'round' | number`.\n Controls take the full multiplier; surface steps (`lg` and up) stop at `large` (1.5\u00D7).\n- `typeScale`: `'compact' | 'default' | 'comfortable' | 'large' | TypeScaleOptions`.\n- `raisedWithBorder`: toggles the border used by `raised-*` utilities.\n- `defaultColor`: `Colors` role kit chrome inherits when a control omits `color`.\n Defaults to `neutral`. Set `primary` to restore an accent-colored kit. Compiles the\n current-color family (`--color`, `--color-readable`, muted/contrast/light/dark variants)\n and `--default-color` onto the theme selector. Do not set `data-color` on `html`.\n- `focusColor`, `selectedColor`, `hoverColor`, `pressedColor`: the four `Colors` **state\n roles**. They pin, for the whole theme, what a focus ring, a persistent selection and the\n transient hover/pressed layer look like, independently of the role of the control the state\n lands on. They compile `--color-focus`, `--color-selected` (plus its `-contrast`,\n `-readable` and `-muted-readable` companions), `--color-hover` and\n `--color-pressed` onto the theme selector. There is no `--color-selected-muted`: the soft\n fill is a translucent tint of `--color-selected` at `--state-selected-opacity`, so it reads\n on any surface. None is declared at `:root`: every use site\n falls back to the matching current role (`ring-focus` is\n `var(--color-focus, var(--color))`, `bg-selected-muted` tints\n `var(--color-selected, var(--color))`, the state layer is\n `var(--color-hover, currentColor)` and on `:active`\n `var(--color-pressed, var(--color-hover, currentColor))`), so leaving them unset changes\n nothing and `data-color` keeps moving the states with `--color`. Theme-level only: there is\n no per-component override. An unknown role throws.\n\nComponent-level density remains a local variant. It selects utility classes whose values inherit\nthe active global spacing token.\n\nGenerated interfaces should use the public `xs | sm | md | lg | xl` vocabulary through component\nprops and named gap/padding utilities. `micro` and `layout-*` are internal recipe tokens. Prefer\nparent-owned gaps over child margins; do not emit arbitrary spacing or unsupported radius values.\n\n## Theme selection\n\nThe selection props wrap `svelte-themes`: `themes`, `defaultTheme`, `forcedTheme`,\n`systemTheme`, `syncColorScheme`, `transitionOnChange`, `storageKey`, `attribute`,\n`value`, and `colorScheme`. The default themes are light and dark, with system selection\nenabled. `systemTheme`, `syncColorScheme`, and `transitionOnChange` all default to\n`true`; they map onto the library's `enableSystem`, `enableColorScheme`, and\n`disableTransitionOnChange` options.\n\n`ThemeState` exposes `theme`, `resolvedTheme`, `themes`, and `systemTheme`. Assign\n`theme.theme` to switch themes. The optional `transition` prop applies a named view transition;\nunsupported browsers and reduced-motion users switch instantly.\n\n`spinnerVariant` sets the global default spinner animation. The children snippet is required.\n\n## Motion tokens\n\nMotion is a token scale like spacing and radius: five duration steps and four easing roles.\n`motion` retunes them app-wide; an omitted token keeps its default.\n\n| Duration | Default | | Easing role | Default |\n| ---------- | ------- | --- | ------------ | ------------- |\n| `instant` | 0ms | | `standard` | `cubicInOut` |\n| `fast` | 100ms | | `enter` | `cubicOut` |\n| `normal` | 200ms | | `exit` | `cubicIn` |\n| `slow` | 300ms | | `emphasized` | `backOut` |\n| `slower` | 500ms | | | |\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme motion={{ duration: { normal: 150, slow: 260 }, easing: { standard: 'quintOut' } }}>\n\t{@render children()}\n</Theme>\n```\n\nThe Tailwind plugin emits the same scale as CSS variables on `html` (`--duration-normal`,\n`--ease-standard`, ...) plus the matching `duration-*` / `ease-*` utilities, so CSS transitions\nand Svelte transitions read one set of numbers. The `motion` prop rewrites those variables on\n`html` at runtime and `designTokens.motion` rewrites them again per theme, layered over the prop;\n`ThemeState.motion` resolves through the same two rungs, so the utilities and the presets never\ndisagree. `ThemeState.transition` is a deprecated alias for its `normal` duration and `standard`\neasing. Reduced motion resolves every duration to 0 and collapses the `--duration-*` variables via\nthe `data-entasis-reduce-motion` attribute on `html`.\n\nComponents keep their own transition in a reserved `motion` slot on their theme, so the `theme`\nprop covers motion as well as classes:\n\n```svelte\n<script lang=\"ts\">\n\timport { Dialog } from 'entasis/dialog';\n</script>\n\n<Dialog theme={{ motion: { duration: 'fast', easing: 'emphasized' } }} title=\"Quick\">Body</Dialog>\n```\n\n## Component theme registry\n\n`components` sets app-wide component theme defaults without a wrapper component per component:\nit is keyed by theme name (`dialog`, `button`, ...) and each entry takes the same slots as that\ncomponent's `theme` prop, the `motion` slot included. A `set<Component>Theme` call in a subtree\nbeats the registry, and an instance `theme` prop beats both \u2014 per slot: each rung layers on the one\nbelow it, so a subtree that restyles one slot keeps the registry's others.\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme\n\tcomponents={{\n\t\tdialog: { motion: { duration: 'fast' }, content: { base: 'rounded-2xl' } },\n\t\tbutton: { root: { base: 'tracking-wide' } }\n\t}}\n>\n\t{@render children()}\n</Theme>\n```\n\n## Reduced motion\n\n`reduceMotion` forces reduced motion on (`true`) or off (`false`) for every entasis animation,\noverriding the OS `prefers-reduced-motion` setting; omit it to follow the OS. The live result is\nexposed as `ThemeState.preferReducesMotion` (reactive, so it updates when the OS setting changes)\nand mirrored as a `data-entasis-reduce-motion` attribute on `html` for CSS-only animations.\n\n```svelte\n<script>\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme reduceMotion>{@render children()}</Theme>\n```\n\n## Content Security Policy\n\n`nonce` is stamped on every `<script>` and `<style>` the Theme and the components below it\ninject: the theme and viewport boot scripts, the design-token stylesheet, the `transitionOnChange={false}`\nstyle, and the CDN loaders of PhoneInput, Map and Mermaid (the document print frame and Mermaid's own\nsvg `<style>` too). Drag-and-drop base styles are rules in the Tailwind plugin CSS, so nothing is\ninjected for them. Generate one nonce per request in `hooks.server.ts`, send it through `locals` to\n`<Theme nonce>`, and send the Content-Security-Policy header yourself with that same nonce.\nSvelteKit's `kit.csp` cannot be used for this: it generates its own nonce per request, exposes it\nonly as `%sveltekit.nonce%` in `app.html`, and has no option to take yours, so the header would\ncarry one nonce while the Theme's tags carry another. Leave `kit.csp` unset and put the nonce on\nSvelteKit's own inline tags in `transformPageChunk`:\n\n```ts\n// doc-fence: skip (needs nonce declared on the app's App.Locals)\n// src/hooks.server.ts\nimport type { Handle } from '@sveltejs/kit';\n\nexport const handle: Handle = async ({ event, resolve }) => {\n\tconst nonce = crypto.randomUUID();\n\tevent.locals.nonce = nonce;\n\tconst response = await resolve(event, {\n\t\t// Adds the nonce to SvelteKit's inline <script> and <style> tags; tags that already carry one\n\t\t// (the Theme's) are left alone.\n\t\ttransformPageChunk: ({ html }) =>\n\t\t\thtml.replace(/<(script|style)(?=[\\s>])(?![^>]*\\snonce=)/g, `<$1 nonce=\"${nonce}\"`)\n\t});\n\tresponse.headers.set(\n\t\t'content-security-policy',\n\t\t`script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}'; style-src-attr 'unsafe-inline'`\n\t);\n\treturn response;\n};\n```\n\n```ts\n// doc-fence: skip (needs nonce declared on the app's App.Locals)\n// src/routes/+layout.server.ts\nimport type { LayoutServerLoad } from './$types';\n\nexport const load: LayoutServerLoad = ({ locals }) => ({ nonce: locals.nonce });\n```\n\n```svelte\n<!-- src/routes/+layout.svelte -->\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { data, children } = $props();\n</script>\n\n<Theme nonce={data.nonce}>\n\t{@render children()}\n</Theme>\n```\n\nServer-rendered `style` attributes (`style:` directives) are not covered by a nonce: allow them with\n`style-src-attr 'unsafe-inline'` (as above). `ThemeState.nonce` exposes the value to your own injected code.\n\n## Shadow DOM and portal containers\n\nDialogs, popovers, menus, tooltips and the floating-window layer mount in `document.body`. Inside a\nshadow root they mount in that root so adopted styles keep applying. `portalContainer` overrides both:\nan element or shadow root, or a function returning one for a container that is not rendered yet.\nA function that reads `$state` (as below) moves the floating-window layer in as soon as the container\nappears; a function over a plain variable is read each time a panel mounts, so the layer, which mounts\nwith the Theme, needs the container to exist by then.\nOutside presses and Escape are heard on the document a panel lives in, so a panel rendered into an\niframe's document dismisses correctly.\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n\tlet host = $state<HTMLElement>();\n</script>\n\n<div bind:this={host}></div>\n<Theme portalContainer={() => host}>\n\t{@render children()}\n</Theme>\n```\n\nWhen several Themes are mounted, the most recently mounted one that sets `portalContainer` wins.\n\n## Scroll lock and viewport variables\n\nA dialog or popover locks page scroll with a `data-entasis-scroll-lock` attribute on `<html>` and one\nrule in the Tailwind plugin CSS (`overflow: hidden`); nothing is written to inline styles, so a lock a\nthird-party library holds on the same element is neither overwritten nor released early. While\nlocked, the hidden scrollbar's room is kept with `scrollbar-gutter: stable` (the lock measures the\npage and pads the scrollbar's width instead where the gutter does not hold) unless the page already reserves a gutter, so there is no double gutter. Two variables\ndescribe the viewport for your own layout:\n\n- `--entasis-scrollbar-width`: set on `<html>` while locked, the width in px of the scrollbar the\n lock hides (`0px` with overlay scrollbars). Add it to `position: fixed` bars:\n `padding-inline-end: var(--entasis-scrollbar-width, 0px)`.\n- `--keyboard-inset`: the height in px the on-screen keyboard covers at the bottom of the layout\n viewport, measured from the visual viewport (`0px` without a keyboard, while pinch-zoomed, and in\n browsers that resize the layout viewport for the keyboard). Pin a bottom bar above the keyboard with\n `bottom: var(--keyboard-inset)`.\n- `--visual-viewport-top`: how far in px the visible area starts below the top of the layout\n viewport. iOS scrolls it down to bring a focused field above the keyboard, and fixed content then\n starts above the screen; pad it by this to keep it visible (`0px` without a keyboard and while\n pinch-zoomed). The layout viewport is the larger of `innerHeight` and the root's\n `clientHeight`, because iOS 26 shrinks `innerHeight` along with the keyboard.\n\nOn iOS, the same script adds `maximum-scale=1` to the page's `<meta name=\"viewport\">` (when it\nsets no `maximum-scale` of its own). Safari otherwise zooms the page into any text field below\n16px when it takes focus and leaves it zoomed. iOS ignores the limit for the user's own pinch zoom,\nso the page stays zoomable; other platforms would honour it, so they are left alone.\n\n## Build-time boundary\n\nThe Tailwind plugin still generates color palettes and registers utility names, variants,\nkeyframes, and spinner CSS. Spacing, radius, typography scale, raised borders,\n`defaultColor` and the four state roles (`focusColor`, `selectedColor`, `hoverColor`,\n`pressedColor`) belong to `Theme.designTokens`; colors remain CSS variables and can be\noverridden directly. `ThemeState.defaultColor` exposes the active role. Kit chrome should\nresolve omitted `color` props with `useDefaultColor`.\n";
|
|
1
|
+
export declare const themeDescription = "\n# Theme\n\n`Theme` owns global theme selection, runtime design tokens, shared overlay state, and theme\ntransitions. Wrap the application once and use the `ThemeState` received by the children snippet.\nComponents below it read the same state with `useTheme()` from `entasis/theme`, called during\ncomponent initialisation; it throws when no `Theme` is above. For a palette computed at runtime,\nsee `generateColorPalette` in `entasis/color-palette`.\n\n## Runtime design tokens\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme, type ThemeDesignTokenMap } from 'entasis/theme';\n\n\tlet spacing = $state<'small' | 'normal' | 'large'>('normal');\n\tconst designTokens = $derived({\n\t\tlight: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'normal',\n\t\t\ttypeScale: 'default',\n\t\t\traisedWithBorder: true,\n\t\t\tdefaultColor: 'neutral'\n\t\t},\n\t\tdark: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'small',\n\t\t\ttypeScale: 'compact',\n\t\t\traisedWithBorder: false,\n\t\t\tdefaultColor: 'neutral'\n\t\t}\n\t} satisfies ThemeDesignTokenMap<readonly ['light', 'dark']>);\n</script>\n\n<Theme {designTokens} transition=\"radial-top-right\">\n\t{#snippet children(theme)}\n\t\t<button onclick={() => (spacing = spacing === 'small' ? 'large' : 'small')}>\n\t\t\tChange density\n\t\t</button>\n\t\t<button onclick={() => (theme.theme = theme.resolvedTheme === 'dark' ? 'light' : 'dark')}>\n\t\t\tToggle color scheme\n\t\t</button>\n\t{/snippet}\n</Theme>\n```\n\n`designTokens` is keyed by logical theme name and respects the `attribute` and `value` props.\nEleven presets ship as `themePresets` (`dense`, `compact`, `balanced`, `comfortable`, `spacious`,\n`sharp`, `rounded`, `display`, `editorial`, `glass`, `terminal`): `designTokens={{ light: themePresets.glass.tokens, dark: themePresets.glass.tokens }}`.\nChanging the controlled object updates already-rendered Tailwind utilities without rebuilding CSS.\n\n### ThemeDesignTokens\n\n- `spacing`: `'small' | 'normal' | 'large' | number`. Globally scales density.\n- `spacingScale`: partial overrides for the strictly increasing `xs`, `sm`, `md`, `lg`,\n and `xl` spacing multipliers. Defaults to 1/1.5/2/3/4.\n- `radius`: `'none' | 'subtile' | 'small' | 'normal' | 'large' | 'round' | number`.\n Controls take the full multiplier; surface steps (`lg` and up) stop at `large` (1.5\u00D7).\n- `typeScale`: `'compact' | 'default' | 'comfortable' | 'large' | TypeScaleOptions`.\n- `raisedWithBorder`: toggles the border used by `raised-*` utilities.\n- `defaultColor`: `Colors` role kit chrome inherits when a control omits `color`.\n Defaults to `neutral`. Set `primary` to restore an accent-colored kit. Compiles the\n current-color family (`--color`, `--color-readable`, muted/contrast/light/dark variants)\n and `--default-color` onto the theme selector. Do not set `data-color` on `html`.\n- `focusColor`, `selectedColor`, `hoverColor`, `pressedColor`: the four `Colors` **state\n roles**. They pin, for the whole theme, what a focus ring, a persistent selection and the\n transient hover/pressed layer look like, independently of the role of the control the state\n lands on. They compile `--color-focus`, `--color-selected` (plus its `-contrast`,\n `-readable` and `-muted-readable` companions), `--color-hover` and\n `--color-pressed` onto the theme selector. There is no `--color-selected-muted`: the soft\n fill is a translucent tint of `--color-selected` at `--state-selected-opacity`, so it reads\n on any surface. None is declared at `:root`: every use site\n falls back to the matching current role (`ring-focus` is\n `var(--color-focus, var(--color))`, `bg-selected-muted` tints\n `var(--color-selected, var(--color))`, the state layer is\n `var(--color-hover, currentColor)` and on `:active`\n `var(--color-pressed, var(--color-hover, currentColor))`), so leaving them unset changes\n nothing and `data-color` keeps moving the states with `--color`. Theme-level only: there is\n no per-component override. An unknown role throws.\n\nComponent-level density remains a local variant. It selects utility classes whose values inherit\nthe active global spacing token.\n\nGenerated interfaces should use the public `xs | sm | md | lg | xl` vocabulary through component\nprops and named gap/padding utilities. `micro` and `layout-*` are internal recipe tokens. Prefer\nparent-owned gaps over child margins; do not emit arbitrary spacing or unsupported radius values.\n\n## Theme selection\n\nThe selection props wrap `svelte-themes`: `themes`, `defaultTheme`, `forcedTheme`,\n`systemTheme`, `syncColorScheme`, `transitionOnChange`, `storageKey`, `attribute`,\n`value`, and `colorScheme`. The default themes are light and dark, with system selection\nenabled. `systemTheme`, `syncColorScheme`, and `transitionOnChange` all default to\n`true`; they map onto the library's `enableSystem`, `enableColorScheme`, and\n`disableTransitionOnChange` options.\n\n`ThemeState` exposes `theme`, `resolvedTheme`, `themes`, and `systemTheme`. Assign\n`theme.theme` to switch themes. The optional `transition` prop applies a named view transition;\nunsupported browsers and reduced-motion users switch instantly. Pass\n`{ type, duration, easing }` instead of a name to tune its timing, e.g.\n`transition={{ type: 'radial-top-right', duration: 600 }}` (duration in ms; grid and shutter steps\nscale with it).\n\n`spinnerVariant` sets the global default spinner animation. The children snippet is required.\n\n## Motion tokens\n\nMotion is a token scale like spacing and radius: five duration steps and four easing roles.\n`motion` retunes them app-wide; an omitted token keeps its default.\n\n| Duration | Default | | Easing role | Default |\n| ---------- | ------- | --- | ------------ | ------------- |\n| `instant` | 0ms | | `standard` | `cubicInOut` |\n| `fast` | 100ms | | `enter` | `cubicOut` |\n| `normal` | 200ms | | `exit` | `cubicIn` |\n| `slow` | 300ms | | `emphasized` | `backOut` |\n| `slower` | 500ms | | | |\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme motion={{ duration: { normal: 150, slow: 260 }, easing: { standard: 'quintOut' } }}>\n\t{@render children()}\n</Theme>\n```\n\nThe Tailwind plugin emits the same scale as CSS variables on `html` (`--duration-normal`,\n`--ease-standard`, ...) plus the matching `duration-*` / `ease-*` utilities, so CSS transitions\nand Svelte transitions read one set of numbers. The `motion` prop rewrites those variables on\n`html` at runtime and `designTokens.motion` rewrites them again per theme, layered over the prop;\n`ThemeState.motion` resolves through the same two rungs, so the utilities and the presets never\ndisagree. `ThemeState.transition` is a deprecated alias for its `normal` duration and `standard`\neasing. Reduced motion resolves every duration to 0 and collapses the `--duration-*` variables via\nthe `data-entasis-reduce-motion` attribute on `html`.\n\nComponents keep their own transition in a reserved `motion` slot on their theme, so the `theme`\nprop covers motion as well as classes:\n\n```svelte\n<script lang=\"ts\">\n\timport { Dialog } from 'entasis/dialog';\n</script>\n\n<Dialog theme={{ motion: { duration: 'fast', easing: 'emphasized' } }} title=\"Quick\">Body</Dialog>\n```\n\n## Component theme registry\n\n`components` sets app-wide component theme defaults without a wrapper component per component:\nit is keyed by theme name (`dialog`, `button`, ...) and each entry takes the same slots as that\ncomponent's `theme` prop, the `motion` slot included. A `set<Component>Theme` call in a subtree\nbeats the registry, and an instance `theme` prop beats both \u2014 per slot: each rung layers on the one\nbelow it, so a subtree that restyles one slot keeps the registry's others.\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme\n\tcomponents={{\n\t\tdialog: { motion: { duration: 'fast' }, content: { base: 'rounded-2xl' } },\n\t\tbutton: { root: { base: 'tracking-wide' } }\n\t}}\n>\n\t{@render children()}\n</Theme>\n```\n\n## Reduced motion\n\n`reduceMotion` forces reduced motion on (`true`) or off (`false`) for every entasis animation,\noverriding the OS `prefers-reduced-motion` setting; omit it to follow the OS. The live result is\nexposed as `ThemeState.preferReducesMotion` (reactive, so it updates when the OS setting changes)\nand mirrored as a `data-entasis-reduce-motion` attribute on `html` for CSS-only animations.\n\n```svelte\n<script>\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme reduceMotion>{@render children()}</Theme>\n```\n\n## Content Security Policy\n\n`nonce` is stamped on every `<script>` and `<style>` the Theme and the components below it\ninject: the theme and viewport boot scripts, the design-token stylesheet, the `transitionOnChange={false}`\nstyle, and the CDN loaders of PhoneInput, Map and Mermaid (the document print frame and Mermaid's own\nsvg `<style>` too). Drag-and-drop base styles are rules in the Tailwind plugin CSS, so nothing is\ninjected for them. Generate one nonce per request in `hooks.server.ts`, send it through `locals` to\n`<Theme nonce>`, and send the Content-Security-Policy header yourself with that same nonce.\nSvelteKit's `kit.csp` cannot be used for this: it generates its own nonce per request, exposes it\nonly as `%sveltekit.nonce%` in `app.html`, and has no option to take yours, so the header would\ncarry one nonce while the Theme's tags carry another. Leave `kit.csp` unset and put the nonce on\nSvelteKit's own inline tags in `transformPageChunk`:\n\n```ts\n// doc-fence: skip (needs nonce declared on the app's App.Locals)\n// src/hooks.server.ts\nimport type { Handle } from '@sveltejs/kit';\n\nexport const handle: Handle = async ({ event, resolve }) => {\n\tconst nonce = crypto.randomUUID();\n\tevent.locals.nonce = nonce;\n\tconst response = await resolve(event, {\n\t\t// Adds the nonce to SvelteKit's inline <script> and <style> tags; tags that already carry one\n\t\t// (the Theme's) are left alone.\n\t\ttransformPageChunk: ({ html }) =>\n\t\t\thtml.replace(/<(script|style)(?=[\\s>])(?![^>]*\\snonce=)/g, `<$1 nonce=\"${nonce}\"`)\n\t});\n\tresponse.headers.set(\n\t\t'content-security-policy',\n\t\t`script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}'; style-src-attr 'unsafe-inline'`\n\t);\n\treturn response;\n};\n```\n\n```ts\n// doc-fence: skip (needs nonce declared on the app's App.Locals)\n// src/routes/+layout.server.ts\nimport type { LayoutServerLoad } from './$types';\n\nexport const load: LayoutServerLoad = ({ locals }) => ({ nonce: locals.nonce });\n```\n\n```svelte\n<!-- src/routes/+layout.svelte -->\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { data, children } = $props();\n</script>\n\n<Theme nonce={data.nonce}>\n\t{@render children()}\n</Theme>\n```\n\nServer-rendered `style` attributes (`style:` directives) are not covered by a nonce: allow them with\n`style-src-attr 'unsafe-inline'` (as above). `ThemeState.nonce` exposes the value to your own injected code.\n\n## Shadow DOM and portal containers\n\nDialogs, popovers, menus, tooltips and the floating-window layer mount in `document.body`. Inside a\nshadow root they mount in that root so adopted styles keep applying. `portalContainer` overrides both:\nan element or shadow root, or a function returning one for a container that is not rendered yet.\nA function that reads `$state` (as below) moves the floating-window layer in as soon as the container\nappears; a function over a plain variable is read each time a panel mounts, so the layer, which mounts\nwith the Theme, needs the container to exist by then.\nOutside presses and Escape are heard on the document a panel lives in, so a panel rendered into an\niframe's document dismisses correctly.\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n\tlet host = $state<HTMLElement>();\n</script>\n\n<div bind:this={host}></div>\n<Theme portalContainer={() => host}>\n\t{@render children()}\n</Theme>\n```\n\nWhen several Themes are mounted, the most recently mounted one that sets `portalContainer` wins.\n\n## Scroll lock and viewport variables\n\nA dialog or popover locks page scroll with a `data-entasis-scroll-lock` attribute on `<html>` and one\nrule in the Tailwind plugin CSS (`overflow: hidden`); nothing is written to inline styles, so a lock a\nthird-party library holds on the same element is neither overwritten nor released early. While\nlocked, the hidden scrollbar's room is kept with `scrollbar-gutter: stable` (the lock measures the\npage and pads the scrollbar's width instead where the gutter does not hold) unless the page already reserves a gutter, so there is no double gutter. Two variables\ndescribe the viewport for your own layout:\n\n- `--entasis-scrollbar-width`: set on `<html>` while locked, the width in px of the scrollbar the\n lock hides (`0px` with overlay scrollbars). Add it to `position: fixed` bars:\n `padding-inline-end: var(--entasis-scrollbar-width, 0px)`.\n- `--keyboard-inset`: the height in px the on-screen keyboard covers at the bottom of the layout\n viewport, measured from the visual viewport (`0px` without a keyboard, while pinch-zoomed, and in\n browsers that resize the layout viewport for the keyboard). Pin a bottom bar above the keyboard with\n `bottom: var(--keyboard-inset)`.\n- `--visual-viewport-top`: how far in px the visible area starts below the top of the layout\n viewport. iOS scrolls it down to bring a focused field above the keyboard, and fixed content then\n starts above the screen; pad it by this to keep it visible (`0px` without a keyboard and while\n pinch-zoomed). The layout viewport is the larger of `innerHeight` and the root's\n `clientHeight`, because iOS 26 shrinks `innerHeight` along with the keyboard.\n\nOn iOS, the same script adds `maximum-scale=1` to the page's `<meta name=\"viewport\">` (when it\nsets no `maximum-scale` of its own). Safari otherwise zooms the page into any text field below\n16px when it takes focus and leaves it zoomed. iOS ignores the limit for the user's own pinch zoom,\nso the page stays zoomable; other platforms would honour it, so they are left alone.\n\n## Build-time boundary\n\nThe Tailwind plugin still generates color palettes and registers utility names, variants,\nkeyframes, and spinner CSS. Spacing, radius, typography scale, raised borders,\n`defaultColor` and the four state roles (`focusColor`, `selectedColor`, `hoverColor`,\n`pressedColor`) belong to `Theme.designTokens`; colors remain CSS variables and can be\noverridden directly. `ThemeState.defaultColor` exposes the active role. Kit chrome should\nresolve omitted `color` props with `useDefaultColor`.\n";
|
|
@@ -98,7 +98,10 @@ enabled. \`systemTheme\`, \`syncColorScheme\`, and \`transitionOnChange\` all de
|
|
|
98
98
|
|
|
99
99
|
\`ThemeState\` exposes \`theme\`, \`resolvedTheme\`, \`themes\`, and \`systemTheme\`. Assign
|
|
100
100
|
\`theme.theme\` to switch themes. The optional \`transition\` prop applies a named view transition;
|
|
101
|
-
unsupported browsers and reduced-motion users switch instantly.
|
|
101
|
+
unsupported browsers and reduced-motion users switch instantly. Pass
|
|
102
|
+
\`{ type, duration, easing }\` instead of a name to tune its timing, e.g.
|
|
103
|
+
\`transition={{ type: 'radial-top-right', duration: 600 }}\` (duration in ms; grid and shutter steps
|
|
104
|
+
scale with it).
|
|
102
105
|
|
|
103
106
|
\`spinnerVariant\` sets the global default spinner animation. The children snippet is required.
|
|
104
107
|
|
|
@@ -2,7 +2,7 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
import type { SvelteThemeProps } from 'svelte-themes';
|
|
3
3
|
import type { SpinnerVariant } from '../Spinner/spinner.props.js';
|
|
4
4
|
import type { ThemeState } from './theme.state.svelte.js';
|
|
5
|
-
import type {
|
|
5
|
+
import type { ThemeTransitionInput } from './themeTransition.js';
|
|
6
6
|
import type { ThemeDesignTokenMap } from './theme.designTokens.js';
|
|
7
7
|
import type { DeepPartial, MotionTokens } from '../../tailwind/scales.js';
|
|
8
8
|
import type { ComponentThemeRegistry } from '../../utils/cva/theme.js';
|
|
@@ -28,8 +28,11 @@ export type ThemeProps<T extends readonly string[] = readonly string[]> = Omit<S
|
|
|
28
28
|
spinnerVariant?: SpinnerVariant;
|
|
29
29
|
/** Runtime design tokens keyed by logical theme name. */
|
|
30
30
|
designTokens?: ThemeDesignTokenMap<T>;
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* View transition used when ThemeState.theme changes: a variation name, or
|
|
33
|
+
* `{ type, duration, easing }` to tune its timing (duration in ms). Omit for an instant change.
|
|
34
|
+
*/
|
|
35
|
+
transition?: ThemeTransitionInput;
|
|
33
36
|
/**
|
|
34
37
|
* Forces reduced motion on (`true`) or off (`false`) for every entasis animation.
|
|
35
38
|
* Omit to follow the user's OS reduced-motion setting.
|
|
@@ -10,7 +10,7 @@ import { LayerStack } from '../../utils/layers.svelte.js';
|
|
|
10
10
|
import { HoverIntentGroup } from '../../utils/hoverIntent.js';
|
|
11
11
|
import type { TooltipOptions } from '../Tooltip/tooltip.props.js';
|
|
12
12
|
import type { ConfirmationHost } from '../Confirmation/confirmation.state.svelte.js';
|
|
13
|
-
import { type
|
|
13
|
+
import { type ThemeTransitionInput } from './themeTransition.js';
|
|
14
14
|
import { ThemeFloatingWindows } from './theme.floatingWindows.js';
|
|
15
15
|
import type { Colors } from '../../types/theme.js';
|
|
16
16
|
import { type PortalContainerSource } from '../../attachments/portal.js';
|
|
@@ -26,7 +26,7 @@ type EventPayload = {
|
|
|
26
26
|
type ThemeEventListener = (event: never) => void;
|
|
27
27
|
interface ThemeOptions {
|
|
28
28
|
readonly spinnerVariant: SpinnerVariant;
|
|
29
|
-
readonly themeTransition?:
|
|
29
|
+
readonly themeTransition?: ThemeTransitionInput;
|
|
30
30
|
readonly defaultColor?: Colors;
|
|
31
31
|
readonly reduceMotion?: boolean;
|
|
32
32
|
/** The partial scale passed as `<Theme motion>`; `motion` below is the resolved one. */
|
|
@@ -1,3 +1,16 @@
|
|
|
1
1
|
export declare const themeTransitions: readonly ["radial-top-left", "radial-top-right", "radial-bottom-left", "radial-bottom-right", "line-top", "line-right", "line-bottom", "line-left", "shutter-top", "shutter-right", "shutter-bottom", "shutter-left", "random-grid", "column-grid-left", "column-grid-right"];
|
|
2
2
|
export type ThemeTransition = (typeof themeTransitions)[number];
|
|
3
|
-
|
|
3
|
+
/** A theme transition with its timing tuned. Left out, each keeps the variation's own value. */
|
|
4
|
+
export type ThemeTransitionOptions = {
|
|
5
|
+
/** The variation. */
|
|
6
|
+
type: ThemeTransition;
|
|
7
|
+
/** Length of the whole reveal in ms; grid and shutter steps scale with it. */
|
|
8
|
+
duration?: number;
|
|
9
|
+
/** CSS easing of the reveal. Grid variations step from cell to cell whatever the easing. */
|
|
10
|
+
easing?: string;
|
|
11
|
+
};
|
|
12
|
+
/** A variation name, or the variation with its duration and easing. */
|
|
13
|
+
export type ThemeTransitionInput = ThemeTransition | ThemeTransitionOptions;
|
|
14
|
+
/** The options form of a transition input. */
|
|
15
|
+
export declare function resolveThemeTransition(transition: ThemeTransitionInput | undefined): ThemeTransitionOptions | undefined;
|
|
16
|
+
export declare function updateThemeWithTransition(input: ThemeTransitionInput | undefined, update: () => void | Promise<void>): void;
|
|
@@ -15,6 +15,12 @@ export const themeTransitions = [
|
|
|
15
15
|
'column-grid-left',
|
|
16
16
|
'column-grid-right'
|
|
17
17
|
];
|
|
18
|
+
/** The options form of a transition input. */
|
|
19
|
+
export function resolveThemeTransition(transition) {
|
|
20
|
+
if (!transition)
|
|
21
|
+
return undefined;
|
|
22
|
+
return typeof transition === 'string' ? { type: transition } : transition;
|
|
23
|
+
}
|
|
18
24
|
const radialOrigins = {
|
|
19
25
|
'radial-top-left': '0% 0%',
|
|
20
26
|
'radial-top-right': '100% 0%',
|
|
@@ -32,14 +38,23 @@ const SHUTTER_PHASES_PER_BAND = 3;
|
|
|
32
38
|
const SHUTTER_START_INTERVAL = 1;
|
|
33
39
|
const SHUTTER_PHASE_DURATION = 50;
|
|
34
40
|
let activeTransitionId = 0;
|
|
35
|
-
export function updateThemeWithTransition(
|
|
41
|
+
export function updateThemeWithTransition(input, update) {
|
|
42
|
+
const options = resolveThemeTransition(input);
|
|
43
|
+
const transition = options?.type;
|
|
36
44
|
if (!canAnimateThemeChange(transition)) {
|
|
37
45
|
void update();
|
|
38
46
|
return;
|
|
39
47
|
}
|
|
40
48
|
const root = document.documentElement;
|
|
41
49
|
const transitionId = ++activeTransitionId;
|
|
42
|
-
const
|
|
50
|
+
const variation = createThemeTransitionAnimation(transition);
|
|
51
|
+
const animation = {
|
|
52
|
+
...variation,
|
|
53
|
+
duration: options?.duration !== undefined && Number.isFinite(options.duration) && options.duration >= 0
|
|
54
|
+
? options.duration
|
|
55
|
+
: variation.duration,
|
|
56
|
+
easing: options?.easing ?? variation.easing
|
|
57
|
+
};
|
|
43
58
|
root.dataset.entasisThemeTransition = transition;
|
|
44
59
|
let viewTransition;
|
|
45
60
|
try {
|
|
@@ -1821,7 +1821,7 @@ export declare const componentInventory: readonly [{
|
|
|
1821
1821
|
readonly id: "theme";
|
|
1822
1822
|
readonly subpath: "entasis/theme";
|
|
1823
1823
|
readonly sourceIndex: "src/lib/components/Theme/index.ts";
|
|
1824
|
-
readonly exportedSymbols: readonly ["Breakpoint", "ContainerBreakpoint", "ResponsiveProps", "Theme", "ThemeDesignTokenMap", "ThemeDesignTokens", "ThemePreset", "ThemePresetName", "ThemePresetTokens", "ThemeProps", "ThemeRadius", "ThemeSpacing", "ThemeSpacingScale", "ThemeSpacingStep", "ThemeState", "ThemeTransition", "TypeScaleOptions", "TypeScalePreset", "TypeScaleRatio", "breakpoints", "containerBreakpoints", "defaultThemeSpacingScale", "focusRing", "resolveContainerBreakpoint", "resolveResponsive", "responsiveContainerClasses", "responsiveVariables", "selectedSoft", "selectedSolid", "themePresetNames", "themePresets", "themeTransitions", "typeScalePresets", "useDefaultColor", "useTheme"];
|
|
1824
|
+
readonly exportedSymbols: readonly ["Breakpoint", "ContainerBreakpoint", "ResponsiveProps", "Theme", "ThemeDesignTokenMap", "ThemeDesignTokens", "ThemePreset", "ThemePresetName", "ThemePresetTokens", "ThemeProps", "ThemeRadius", "ThemeSpacing", "ThemeSpacingScale", "ThemeSpacingStep", "ThemeState", "ThemeTransition", "ThemeTransitionInput", "ThemeTransitionOptions", "TypeScaleOptions", "TypeScalePreset", "TypeScaleRatio", "breakpoints", "containerBreakpoints", "defaultThemeSpacingScale", "focusRing", "resolveContainerBreakpoint", "resolveResponsive", "resolveThemeTransition", "responsiveContainerClasses", "responsiveVariables", "selectedSoft", "selectedSolid", "themePresetNames", "themePresets", "themeTransitions", "typeScalePresets", "useDefaultColor", "useTheme"];
|
|
1825
1825
|
readonly docs: readonly [];
|
|
1826
1826
|
readonly capabilities: readonly ["svelte", "component", "configuration"];
|
|
1827
1827
|
readonly relatedComponents: readonly [];
|
|
@@ -1830,7 +1830,7 @@ export declare const componentInventory: readonly [{
|
|
|
1830
1830
|
readonly id: "i18n";
|
|
1831
1831
|
readonly subpath: "entasis/i18n";
|
|
1832
1832
|
readonly sourceIndex: "src/lib/i18n/index.ts";
|
|
1833
|
-
readonly exportedSymbols: readonly ["I18n", "I18nInput", "LocaleCode", "LocaleMeta", "Messages", "en", "localeList", "locales", "setI18n", "useI18n"];
|
|
1833
|
+
readonly exportedSymbols: readonly ["I18n", "I18nInput", "LocaleCode", "LocaleMeta", "Messages", "ar", "de", "en", "es", "fr", "ja", "localeList", "locales", "pt", "setI18n", "useI18n", "zh"];
|
|
1834
1834
|
readonly docs: readonly [];
|
|
1835
1835
|
readonly capabilities: readonly ["svelte", "localization"];
|
|
1836
1836
|
readonly relatedComponents: readonly ["theme"];
|
|
@@ -4429,6 +4429,8 @@ export const componentInventory = [
|
|
|
4429
4429
|
'ThemeSpacingStep',
|
|
4430
4430
|
'ThemeState',
|
|
4431
4431
|
'ThemeTransition',
|
|
4432
|
+
'ThemeTransitionInput',
|
|
4433
|
+
'ThemeTransitionOptions',
|
|
4432
4434
|
'TypeScaleOptions',
|
|
4433
4435
|
'TypeScalePreset',
|
|
4434
4436
|
'TypeScaleRatio',
|
|
@@ -4438,6 +4440,7 @@ export const componentInventory = [
|
|
|
4438
4440
|
'focusRing',
|
|
4439
4441
|
'resolveContainerBreakpoint',
|
|
4440
4442
|
'resolveResponsive',
|
|
4443
|
+
'resolveThemeTransition',
|
|
4441
4444
|
'responsiveContainerClasses',
|
|
4442
4445
|
'responsiveVariables',
|
|
4443
4446
|
'selectedSoft',
|
|
@@ -4464,11 +4467,18 @@ export const componentInventory = [
|
|
|
4464
4467
|
'LocaleCode',
|
|
4465
4468
|
'LocaleMeta',
|
|
4466
4469
|
'Messages',
|
|
4470
|
+
'ar',
|
|
4471
|
+
'de',
|
|
4467
4472
|
'en',
|
|
4473
|
+
'es',
|
|
4474
|
+
'fr',
|
|
4475
|
+
'ja',
|
|
4468
4476
|
'localeList',
|
|
4469
4477
|
'locales',
|
|
4478
|
+
'pt',
|
|
4470
4479
|
'setI18n',
|
|
4471
|
-
'useI18n'
|
|
4480
|
+
'useI18n',
|
|
4481
|
+
'zh'
|
|
4472
4482
|
],
|
|
4473
4483
|
docs: [],
|
|
4474
4484
|
capabilities: ['svelte', 'localization'],
|
package/dist/i18n/I18n.svelte
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import { BROWSER, DEV } from 'esm-env';
|
|
3
4
|
import { setI18n, setI18nDirection } from './context.svelte.js';
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
5
|
+
import { loadLocaleMessages, localeDirection } from './localeCatalogs.js';
|
|
6
|
+
import type { LocaleCode } from './locales.js';
|
|
7
|
+
import { en, type Messages } from './en.js';
|
|
6
8
|
|
|
7
9
|
let {
|
|
8
10
|
locale = $bindable('en'),
|
|
11
|
+
catalogs,
|
|
9
12
|
messages,
|
|
10
13
|
dir,
|
|
11
14
|
manageDocument = true,
|
|
@@ -13,6 +16,12 @@
|
|
|
13
16
|
}: {
|
|
14
17
|
/** Active locale, bindable so a language picker can drive it. */
|
|
15
18
|
locale?: LocaleCode;
|
|
19
|
+
/**
|
|
20
|
+
* Catalogs available without loading, by locale: `catalogs={{ fr }}` with `fr` imported from
|
|
21
|
+
* `entasis/i18n`. A locale missing here loads its catalog on demand (English shows until it
|
|
22
|
+
* arrives, and server rendering stays English), so pass the locales an app renders first.
|
|
23
|
+
*/
|
|
24
|
+
catalogs?: Partial<Record<LocaleCode, Messages>>;
|
|
16
25
|
/** Extra per-key overrides merged on top of the locale's messages. */
|
|
17
26
|
messages?: Partial<Messages>;
|
|
18
27
|
/** Force a text direction; defaults to the active locale's direction (rtl for Arabic). */
|
|
@@ -23,9 +32,32 @@
|
|
|
23
32
|
children?: Snippet;
|
|
24
33
|
} = $props();
|
|
25
34
|
|
|
35
|
+
let loaded = $state<Partial<Record<LocaleCode, Messages>>>({});
|
|
36
|
+
// While a switched-to locale loads, keep showing the previous catalog rather than English.
|
|
37
|
+
let shown: Messages = en;
|
|
38
|
+
const catalog = $derived.by(() => {
|
|
39
|
+
const next = locale === 'en' ? en : (catalogs?.[locale] ?? loaded[locale]);
|
|
40
|
+
if (next) shown = next;
|
|
41
|
+
return next ?? shown;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (DEV && !BROWSER && locale !== 'en' && !catalogs?.[locale]) {
|
|
45
|
+
console.warn(
|
|
46
|
+
`entasis: <I18n locale="${locale}"> has no catalog for "${locale}" and renders English on the server. Pass catalogs={{ ... }} with the catalog imported from 'entasis/i18n'.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
$effect(() => {
|
|
51
|
+
const code = locale;
|
|
52
|
+
if (code === 'en' || catalogs?.[code] || loaded[code]) return;
|
|
53
|
+
loadLocaleMessages(code)?.then((catalog) => {
|
|
54
|
+
loaded[code] = catalog;
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
26
58
|
// Provide the merged catalog to all descendants. The getter keeps them reactive to `locale`.
|
|
27
|
-
setI18n(() => ({ ...
|
|
28
|
-
setI18nDirection(() => dir ??
|
|
59
|
+
setI18n(() => ({ ...catalog, ...messages }));
|
|
60
|
+
setI18nDirection(() => dir ?? localeDirection(locale));
|
|
29
61
|
|
|
30
62
|
// Client-only: mirror direction + language onto the document element.
|
|
31
63
|
// Restore the prior values on unmount / when manageDocument turns off, so a page-scoped
|
|
@@ -35,7 +67,7 @@
|
|
|
35
67
|
const root = document.documentElement;
|
|
36
68
|
const prevDir = root.dir;
|
|
37
69
|
const prevLang = root.lang;
|
|
38
|
-
root.dir = dir ??
|
|
70
|
+
root.dir = dir ?? localeDirection(locale);
|
|
39
71
|
root.lang = locale;
|
|
40
72
|
return () => {
|
|
41
73
|
root.dir = prevDir;
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import {
|
|
3
|
-
import type
|
|
2
|
+
import type { LocaleCode } from './locales.js';
|
|
3
|
+
import { type Messages } from './en.js';
|
|
4
4
|
type $$ComponentProps = {
|
|
5
5
|
/** Active locale, bindable so a language picker can drive it. */
|
|
6
6
|
locale?: LocaleCode;
|
|
7
|
+
/**
|
|
8
|
+
* Catalogs available without loading, by locale: `catalogs={{ fr }}` with `fr` imported from
|
|
9
|
+
* `entasis/i18n`. A locale missing here loads its catalog on demand (English shows until it
|
|
10
|
+
* arrives, and server rendering stays English), so pass the locales an app renders first.
|
|
11
|
+
*/
|
|
12
|
+
catalogs?: Partial<Record<LocaleCode, Messages>>;
|
|
7
13
|
/** Extra per-key overrides merged on top of the locale's messages. */
|
|
8
14
|
messages?: Partial<Messages>;
|
|
9
15
|
/** Force a text direction; defaults to the active locale's direction (rtl for Arabic). */
|
package/dist/i18n/i18n.mcp.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const i18nDescription = "\n# Internationalization\n\nImport from `entasis/i18n`.\n\nI18n provides locale messages to child components. setI18n and useI18n set and read the component context. en is the English message set; Messages and I18nInput describe translation inputs. locales and localeList expose the supported locale inventory with LocaleCode and LocaleMeta types.\n";
|
|
1
|
+
export declare const i18nDescription = "\n# Internationalization\n\nImport from `entasis/i18n`.\n\nI18n provides locale messages to child components. setI18n and useI18n set and read the component context. en is the English message set; Messages and I18nInput describe translation inputs. es, fr, de, ja, zh, pt and ar are the other catalogs, exported by name so an app bundles only the ones it imports: `<I18n locale=\"fr\" catalogs={{ fr }}>`. A locale missing from catalogs loads its catalog on demand and renders English until then, including on the server. locales and localeList expose the supported locale inventory with LocaleCode and LocaleMeta types; they import every catalog, so keep them for language pickers.\n";
|
package/dist/i18n/i18n.mcp.js
CHANGED
|
@@ -3,5 +3,5 @@ export const i18nDescription = `
|
|
|
3
3
|
|
|
4
4
|
Import from \`entasis/i18n\`.
|
|
5
5
|
|
|
6
|
-
I18n provides locale messages to child components. setI18n and useI18n set and read the component context. en is the English message set; Messages and I18nInput describe translation inputs. locales and localeList expose the supported locale inventory with LocaleCode and LocaleMeta types.
|
|
6
|
+
I18n provides locale messages to child components. setI18n and useI18n set and read the component context. en is the English message set; Messages and I18nInput describe translation inputs. es, fr, de, ja, zh, pt and ar are the other catalogs, exported by name so an app bundles only the ones it imports: \`<I18n locale="fr" catalogs={{ fr }}>\`. A locale missing from catalogs loads its catalog on demand and renders English until then, including on the server. locales and localeList expose the supported locale inventory with LocaleCode and LocaleMeta types; they import every catalog, so keep them for language pickers.
|
|
7
7
|
`;
|
package/dist/i18n/index.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export { default as I18n } from './I18n.svelte';
|
|
2
2
|
export { en, type Messages } from './en.js';
|
|
3
|
+
export { es } from './es.js';
|
|
4
|
+
export { fr } from './fr.js';
|
|
5
|
+
export { de } from './de.js';
|
|
6
|
+
export { ja } from './ja.js';
|
|
7
|
+
export { zh } from './zh.js';
|
|
8
|
+
export { pt } from './pt.js';
|
|
9
|
+
export { ar } from './ar.js';
|
|
3
10
|
export { setI18n, useI18n, type I18nInput } from './context.svelte.js';
|
|
4
11
|
export { locales, localeList, type LocaleCode, type LocaleMeta } from './locales.js';
|
package/dist/i18n/index.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export { default as I18n } from './I18n.svelte';
|
|
2
2
|
export { en } from './en.js';
|
|
3
|
+
export { es } from './es.js';
|
|
4
|
+
export { fr } from './fr.js';
|
|
5
|
+
export { de } from './de.js';
|
|
6
|
+
export { ja } from './ja.js';
|
|
7
|
+
export { zh } from './zh.js';
|
|
8
|
+
export { pt } from './pt.js';
|
|
9
|
+
export { ar } from './ar.js';
|
|
3
10
|
export { setI18n, useI18n } from './context.svelte.js';
|
|
4
11
|
export { locales, localeList } from './locales.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Messages } from './en.js';
|
|
2
|
+
import type { LocaleCode } from './locales.js';
|
|
3
|
+
/** Load a shipped locale's catalog on demand. Repeated calls share one request. */
|
|
4
|
+
export declare const loadLocaleMessages: (code: LocaleCode) => Promise<Messages> | undefined;
|
|
5
|
+
/** Text direction of a shipped locale, without importing its catalog. */
|
|
6
|
+
export declare const localeDirection: (code: LocaleCode) => "ltr" | "rtl";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One dynamic import per locale, so a bundler emits each catalog as its own chunk and an app
|
|
3
|
+
* only downloads the locales it renders. English is the built-in default and never loads.
|
|
4
|
+
*/
|
|
5
|
+
const loaders = {
|
|
6
|
+
es: () => import('./es.js').then((m) => m.es),
|
|
7
|
+
fr: () => import('./fr.js').then((m) => m.fr),
|
|
8
|
+
de: () => import('./de.js').then((m) => m.de),
|
|
9
|
+
ja: () => import('./ja.js').then((m) => m.ja),
|
|
10
|
+
'zh-CN': () => import('./zh.js').then((m) => m.zh),
|
|
11
|
+
'pt-BR': () => import('./pt.js').then((m) => m.pt),
|
|
12
|
+
ar: () => import('./ar.js').then((m) => m.ar)
|
|
13
|
+
};
|
|
14
|
+
const pending = new Map();
|
|
15
|
+
/** Load a shipped locale's catalog on demand. Repeated calls share one request. */
|
|
16
|
+
export const loadLocaleMessages = (code) => {
|
|
17
|
+
if (code === 'en')
|
|
18
|
+
return undefined;
|
|
19
|
+
let request = pending.get(code);
|
|
20
|
+
if (!request) {
|
|
21
|
+
request = loaders[code]();
|
|
22
|
+
pending.set(code, request);
|
|
23
|
+
request.catch(() => pending.delete(code));
|
|
24
|
+
}
|
|
25
|
+
return request;
|
|
26
|
+
};
|
|
27
|
+
/** Text direction of a shipped locale, without importing its catalog. */
|
|
28
|
+
export const localeDirection = (code) => (code === 'ar' ? 'rtl' : 'ltr');
|
package/dist/i18n/locales.d.ts
CHANGED
|
@@ -8,7 +8,11 @@ export type LocaleMeta = {
|
|
|
8
8
|
/** The message catalog for this locale. */
|
|
9
9
|
messages: Messages;
|
|
10
10
|
};
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Every shipped locale with its catalog, for a language picker. Importing it bundles all eight
|
|
13
|
+
* catalogs: an app with a fixed locale imports that catalog by name instead (`import { fr } from
|
|
14
|
+
* 'entasis/i18n'`) and passes it to `<I18n catalogs={{ fr }}>` or `setI18n`.
|
|
15
|
+
*/
|
|
12
16
|
export declare const locales: Record<LocaleCode, LocaleMeta>;
|
|
13
17
|
export declare const localeList: {
|
|
14
18
|
/** Native language name, for a language picker. */
|
package/dist/i18n/locales.js
CHANGED
|
@@ -6,7 +6,11 @@ import { ja } from './ja.js';
|
|
|
6
6
|
import { zh } from './zh.js';
|
|
7
7
|
import { pt } from './pt.js';
|
|
8
8
|
import { ar } from './ar.js';
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Every shipped locale with its catalog, for a language picker. Importing it bundles all eight
|
|
11
|
+
* catalogs: an app with a fixed locale imports that catalog by name instead (`import { fr } from
|
|
12
|
+
* 'entasis/i18n'`) and passes it to `<I18n catalogs={{ fr }}>` or `setI18n`.
|
|
13
|
+
*/
|
|
10
14
|
export const locales = {
|
|
11
15
|
en: { name: 'English', dir: 'ltr', messages: en },
|
|
12
16
|
es: { name: 'Español', dir: 'ltr', messages: es },
|
|
@@ -17,7 +21,7 @@ export const locales = {
|
|
|
17
21
|
'pt-BR': { name: 'Português (Brasil)', dir: 'ltr', messages: pt },
|
|
18
22
|
ar: { name: 'العربية', dir: 'rtl', messages: ar }
|
|
19
23
|
};
|
|
20
|
-
export const localeList = Object.entries(locales).map(([code, meta]) => ({
|
|
24
|
+
export const localeList = /* @__PURE__ */ Object.entries(locales).map(([code, meta]) => ({
|
|
21
25
|
code: code,
|
|
22
26
|
...meta
|
|
23
27
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "entasis",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite dev",
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"lightgallery": "^3.0.0",
|
|
151
151
|
"melt": "^0.44.0",
|
|
152
152
|
"runed": "^0.37.1",
|
|
153
|
-
"svelte-streamdown": "^
|
|
153
|
+
"svelte-streamdown": "^5.0.0",
|
|
154
154
|
"svelte-themes": "^2.0.10",
|
|
155
155
|
"valibot": "^1.5.0"
|
|
156
156
|
},
|