@milktop/react-ui 0.1.2 → 0.2.1
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/README.md +57 -6
- package/dist/accordion/Accordion.d.ts +1 -1
- package/dist/accordion/Accordion.js +1 -1
- package/dist/action-bar/ActionBar.d.ts +1 -1
- package/dist/action-bar/ActionBar.js +1 -1
- package/dist/alert/Alert.d.ts +1 -1
- package/dist/alert/Alert.js +1 -1
- package/dist/app-shell/AppShell.d.ts +9 -1
- package/dist/app-shell/AppShell.js +46 -5
- package/dist/app-shell/Topbar.d.ts +1 -1
- package/dist/app-shell/Topbar.js +1 -1
- package/dist/app-shell/app-shell.css +5 -0
- package/dist/app-shell/shell.d.ts +2 -0
- package/dist/appearance-panel/Appearance.d.ts +39 -0
- package/dist/appearance-panel/Appearance.js +74 -0
- package/dist/appearance-panel/appearance.css +17 -0
- package/dist/appearance.d.ts +42 -0
- package/dist/appearance.js +157 -0
- package/dist/attachments/Attachments.d.ts +1 -1
- package/dist/attachments/Attachments.js +1 -1
- package/dist/avatar/Avatar.d.ts +1 -1
- package/dist/avatar/Avatar.js +1 -1
- package/dist/badge/Badge.d.ts +1 -1
- package/dist/badge/Badge.js +1 -1
- package/dist/banner/Banner.d.ts +1 -1
- package/dist/banner/Banner.js +1 -1
- package/dist/breadcrumbs/Breadcrumbs.d.ts +1 -1
- package/dist/breadcrumbs/Breadcrumbs.js +1 -1
- package/dist/button/Button.d.ts +3 -2
- package/dist/button/Button.js +1 -1
- package/dist/card/Card.d.ts +2 -2
- package/dist/card/Card.js +1 -1
- package/dist/checkbox/Checkbox.d.ts +1 -1
- package/dist/checkbox/Checkbox.js +1 -1
- package/dist/checkbox/CheckboxGroup.d.ts +1 -1
- package/dist/checkbox/CheckboxGroup.js +1 -1
- package/dist/checkbox/checkbox.css +1 -1
- package/dist/collapsible/Collapsible.d.ts +1 -1
- package/dist/collapsible/Collapsible.js +1 -1
- package/dist/command/Command.d.ts +1 -1
- package/dist/command/Command.js +1 -1
- package/dist/copy-button/CopyButton.d.ts +1 -1
- package/dist/copy-button/CopyButton.js +1 -1
- package/dist/data-list/DataList.d.ts +1 -1
- package/dist/data-list/DataList.js +1 -1
- package/dist/date-picker/DatePicker.d.ts +1 -1
- package/dist/date-picker/DatePicker.js +1 -1
- package/dist/date-picker/date-picker.css +6 -3
- package/dist/dialog/Dialog.d.ts +1 -1
- package/dist/dialog/Dialog.js +1 -1
- package/dist/editable/Editable.d.ts +1 -1
- package/dist/editable/Editable.js +1 -1
- package/dist/editor/Editor.d.ts +1 -1
- package/dist/editor/Editor.js +1 -1
- package/dist/editor/editor.css +2 -1
- package/dist/empty-state/EmptyState.d.ts +1 -1
- package/dist/empty-state/EmptyState.js +1 -1
- package/dist/event-calendar/EventCalendar.d.ts +12 -5
- package/dist/event-calendar/EventCalendar.js +155 -41
- package/dist/event-calendar/event-calendar.css +10 -5
- package/dist/file-upload/FileUpload.d.ts +1 -1
- package/dist/file-upload/FileUpload.js +1 -1
- package/dist/format/messages.d.ts +12 -0
- package/dist/format/messages.js +12 -0
- package/dist/hover-card/hover-card.css +3 -1
- package/dist/icons.d.ts +1 -0
- package/dist/icons.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/input/Input.d.ts +1 -1
- package/dist/input/Input.js +1 -1
- package/dist/input/input.css +7 -1
- package/dist/menu/Menu.d.ts +1 -1
- package/dist/menu/Menu.js +1 -1
- package/dist/menu/menu.css +3 -1
- package/dist/native-select/NativeSelect.d.ts +1 -1
- package/dist/native-select/NativeSelect.js +1 -1
- package/dist/native-select/native-select.css +2 -1
- package/dist/number-input/NumberInput.d.ts +1 -1
- package/dist/number-input/NumberInput.js +1 -1
- package/dist/number-input/number-input.css +6 -3
- package/dist/page/Page.d.ts +4 -0
- package/dist/page/Page.js +4 -2
- package/dist/pagination/Pagination.d.ts +1 -1
- package/dist/pagination/Pagination.js +1 -1
- package/dist/password-input/PasswordInput.d.ts +1 -1
- package/dist/password-input/PasswordInput.js +1 -1
- package/dist/password-input/password-input.css +6 -3
- package/dist/pin-input/pin-input.css +2 -1
- package/dist/popover/Popover.d.ts +1 -1
- package/dist/popover/Popover.js +1 -1
- package/dist/popover/popover.css +3 -1
- package/dist/radio-group/RadioGroup.d.ts +1 -1
- package/dist/radio-group/RadioGroup.js +1 -1
- package/dist/radio-group/radio-group.css +1 -1
- package/dist/segment-group/Segmented.d.ts +1 -1
- package/dist/segment-group/Segmented.js +1 -1
- package/dist/select/ListSelect.d.ts +1 -1
- package/dist/select/ListSelect.js +1 -1
- package/dist/select/SearchSelect.d.ts +1 -1
- package/dist/select/SearchSelect.js +1 -1
- package/dist/select/search-select.css +6 -3
- package/dist/select/select.css +5 -2
- package/dist/sheet/Sheet.d.ts +1 -1
- package/dist/sheet/Sheet.js +1 -1
- package/dist/sidebar/NavGroup.d.ts +3 -2
- package/dist/sidebar/NavGroup.js +14 -4
- package/dist/sidebar/NavItem.d.ts +1 -1
- package/dist/sidebar/NavItem.js +1 -1
- package/dist/sidebar/NavSection.d.ts +1 -1
- package/dist/sidebar/NavSection.js +1 -1
- package/dist/sidebar/Sidebar.d.ts +1 -1
- package/dist/sidebar/Sidebar.js +1 -1
- package/dist/sidebar/SidebarUser.d.ts +1 -1
- package/dist/sidebar/SidebarUser.js +1 -1
- package/dist/sidebar/sidebar.css +75 -14
- package/dist/sortable-list/SortableList.d.ts +1 -1
- package/dist/sortable-list/SortableList.js +1 -1
- package/dist/stat/Stat.d.ts +1 -1
- package/dist/stat/Stat.js +1 -1
- package/dist/swatch-picker/SwatchPicker.d.ts +1 -1
- package/dist/swatch-picker/SwatchPicker.js +1 -1
- package/dist/swatch-picker/swatch-picker.css +3 -2
- package/dist/table/Table.d.ts +1 -1
- package/dist/table/Table.js +1 -1
- package/dist/tabs/Tabs.d.ts +1 -1
- package/dist/tabs/Tabs.js +1 -1
- package/dist/tags-input/TagsInput.d.ts +1 -1
- package/dist/tags-input/TagsInput.js +1 -1
- package/dist/tags-input/tags-input.css +6 -3
- package/dist/textarea/textarea.css +2 -1
- package/dist/theme-toggle/ThemeToggle.d.ts +1 -1
- package/dist/theme-toggle/ThemeToggle.js +1 -1
- package/dist/theme.css +51 -10
- package/dist/timeline/Timeline.d.ts +1 -1
- package/dist/timeline/Timeline.js +1 -1
- package/dist/toast/Toaster.d.ts +1 -1
- package/dist/toast/Toaster.js +1 -1
- package/dist/tooltip/tooltip.css +3 -1
- package/dist/tree-view/TreeView.d.ts +1 -1
- package/dist/tree-view/TreeView.js +1 -1
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -52,7 +52,8 @@ So far:
|
|
|
52
52
|
- Pickers: TimeInput · NativeSelect · SwatchPicker · Select (one component: searchable, server `load`, `onCreate`,
|
|
53
53
|
multiple, tags; Combobox's parts for building one by hand) · DatePicker
|
|
54
54
|
- Choices: Checkbox · CheckboxGroup · Switch · RadioGroup · Segmented ·
|
|
55
|
-
ThemeToggle (`colorScheme`, `useColorScheme`)
|
|
55
|
+
ThemeToggle (`colorScheme`, `useColorScheme`) · Appearance (light and dark, the accent and the
|
|
56
|
+
sidebar style behind one button; `appearance`, `useAppearance`)
|
|
56
57
|
- Forms: Field · Fields
|
|
57
58
|
- Overlays: Tooltip · Popover · HoverCard · Dialog · Sheet (both: props or
|
|
58
59
|
composed from Header/Body/Footer) · ActionBar · Menu · ContextMenu (Menu.Item,
|
|
@@ -65,7 +66,8 @@ So far:
|
|
|
65
66
|
events, a date jump) · SortableList (drag or keyboard, on dnd-kit)
|
|
66
67
|
- Disclosure: Tabs/Tab (with counts and dots, inline or in the corner) · Accordion · Collapsible
|
|
67
68
|
- Navigation: AppShell (flat or inset; the sidebar collapses to a rail with ⌘B
|
|
68
|
-
and is a drawer on phones
|
|
69
|
+
and is a drawer on phones; inset, or `scroll="page"`, scrolls only the page
|
|
70
|
+
on wide screens) · Topbar · Sidebar (NavSection, NavGroup, NavItem,
|
|
69
71
|
SidebarUser; also Sidebar.Section…) · Page · Breadcrumbs · Command (⌘K)
|
|
70
72
|
|
|
71
73
|
That's all of svelte-ui's components.
|
|
@@ -104,7 +106,8 @@ the words of the app's provider.
|
|
|
104
106
|
(`classes={{ itemText: 'font-mono' }}`).
|
|
105
107
|
- Slots replace a part's content: a node, or a function given its state
|
|
106
108
|
(`indicator={({ indeterminate }) => …}`, `renderItem={(item) => …}`).
|
|
107
|
-
-
|
|
109
|
+
- Tokens for one instance or a section: a class (`className="[--ui-accent:#16a34a]"` with
|
|
110
|
+
Tailwind) or `style={{ '--ui-accent': … }}`; on `:root` for the whole app.
|
|
108
111
|
- Typed: written in TypeScript, shipped as JS with `.d.ts` types, so TSX apps
|
|
109
112
|
get checked props (Select's value is the type of your items' `valueKey`)
|
|
110
113
|
and JSX apps get autocomplete, with no errors either way.
|
|
@@ -133,6 +136,53 @@ dark values apply under `html.dark`:
|
|
|
133
136
|
:root { --ui-accent: #e11d48; --ui-radius: 10px; }
|
|
134
137
|
```
|
|
135
138
|
|
|
139
|
+
### Accent and sidebar style
|
|
140
|
+
|
|
141
|
+
`appearance` holds the accent colour and the sidebar style, saved like
|
|
142
|
+
`colorScheme`. `<Appearance>` lets people choose them, with light and dark,
|
|
143
|
+
behind one button:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
<Appearance accents={['teal', 'blue', 'violet']} sidebars={['neutral', 'tint', 'dark']} />
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`accents` and `sidebars` are what users may choose (every one if left out),
|
|
150
|
+
the default first: a saved choice outside them moves to it. While building
|
|
151
|
+
the app, add `dev`: it offers every accent and sidebar style, any colour, a
|
|
152
|
+
reset, and the props to paste for the current choice. Take it off before
|
|
153
|
+
shipping.
|
|
154
|
+
|
|
155
|
+
The accents are Tailwind's scales (`slate`, `blue`, `indigo`, `violet`,
|
|
156
|
+
`teal`, `emerald`, `rose`); a custom hex colour gets its scale worked out in
|
|
157
|
+
the browser. It sets the `--ui-accent-50` … `-950` scale on `<html>`, which
|
|
158
|
+
the accent and ring tokens are steps of, so setting that scale in your own
|
|
159
|
+
CSS recolours them too. The sidebar styles (`default`, `neutral`, `soft`,
|
|
160
|
+
`tint`, `accent`, `dark`, `deep`) are light-mode only. From code:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { appearance } from '@milktop/react-ui'
|
|
164
|
+
|
|
165
|
+
appearance.defaults = { accent: 'teal', sidebar: 'neutral' } // before anything is saved, and for reset
|
|
166
|
+
appearance.update({ accent: 'violet', sidebar: 'dark' })
|
|
167
|
+
appearance.load(user.appearance) // apply a saved one
|
|
168
|
+
appearance.onChange = (value) => save(value) // { accent, custom, sidebar }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
It saves under `ui-appearance` (`appearance.storageKey` changes it), scale
|
|
172
|
+
included, so the `<head>` can paint it before your bundle loads:
|
|
173
|
+
|
|
174
|
+
```html
|
|
175
|
+
<script>
|
|
176
|
+
try {
|
|
177
|
+
var a = JSON.parse(localStorage.getItem('ui-appearance') || '{}'), r = document.documentElement
|
|
178
|
+
if (a.sidebar && a.sidebar !== 'default') r.dataset.uiSidebarStyle = a.sidebar
|
|
179
|
+
var steps = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
|
|
180
|
+
if (a.scale) steps.forEach(function (s, i) { r.style.setProperty('--ui-accent-' + s, a.scale[i]) })
|
|
181
|
+
if (a.scaleText) r.style.setProperty('--ui-accent-500-text', a.scaleText)
|
|
182
|
+
} catch (e) {}
|
|
183
|
+
</script>
|
|
184
|
+
```
|
|
185
|
+
|
|
136
186
|
### React Server Components
|
|
137
187
|
|
|
138
188
|
Not supported yet, and not needed in Vite apps. A server component can't
|
|
@@ -150,9 +200,10 @@ npm run build # the playground as a static site, in playground/dist
|
|
|
150
200
|
npm run build:lib # the package: src/ to dist/ (JS, .d.ts, CSS); runs before npm publish
|
|
151
201
|
```
|
|
152
202
|
|
|
153
|
-
Releasing (
|
|
154
|
-
|
|
155
|
-
|
|
203
|
+
Releasing (on a clean main): each runs the checks and the build, bumps the
|
|
204
|
+
version, commits and tags it, and pushes with the tag. The tag starts the
|
|
205
|
+
Publish workflow (`.github/workflows/publish.yml`), which publishes to npm
|
|
206
|
+
through Trusted publishing: no npm login or token needed.
|
|
156
207
|
|
|
157
208
|
```sh
|
|
158
209
|
npm run release:patch # 0.1.0 → 0.1.1
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { type ReactNode } from 'react';
|
|
26
26
|
import { Accordion as Ark } from '@ark-ui/react/accordion';
|
|
27
|
-
import '
|
|
27
|
+
import '../icons.js';
|
|
28
28
|
import { type Classes } from '../utils.js';
|
|
29
29
|
import '../theme.css';
|
|
30
30
|
import './accordion.css';
|
|
@@ -25,7 +25,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
25
25
|
*/
|
|
26
26
|
import { createContext, useContext, useRef } from 'react';
|
|
27
27
|
import { Accordion as Ark } from '@ark-ui/react/accordion';
|
|
28
|
-
import '
|
|
28
|
+
import '../icons.js';
|
|
29
29
|
import { cx, partsOf, useControllable, withParts } from '../utils.js';
|
|
30
30
|
import '../theme.css';
|
|
31
31
|
import './accordion.css';
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* - `label`: the toolbar's accessible name
|
|
19
19
|
*/
|
|
20
20
|
import { type ComponentProps, type ReactNode } from 'react';
|
|
21
|
-
import '
|
|
21
|
+
import '../icons.js';
|
|
22
22
|
import { type Classes } from '../utils.js';
|
|
23
23
|
import '../theme.css';
|
|
24
24
|
import './action-bar.css';
|
|
@@ -20,7 +20,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
20
20
|
*/
|
|
21
21
|
import { useEffect, useState } from 'react';
|
|
22
22
|
import { Portal } from '@ark-ui/react/portal';
|
|
23
|
-
import '
|
|
23
|
+
import '../icons.js';
|
|
24
24
|
import { cx, partsOf } from '../utils.js';
|
|
25
25
|
import { useMessages } from '../format/LocaleProvider.js';
|
|
26
26
|
import '../theme.css';
|
package/dist/alert/Alert.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* when they appear; the others are role=status.
|
|
14
14
|
*/
|
|
15
15
|
import type { ComponentProps, ReactNode } from 'react';
|
|
16
|
-
import '
|
|
16
|
+
import '../icons.js';
|
|
17
17
|
import { type Classes } from '../utils.js';
|
|
18
18
|
import '../theme.css';
|
|
19
19
|
import './alert.css';
|
package/dist/alert/Alert.js
CHANGED
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
*
|
|
11
11
|
* - `inset`: the page as a rounded panel on a frame, with a grey sidebar
|
|
12
12
|
* whose hovered and current items go white (the `--ui-sidebar-inset-*` tokens)
|
|
13
|
+
* - `scroll`: what scrolls on wide screens: 'window' (default) or 'page',
|
|
14
|
+
* where the shell fills the viewport and only the page scrolls. `inset`
|
|
15
|
+
* always scrolls the page there. Phones scroll the window either way, so
|
|
16
|
+
* the browser can hide its toolbars. The page then takes focus, so Space
|
|
17
|
+
* and the arrow keys scroll it straight away, prints in full, and is marked
|
|
18
|
+
* `scroll-region` for Inertia's scroll resets. Listen for scrolling on the
|
|
19
|
+
* page (`useShell().scroller()` in the shell's parts), not the window.
|
|
13
20
|
* - `collapsed`, `defaultCollapsed`; `onCollapsedChange`: the sidebar as an
|
|
14
21
|
* icon rail
|
|
15
22
|
* - `persist`: a localStorage key to remember `collapsed`
|
|
@@ -26,6 +33,7 @@ import '../theme.css';
|
|
|
26
33
|
import './app-shell.css';
|
|
27
34
|
export type AppShellProps = HTMLAttributes<HTMLDivElement> & {
|
|
28
35
|
inset?: boolean;
|
|
36
|
+
scroll?: 'window' | 'page';
|
|
29
37
|
collapsed?: boolean;
|
|
30
38
|
defaultCollapsed?: boolean;
|
|
31
39
|
onCollapsedChange?: (collapsed: boolean) => void;
|
|
@@ -34,5 +42,5 @@ export type AppShellProps = HTMLAttributes<HTMLDivElement> & {
|
|
|
34
42
|
shortcut?: boolean;
|
|
35
43
|
classes?: Classes<'root' | 'backdrop'>;
|
|
36
44
|
};
|
|
37
|
-
export declare function AppShell({ inset, collapsed: collapsedProp, defaultCollapsed, onCollapsedChange, persist, breakpoint, shortcut, className, classes, children, ...rest }: AppShellProps): import("react").JSX.Element;
|
|
45
|
+
export declare function AppShell({ inset, scroll, collapsed: collapsedProp, defaultCollapsed, onCollapsedChange, persist, breakpoint, shortcut, className, classes, children, ...rest }: AppShellProps): import("react").JSX.Element;
|
|
38
46
|
export { useShell } from './shell.js';
|
|
@@ -11,6 +11,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
*
|
|
12
12
|
* - `inset`: the page as a rounded panel on a frame, with a grey sidebar
|
|
13
13
|
* whose hovered and current items go white (the `--ui-sidebar-inset-*` tokens)
|
|
14
|
+
* - `scroll`: what scrolls on wide screens: 'window' (default) or 'page',
|
|
15
|
+
* where the shell fills the viewport and only the page scrolls. `inset`
|
|
16
|
+
* always scrolls the page there. Phones scroll the window either way, so
|
|
17
|
+
* the browser can hide its toolbars. The page then takes focus, so Space
|
|
18
|
+
* and the arrow keys scroll it straight away, prints in full, and is marked
|
|
19
|
+
* `scroll-region` for Inertia's scroll resets. Listen for scrolling on the
|
|
20
|
+
* page (`useShell().scroller()` in the shell's parts), not the window.
|
|
14
21
|
* - `collapsed`, `defaultCollapsed`; `onCollapsedChange`: the sidebar as an
|
|
15
22
|
* icon rail
|
|
16
23
|
* - `persist`: a localStorage key to remember `collapsed`
|
|
@@ -21,7 +28,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
21
28
|
* ⌘B (Ctrl+B) collapses the sidebar, or opens the drawer on phones; Escape
|
|
22
29
|
* closes the drawer. Parts inside read the shell with `useShell()`.
|
|
23
30
|
*/
|
|
24
|
-
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
|
|
31
|
+
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
25
32
|
import { cx, partsOf, useControllable } from '../utils.js';
|
|
26
33
|
import { ShellContext } from './shell.js';
|
|
27
34
|
import '../theme.css';
|
|
@@ -45,7 +52,29 @@ function useNarrow(breakpoint) {
|
|
|
45
52
|
}, [query]);
|
|
46
53
|
return useSyncExternalStore(subscribe, () => matchMedia(query).matches, () => false);
|
|
47
54
|
}
|
|
48
|
-
|
|
55
|
+
// The keys that scroll the window do nothing for the page while nothing is
|
|
56
|
+
// focused, and Chrome ignores them on the page itself (focusable, see Page):
|
|
57
|
+
// they scroll it here, and focus it. Focus inside the page leaves them to
|
|
58
|
+
// the browser (fields, nested scrollers).
|
|
59
|
+
function scrollKey(e, page) {
|
|
60
|
+
if (![null, document.body, document.documentElement, page].includes(document.activeElement))
|
|
61
|
+
return;
|
|
62
|
+
if (e.altKey || e.ctrlKey || e.metaKey)
|
|
63
|
+
return;
|
|
64
|
+
const step = page.clientHeight * 0.875;
|
|
65
|
+
const top = {
|
|
66
|
+
ArrowDown: 40, ArrowUp: -40, PageDown: step, PageUp: -step,
|
|
67
|
+
' ': e.shiftKey ? -step : step,
|
|
68
|
+
Home: -page.scrollTop, End: page.scrollHeight,
|
|
69
|
+
}[e.key];
|
|
70
|
+
if (top === undefined)
|
|
71
|
+
return;
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
page.focus({ preventScroll: true });
|
|
74
|
+
const still = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
75
|
+
page.scrollBy({ top, behavior: still ? 'auto' : 'smooth' });
|
|
76
|
+
}
|
|
77
|
+
export function AppShell({ inset = false, scroll = 'window', collapsed: collapsedProp, defaultCollapsed = false, onCollapsedChange, persist = null, breakpoint = 768, shortcut = true, className, classes, children, ...rest }) {
|
|
49
78
|
const part = partsOf('app-shell', classes);
|
|
50
79
|
const [initial] = useState(() => saved(persist) ?? defaultCollapsed);
|
|
51
80
|
const [collapsed, setCollapsed] = useControllable(collapsedProp, initial, onCollapsedChange);
|
|
@@ -54,6 +83,10 @@ export function AppShell({ inset = false, collapsed: collapsedProp, defaultColla
|
|
|
54
83
|
// Widening the window past the breakpoint closes the drawer.
|
|
55
84
|
const drawerOpen = drawer && mobile;
|
|
56
85
|
const rail = collapsed && !mobile;
|
|
86
|
+
// Only the page scrolls (see `scroll`).
|
|
87
|
+
const pageScroll = !mobile && (inset || scroll === 'page');
|
|
88
|
+
const root = useRef(null);
|
|
89
|
+
const page = useCallback(() => root.current?.querySelector(':scope > [data-scope=page]') ?? null, []);
|
|
57
90
|
const toggle = useCallback(() => {
|
|
58
91
|
if (mobile)
|
|
59
92
|
return setDrawer(!drawerOpen);
|
|
@@ -64,7 +97,10 @@ export function AppShell({ inset = false, collapsed: collapsedProp, defaultColla
|
|
|
64
97
|
}
|
|
65
98
|
catch { /* private mode */ }
|
|
66
99
|
}, [mobile, drawerOpen, collapsed, setCollapsed, persist]);
|
|
67
|
-
const shell = useMemo(() => ({
|
|
100
|
+
const shell = useMemo(() => ({
|
|
101
|
+
rail, mobile, drawerOpen, pageScroll, toggle, close: () => setDrawer(false),
|
|
102
|
+
scroller: () => (pageScroll && page()) || document.scrollingElement,
|
|
103
|
+
}), [rail, mobile, drawerOpen, pageScroll, toggle, page]);
|
|
68
104
|
// Where the main area starts, for things fixed to the viewport that centre
|
|
69
105
|
// over it (ActionBar).
|
|
70
106
|
useEffect(() => {
|
|
@@ -83,10 +119,15 @@ export function AppShell({ inset = false, collapsed: collapsedProp, defaultColla
|
|
|
83
119
|
}
|
|
84
120
|
else if (e.key === 'Escape' && drawerOpen)
|
|
85
121
|
setDrawer(false);
|
|
122
|
+
else if (pageScroll && !e.defaultPrevented) {
|
|
123
|
+
const el = page();
|
|
124
|
+
if (el)
|
|
125
|
+
scrollKey(e, el);
|
|
126
|
+
}
|
|
86
127
|
}
|
|
87
128
|
addEventListener('keydown', keydown);
|
|
88
129
|
return () => removeEventListener('keydown', keydown);
|
|
89
|
-
}, [shortcut, toggle, drawerOpen]);
|
|
90
|
-
return (_jsx(ShellContext, { value: shell, children: _jsxs("div", { ...rest, ...part('root'), className: cx(className, classes?.root), "data-inset": inset || undefined, "data-rail": rail || undefined, "data-mobile": mobile || undefined, children: [children, drawerOpen && _jsx("div", { ...part('backdrop'), role: "presentation", onClick: () => setDrawer(false) })] }) }));
|
|
130
|
+
}, [shortcut, toggle, drawerOpen, pageScroll, page]);
|
|
131
|
+
return (_jsx(ShellContext, { value: shell, children: _jsxs("div", { ref: root, ...rest, ...part('root'), className: cx(className, classes?.root), "data-inset": inset || undefined, "data-page-scroll": pageScroll || undefined, "data-rail": rail || undefined, "data-mobile": mobile || undefined, children: [children, drawerOpen && _jsx("div", { ...part('backdrop'), role: "presentation", onClick: () => setDrawer(false) })] }) }));
|
|
91
132
|
}
|
|
92
133
|
export { useShell } from './shell.js';
|
package/dist/app-shell/Topbar.js
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
&[data-inset][data-mobile] > [data-scope=page] { margin: 0 0.5rem 0.5rem; }
|
|
13
13
|
> [data-scope=topbar] { grid-area: topbar; }
|
|
14
14
|
> [data-scope=page] { grid-area: main; }
|
|
15
|
+
/* Only the page scrolls; printing lets it run on. */
|
|
16
|
+
&[data-page-scroll] { height: 100dvh; overflow: hidden;
|
|
17
|
+
> [data-scope=page] { min-height: 0; overflow-y: auto; overscroll-behavior: contain; outline: none; }
|
|
18
|
+
@media print { height: auto; overflow: visible;
|
|
19
|
+
> [data-scope=page] { overflow-y: visible; } } }
|
|
15
20
|
|
|
16
21
|
/* Inset: the page is a rounded panel on the frame, and the sidebar and top
|
|
17
22
|
bar sit flat around it. The grey sidebar's items go white. */
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light or dark (`colorScheme`), the accent colour and the sidebar style
|
|
3
|
+
* (`appearance`), behind one button. The choices are saved.
|
|
4
|
+
*
|
|
5
|
+
* <Appearance accents={['teal', 'blue', 'violet']} sidebars={['neutral', 'tint', 'dark']} />
|
|
6
|
+
* <Appearance dev />
|
|
7
|
+
*
|
|
8
|
+
* - `accents`, `sidebars`: what users may choose, by name (every one if left
|
|
9
|
+
* out; see `accents` and `sidebarStyles`). A saved choice outside them moves
|
|
10
|
+
* to their first item, so put the default first.
|
|
11
|
+
* - `dev`: for trying themes out while building the app; take it off before
|
|
12
|
+
* shipping. Offers every accent and sidebar style whatever the lists say,
|
|
13
|
+
* any colour, a reset, and the `accents` and `sidebars` to paste for the
|
|
14
|
+
* current choice. Its switch turns dev mode off to preview what users get.
|
|
15
|
+
* - `schemes`: false leaves out light and dark (e.g. with a ThemeToggle
|
|
16
|
+
* elsewhere)
|
|
17
|
+
* - `placement`: where the panel opens ('bottom-end')
|
|
18
|
+
* - `labels`: words over the locale's `appearance` ones
|
|
19
|
+
* - `onChange`: called with appearance's value ({ accent, custom, sidebar })
|
|
20
|
+
* when the user changes it
|
|
21
|
+
*/
|
|
22
|
+
import { type HTMLAttributes } from 'react';
|
|
23
|
+
import { type PopoverProps } from '../popover/Popover.js';
|
|
24
|
+
import { type AppearanceValue } from '../appearance.js';
|
|
25
|
+
import { type Classes } from '../utils.js';
|
|
26
|
+
import type { Messages } from '../format/messages.js';
|
|
27
|
+
import '../theme.css';
|
|
28
|
+
import './appearance.css';
|
|
29
|
+
export type AppearanceProps = Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> & {
|
|
30
|
+
accents?: string[] | null;
|
|
31
|
+
sidebars?: string[] | null;
|
|
32
|
+
dev?: boolean;
|
|
33
|
+
schemes?: boolean;
|
|
34
|
+
placement?: PopoverProps['placement'];
|
|
35
|
+
labels?: Partial<Messages['appearance']> | null;
|
|
36
|
+
onChange?: (value: AppearanceValue) => void;
|
|
37
|
+
classes?: Classes<'root' | 'trigger' | 'panel' | 'setting' | 'label' | 'hint' | 'custom' | 'dev' | 'config'>;
|
|
38
|
+
};
|
|
39
|
+
export declare function Appearance({ accents, sidebars, dev, schemes, placement, labels, onChange, className, classes, ...rest }: AppearanceProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Light or dark (`colorScheme`), the accent colour and the sidebar style
|
|
4
|
+
* (`appearance`), behind one button. The choices are saved.
|
|
5
|
+
*
|
|
6
|
+
* <Appearance accents={['teal', 'blue', 'violet']} sidebars={['neutral', 'tint', 'dark']} />
|
|
7
|
+
* <Appearance dev />
|
|
8
|
+
*
|
|
9
|
+
* - `accents`, `sidebars`: what users may choose, by name (every one if left
|
|
10
|
+
* out; see `accents` and `sidebarStyles`). A saved choice outside them moves
|
|
11
|
+
* to their first item, so put the default first.
|
|
12
|
+
* - `dev`: for trying themes out while building the app; take it off before
|
|
13
|
+
* shipping. Offers every accent and sidebar style whatever the lists say,
|
|
14
|
+
* any colour, a reset, and the `accents` and `sidebars` to paste for the
|
|
15
|
+
* current choice. Its switch turns dev mode off to preview what users get.
|
|
16
|
+
* - `schemes`: false leaves out light and dark (e.g. with a ThemeToggle
|
|
17
|
+
* elsewhere)
|
|
18
|
+
* - `placement`: where the panel opens ('bottom-end')
|
|
19
|
+
* - `labels`: words over the locale's `appearance` ones
|
|
20
|
+
* - `onChange`: called with appearance's value ({ accent, custom, sidebar })
|
|
21
|
+
* when the user changes it
|
|
22
|
+
*/
|
|
23
|
+
import { useEffect, useState } from 'react';
|
|
24
|
+
import { Popover } from '../popover/Popover.js';
|
|
25
|
+
import { Button } from '../button/Button.js';
|
|
26
|
+
import { Segmented } from '../segment-group/Segmented.js';
|
|
27
|
+
import { SwatchPicker } from '../swatch-picker/SwatchPicker.js';
|
|
28
|
+
import { NativeSelect } from '../native-select/NativeSelect.js';
|
|
29
|
+
import { Switch } from '../switch/Switch.js';
|
|
30
|
+
import { useColorScheme } from '../color-scheme.js';
|
|
31
|
+
import { appearance, accents as allAccents, sidebarStyles as allSidebars, useAppearance, } from '../appearance.js';
|
|
32
|
+
import { cx, partsOf } from '../utils.js';
|
|
33
|
+
import { useMessages } from '../format/LocaleProvider.js';
|
|
34
|
+
import '../theme.css';
|
|
35
|
+
import './appearance.css';
|
|
36
|
+
const schemeIcons = { system: 'lucide:monitor', light: 'lucide:sun', dark: 'lucide:moon' };
|
|
37
|
+
export function Appearance({ accents = null, sidebars = null, dev = false, schemes = true, placement = 'bottom-end', labels = null, onChange, className, classes, ...rest }) {
|
|
38
|
+
const t = useMessages();
|
|
39
|
+
const words = { ...t.appearance, ...labels };
|
|
40
|
+
const part = partsOf('appearance', classes);
|
|
41
|
+
const { scheme, dark, setScheme } = useColorScheme();
|
|
42
|
+
const value = useAppearance();
|
|
43
|
+
const [devMode, setDevMode] = useState(true);
|
|
44
|
+
const exploring = dev && devMode;
|
|
45
|
+
// In the lists' own order (the first is the fallback), or every one
|
|
46
|
+
const accentNames = exploring || !accents ? Object.keys(allAccents) : accents.filter((name) => name in allAccents);
|
|
47
|
+
const sidebarNames = exploring || !sidebars ? [...allSidebars] : sidebars.filter((name) => allSidebars.includes(name));
|
|
48
|
+
const schemeItems = ['system', 'light', 'dark'].map((s) => ({ value: s, label: t.themeToggle[s], icon: schemeIcons[s] }));
|
|
49
|
+
const accentItems = accentNames.map((name) => ({ value: name, label: words.accents[name] ?? name, color: allAccents[name][0][6] }));
|
|
50
|
+
const sidebarItems = sidebarNames.map((name) => ({ value: name, label: words.sidebars[name] ?? name }));
|
|
51
|
+
// A saved choice outside the lists moves to their first item.
|
|
52
|
+
const allowedAccents = accentNames.join();
|
|
53
|
+
const allowedSidebars = sidebarNames.join();
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!exploring)
|
|
56
|
+
appearance.restrict(allowedAccents.split(','), allowedSidebars.split(','));
|
|
57
|
+
}, [exploring, allowedAccents, allowedSidebars]);
|
|
58
|
+
// The current choice as code to paste into the app: the props that offer
|
|
59
|
+
// it, or for a custom colour, the default that sets it.
|
|
60
|
+
const config = value.custom
|
|
61
|
+
? `appearance.defaults = ${JSON.stringify({ custom: value.custom, sidebar: value.sidebar })}`
|
|
62
|
+
: `accents={['${value.accent}']} sidebars={['${value.sidebar}']}`;
|
|
63
|
+
function pick(changes) {
|
|
64
|
+
appearance.update(changes);
|
|
65
|
+
onChange?.(appearance.value);
|
|
66
|
+
}
|
|
67
|
+
function reset() {
|
|
68
|
+
appearance.reset();
|
|
69
|
+
onChange?.(appearance.value);
|
|
70
|
+
}
|
|
71
|
+
return (_jsx("div", { ...rest, ...part('root'), className: cx(className, classes?.root), children: _jsx(Popover, { heading: words.label, placement: placement, trigger: _jsx(Button, { className: classes?.trigger, variant: "ghost", icon: "lucide:palette", "aria-label": words.label }), children: _jsxs("div", { ...part('panel'), children: [schemes && (_jsxs("div", { ...part('setting'), children: [_jsx("div", { ...part('label'), children: words.mode }), _jsx(Segmented, { size: "sm", items: schemeItems, value: scheme, "aria-label": words.mode, onValueChange: (next) => next && setScheme(next) })] })), _jsxs("div", { ...part('setting'), children: [_jsx("div", { ...part('label'), children: words.accent }), _jsx(SwatchPicker, { size: "sm", items: accentItems, value: value.custom ? null : value.accent, "aria-label": words.accent, onValueChange: (next) => next && pick({ accent: next, custom: null }) })] }), exploring && (_jsxs("label", { ...part('custom'), children: [_jsx("input", { type: "color", value: value.custom ?? appearance.scale[6], onChange: (e) => pick({ custom: e.target.value }) }), _jsx("span", { children: words.custom }), value.custom && _jsx("code", { children: value.custom })] })), _jsxs("div", { ...part('setting'), children: [_jsxs("div", { ...part('label'), children: [_jsx("span", { children: words.sidebar }), dark && _jsx("span", { ...part('hint'), children: words.lightOnly })] }), sidebarItems.length <= 4
|
|
72
|
+
? _jsx(Segmented, { size: "sm", items: sidebarItems, value: value.sidebar, "aria-label": words.sidebar, onValueChange: (next) => next && pick({ sidebar: next }) })
|
|
73
|
+
: _jsx(NativeSelect, { size: "sm", items: sidebarItems, value: value.sidebar, "aria-label": words.sidebar, onValueChange: (next) => next && pick({ sidebar: next }) })] }), dev && (_jsxs("div", { ...part('dev'), children: [_jsx(Switch, { label: words.dev, checked: devMode, onCheckedChange: setDevMode }), exploring && _jsxs(_Fragment, { children: [_jsx("code", { ...part('config'), children: config }), _jsx(Button, { size: "sm", onClick: reset, children: words.reset })] })] }))] }) }) }));
|
|
74
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
@layer ui {
|
|
2
|
+
[data-scope=appearance][data-part=root] { display: inline-block;
|
|
3
|
+
/* A little wider than a popover's default, for four sidebar styles. */
|
|
4
|
+
& [data-scope=popover][data-part=content] { width: 20rem; } }
|
|
5
|
+
[data-scope=appearance][data-part=panel] { display: flex; flex-direction: column; gap: 1rem; }
|
|
6
|
+
[data-scope=appearance][data-part=label] { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem;
|
|
7
|
+
margin-bottom: 0.5rem; font-size: 12px; font-weight: 600; color: var(--ui-muted); }
|
|
8
|
+
[data-scope=appearance][data-part=hint] { font-weight: 400; }
|
|
9
|
+
[data-scope=appearance][data-part=custom] { display: flex; align-items: center; gap: 0.5rem; font-size: 14px; cursor: pointer;
|
|
10
|
+
& input { width: 2rem; height: 2rem; padding: 0; border: 1px solid var(--ui-border); border-radius: var(--ui-radius);
|
|
11
|
+
background: transparent; cursor: pointer; }
|
|
12
|
+
& code { font-size: 12px; color: var(--ui-muted); } }
|
|
13
|
+
[data-scope=appearance][data-part=dev] { display: flex; flex-direction: column; align-items: flex-start; gap: 0.75rem;
|
|
14
|
+
padding-top: 0.75rem; border-top: 1px solid var(--ui-border); }
|
|
15
|
+
[data-scope=appearance][data-part=config] { display: block; align-self: stretch; padding: 0.5rem; background: var(--ui-hover);
|
|
16
|
+
border-radius: var(--ui-radius); font-size: 12px; color: var(--ui-muted); white-space: pre-wrap; user-select: all; }
|
|
17
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare const accentSteps: number[];
|
|
2
|
+
export declare const accents: {
|
|
3
|
+
slate: [string[], string];
|
|
4
|
+
blue: [string[], string];
|
|
5
|
+
indigo: [string[], string];
|
|
6
|
+
violet: [string[], string];
|
|
7
|
+
teal: [string[], string];
|
|
8
|
+
emerald: [string[], string];
|
|
9
|
+
rose: [string[], string];
|
|
10
|
+
};
|
|
11
|
+
export type AccentName = keyof typeof accents;
|
|
12
|
+
export declare const sidebarStyles: readonly ["default", "neutral", "soft", "tint", "accent", "dark", "deep"];
|
|
13
|
+
export type SidebarStyle = typeof sidebarStyles[number];
|
|
14
|
+
export type AppearanceValue = {
|
|
15
|
+
accent: AccentName;
|
|
16
|
+
custom: string | null;
|
|
17
|
+
sidebar: SidebarStyle;
|
|
18
|
+
};
|
|
19
|
+
export declare const accentScale: (hex: string) => string[];
|
|
20
|
+
declare class Appearance {
|
|
21
|
+
#private;
|
|
22
|
+
onChange: ((value: AppearanceValue) => void) | null;
|
|
23
|
+
constructor();
|
|
24
|
+
get storageKey(): string;
|
|
25
|
+
set storageKey(key: string);
|
|
26
|
+
get defaults(): AppearanceValue;
|
|
27
|
+
set defaults(value: Partial<AppearanceValue>);
|
|
28
|
+
get value(): AppearanceValue;
|
|
29
|
+
get accent(): "slate" | "blue" | "indigo" | "violet" | "teal" | "emerald" | "rose";
|
|
30
|
+
get custom(): string | null;
|
|
31
|
+
get sidebar(): "default" | "neutral" | "soft" | "tint" | "accent" | "dark" | "deep";
|
|
32
|
+
get scale(): string[];
|
|
33
|
+
get scaleText(): string;
|
|
34
|
+
update(changes: Partial<AppearanceValue>): void;
|
|
35
|
+
load(value: unknown): void;
|
|
36
|
+
reset(): void;
|
|
37
|
+
restrict(allowedAccents?: readonly string[] | null, allowedSidebars?: readonly string[] | null): void;
|
|
38
|
+
subscribe: (listener: () => void) => () => void;
|
|
39
|
+
}
|
|
40
|
+
export declare const appearance: Appearance;
|
|
41
|
+
export declare function useAppearance(): AppearanceValue;
|
|
42
|
+
export {};
|