@orochibraru/ui 1.0.0-canary.2 → 1.0.1-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -3
- package/dist/components/blocks/accent-toggle.svelte +7 -3
- package/dist/components/blocks/accent-toggle.svelte.d.ts +1 -1
- package/dist/components/blocks/bottom-nav.svelte +1 -1
- package/dist/components/blocks/command-palette.svelte +268 -0
- package/dist/components/blocks/command-palette.svelte.d.ts +37 -0
- package/dist/components/blocks/error-page.svelte +23 -7
- package/dist/components/blocks/error-page.svelte.d.ts +4 -0
- package/dist/components/blocks/fab.svelte +1 -1
- package/dist/components/blocks/install-prompt.svelte +14 -8
- package/dist/components/blocks/install-prompt.svelte.d.ts +2 -1
- package/dist/components/blocks/music-player/labelled-slider.svelte +3 -12
- package/dist/components/blocks/music-player/labelled-slider.svelte.d.ts +1 -2
- package/dist/components/blocks/music-player.svelte +13 -12
- package/dist/components/blocks/otp-field.svelte +77 -33
- package/dist/components/blocks/otp-field.svelte.d.ts +7 -0
- package/dist/components/blocks/password-input.svelte +56 -16
- package/dist/components/blocks/password-input.svelte.d.ts +3 -0
- package/dist/components/blocks/stepper/stepper-phone-header.svelte +93 -0
- package/dist/components/blocks/stepper/stepper-phone-header.svelte.d.ts +13 -0
- package/dist/components/blocks/stepper.svelte +175 -0
- package/dist/components/blocks/stepper.svelte.d.ts +30 -0
- package/dist/components/blocks/time-range-picker.svelte +229 -0
- package/dist/components/blocks/time-range-picker.svelte.d.ts +33 -0
- package/dist/components/blocks/time-series-chart/data-table.svelte +43 -0
- package/dist/components/blocks/time-series-chart/data-table.svelte.d.ts +15 -0
- package/dist/components/blocks/time-series-chart/legend.svelte +23 -0
- package/dist/components/blocks/time-series-chart/legend.svelte.d.ts +10 -0
- package/dist/components/blocks/time-series-chart/marks.svelte +118 -0
- package/dist/components/blocks/time-series-chart/marks.svelte.d.ts +29 -0
- package/dist/components/blocks/time-series-chart/messages.d.ts +13 -0
- package/dist/components/blocks/time-series-chart/messages.js +12 -0
- package/dist/components/blocks/time-series-chart/readout.svelte +44 -0
- package/dist/components/blocks/time-series-chart/readout.svelte.d.ts +15 -0
- package/dist/components/blocks/time-series-chart/scale.d.ts +24 -0
- package/dist/components/blocks/time-series-chart/scale.js +136 -0
- package/dist/components/blocks/time-series-chart/time.d.ts +7 -0
- package/dist/components/blocks/time-series-chart/time.js +96 -0
- package/dist/components/blocks/time-series-chart.svelte +492 -0
- package/dist/components/blocks/time-series-chart.svelte.d.ts +18 -0
- package/dist/components/blocks/top-loading-bar.svelte +1 -1
- package/dist/components/blocks/video-player/center-controls.svelte +7 -7
- package/dist/components/blocks/video-player/control-bar.svelte +41 -35
- package/dist/components/blocks/video-player/control-button.svelte +1 -1
- package/dist/components/blocks/video-player/option-menu.svelte +1 -1
- package/dist/components/blocks/video-player/scrub-bar.svelte +39 -12
- package/dist/components/blocks/video-player/scrub-bar.svelte.d.ts +2 -0
- package/dist/components/blocks/video-player.svelte +2 -2
- package/dist/components/kit/accordion.svelte +70 -0
- package/dist/components/kit/accordion.svelte.d.ts +37 -0
- package/dist/components/kit/avatar-group.svelte +84 -0
- package/dist/components/kit/avatar-group.svelte.d.ts +14 -0
- package/dist/components/kit/bar-list.svelte +104 -0
- package/dist/components/kit/bar-list.svelte.d.ts +22 -0
- package/dist/components/kit/breadcrumbs.svelte +102 -0
- package/dist/components/kit/breadcrumbs.svelte.d.ts +14 -0
- package/dist/components/kit/checkbox-list.svelte +98 -0
- package/dist/components/kit/checkbox-list.svelte.d.ts +15 -0
- package/dist/components/kit/checkbox.svelte +84 -0
- package/dist/components/kit/checkbox.svelte.d.ts +8 -0
- package/dist/components/kit/confirm-dialog.svelte +127 -0
- package/dist/components/kit/confirm-dialog.svelte.d.ts +19 -0
- package/dist/components/kit/data-table.svelte +280 -0
- package/dist/components/kit/data-table.svelte.d.ts +46 -0
- package/dist/components/kit/date-field.svelte +210 -0
- package/dist/components/kit/date-field.svelte.d.ts +14 -0
- package/dist/components/kit/field-frame.svelte +71 -0
- package/dist/components/kit/field-frame.svelte.d.ts +26 -0
- package/dist/components/kit/fieldset-frame.svelte +69 -0
- package/dist/components/kit/fieldset-frame.svelte.d.ts +14 -0
- package/dist/components/kit/file-dropzone/files.d.ts +14 -0
- package/dist/components/kit/file-dropzone/files.js +69 -0
- package/dist/components/kit/file-dropzone.svelte +253 -0
- package/dist/components/kit/file-dropzone.svelte.d.ts +25 -0
- package/dist/components/kit/form.svelte +335 -0
- package/dist/components/kit/form.svelte.d.ts +61 -0
- package/dist/components/kit/input.svelte +73 -0
- package/dist/components/kit/input.svelte.d.ts +9 -0
- package/dist/components/kit/menu.svelte +163 -0
- package/dist/components/kit/menu.svelte.d.ts +28 -0
- package/dist/components/kit/pagination.svelte +134 -0
- package/dist/components/kit/pagination.svelte.d.ts +17 -0
- package/dist/components/kit/radio-list.svelte +131 -0
- package/dist/components/kit/radio-list.svelte.d.ts +15 -0
- package/dist/components/kit/segmented-control/segment.svelte +47 -0
- package/dist/components/kit/segmented-control/segment.svelte.d.ts +11 -0
- package/dist/components/kit/segmented-control.svelte +70 -0
- package/dist/components/kit/segmented-control.svelte.d.ts +16 -0
- package/dist/components/kit/select.svelte +244 -0
- package/dist/components/kit/select.svelte.d.ts +26 -0
- package/dist/components/kit/status-badge.svelte +54 -0
- package/dist/components/kit/status-badge.svelte.d.ts +31 -0
- package/dist/components/kit/switch.svelte +84 -0
- package/dist/components/kit/switch.svelte.d.ts +8 -0
- package/dist/components/kit/tabs.svelte +86 -0
- package/dist/components/kit/tabs.svelte.d.ts +20 -0
- package/dist/components/kit/textarea.svelte +92 -0
- package/dist/components/kit/textarea.svelte.d.ts +11 -0
- package/dist/components/ui/drawer/drawer-content.svelte +1 -1
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/components/ui/sonner/sonner.svelte +2 -4
- package/dist/mcp-content.json +1 -1
- package/dist/mcp.js +11 -11
- package/dist/server/hooks.d.ts +9 -0
- package/dist/server/hooks.js +41 -0
- package/dist/server/logger.d.ts +39 -0
- package/dist/server/logger.js +144 -0
- package/dist/server/telemetry.d.ts +23 -0
- package/dist/server/telemetry.js +82 -0
- package/dist/theme.css +18 -6
- package/package.json +40 -1
package/README.md
CHANGED
|
@@ -55,6 +55,66 @@ spinner, switch, table, tabs, textarea, toggle, toggle-group, tooltip.
|
|
|
55
55
|
`input` turns password managers off on fields that don't set `autocomplete`, so
|
|
56
56
|
a search box never gets a login suggestion.
|
|
57
57
|
|
|
58
|
+
## Components
|
|
59
|
+
|
|
60
|
+
Under `@orochibraru/ui/components/<name>`: one import, data in. Where a
|
|
61
|
+
primitive is assembled from parts, a component takes an array or an object and
|
|
62
|
+
renders the whole thing, phones included.
|
|
63
|
+
|
|
64
|
+
```svelte
|
|
65
|
+
<script lang="ts">
|
|
66
|
+
import Input from "@orochibraru/ui/components/input";
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<Input label="Email" name="email" type="email" required bind:value={email} />
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Every form component takes the same field props: `name`, `label`, `description`,
|
|
73
|
+
`error` (a string or the array a form action returns), `required`, `disabled`,
|
|
74
|
+
`id` and a bindable `value` (`checked` for `checkbox` and `switch`, `files` for
|
|
75
|
+
`file-dropzone`). The label, the description or the error, and the ARIA wiring
|
|
76
|
+
between them come with it.
|
|
77
|
+
|
|
78
|
+
`form` renders them from an array and posts to a form action. A failed action
|
|
79
|
+
that returns `fail(400, { errors: { field: ["message"] } })` puts each message
|
|
80
|
+
under its field; a toast narrates the submit:
|
|
81
|
+
|
|
82
|
+
```svelte
|
|
83
|
+
<Form
|
|
84
|
+
action="?/create"
|
|
85
|
+
fields={[
|
|
86
|
+
{ name: "name", label: "Project name", required: true },
|
|
87
|
+
{ name: "role", type: "select", label: "Role", options: roles },
|
|
88
|
+
{ name: "notify", type: "switch", label: "Notify the team" },
|
|
89
|
+
]}
|
|
90
|
+
messages={{ submit: "Create project", success: "Project created." }}
|
|
91
|
+
/>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| Component | What it does |
|
|
95
|
+
| ------------------- | ---------------------------------------------------------------------------------------------- |
|
|
96
|
+
| `accordion` | Questions and answers from an array: plain text or a snippet per item. |
|
|
97
|
+
| `avatar-group` | Overlapping avatars with initials, a +N count and names in tooltips. |
|
|
98
|
+
| `bar-list` | A ranked list with proportional bars, links and a show-more toggle. |
|
|
99
|
+
| `breadcrumbs` | A trail of pages from an array, collapsing the middle into a menu on phones. |
|
|
100
|
+
| `checkbox` | One checkbox with its label and description, the whole row tappable. |
|
|
101
|
+
| `checkbox-list` | Options in, a labelled group of checkboxes out, posting every checked value. |
|
|
102
|
+
| `confirm-dialog` | Ask before acting: waits on an async confirm, a drawer on phones. |
|
|
103
|
+
| `data-table` | Columns and rows in, a sortable table out: cards on phones, links per row. |
|
|
104
|
+
| `date-field` | A date with its label: calendar in a popover, a drawer on phones, ISO value. |
|
|
105
|
+
| `file-dropzone` | Drop or pick files, folders too: type and size limits, a list to review, posted with the form. |
|
|
106
|
+
| `form` | Fields from an array, laid out and posted to a form action, with errors under each field. |
|
|
107
|
+
| `input` | A text field with its label, description and error in one: passwords get a reveal button. |
|
|
108
|
+
| `menu` | Actions from an array: a dropdown on desktop, a drawer of big rows on phones. |
|
|
109
|
+
| `pagination` | Page numbers from a total and a page size, prev and next on phones. |
|
|
110
|
+
| `radio-list` | Pick one option from a list or from cards, with a legend and errors. |
|
|
111
|
+
| `segmented-control` | A single choice as equal segments that fold to icons when space runs out. |
|
|
112
|
+
| `select` | Options in, a labelled select out: searchable, and a drawer on phones. |
|
|
113
|
+
| `status-badge` | A status pill with a tone, a dot and an optional pulse for live states. |
|
|
114
|
+
| `switch` | A labelled switch row that posts true when on. |
|
|
115
|
+
| `tabs` | Tabs from an array, scrolling sideways when they run out of room. |
|
|
116
|
+
| `textarea` | A multi-line field with its label, error and an optional character counter. |
|
|
117
|
+
|
|
58
118
|
## Blocks
|
|
59
119
|
|
|
60
120
|
Under `@orochibraru/ui/blocks/<name>`:
|
|
@@ -64,6 +124,7 @@ Under `@orochibraru/ui/blocks/<name>`:
|
|
|
64
124
|
| `accent-toggle` | The accent presets: swatch strip on desktop, labelled rows on phones. |
|
|
65
125
|
| `auth-shell` | Centered sign-in layout: heading, optional `brand` and `footer` snippets. |
|
|
66
126
|
| `bottom-nav` | Phone tab bar of top-level destinations, the current one lit. |
|
|
127
|
+
| `command-palette` | ⌘K search over pages and actions: dialog on desktop, tall drawer on phones. |
|
|
67
128
|
| `copy-field` | Monospace value with a copy button and a promise toast. |
|
|
68
129
|
| `document-title` | `<title>` from the page title, suffixed with `appName`. |
|
|
69
130
|
| `empty-state` | Icon, title, subtitle and an action slot for an empty list. |
|
|
@@ -80,6 +141,7 @@ Under `@orochibraru/ui/blocks/<name>`:
|
|
|
80
141
|
| `responsive-sheet` | A dialog on desktop, a bottom drawer under 768px. |
|
|
81
142
|
| `setting-switch` | Labelled switch row for settings forms, posting `name=true` when on. |
|
|
82
143
|
| `stat-card` | Glass card with an icon, a value and a hint, optionally a link. |
|
|
144
|
+
| `stepper` | Multi-step flow: markers on desktop, progress bar and current step on phones. |
|
|
83
145
|
| `tab-nav` | Pill tab bar of links, the current page's tab highlighted. |
|
|
84
146
|
| `tab-section` | Layout for a tabbed section: tabs on desktop, nav cards on phones. |
|
|
85
147
|
| `theme-picker` | System, light and dark as radio cards, for an appearance page. |
|
|
@@ -129,6 +191,16 @@ them, which apps put in their root layout.
|
|
|
129
191
|
- `@orochibraru/ui/password`: `passwordRules` and `passwordScore`, the rules a
|
|
130
192
|
new password must pass and a 0 to 4 strength, the same on the form and the
|
|
131
193
|
server. `new-password-fields` renders them; `ruleLabels` translates them.
|
|
194
|
+
- `@orochibraru/ui/server/logger`: `Logger` writes scoped lines as coloured text
|
|
195
|
+
or JSON lines (`LOG_FORMAT=json`) at `LOG_LEVEL` or above; `configureLogging`
|
|
196
|
+
sets both in code. JSON lines carry the active trace and span ids.
|
|
197
|
+
- `@orochibraru/ui/server/telemetry`: `Telemetry` starts OpenTelemetry tracing
|
|
198
|
+
over OTLP from `instrumentation.server.ts`; with `tracing: { server: true }`
|
|
199
|
+
in the kit options, SvelteKit's own spans go with it. Its SDK packages are
|
|
200
|
+
optional peer dependencies, installed only by apps that trace.
|
|
201
|
+
- `@orochibraru/ui/server/hooks`: `requestLog` logs one line per request and
|
|
202
|
+
`unexpectedErrors` logs a crash, records it on the span and returns its trace
|
|
203
|
+
id as the error id.
|
|
132
204
|
- `@orochibraru/ui/haptics`: `haptic("tap" | "success" | "error")`, a no-op
|
|
133
205
|
where vibration isn't supported.
|
|
134
206
|
|
|
@@ -148,8 +220,8 @@ On `<html>`:
|
|
|
148
220
|
|
|
149
221
|
- `class="dark"` switches to the dark palette, `data-dark-variant="amoled"`
|
|
150
222
|
makes it pure black.
|
|
151
|
-
- `data-accent` picks a preset: `bordeaux`, `green`,
|
|
152
|
-
`orange`, `amber`, `cyan` or `neutral`.
|
|
223
|
+
- Bordeaux is the default. `data-accent` picks a preset: `bordeaux`, `green`,
|
|
224
|
+
`blue`, `violet`, `rose`, `orange`, `amber`, `cyan` or `neutral`.
|
|
153
225
|
- `data-font` (`system`, `serif`, `mono`) and `data-corners="square"` change the
|
|
154
226
|
type and the radius.
|
|
155
227
|
|
|
@@ -168,13 +240,22 @@ bun install
|
|
|
168
240
|
bun run dev
|
|
169
241
|
```
|
|
170
242
|
|
|
171
|
-
The dev server is the docs site: a page per block,
|
|
243
|
+
The dev server is the docs site: a page per block, component and module, with
|
|
172
244
|
the component live and the code that renders it. That code is the example file
|
|
173
245
|
itself, its `#lib` imports rewritten to the package subpaths, so the two can't
|
|
174
246
|
drift apart. A file named `<slug>.<name>` next to an example is a companion,
|
|
175
247
|
shown after it: the layout, action or page an app needs for what the preview
|
|
176
248
|
can't hold. An example without a script is a note for the preview only.
|
|
177
249
|
|
|
250
|
+
Components built mostly for phones (drawers, steppers, cards, the media players)
|
|
251
|
+
also show on their page inside an iPhone or Android frame: an iframe of a bare
|
|
252
|
+
preview route at the device's real width, so phone layouts and drawers behave as
|
|
253
|
+
they would on the device.
|
|
254
|
+
|
|
255
|
+
The primitives are shadcn-svelte's components, so the site links to its docs
|
|
256
|
+
instead of repeating them; llms.txt and the MCP server still carry their usage
|
|
257
|
+
code. The privacy page lists what the site stores and loads, and the credits.
|
|
258
|
+
|
|
178
259
|
Every component page can be copied as Markdown, viewed as Markdown, exported as
|
|
179
260
|
a PDF (through the browser's print dialog, in the light theme), or opened in
|
|
180
261
|
Claude or ChatGPT. The list pages filter by category.
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
const defaultMessages = {
|
|
18
18
|
label: "Accent",
|
|
19
|
-
blue: "Blue",
|
|
20
19
|
bordeaux: "Bordeaux",
|
|
20
|
+
blue: "Blue",
|
|
21
21
|
green: "Green",
|
|
22
22
|
violet: "Violet",
|
|
23
23
|
rose: "Rose",
|
|
@@ -33,11 +33,15 @@
|
|
|
33
33
|
messages?: Partial<typeof defaultMessages>;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
let {
|
|
36
|
+
let {
|
|
37
|
+
value = $bindable("bordeaux"),
|
|
38
|
+
onchange,
|
|
39
|
+
messages = {},
|
|
40
|
+
}: Props = $props();
|
|
37
41
|
|
|
38
42
|
const accents: Accent[] = [
|
|
39
|
-
"blue",
|
|
40
43
|
"bordeaux",
|
|
44
|
+
"blue",
|
|
41
45
|
"green",
|
|
42
46
|
"violet",
|
|
43
47
|
"rose",
|
|
@@ -3,8 +3,8 @@ declare const AccentToggle: import("svelte").Component<{
|
|
|
3
3
|
onchange?: (accent: "blue" | "bordeaux" | "green" | "violet" | "rose" | "orange" | "amber" | "cyan" | "neutral") => void;
|
|
4
4
|
messages?: Partial<{
|
|
5
5
|
label: string;
|
|
6
|
-
blue: string;
|
|
7
6
|
bordeaux: string;
|
|
7
|
+
blue: string;
|
|
8
8
|
green: string;
|
|
9
9
|
violet: string;
|
|
10
10
|
rose: string;
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
<svelte:window {onscroll} />
|
|
43
43
|
|
|
44
44
|
<nav
|
|
45
|
-
class="group/nav fixed inset-x-0 bottom-0 z-40 px-4 pb-[max(0.75rem,
|
|
45
|
+
class="group/nav fixed inset-x-0 bottom-0 z-40 px-4 pb-[max(0.75rem,var(--safe-bottom))] md:hidden print:hidden"
|
|
46
46
|
style="view-transition-name: tab-bar"
|
|
47
47
|
aria-label={label}
|
|
48
48
|
data-compact={compact || undefined}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Component } from "svelte";
|
|
3
|
+
|
|
4
|
+
export interface CommandItem {
|
|
5
|
+
label: string;
|
|
6
|
+
href?: string;
|
|
7
|
+
onSelect?: () => void;
|
|
8
|
+
icon?: Component<{ class?: string }>;
|
|
9
|
+
description?: string;
|
|
10
|
+
shortcut?: string;
|
|
11
|
+
keywords?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CommandGroup {
|
|
15
|
+
heading: string;
|
|
16
|
+
items: CommandItem[];
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<script lang="ts">
|
|
21
|
+
import { goto } from "$app/navigation";
|
|
22
|
+
import SearchIcon from "@lucide/svelte/icons/search";
|
|
23
|
+
import SearchXIcon from "@lucide/svelte/icons/search-x";
|
|
24
|
+
import { onMount } from "svelte";
|
|
25
|
+
import { MediaQuery } from "svelte/reactivity";
|
|
26
|
+
|
|
27
|
+
import * as Command from "../ui/command/index.js";
|
|
28
|
+
import * as Drawer from "../ui/drawer/index.js";
|
|
29
|
+
import { haptic } from "../../haptics.js";
|
|
30
|
+
|
|
31
|
+
const defaultMessages = {
|
|
32
|
+
trigger: "Search…",
|
|
33
|
+
label: "Search",
|
|
34
|
+
title: "Command palette",
|
|
35
|
+
description: "Search for a page or run a command",
|
|
36
|
+
empty: "No results.",
|
|
37
|
+
navigate: "to navigate",
|
|
38
|
+
open: "to open",
|
|
39
|
+
close: "to close",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
interface Props {
|
|
43
|
+
groups: CommandGroup[];
|
|
44
|
+
open?: boolean;
|
|
45
|
+
placeholder?: string;
|
|
46
|
+
shortcut?: boolean;
|
|
47
|
+
trigger?: boolean;
|
|
48
|
+
filter?: boolean;
|
|
49
|
+
onsearch?: (term: string) => void;
|
|
50
|
+
messages?: Partial<typeof defaultMessages>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let {
|
|
54
|
+
groups,
|
|
55
|
+
open = $bindable(false),
|
|
56
|
+
placeholder = "Type a command or search…",
|
|
57
|
+
shortcut = true,
|
|
58
|
+
trigger = true,
|
|
59
|
+
filter = true,
|
|
60
|
+
onsearch,
|
|
61
|
+
messages = {},
|
|
62
|
+
}: Props = $props();
|
|
63
|
+
|
|
64
|
+
const desktop = new MediaQuery("(min-width: 768px)");
|
|
65
|
+
const text = $derived({ ...defaultMessages, ...messages });
|
|
66
|
+
|
|
67
|
+
let search = $state("");
|
|
68
|
+
let mac = $state(false);
|
|
69
|
+
|
|
70
|
+
onMount(() => {
|
|
71
|
+
mac = /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** Every typed word has to appear in the label or a keyword; label matches rank first. */
|
|
75
|
+
function matchWords(value: string, term: string, keywords: string[] = []) {
|
|
76
|
+
const words = term.toLowerCase().split(/\s+/).filter(Boolean);
|
|
77
|
+
const label = value.toLowerCase();
|
|
78
|
+
const haystack = [label, ...keywords].join(" ").toLowerCase();
|
|
79
|
+
if (!words.every((word) => haystack.includes(word))) {
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
return words.every((word) => label.includes(word)) ? 1 : 0.5;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setSearch(term: string) {
|
|
86
|
+
search = term;
|
|
87
|
+
onsearch?.(term);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isTyping(target: EventTarget | null): boolean {
|
|
91
|
+
return (
|
|
92
|
+
target instanceof HTMLElement &&
|
|
93
|
+
(target.isContentEditable || target.matches("input, textarea, select"))
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function onkeydown(event: KeyboardEvent) {
|
|
98
|
+
if (
|
|
99
|
+
shortcut &&
|
|
100
|
+
event.key.toLowerCase() === "k" &&
|
|
101
|
+
(mac ? event.metaKey : event.ctrlKey) &&
|
|
102
|
+
(open || !isTyping(event.target))
|
|
103
|
+
) {
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
open = !open;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function select(item: CommandItem) {
|
|
110
|
+
haptic("tap");
|
|
111
|
+
open = false;
|
|
112
|
+
setSearch("");
|
|
113
|
+
if (item.onSelect) {
|
|
114
|
+
item.onSelect();
|
|
115
|
+
} else if (item.href) {
|
|
116
|
+
void goto(item.href);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
<svelte:window {onkeydown} />
|
|
122
|
+
|
|
123
|
+
{#snippet results()}
|
|
124
|
+
<Command.Empty>
|
|
125
|
+
<div
|
|
126
|
+
class="text-muted-foreground flex flex-col items-center gap-2 py-10 text-sm"
|
|
127
|
+
>
|
|
128
|
+
<SearchXIcon class="size-6" />
|
|
129
|
+
{text.empty}
|
|
130
|
+
</div>
|
|
131
|
+
</Command.Empty>
|
|
132
|
+
{#each groups as group (group.heading)}
|
|
133
|
+
<Command.Group
|
|
134
|
+
heading={group.heading}
|
|
135
|
+
class="p-0 pb-2 **:data-command-group-heading:px-2.5 **:data-command-group-heading:pt-3 **:data-command-group-heading:pb-1.5 **:data-command-group-heading:text-[0.6875rem] **:data-command-group-heading:font-semibold **:data-command-group-heading:tracking-wider **:data-command-group-heading:uppercase"
|
|
136
|
+
>
|
|
137
|
+
{#each group.items as item (item.label)}
|
|
138
|
+
<Command.Item
|
|
139
|
+
value={item.label}
|
|
140
|
+
keywords={item.description
|
|
141
|
+
? [...(item.keywords ?? []), item.description]
|
|
142
|
+
: item.keywords}
|
|
143
|
+
onSelect={() => select(item)}
|
|
144
|
+
class="group/item data-selected:bg-accent data-selected:**:[svg]:text-current min-h-11 gap-3 rounded-xl! px-2.5 py-1.5 max-md:min-h-13 max-md:text-base [&>svg:last-child]:hidden"
|
|
145
|
+
>
|
|
146
|
+
{#if item.icon}
|
|
147
|
+
<span
|
|
148
|
+
class="bg-muted text-muted-foreground group-data-selected/item:bg-primary group-data-selected/item:text-primary-foreground flex size-8 shrink-0 items-center justify-center rounded-lg transition-colors"
|
|
149
|
+
>
|
|
150
|
+
<item.icon class="size-4" />
|
|
151
|
+
</span>
|
|
152
|
+
{/if}
|
|
153
|
+
<span class="min-w-0 flex-1 truncate font-medium">{item.label}</span>
|
|
154
|
+
{#if item.description}
|
|
155
|
+
<span
|
|
156
|
+
class="text-muted-foreground bg-muted group-data-selected/item:bg-background/70 max-w-[40%] shrink-0 truncate rounded-full px-2 py-0.5 text-[0.6875rem]"
|
|
157
|
+
>
|
|
158
|
+
{item.description}
|
|
159
|
+
</span>
|
|
160
|
+
{/if}
|
|
161
|
+
{#if item.shortcut}
|
|
162
|
+
<kbd
|
|
163
|
+
class="bg-background text-muted-foreground shrink-0 rounded-md border px-1.5 py-0.5 font-sans text-[0.6875rem] font-medium tracking-widest max-md:hidden"
|
|
164
|
+
>
|
|
165
|
+
{item.shortcut}
|
|
166
|
+
</kbd>
|
|
167
|
+
{/if}
|
|
168
|
+
</Command.Item>
|
|
169
|
+
{/each}
|
|
170
|
+
</Command.Group>
|
|
171
|
+
{/each}
|
|
172
|
+
{/snippet}
|
|
173
|
+
|
|
174
|
+
{#snippet key(label: string)}
|
|
175
|
+
<kbd
|
|
176
|
+
class="bg-background text-foreground inline-flex h-5 min-w-5 items-center justify-center rounded-md border px-1 font-sans text-[0.6875rem] font-medium shadow-xs"
|
|
177
|
+
>{label}</kbd
|
|
178
|
+
>
|
|
179
|
+
{/snippet}
|
|
180
|
+
|
|
181
|
+
{#if trigger}
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
aria-label={text.label}
|
|
185
|
+
class="text-muted-foreground hover:text-foreground border-input focus-visible:ring-ring/50 dark:bg-input/30 flex h-11 w-full min-w-0 items-center gap-2 rounded-xl border bg-white px-3 text-base shadow-xs transition-colors outline-none focus-visible:ring-3 md:h-9 md:w-64 md:rounded-md md:px-2.5 md:text-sm"
|
|
186
|
+
onclick={() => {
|
|
187
|
+
open = true;
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
<SearchIcon class="size-5 shrink-0 md:size-4" />
|
|
191
|
+
<span class="truncate">{text.trigger}</span>
|
|
192
|
+
{#if shortcut}
|
|
193
|
+
<kbd
|
|
194
|
+
class="bg-muted ml-auto hidden rounded px-1.5 font-sans text-[0.6875rem] font-medium tracking-widest md:inline"
|
|
195
|
+
>
|
|
196
|
+
{mac ? "⌘K" : "Ctrl K"}
|
|
197
|
+
</kbd>
|
|
198
|
+
{/if}
|
|
199
|
+
</button>
|
|
200
|
+
{/if}
|
|
201
|
+
|
|
202
|
+
{#if desktop.current}
|
|
203
|
+
<Command.Dialog
|
|
204
|
+
bind:open
|
|
205
|
+
class="rounded-2xl! shadow-2xl sm:max-w-2xl"
|
|
206
|
+
title={text.title}
|
|
207
|
+
description={text.description}
|
|
208
|
+
shouldFilter={filter}
|
|
209
|
+
filter={matchWords}
|
|
210
|
+
>
|
|
211
|
+
<div class="relative border-b">
|
|
212
|
+
<Command.Input
|
|
213
|
+
bind:value={() => search, setSearch}
|
|
214
|
+
{placeholder}
|
|
215
|
+
class="text-base"
|
|
216
|
+
wrapperClass="p-2"
|
|
217
|
+
groupClass="h-12! border-0! bg-transparent! *:data-[slot=input-group-addon]:pl-3!"
|
|
218
|
+
/>
|
|
219
|
+
<span class="absolute top-1/2 right-4 -translate-y-1/2">
|
|
220
|
+
{@render key("esc")}
|
|
221
|
+
</span>
|
|
222
|
+
</div>
|
|
223
|
+
<Command.List class="max-h-[min(26rem,60dvh)] scroll-py-2 p-2">
|
|
224
|
+
{@render results()}
|
|
225
|
+
</Command.List>
|
|
226
|
+
<div
|
|
227
|
+
class="text-muted-foreground bg-muted/40 flex items-center gap-4 border-t px-4 py-2.5 text-xs"
|
|
228
|
+
>
|
|
229
|
+
<span class="flex items-center gap-1.5">
|
|
230
|
+
{@render key("↑")}{@render key("↓")}
|
|
231
|
+
{text.navigate}
|
|
232
|
+
</span>
|
|
233
|
+
<span class="flex items-center gap-1.5">
|
|
234
|
+
{@render key("↵")}
|
|
235
|
+
{text.open}
|
|
236
|
+
</span>
|
|
237
|
+
<span class="ml-auto flex items-center gap-1.5">
|
|
238
|
+
{@render key("esc")}
|
|
239
|
+
{text.close}
|
|
240
|
+
</span>
|
|
241
|
+
</div>
|
|
242
|
+
</Command.Dialog>
|
|
243
|
+
{:else}
|
|
244
|
+
<Drawer.Root bind:open autoFocus>
|
|
245
|
+
<Drawer.Content class="h-[85dvh]">
|
|
246
|
+
<Drawer.Header class="sr-only">
|
|
247
|
+
<Drawer.Title>{text.title}</Drawer.Title>
|
|
248
|
+
<Drawer.Description>{text.description}</Drawer.Description>
|
|
249
|
+
</Drawer.Header>
|
|
250
|
+
<Command.Root
|
|
251
|
+
shouldFilter={filter}
|
|
252
|
+
filter={matchWords}
|
|
253
|
+
class="min-h-0 flex-1 rounded-none! bg-transparent p-0"
|
|
254
|
+
>
|
|
255
|
+
<Command.Input
|
|
256
|
+
bind:value={() => search, setSearch}
|
|
257
|
+
{placeholder}
|
|
258
|
+
class="text-base"
|
|
259
|
+
wrapperClass="px-4 pb-3"
|
|
260
|
+
groupClass="h-12! rounded-xl! border-0! bg-muted! *:data-[slot=input-group-addon]:pl-3!"
|
|
261
|
+
/>
|
|
262
|
+
<Command.List class="pb-safe-bottom max-h-none flex-1 px-2">
|
|
263
|
+
{@render results()}
|
|
264
|
+
</Command.List>
|
|
265
|
+
</Command.Root>
|
|
266
|
+
</Drawer.Content>
|
|
267
|
+
</Drawer.Root>
|
|
268
|
+
{/if}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
export interface CommandItem {
|
|
3
|
+
label: string;
|
|
4
|
+
href?: string;
|
|
5
|
+
onSelect?: () => void;
|
|
6
|
+
icon?: Component<{
|
|
7
|
+
class?: string;
|
|
8
|
+
}>;
|
|
9
|
+
description?: string;
|
|
10
|
+
shortcut?: string;
|
|
11
|
+
keywords?: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface CommandGroup {
|
|
14
|
+
heading: string;
|
|
15
|
+
items: CommandItem[];
|
|
16
|
+
}
|
|
17
|
+
declare const CommandPalette: Component<{
|
|
18
|
+
groups: CommandGroup[];
|
|
19
|
+
open?: boolean;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
shortcut?: boolean;
|
|
22
|
+
trigger?: boolean;
|
|
23
|
+
filter?: boolean;
|
|
24
|
+
onsearch?: (term: string) => void;
|
|
25
|
+
messages?: Partial<{
|
|
26
|
+
trigger: string;
|
|
27
|
+
label: string;
|
|
28
|
+
title: string;
|
|
29
|
+
description: string;
|
|
30
|
+
empty: string;
|
|
31
|
+
navigate: string;
|
|
32
|
+
open: string;
|
|
33
|
+
close: string;
|
|
34
|
+
}>;
|
|
35
|
+
}, {}, "open">;
|
|
36
|
+
type CommandPalette = ReturnType<typeof CommandPalette>;
|
|
37
|
+
export default CommandPalette;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { page } from "$app/state";
|
|
3
3
|
|
|
4
4
|
import { Button } from "../ui/button/index.js";
|
|
5
|
+
import { cn } from "../../utils.js";
|
|
5
6
|
|
|
6
7
|
const defaultMessages = {
|
|
7
8
|
notFoundTitle: "Nothing here",
|
|
@@ -13,31 +14,46 @@
|
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
15
16
|
homeHref: string;
|
|
17
|
+
/** The status to show instead of the page's, to render an error screen outside +error.svelte. */
|
|
18
|
+
status?: number;
|
|
19
|
+
error?: App.Error | null;
|
|
16
20
|
messages?: Partial<typeof defaultMessages>;
|
|
21
|
+
class?: string;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
const {
|
|
24
|
+
const {
|
|
25
|
+
homeHref,
|
|
26
|
+
status,
|
|
27
|
+
error,
|
|
28
|
+
messages = {},
|
|
29
|
+
class: className,
|
|
30
|
+
}: Props = $props();
|
|
20
31
|
|
|
21
32
|
const text = $derived({ ...defaultMessages, ...messages });
|
|
22
|
-
const
|
|
33
|
+
const shownStatus = $derived(status ?? page.status);
|
|
34
|
+
const shownError = $derived(error === undefined ? page.error : error);
|
|
35
|
+
const notFound = $derived(shownStatus === 404);
|
|
23
36
|
</script>
|
|
24
37
|
|
|
25
38
|
<div
|
|
26
|
-
class=
|
|
39
|
+
class={cn(
|
|
40
|
+
"pt-safe-top pb-safe-bottom flex min-h-dvh flex-col items-center justify-center gap-6 px-6 text-center",
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
27
43
|
>
|
|
28
44
|
<p class="text-primary text-7xl font-bold tracking-tight tabular">
|
|
29
|
-
{
|
|
45
|
+
{shownStatus}
|
|
30
46
|
</p>
|
|
31
47
|
<div>
|
|
32
48
|
<h1 class="text-xl font-semibold">
|
|
33
49
|
{notFound ? text.notFoundTitle : text.errorTitle}
|
|
34
50
|
</h1>
|
|
35
51
|
<p class="text-muted-foreground mt-2 text-sm">
|
|
36
|
-
{notFound ? text.notFoundDescription :
|
|
52
|
+
{notFound ? text.notFoundDescription : shownError?.message}
|
|
37
53
|
</p>
|
|
38
|
-
{#if
|
|
54
|
+
{#if shownError?.errorId}
|
|
39
55
|
<p class="text-muted-foreground mt-3 font-mono text-xs">
|
|
40
|
-
{text.errorId}: {
|
|
56
|
+
{text.errorId}: {shownError.errorId}
|
|
41
57
|
</p>
|
|
42
58
|
{/if}
|
|
43
59
|
</div>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
declare const ErrorPage: import("svelte").Component<{
|
|
2
2
|
homeHref: string;
|
|
3
|
+
/** The status to show instead of the page's, to render an error screen outside +error.svelte. */
|
|
4
|
+
status?: number;
|
|
5
|
+
error?: App.Error | null;
|
|
3
6
|
messages?: Partial<{
|
|
4
7
|
notFoundTitle: string;
|
|
5
8
|
notFoundDescription: string;
|
|
@@ -7,6 +10,7 @@ declare const ErrorPage: import("svelte").Component<{
|
|
|
7
10
|
errorId: string;
|
|
8
11
|
home: string;
|
|
9
12
|
}>;
|
|
13
|
+
class?: string;
|
|
10
14
|
}, {}, "">;
|
|
11
15
|
type ErrorPage = ReturnType<typeof ErrorPage>;
|
|
12
16
|
export default ErrorPage;
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
haptic("tap");
|
|
20
20
|
onclick();
|
|
21
21
|
}}
|
|
22
|
-
class="from-primary glow fixed right-5 bottom-[calc(max(0.75rem,
|
|
22
|
+
class="from-primary glow fixed right-5 bottom-[calc(max(0.75rem,var(--safe-bottom))+5.25rem)] z-40 flex size-15 items-center justify-center rounded-2xl bg-linear-to-br to-[oklch(0.62_calc(var(--primary-chroma)*1.1)_calc(var(--hue)+40))] text-white transition-transform duration-200 active:scale-90 md:hidden"
|
|
23
23
|
style="view-transition-name: fab"
|
|
24
24
|
>
|
|
25
25
|
<Icon class="size-7" />
|
|
@@ -31,21 +31,27 @@
|
|
|
31
31
|
brand?: Snippet;
|
|
32
32
|
title?: string;
|
|
33
33
|
messages?: Partial<typeof defaultMessages>;
|
|
34
|
+
open?: boolean;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
let {
|
|
37
38
|
appName,
|
|
38
39
|
storageKey,
|
|
39
40
|
brand,
|
|
40
41
|
title = `Install ${appName}`,
|
|
41
42
|
messages = {},
|
|
43
|
+
open = $bindable(false),
|
|
42
44
|
}: Props = $props();
|
|
43
45
|
|
|
44
46
|
const text = $derived({ ...defaultMessages, ...messages });
|
|
45
47
|
|
|
46
|
-
let open = $state(false);
|
|
47
48
|
let mode = $state<"native" | "apple" | "menu">("native");
|
|
48
|
-
let deferred
|
|
49
|
+
let deferred = $state.raw<InstallPromptEvent>();
|
|
50
|
+
let apple = $state(false);
|
|
51
|
+
|
|
52
|
+
const shownMode = $derived(
|
|
53
|
+
mode === "native" && !deferred ? (apple ? "apple" : "menu") : mode,
|
|
54
|
+
);
|
|
49
55
|
|
|
50
56
|
function alreadyPrompted(): boolean {
|
|
51
57
|
try {
|
|
@@ -69,6 +75,9 @@
|
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
onMount(() => {
|
|
78
|
+
apple =
|
|
79
|
+
/iPhone|iPad/.test(navigator.userAgent) ||
|
|
80
|
+
(/Macintosh/.test(navigator.userAgent) && navigator.maxTouchPoints > 1);
|
|
72
81
|
const standalone =
|
|
73
82
|
matchMedia("(display-mode: standalone)").matches ||
|
|
74
83
|
("standalone" in navigator && navigator.standalone === true);
|
|
@@ -76,9 +85,6 @@
|
|
|
76
85
|
if (standalone || !touch || alreadyPrompted()) {
|
|
77
86
|
return;
|
|
78
87
|
}
|
|
79
|
-
const apple =
|
|
80
|
-
/iPhone|iPad/.test(navigator.userAgent) ||
|
|
81
|
-
(/Macintosh/.test(navigator.userAgent) && navigator.maxTouchPoints > 1);
|
|
82
88
|
if (apple) {
|
|
83
89
|
const timer = setTimeout(() => show("apple"), 1500);
|
|
84
90
|
return () => clearTimeout(timer);
|
|
@@ -128,7 +134,7 @@
|
|
|
128
134
|
{@render brand()}
|
|
129
135
|
</div>
|
|
130
136
|
{/if}
|
|
131
|
-
{#if
|
|
137
|
+
{#if shownMode === "native"}
|
|
132
138
|
<Button class="glow h-12 rounded-xl text-base" onclick={install}>
|
|
133
139
|
<DownloadIcon />
|
|
134
140
|
{text.install}
|
|
@@ -140,7 +146,7 @@
|
|
|
140
146
|
>
|
|
141
147
|
{:else}
|
|
142
148
|
<ol class="flex flex-col gap-3 text-sm">
|
|
143
|
-
{#if
|
|
149
|
+
{#if shownMode === "apple"}
|
|
144
150
|
{@render step(ShareIcon, text.appleShare)}
|
|
145
151
|
{@render step(SquarePlusIcon, text.appleAdd)}
|
|
146
152
|
{:else}
|