@orz/ui 0.0.0-stage → 0.0.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 +29 -0
- package/README.md +541 -2
- package/dist/components/avatar.d.ts +14 -0
- package/dist/components/avatar.d.ts.map +1 -0
- package/dist/components/avatar.js +23 -0
- package/dist/components/avatar.js.map +1 -0
- package/dist/components/badge.d.ts +10 -0
- package/dist/components/badge.d.ts.map +1 -0
- package/dist/components/badge.js +26 -0
- package/dist/components/badge.js.map +1 -0
- package/dist/components/button.d.ts +11 -0
- package/dist/components/button.d.ts.map +1 -0
- package/dist/components/button.js +40 -0
- package/dist/components/button.js.map +1 -0
- package/dist/components/card.d.ts +12 -0
- package/dist/components/card.d.ts.map +1 -0
- package/dist/components/card.js +25 -0
- package/dist/components/card.js.map +1 -0
- package/dist/components/color-picker.d.ts +58 -0
- package/dist/components/color-picker.d.ts.map +1 -0
- package/dist/components/color-picker.js +950 -0
- package/dist/components/color-picker.js.map +1 -0
- package/dist/components/confirm-dialog.d.ts +26 -0
- package/dist/components/confirm-dialog.d.ts.map +1 -0
- package/dist/components/confirm-dialog.js +18 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/dialog.d.ts +26 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.js +149 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/components/dropdown-menu.d.ts +30 -0
- package/dist/components/dropdown-menu.d.ts.map +1 -0
- package/dist/components/dropdown-menu.js +100 -0
- package/dist/components/dropdown-menu.js.map +1 -0
- package/dist/components/filter-menu.d.ts +98 -0
- package/dist/components/filter-menu.d.ts.map +1 -0
- package/dist/components/filter-menu.js +745 -0
- package/dist/components/filter-menu.js.map +1 -0
- package/dist/components/highlighted-input.d.ts +40 -0
- package/dist/components/highlighted-input.d.ts.map +1 -0
- package/dist/components/highlighted-input.js +95 -0
- package/dist/components/highlighted-input.js.map +1 -0
- package/dist/components/hover-card.d.ts +7 -0
- package/dist/components/hover-card.d.ts.map +1 -0
- package/dist/components/hover-card.js +22 -0
- package/dist/components/hover-card.js.map +1 -0
- package/dist/components/input.d.ts +4 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.js +7 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/label.d.ts +5 -0
- package/dist/components/label.d.ts.map +1 -0
- package/dist/components/label.js +8 -0
- package/dist/components/label.js.map +1 -0
- package/dist/components/link-chip.d.ts +67 -0
- package/dist/components/link-chip.d.ts.map +1 -0
- package/dist/components/link-chip.js +71 -0
- package/dist/components/link-chip.js.map +1 -0
- package/dist/components/load-failure.d.ts +24 -0
- package/dist/components/load-failure.d.ts.map +1 -0
- package/dist/components/load-failure.js +27 -0
- package/dist/components/load-failure.js.map +1 -0
- package/dist/components/load-more.d.ts +15 -0
- package/dist/components/load-more.d.ts.map +1 -0
- package/dist/components/load-more.js +25 -0
- package/dist/components/load-more.js.map +1 -0
- package/dist/components/media-controls.d.ts +50 -0
- package/dist/components/media-controls.d.ts.map +1 -0
- package/dist/components/media-controls.js +71 -0
- package/dist/components/media-controls.js.map +1 -0
- package/dist/components/popover.d.ts +9 -0
- package/dist/components/popover.d.ts.map +1 -0
- package/dist/components/popover.js +82 -0
- package/dist/components/popover.js.map +1 -0
- package/dist/components/query-box.d.ts +78 -0
- package/dist/components/query-box.d.ts.map +1 -0
- package/dist/components/query-box.js +332 -0
- package/dist/components/query-box.js.map +1 -0
- package/dist/components/query-highlight.d.ts +14 -0
- package/dist/components/query-highlight.d.ts.map +1 -0
- package/dist/components/query-highlight.js +132 -0
- package/dist/components/query-highlight.js.map +1 -0
- package/dist/components/select.d.ts +19 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/select.js +38 -0
- package/dist/components/select.js.map +1 -0
- package/dist/components/separator.d.ts +5 -0
- package/dist/components/separator.d.ts.map +1 -0
- package/dist/components/separator.js +9 -0
- package/dist/components/separator.js.map +1 -0
- package/dist/components/sheet.d.ts +16 -0
- package/dist/components/sheet.d.ts.map +1 -0
- package/dist/components/sheet.js +41 -0
- package/dist/components/sheet.js.map +1 -0
- package/dist/components/skeleton.d.ts +3 -0
- package/dist/components/skeleton.d.ts.map +1 -0
- package/dist/components/skeleton.js +7 -0
- package/dist/components/skeleton.js.map +1 -0
- package/dist/components/slider.d.ts +8 -0
- package/dist/components/slider.d.ts.map +1 -0
- package/dist/components/slider.js +14 -0
- package/dist/components/slider.js.map +1 -0
- package/dist/components/sonner.d.ts +4 -0
- package/dist/components/sonner.d.ts.map +1 -0
- package/dist/components/sonner.js +26 -0
- package/dist/components/sonner.js.map +1 -0
- package/dist/components/switch.d.ts +5 -0
- package/dist/components/switch.d.ts.map +1 -0
- package/dist/components/switch.js +8 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/table.d.ts +11 -0
- package/dist/components/table.d.ts.map +1 -0
- package/dist/components/table.js +28 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/tabs.d.ts +8 -0
- package/dist/components/tabs.d.ts.map +1 -0
- package/dist/components/tabs.js +18 -0
- package/dist/components/tabs.js.map +1 -0
- package/dist/components/textarea.d.ts +4 -0
- package/dist/components/textarea.d.ts.map +1 -0
- package/dist/components/textarea.js +7 -0
- package/dist/components/textarea.js.map +1 -0
- package/dist/components/theme-editor/draft.d.ts +235 -0
- package/dist/components/theme-editor/draft.d.ts.map +1 -0
- package/dist/components/theme-editor/draft.js +638 -0
- package/dist/components/theme-editor/draft.js.map +1 -0
- package/dist/components/theme-editor/export-panel.d.ts +14 -0
- package/dist/components/theme-editor/export-panel.d.ts.map +1 -0
- package/dist/components/theme-editor/export-panel.js +39 -0
- package/dist/components/theme-editor/export-panel.js.map +1 -0
- package/dist/components/theme-editor/form.d.ts +69 -0
- package/dist/components/theme-editor/form.d.ts.map +1 -0
- package/dist/components/theme-editor/form.js +350 -0
- package/dist/components/theme-editor/form.js.map +1 -0
- package/dist/components/theme-editor/import-panel.d.ts +21 -0
- package/dist/components/theme-editor/import-panel.d.ts.map +1 -0
- package/dist/components/theme-editor/import-panel.js +117 -0
- package/dist/components/theme-editor/import-panel.js.map +1 -0
- package/dist/components/theme-editor/theme-list.d.ts +23 -0
- package/dist/components/theme-editor/theme-list.d.ts.map +1 -0
- package/dist/components/theme-editor/theme-list.js +42 -0
- package/dist/components/theme-editor/theme-list.js.map +1 -0
- package/dist/components/theme-editor/token-list.d.ts +30 -0
- package/dist/components/theme-editor/token-list.d.ts.map +1 -0
- package/dist/components/theme-editor/token-list.js +124 -0
- package/dist/components/theme-editor/token-list.js.map +1 -0
- package/dist/components/theme-editor/tokens.d.ts +37 -0
- package/dist/components/theme-editor/tokens.d.ts.map +1 -0
- package/dist/components/theme-editor/tokens.js +155 -0
- package/dist/components/theme-editor/tokens.js.map +1 -0
- package/dist/components/theme-editor.d.ts +26 -0
- package/dist/components/theme-editor.d.ts.map +1 -0
- package/dist/components/theme-editor.js +467 -0
- package/dist/components/theme-editor.js.map +1 -0
- package/dist/components/theme-menu.d.ts +28 -0
- package/dist/components/theme-menu.d.ts.map +1 -0
- package/dist/components/theme-menu.js +308 -0
- package/dist/components/theme-menu.js.map +1 -0
- package/dist/components/tooltip.d.ts +11 -0
- package/dist/components/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip.js +18 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/components/unsaved-changes-dialog.d.ts +39 -0
- package/dist/components/unsaved-changes-dialog.d.ts.map +1 -0
- package/dist/components/unsaved-changes-dialog.js +46 -0
- package/dist/components/unsaved-changes-dialog.js.map +1 -0
- package/dist/hooks/scroll-insets.d.ts +89 -0
- package/dist/hooks/scroll-insets.d.ts.map +1 -0
- package/dist/hooks/scroll-insets.js +265 -0
- package/dist/hooks/scroll-insets.js.map +1 -0
- package/dist/hooks/use-element-height.d.ts +11 -0
- package/dist/hooks/use-element-height.d.ts.map +1 -0
- package/dist/hooks/use-element-height.js +33 -0
- package/dist/hooks/use-element-height.js.map +1 -0
- package/dist/hooks/use-media-query.d.ts +22 -0
- package/dist/hooks/use-media-query.d.ts.map +1 -0
- package/dist/hooks/use-media-query.js +38 -0
- package/dist/hooks/use-media-query.js.map +1 -0
- package/dist/hooks/use-overlay-scrollbars.d.ts +31 -0
- package/dist/hooks/use-overlay-scrollbars.d.ts.map +1 -0
- package/dist/hooks/use-overlay-scrollbars.js +80 -0
- package/dist/hooks/use-overlay-scrollbars.js.map +1 -0
- package/dist/hooks/use-paged-append.d.ts +7 -0
- package/dist/hooks/use-paged-append.d.ts.map +1 -0
- package/dist/hooks/use-paged-append.js +53 -0
- package/dist/hooks/use-paged-append.js.map +1 -0
- package/dist/hooks/use-query-completion.d.ts +27 -0
- package/dist/hooks/use-query-completion.d.ts.map +1 -0
- package/dist/hooks/use-query-completion.js +100 -0
- package/dist/hooks/use-query-completion.js.map +1 -0
- package/dist/hooks/use-slash-shortcut.d.ts +9 -0
- package/dist/hooks/use-slash-shortcut.d.ts.map +1 -0
- package/dist/hooks/use-slash-shortcut.js +38 -0
- package/dist/hooks/use-slash-shortcut.js.map +1 -0
- package/dist/hooks/viewport-chrome.d.ts +50 -0
- package/dist/hooks/viewport-chrome.d.ts.map +1 -0
- package/dist/hooks/viewport-chrome.js +170 -0
- package/dist/hooks/viewport-chrome.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cell-width-table.d.ts +3 -0
- package/dist/lib/cell-width-table.d.ts.map +1 -0
- package/dist/lib/cell-width-table.js +6 -0
- package/dist/lib/cell-width-table.js.map +1 -0
- package/dist/lib/cell-width.d.ts +7 -0
- package/dist/lib/cell-width.d.ts.map +1 -0
- package/dist/lib/cell-width.js +50 -0
- package/dist/lib/cell-width.js.map +1 -0
- package/dist/lib/clipboard.d.ts +8 -0
- package/dist/lib/clipboard.d.ts.map +1 -0
- package/dist/lib/clipboard.js +19 -0
- package/dist/lib/clipboard.js.map +1 -0
- package/dist/lib/color.d.ts +40 -0
- package/dist/lib/color.d.ts.map +1 -0
- package/dist/lib/color.js +177 -0
- package/dist/lib/color.js.map +1 -0
- package/dist/lib/scroll-map.d.ts +20 -0
- package/dist/lib/scroll-map.d.ts.map +1 -0
- package/dist/lib/scroll-map.js +46 -0
- package/dist/lib/scroll-map.js.map +1 -0
- package/dist/lib/text-metrics.d.ts +33 -0
- package/dist/lib/text-metrics.d.ts.map +1 -0
- package/dist/lib/text-metrics.js +89 -0
- package/dist/lib/text-metrics.js.map +1 -0
- package/dist/lib/theme-file.d.ts +51 -0
- package/dist/lib/theme-file.d.ts.map +1 -0
- package/dist/lib/theme-file.js +511 -0
- package/dist/lib/theme-file.js.map +1 -0
- package/dist/lib/unsaved-guard.d.ts +33 -0
- package/dist/lib/unsaved-guard.d.ts.map +1 -0
- package/dist/lib/unsaved-guard.js +37 -0
- package/dist/lib/unsaved-guard.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/theme/custom.d.ts +8 -0
- package/dist/theme/custom.d.ts.map +1 -0
- package/dist/theme/custom.js +56 -0
- package/dist/theme/custom.js.map +1 -0
- package/dist/theme/index.d.ts +6 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +5 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/paint-guard.d.ts +20 -0
- package/dist/theme/paint-guard.d.ts.map +1 -0
- package/dist/theme/paint-guard.js +120 -0
- package/dist/theme/paint-guard.js.map +1 -0
- package/dist/theme/picks.d.ts +22 -0
- package/dist/theme/picks.d.ts.map +1 -0
- package/dist/theme/picks.js +43 -0
- package/dist/theme/picks.js.map +1 -0
- package/dist/theme/pref.d.ts +39 -0
- package/dist/theme/pref.d.ts.map +1 -0
- package/dist/theme/pref.js +129 -0
- package/dist/theme/pref.js.map +1 -0
- package/dist/theme/provider.d.ts +41 -0
- package/dist/theme/provider.d.ts.map +1 -0
- package/dist/theme/provider.js +103 -0
- package/dist/theme/provider.js.map +1 -0
- package/dist/theme/store.d.ts +104 -0
- package/dist/theme/store.d.ts.map +1 -0
- package/dist/theme/store.js +509 -0
- package/dist/theme/store.js.map +1 -0
- package/package.json +94 -3
- package/styles.css +79 -0
- package/theme.css +186 -0
- package/vendor/shadcn-tailwind.css +664 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 the orzui team
|
|
4
|
+
Copyright 2026 the todou team
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,542 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @orz/ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React components, hooks and a theme store, built on shadcn's radix-nova primitives and Tailwind
|
|
4
|
+
CSS v4. The theme data itself lives in `@orz/themes`.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm add @orz/ui react react-dom radix-ui lucide-react sonner
|
|
10
|
+
# only for @orz/ui/hooks/use-overlay-scrollbars:
|
|
11
|
+
pnpm add overlayscrollbars
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`radix-ui`, `sonner`, `lucide-react` and React are peer dependencies. They have to be the app's
|
|
15
|
+
own copies, because radix's dismissable-layer and focus stacks and sonner's toast store are module
|
|
16
|
+
state. If a second copy sat beside the app's, Escape and outside clicks would stop working between
|
|
17
|
+
a dialog from one copy and a menu from the other, and `toast()` would not reach `<Toaster />`.
|
|
18
|
+
|
|
19
|
+
## CSS
|
|
20
|
+
|
|
21
|
+
```css
|
|
22
|
+
@import "tailwindcss";
|
|
23
|
+
@import "@orz/ui/styles.css";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`styles.css` brings the baseline tokens, the base layer, `tw-animate-css` and shadcn's
|
|
27
|
+
Tailwind variants. It also registers `@source "./dist"` on its own, so Tailwind finds the classes
|
|
28
|
+
used inside the package without any `@source` line from you.
|
|
29
|
+
|
|
30
|
+
If you keep your own copy of the tokens and do not import `styles.css`, point Tailwind at the
|
|
31
|
+
package yourself:
|
|
32
|
+
|
|
33
|
+
```css
|
|
34
|
+
@source "../node_modules/@orz/ui/dist";
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`styles.css` is `theme.css` plus the page-level rules: the base layer (`* { border-color }`,
|
|
38
|
+
`body` colours, `html` font), the overlayscrollbars theme, `revealBlock`'s `.anchor-flash` and
|
|
39
|
+
`@source "./dist"`. A surface that
|
|
40
|
+
shares a document with someone else's UI, and must not restyle it, imports
|
|
41
|
+
`@orz/ui/theme.css` instead: the tokens, `.dark`, `tw-animate-css` and shadcn's variants, nothing
|
|
42
|
+
global. `theme.css` carries no `@source`, so that surface names the files Tailwind scans itself.
|
|
43
|
+
|
|
44
|
+
`styles.css` does not load a font. `--font-sans` names `'Geist Variable'` first and falls back to
|
|
45
|
+
the system UI font, so install `@fontsource-variable/geist` (or define `--font-sans`) if you want it.
|
|
46
|
+
`useOverlayScrollbars` needs the library's own stylesheet, which you import yourself:
|
|
47
|
+
`@import "overlayscrollbars/overlayscrollbars.css";`.
|
|
48
|
+
|
|
49
|
+
## Theme
|
|
50
|
+
|
|
51
|
+
Importing anything from `@orz/ui` has no side effects. Creating a theme store does: it reads the
|
|
52
|
+
stored pref, paints it, and starts following the OS and other tabs. The store takes its themes as
|
|
53
|
+
input, so an app ships only the themes it passes.
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { builtinThemes } from "@orz/themes";
|
|
57
|
+
import { Toaster } from "@orz/ui/components/sonner";
|
|
58
|
+
import { createThemeStore, ThemeProvider } from "@orz/ui/theme";
|
|
59
|
+
|
|
60
|
+
const themes = createThemeStore({
|
|
61
|
+
storageKey: "app-theme",
|
|
62
|
+
themes: builtinThemes,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
createRoot(root).render(
|
|
66
|
+
<ThemeProvider store={themes}>
|
|
67
|
+
<App />
|
|
68
|
+
<Toaster />
|
|
69
|
+
</ThemeProvider>,
|
|
70
|
+
);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The store has two slots, and an app enables either or both by handing over a list for it:
|
|
74
|
+
|
|
75
|
+
- `themes` is the UI slot. Its pick paints the page.
|
|
76
|
+
- `terminalThemes` is the terminal slot. It may also hold terminal-only themes from
|
|
77
|
+
`@orz/themes`. Without a terminal pick, the terminal follows the UI pick: same theme, color
|
|
78
|
+
scheme and variant.
|
|
79
|
+
|
|
80
|
+
A store with only a UI slot still writes the UI pick's terminal tokens, so an ANSI-escape renderer
|
|
81
|
+
anywhere on the page works without a slot of its own. A store with only a terminal slot writes the
|
|
82
|
+
terminal tokens alone and leaves `color-scheme`, `.dark` and `data-theme` to the page.
|
|
83
|
+
|
|
84
|
+
The pick is one theme, a mode and an optional variant per slot:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
store.setPref({
|
|
88
|
+
ui: { theme: "github", mode: "light" },
|
|
89
|
+
terminal: { theme: "dracula", mode: "dark", variant: "amoled" },
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- `mode` is `"system"`, `"light"` or `"dark"`. On a theme with one palette it is always that
|
|
94
|
+
palette's side.
|
|
95
|
+
- `variant` names one variant for the whole theme. A palette that does not define it shows its
|
|
96
|
+
default, so AMOLED Solarized under `system` is default Solarized Light by day and AMOLED Solarized
|
|
97
|
+
Dark at night.
|
|
98
|
+
- Every read and write normalizes the pref against the current lists: an unknown theme falls back
|
|
99
|
+
to the first theme of the list the app passed, under `system`, or to following for a terminal
|
|
100
|
+
pick.
|
|
101
|
+
- `setThemes(list)` and `setTerminalThemes(list)` replace those lists at runtime. A pick whose theme
|
|
102
|
+
leaves the list comes back when the theme returns.
|
|
103
|
+
|
|
104
|
+
The store paints through one `<style data-orz-theme>` element in `<head>`, holding a single
|
|
105
|
+
`:root { … }` rule rendered from the data. It also toggles `.dark` and sets `data-theme`,
|
|
106
|
+
`data-variant` and `data-terminal-theme` on the root. Given a `root` other than `<html>`, the store
|
|
107
|
+
keeps an element of its own, sets `data-orz-theme-root` on that root to an id unique to the store,
|
|
108
|
+
and scopes the rule to it, so stores on different roots never repaint each other. The rule has
|
|
109
|
+
plain `:root` specificity, so a stylesheet loaded later, or a `:root:root` selector, can still
|
|
110
|
+
retune a token for every theme.
|
|
111
|
+
|
|
112
|
+
- `useTheme()` returns `{ pref, themes, theme, colorScheme, variant, setPref }`: the UI slot's
|
|
113
|
+
normalized pref and list, the resolved theme object, the color scheme on screen, and the variant
|
|
114
|
+
that side shows. Outside a provider it reports the default theme, light, and ignores `setPref`.
|
|
115
|
+
- `useTerminalTheme()` returns `{ following, themes, theme, colorScheme, variant, tokens }` for the
|
|
116
|
+
terminal slot.
|
|
117
|
+
- Both also return `preview`, `customThemes` and `setCustomThemes` (below). Outside a provider the
|
|
118
|
+
list is empty and the two calls are ignored.
|
|
119
|
+
- `useThemeSlots()` returns `{ ui, terminal }`: which slots the store has. The two hooks fall back
|
|
120
|
+
where theirs is missing, so they cannot tell you.
|
|
121
|
+
|
|
122
|
+
### Theme menu
|
|
123
|
+
|
|
124
|
+
`<ThemeMenu />` from `@orz/ui/components/theme-menu` is a ready-made picker: an icon button opening
|
|
125
|
+
a filter box, an Auto / Light / Dark switch, and the themes, each with its variants in a submenu.
|
|
126
|
+
Whatever is highlighted previews on the whole page after resting 80 ms, and a pick commits it.
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
<ThemeMenu /> // the UI pick
|
|
130
|
+
<ThemeMenu slot="terminal" /> // the terminal pick, e.g. on a terminal pane
|
|
131
|
+
<ThemeMenu onCustomize={(id, slot) => openEditor(id, slot)} />
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- `slot` is `"ui"` (default) or `"terminal"`. A picker whose slot the store lacks renders nothing.
|
|
135
|
+
The terminal picker adds a "Same as UI" row and a Terminal only group.
|
|
136
|
+
- `onCustomize` adds a "Custom themes…" row, which calls it with no id, and an edit button on each
|
|
137
|
+
custom theme's row, which calls it with that theme's id. The second argument is the menu's slot,
|
|
138
|
+
so a new theme opened from the terminal menu is a terminal theme; a callback that takes only the
|
|
139
|
+
id still fits. The app brings the editor, such as `ThemeEditor` below.
|
|
140
|
+
|
|
141
|
+
`trigger` replaces the icon button, for a toolbar with buttons of its own look. It gets the icon
|
|
142
|
+
the default button would show (it follows the mode in the UI picker) and the menu's name, "Theme"
|
|
143
|
+
or "Terminal theme", which the element should carry as its accessible name. The element goes
|
|
144
|
+
through Radix's `PopoverTrigger asChild`, so it must take a ref and spread the props it is given
|
|
145
|
+
onto a DOM element.
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
<ThemeMenu
|
|
149
|
+
trigger={({ Icon, label }) => (
|
|
150
|
+
<button type="button" aria-label={label} title={label} className="…">
|
|
151
|
+
<Icon className="size-3.5" />
|
|
152
|
+
</button>
|
|
153
|
+
)}
|
|
154
|
+
/>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Custom themes
|
|
158
|
+
|
|
159
|
+
The store keeps the user's custom themes from `@orz/themes` in one list of two kinds, and lists the
|
|
160
|
+
ones that compose ahead of each slot's own list, in list order:
|
|
161
|
+
|
|
162
|
+
- a `CustomTheme` (no `kind`) is composed with `composeTheme` and goes into both slots. In a store with no UI slot it stays in the list and shows nowhere;
|
|
163
|
+
- a `CustomTerminalTheme` (`kind: "terminal"`), made of terminal colours alone, is composed with
|
|
164
|
+
`composeTerminalTheme` and goes into the terminal slot only. In a store with no terminal slot it
|
|
165
|
+
stays in the list and shows nowhere.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const { customThemes, setCustomThemes } = useTheme();
|
|
169
|
+
setCustomThemes([...customThemes, draft]);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- `getCustomThemes()` / `setCustomThemes(list)` on the store, typed `AnyCustomTheme`.
|
|
173
|
+
`setCustomThemes` throws on an id that repeats across both kinds or that a theme in either list
|
|
174
|
+
already has.
|
|
175
|
+
- `setThemes` and `setTerminalThemes` replace the app's own lists; the custom themes are composed
|
|
176
|
+
again against them.
|
|
177
|
+
- A custom theme whose bases are gone stays in `getCustomThemes()` but out of the lists, so an
|
|
178
|
+
editor can still show and repair it.
|
|
179
|
+
- The list is stored as JSON under `customStorageKey` (default `` `${storageKey}-custom` ``) and
|
|
180
|
+
follows other tabs. Entries that do not have the shape of either kind, or carry a `kind` this
|
|
181
|
+
version does not know, are dropped on read, and the key is only written by `setCustomThemes`.
|
|
182
|
+
- `persistCustomThemes: false` keeps the store away from that key altogether. The app then loads its
|
|
183
|
+
list with `setCustomThemes` and saves it from `subscribe` and `getCustomThemes`, for example to its
|
|
184
|
+
server.
|
|
185
|
+
- `persistPref: false` does the same for the pref under `storageKey`: the store never writes the
|
|
186
|
+
pref and ignores other tabs' writes to it. The app then commits picks itself with `setPref`, for
|
|
187
|
+
example from a URL. Until it does, the store shows the pick the paint cache last painted (the
|
|
188
|
+
first theme when there is none), so it does not repaint over the [paint guard](#paint-guard).
|
|
189
|
+
An app whose source holds no pick, such as a bare URL, commits its default with `setPref` rather
|
|
190
|
+
than relying on the store's. The paint cache and the custom themes are unaffected.
|
|
191
|
+
|
|
192
|
+
### Custom theme editor
|
|
193
|
+
|
|
194
|
+
`<ThemeEditor />` from `@orz/ui/components/theme-editor` is the editor `ThemeMenu`'s
|
|
195
|
+
`onCustomize` opens: a panel docked to the right, with the page left visible and repainted with the
|
|
196
|
+
draft as it changes. Each side of a theme is based on a theme from the store's lists with
|
|
197
|
+
overrides, fully custom, or absent; its terminal colours follow the base or are edited the same
|
|
198
|
+
way.
|
|
199
|
+
|
|
200
|
+
A store with a terminal slot also gets terminal themes, in a layout of their own: Light and Dark
|
|
201
|
+
tabs, each side None, based on a palette (with its scheme and variant) or fully custom, over the
|
|
202
|
+
terminal colours. A terminal theme with one side serves both modes. A terminal-only store edits
|
|
203
|
+
terminal themes only; a store with both slots lists the two kinds in two groups, and **+ New** asks
|
|
204
|
+
which to make.
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
const [editing, setEditing] = useState<{ id?: string; slot: "ui" | "terminal" } | null>(null);
|
|
208
|
+
|
|
209
|
+
<ThemeMenu onCustomize={(id, slot) => setEditing({ id, slot })} />
|
|
210
|
+
<ThemeMenu slot="terminal" onCustomize={(id, slot) => setEditing({ id, slot })} />
|
|
211
|
+
<ThemeEditor
|
|
212
|
+
open={editing !== null}
|
|
213
|
+
themeId={editing?.id}
|
|
214
|
+
kind={editing?.slot}
|
|
215
|
+
onOpenChange={(open) => { if (!open) setEditing(null); }}
|
|
216
|
+
/>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- `themeId` opens that custom theme; without one, with an id the store does not hold, or with a
|
|
220
|
+
theme of a kind the store cannot show, the editor starts a new theme from what the page shows (a
|
|
221
|
+
terminal theme from what the terminal shows).
|
|
222
|
+
- `kind` is the kind of that new theme. Unset, or naming a slot the store lacks, it is a UI theme
|
|
223
|
+
when the store has a UI slot, else a terminal theme.
|
|
224
|
+
- It reads and writes the store's custom list (`customThemes` / `setCustomThemes`), so the themes
|
|
225
|
+
are stored wherever the store keeps that list: under `customStorageKey`, or wherever the app saves
|
|
226
|
+
it with `persistCustomThemes: false`. A kind the store cannot show is left out of the editor and
|
|
227
|
+
written back untouched. Save also makes a UI theme the UI pick, and a terminal theme the terminal
|
|
228
|
+
pick, leaving the UI pick alone.
|
|
229
|
+
- `onOpenChange(false)` comes after Save, or after the unsaved-changes prompt resolves to Discard.
|
|
230
|
+
While the draft has unsaved edits it is registered with `@orz/ui/lib/unsaved-guard`, so the app's
|
|
231
|
+
leave-page guard covers it.
|
|
232
|
+
|
|
233
|
+
Import… and Export carry themes as JSON files (`format: "orz-themes"`, `version: 1`). UI themes go
|
|
234
|
+
in `themes`, always written; terminal themes in `terminalThemes`, written only when there is one. An
|
|
235
|
+
entry is a custom theme without its id or kind, and every based side or terminal also carries the
|
|
236
|
+
colours it showed at export, so a theme whose base the importing app lacks arrives as fully custom
|
|
237
|
+
instead of being lost. Import follows the store's slots: a terminal theme is refused where there is
|
|
238
|
+
no terminal, and a UI theme arrives in a terminal-only store as a terminal theme made from the
|
|
239
|
+
terminal colours each side shows. `serializeThemeFile`, `parseThemeFile` and `uniqueLabel` in `@orz/ui/lib/theme-file` read and
|
|
240
|
+
write the format with no React or DOM, for example to validate uploads on a server.
|
|
241
|
+
|
|
242
|
+
### Preview
|
|
243
|
+
|
|
244
|
+
`preview(next)` shows a pick without committing it, and `preview(null)` shows the committed pick
|
|
245
|
+
again. It is what `ThemeMenu` calls on a highlight.
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
store.preview({ ui: { theme: "dracula", mode: "dark" } });
|
|
249
|
+
store.preview({ terminal: "follow" }); // the terminal following the UI
|
|
250
|
+
store.preview({ ui: { theme: composeTheme(draft, themes), mode } }); // an unsaved theme
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
- A slot left out of `next` shows its committed pick. A theme passed as an object need not be in
|
|
254
|
+
any list; for `ui` it must be a UI theme (`isUiTheme`), or `preview` throws.
|
|
255
|
+
- It paints exactly what `setPref` paints, and writes neither `storageKey` nor the paint cache, so
|
|
256
|
+
other tabs and a reload see only the committed pick.
|
|
257
|
+
- While it is active, `getResolved()`, `getTerminal()` and the hooks report what is shown, so
|
|
258
|
+
everything themed follows the preview; `getPref()` and the hooks' `pref` stay the committed pick.
|
|
259
|
+
- `setPref` and `dispose` clear it. A previewed theme that leaves its list falls back to the
|
|
260
|
+
committed pick.
|
|
261
|
+
|
|
262
|
+
### Terminals
|
|
263
|
+
|
|
264
|
+
The terminal tokens are CSS variables (`--terminal-background`, `--terminal-foreground`,
|
|
265
|
+
`--terminal-cursor`, `--terminal-selection`, and `--ansi-black` through `--ansi-bright-white`), and
|
|
266
|
+
Tailwind classes such as `bg-terminal-background` and `text-ansi-red`. An ANSI-escape renderer can
|
|
267
|
+
use the classes directly. xterm.js takes the resolved tokens:
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
import { xtermTheme } from "@orz/themes";
|
|
271
|
+
import { useTerminalTheme } from "@orz/ui/theme";
|
|
272
|
+
|
|
273
|
+
const { tokens } = useTerminalTheme();
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
terminal.options.theme = xtermTheme(tokens);
|
|
276
|
+
}, [terminal, tokens]);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Storage
|
|
280
|
+
|
|
281
|
+
`storageKey` holds the pref as JSON:
|
|
282
|
+
`{"ui":{"theme":"github","mode":"light"},"terminal":{"theme":"dracula","mode":"dark"}}`. A default
|
|
283
|
+
variant and a following terminal are left out.
|
|
284
|
+
|
|
285
|
+
Prefs stored by earlier versions of `@orz/ui` are read and rewritten in this shape on the next
|
|
286
|
+
write. A plain slug such as `solarized-dark` becomes `{ theme: "solarized", mode: "dark" }`, and
|
|
287
|
+
`system`, `light` and `dark` map onto the `default` theme. An app that keeps its old `storageKey`
|
|
288
|
+
keeps its users' themes.
|
|
289
|
+
|
|
290
|
+
When the app keeps the pick on its server, keep `persistPref` on and hand the server's answer to
|
|
291
|
+
`setPref` when it arrives. Until then the store shows the copy in `storageKey`, so a reload paints
|
|
292
|
+
the last pick at once, and the server's answer repaints only when another device changed it. Save
|
|
293
|
+
the user's picks from `subscribe` and `getPref`. `persistPref: false` is for an app whose pick must
|
|
294
|
+
not have a second source, such as one held in the URL.
|
|
295
|
+
|
|
296
|
+
### Paint guard
|
|
297
|
+
|
|
298
|
+
Without a guard, a reload in a dark theme flashes white before the bundle runs. The store caches
|
|
299
|
+
the declarations it painted under `paintKey` (default `` `${storageKey}-paint` ``), one per side the
|
|
300
|
+
slot's mode can show. `themePaintGuardScript()` returns an inline script that replays them before
|
|
301
|
+
the bundle loads, choosing the side from the OS when both are cached, so neither the guard nor the
|
|
302
|
+
page needs a theme list. The store adopts the guard's `<style>` element when it starts. Give the
|
|
303
|
+
guard the same keys as the store:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
// vite.config.ts
|
|
307
|
+
import { themePaintGuardScript } from "@orz/ui/theme";
|
|
308
|
+
|
|
309
|
+
export default defineConfig({
|
|
310
|
+
plugins: [
|
|
311
|
+
{
|
|
312
|
+
name: "theme-paint-guard",
|
|
313
|
+
transformIndexHtml: () => [
|
|
314
|
+
{
|
|
315
|
+
tag: "script",
|
|
316
|
+
children: themePaintGuardScript({ storageKey: "app-theme" }),
|
|
317
|
+
injectTo: "head-prepend",
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
});
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
On the first load after an upgrade there is no cache yet. The guard then sets `.dark` from the
|
|
326
|
+
`` `${storageKey}-kind` `` value earlier versions stored, and the store writes the cache.
|
|
327
|
+
|
|
328
|
+
Under a UI pick, the guard's rule also sets `background-color: var(--background)` on `<html>`, so
|
|
329
|
+
frames painted before the stylesheet loads already show the theme's background. The store's rule
|
|
330
|
+
leaves it out, so once the store starts, `body`'s background reaches the canvas as usual.
|
|
331
|
+
|
|
332
|
+
A page that cannot run an inline script, under a Content Security Policy without
|
|
333
|
+
`'unsafe-inline'` for example, can have its server paint the background instead. Given
|
|
334
|
+
`paintCookie: "app-theme-paint"`, the store also writes the committed UI pick's background for each
|
|
335
|
+
side it can show to that cookie (path `/`, one year, `SameSite=Lax`). `themePaintCookieStyle(value)`
|
|
336
|
+
turns the cookie's value, URL-encoded or not, into CSS for a `<style>` in `<head>`: the background
|
|
337
|
+
and `color-scheme` on `html`, with the dark side under `@media (prefers-color-scheme: dark)` when
|
|
338
|
+
both are stored. A side whose colour could break out of the rule is dropped, and a missing or
|
|
339
|
+
malformed value gives `""`.
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
// a server template
|
|
343
|
+
const css = themePaintCookieStyle(request.cookies["app-theme-paint"]);
|
|
344
|
+
html`<head><style>${css}</style>…</head>`;
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Query input
|
|
348
|
+
|
|
349
|
+
A search box that colours its own query and completes it, driven by an `@orz/query` schema.
|
|
350
|
+
`@orz/query` is a dependency of this package, so it is installed with it; import the schema
|
|
351
|
+
helpers from it directly.
|
|
352
|
+
|
|
353
|
+
| Import | Contents |
|
|
354
|
+
|---|---|
|
|
355
|
+
| `@orz/ui/components/highlighted-input` | `HighlightedInput`: a one-line input whose text is painted by a mirror layer underneath, so it can be coloured while selection, undo, autofill and IME composition stay native |
|
|
356
|
+
| `@orz/ui/components/query-highlight` | `highlightQuery(text, parts, schema, providers)`: the spans `HighlightedInput` paints, with keys, punctuation and values coloured by verdict |
|
|
357
|
+
| `@orz/ui/hooks/use-query-completion` | `useQueryCompletion({ schema, value, caret, providers, enabled })`: key and value completions at the caret; a `search` provider is asked after 150 ms of quiet, and its answers are cached per prefix |
|
|
358
|
+
| `@orz/ui/components/query-box` | `QueryBox`: the three above as a combobox, with the app's own rows (history, jumps) as `RowSource`s |
|
|
359
|
+
|
|
360
|
+
```tsx
|
|
361
|
+
import { defineQuerySchema } from "@orz/query";
|
|
362
|
+
import { QueryBox } from "@orz/ui/components/query-box";
|
|
363
|
+
import { SearchIcon } from "lucide-react";
|
|
364
|
+
|
|
365
|
+
const schema = defineQuerySchema({
|
|
366
|
+
qualifiers: {
|
|
367
|
+
is: { value: { kind: "enum", values: ["open", "closed"] }, hint: "open or closed" },
|
|
368
|
+
label: { value: { kind: "named", source: "label" }, hint: "a label name" },
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
<QueryBox
|
|
373
|
+
schema={schema}
|
|
374
|
+
providers={{ label: { options: labels?.map((name) => ({ value: name })) } }}
|
|
375
|
+
value={value}
|
|
376
|
+
onValueChange={setValue}
|
|
377
|
+
onSubmit={(query) => navigate(`/search?q=${encodeURIComponent(query)}`)}
|
|
378
|
+
renderSubmitRow={(query, option) => <div {...option}>Search for “{query}”</div>}
|
|
379
|
+
leading={<SearchIcon />}
|
|
380
|
+
inputProps={{ "aria-label": "Search", placeholder: "Search…" }}
|
|
381
|
+
/>;
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Nothing in the panel is preselected, so Enter submits the query as typed unless the reader arrowed
|
|
385
|
+
onto a row. Tab takes the highlighted completion, or the first one. Escape closes the panel, and a
|
|
386
|
+
second Escape goes to `onEscape`. Pass `options: undefined` for a list that has not loaded: its
|
|
387
|
+
values paint as valid rather than flashing amber, and `onOpenChange` tells you when the box is in
|
|
388
|
+
use, so loading can wait until then.
|
|
389
|
+
|
|
390
|
+
## Filter menu
|
|
391
|
+
|
|
392
|
+
`@orz/ui/components/filter-menu` exports `FilterMenu`: a popover with a filter box over a list of
|
|
393
|
+
sections, for a pick from more rows than a menu should make anyone scroll through. Focus stays in the
|
|
394
|
+
box, so typing always filters; ↑↓, Home and End move a highlight that `aria-activedescendant`
|
|
395
|
+
follows, Enter picks, Escape closes, and Tab closes and moves on. The caller does the filtering and
|
|
396
|
+
passes the sections in, so it decides what a match means (aliases, ids, other fields);
|
|
397
|
+
`matchText(text, query)` and `MatchedText` fold case and diacritics, and mark the hit.
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
import { FilterMenu, MatchedText, matchText } from "@orz/ui/components/filter-menu";
|
|
401
|
+
|
|
402
|
+
const [query, setQuery] = useState("");
|
|
403
|
+
const rows = FRUIT.filter((fruit) => matchText(fruit.label, query));
|
|
404
|
+
|
|
405
|
+
<FilterMenu
|
|
406
|
+
query={query}
|
|
407
|
+
onQueryChange={setQuery}
|
|
408
|
+
sections={[{ id: "fruit", label: "Fruit", rows }]}
|
|
409
|
+
selectedId={fruit}
|
|
410
|
+
onSelect={(row) => setFruit(row.id)}
|
|
411
|
+
renderRow={(row) => <MatchedText text={row.label} query={query} />}
|
|
412
|
+
inputLabel="Filter fruit"
|
|
413
|
+
>
|
|
414
|
+
<Button variant="outline">{label}</Button>
|
|
415
|
+
</FilterMenu>;
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
On opening, the query is cleared and the highlight rests on `selectedId`. `onHighlight(row)` reports
|
|
419
|
+
each highlight the user moves to, and `onHighlight(null)` a return to the committed state (the
|
|
420
|
+
highlight resting on `selectedId`, the pointer leaving the list, the menu closing), which is what a
|
|
421
|
+
live preview needs. A control in `header` that commits at once puts the highlight back through the
|
|
422
|
+
`ref`'s `resetHighlight()`.
|
|
423
|
+
|
|
424
|
+
Rows can carry a submenu: `getSubmenu(row)` returns its rows (ids unique across the whole menu), and
|
|
425
|
+
`selectedSubmenuId` checks one of them. The submenu opens beside a row the user highlights, by
|
|
426
|
+
pointer or keys, but not for the highlight resting on `selectedId` or on the query's first hit. →
|
|
427
|
+
with the caret at the end of the query enters it, ← and Escape come back, and `onHighlight` and
|
|
428
|
+
`onSelect` receive its rows like any other. The pointer can cut across other rows on its way to the
|
|
429
|
+
submenu without moving the highlight. A row with a submenu is a split button: clicking the row picks
|
|
430
|
+
it, and clicking its chevron, past the divider, opens the submenu instead. `submenuLabel(row)` names
|
|
431
|
+
the submenu's listbox.
|
|
432
|
+
|
|
433
|
+
## Colour picker
|
|
434
|
+
|
|
435
|
+
`ColorPicker` is the panel, rendered inline; `ColorPickerPopover` puts it behind a trigger that
|
|
436
|
+
shows the value, or behind your own trigger passed as `children`. Both come from
|
|
437
|
+
`@orz/ui/components/color-picker`. The colour maths they use is `@orz/ui/lib/color`: `parseColor`,
|
|
438
|
+
`formatColor`, `gamutOf`, `mapToSrgb`, `maxChroma` and `contrastRatio`, on oklch values.
|
|
439
|
+
`maxChroma(l, h, gamut)` is the largest chroma at a lightness and hue that stays inside `"srgb"` or
|
|
440
|
+
`"p3"`.
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
import { ColorPickerPopover } from "@orz/ui/components/color-picker";
|
|
444
|
+
|
|
445
|
+
<ColorPickerPopover
|
|
446
|
+
value={token}
|
|
447
|
+
onValueChange={preview}
|
|
448
|
+
onValueCommit={save}
|
|
449
|
+
format="oklch"
|
|
450
|
+
contrastWith={background}
|
|
451
|
+
contrastLabel="background"
|
|
452
|
+
swatches={[{ label: "This theme", colors: tokens }]}
|
|
453
|
+
/>;
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
- `value` is any CSS colour. The panel follows it, except that the parent echoing the panel's own
|
|
457
|
+
output changes nothing, so the panel keeps full precision between strings.
|
|
458
|
+
- `onValueChange` fires on every drag frame and every keystroke that parses; `onValueCommit` when
|
|
459
|
+
an interaction ends (pointer release, a keyboard step, Enter or blur, a swatch, the eyedropper).
|
|
460
|
+
Preview on change, save on commit.
|
|
461
|
+
- The output keeps the incoming value's format when that is oklch, hex, rgb or hsl, and is oklch
|
|
462
|
+
otherwise; `format` pins it. hex, rgb and hsl cannot leave sRGB, so they are gamut-mapped.
|
|
463
|
+
`alpha={false}` makes every output opaque.
|
|
464
|
+
- Three picking surfaces, all at the current hue:
|
|
465
|
+
- `oklch`: lightness up, and across, chroma as a share of the largest chroma the `gamut` holds at
|
|
466
|
+
that lightness, so every point is a colour inside it. A colour beyond the gamut sits pinned to
|
|
467
|
+
the right edge with a dashed ring, and keeps its value until the area is touched.
|
|
468
|
+
- `absolute`: lightness up, and across, chroma itself, the axis fitted to the hue's Display P3
|
|
469
|
+
wedge. The sRGB and P3 edges are drawn, and past P3 is a faded veil. It is the surface for exact
|
|
470
|
+
chroma and for colours outside sRGB.
|
|
471
|
+
- `hsv`: the HSV square.
|
|
472
|
+
|
|
473
|
+
The default is `oklch` when the output is oklch and `hsv` otherwise; `surfaces` limits them, and
|
|
474
|
+
`surface` with `onSurfaceChange` controls the choice if you want to persist it.
|
|
475
|
+
- `gamut` (`"srgb"`, the default, or `"p3"`) bounds the `oklch` surface and the chroma lock.
|
|
476
|
+
- `resetValue` shows a reset button beside the text field that commits that value. The prop is not
|
|
477
|
+
`defaultValue`, which React reads as an uncontrolled input's initial value.
|
|
478
|
+
- The chroma lock, a toggle beside the hue slider on the oklch surfaces, keeps chroma's share of
|
|
479
|
+
the gamut's maximum while the hue slider or the L and H fields move, so a vivid colour stays
|
|
480
|
+
vivid instead of falling out of the gamut. `chromaLock` / `defaultChromaLock` /
|
|
481
|
+
`onChromaLockChange` control it.
|
|
482
|
+
- `extra` renders at the bottom of the panel, for a consumer's own controls.
|
|
483
|
+
|
|
484
|
+
Importing either entry point brings in [culori](https://culorijs.org) (about 9 KB gzipped, only the
|
|
485
|
+
modes the picker uses); nothing else in `@orz/ui` does.
|
|
486
|
+
|
|
487
|
+
## Media controls
|
|
488
|
+
|
|
489
|
+
`MediaControls`, from `@orz/ui/components/media-controls`, is the control bar under a media frame:
|
|
490
|
+
play, time, seek, volume, speed, picture-in-picture and fullscreen, in that order. It plays nothing
|
|
491
|
+
itself; the player passes the values in and acts on the callbacks. `@orz/ui-asciinema`'s cast
|
|
492
|
+
player and `@orz/ui-content`'s `VideoPlayer` and `AudioPlayer` draw it.
|
|
493
|
+
|
|
494
|
+
```tsx
|
|
495
|
+
import { MediaControls } from "@orz/ui/components/media-controls";
|
|
496
|
+
|
|
497
|
+
<MediaControls
|
|
498
|
+
playing={playing}
|
|
499
|
+
time={time}
|
|
500
|
+
duration={duration}
|
|
501
|
+
speed={speed}
|
|
502
|
+
onToggle={toggle}
|
|
503
|
+
onSeek={seek}
|
|
504
|
+
onSpeed={setSpeed}
|
|
505
|
+
buffered={[[0, 42]]}
|
|
506
|
+
volume={{ level, muted, onLevel: setLevel, onMute: toggleMute }}
|
|
507
|
+
fullscreen={{ active: fullscreen, onToggle: toggleFullscreen }}
|
|
508
|
+
portal={fullscreen ? frame : undefined}
|
|
509
|
+
/>;
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
- `time` and `duration` are seconds. A `duration` that is not finite, as a recorded WebM reports
|
|
513
|
+
until it has been read to the end, disables the seek track and shows the time alone.
|
|
514
|
+
- Dragging the seek track moves only the time shown; `onSeek` fires once, on release, and a value
|
|
515
|
+
within a step of the end snaps to the end.
|
|
516
|
+
- `markers` are ticks over the track that seek to their time; `buffered` ranges are shaded behind
|
|
517
|
+
the played range.
|
|
518
|
+
- `volume`, `fullscreen` and `pictureInPicture` are each one object of value and handler; leave one
|
|
519
|
+
out and its control is not drawn. `onLevel` only reports a level: whether dragging a muted
|
|
520
|
+
volume unmutes is the player's call.
|
|
521
|
+
- From 24rem wide, volume is a mute button beside a level slider; below that, a single button opens
|
|
522
|
+
a popover with a vertical slider and the mute button. A container query makes the switch, so
|
|
523
|
+
nothing is measured.
|
|
524
|
+
- `portal` is where the popover, the speed list and the tooltips render: the frame, while it is
|
|
525
|
+
fullscreen, or they would open outside it. `MEDIA_SPEEDS` lists the speeds offered, and a speed
|
|
526
|
+
outside it joins the list. `formatTime(seconds, long?)` writes `m:ss`, or `h:mm:ss`.
|
|
527
|
+
|
|
528
|
+
## Entry points
|
|
529
|
+
|
|
530
|
+
| Import | Contents |
|
|
531
|
+
|---|---|
|
|
532
|
+
| `@orz/ui` | `cn` |
|
|
533
|
+
| `@orz/ui/components/<name>` | `avatar`, `badge`, `button`, `card`, `color-picker`, `confirm-dialog`, `dialog`, `dropdown-menu`, `filter-menu`, `highlighted-input`, `hover-card`, `input`, `label`, `link-chip`, `load-failure`, `load-more`, `media-controls`, `popover`, `query-box`, `query-highlight`, `select`, `separator`, `sheet`, `skeleton`, `sonner`, `switch`, `table`, `tabs`, `textarea`, `theme-editor`, `theme-menu`, `tooltip`, `unsaved-changes-dialog` |
|
|
534
|
+
| `@orz/ui/hooks/<name>` | `scroll-insets`, `use-element-height`, `use-media-query`, `use-overlay-scrollbars`, `use-paged-append`, `use-query-completion`, `use-slash-shortcut` |
|
|
535
|
+
| `@orz/ui/lib/<name>` | `cell-width`, `clipboard`, `color`, `scroll-map`, `text-metrics`, `theme-file`, `unsaved-guard`, `utils` |
|
|
536
|
+
| `@orz/ui/theme` | `createThemeStore`, `ThemeProvider`, `useTheme`, `useTerminalTheme`, `useThemeSlots`, `themePaintGuardScript`, `normalizePref`, `parseStoredPref`, types (the theme data types re-exported from `@orz/themes`) |
|
|
537
|
+
| `@orz/ui/styles.css` | baseline tokens (the `default` theme), base layer |
|
|
538
|
+
| `@orz/ui/theme.css` | baseline tokens and custom variants, without the base layer or @source |
|
|
539
|
+
|
|
540
|
+
`UnsavedChangesDialog` only renders the prompt, and the blocker belongs to your router. Its JSDoc
|
|
541
|
+
shows the TanStack Router wiring. Register the work that can be lost with `useDirtySource()` from
|
|
542
|
+
`@orz/ui/lib/unsaved-guard`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Avatar as AvatarPrimitive } from "radix-ui";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
declare function Avatar({ className, size, shape, ...props }: React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
|
4
|
+
size?: "default" | "sm" | "lg";
|
|
5
|
+
/** Square for marks that are not a person; people stay round. */
|
|
6
|
+
shape?: "circle" | "square";
|
|
7
|
+
}): React.JSX.Element;
|
|
8
|
+
declare function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>): React.JSX.Element;
|
|
9
|
+
declare function AvatarFallback({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Fallback>): React.JSX.Element;
|
|
10
|
+
declare function AvatarBadge({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
|
|
11
|
+
declare function AvatarGroup({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
12
|
+
declare function AvatarGroupCount({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
13
|
+
export { Avatar, AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage, };
|
|
14
|
+
//# sourceMappingURL=avatar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../src/components/avatar.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,eAAe,EAAE,MAAM,UAAU,CAAC;AACrD,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAIpC,iBAAS,MAAM,CAAC,EACd,SAAS,EACT,IAAgB,EAChB,KAAgB,EAChB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,GAAG;IACrD,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI,CAAC;IAC/B,iEAAiE;IACjE,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;CAC7B,qBAcA;AAED,iBAAS,WAAW,CAAC,EACnB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,KAAK,CAAC,qBAWpD;AAED,iBAAS,cAAc,CAAC,EACtB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,QAAQ,CAAC,qBAWvD;AAED,iBAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,qBAczE;AAED,iBAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWxE;AAED,iBAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAW7B;AAED,OAAO,EACL,MAAM,EACN,WAAW,EACX,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,WAAW,GACZ,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Avatar as AvatarPrimitive } from "radix-ui";
|
|
3
|
+
import { cn } from "../lib/utils.js";
|
|
4
|
+
function Avatar({ className, size = "default", shape = "circle", ...props }) {
|
|
5
|
+
return (_jsx(AvatarPrimitive.Root, { "data-slot": "avatar", "data-size": size, "data-shape": shape, className: cn("group/avatar relative flex size-8 shrink-0 select-none after:absolute after:inset-0 after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten", "data-[shape=circle]:rounded-full data-[shape=circle]:after:rounded-full data-[shape=square]:rounded-md data-[shape=square]:after:rounded-md", className), ...props }));
|
|
6
|
+
}
|
|
7
|
+
function AvatarImage({ className, ...props }) {
|
|
8
|
+
return (_jsx(AvatarPrimitive.Image, { "data-slot": "avatar-image", className: cn("aspect-square size-full rounded-[inherit] object-cover", className), ...props }));
|
|
9
|
+
}
|
|
10
|
+
function AvatarFallback({ className, ...props }) {
|
|
11
|
+
return (_jsx(AvatarPrimitive.Fallback, { "data-slot": "avatar-fallback", className: cn("flex size-full items-center justify-center rounded-[inherit] bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs", className), ...props }));
|
|
12
|
+
}
|
|
13
|
+
function AvatarBadge({ className, ...props }) {
|
|
14
|
+
return (_jsx("span", { "data-slot": "avatar-badge", className: cn("absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none", "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden", "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", className), ...props }));
|
|
15
|
+
}
|
|
16
|
+
function AvatarGroup({ className, ...props }) {
|
|
17
|
+
return (_jsx("div", { "data-slot": "avatar-group", className: cn("group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background", className), ...props }));
|
|
18
|
+
}
|
|
19
|
+
function AvatarGroupCount({ className, ...props }) {
|
|
20
|
+
return (_jsx("div", { "data-slot": "avatar-group-count", className: cn("relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", className), ...props }));
|
|
21
|
+
}
|
|
22
|
+
export { Avatar, AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage, };
|
|
23
|
+
//# sourceMappingURL=avatar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"avatar.js","sourceRoot":"","sources":["../../src/components/avatar.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,IAAI,eAAe,EAAE,MAAM,UAAU,CAAC;AAGrD,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAC;AAErC,SAAS,MAAM,CAAC,EACd,SAAS,EACT,IAAI,GAAG,SAAS,EAChB,KAAK,GAAG,QAAQ,EAChB,GAAG,KAAK,EAKT;IACC,OAAO,CACL,KAAC,eAAe,CAAC,IAAI,iBACT,QAAQ,eACP,IAAI,gBACH,KAAK,EACjB,SAAS,EAAE,EAAE,CACX,uNAAuN,EACvN,6IAA6I,EAC7I,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,EACnB,SAAS,EACT,GAAG,KAAK,EAC2C;IACnD,OAAO,CACL,KAAC,eAAe,CAAC,KAAK,iBACV,cAAc,EACxB,SAAS,EAAE,EAAE,CACX,wDAAwD,EACxD,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,EACtB,SAAS,EACT,GAAG,KAAK,EAC8C;IACtD,OAAO,CACL,KAAC,eAAe,CAAC,QAAQ,iBACb,iBAAiB,EAC3B,SAAS,EAAE,EAAE,CACX,yIAAyI,EACzI,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAgC;IACxE,OAAO,CACL,4BACY,cAAc,EACxB,SAAS,EAAE,EAAE,CACX,0KAA0K,EAC1K,+EAA+E,EAC/E,2FAA2F,EAC3F,+EAA+E,EAC/E,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACvE,OAAO,CACL,2BACY,cAAc,EACxB,SAAS,EAAE,EAAE,CACX,qGAAqG,EACrG,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,GAAG,KAAK,EACoB;IAC5B,OAAO,CACL,2BACY,oBAAoB,EAC9B,SAAS,EAAE,EAAE,CACX,0VAA0V,EAC1V,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAC;AACJ,CAAC;AAED,OAAO,EACL,MAAM,EACN,WAAW,EACX,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,WAAW,GACZ,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type VariantProps } from "class-variance-authority";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
declare const badgeVariants: (props?: ({
|
|
4
|
+
variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | undefined;
|
|
5
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
+
declare function Badge({ className, variant, asChild, ...props }: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & {
|
|
7
|
+
asChild?: boolean;
|
|
8
|
+
}): React.JSX.Element;
|
|
9
|
+
export { Badge, badgeVariants };
|
|
10
|
+
//# sourceMappingURL=badge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"badge.d.ts","sourceRoot":"","sources":["../../src/components/badge.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAElE,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAIpC,QAAA,MAAM,aAAa;;8EAsBlB,CAAC;AAEF,iBAAS,KAAK,CAAC,EACb,SAAS,EACT,OAAmB,EACnB,OAAe,EACf,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,GAC7B,YAAY,CAAC,OAAO,aAAa,CAAC,GAAG;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,qBAW3D;AAED,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC"}
|