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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/NOTICE +32 -0
- package/README.md +282 -0
- package/dist/components/AddressFields.js +83 -0
- package/dist/components/AuthCard.js +94 -0
- package/dist/components/AuthSplash.js +88 -0
- package/dist/components/BrandHeader.js +81 -0
- package/dist/components/BrandLink.js +22 -0
- package/dist/components/BrandMark.js +69 -0
- package/dist/components/BrandProvider.js +14 -0
- package/dist/components/Breadcrumbs.js +62 -0
- package/dist/components/CardHead.js +46 -0
- package/dist/components/ConfirmProvider.js +77 -0
- package/dist/components/DashboardLayout.js +645 -0
- package/dist/components/DataTable.js +546 -0
- package/dist/components/DetailRows.js +42 -0
- package/dist/components/EmptyNote.js +28 -0
- package/dist/components/Eyebrow.js +21 -0
- package/dist/components/HeadingHelp.js +38 -0
- package/dist/components/HeadingLevel.js +18 -0
- package/dist/components/IconTile.js +42 -0
- package/dist/components/ImagePreviewDialog.js +86 -0
- package/dist/components/LabeledField.js +156 -0
- package/dist/components/LanguageSwitcher.js +43 -0
- package/dist/components/ListRow.js +59 -0
- package/dist/components/ModeToggle.js +35 -0
- package/dist/components/PageHeader.js +65 -0
- package/dist/components/PasswordField.js +43 -0
- package/dist/components/PublicLayout.js +27 -0
- package/dist/components/PublicNav.js +17 -0
- package/dist/components/RouteMessage.js +31 -0
- package/dist/components/RowList.js +10 -0
- package/dist/components/SafeHtml.js +57 -0
- package/dist/components/SaveState.js +13 -0
- package/dist/components/SectionPanel.js +48 -0
- package/dist/components/SectionTabs.js +139 -0
- package/dist/components/SiteNav.js +130 -0
- package/dist/components/Skeletons.js +63 -0
- package/dist/components/Sortable.js +81 -0
- package/dist/components/StatTile.js +66 -0
- package/dist/components/StatusChip.js +17 -0
- package/dist/components/StatusDot.js +42 -0
- package/dist/components/SubCard.js +30 -0
- package/dist/components/TabbedCard.js +20 -0
- package/dist/components/TableFilters.js +247 -0
- package/dist/components/ToastProvider.js +65 -0
- package/dist/eslint/chipRules.mjs +65 -0
- package/dist/hooks/useDocumentTitle.js +19 -0
- package/dist/hooks/useOverflowTabs.js +53 -0
- package/dist/hooks/useUnsavedChanges.js +46 -0
- package/dist/i18n/loadMessages.js +15 -0
- package/dist/i18n/mergeMessages.js +13 -0
- package/dist/i18n/messages/en.json +103 -0
- package/dist/i18n/messages/es.json +103 -0
- package/dist/i18n/messages/zh.json +103 -0
- package/dist/lib/authCard.js +4 -0
- package/dist/lib/chipTone.js +46 -0
- package/dist/lib/countryRegionData.js +29 -0
- package/dist/lib/countryRegionData.json +1 -0
- package/dist/lib/navLock.js +22 -0
- package/dist/lib/navTabSx.js +78 -0
- package/dist/lib/orgTime.js +243 -0
- package/dist/lib/pageHtml.js +139 -0
- package/dist/lib/safeNext.js +13 -0
- package/dist/lib/sanitizeHtml.js +36 -0
- package/dist/lib/slugify.js +7 -0
- package/dist/lib/tableFilter.js +147 -0
- package/dist/lib/tableStyle.js +87 -0
- package/dist/test/jsdomShims.js +53 -0
- package/dist/test/themeContract.js +178 -0
- package/dist/theme/OrgThemeRegistry.js +71 -0
- package/dist/theme/ThemeRegistry.js +17 -0
- package/dist/theme/berryNavRow.js +21 -0
- package/dist/theme/createSiteTheme.js +974 -0
- package/dist/theme/themes.js +438 -0
- 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
|
+
};
|