mui-atelier 0.1.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +32 -0
  4. package/README.md +282 -0
  5. package/dist/components/AddressFields.js +83 -0
  6. package/dist/components/AuthCard.js +94 -0
  7. package/dist/components/AuthSplash.js +88 -0
  8. package/dist/components/BrandHeader.js +81 -0
  9. package/dist/components/BrandLink.js +22 -0
  10. package/dist/components/BrandMark.js +69 -0
  11. package/dist/components/BrandProvider.js +14 -0
  12. package/dist/components/Breadcrumbs.js +62 -0
  13. package/dist/components/CardHead.js +46 -0
  14. package/dist/components/ConfirmProvider.js +77 -0
  15. package/dist/components/DashboardLayout.js +645 -0
  16. package/dist/components/DataTable.js +546 -0
  17. package/dist/components/DetailRows.js +42 -0
  18. package/dist/components/EmptyNote.js +28 -0
  19. package/dist/components/Eyebrow.js +21 -0
  20. package/dist/components/HeadingHelp.js +38 -0
  21. package/dist/components/HeadingLevel.js +18 -0
  22. package/dist/components/IconTile.js +42 -0
  23. package/dist/components/ImagePreviewDialog.js +86 -0
  24. package/dist/components/LabeledField.js +156 -0
  25. package/dist/components/LanguageSwitcher.js +43 -0
  26. package/dist/components/ListRow.js +59 -0
  27. package/dist/components/ModeToggle.js +35 -0
  28. package/dist/components/PageHeader.js +65 -0
  29. package/dist/components/PasswordField.js +43 -0
  30. package/dist/components/PublicLayout.js +27 -0
  31. package/dist/components/PublicNav.js +17 -0
  32. package/dist/components/RouteMessage.js +31 -0
  33. package/dist/components/RowList.js +10 -0
  34. package/dist/components/SafeHtml.js +57 -0
  35. package/dist/components/SaveState.js +13 -0
  36. package/dist/components/SectionPanel.js +48 -0
  37. package/dist/components/SectionTabs.js +139 -0
  38. package/dist/components/SiteNav.js +130 -0
  39. package/dist/components/Skeletons.js +63 -0
  40. package/dist/components/Sortable.js +81 -0
  41. package/dist/components/StatTile.js +66 -0
  42. package/dist/components/StatusChip.js +17 -0
  43. package/dist/components/StatusDot.js +42 -0
  44. package/dist/components/SubCard.js +30 -0
  45. package/dist/components/TabbedCard.js +20 -0
  46. package/dist/components/TableFilters.js +247 -0
  47. package/dist/components/ToastProvider.js +65 -0
  48. package/dist/eslint/chipRules.mjs +65 -0
  49. package/dist/hooks/useDocumentTitle.js +19 -0
  50. package/dist/hooks/useOverflowTabs.js +53 -0
  51. package/dist/hooks/useUnsavedChanges.js +46 -0
  52. package/dist/i18n/loadMessages.js +15 -0
  53. package/dist/i18n/mergeMessages.js +13 -0
  54. package/dist/i18n/messages/en.json +103 -0
  55. package/dist/i18n/messages/es.json +103 -0
  56. package/dist/i18n/messages/zh.json +103 -0
  57. package/dist/lib/authCard.js +4 -0
  58. package/dist/lib/chipTone.js +46 -0
  59. package/dist/lib/countryRegionData.js +29 -0
  60. package/dist/lib/countryRegionData.json +1 -0
  61. package/dist/lib/navLock.js +22 -0
  62. package/dist/lib/navTabSx.js +78 -0
  63. package/dist/lib/orgTime.js +243 -0
  64. package/dist/lib/pageHtml.js +139 -0
  65. package/dist/lib/safeNext.js +13 -0
  66. package/dist/lib/sanitizeHtml.js +36 -0
  67. package/dist/lib/slugify.js +7 -0
  68. package/dist/lib/tableFilter.js +147 -0
  69. package/dist/lib/tableStyle.js +87 -0
  70. package/dist/test/jsdomShims.js +53 -0
  71. package/dist/test/themeContract.js +178 -0
  72. package/dist/theme/OrgThemeRegistry.js +71 -0
  73. package/dist/theme/ThemeRegistry.js +17 -0
  74. package/dist/theme/berryNavRow.js +21 -0
  75. package/dist/theme/createSiteTheme.js +974 -0
  76. package/dist/theme/themes.js +438 -0
  77. package/package.json +118 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
5
+ [Semantic Versioning](https://semver.org/). While the version is 0.x, a minor
6
+ release may change the API.
7
+
8
+ ## 0.1.0 — unreleased
9
+
10
+ First release.
11
+
12
+ - 19 themes with light and dark schemes, their accent, data and status
13
+ colours tested to WCAG AA, built by `createSiteTheme` from plain definitions.
14
+ - `createThemeCatalog` for an app's own list of themes, `ThemeRegistry` for the
15
+ root theme and `OrgThemeRegistry` for a nested one with scoped CSS variables.
16
+ - Layout, data, form and feedback components, including `DataTable`,
17
+ `DashboardLayout`, `StatusChip`, `LabeledField`, `ConfirmProvider` and
18
+ `ToastProvider`.
19
+ - Strings in English, Spanish and Chinese under the `uikit` namespace.
20
+ - `chipRules` for eslint, `jsdomShims` and `describeThemeContract` for tests.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vlad Slobodkin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,32 @@
1
+ mui-atelier includes material derived from the following MIT-licensed works.
2
+
3
+ country-region-data
4
+ https://github.com/country-regions/country-region-data
5
+ Copyright (c) 2022 Benjamin Keen
6
+ Used for: src/lib/countryRegionData.json (trimmed to names and codes).
7
+
8
+ Berry, the free React admin template by CodedThemes
9
+ https://github.com/codedthemes/berry-free-react-admin-template
10
+ Copyright (c) 2026 CodedThemes
11
+ Used for: the `berry` ui treatment in src/theme (bar, drawer, card and
12
+ chip styling, and its type ramp). No Berry palette ships as a theme.
13
+
14
+ Both are distributed under the MIT License:
15
+
16
+ Permission is hereby granted, free of charge, to any person obtaining a copy
17
+ of this software and associated documentation files (the "Software"), to deal
18
+ in the Software without restriction, including without limitation the rights
19
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20
+ copies of the Software, and to permit persons to whom the Software is
21
+ furnished to do so, subject to the following conditions:
22
+
23
+ The above copyright notice and this permission notice shall be included in
24
+ all copies or substantial portions of the Software.
25
+
26
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,282 @@
1
+ # mui-atelier
2
+
3
+ Nineteen complete MUI themes, the factory that builds them, per-organisation theme scoping, and
4
+ the Next.js components built on top — for React 19, MUI 9 and the Next.js App Router.
5
+
6
+ A theme here is more than a palette. Each one carries a `ui` personality (buttons, inputs,
7
+ surfaces, chips, the dashboard's navigation, table headers), so every component in the kit, and
8
+ any component you build on MUI, takes on that theme's look. Every theme ships light and dark
9
+ schemes, and its accent, data and status colours are tested to WCAG AA (4.5:1) on its own
10
+ paper.
11
+
12
+ Plain JavaScript, published as compiled ES modules with each file's `'use client'` intact.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install mui-atelier
18
+ ```
19
+
20
+ **Required peers:** `react`, `react-dom` (19), `@mui/material`, `@mui/icons-material` (9),
21
+ `@emotion/react`, `@emotion/styled` (11), `next` (15+), `next-intl` (4). You will also want
22
+ `@mui/material-nextjs` for the App Router cache provider.
23
+
24
+ **Optional peers**, needed only by the modules that use them:
25
+
26
+ | Module | Needs |
27
+ |---|---|
28
+ | `components/DataTable`, `components/TableFilters` | `@tanstack/react-table` |
29
+ | `components/Sortable` | `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities` |
30
+ | `components/SafeHtml`, `lib/sanitizeHtml` | `dompurify` |
31
+ | `test/themeContract` | `vitest` |
32
+
33
+ ## Import by path
34
+
35
+ There is no root entry. Every module is imported by its own path, which keeps server components
36
+ from pulling in client-only code:
37
+
38
+ ```js
39
+ import StatusChip from 'mui-atelier/components/StatusChip';
40
+ import { getOrgTheme } from 'mui-atelier/theme/themes';
41
+ ```
42
+
43
+ ## Set up the root layout
44
+
45
+ ```jsx
46
+ // app/layout.js
47
+ import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';
48
+ import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter';
49
+ import { NextIntlClientProvider } from 'next-intl';
50
+ import { getLocale, getMessages } from 'next-intl/server';
51
+ import { Inter } from 'next/font/google';
52
+ import ThemeRegistry from 'mui-atelier/theme/ThemeRegistry';
53
+ import BrandProvider from 'mui-atelier/components/BrandProvider';
54
+ import { getOrgTheme } from 'mui-atelier/theme/themes';
55
+
56
+ const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-inter' });
57
+
58
+ export default async function RootLayout({ children }) {
59
+ const locale = await getLocale();
60
+ const messages = await getMessages();
61
+
62
+ return (
63
+ <html lang={locale} className={inter.variable} suppressHydrationWarning>
64
+ <body>
65
+ <InitColorSchemeScript attribute="class" defaultMode="system" />
66
+ <AppRouterCacheProvider>
67
+ <NextIntlClientProvider locale={locale} messages={messages}>
68
+ <ThemeRegistry theme={getOrgTheme('default')}>
69
+ <BrandProvider siteName="Acme" copyright="Acme Inc.">
70
+ {children}
71
+ </BrandProvider>
72
+ </ThemeRegistry>
73
+ </NextIntlClientProvider>
74
+ </AppRouterCacheProvider>
75
+ </body>
76
+ </html>
77
+ );
78
+ }
79
+ ```
80
+
81
+ `ThemeRegistry` takes a theme **definition** (plain data), not a built MUI theme, so it can be
82
+ passed from a server component. Colour schemes use MUI's CSS variables with a `class` selector,
83
+ which is why the layout renders `InitColorSchemeScript`: the right scheme is applied before
84
+ first paint.
85
+
86
+ `BrandProvider` names your product once for the wordmark (`BrandHeader`, `BrandLink`) and the
87
+ dashboard footer. Without it, `BrandHeader` shows no name and the footer shows no copyright.
88
+
89
+ ## Themes
90
+
91
+ | id | Name | Body font | Heading font |
92
+ |---|---|---|---|
93
+ | `default` | Warm & rounded | `--font-inter` | |
94
+ | `luxe` | Dark luxe | `--font-inter` | |
95
+ | `editorial` | Editorial | `--font-inter` | `--font-editorial` |
96
+ | `pro` | Data-dense pro | `--font-inter` | |
97
+ | `minimal` | Minimal | `--font-public-sans` | |
98
+ | `cobalt` | Electric cobalt | `--font-inter` | `--font-sora` |
99
+ | `noir` | Noir | `--font-inter` | `--font-grotesk` |
100
+ | `azure` | Azure | `--font-manrope` | |
101
+ | `purplehaze` | Purple haze | `--font-inter` | `--font-chakra` |
102
+ | `verdigris` | Verdigris | `--font-public-sans` | |
103
+ | `console` | Console | `--font-jetbrains` | |
104
+ | `ledger` | Ledger | `--font-spectral` | `--font-spectral` |
105
+ | `damson` | Damson | `--font-public-sans` | |
106
+ | `damsonlight` | Damson Light | `--font-public-sans` | |
107
+ | `seafoam` | Seafoam | `--font-manrope` | |
108
+ | `periwinkle` | Periwinkle | `--font-inter` | `--font-sora` |
109
+ | `verdinklesquare` | Verdinkle Square | `--font-manrope` | |
110
+ | `azurinklerounded` | Azurinkle Rounded | `--font-manrope` | |
111
+ | `butter` | Butter | `--font-manrope` | `--font-spectral` |
112
+
113
+ `mui-atelier/theme/themes` exports `ORG_THEMES` (`[{ id, label, def }]`), `getOrgTheme(id)`,
114
+ `resolveThemeId(id)`, `getChromeTheme(id)`, `appRootTheme(accent)`, `FONTS` and
115
+ `createThemeCatalog`. An unknown or empty id resolves to `default`, never to `null`, so a stored
116
+ choice that no longer exists still renders.
117
+
118
+ ### Fonts
119
+
120
+ Themes never load fonts. They name CSS variables, each with a fallback stack inside the `var()`,
121
+ so a face you have not loaded degrades to a system font rather than to Times. Load the ones your
122
+ chosen themes use with `next/font` and put the variables on `<html>`:
123
+
124
+ | Variable | Face |
125
+ |---|---|
126
+ | `--font-inter` | Inter |
127
+ | `--font-editorial` | Source Serif 4 |
128
+ | `--font-mono` | IBM Plex Mono (meta labels) |
129
+ | `--font-public-sans` | Public Sans |
130
+ | `--font-sora` | Sora |
131
+ | `--font-grotesk` | Space Grotesk |
132
+ | `--font-manrope` | Manrope |
133
+ | `--font-chakra` | Chakra Petch |
134
+ | `--font-jetbrains` | JetBrains Mono |
135
+ | `--font-spectral` | Spectral |
136
+ | `--font-roboto` | Roboto (the `berry` type ramp) |
137
+
138
+ `preload: false` on everything except your default face keeps unused themes from costing a
139
+ download.
140
+
141
+ ### A different theme for part of the page
142
+
143
+ `OrgThemeRegistry` applies another theme to a subtree inside the root one: an organisation's own
144
+ theme on its pages, say. MUI injects palette variables once at `:root`, so this component
145
+ re-scopes the nested theme's CSS variables to a class on its wrapper, and puts that class on
146
+ dialogs, menus, popovers, drawers and tooltips so portalled content keeps the palette too.
147
+
148
+ ```jsx
149
+ import OrgThemeRegistry, { useOrgThemeScope } from 'mui-atelier/theme/OrgThemeRegistry';
150
+
151
+ <OrgThemeRegistry themeId={org.theme}>{children}</OrgThemeRegistry>
152
+ ```
153
+
154
+ Anything else you portal to `document.body` can take `useOrgThemeScope()` as its class.
155
+
156
+ ### Your own themes
157
+
158
+ A theme is a plain object: `accent`, `palette.light` / `palette.dark`, `radii`, `shadow`,
159
+ `bodyFont`, optional `headingFont`, and the `ui` personality. The full shape is documented at the
160
+ top of `src/theme/themes.js`. The simplest way to write one is to start from an existing
161
+ definition:
162
+
163
+ ```js
164
+ import { createThemeCatalog, getOrgTheme, ORG_THEMES } from 'mui-atelier/theme/themes';
165
+
166
+ const harbour = {
167
+ ...getOrgTheme('azure'),
168
+ accent: { light: '#0B5CAD', dark: '#7FB6F0', contrastLight: '#FFFFFF', contrastDark: '#0A1B2E',
169
+ gradient: { light: ['#1A6FC4', '#0B5CAD'], dark: ['#7FB6F0', '#5C9BDB'] } },
170
+ };
171
+
172
+ export const catalog = createThemeCatalog({
173
+ themes: [...ORG_THEMES, { id: 'harbour', label: 'Harbour', def: harbour }],
174
+ chrome: { signin: harbour }, // named looks that are not user-selectable
175
+ chromeDefaultId: 'signin',
176
+ });
177
+ ```
178
+
179
+ A catalogue is data and functions, not a registry. Import it wherever themes are resolved, on
180
+ the server and in the browser, so both compute the same answer. A theme from your own catalogue
181
+ goes to `OrgThemeRegistry` as its definition:
182
+
183
+ ```jsx
184
+ <OrgThemeRegistry themeId={id} theme={catalog.getOrgTheme(id)}>{children}</OrgThemeRegistry>
185
+ ```
186
+
187
+ Test your themes against the same contract the built-in ones pass: every part the factory reads,
188
+ AA contrast for the accent, data and status colours, a visible focus ring, and more.
189
+
190
+ ```js
191
+ // themes.test.js (// @vitest-environment jsdom)
192
+ import { describeThemeContract } from 'mui-atelier/test/themeContract';
193
+ import { catalog } from './themes';
194
+
195
+ describeThemeContract(catalog.themes);
196
+ ```
197
+
198
+ ## Components
199
+
200
+ Layout: `DashboardLayout`, `PublicLayout`, `PageHeader`, `SectionPanel`, `SubCard`, `CardHead`,
201
+ `TabbedCard`, `SectionTabs`, `SiteNav`, `PublicNav`, `Breadcrumbs`, `Eyebrow`, `HeadingLevel`,
202
+ `HeadingHelp`, `BrandHeader`, `BrandLink`, `BrandMark`, `AuthCard`, `AuthSplash`.
203
+
204
+ Data and status: `DataTable` (sorting, a filter bar generated from the columns, selection,
205
+ pagination, a card layout on phones), `TableFilters`, `StatusChip`, `StatusDot`, `StatTile`,
206
+ `ListRow`, `RowList`, `IconTile`, `DetailRows`, `EmptyNote`, `Skeletons`.
207
+
208
+ Forms and feedback: `LabeledField` / `LabeledSelect` (label above the control), `PasswordField`,
209
+ `AddressFields` (every country and its regions), `SaveState`, `ConfirmProvider` / `useConfirm`,
210
+ `ToastProvider` / `useToast`, `RouteMessage`, `Sortable`, `ImagePreviewDialog`, `SafeHtml`,
211
+ `ModeToggle`, `LanguageSwitcher`.
212
+
213
+ Hooks: `useUnsavedChanges`, `useOverflowTabs`, `useDocumentTitle`.
214
+
215
+ ### Status colours
216
+
217
+ A chip states a **role**, and the theme supplies the colour:
218
+
219
+ ```jsx
220
+ <StatusChip role="progress" label="Published" /> // progress | done | caution | bad | idle | tag
221
+ ```
222
+
223
+ `progress` takes the theme's data hue (MUI `info`), `done` success, `caution` warning, `bad`
224
+ error, `idle` neutral, and `tag` marks a label rather than a state. The lint rule below stops a
225
+ call site hardcoding a chip colour, which would look right in one theme and wrong in the rest.
226
+
227
+ ## Strings
228
+
229
+ Components read their text through next-intl from the `uikit` namespace. English, Spanish and
230
+ Chinese ship with the package:
231
+
232
+ ```js
233
+ // i18n/request.js
234
+ import { getRequestConfig } from 'next-intl/server';
235
+ import { loadUikitMessages } from 'mui-atelier/i18n/loadMessages';
236
+
237
+ export default getRequestConfig(async () => {
238
+ const locale = 'en';
239
+ const messages = (await import(`../messages/${locale}.json`)).default;
240
+
241
+ return { locale, messages: { ...messages, uikit: await loadUikitMessages(locale) } };
242
+ });
243
+ ```
244
+
245
+ A locale the package does not ship falls back to English. To add one, lay your catalogue over
246
+ the English one with `mergeMessages` from `mui-atelier/i18n/mergeMessages`.
247
+
248
+ ## Lint
249
+
250
+ ```js
251
+ // eslint.config.mjs
252
+ import { chipRules } from 'mui-atelier/eslint';
253
+
254
+ export default defineConfig([...nextVitals, chipRules]);
255
+ ```
256
+
257
+ It rejects `<Chip color="…">` (use `StatusChip`) and a dotted palette path in a `color` prop
258
+ (`color="text.secondary"`), which MUI silently ignores (use `sx={{ color: 'text.secondary' }}`).
259
+
260
+ ## Testing with Vitest
261
+
262
+ The package is plain ES modules that import `next/link` and `next/navigation`, so let Vitest
263
+ process it rather than load it from `node_modules` as-is:
264
+
265
+ ```js
266
+ // vitest.config.js
267
+ export default defineConfig({ test: { server: { deps: { inline: ['mui-atelier'] } } } });
268
+ ```
269
+
270
+ `mui-atelier/test/jsdomShims` adds the browser APIs jsdom lacks and the components use
271
+ (`ResizeObserver`, `localStorage`, `Range` layout). Import it from your setup file.
272
+
273
+ ## Namespaces
274
+
275
+ The kit's own extensions carry an `rt` prefix: `theme.rt` (radii, surface and navigation settings
276
+ the components read), the CSS variables `--rt-grad-from`, `--rt-grad-to`, `--rt-focus`,
277
+ `--rt-mark-radius` and `--rt-mark-shadow`, the class names `rt-org-*`, `rt-tbl` and
278
+ `rt-wordmark`, and the `localStorage` key `rt:nav:collapsed`.
279
+
280
+ ## Licence
281
+
282
+ MIT. See [NOTICE](NOTICE) for the MIT-licensed works this package includes material from.
@@ -0,0 +1,83 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useTranslations } from "next-intl";
4
+ import { useEffect, useState } from "react";
5
+ import Autocomplete from "@mui/material/Autocomplete";
6
+ import Stack from "@mui/material/Stack";
7
+ import TextField from "@mui/material/TextField";
8
+ import LabeledField from "./LabeledField.js";
9
+ let dataPromise;
10
+ const loadData = () => dataPromise ??= import("../lib/countryRegionData.js");
11
+ function AddressFields({ value, onChange, disabled = false }) {
12
+ const t = useTranslations("uikit");
13
+ const [data, setData] = useState(null);
14
+ useEffect(() => {
15
+ let active = true;
16
+ loadData().then((m) => {
17
+ if (active) setData(m);
18
+ });
19
+ return () => {
20
+ active = false;
21
+ };
22
+ }, []);
23
+ const set = (patch) => onChange({ ...value, ...patch });
24
+ const text = (key, label, { sx, ...props } = {}) => /* @__PURE__ */ jsx(LabeledField, { label, sx, children: ({ id }) => /* @__PURE__ */ jsx(
25
+ TextField,
26
+ {
27
+ id,
28
+ fullWidth: true,
29
+ value: value[key] ?? "",
30
+ disabled,
31
+ onChange: (e) => set({ [key]: e.target.value }),
32
+ ...props
33
+ }
34
+ ) });
35
+ const countries = data?.COUNTRIES ?? [];
36
+ const country = data ? countries.find((c) => c.countryShortCode === value.country) ?? null : null;
37
+ const regions = data ? data.regionsForCountry(value.country) : [];
38
+ const region = regions.find((r) => data.regionCode(value.country, r) === value.region || r.name === value.region) ?? null;
39
+ return /* @__PURE__ */ jsxs(Stack, { spacing: 2, children: [
40
+ text("address_line1", t("address.line1"), { fullWidth: true }),
41
+ text("address_line2", t("address.line2"), { fullWidth: true }),
42
+ /* @__PURE__ */ jsxs(Stack, { direction: { xs: "column", sm: "row" }, spacing: 2, children: [
43
+ text("city", t("address.city"), { sx: { flex: 1 } }),
44
+ text("postal_code", t("address.postalCode"), { sx: { flex: 1 }, slotProps: { htmlInput: { maxLength: 32 } } })
45
+ ] }),
46
+ /* @__PURE__ */ jsxs(Stack, { direction: { xs: "column", sm: "row" }, spacing: 2, children: [
47
+ /* @__PURE__ */ jsx(LabeledField, { label: t("address.country"), sx: { flex: 1 }, children: ({ id }) => /* @__PURE__ */ jsx(
48
+ Autocomplete,
49
+ {
50
+ id,
51
+ fullWidth: true,
52
+ options: countries,
53
+ value: country,
54
+ loading: !data,
55
+ disabled: disabled || !data,
56
+ autoHighlight: true,
57
+ getOptionLabel: (c) => c.countryName,
58
+ isOptionEqualToValue: (a, b) => a.countryShortCode === b.countryShortCode,
59
+ onChange: (_, c) => set({ country: c?.countryShortCode ?? "", region: "" }),
60
+ renderInput: (params) => /* @__PURE__ */ jsx(TextField, { ...params })
61
+ }
62
+ ) }),
63
+ /* @__PURE__ */ jsx(LabeledField, { label: t("address.region"), sx: { flex: 1 }, children: ({ id }) => /* @__PURE__ */ jsx(
64
+ Autocomplete,
65
+ {
66
+ id,
67
+ fullWidth: true,
68
+ options: regions,
69
+ value: region,
70
+ disabled: disabled || !value.country,
71
+ autoHighlight: true,
72
+ getOptionLabel: (r) => r.name,
73
+ isOptionEqualToValue: (a, b) => a.name === b.name,
74
+ onChange: (_, r) => set({ region: r ? data.regionCode(value.country, r) : "" }),
75
+ renderInput: (params) => /* @__PURE__ */ jsx(TextField, { ...params })
76
+ }
77
+ ) })
78
+ ] })
79
+ ] });
80
+ }
81
+ export {
82
+ AddressFields as default
83
+ };
@@ -0,0 +1,94 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import Box from "@mui/material/Box";
4
+ import Card from "@mui/material/Card";
5
+ import CardContent from "@mui/material/CardContent";
6
+ import Typography from "@mui/material/Typography";
7
+ import Stack from "@mui/material/Stack";
8
+ import BrandMark from "./BrandMark.js";
9
+ import BrandHeader from "./BrandHeader.js";
10
+ import { AuthInsetContext } from "./LabeledField.js";
11
+ import { AUTH_CARD_MAX_WIDTH } from "../lib/authCard.js";
12
+ import useDocumentTitle from "../hooks/useDocumentTitle.js";
13
+ function AuthCard({ title, subtitle, children, footer, header = /* @__PURE__ */ jsx(BrandHeader, {}), splash = null }) {
14
+ useDocumentTitle(typeof title === "string" ? title : void 0);
15
+ const asideOfSplash = splash ? { display: { xs: "block", md: "none" } } : void 0;
16
+ const column = /* @__PURE__ */ jsx(Card, { sx: { width: "100%", maxWidth: AUTH_CARD_MAX_WIDTH }, children: /* @__PURE__ */ jsxs(CardContent, { sx: { p: { xs: 3, sm: 5 }, "&:last-child": { pb: { xs: 3, sm: 5 } } }, children: [
17
+ /* @__PURE__ */ jsxs(Stack, { spacing: 2, sx: { alignItems: "center", mb: 3 }, children: [
18
+ header || splash ? null : /* @__PURE__ */ jsx(BrandMark, { size: 44, sx: asideOfSplash }),
19
+ /* @__PURE__ */ jsx(
20
+ Typography,
21
+ {
22
+ variant: "h3",
23
+ component: "h1",
24
+ align: "center",
25
+ sx: (theme) => ({
26
+ color: theme.rt?.ui?.authTitle === "ink" ? theme.vars.palette.text.primary : theme.vars.palette.primary.main,
27
+ fontWeight: 700
28
+ }),
29
+ children: title
30
+ }
31
+ ),
32
+ subtitle ? (
33
+ // 16px, not the ramp's body1: berry's body1 is 14px (Roboto density
34
+ // for dense app screens) and this is a sparse one. A no-op for every
35
+ // other theme, whose body1 is already 16.
36
+ /* @__PURE__ */ jsx(Typography, { variant: "body1", align: "center", sx: { color: "text.secondary", fontSize: 16, ...asideOfSplash }, children: subtitle })
37
+ ) : null
38
+ ] }),
39
+ children,
40
+ footer ? /* @__PURE__ */ jsx(Box, { sx: { mt: 3, pt: 2, borderTop: "1px solid", borderColor: "divider", textAlign: "center" }, children: footer }) : null
41
+ ] }) });
42
+ const page = splash ? /* @__PURE__ */ jsxs(
43
+ Box,
44
+ {
45
+ sx: {
46
+ minHeight: "100dvh",
47
+ // No accent wash here, unlike the single-column page below: this ground
48
+ // is a 40%-wide strip beside a full-bleed brand panel, and a glow
49
+ // centred on the whole grid would be behind the panel, not the form.
50
+ bgcolor: "background.default",
51
+ display: "grid",
52
+ // One column below md, and the splash simply isn't rendered there.
53
+ // Not 50/50: the form is a fixed-width column, so an even split
54
+ // leaves it adrift in the middle of its own half on a wide screen.
55
+ gridTemplateColumns: { xs: "1fr", md: "1.05fr 1fr", lg: "1.15fr 1fr" }
56
+ },
57
+ children: [
58
+ /* @__PURE__ */ jsx(Box, { sx: { display: { xs: "none", md: "flex" }, flexDirection: "column", "& > *": { flex: 1, minHeight: 0 } }, children: splash }),
59
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", flexDirection: "column", minWidth: 0 }, children: [
60
+ /* @__PURE__ */ jsx(Box, { sx: { display: { md: "none" } }, children: header }),
61
+ /* @__PURE__ */ jsx(Box, { sx: { flex: 1, display: "flex", alignItems: "center", justifyContent: "center", py: 6, px: { xs: 2, md: 3 } }, children: column })
62
+ ] })
63
+ ]
64
+ }
65
+ ) : /* @__PURE__ */ jsxs(
66
+ Box,
67
+ {
68
+ sx: {
69
+ minHeight: "100dvh",
70
+ display: "flex",
71
+ flexDirection: "column",
72
+ bgcolor: "background.default",
73
+ /*
74
+ * The ground, on EVERY theme. A 60rem circle of the
75
+ * theme's own accent at 10%, centred a fifth of a page above the top
76
+ * edge, so only its lower arc is on screen: the page warms towards the
77
+ * bar and is flat by the middle. It reads as depth on a saturated
78
+ * accent and as almost nothing on a muted one, which is the theme
79
+ * speaking, not a branch — no theme opts out of it, so there is no
80
+ * per-palette fork on this screen.
81
+ */
82
+ backgroundImage: (theme) => `radial-gradient(60rem 60rem at 50% -20%, rgba(${theme.vars.palette.primary.mainChannel} / 0.10), transparent 60%)`
83
+ },
84
+ children: [
85
+ header,
86
+ /* @__PURE__ */ jsx(Box, { sx: { flex: 1, display: "flex", alignItems: "center", justifyContent: "center", py: 6, px: 2 }, children: column })
87
+ ]
88
+ }
89
+ );
90
+ return /* @__PURE__ */ jsx(AuthInsetContext.Provider, { value: true, children: page });
91
+ }
92
+ export {
93
+ AuthCard as default
94
+ };
@@ -0,0 +1,88 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import Box from "@mui/material/Box";
4
+ import Stack from "@mui/material/Stack";
5
+ import Typography from "@mui/material/Typography";
6
+ import BrandMark from "./BrandMark.js";
7
+ function AuthSplash({ title, tagline, logoUrl = null }) {
8
+ return /* @__PURE__ */ jsxs(
9
+ Box,
10
+ {
11
+ sx: {
12
+ position: "relative",
13
+ overflow: "hidden",
14
+ display: "flex",
15
+ alignItems: "center",
16
+ px: { md: 6, lg: 9 },
17
+ py: 8,
18
+ color: "primary.contrastText",
19
+ // The ground. `primary.dark → primary.main → data` runs the theme's own
20
+ // accent into its data hue, which is the one other colour every theme
21
+ // guarantees and guarantees to be distinct from the accent.
22
+ backgroundImage: (t) => `linear-gradient(145deg,
23
+ rgb(${t.vars.palette.primary.darkChannel}) 0%,
24
+ rgb(${t.vars.palette.primary.mainChannel}) 52%,
25
+ rgb(${t.vars.palette.info.mainChannel}) 100%)`
26
+ },
27
+ children: [
28
+ /* @__PURE__ */ jsx(
29
+ Box,
30
+ {
31
+ "aria-hidden": true,
32
+ sx: {
33
+ position: "absolute",
34
+ inset: 0,
35
+ backgroundImage: (t) => `linear-gradient(100deg,
36
+ rgb(${t.vars.palette.primary.mainChannel}) 0%,
37
+ rgba(${t.vars.palette.primary.mainChannel} / 0.72) 48%,
38
+ rgba(${t.vars.palette.primary.mainChannel} / 0) 82%)`,
39
+ pointerEvents: "none"
40
+ }
41
+ }
42
+ ),
43
+ /* @__PURE__ */ jsxs(
44
+ Box,
45
+ {
46
+ component: "svg",
47
+ "aria-hidden": true,
48
+ viewBox: "0 0 600 600",
49
+ sx: {
50
+ position: "absolute",
51
+ inset: "-10% -20%",
52
+ width: "140%",
53
+ height: "120%",
54
+ opacity: 0.5,
55
+ pointerEvents: "none"
56
+ },
57
+ children: [
58
+ /* @__PURE__ */ jsx("g", { fill: "none", stroke: "currentColor", strokeWidth: "1.25", children: [90, 150, 210, 270, 330, 390].map((r) => /* @__PURE__ */ jsx("circle", { cx: "70", cy: "520", r, opacity: 0.5 - r / 1200 }, r)) }),
59
+ /* @__PURE__ */ jsx("g", { fill: "currentColor", opacity: "0.35", children: Array.from({ length: 7 }, (_, row) => Array.from({ length: 7 }, (_2, col) => /* @__PURE__ */ jsx("circle", { cx: 390 + col * 30, cy: 60 + row * 30, r: "2.5" }, `${row}-${col}`))) })
60
+ ]
61
+ }
62
+ ),
63
+ /* @__PURE__ */ jsxs(Stack, { spacing: 3, sx: { position: "relative", maxWidth: 460 }, children: [
64
+ /* @__PURE__ */ jsx(
65
+ Box,
66
+ {
67
+ sx: {
68
+ alignSelf: "flex-start",
69
+ bgcolor: "common.white",
70
+ borderRadius: 2,
71
+ px: 1.5,
72
+ py: 1.25,
73
+ display: "inline-flex",
74
+ boxShadow: "0 2px 10px -4px rgba(16,24,40,0.35)"
75
+ },
76
+ children: /* @__PURE__ */ jsx(BrandMark, { size: 40, logoUrl, alt: "" })
77
+ }
78
+ ),
79
+ title ? /* @__PURE__ */ jsx(Typography, { variant: "h3", component: "p", sx: { fontWeight: 700, lineHeight: 1.15 }, children: title }) : null,
80
+ tagline ? /* @__PURE__ */ jsx(Typography, { sx: { fontSize: "1.0625rem", lineHeight: 1.6, opacity: 0.88 }, children: tagline }) : null
81
+ ] })
82
+ ]
83
+ }
84
+ );
85
+ }
86
+ export {
87
+ AuthSplash as default
88
+ };