@adecore/ui 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/AccentSwatches.d.ts +15 -0
- package/dist/AccentSwatches.js +21 -0
- package/dist/Banner.d.ts +12 -0
- package/dist/Banner.js +18 -0
- package/dist/Button.d.ts +9 -0
- package/dist/Button.js +28 -0
- package/dist/ButtonGroup.d.ts +3 -0
- package/dist/ButtonGroup.js +11 -0
- package/dist/Checkbox.d.ts +11 -0
- package/dist/Checkbox.js +9 -0
- package/dist/ChoiceCards.d.ts +23 -0
- package/dist/ChoiceCards.js +21 -0
- package/dist/CloseButton.d.ts +15 -0
- package/dist/CloseButton.js +11 -0
- package/dist/ColorSwatch.d.ts +14 -0
- package/dist/ColorSwatch.js +28 -0
- package/dist/ColumnResizeHandle.d.ts +9 -0
- package/dist/ColumnResizeHandle.js +12 -0
- package/dist/DisabledReason.d.ts +6 -0
- package/dist/DisabledReason.js +8 -0
- package/dist/DockShell.d.ts +9 -0
- package/dist/DockShell.js +49 -0
- package/dist/EmptyState.d.ts +16 -0
- package/dist/EmptyState.js +11 -0
- package/dist/ErrorBoundary.d.ts +24 -0
- package/dist/ErrorBoundary.js +47 -0
- package/dist/Field.d.ts +17 -0
- package/dist/Field.js +48 -0
- package/dist/FileIcon.d.ts +8 -0
- package/dist/FileIcon.js +15 -0
- package/dist/Icon.d.ts +9 -0
- package/dist/Icon.js +12 -0
- package/dist/IconButton.d.ts +23 -0
- package/dist/IconButton.js +25 -0
- package/dist/IconPicker.d.ts +17 -0
- package/dist/IconPicker.js +93 -0
- package/dist/Input.d.ts +14 -0
- package/dist/Input.js +25 -0
- package/dist/Kbd.d.ts +23 -0
- package/dist/Kbd.js +20 -0
- package/dist/ListRow.d.ts +10 -0
- package/dist/ListRow.js +20 -0
- package/dist/Meter.d.ts +12 -0
- package/dist/Meter.js +7 -0
- package/dist/PanelEmpty.d.ts +16 -0
- package/dist/PanelEmpty.js +7 -0
- package/dist/PanelHeader.d.ts +6 -0
- package/dist/PanelHeader.js +20 -0
- package/dist/Pill.d.ts +27 -0
- package/dist/Pill.js +25 -0
- package/dist/ProjectSwitcher.d.ts +30 -0
- package/dist/ProjectSwitcher.js +18 -0
- package/dist/PromptDialog.d.ts +40 -0
- package/dist/PromptDialog.js +47 -0
- package/dist/SectionLabel.d.ts +3 -0
- package/dist/SectionLabel.js +16 -0
- package/dist/SegmentBar.d.ts +18 -0
- package/dist/SegmentBar.js +44 -0
- package/dist/Segmented.d.ts +17 -0
- package/dist/Segmented.js +7 -0
- package/dist/Select.d.ts +27 -0
- package/dist/Select.js +34 -0
- package/dist/Separator.d.ts +7 -0
- package/dist/Separator.js +8 -0
- package/dist/ShortcutHints.d.ts +1 -0
- package/dist/ShortcutHints.js +94 -0
- package/dist/Skeleton.d.ts +6 -0
- package/dist/Skeleton.js +6 -0
- package/dist/SlidingColumn.d.ts +19 -0
- package/dist/SlidingColumn.js +41 -0
- package/dist/Spinner.d.ts +6 -0
- package/dist/Spinner.js +7 -0
- package/dist/Stepper.d.ts +14 -0
- package/dist/Stepper.js +18 -0
- package/dist/Surface.d.ts +3 -0
- package/dist/Surface.js +16 -0
- package/dist/Switch.d.ts +10 -0
- package/dist/Switch.js +7 -0
- package/dist/TextMenu.d.ts +5 -0
- package/dist/TextMenu.js +31 -0
- package/dist/Tile.d.ts +11 -0
- package/dist/Tile.js +10 -0
- package/dist/Toasts.d.ts +9 -0
- package/dist/Toasts.js +58 -0
- package/dist/Tooltip.d.ts +16 -0
- package/dist/Tooltip.js +21 -0
- package/dist/UIProvider.d.ts +9 -0
- package/dist/UIProvider.js +26 -0
- package/dist/Waveform.d.ts +16 -0
- package/dist/Waveform.js +123 -0
- package/dist/Wipe.d.ts +12 -0
- package/dist/Wipe.js +47 -0
- package/dist/ZoomControls.d.ts +30 -0
- package/dist/ZoomControls.js +16 -0
- package/dist/class-name.d.ts +2 -0
- package/dist/class-name.js +3 -0
- package/dist/clipboard.d.ts +2 -0
- package/dist/clipboard.js +16 -0
- package/dist/context-menu/index.parts.d.ts +1 -0
- package/dist/context-menu/index.parts.js +2 -0
- package/dist/dialog/index.parts.d.ts +1 -0
- package/dist/dialog/index.parts.js +1 -0
- package/dist/dialog/parts.d.ts +36 -0
- package/dist/dialog/parts.js +55 -0
- package/dist/dialog-layer.d.ts +1 -0
- package/dist/dialog-layer.js +27 -0
- package/dist/error-boundary.d.ts +4 -0
- package/dist/error-boundary.js +34 -0
- package/dist/error-message.d.ts +1 -0
- package/dist/error-message.js +4 -0
- package/dist/field-context.d.ts +7 -0
- package/dist/field-context.js +4 -0
- package/dist/file-icon.d.ts +4 -0
- package/dist/file-icon.js +46 -0
- package/dist/floating.d.ts +5 -0
- package/dist/floating.js +6 -0
- package/dist/format/datetime.d.ts +11 -0
- package/dist/format/datetime.js +50 -0
- package/dist/format/duration.d.ts +5 -0
- package/dist/format/duration.js +76 -0
- package/dist/format/index.d.ts +6 -0
- package/dist/format/index.js +7 -0
- package/dist/format/locale.d.ts +17 -0
- package/dist/format/locale.js +105 -0
- package/dist/format/number.d.ts +8 -0
- package/dist/format/number.js +72 -0
- package/dist/format/regions.d.ts +6 -0
- package/dist/format/regions.js +15 -0
- package/dist/format/time-zone.d.ts +1 -0
- package/dist/format/time-zone.js +2 -0
- package/dist/icon-button-size.d.ts +3 -0
- package/dist/icon-button-size.js +3 -0
- package/dist/icon-picker.d.ts +23 -0
- package/dist/icon-picker.js +80 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +74 -0
- package/dist/lazy.d.ts +12 -0
- package/dist/lazy.js +104 -0
- package/dist/locales/en.json +59 -0
- package/dist/locales/nl.json +59 -0
- package/dist/locales.d.ts +4 -0
- package/dist/locales.js +17 -0
- package/dist/menu/index.parts.d.ts +1 -0
- package/dist/menu/index.parts.js +1 -0
- package/dist/menu/parts.d.ts +67 -0
- package/dist/menu/parts.js +113 -0
- package/dist/merge-refs.d.ts +2 -0
- package/dist/merge-refs.js +11 -0
- package/dist/modality.d.ts +1 -0
- package/dist/modality.js +29 -0
- package/dist/platform.d.ts +2 -0
- package/dist/platform.js +9 -0
- package/dist/popover/index.parts.d.ts +1 -0
- package/dist/popover/index.parts.js +1 -0
- package/dist/popover/parts.d.ts +26 -0
- package/dist/popover/parts.js +28 -0
- package/dist/prefetch.d.ts +21 -0
- package/dist/prefetch.js +99 -0
- package/dist/preview-card/index.parts.d.ts +1 -0
- package/dist/preview-card/index.parts.js +1 -0
- package/dist/project-switcher-row.d.ts +6 -0
- package/dist/project-switcher-row.js +16 -0
- package/dist/selection.d.ts +2 -0
- package/dist/selection.js +22 -0
- package/dist/seti-icons.d.ts +5 -0
- package/dist/seti-icons.js +445 -0
- package/dist/settings/ConfirmDialog.d.ts +9 -0
- package/dist/settings/ConfirmDialog.js +9 -0
- package/dist/settings/DetailHeader.d.ts +10 -0
- package/dist/settings/DetailHeader.js +6 -0
- package/dist/settings/MasterDetail.d.ts +24 -0
- package/dist/settings/MasterDetail.js +16 -0
- package/dist/settings/SettingsDialog.d.ts +46 -0
- package/dist/settings/SettingsDialog.js +96 -0
- package/dist/settings/SettingsRow.d.ts +22 -0
- package/dist/settings/SettingsRow.js +30 -0
- package/dist/settings/SettingsSection.d.ts +14 -0
- package/dist/settings/SettingsSection.js +8 -0
- package/dist/settings/index.d.ts +7 -0
- package/dist/settings/index.js +8 -0
- package/dist/settings/target.d.ts +7 -0
- package/dist/settings/target.js +3 -0
- package/dist/shortcut-hints.d.ts +38 -0
- package/dist/shortcut-hints.js +81 -0
- package/dist/shortcut.d.ts +32 -0
- package/dist/shortcut.js +137 -0
- package/dist/testing/dedupe.d.ts +1 -0
- package/dist/testing/dedupe.js +73 -0
- package/dist/testing/fake-source.d.ts +8 -0
- package/dist/testing/fake-source.js +15 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +2 -0
- package/dist/theme.css +562 -0
- package/dist/toast-store.d.ts +38 -0
- package/dist/toast-store.js +61 -0
- package/dist/useAsyncAction.d.ts +8 -0
- package/dist/useAsyncAction.js +23 -0
- package/dist/useColumnResize.d.ts +14 -0
- package/dist/useColumnResize.js +49 -0
- package/dist/useContentSize.d.ts +5 -0
- package/dist/useContentSize.js +21 -0
- package/dist/useMeasuredWidth.d.ts +1 -0
- package/dist/useMeasuredWidth.js +6 -0
- package/dist/useNow.d.ts +3 -0
- package/dist/useNow.js +41 -0
- package/dist/wipe-split.d.ts +7 -0
- package/dist/wipe-split.js +20 -0
- package/dist/zoom.d.ts +3 -0
- package/dist/zoom.js +6 -0
- package/package.json +92 -0
- package/src/AccentSwatches.tsx +73 -0
- package/src/Banner.tsx +43 -0
- package/src/Button.tsx +55 -0
- package/src/ButtonGroup.tsx +14 -0
- package/src/Checkbox.tsx +44 -0
- package/src/ChoiceCards.tsx +108 -0
- package/src/CloseButton.tsx +21 -0
- package/src/ColorSwatch.tsx +41 -0
- package/src/ColumnResizeHandle.tsx +23 -0
- package/src/DisabledReason.tsx +23 -0
- package/src/DockShell.tsx +76 -0
- package/src/EmptyState.tsx +48 -0
- package/src/ErrorBoundary.tsx +116 -0
- package/src/Field.tsx +116 -0
- package/src/FileIcon.tsx +27 -0
- package/src/Icon.tsx +22 -0
- package/src/IconButton.tsx +84 -0
- package/src/IconPicker.tsx +257 -0
- package/src/Input.tsx +78 -0
- package/src/Kbd.tsx +67 -0
- package/src/ListRow.tsx +26 -0
- package/src/Meter.tsx +41 -0
- package/src/PanelEmpty.tsx +37 -0
- package/src/PanelHeader.tsx +31 -0
- package/src/Pill.tsx +61 -0
- package/src/ProjectSwitcher.tsx +127 -0
- package/src/PromptDialog.tsx +168 -0
- package/src/SectionLabel.tsx +20 -0
- package/src/SegmentBar.tsx +128 -0
- package/src/Segmented.tsx +51 -0
- package/src/Select.tsx +135 -0
- package/src/Separator.tsx +15 -0
- package/src/ShortcutHints.tsx +118 -0
- package/src/Skeleton.tsx +13 -0
- package/src/SlidingColumn.tsx +91 -0
- package/src/Spinner.tsx +32 -0
- package/src/Stepper.tsx +67 -0
- package/src/Surface.tsx +20 -0
- package/src/Switch.tsx +32 -0
- package/src/TextMenu.tsx +59 -0
- package/src/Tile.tsx +49 -0
- package/src/Toasts.tsx +151 -0
- package/src/Tooltip.tsx +70 -0
- package/src/UIProvider.tsx +41 -0
- package/src/Waveform.tsx +199 -0
- package/src/Wipe.tsx +99 -0
- package/src/ZoomControls.tsx +99 -0
- package/src/class-name.ts +8 -0
- package/src/clipboard.ts +17 -0
- package/src/context-menu/index.parts.ts +21 -0
- package/src/dialog/index.parts.ts +10 -0
- package/src/dialog/parts.tsx +112 -0
- package/src/dialog-layer.ts +31 -0
- package/src/error-boundary.ts +40 -0
- package/src/error-message.ts +5 -0
- package/src/field-context.ts +12 -0
- package/src/file-icon.ts +52 -0
- package/src/floating.ts +9 -0
- package/src/format/datetime.ts +63 -0
- package/src/format/duration.ts +83 -0
- package/src/format/index.ts +20 -0
- package/src/format/locale.ts +153 -0
- package/src/format/number.ts +87 -0
- package/src/format/regions.ts +20 -0
- package/src/format/time-zone.ts +2 -0
- package/src/icon-button-size.ts +6 -0
- package/src/icon-picker.ts +112 -0
- package/src/index.ts +103 -0
- package/src/lazy.tsx +124 -0
- package/src/locales/en.json +59 -0
- package/src/locales/nl.json +59 -0
- package/src/locales.ts +21 -0
- package/src/menu/index.parts.ts +20 -0
- package/src/menu/parts.tsx +296 -0
- package/src/merge-refs.ts +14 -0
- package/src/modality.ts +37 -0
- package/src/platform.ts +10 -0
- package/src/popover/index.parts.ts +8 -0
- package/src/popover/parts.tsx +119 -0
- package/src/prefetch.ts +113 -0
- package/src/preview-card/index.parts.ts +1 -0
- package/src/project-switcher-row.tsx +51 -0
- package/src/selection.ts +24 -0
- package/src/seti-icons.ts +451 -0
- package/src/settings/ConfirmDialog.tsx +30 -0
- package/src/settings/DetailHeader.tsx +25 -0
- package/src/settings/MasterDetail.tsx +67 -0
- package/src/settings/SettingsDialog.tsx +342 -0
- package/src/settings/SettingsRow.tsx +89 -0
- package/src/settings/SettingsSection.tsx +46 -0
- package/src/settings/index.ts +16 -0
- package/src/settings/target.ts +12 -0
- package/src/shortcut-hints.ts +117 -0
- package/src/shortcut.ts +170 -0
- package/src/testing/dedupe.ts +93 -0
- package/src/testing/fake-source.ts +21 -0
- package/src/testing/index.ts +3 -0
- package/src/theme.css +562 -0
- package/src/toast-store.ts +120 -0
- package/src/useAsyncAction.ts +32 -0
- package/src/useColumnResize.ts +75 -0
- package/src/useContentSize.ts +27 -0
- package/src/useMeasuredWidth.ts +7 -0
- package/src/useNow.ts +49 -0
- package/src/wipe-split.ts +24 -0
- package/src/zoom.ts +9 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bas Milius
|
|
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/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @adecore/ui
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@adecore/ui)
|
|
4
|
+
[](https://adecore.dev/ui/)
|
|
5
|
+
|
|
6
|
+
React components for apps that behave like desktop software: menus with shortcuts, dialogs over dialogs, tooltips, fields, toasts and a settings dialog. Every number and date goes through formatters that follow the language and the region a person set, and one theme of tokens draws it all. Built on React 19, [Base UI](https://base-ui.com), [Lucide](https://lucide.dev) and Tailwind 4.
|
|
7
|
+
|
|
8
|
+
**[Documentation with a live demo of every component](https://adecore.dev/ui/)**
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
bun add @adecore/ui
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
React 19, `react-dom`, `i18next` and `react-i18next` are peer dependencies; the app brings them.
|
|
17
|
+
|
|
18
|
+
## Set up
|
|
19
|
+
|
|
20
|
+
Import the theme right after Tailwind and let Tailwind scan the library for the classes it uses. The `@source` path is relative to the CSS file.
|
|
21
|
+
|
|
22
|
+
```css
|
|
23
|
+
@import "tailwindcss";
|
|
24
|
+
@import "@adecore/ui/theme.css";
|
|
25
|
+
@source "../node_modules/@adecore/ui/dist";
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Wrap the app in `UIProvider` once, above the first component of the library. It adds the library's words to the app's i18next (English and Dutch ship with it), hands the formatters their [format source](https://adecore.dev/ui/formatting/) and mounts the tooltip provider.
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import i18next from 'i18next';
|
|
32
|
+
import { UIProvider } from '@adecore/ui';
|
|
33
|
+
|
|
34
|
+
createRoot(root).render(
|
|
35
|
+
<UIProvider i18n={i18next} formatSource={formatSource}>
|
|
36
|
+
<App />
|
|
37
|
+
</UIProvider>
|
|
38
|
+
);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
From there, every component is ready to use:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
<Menu.Root>
|
|
45
|
+
<IconButton icon={Ellipsis} label="More" render={<Menu.Trigger />} />
|
|
46
|
+
<Menu.Popup align="end">
|
|
47
|
+
<Menu.Item onClick={rename}>
|
|
48
|
+
Rename <Kbd shortcut={KEY_SHORTCUTS.rename} />
|
|
49
|
+
</Menu.Item>
|
|
50
|
+
<Menu.Item onClick={remove}>Remove</Menu.Item>
|
|
51
|
+
</Menu.Popup>
|
|
52
|
+
</Menu.Root>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
[Getting started](https://adecore.dev/ui/guide/getting-started) covers the rest: the format source, a third language, the optional pieces and how to work on a local checkout.
|
|
56
|
+
|
|
57
|
+
## Entry points
|
|
58
|
+
|
|
59
|
+
| Import | What it holds |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `@adecore/ui` | The components, hooks and helpers |
|
|
62
|
+
| `@adecore/ui/settings` | `SettingsDialog` and the parts of a pane |
|
|
63
|
+
| `@adecore/ui/format` | Numbers, dates, durations and regions, and `setFormatSource` |
|
|
64
|
+
| `@adecore/ui/testing` | Fakes for an app's tests, such as `fakeFormatSource()` |
|
|
65
|
+
| `@adecore/ui/testing/dedupe` | A `bun test` preload for an app that links a checkout |
|
|
66
|
+
| `@adecore/ui/theme.css` | The theme |
|
|
67
|
+
|
|
68
|
+
Nothing else is public. No module does work on import, so a bundler keeps only what an app uses.
|
|
69
|
+
|
|
70
|
+
## Documentation
|
|
71
|
+
|
|
72
|
+
| Page | What it covers |
|
|
73
|
+
|---|---|
|
|
74
|
+
| [Getting started](https://adecore.dev/ui/guide/getting-started) | Install, the theme, `UIProvider`, the format source and a local checkout |
|
|
75
|
+
| [Principles](https://adecore.dev/ui/guide/principles) | The rules every component follows, from the type scale to keyboard focus |
|
|
76
|
+
| [Theme](https://adecore.dev/ui/guide/theme) | Every token in light and dark, and how to set an accent |
|
|
77
|
+
| [Components](https://adecore.dev/ui/actions/button) | A page per component with a live demo, grouped in the sidebar |
|
|
78
|
+
| [Formatting](https://adecore.dev/ui/formatting/) | Numbers, dates, durations and regions |
|
|
79
|
+
| [Testing](https://adecore.dev/ui/utilities/testing) | Fakes for an app's tests and the preload for a linked checkout |
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Ref } from 'react';
|
|
2
|
+
export interface AccentSwatchesProps<Id extends string> {
|
|
3
|
+
value: string | undefined;
|
|
4
|
+
onValueChange(id: Id): void;
|
|
5
|
+
label: string;
|
|
6
|
+
accents: readonly {
|
|
7
|
+
id: Id;
|
|
8
|
+
color: string;
|
|
9
|
+
}[];
|
|
10
|
+
featured: readonly Id[];
|
|
11
|
+
labelOf(id: Id): string;
|
|
12
|
+
className?: string;
|
|
13
|
+
ref?: Ref<HTMLDivElement>;
|
|
14
|
+
}
|
|
15
|
+
export declare function AccentSwatches<Id extends string>({ value, onValueChange, label, accents, featured, labelOf, className, ref }: AccentSwatchesProps<Id>): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { Ellipsis } from 'lucide-react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { ColorSwatch } from './ColorSwatch.js';
|
|
6
|
+
import { Icon } from './Icon.js';
|
|
7
|
+
import { MenuPopup, MenuRadioGroup, MenuRadioItem, MenuRoot, MenuTrigger } from './menu/parts.js';
|
|
8
|
+
import { Tooltip } from './Tooltip.js';
|
|
9
|
+
/*
|
|
10
|
+
* An accent, a few colors at a time. A palette is more than a settings row can carry, so the list
|
|
11
|
+
* behind them holds all of them, the featured ones included; the trigger wears the chosen color
|
|
12
|
+
* itself whenever that color is not one of the few. A value that is no accent at all picks nothing.
|
|
13
|
+
*/
|
|
14
|
+
export function AccentSwatches({ value, onValueChange, label, accents, featured, labelOf, className, ref }) {
|
|
15
|
+
const { t } = useTranslation('ui');
|
|
16
|
+
const isFeatured = (id) => featured.some((entry) => entry === id);
|
|
17
|
+
const colorOf = (id) => accents.find((entry) => entry.id === id)?.color;
|
|
18
|
+
const shown = accents.filter((entry) => isFeatured(entry.id)).sort((a, b) => featured.indexOf(a.id) - featured.indexOf(b.id));
|
|
19
|
+
const hidden = value !== undefined && !isFeatured(value) && colorOf(value) !== undefined ? value : null;
|
|
20
|
+
return (_jsxs("div", { ref: ref, className: clsx('flex items-center gap-2', className), role: "radiogroup", "aria-label": label, children: [shown.map((entry) => (_jsx(Tooltip, { label: labelOf(entry.id), children: _jsx(ColorSwatch, { role: "radio", "aria-checked": value === entry.id, "aria-label": labelOf(entry.id), color: entry.color, picked: value === entry.id, onClick: () => onValueChange(entry.id) }) }, entry.id))), _jsxs(MenuRoot, { children: [_jsx(Tooltip, { label: t('accent.more'), children: _jsx(ColorSwatch, { render: _jsx(MenuTrigger, {}), "aria-label": t('accent.more'), color: hidden === null ? undefined : colorOf(hidden), picked: hidden !== null, children: hidden === null ? _jsx(Icon, { icon: Ellipsis, size: 12 }) : undefined }) }), _jsx(MenuPopup, { align: "end", className: "max-h-96 overflow-y-auto", children: _jsx(MenuRadioGroup, { value: value ?? null, onValueChange: (id) => onValueChange(id), children: accents.map((entry) => (_jsxs(MenuRadioItem, { value: entry.id, children: [_jsx("span", { className: "h-3 w-3 shrink-0 rounded-full", style: { background: entry.color }, "aria-hidden": true }), _jsx("span", { className: "grow", children: labelOf(entry.id) })] }, entry.id))) }) })] })] }));
|
|
21
|
+
}
|
package/dist/Banner.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode, Ref } from 'react';
|
|
2
|
+
import type { LucideIcon } from 'lucide-react';
|
|
3
|
+
export type BannerTone = 'attention' | 'error' | 'neutral';
|
|
4
|
+
export interface BannerProps {
|
|
5
|
+
icon: LucideIcon;
|
|
6
|
+
tone: BannerTone;
|
|
7
|
+
message: ReactNode;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
ref?: Ref<HTMLDivElement>;
|
|
11
|
+
}
|
|
12
|
+
export declare function Banner({ icon, tone, message, children, className, ref }: BannerProps): import("react").JSX.Element;
|
package/dist/Banner.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { Icon } from './Icon.js';
|
|
4
|
+
import { Surface } from './Surface.js';
|
|
5
|
+
const TONE = {
|
|
6
|
+
attention: 'text-status-needs-you',
|
|
7
|
+
error: 'text-status-error',
|
|
8
|
+
neutral: 'text-text-muted'
|
|
9
|
+
};
|
|
10
|
+
/*
|
|
11
|
+
* The strip over a view: one line and the buttons that answer it. It is where anything that waits
|
|
12
|
+
* for a person goes (a conflict, a failure, a request), because a corner toast is read after the fact
|
|
13
|
+
* and a decision has to be where the eyes already are. One at a time: the slot is one row and the
|
|
14
|
+
* caller picks what stands in it.
|
|
15
|
+
*/
|
|
16
|
+
export function Banner({ icon, tone, message, children, className = 'absolute inset-x-0 top-3 z-20', ref }) {
|
|
17
|
+
return (_jsx("div", { ref: ref, className: clsx('pointer-events-auto flex justify-center px-4', className), role: "status", "aria-live": "polite", children: _jsxs(Surface, { className: "flex max-w-[640px] items-center gap-3 rounded-lg px-3 py-2 text-sm text-text", children: [_jsx(Icon, { icon: icon, size: 16, className: clsx('shrink-0', TONE[tone]) }), _jsx("span", { className: "grow", children: message }), children] }) }));
|
|
18
|
+
}
|
package/dist/Button.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'danger-outline' | 'positive' | 'inverse';
|
|
3
|
+
export type ButtonSize = 'xs' | 'sm' | 'md';
|
|
4
|
+
export interface ButtonProps extends ComponentProps<'button'> {
|
|
5
|
+
variant?: ButtonVariant;
|
|
6
|
+
size?: ButtonSize;
|
|
7
|
+
href?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function Button({ variant, size, className, href, type, ref, ...rest }: ButtonProps): import("react").JSX.Element;
|
package/dist/Button.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
const VARIANT = {
|
|
4
|
+
// A filled button darkens under the pointer. Its color is the meaning, so no token of its own for hover.
|
|
5
|
+
primary: 'bg-accent text-accent-text hover:brightness-90 disabled:hover:brightness-100',
|
|
6
|
+
secondary: 'border border-border bg-surface-raised text-text hover:bg-surface-hover disabled:hover:bg-surface-raised',
|
|
7
|
+
ghost: 'text-text-muted hover:bg-surface-hover hover:text-text',
|
|
8
|
+
danger: 'bg-status-error text-accent-text hover:brightness-90 disabled:hover:brightness-100',
|
|
9
|
+
// A step that forgets something rather than destroys it: softer than the filled button of a deletion that cannot come back.
|
|
10
|
+
'danger-outline': 'border border-status-error/35 text-status-error hover:bg-status-error/10 disabled:hover:bg-transparent',
|
|
11
|
+
positive: 'bg-positive text-positive-text hover:brightness-90 disabled:hover:brightness-100',
|
|
12
|
+
// Dark on a light theme, light on a dark one, matching what Apple's and GitHub's sign-in buttons ask for.
|
|
13
|
+
inverse: 'bg-text text-bg hover:opacity-90 disabled:hover:opacity-100'
|
|
14
|
+
};
|
|
15
|
+
/* 28 and 32 pixels are the compact and the normal control; 24 fits a header of 32. */
|
|
16
|
+
const SIZE = {
|
|
17
|
+
xs: 'h-6 px-2',
|
|
18
|
+
sm: 'h-7 px-2.5',
|
|
19
|
+
md: 'h-8 px-3'
|
|
20
|
+
};
|
|
21
|
+
/* Every button with a word in it. A button that is only an icon is an `IconButton`, which is a square, not a label. */
|
|
22
|
+
export function Button({ variant = 'ghost', size = 'md', className, href, type = 'button', ref, ...rest }) {
|
|
23
|
+
const classes = clsx('inline-flex shrink-0 items-center justify-center gap-1.5 rounded-md text-xs font-medium disabled:opacity-50', VARIANT[variant], SIZE[size], className);
|
|
24
|
+
if (href) {
|
|
25
|
+
return (_jsx("a", { ref: ref, className: classes, href: href, target: "_blank", rel: "noreferrer", ...rest }));
|
|
26
|
+
}
|
|
27
|
+
return _jsx("button", { ref: ref, type: type, className: classes, ...rest });
|
|
28
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { useRender } from '@base-ui-components/react/use-render';
|
|
2
|
+
export type ButtonGroupProps = useRender.ComponentProps<'div'>;
|
|
3
|
+
export declare function ButtonGroup({ render, className, ref, ...props }: ButtonGroupProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { useRender } from '@base-ui-components/react/use-render';
|
|
3
|
+
/* Icon buttons that belong together sit 1px apart; groups keep the wider gap of their container. */
|
|
4
|
+
export function ButtonGroup({ render, className, ref, ...props }) {
|
|
5
|
+
return useRender({
|
|
6
|
+
render,
|
|
7
|
+
ref,
|
|
8
|
+
defaultTagName: 'div',
|
|
9
|
+
props: { ...props, className: clsx('inline-flex items-center gap-px', className) }
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Ref } from 'react';
|
|
2
|
+
export interface CheckboxProps {
|
|
3
|
+
checked: boolean;
|
|
4
|
+
onCheckedChange(checked: boolean): void;
|
|
5
|
+
label: string;
|
|
6
|
+
indeterminate?: boolean;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
ref?: Ref<HTMLButtonElement>;
|
|
10
|
+
}
|
|
11
|
+
export declare function Checkbox({ checked, onCheckedChange, label, indeterminate, disabled, className, ref }: CheckboxProps): import("react").JSX.Element;
|
package/dist/Checkbox.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { Check, Minus } from 'lucide-react';
|
|
4
|
+
import { Checkbox as BaseCheckbox } from '@base-ui-components/react/checkbox';
|
|
5
|
+
import { Icon } from './Icon.js';
|
|
6
|
+
/* Picked or not, as one item out of a list or an option that waits for a Save button. */
|
|
7
|
+
export function Checkbox({ checked, onCheckedChange, label, indeterminate = false, disabled, className, ref }) {
|
|
8
|
+
return (_jsx(BaseCheckbox.Root, { ref: ref, checked: checked, indeterminate: indeterminate, onCheckedChange: (next) => onCheckedChange(next), "aria-label": label, disabled: disabled, className: clsx('flex size-4 shrink-0 items-center justify-center rounded-sm border border-border-strong bg-surface text-accent-text transition-colors', 'data-checked:border-accent data-checked:bg-accent data-indeterminate:border-accent data-indeterminate:bg-accent data-disabled:opacity-50', className), children: _jsx(BaseCheckbox.Indicator, { render: (props, state) => (_jsx("span", { ...props, children: _jsx(Icon, { icon: state.indeterminate ? Minus : Check, size: 12 }) })) }) }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ReactNode, type Ref } from 'react';
|
|
2
|
+
import type { LucideIcon } from 'lucide-react';
|
|
3
|
+
export interface Choice<Value extends string | number> {
|
|
4
|
+
value: Value;
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
icon?: LucideIcon;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ChoiceCardsProps<Value extends string | number> {
|
|
11
|
+
value: Value;
|
|
12
|
+
onValueChange(value: Value): void;
|
|
13
|
+
choices: readonly Choice<Value>[];
|
|
14
|
+
label: string;
|
|
15
|
+
orientation?: 'horizontal' | 'vertical';
|
|
16
|
+
columns?: number;
|
|
17
|
+
radio?: 'start' | 'end';
|
|
18
|
+
detail?: ReactNode;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
className?: string;
|
|
21
|
+
ref?: Ref<HTMLDivElement>;
|
|
22
|
+
}
|
|
23
|
+
export declare function ChoiceCards<Value extends string | number>({ value, onValueChange, choices, label, orientation, columns, radio, detail, disabled, className, ref }: ChoiceCardsProps<Value>): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useId } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { Radio } from '@base-ui-components/react/radio';
|
|
5
|
+
import { RadioGroup } from '@base-ui-components/react/radio-group';
|
|
6
|
+
import { Icon } from './Icon.js';
|
|
7
|
+
function ChoiceCard({ choice, radio }) {
|
|
8
|
+
const id = useId();
|
|
9
|
+
// As tall as the title's line, so the dot centers on it.
|
|
10
|
+
const dot = (_jsx("span", { className: "flex h-(--text-sm--line-height) shrink-0 items-center", "aria-hidden": true, children: _jsx("span", { className: "size-4 rounded-full border border-border-strong group-data-checked:border-5 group-data-checked:border-accent group-data-checked:bg-accent-text" }) }));
|
|
11
|
+
return (_jsxs(Radio.Root, { value: choice.value, disabled: choice.disabled, "aria-labelledby": `${id}-title`, "aria-describedby": `${id}-description`, className: "group focus-ring flex min-w-0 items-start gap-3 rounded-xl border border-border bg-surface p-3 text-left hover:bg-surface-hover data-checked:border-accent data-disabled:opacity-50 data-disabled:hover:bg-surface", children: [radio === 'start' && dot, choice.icon && (_jsx("span", { className: "grid size-8 shrink-0 place-items-center rounded-lg bg-surface-sunken text-text-muted group-data-checked:text-accent", children: _jsx(Icon, { icon: choice.icon, size: 16 }) })), _jsxs("span", { className: "flex min-w-0 grow flex-col gap-0.5", children: [_jsx("span", { id: `${id}-title`, className: "text-sm font-medium text-text", children: choice.title }), _jsx("span", { id: `${id}-description`, className: "text-xs text-pretty text-text-muted", children: choice.description })] }), radio === 'end' && dot] }));
|
|
12
|
+
}
|
|
13
|
+
/*
|
|
14
|
+
* One of a few options, each a card that says what it means, for a choice that deserves more than a
|
|
15
|
+
* select: the cards stand side by side at the same width, one under the other or in a set number of
|
|
16
|
+
* columns, and the arrow keys move between them.
|
|
17
|
+
*/
|
|
18
|
+
export function ChoiceCards({ value, onValueChange, choices, label, orientation = 'horizontal', columns, radio = 'end', detail, disabled, className, ref }) {
|
|
19
|
+
const vertical = columns === undefined && orientation === 'vertical';
|
|
20
|
+
return (_jsx(RadioGroup, { ref: ref, value: value, onValueChange: (next) => onValueChange(next), disabled: disabled, "aria-label": label, className: clsx('grid gap-2', columns !== undefined || vertical ? 'grid-flow-row' : 'auto-cols-fr grid-flow-col', className), style: columns !== undefined ? { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` } : undefined, children: choices.map((choice) => (_jsxs(Fragment, { children: [_jsx(ChoiceCard, { choice: choice, radio: radio }), vertical && choice.value === value && detail] }, choice.value))) }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Ref } from 'react';
|
|
2
|
+
import { type IconButtonSize } from './IconButton.tsx';
|
|
3
|
+
import type { Shortcut } from './shortcut.ts';
|
|
4
|
+
export type CloseButtonProps = {
|
|
5
|
+
label: string;
|
|
6
|
+
kbd?: Shortcut | string;
|
|
7
|
+
size?: IconButtonSize;
|
|
8
|
+
className?: string;
|
|
9
|
+
ref?: Ref<HTMLButtonElement>;
|
|
10
|
+
} & ({
|
|
11
|
+
onClick: () => void;
|
|
12
|
+
} | {
|
|
13
|
+
dialog: true;
|
|
14
|
+
});
|
|
15
|
+
export declare function CloseButton({ label, kbd, size, className, ref, ...closes }: CloseButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { X } from 'lucide-react';
|
|
3
|
+
import { Dialog } from '@base-ui-components/react/dialog';
|
|
4
|
+
import { IconButton } from './IconButton.js';
|
|
5
|
+
/* The close button in the header of every panel and dialog. In a dialog it is that dialog's own close part. */
|
|
6
|
+
export function CloseButton({ label, kbd, size, className, ref, ...closes }) {
|
|
7
|
+
if ('dialog' in closes) {
|
|
8
|
+
return _jsx(IconButton, { ref: ref, icon: X, label: label, kbd: kbd, size: size, className: className, render: _jsx(Dialog.Close, {}) });
|
|
9
|
+
}
|
|
10
|
+
return _jsx(IconButton, { ref: ref, icon: X, label: label, kbd: kbd, size: size, className: className, onClick: closes.onClick });
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
import { useRender } from '@base-ui-components/react/use-render';
|
|
3
|
+
declare const PICKED_RING: {
|
|
4
|
+
readonly surface: 'ring-2 ring-accent ring-offset-2 ring-offset-surface';
|
|
5
|
+
readonly popup: 'ring-2 ring-accent ring-offset-1 ring-offset-surface-raised';
|
|
6
|
+
};
|
|
7
|
+
export interface ColorSwatchProps extends ComponentProps<'button'> {
|
|
8
|
+
color?: string;
|
|
9
|
+
picked?: boolean;
|
|
10
|
+
on?: keyof typeof PICKED_RING;
|
|
11
|
+
render?: useRender.RenderProp;
|
|
12
|
+
}
|
|
13
|
+
export declare function ColorSwatch({ color, picked, on, render, className, style, ref, children, ...props }: ColorSwatchProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { Check } from 'lucide-react';
|
|
4
|
+
import { useRender } from '@base-ui-components/react/use-render';
|
|
5
|
+
import { Icon } from './Icon.js';
|
|
6
|
+
/* `focus-ring` for the swatch that is a menu item rather than a button, which the keyboard reaches all the same. */
|
|
7
|
+
const SWATCH = 'focus-ring grid h-6 w-6 place-items-center rounded-full text-accent-text';
|
|
8
|
+
/* The ring of the picked color is offset against whatever the swatch stands on. */
|
|
9
|
+
const PICKED_RING = {
|
|
10
|
+
surface: 'ring-2 ring-accent ring-offset-2 ring-offset-surface',
|
|
11
|
+
popup: 'ring-2 ring-accent ring-offset-1 ring-offset-surface-raised'
|
|
12
|
+
};
|
|
13
|
+
/*
|
|
14
|
+
* One color, drawn as the color itself. Its name belongs in a tooltip, so the circle carries a tick
|
|
15
|
+
* alone when it is picked. Children replace the tick, such as the ellipsis of a "more colors" swatch.
|
|
16
|
+
*/
|
|
17
|
+
export function ColorSwatch({ color, picked = false, on = 'surface', render, className, style, ref, children, ...props }) {
|
|
18
|
+
return useRender({
|
|
19
|
+
render: render ?? _jsx("button", { type: "button" }),
|
|
20
|
+
ref,
|
|
21
|
+
props: {
|
|
22
|
+
...props,
|
|
23
|
+
className: clsx(SWATCH, color === undefined ? 'border border-border-strong text-text-muted' : picked && PICKED_RING[on], className),
|
|
24
|
+
style: color === undefined ? style : { ...style, background: color },
|
|
25
|
+
children: children ?? (picked && _jsx(Icon, { icon: Check, size: 12 }))
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PointerEvent as ReactPointerEvent, Ref } from 'react';
|
|
2
|
+
import type { ColumnEdge } from './useColumnResize.ts';
|
|
3
|
+
export interface ColumnResizeHandleProps {
|
|
4
|
+
from: ColumnEdge;
|
|
5
|
+
onPointerDown(event: ReactPointerEvent<HTMLElement>): void;
|
|
6
|
+
className?: string;
|
|
7
|
+
ref?: Ref<HTMLDivElement>;
|
|
8
|
+
}
|
|
9
|
+
export declare function ColumnResizeHandle({ from, onPointerDown, className, ref }: ColumnResizeHandleProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
const PLACEMENT = {
|
|
4
|
+
left: 'inset-y-0 right-0 w-2 cursor-col-resize',
|
|
5
|
+
right: 'inset-y-0 left-0 w-2 cursor-col-resize',
|
|
6
|
+
top: 'inset-x-0 bottom-0 h-2 cursor-row-resize',
|
|
7
|
+
bottom: 'inset-x-0 top-0 h-2 cursor-row-resize'
|
|
8
|
+
};
|
|
9
|
+
/* An invisible strip over the free edge of a column, so the column needs `relative` and draws its own border. */
|
|
10
|
+
export function ColumnResizeHandle({ from, onPointerDown, className, ref }) {
|
|
11
|
+
return _jsx("div", { ref: ref, className: clsx('absolute z-10', PLACEMENT[from], className), onPointerDown: onPointerDown });
|
|
12
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
export interface DisabledReasonProps {
|
|
3
|
+
reason: string | null;
|
|
4
|
+
children: ReactElement<Record<string, unknown>>;
|
|
5
|
+
}
|
|
6
|
+
export declare function DisabledReason({ reason, children }: DisabledReasonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Tooltip } from './Tooltip.js';
|
|
3
|
+
export function DisabledReason({ reason, children }) {
|
|
4
|
+
if (reason === null) {
|
|
5
|
+
return children;
|
|
6
|
+
}
|
|
7
|
+
return (_jsx(Tooltip, { label: reason, side: "right", children: children }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode, type Ref } from 'react';
|
|
2
|
+
export interface DockShellProps extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
autoHide?: boolean;
|
|
5
|
+
barClassName?: string;
|
|
6
|
+
onHiddenChange?: (hidden: boolean) => void;
|
|
7
|
+
ref?: Ref<HTMLDivElement>;
|
|
8
|
+
}
|
|
9
|
+
export declare function DockShell({ children, autoHide, className, barClassName, onHiddenChange, ref, ...rest }: DockShellProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { Surface } from './Surface.js';
|
|
5
|
+
/* How close to the bottom of the window the pointer has to come before a hidden dock slides back. */
|
|
6
|
+
const REVEAL_ZONE = 120;
|
|
7
|
+
/*
|
|
8
|
+
* The floating bar at the bottom of a canvas-like view, and the one place that knows how to get out
|
|
9
|
+
* of the way. With `autoHide` it waits below the edge until the pointer comes down to it, an open
|
|
10
|
+
* menu keeps it up (it would take its own popup with it), and a keyboard reaches it by tabbing. The
|
|
11
|
+
* buttons stay in the tab order while it is out of sight, so focus brings it back. Something that
|
|
12
|
+
* moves with the dock marks itself `data-holds-dock`, so reaching for it does not hide the dock.
|
|
13
|
+
*/
|
|
14
|
+
export function DockShell({ children, autoHide = false, className, barClassName, onHiddenChange, ref, ...rest }) {
|
|
15
|
+
const barRef = useRef(null);
|
|
16
|
+
const [revealed, setRevealed] = useState(false);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!autoHide) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let pointerNear = false;
|
|
22
|
+
const evaluate = () => {
|
|
23
|
+
const bar = barRef.current;
|
|
24
|
+
const holds = bar !== null && (bar.querySelector('[data-popup-open]') !== null || bar.querySelector(':focus-visible') !== null);
|
|
25
|
+
setRevealed(pointerNear || holds);
|
|
26
|
+
};
|
|
27
|
+
const onMove = (event) => {
|
|
28
|
+
/* Something that moves with the dock would slide away under the pointer reaching for it,
|
|
29
|
+
so over one the dock stays as it is. */
|
|
30
|
+
if (event.target instanceof Element && event.target.closest('[data-holds-dock]') !== null) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
pointerNear = window.innerHeight - event.clientY <= REVEAL_ZONE;
|
|
34
|
+
evaluate();
|
|
35
|
+
};
|
|
36
|
+
window.addEventListener('pointermove', onMove);
|
|
37
|
+
window.addEventListener('focusin', evaluate);
|
|
38
|
+
return () => {
|
|
39
|
+
window.removeEventListener('pointermove', onMove);
|
|
40
|
+
window.removeEventListener('focusin', evaluate);
|
|
41
|
+
setRevealed(false);
|
|
42
|
+
};
|
|
43
|
+
}, [autoHide]);
|
|
44
|
+
const hidden = autoHide && !revealed;
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
onHiddenChange?.(hidden);
|
|
47
|
+
}, [hidden, onHiddenChange]);
|
|
48
|
+
return (_jsx("div", { ref: ref, ...rest, className: clsx('pointer-events-none absolute inset-x-0 bottom-4 flex justify-center', className), children: _jsx(Surface, { ref: barRef, className: clsx('flex items-center gap-2 rounded-xl p-1 transition-[opacity,translate] duration-200', barClassName, hidden ? 'pointer-events-none translate-y-6 opacity-0' : 'pointer-events-auto'), children: children }) }));
|
|
49
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, type Ref } from 'react';
|
|
2
|
+
import type { LucideIcon } from 'lucide-react';
|
|
3
|
+
export interface EmptyStateProps {
|
|
4
|
+
icon?: LucideIcon | ReactElement<{
|
|
5
|
+
size?: number;
|
|
6
|
+
}>;
|
|
7
|
+
busy?: boolean;
|
|
8
|
+
/** @deprecated Turns a Lucide icon; use `busy`, which draws a `Spinner` in its place. */
|
|
9
|
+
spin?: boolean;
|
|
10
|
+
title?: ReactNode;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
action?: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
ref?: Ref<HTMLDivElement>;
|
|
15
|
+
}
|
|
16
|
+
export declare function EmptyState({ icon, busy, spin, title, children, action, className, ref }: EmptyStateProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cloneElement, isValidElement } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { Icon } from './Icon.js';
|
|
5
|
+
import { Spinner } from './Spinner.js';
|
|
6
|
+
// One size in a panel, a card and a dialog alike, so no caller picks its own.
|
|
7
|
+
const ICON_SIZE = 20;
|
|
8
|
+
/* What a list, a canvas or a thread shows before it holds anything. */
|
|
9
|
+
export function EmptyState({ icon, busy = false, spin = false, title, children, action, className, ref }) {
|
|
10
|
+
return (_jsxs("div", { ref: ref, className: clsx('flex flex-col items-center justify-center gap-2 px-6 py-8 text-center', className), children: [(busy || icon) && (_jsx("span", { className: "text-text-faint", children: busy ? (_jsx(Spinner, { size: ICON_SIZE })) : isValidElement(icon) ? (cloneElement(icon, { size: ICON_SIZE })) : (icon && _jsx(Icon, { icon: icon, size: ICON_SIZE, className: spin ? 'animate-spin' : undefined })) })), title && _jsx("p", { className: "text-sm font-medium text-text", children: title }), _jsx("p", { className: "max-w-[280px] text-xs leading-snug text-text-muted", children: children }), action && _jsx("div", { className: "mt-2", children: action })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
|
2
|
+
import { type ResetKeys } from './error-boundary.ts';
|
|
3
|
+
export interface ErrorBoundaryProps {
|
|
4
|
+
label: string;
|
|
5
|
+
resetKeys?: ResetKeys;
|
|
6
|
+
compact?: boolean;
|
|
7
|
+
reload?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
interface ErrorBoundaryState {
|
|
12
|
+
error: unknown;
|
|
13
|
+
failed: boolean;
|
|
14
|
+
componentStack: string | null;
|
|
15
|
+
}
|
|
16
|
+
export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
17
|
+
state: ErrorBoundaryState;
|
|
18
|
+
static getDerivedStateFromError(error: unknown): Partial<ErrorBoundaryState>;
|
|
19
|
+
componentDidCatch(error: unknown, info: ErrorInfo): void;
|
|
20
|
+
componentDidUpdate(previous: ErrorBoundaryProps): void;
|
|
21
|
+
reset(): void;
|
|
22
|
+
render(): ReactNode;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Component } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { Copy, RotateCw, TriangleAlert } from 'lucide-react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { Button } from './Button.js';
|
|
7
|
+
import { ButtonGroup } from './ButtonGroup.js';
|
|
8
|
+
import { copyText } from './clipboard.js';
|
|
9
|
+
import { errorMessageOf, errorReport, shouldReset } from './error-boundary.js';
|
|
10
|
+
import { Icon } from './Icon.js';
|
|
11
|
+
import { IconButton } from './IconButton.js';
|
|
12
|
+
const CLEAR = { error: null, failed: false, componentStack: null };
|
|
13
|
+
/* A function component, so the words come from the i18next instance the app renders with. */
|
|
14
|
+
function ErrorFallback({ label, error, componentStack, compact, reload, className, onRetry }) {
|
|
15
|
+
const { t } = useTranslation('ui');
|
|
16
|
+
return (_jsx("div", { role: "alert", className: clsx('grid place-items-center overflow-auto bg-surface', compact ? 'p-3' : 'p-6', className), children: _jsxs("div", { className: clsx('flex max-w-[360px] flex-col items-center text-center', compact ? 'gap-1' : 'gap-2'), children: [!compact && _jsx(Icon, { icon: TriangleAlert, size: 20, className: "text-status-error" }), _jsx("p", { className: "text-sm font-medium text-text", children: label }), _jsx("p", { className: "line-clamp-4 text-xs break-words text-text-muted", children: errorMessageOf(error) }), _jsxs("div", { className: clsx('flex items-center gap-2', compact ? 'mt-1' : 'mt-2'), children: [_jsx(Button, { variant: "secondary", size: "sm", onClick: onRetry, children: t('action.retry') }), reload && (_jsxs(Button, { variant: "secondary", size: "sm", onClick: () => window.location.reload(), children: [_jsx(Icon, { icon: RotateCw, size: 14 }), t('action.reload')] })), _jsx(ButtonGroup, { children: _jsx(IconButton, { icon: Copy, size: "sm", label: t('action.copyError'), onClick: () => copyText(errorReport(label, error, componentStack)) }) })] })] }) }));
|
|
17
|
+
}
|
|
18
|
+
/*
|
|
19
|
+
* Keeps a render failure to the subtree it happened in. Without one React unmounts the whole tree,
|
|
20
|
+
* and whatever lives in it (an embedded page, a running terminal) goes down with it. Key it so it
|
|
21
|
+
* resets only while it holds an error: `resetKeys` for what the children draw from.
|
|
22
|
+
*/
|
|
23
|
+
export class ErrorBoundary extends Component {
|
|
24
|
+
state = CLEAR;
|
|
25
|
+
static getDerivedStateFromError(error) {
|
|
26
|
+
return { error, failed: true };
|
|
27
|
+
}
|
|
28
|
+
componentDidCatch(error, info) {
|
|
29
|
+
console.error(`${this.props.label}:`, error, info.componentStack);
|
|
30
|
+
this.setState({ componentStack: info.componentStack ?? null });
|
|
31
|
+
}
|
|
32
|
+
componentDidUpdate(previous) {
|
|
33
|
+
if (shouldReset(this.state.failed, previous.resetKeys ?? [], this.props.resetKeys ?? [])) {
|
|
34
|
+
this.reset();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
reset() {
|
|
38
|
+
this.setState(CLEAR);
|
|
39
|
+
}
|
|
40
|
+
render() {
|
|
41
|
+
if (!this.state.failed) {
|
|
42
|
+
return this.props.children;
|
|
43
|
+
}
|
|
44
|
+
const { label, compact = false, reload = false, className = 'absolute inset-0' } = this.props;
|
|
45
|
+
return (_jsx(ErrorFallback, { label: label, error: this.state.error, componentStack: this.state.componentStack, compact: compact, reload: reload, className: className, onRetry: () => this.reset() }));
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/Field.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactNode, type Ref } from 'react';
|
|
2
|
+
import { useRender } from '@base-ui-components/react/use-render';
|
|
3
|
+
export type FieldHintProps = useRender.ComponentProps<'p'>;
|
|
4
|
+
export declare function FieldHint({ render, className, ref, ...props }: FieldHintProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
5
|
+
export type FormErrorProps = useRender.ComponentProps<'p'>;
|
|
6
|
+
export declare function FormError({ render, className, ref, ...props }: FormErrorProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
7
|
+
export interface FieldProps {
|
|
8
|
+
label?: ReactNode;
|
|
9
|
+
hint?: ReactNode;
|
|
10
|
+
error?: ReactNode;
|
|
11
|
+
group?: boolean;
|
|
12
|
+
orientation?: 'vertical' | 'horizontal';
|
|
13
|
+
className?: string;
|
|
14
|
+
ref?: Ref<HTMLDivElement>;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
export declare function Field({ label, hint, error, group, orientation, className, ref, children }: FieldProps): import("react").JSX.Element;
|