@dowel-ui/react 0.4.0 → 0.7.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/README.md +9 -5
- package/dist/blocks/admin-dashboard/meta.js +31 -0
- package/dist/blocks/admin-dashboard/meta.js.map +1 -0
- package/dist/blocks/agent-console/meta.js +27 -0
- package/dist/blocks/agent-console/meta.js.map +1 -0
- package/dist/blocks/ai-dashboard/meta.js +25 -0
- package/dist/blocks/ai-dashboard/meta.js.map +1 -0
- package/dist/blocks/ai-workspace/meta.js +33 -0
- package/dist/blocks/ai-workspace/meta.js.map +1 -0
- package/dist/blocks/analytics/meta.js +24 -0
- package/dist/blocks/analytics/meta.js.map +1 -0
- package/dist/blocks/billing/meta.js +25 -0
- package/dist/blocks/billing/meta.js.map +1 -0
- package/dist/blocks/command-center/meta.js +28 -0
- package/dist/blocks/command-center/meta.js.map +1 -0
- package/dist/blocks/crm/meta.js +32 -0
- package/dist/blocks/crm/meta.js.map +1 -0
- package/dist/blocks/onboarding/meta.js +23 -0
- package/dist/blocks/onboarding/meta.js.map +1 -0
- package/dist/components/accordion/accordion.js +1 -1
- package/dist/components/accordion/accordion.js.map +1 -1
- package/dist/components/activity-feed/activity-feed.js +1 -1
- package/dist/components/activity-feed/activity-feed.js.map +1 -1
- package/dist/components/ai-agent-plan/ai-agent-plan.js +1 -1
- package/dist/components/ai-agent-plan/ai-agent-plan.js.map +1 -1
- package/dist/components/ai-extraction-review/ai-extraction-review.d.ts +52 -0
- package/dist/components/ai-extraction-review/ai-extraction-review.d.ts.map +1 -0
- package/dist/components/ai-extraction-review/ai-extraction-review.js +249 -0
- package/dist/components/ai-extraction-review/ai-extraction-review.js.map +1 -0
- package/dist/components/ai-extraction-review/extraction-model.d.ts +94 -0
- package/dist/components/ai-extraction-review/extraction-model.d.ts.map +1 -0
- package/dist/components/ai-extraction-review/extraction-model.js +91 -0
- package/dist/components/ai-extraction-review/extraction-model.js.map +1 -0
- package/dist/components/ai-extraction-review/index.d.ts +3 -0
- package/dist/components/ai-extraction-review/index.js +3 -0
- package/dist/components/ai-extraction-review/meta.js +17 -0
- package/dist/components/ai-extraction-review/meta.js.map +1 -0
- package/dist/components/ai-prompt-input/ai-prompt-input.js +1 -1
- package/dist/components/ai-prompt-input/ai-prompt-input.js.map +1 -1
- package/dist/components/ai-reasoning/ai-reasoning.js +1 -1
- package/dist/components/ai-reasoning/ai-reasoning.js.map +1 -1
- package/dist/components/ai-response/ai-response.js +2 -2
- package/dist/components/ai-response/ai-response.js.map +1 -1
- package/dist/components/ai-suggested-value/ai-suggested-value.d.ts +72 -0
- package/dist/components/ai-suggested-value/ai-suggested-value.d.ts.map +1 -0
- package/dist/components/ai-suggested-value/ai-suggested-value.js +126 -0
- package/dist/components/ai-suggested-value/ai-suggested-value.js.map +1 -0
- package/dist/components/ai-suggested-value/index.d.ts +2 -0
- package/dist/components/ai-suggested-value/index.js +2 -0
- package/dist/components/ai-suggested-value/meta.js +17 -0
- package/dist/components/ai-suggested-value/meta.js.map +1 -0
- package/dist/components/ai-tool/ai-tool.js +2 -2
- package/dist/components/ai-tool/ai-tool.js.map +1 -1
- package/dist/components/breadcrumb/breadcrumb.d.ts +52 -0
- package/dist/components/breadcrumb/breadcrumb.d.ts.map +1 -0
- package/dist/components/breadcrumb/breadcrumb.js +141 -0
- package/dist/components/breadcrumb/breadcrumb.js.map +1 -0
- package/dist/components/breadcrumb/index.d.ts +2 -0
- package/dist/components/breadcrumb/index.js +2 -0
- package/dist/components/breadcrumb/meta.js +17 -0
- package/dist/components/breadcrumb/meta.js.map +1 -0
- package/dist/components/calendar/calendar.js +2 -2
- package/dist/components/calendar/calendar.js.map +1 -1
- package/dist/components/code-block/code-block.js +1 -1
- package/dist/components/code-block/code-block.js.map +1 -1
- package/dist/components/collapsible/collapsible.d.ts +30 -0
- package/dist/components/collapsible/collapsible.d.ts.map +1 -0
- package/dist/components/collapsible/collapsible.js +38 -0
- package/dist/components/collapsible/collapsible.js.map +1 -0
- package/dist/components/collapsible/index.d.ts +2 -0
- package/dist/components/collapsible/index.js +2 -0
- package/dist/components/collapsible/meta.js +17 -0
- package/dist/components/collapsible/meta.js.map +1 -0
- package/dist/components/combobox/combobox.js +2 -2
- package/dist/components/combobox/combobox.js.map +1 -1
- package/dist/components/command/command.js +1 -1
- package/dist/components/command/command.js.map +1 -1
- package/dist/components/confirm-typed/confirm-typed.d.ts +47 -0
- package/dist/components/confirm-typed/confirm-typed.d.ts.map +1 -0
- package/dist/components/confirm-typed/confirm-typed.js +106 -0
- package/dist/components/confirm-typed/confirm-typed.js.map +1 -0
- package/dist/components/confirm-typed/index.d.ts +2 -0
- package/dist/components/confirm-typed/index.js +2 -0
- package/dist/components/confirm-typed/meta.js +17 -0
- package/dist/components/confirm-typed/meta.js.map +1 -0
- package/dist/components/cron-editor/cron-editor.d.ts +36 -0
- package/dist/components/cron-editor/cron-editor.d.ts.map +1 -0
- package/dist/components/cron-editor/cron-editor.js +493 -0
- package/dist/components/cron-editor/cron-editor.js.map +1 -0
- package/dist/components/cron-editor/cron-expression.d.ts +82 -0
- package/dist/components/cron-editor/cron-expression.d.ts.map +1 -0
- package/dist/components/cron-editor/cron-expression.js +352 -0
- package/dist/components/cron-editor/cron-expression.js.map +1 -0
- package/dist/components/cron-editor/index.d.ts +3 -0
- package/dist/components/cron-editor/index.js +3 -0
- package/dist/components/cron-editor/meta.js +17 -0
- package/dist/components/cron-editor/meta.js.map +1 -0
- package/dist/components/data-table/data-table.d.ts.map +1 -1
- package/dist/components/data-table/data-table.js +4 -1
- package/dist/components/data-table/data-table.js.map +1 -1
- package/dist/components/dialog/dialog.js +2 -2
- package/dist/components/dialog/dialog.js.map +1 -1
- package/dist/components/diff-viewer/diff-viewer.js +4 -4
- package/dist/components/diff-viewer/diff-viewer.js.map +1 -1
- package/dist/components/direction/direction.d.ts +35 -0
- package/dist/components/direction/direction.d.ts.map +1 -0
- package/dist/components/direction/direction.js +14 -0
- package/dist/components/direction/direction.js.map +1 -0
- package/dist/components/direction/index.d.ts +2 -0
- package/dist/components/direction/index.js +2 -0
- package/dist/components/direction/meta.js +17 -0
- package/dist/components/direction/meta.js.map +1 -0
- package/dist/components/dns-record/dns-record.d.ts +61 -0
- package/dist/components/dns-record/dns-record.d.ts.map +1 -0
- package/dist/components/dns-record/dns-record.js +227 -0
- package/dist/components/dns-record/dns-record.js.map +1 -0
- package/dist/components/dns-record/index.d.ts +2 -0
- package/dist/components/dns-record/index.js +2 -0
- package/dist/components/dns-record/meta.js +17 -0
- package/dist/components/dns-record/meta.js.map +1 -0
- package/dist/components/drawer/drawer.js +1 -1
- package/dist/components/drawer/drawer.js.map +1 -1
- package/dist/components/dropdown-menu/dropdown-menu.d.ts.map +1 -1
- package/dist/components/dropdown-menu/dropdown-menu.js +9 -8
- package/dist/components/dropdown-menu/dropdown-menu.js.map +1 -1
- package/dist/components/input/input.js +1 -1
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/log-viewer/log-viewer.js +2 -2
- package/dist/components/log-viewer/log-viewer.js.map +1 -1
- package/dist/components/meter/meter.js +1 -1
- package/dist/components/meter/meter.js.map +1 -1
- package/dist/components/pagination/pagination.d.ts.map +1 -1
- package/dist/components/pagination/pagination.js +3 -2
- package/dist/components/pagination/pagination.js.map +1 -1
- package/dist/components/permission-matrix/index.d.ts +3 -0
- package/dist/components/permission-matrix/index.js +3 -0
- package/dist/components/permission-matrix/meta.js +17 -0
- package/dist/components/permission-matrix/meta.js.map +1 -0
- package/dist/components/permission-matrix/permission-matrix.d.ts +46 -0
- package/dist/components/permission-matrix/permission-matrix.d.ts.map +1 -0
- package/dist/components/permission-matrix/permission-matrix.js +199 -0
- package/dist/components/permission-matrix/permission-matrix.js.map +1 -0
- package/dist/components/permission-matrix/permission-model.d.ts +53 -0
- package/dist/components/permission-matrix/permission-model.d.ts.map +1 -0
- package/dist/components/permission-matrix/permission-model.js +64 -0
- package/dist/components/permission-matrix/permission-model.js.map +1 -0
- package/dist/components/record-diff/record-diff.js +4 -4
- package/dist/components/record-diff/record-diff.js.map +1 -1
- package/dist/components/secret-field/index.d.ts +2 -0
- package/dist/components/secret-field/index.js +2 -0
- package/dist/components/secret-field/meta.js +17 -0
- package/dist/components/secret-field/meta.js.map +1 -0
- package/dist/components/secret-field/secret-field.d.ts +58 -0
- package/dist/components/secret-field/secret-field.d.ts.map +1 -0
- package/dist/components/secret-field/secret-field.js +168 -0
- package/dist/components/secret-field/secret-field.js.map +1 -0
- package/dist/components/select/select.js +2 -2
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/session-expiry/index.d.ts +2 -0
- package/dist/components/session-expiry/index.js +2 -0
- package/dist/components/session-expiry/meta.js +17 -0
- package/dist/components/session-expiry/meta.js.map +1 -0
- package/dist/components/session-expiry/session-expiry.d.ts +64 -0
- package/dist/components/session-expiry/session-expiry.d.ts.map +1 -0
- package/dist/components/session-expiry/session-expiry.js +185 -0
- package/dist/components/session-expiry/session-expiry.js.map +1 -0
- package/dist/components/sheet/sheet.d.ts.map +1 -1
- package/dist/components/sheet/sheet.js +2 -2
- package/dist/components/sheet/sheet.js.map +1 -1
- package/dist/components/shortcut-recorder/index.d.ts +3 -0
- package/dist/components/shortcut-recorder/index.js +3 -0
- package/dist/components/shortcut-recorder/meta.js +17 -0
- package/dist/components/shortcut-recorder/meta.js.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-model.d.ts +67 -0
- package/dist/components/shortcut-recorder/shortcut-model.d.ts.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-model.js +168 -0
- package/dist/components/shortcut-recorder/shortcut-model.js.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.d.ts +47 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.d.ts.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.js +212 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.js.map +1 -0
- package/dist/components/sidebar/index.d.ts +2 -0
- package/dist/components/sidebar/index.js +2 -0
- package/dist/components/sidebar/meta.js +17 -0
- package/dist/components/sidebar/meta.js.map +1 -0
- package/dist/components/sidebar/sidebar.d.ts +69 -0
- package/dist/components/sidebar/sidebar.d.ts.map +1 -0
- package/dist/components/sidebar/sidebar.js +256 -0
- package/dist/components/sidebar/sidebar.js.map +1 -0
- package/dist/components/sync-status/index.d.ts +2 -0
- package/dist/components/sync-status/index.js +2 -0
- package/dist/components/sync-status/meta.js +17 -0
- package/dist/components/sync-status/meta.js.map +1 -0
- package/dist/components/sync-status/sync-status.d.ts +65 -0
- package/dist/components/sync-status/sync-status.d.ts.map +1 -0
- package/dist/components/sync-status/sync-status.js +103 -0
- package/dist/components/sync-status/sync-status.js.map +1 -0
- package/dist/components/table/table.js +2 -2
- package/dist/components/table/table.js.map +1 -1
- package/dist/components/tags-input/tags-input.js +1 -1
- package/dist/components/tags-input/tags-input.js.map +1 -1
- package/dist/components/textarea/index.d.ts +2 -0
- package/dist/components/textarea/index.js +2 -0
- package/dist/components/textarea/meta.js +17 -0
- package/dist/components/textarea/meta.js.map +1 -0
- package/dist/components/textarea/textarea.d.ts +43 -0
- package/dist/components/textarea/textarea.d.ts.map +1 -0
- package/dist/components/textarea/textarea.js +119 -0
- package/dist/components/textarea/textarea.js.map +1 -0
- package/dist/components/time-range-picker/time-range-picker.js +3 -3
- package/dist/components/time-range-picker/time-range-picker.js.map +1 -1
- package/dist/components/toast/toast.d.ts.map +1 -1
- package/dist/components/toast/toast.js +2 -2
- package/dist/components/toast/toast.js.map +1 -1
- package/dist/index.d.ts +36 -2
- package/dist/index.js +22 -3
- package/dist/lib/styles.d.ts +16 -1
- package/dist/lib/styles.d.ts.map +1 -1
- package/dist/lib/styles.js +16 -1
- package/dist/lib/styles.js.map +1 -1
- package/dist/registry/blocks.d.ts.map +1 -1
- package/dist/registry/blocks.js +27 -9
- package/dist/registry/blocks.js.map +1 -1
- package/dist/registry/components.d.ts.map +1 -1
- package/dist/registry/components.js +84 -54
- package/dist/registry/components.js.map +1 -1
- package/dist/registry/schema.d.ts +8 -0
- package/dist/registry/schema.d.ts.map +1 -1
- package/dist/registry/schema.js.map +1 -1
- package/package.json +5 -4
- package/src/components/accordion/accordion.tsx +1 -1
- package/src/components/activity-feed/activity-feed.tsx +1 -1
- package/src/components/ai-agent-plan/ai-agent-plan.tsx +0 -0
- package/src/components/ai-extraction-review/ai-extraction-review.tsx +492 -0
- package/src/components/ai-extraction-review/extraction-model.ts +159 -0
- package/src/components/ai-extraction-review/index.ts +13 -0
- package/src/components/ai-extraction-review/meta.ts +24 -0
- package/src/components/ai-prompt-input/ai-prompt-input.tsx +1 -1
- package/src/components/ai-reasoning/ai-reasoning.tsx +1 -1
- package/src/components/ai-response/ai-response.tsx +2 -2
- package/src/components/ai-suggested-value/ai-suggested-value.tsx +295 -0
- package/src/components/ai-suggested-value/index.ts +6 -0
- package/src/components/ai-suggested-value/meta.ts +24 -0
- package/src/components/ai-tool/ai-tool.tsx +2 -2
- package/src/components/breadcrumb/breadcrumb.tsx +157 -0
- package/src/components/breadcrumb/index.ts +10 -0
- package/src/components/breadcrumb/meta.ts +19 -0
- package/src/components/calendar/calendar.tsx +2 -2
- package/src/components/code-block/code-block.tsx +1 -1
- package/src/components/collapsible/collapsible.tsx +63 -0
- package/src/components/collapsible/index.ts +6 -0
- package/src/components/collapsible/meta.ts +16 -0
- package/src/components/combobox/combobox.tsx +2 -2
- package/src/components/command/command.tsx +1 -1
- package/src/components/confirm-typed/confirm-typed.tsx +191 -0
- package/src/components/confirm-typed/index.ts +1 -0
- package/src/components/confirm-typed/meta.ts +22 -0
- package/src/components/cron-editor/cron-editor.tsx +676 -0
- package/src/components/cron-editor/cron-expression.ts +552 -0
- package/src/components/cron-editor/index.ts +23 -0
- package/src/components/cron-editor/meta.ts +23 -0
- package/src/components/data-table/data-table.tsx +14 -3
- package/src/components/dialog/dialog.tsx +2 -2
- package/src/components/diff-viewer/diff-viewer.tsx +4 -4
- package/src/components/direction/direction.tsx +38 -0
- package/src/components/direction/index.ts +1 -0
- package/src/components/direction/meta.ts +19 -0
- package/src/components/dns-record/dns-record.tsx +315 -0
- package/src/components/dns-record/index.ts +7 -0
- package/src/components/dns-record/meta.ts +21 -0
- package/src/components/drawer/drawer.tsx +1 -1
- package/src/components/dropdown-menu/dropdown-menu.tsx +14 -8
- package/src/components/input/input.tsx +1 -1
- package/src/components/log-viewer/log-viewer.tsx +2 -2
- package/src/components/meter/meter.tsx +1 -1
- package/src/components/pagination/pagination.tsx +13 -2
- package/src/components/permission-matrix/index.ts +14 -0
- package/src/components/permission-matrix/meta.ts +23 -0
- package/src/components/permission-matrix/permission-matrix.tsx +368 -0
- package/src/components/permission-matrix/permission-model.ts +120 -0
- package/src/components/record-diff/record-diff.tsx +4 -4
- package/src/components/secret-field/index.ts +6 -0
- package/src/components/secret-field/meta.ts +21 -0
- package/src/components/secret-field/secret-field.tsx +284 -0
- package/src/components/select/select.tsx +2 -2
- package/src/components/session-expiry/index.ts +9 -0
- package/src/components/session-expiry/meta.ts +23 -0
- package/src/components/session-expiry/session-expiry.tsx +321 -0
- package/src/components/sheet/sheet.tsx +8 -4
- package/src/components/shortcut-recorder/index.ts +19 -0
- package/src/components/shortcut-recorder/meta.ts +22 -0
- package/src/components/shortcut-recorder/shortcut-model.ts +204 -0
- package/src/components/shortcut-recorder/shortcut-recorder.tsx +336 -0
- package/src/components/sidebar/index.ts +19 -0
- package/src/components/sidebar/meta.ts +25 -0
- package/src/components/sidebar/sidebar.tsx +374 -0
- package/src/components/sync-status/index.ts +9 -0
- package/src/components/sync-status/meta.ts +21 -0
- package/src/components/sync-status/sync-status.tsx +217 -0
- package/src/components/table/table.tsx +3 -3
- package/src/components/tags-input/tags-input.tsx +1 -1
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/meta.ts +21 -0
- package/src/components/textarea/textarea.tsx +210 -0
- package/src/components/time-range-picker/time-range-picker.tsx +3 -3
- package/src/components/toast/toast.tsx +14 -6
- package/src/index.ts +15 -0
- package/src/lib/styles.ts +16 -0
- package/src/registry/blocks.ts +18 -0
- package/src/registry/components.ts +30 -0
- package/src/registry/schema.ts +8 -0
- package/src/blocks/admin-users/admin-users.tsx +0 -251
- package/src/blocks/admin-users/index.ts +0 -1
- package/src/blocks/admin-users/meta.ts +0 -27
- package/src/blocks/ai-chat/ai-chat.tsx +0 -280
- package/src/blocks/ai-chat/index.ts +0 -7
- package/src/blocks/ai-chat/meta.ts +0 -32
- package/src/blocks/dashboard/dashboard.tsx +0 -192
- package/src/blocks/dashboard/index.ts +0 -6
- package/src/blocks/dashboard/meta.ts +0 -18
- package/src/blocks/forgot-password/forgot-password.tsx +0 -158
- package/src/blocks/forgot-password/index.ts +0 -1
- package/src/blocks/forgot-password/meta.ts +0 -18
- package/src/blocks/login/index.ts +0 -1
- package/src/blocks/login/login.tsx +0 -178
- package/src/blocks/login/meta.ts +0 -18
- package/src/blocks/pricing/index.ts +0 -1
- package/src/blocks/pricing/meta.ts +0 -18
- package/src/blocks/pricing/pricing.tsx +0 -176
- package/src/blocks/settings/index.ts +0 -6
- package/src/blocks/settings/meta.ts +0 -29
- package/src/blocks/settings/settings.tsx +0 -317
- package/src/blocks/signup/index.ts +0 -1
- package/src/blocks/signup/meta.ts +0 -18
- package/src/blocks/signup/signup.tsx +0 -240
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineMeta } from "../../registry/schema.js";
|
|
2
|
+
//#region src/components/sidebar/meta.ts
|
|
3
|
+
const meta = defineMeta({
|
|
4
|
+
name: "sidebar",
|
|
5
|
+
title: "Sidebar",
|
|
6
|
+
description: "The application's own navigation: a collapsible rail on a wide screen, an overlay on a narrow one.",
|
|
7
|
+
category: "navigation",
|
|
8
|
+
status: "stable",
|
|
9
|
+
dependencies: ["radix-ui"],
|
|
10
|
+
registryDependencies: ["sheet"],
|
|
11
|
+
files: ["sidebar.tsx"],
|
|
12
|
+
a11y: "The navigation landmark must be named — a page has several, and three regions all called \"navigation\" is a list nobody can choose from. The active entry carries aria-current=\"page\" rather than only a background colour. On a narrow screen it is a Sheet, not a styled div: an overlay needs a focus trap and an Escape key, and without them the page behind stays reachable by Tab while the menu covers it. When the rail collapses, labels are visually hidden rather than removed — removing them leaves controls whose only content is an icon, which is how a collapsed sidebar becomes a column of links all announced as \"link\". The trigger says what pressing it will do, not what the state currently is. The overlay is mounted only on a narrow screen and has its own open state: a modal dialog that CSS hides is still modal, and mounting it always made every wide screen aria-hidden and unclickable whenever the rail was open."
|
|
13
|
+
});
|
|
14
|
+
//#endregion
|
|
15
|
+
export { meta };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/sidebar/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"sidebar\",\n title: \"Sidebar\",\n description:\n \"The application's own navigation: a collapsible rail on a wide screen, an overlay on a narrow one.\",\n category: \"navigation\",\n status: \"stable\",\n dependencies: [\"radix-ui\"],\n registryDependencies: [\"sheet\"],\n files: [\"sidebar.tsx\"],\n a11y:\n \"The navigation landmark must be named — a page has several, and three regions all called \" +\n '\"navigation\" is a list nobody can choose from. The active entry carries aria-current=' +\n '\"page\" rather than only a background colour. On a narrow screen it is a Sheet, not a ' +\n \"styled div: an overlay needs a focus trap and an Escape key, and without them the page \" +\n \"behind stays reachable by Tab while the menu covers it. When the rail collapses, labels \" +\n \"are visually hidden rather than removed — removing them leaves controls whose only content \" +\n \"is an icon, which is how a collapsed sidebar becomes a column of links all announced as \" +\n '\"link\". The trigger says what pressing it will do, not what the state currently is. The ' +\n \"overlay is mounted only on a narrow screen and has its own open state: a modal dialog that \" +\n \"CSS hides is still modal, and mounting it always made every wide screen aria-hidden and \" +\n \"unclickable whenever the rail was open.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aACE;CACF,UAAU;CACV,QAAQ;CACR,cAAc,CAAC,UAAU;CACzB,sBAAsB,CAAC,OAAO;CAC9B,OAAO,CAAC,aAAa;CACrB,MACE;AAWJ,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ComponentPropsWithRef, ReactNode } from "react";
|
|
2
|
+
//#region src/components/sidebar/sidebar.d.ts
|
|
3
|
+
interface SidebarProviderProps {
|
|
4
|
+
/** Controlled open state. */
|
|
5
|
+
open?: boolean;
|
|
6
|
+
defaultOpen?: boolean;
|
|
7
|
+
onOpenChange?: (open: boolean) => void;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
declare function SidebarProvider({ open: openProp, defaultOpen, onOpenChange, children }: SidebarProviderProps): import("react").JSX.Element;
|
|
11
|
+
interface SidebarProps extends ComponentPropsWithRef<"aside"> {
|
|
12
|
+
/**
|
|
13
|
+
* Names the navigation landmark.
|
|
14
|
+
*
|
|
15
|
+
* Required, not optional. A page has more than one navigation region — this,
|
|
16
|
+
* a breadcrumb, a footer — and three landmarks all called "navigation" is a
|
|
17
|
+
* list a screen reader user cannot choose from.
|
|
18
|
+
*/
|
|
19
|
+
label: string;
|
|
20
|
+
/** Title for the overlay on a narrow screen, where it is a dialog. */
|
|
21
|
+
mobileTitle?: string;
|
|
22
|
+
}
|
|
23
|
+
declare function Sidebar({ className, label, mobileTitle, children, ...props }: SidebarProps): import("react").JSX.Element;
|
|
24
|
+
declare function SidebarHeader({ className, ...props }: ComponentPropsWithRef<"div">): import("react").JSX.Element;
|
|
25
|
+
declare function SidebarContent({ className, ...props }: ComponentPropsWithRef<"div">): import("react").JSX.Element;
|
|
26
|
+
declare function SidebarFooter({ className, ...props }: ComponentPropsWithRef<"div">): import("react").JSX.Element;
|
|
27
|
+
declare function SidebarGroup({ className, ...props }: ComponentPropsWithRef<"div">): import("react").JSX.Element;
|
|
28
|
+
/**
|
|
29
|
+
* A heading for a group of links.
|
|
30
|
+
*
|
|
31
|
+
* Hidden rather than removed when the rail is collapsed, so the grouping
|
|
32
|
+
* survives for anyone not reading the layout visually.
|
|
33
|
+
*/
|
|
34
|
+
declare function SidebarGroupLabel({ className, ...props }: ComponentPropsWithRef<"div">): import("react").JSX.Element;
|
|
35
|
+
declare function SidebarMenu({ className, ...props }: ComponentPropsWithRef<"ul">): import("react").JSX.Element;
|
|
36
|
+
declare function SidebarMenuItem({ className, ...props }: ComponentPropsWithRef<"li">): import("react").JSX.Element;
|
|
37
|
+
interface SidebarMenuButtonProps extends ComponentPropsWithRef<"a"> {
|
|
38
|
+
/** Render the child as the control, for a router's own link or a button. */
|
|
39
|
+
asChild?: boolean;
|
|
40
|
+
/** The page currently being shown. */
|
|
41
|
+
isActive?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* One entry in the navigation.
|
|
45
|
+
*
|
|
46
|
+
* `aria-current="page"` when active, not merely a background colour: which page
|
|
47
|
+
* you are on is information, and a highlight conveys it to exactly one kind of
|
|
48
|
+
* reader.
|
|
49
|
+
*/
|
|
50
|
+
declare function SidebarMenuButton({ className, asChild, isActive, children, ...props }: SidebarMenuButtonProps): import("react").JSX.Element;
|
|
51
|
+
/**
|
|
52
|
+
* The text of a menu entry.
|
|
53
|
+
*
|
|
54
|
+
* Visually hidden rather than removed when the rail is collapsed. Removing it
|
|
55
|
+
* leaves a control whose only content is an icon, and an icon has no accessible
|
|
56
|
+
* name — which is how a collapsed sidebar becomes a column of buttons all
|
|
57
|
+
* announced as "link".
|
|
58
|
+
*/
|
|
59
|
+
declare function SidebarMenuLabel({ className, ...props }: ComponentPropsWithRef<"span">): import("react").JSX.Element;
|
|
60
|
+
interface SidebarTriggerProps extends ComponentPropsWithRef<"button"> {
|
|
61
|
+
/** Overrides the label, which otherwise says what pressing it will do. */
|
|
62
|
+
label?: string;
|
|
63
|
+
}
|
|
64
|
+
declare function SidebarTrigger({ className, label, ...props }: SidebarTriggerProps): import("react").JSX.Element;
|
|
65
|
+
/** The region the sidebar sits beside. */
|
|
66
|
+
declare function SidebarInset({ className, ...props }: ComponentPropsWithRef<"main">): import("react").JSX.Element;
|
|
67
|
+
//#endregion
|
|
68
|
+
export { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuButtonProps, SidebarMenuItem, SidebarMenuLabel, SidebarProps, SidebarProvider, SidebarProviderProps, SidebarTrigger, SidebarTriggerProps };
|
|
69
|
+
//# sourceMappingURL=sidebar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sidebar.d.ts","names":[],"sources":["../../../src/components/sidebar/sidebar.tsx"],"mappings":";;UA6FiB;;EAEf;EACA;EACA,gBAAgB;EAChB,WAAW;;iBAGG,kBACd,MAAM,UACN,aACA,cACA,YACC,uCAAoB,IAAA;UAiCN,qBAAqB;;;;;;;;EAQpC;;EAEA;;iBAGc,UAAU,WAAW,OAAO,aAAa,aAAa,SAAS,+BAAY,IAAA;iBA6C3E,gBAAgB,cAAc,SAAS,+CAA4B,IAAA;iBAUnE,iBAAiB,cAAc,SAAS,+CAA4B,IAAA;iBAUpE,gBAAgB,cAAc,SAAS,+CAA4B,IAAA;iBAInE,eAAe,cAAc,SAAS,+CAA4B,IAAA;;;;;;;iBAUlE,oBAAoB,cAAc,SAAS,+CAA4B,IAAA;iBAkBvE,cAAc,cAAc,SAAS,8CAA2B,IAAA;iBAIhE,kBAAkB,cAAc,SAAS,8CAA2B,IAAA;UAInE,+BAA+B;;EAE9C;;EAEA;;;;;;;;;iBAUc,oBACd,WACA,SACA,UACA,aACG,SACF,yCAAsB,IAAA;;;;;;;;;iBAkCT,mBAAmB,cAAc,SAAS,gDAA6B,IAAA;UAYtE,4BAA4B;;EAE3C;;iBAGc,iBAAiB,WAAW,UAAU,SAAS,sCAAmB,IAAA;;iBAyClE,eAAe,cAAc,SAAS,gDAA6B,IAAA"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { disabledStyles, focusRing } from "../../lib/styles.js";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
import { Sheet, SheetContent, SheetTitle } from "../sheet/sheet.js";
|
|
5
|
+
import { Slot } from "radix-ui";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { createContext, useContext, useId, useMemo, useState, useSyncExternalStore } from "react";
|
|
8
|
+
//#region src/components/sidebar/sidebar.tsx
|
|
9
|
+
/**
|
|
10
|
+
* The application's own navigation.
|
|
11
|
+
*
|
|
12
|
+
* Two states, not one. On a wide screen it collapses to a rail of icons and
|
|
13
|
+
* stays in the page; on a narrow one it is an overlay, which needs a focus trap
|
|
14
|
+
* and an Escape key and is therefore a Sheet rather than a div with a
|
|
15
|
+
* transform. Building one behaviour and hiding it at a breakpoint gets the
|
|
16
|
+
* mobile case wrong every time — the page behind stays reachable by Tab while
|
|
17
|
+
* the menu covers it.
|
|
18
|
+
*
|
|
19
|
+
* The collapsed rail is where these usually fail. Hiding the label leaves a
|
|
20
|
+
* button whose only content is an icon, and an icon has no accessible name. The
|
|
21
|
+
* label here is not removed but visually hidden, so the control keeps its name
|
|
22
|
+
* whether or not the text is on screen.
|
|
23
|
+
*
|
|
24
|
+
* The overlay is mounted only on a narrow screen, and it has its own open
|
|
25
|
+
* state. Mounting it always and hiding it with CSS was the first version, and
|
|
26
|
+
* it was wrong in a way no visual check catches: a modal dialog that is
|
|
27
|
+
* `display: none` is still modal. It marks the rest of the page `aria-hidden`,
|
|
28
|
+
* traps focus in an element nothing can focus, and turns pointer events off on
|
|
29
|
+
* the body — so on a desktop the whole application was invisible to a screen
|
|
30
|
+
* reader and unclickable the moment the sidebar was open, which is its default.
|
|
31
|
+
*/
|
|
32
|
+
/** Below Tailwind's `md` breakpoint, which is where the rail gives way to the overlay. */
|
|
33
|
+
const NARROW_QUERY = "(max-width: 767px)";
|
|
34
|
+
function subscribeToNarrow(callback) {
|
|
35
|
+
const query = window.matchMedia(NARROW_QUERY);
|
|
36
|
+
query.addEventListener("change", callback);
|
|
37
|
+
return () => {
|
|
38
|
+
query.removeEventListener("change", callback);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether the viewport is narrow enough for the overlay.
|
|
43
|
+
*
|
|
44
|
+
* The server snapshot says "wide": the rail renders in the HTML and CSS hides
|
|
45
|
+
* it below the breakpoint, so a narrow screen shows nothing wrong before
|
|
46
|
+
* hydration and the sheet mounts, closed, straight after.
|
|
47
|
+
*/
|
|
48
|
+
function useIsNarrow() {
|
|
49
|
+
return useSyncExternalStore(subscribeToNarrow, () => window.matchMedia(NARROW_QUERY).matches, () => false);
|
|
50
|
+
}
|
|
51
|
+
const SidebarContext = createContext(null);
|
|
52
|
+
function useSidebar(component) {
|
|
53
|
+
const context = useContext(SidebarContext);
|
|
54
|
+
if (!context) throw new Error(`${component} must be rendered inside <SidebarProvider>.`);
|
|
55
|
+
return context;
|
|
56
|
+
}
|
|
57
|
+
function SidebarProvider({ open: openProp, defaultOpen = true, onOpenChange, children }) {
|
|
58
|
+
const [uncontrolled, setUncontrolled] = useState(defaultOpen);
|
|
59
|
+
const open = openProp ?? uncontrolled;
|
|
60
|
+
const [overlayOpen, setOverlayOpen] = useState(false);
|
|
61
|
+
const narrow = useIsNarrow();
|
|
62
|
+
const sidebarId = useId();
|
|
63
|
+
const value = useMemo(() => {
|
|
64
|
+
const setOpen = (next) => {
|
|
65
|
+
if (openProp === void 0) setUncontrolled(next);
|
|
66
|
+
onOpenChange?.(next);
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
open,
|
|
70
|
+
setOpen,
|
|
71
|
+
narrow,
|
|
72
|
+
overlayOpen,
|
|
73
|
+
setOverlayOpen,
|
|
74
|
+
toggle: () => {
|
|
75
|
+
if (narrow) setOverlayOpen(!overlayOpen);
|
|
76
|
+
else setOpen(!open);
|
|
77
|
+
},
|
|
78
|
+
sidebarId
|
|
79
|
+
};
|
|
80
|
+
}, [
|
|
81
|
+
open,
|
|
82
|
+
openProp,
|
|
83
|
+
onOpenChange,
|
|
84
|
+
narrow,
|
|
85
|
+
overlayOpen,
|
|
86
|
+
sidebarId
|
|
87
|
+
]);
|
|
88
|
+
return /* @__PURE__ */ jsx(SidebarContext.Provider, {
|
|
89
|
+
value,
|
|
90
|
+
children
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function Sidebar({ className, label, mobileTitle, children, ...props }) {
|
|
94
|
+
const { open, narrow, overlayOpen, setOverlayOpen, sidebarId } = useSidebar("Sidebar");
|
|
95
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [narrow ? /* @__PURE__ */ jsx(Sheet, {
|
|
96
|
+
open: overlayOpen,
|
|
97
|
+
onOpenChange: setOverlayOpen,
|
|
98
|
+
children: /* @__PURE__ */ jsxs(SheetContent, {
|
|
99
|
+
side: "left",
|
|
100
|
+
className: "w-72 p-0",
|
|
101
|
+
children: [/* @__PURE__ */ jsx(SheetTitle, {
|
|
102
|
+
className: "sr-only",
|
|
103
|
+
children: mobileTitle ?? label
|
|
104
|
+
}), /* @__PURE__ */ jsx("nav", {
|
|
105
|
+
"aria-label": label,
|
|
106
|
+
className: "flex h-full flex-col gap-2 p-3",
|
|
107
|
+
children
|
|
108
|
+
})]
|
|
109
|
+
})
|
|
110
|
+
}) : null, /* @__PURE__ */ jsx("aside", {
|
|
111
|
+
id: sidebarId,
|
|
112
|
+
"data-slot": "sidebar",
|
|
113
|
+
"data-state": open ? "expanded" : "collapsed",
|
|
114
|
+
className: cn("hidden shrink-0 border-e border-border bg-background md:block", "transition-[width] duration-[var(--duration-normal)] ease-[var(--ease-out-quint)]", open ? "w-60" : "w-14", className),
|
|
115
|
+
...props,
|
|
116
|
+
children: /* @__PURE__ */ jsx("nav", {
|
|
117
|
+
"aria-label": label,
|
|
118
|
+
className: "flex h-full flex-col gap-2 p-3",
|
|
119
|
+
children
|
|
120
|
+
})
|
|
121
|
+
})] });
|
|
122
|
+
}
|
|
123
|
+
function SidebarHeader({ className, ...props }) {
|
|
124
|
+
return /* @__PURE__ */ jsx("div", {
|
|
125
|
+
"data-slot": "sidebar-header",
|
|
126
|
+
className: cn("flex min-h-9 items-center gap-2 px-1", className),
|
|
127
|
+
...props
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function SidebarContent({ className, ...props }) {
|
|
131
|
+
return /* @__PURE__ */ jsx("div", {
|
|
132
|
+
"data-slot": "sidebar-content",
|
|
133
|
+
className: cn("flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto", className),
|
|
134
|
+
...props
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function SidebarFooter({ className, ...props }) {
|
|
138
|
+
return /* @__PURE__ */ jsx("div", {
|
|
139
|
+
"data-slot": "sidebar-footer",
|
|
140
|
+
className: cn("mt-auto", className),
|
|
141
|
+
...props
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function SidebarGroup({ className, ...props }) {
|
|
145
|
+
return /* @__PURE__ */ jsx("div", {
|
|
146
|
+
"data-slot": "sidebar-group",
|
|
147
|
+
className: cn("grid gap-1", className),
|
|
148
|
+
...props
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A heading for a group of links.
|
|
153
|
+
*
|
|
154
|
+
* Hidden rather than removed when the rail is collapsed, so the grouping
|
|
155
|
+
* survives for anyone not reading the layout visually.
|
|
156
|
+
*/
|
|
157
|
+
function SidebarGroupLabel({ className, ...props }) {
|
|
158
|
+
const { open, narrow } = useSidebar("SidebarGroupLabel");
|
|
159
|
+
return /* @__PURE__ */ jsx("div", {
|
|
160
|
+
"data-slot": "sidebar-group-label",
|
|
161
|
+
className: cn("px-2 text-xs font-medium tracking-wide text-muted-foreground uppercase", !open && !narrow && "sr-only", className),
|
|
162
|
+
...props
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function SidebarMenu({ className, ...props }) {
|
|
166
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
167
|
+
"data-slot": "sidebar-menu",
|
|
168
|
+
className: cn("grid gap-0.5", className),
|
|
169
|
+
...props
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function SidebarMenuItem({ className, ...props }) {
|
|
173
|
+
return /* @__PURE__ */ jsx("li", {
|
|
174
|
+
"data-slot": "sidebar-menu-item",
|
|
175
|
+
className: cn("min-w-0", className),
|
|
176
|
+
...props
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* One entry in the navigation.
|
|
181
|
+
*
|
|
182
|
+
* `aria-current="page"` when active, not merely a background colour: which page
|
|
183
|
+
* you are on is information, and a highlight conveys it to exactly one kind of
|
|
184
|
+
* reader.
|
|
185
|
+
*/
|
|
186
|
+
function SidebarMenuButton({ className, asChild = false, isActive = false, children, ...props }) {
|
|
187
|
+
const Component = asChild ? Slot.Root : "a";
|
|
188
|
+
return /* @__PURE__ */ jsx(Component, {
|
|
189
|
+
"data-slot": "sidebar-menu-button",
|
|
190
|
+
"data-active": isActive || void 0,
|
|
191
|
+
"aria-current": isActive ? "page" : void 0,
|
|
192
|
+
className: cn("flex h-9 w-full items-center gap-2.5 rounded-md px-2 text-sm", "transition-colors duration-[var(--duration-fast)]", "[&>svg]:size-4 [&>svg]:shrink-0", isActive ? "bg-accent font-medium text-accent-foreground" : "text-muted-foreground hover:bg-accent/60 hover:text-foreground", focusRing, disabledStyles, className),
|
|
193
|
+
...props,
|
|
194
|
+
children
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The text of a menu entry.
|
|
199
|
+
*
|
|
200
|
+
* Visually hidden rather than removed when the rail is collapsed. Removing it
|
|
201
|
+
* leaves a control whose only content is an icon, and an icon has no accessible
|
|
202
|
+
* name — which is how a collapsed sidebar becomes a column of buttons all
|
|
203
|
+
* announced as "link".
|
|
204
|
+
*/
|
|
205
|
+
function SidebarMenuLabel({ className, ...props }) {
|
|
206
|
+
const { open, narrow } = useSidebar("SidebarMenuLabel");
|
|
207
|
+
return /* @__PURE__ */ jsx("span", {
|
|
208
|
+
"data-slot": "sidebar-menu-label",
|
|
209
|
+
className: cn("min-w-0 truncate", !open && !narrow && "sr-only", className),
|
|
210
|
+
...props
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function SidebarTrigger({ className, label, ...props }) {
|
|
214
|
+
const { open, narrow, overlayOpen, toggle, sidebarId } = useSidebar("SidebarTrigger");
|
|
215
|
+
return /* @__PURE__ */ jsx("button", {
|
|
216
|
+
type: "button",
|
|
217
|
+
"data-slot": "sidebar-trigger",
|
|
218
|
+
"aria-expanded": narrow ? overlayOpen : open,
|
|
219
|
+
"aria-controls": narrow ? void 0 : sidebarId,
|
|
220
|
+
"aria-label": label ?? (narrow ? overlayOpen ? "Close navigation" : "Open navigation" : open ? "Collapse navigation" : "Expand navigation"),
|
|
221
|
+
onClick: toggle,
|
|
222
|
+
className: cn("grid size-9 shrink-0 place-items-center rounded-md text-muted-foreground", "transition-colors duration-[var(--duration-fast)] hover:bg-accent hover:text-foreground", focusRing, className),
|
|
223
|
+
...props,
|
|
224
|
+
children: /* @__PURE__ */ jsxs("svg", {
|
|
225
|
+
viewBox: "0 0 24 24",
|
|
226
|
+
fill: "none",
|
|
227
|
+
"aria-hidden": true,
|
|
228
|
+
className: "size-4",
|
|
229
|
+
children: [/* @__PURE__ */ jsx("rect", {
|
|
230
|
+
x: "3",
|
|
231
|
+
y: "4",
|
|
232
|
+
width: "18",
|
|
233
|
+
height: "16",
|
|
234
|
+
rx: "2",
|
|
235
|
+
stroke: "currentColor",
|
|
236
|
+
strokeWidth: "2"
|
|
237
|
+
}), /* @__PURE__ */ jsx("path", {
|
|
238
|
+
d: "M9 4v16",
|
|
239
|
+
stroke: "currentColor",
|
|
240
|
+
strokeWidth: "2"
|
|
241
|
+
})]
|
|
242
|
+
})
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/** The region the sidebar sits beside. */
|
|
246
|
+
function SidebarInset({ className, ...props }) {
|
|
247
|
+
return /* @__PURE__ */ jsx("main", {
|
|
248
|
+
"data-slot": "sidebar-inset",
|
|
249
|
+
className: cn("min-w-0 flex-1", className),
|
|
250
|
+
...props
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
export { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuLabel, SidebarProvider, SidebarTrigger };
|
|
255
|
+
|
|
256
|
+
//# sourceMappingURL=sidebar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sidebar.js","names":[],"sources":["../../../src/components/sidebar/sidebar.tsx"],"sourcesContent":["\"use client\";\n\nimport { Slot } from \"radix-ui\";\nimport {\n createContext,\n useContext,\n useId,\n useMemo,\n useState,\n useSyncExternalStore,\n type ComponentPropsWithRef,\n type ReactNode,\n} from \"react\";\n\nimport { Sheet, SheetContent, SheetTitle } from \"@/components/sheet\";\nimport { disabledStyles, focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * The application's own navigation.\n *\n * Two states, not one. On a wide screen it collapses to a rail of icons and\n * stays in the page; on a narrow one it is an overlay, which needs a focus trap\n * and an Escape key and is therefore a Sheet rather than a div with a\n * transform. Building one behaviour and hiding it at a breakpoint gets the\n * mobile case wrong every time — the page behind stays reachable by Tab while\n * the menu covers it.\n *\n * The collapsed rail is where these usually fail. Hiding the label leaves a\n * button whose only content is an icon, and an icon has no accessible name. The\n * label here is not removed but visually hidden, so the control keeps its name\n * whether or not the text is on screen.\n *\n * The overlay is mounted only on a narrow screen, and it has its own open\n * state. Mounting it always and hiding it with CSS was the first version, and\n * it was wrong in a way no visual check catches: a modal dialog that is\n * `display: none` is still modal. It marks the rest of the page `aria-hidden`,\n * traps focus in an element nothing can focus, and turns pointer events off on\n * the body — so on a desktop the whole application was invisible to a screen\n * reader and unclickable the moment the sidebar was open, which is its default.\n */\n\n/** Below Tailwind's `md` breakpoint, which is where the rail gives way to the overlay. */\nconst NARROW_QUERY = \"(max-width: 767px)\";\n\nfunction subscribeToNarrow(callback: () => void): () => void {\n const query = window.matchMedia(NARROW_QUERY);\n query.addEventListener(\"change\", callback);\n return () => {\n query.removeEventListener(\"change\", callback);\n };\n}\n\n/**\n * Whether the viewport is narrow enough for the overlay.\n *\n * The server snapshot says \"wide\": the rail renders in the HTML and CSS hides\n * it below the breakpoint, so a narrow screen shows nothing wrong before\n * hydration and the sheet mounts, closed, straight after.\n */\nfunction useIsNarrow(): boolean {\n return useSyncExternalStore(\n subscribeToNarrow,\n () => window.matchMedia(NARROW_QUERY).matches,\n () => false,\n );\n}\n\ninterface SidebarContextValue {\n /** The rail is expanded rather than collapsed. */\n open: boolean;\n setOpen: (open: boolean) => void;\n /** The viewport is narrow, so the navigation is an overlay. */\n narrow: boolean;\n /** The overlay is showing. Only meaningful when `narrow`. */\n overlayOpen: boolean;\n setOverlayOpen: (open: boolean) => void;\n /** Whichever of the two states applies on this viewport. */\n toggle: () => void;\n /** Id of the sidebar element, for the trigger's aria-controls. */\n sidebarId: string;\n}\n\nconst SidebarContext = createContext<SidebarContextValue | null>(null);\n\nfunction useSidebar(component: string): SidebarContextValue {\n const context = useContext(SidebarContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <SidebarProvider>.`);\n }\n return context;\n}\n\nexport interface SidebarProviderProps {\n /** Controlled open state. */\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n children?: ReactNode;\n}\n\nexport function SidebarProvider({\n open: openProp,\n defaultOpen = true,\n onOpenChange,\n children,\n}: SidebarProviderProps) {\n const [uncontrolled, setUncontrolled] = useState(defaultOpen);\n const open = openProp ?? uncontrolled;\n // Separate from `open`, and never true to begin with: \"expanded by default\"\n // is a fact about the rail. An overlay that covers the page on first load is\n // a menu nobody asked for.\n const [overlayOpen, setOverlayOpen] = useState(false);\n const narrow = useIsNarrow();\n const sidebarId = useId();\n\n const value = useMemo<SidebarContextValue>(() => {\n const setOpen = (next: boolean) => {\n if (openProp === undefined) setUncontrolled(next);\n onOpenChange?.(next);\n };\n\n return {\n open,\n setOpen,\n narrow,\n overlayOpen,\n setOverlayOpen,\n toggle: () => {\n if (narrow) setOverlayOpen(!overlayOpen);\n else setOpen(!open);\n },\n sidebarId,\n };\n }, [open, openProp, onOpenChange, narrow, overlayOpen, sidebarId]);\n\n return <SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>;\n}\n\nexport interface SidebarProps extends ComponentPropsWithRef<\"aside\"> {\n /**\n * Names the navigation landmark.\n *\n * Required, not optional. A page has more than one navigation region — this,\n * a breadcrumb, a footer — and three landmarks all called \"navigation\" is a\n * list a screen reader user cannot choose from.\n */\n label: string;\n /** Title for the overlay on a narrow screen, where it is a dialog. */\n mobileTitle?: string;\n}\n\nexport function Sidebar({ className, label, mobileTitle, children, ...props }: SidebarProps) {\n const { open, narrow, overlayOpen, setOverlayOpen, sidebarId } = useSidebar(\"Sidebar\");\n\n return (\n <>\n {/*\n The overlay, on a narrow screen. A Sheet rather than a styled div: it\n traps focus, closes on Escape and marks the rest of the page inert,\n none of which a transform does — and without them the page behind stays\n reachable by Tab while the menu covers it.\n\n Mounted only when the screen is narrow. Those three behaviours are\n exactly why it must not exist on a wide one: see the note at the top.\n */}\n {narrow ? (\n <Sheet open={overlayOpen} onOpenChange={setOverlayOpen}>\n <SheetContent side=\"left\" className=\"w-72 p-0\">\n <SheetTitle className=\"sr-only\">{mobileTitle ?? label}</SheetTitle>\n <nav aria-label={label} className=\"flex h-full flex-col gap-2 p-3\">\n {children}\n </nav>\n </SheetContent>\n </Sheet>\n ) : null}\n\n <aside\n id={sidebarId}\n data-slot=\"sidebar\"\n data-state={open ? \"expanded\" : \"collapsed\"}\n className={cn(\n \"hidden shrink-0 border-e border-border bg-background md:block\",\n \"transition-[width] duration-[var(--duration-normal)] ease-[var(--ease-out-quint)]\",\n open ? \"w-60\" : \"w-14\",\n className,\n )}\n {...props}\n >\n <nav aria-label={label} className=\"flex h-full flex-col gap-2 p-3\">\n {children}\n </nav>\n </aside>\n </>\n );\n}\n\nexport function SidebarHeader({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n return (\n <div\n data-slot=\"sidebar-header\"\n className={cn(\"flex min-h-9 items-center gap-2 px-1\", className)}\n {...props}\n />\n );\n}\n\nexport function SidebarContent({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n return (\n <div\n data-slot=\"sidebar-content\"\n className={cn(\"flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto\", className)}\n {...props}\n />\n );\n}\n\nexport function SidebarFooter({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n return <div data-slot=\"sidebar-footer\" className={cn(\"mt-auto\", className)} {...props} />;\n}\n\nexport function SidebarGroup({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n return <div data-slot=\"sidebar-group\" className={cn(\"grid gap-1\", className)} {...props} />;\n}\n\n/**\n * A heading for a group of links.\n *\n * Hidden rather than removed when the rail is collapsed, so the grouping\n * survives for anyone not reading the layout visually.\n */\nexport function SidebarGroupLabel({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n const { open, narrow } = useSidebar(\"SidebarGroupLabel\");\n\n return (\n <div\n data-slot=\"sidebar-group-label\"\n className={cn(\n \"px-2 text-xs font-medium tracking-wide text-muted-foreground uppercase\",\n // The rail's collapsed state is not the overlay's: a sheet is always\n // wide enough for its labels.\n !open && !narrow && \"sr-only\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function SidebarMenu({ className, ...props }: ComponentPropsWithRef<\"ul\">) {\n return <ul data-slot=\"sidebar-menu\" className={cn(\"grid gap-0.5\", className)} {...props} />;\n}\n\nexport function SidebarMenuItem({ className, ...props }: ComponentPropsWithRef<\"li\">) {\n return <li data-slot=\"sidebar-menu-item\" className={cn(\"min-w-0\", className)} {...props} />;\n}\n\nexport interface SidebarMenuButtonProps extends ComponentPropsWithRef<\"a\"> {\n /** Render the child as the control, for a router's own link or a button. */\n asChild?: boolean;\n /** The page currently being shown. */\n isActive?: boolean;\n}\n\n/**\n * One entry in the navigation.\n *\n * `aria-current=\"page\"` when active, not merely a background colour: which page\n * you are on is information, and a highlight conveys it to exactly one kind of\n * reader.\n */\nexport function SidebarMenuButton({\n className,\n asChild = false,\n isActive = false,\n children,\n ...props\n}: SidebarMenuButtonProps) {\n const Component = asChild ? Slot.Root : \"a\";\n\n return (\n <Component\n data-slot=\"sidebar-menu-button\"\n data-active={isActive || undefined}\n aria-current={isActive ? \"page\" : undefined}\n className={cn(\n \"flex h-9 w-full items-center gap-2.5 rounded-md px-2 text-sm\",\n \"transition-colors duration-[var(--duration-fast)]\",\n \"[&>svg]:size-4 [&>svg]:shrink-0\",\n isActive\n ? \"bg-accent font-medium text-accent-foreground\"\n : \"text-muted-foreground hover:bg-accent/60 hover:text-foreground\",\n focusRing,\n disabledStyles,\n className,\n )}\n {...props}\n >\n {children}\n </Component>\n );\n}\n\n/**\n * The text of a menu entry.\n *\n * Visually hidden rather than removed when the rail is collapsed. Removing it\n * leaves a control whose only content is an icon, and an icon has no accessible\n * name — which is how a collapsed sidebar becomes a column of buttons all\n * announced as \"link\".\n */\nexport function SidebarMenuLabel({ className, ...props }: ComponentPropsWithRef<\"span\">) {\n const { open, narrow } = useSidebar(\"SidebarMenuLabel\");\n\n return (\n <span\n data-slot=\"sidebar-menu-label\"\n className={cn(\"min-w-0 truncate\", !open && !narrow && \"sr-only\", className)}\n {...props}\n />\n );\n}\n\nexport interface SidebarTriggerProps extends ComponentPropsWithRef<\"button\"> {\n /** Overrides the label, which otherwise says what pressing it will do. */\n label?: string;\n}\n\nexport function SidebarTrigger({ className, label, ...props }: SidebarTriggerProps) {\n const { open, narrow, overlayOpen, toggle, sidebarId } = useSidebar(\"SidebarTrigger\");\n\n const expanded = narrow ? overlayOpen : open;\n const fallback = narrow\n ? overlayOpen\n ? \"Close navigation\"\n : \"Open navigation\"\n : open\n ? \"Collapse navigation\"\n : \"Expand navigation\";\n\n return (\n <button\n type=\"button\"\n data-slot=\"sidebar-trigger\"\n aria-expanded={expanded}\n // The rail is what this controls. The overlay is a dialog that names\n // itself, and pointing at an element CSS has hidden helps nobody.\n aria-controls={narrow ? undefined : sidebarId}\n // Says what pressing it does, not what the state currently is. \"Collapse\n // navigation\" while it is expanded is the useful half.\n aria-label={label ?? fallback}\n onClick={toggle}\n className={cn(\n \"grid size-9 shrink-0 place-items-center rounded-md text-muted-foreground\",\n \"transition-colors duration-[var(--duration-fast)] hover:bg-accent hover:text-foreground\",\n focusRing,\n className,\n )}\n {...props}\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden className=\"size-4\">\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\" stroke=\"currentColor\" strokeWidth=\"2\" />\n <path d=\"M9 4v16\" stroke=\"currentColor\" strokeWidth=\"2\" />\n </svg>\n </button>\n );\n}\n\n/** The region the sidebar sits beside. */\nexport function SidebarInset({ className, ...props }: ComponentPropsWithRef<\"main\">) {\n return (\n <main data-slot=\"sidebar-inset\" className={cn(\"min-w-0 flex-1\", className)} {...props} />\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineMeta } from "../../registry/schema.js";
|
|
2
|
+
//#region src/components/sync-status/meta.ts
|
|
3
|
+
const meta = defineMeta({
|
|
4
|
+
name: "sync-status",
|
|
5
|
+
title: "Sync Status",
|
|
6
|
+
description: "Offline, saving, saved or failed — with the count of changes being held.",
|
|
7
|
+
category: "feedback",
|
|
8
|
+
status: "stable",
|
|
9
|
+
dependencies: [],
|
|
10
|
+
registryDependencies: ["button"],
|
|
11
|
+
files: ["sync-status.tsx"],
|
|
12
|
+
a11y: "The word is the status and the coloured dot is decoration, hidden from assistive technology. The visible text is not a live region: every save flips it and a region on it would narrate the whole session. A separate region, present from the first render, announces transitions only — went offline with what will happen to the changes, came back with what is being saved, could not save, saved after a failure — and never the churn of saving itself. Announcing is on by default because going offline is the one thing a reader who is typing has to be told, and it can be turned off. The server render assumes online, since a page with no interface to report has no business saying offline."
|
|
13
|
+
});
|
|
14
|
+
//#endregion
|
|
15
|
+
export { meta };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/sync-status/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"sync-status\",\n title: \"Sync Status\",\n description: \"Offline, saving, saved or failed — with the count of changes being held.\",\n category: \"feedback\",\n status: \"stable\",\n dependencies: [],\n registryDependencies: [\"button\"],\n files: [\"sync-status.tsx\"],\n a11y:\n \"The word is the status and the coloured dot is decoration, hidden from assistive \" +\n \"technology. The visible text is not a live region: every save flips it and a region on it \" +\n \"would narrate the whole session. A separate region, present from the first render, \" +\n \"announces transitions only — went offline with what will happen to the changes, came back \" +\n \"with what is being saved, could not save, saved after a failure — and never the churn of \" +\n \"saving itself. Announcing is on by default because going offline is the one thing a reader \" +\n \"who is typing has to be told, and it can be turned off. The server render assumes online, \" +\n \"since a page with no interface to report has no business saying offline.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aAAa;CACb,UAAU;CACV,QAAQ;CACR,cAAc,CAAC;CACf,sBAAsB,CAAC,QAAQ;CAC/B,OAAO,CAAC,iBAAiB;CACzB,MACE;AAQJ,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ComponentPropsWithRef } from "react";
|
|
2
|
+
//#region src/components/sync-status/sync-status.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* "Offline — 3 changes will sync when you're back."
|
|
5
|
+
*
|
|
6
|
+
* Linear, Notion, Figma and Google Docs each built this, and nothing in a
|
|
7
|
+
* component library touches network state at all. It is small, and it is
|
|
8
|
+
* the difference between an app that loses work and one that says it is
|
|
9
|
+
* holding it: the moment a request fails, the reader needs to know whether
|
|
10
|
+
* to keep typing, and whether what they typed a minute ago is anywhere.
|
|
11
|
+
*
|
|
12
|
+
* Two things this gets right that the obvious version gets wrong.
|
|
13
|
+
*
|
|
14
|
+
* `navigator.onLine` is believed in one direction only. False is reliable —
|
|
15
|
+
* the interface is down. True means only that there is an interface, not
|
|
16
|
+
* that the server is reachable, so an application's own failed request is
|
|
17
|
+
* the real signal and takes precedence. The hook is exported for the rest
|
|
18
|
+
* of the app to share the same reading.
|
|
19
|
+
*
|
|
20
|
+
* Announcements are for transitions, not states. Every save flips
|
|
21
|
+
* "Saving…" to "Saved", and a live region on that text narrates the whole
|
|
22
|
+
* session. So the visible text is not live; a separate region says something
|
|
23
|
+
* only when the situation changes — went offline, came back, could not sync —
|
|
24
|
+
* which is when a reader who is typing needs to be told.
|
|
25
|
+
*/
|
|
26
|
+
type SyncState = "synced" | "pending" | "syncing" | "offline" | "error";
|
|
27
|
+
interface SyncInput {
|
|
28
|
+
/** From `useOnlineStatus`, or your own reading. */
|
|
29
|
+
online: boolean;
|
|
30
|
+
/** Changes waiting to be sent. */
|
|
31
|
+
pending?: number;
|
|
32
|
+
/** A send is in flight. */
|
|
33
|
+
syncing?: boolean;
|
|
34
|
+
/** The last send failed, in the reader's language. Takes precedence over `online`. */
|
|
35
|
+
error?: string | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Which state the inputs amount to. Error beats offline beats syncing beats
|
|
39
|
+
* pending beats synced: each one is the thing the reader most needs to know
|
|
40
|
+
* given the others.
|
|
41
|
+
*/
|
|
42
|
+
declare function syncState({ online, pending, syncing, error }: SyncInput): SyncState;
|
|
43
|
+
/** The state as a sentence. Used for the visible text and the announcement. */
|
|
44
|
+
declare function describeSyncState(state: SyncState, pending?: number, error?: string | null): string;
|
|
45
|
+
/** Whether the browser believes it has a network. False is reliable; true is not. */
|
|
46
|
+
declare function useOnlineStatus(): boolean;
|
|
47
|
+
interface SyncStatusProps extends Omit<ComponentPropsWithRef<"div">, "children">, Omit<SyncInput, "online"> {
|
|
48
|
+
/** Omit to read the browser's own status. */
|
|
49
|
+
online?: boolean;
|
|
50
|
+
/** When the last save succeeded. Shown as a time, not a relative phrase, so no clock is needed. */
|
|
51
|
+
lastSyncedAt?: Date;
|
|
52
|
+
locale?: string;
|
|
53
|
+
/** Offered when a save failed. */
|
|
54
|
+
onRetry?: () => void;
|
|
55
|
+
/**
|
|
56
|
+
* Announce transitions. On by default, because going offline is the one
|
|
57
|
+
* thing a reader who is typing has to be told; syncing itself never
|
|
58
|
+
* announces, whichever way this is set.
|
|
59
|
+
*/
|
|
60
|
+
announce?: boolean;
|
|
61
|
+
}
|
|
62
|
+
declare function SyncStatus({ className, online: onlineProp, pending, syncing, error, lastSyncedAt, locale, onRetry, announce, ...props }: SyncStatusProps): import("react").JSX.Element;
|
|
63
|
+
//#endregion
|
|
64
|
+
export { SyncInput, SyncState, SyncStatus, SyncStatusProps, describeSyncState, syncState, useOnlineStatus };
|
|
65
|
+
//# sourceMappingURL=sync-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-status.d.ts","names":[],"sources":["../../../src/components/sync-status/sync-status.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;KA+BY;UAEK;;EAEf;;EAEA;;EAEA;;EAEA;;;;;;;iBAQc,YACd,QACA,SACA,SACA,SACC,YAAY;;iBAWC,kBACd,OAAO,WACP,kBACA;;iBAmCc;UAIC,wBACP,KAAK,2CAA2C,KAAK;;EAE7D;;EAEA,eAAe;EACf;;EAEA;;;;;;EAMA;;iBAGc,aACd,WACA,QAAQ,YACR,SACA,SACA,OACA,cACA,QACA,SACA,aACG,SACF,kCAAe,IAAA"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { Button } from "../button/button.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { useState, useSyncExternalStore } from "react";
|
|
6
|
+
//#region src/components/sync-status/sync-status.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Which state the inputs amount to. Error beats offline beats syncing beats
|
|
9
|
+
* pending beats synced: each one is the thing the reader most needs to know
|
|
10
|
+
* given the others.
|
|
11
|
+
*/
|
|
12
|
+
function syncState({ online, pending = 0, syncing = false, error }) {
|
|
13
|
+
if (error) return "error";
|
|
14
|
+
if (!online) return "offline";
|
|
15
|
+
if (syncing) return "syncing";
|
|
16
|
+
if (pending > 0) return "pending";
|
|
17
|
+
return "synced";
|
|
18
|
+
}
|
|
19
|
+
const changes = (n) => `${String(n)} ${n === 1 ? "change" : "changes"}`;
|
|
20
|
+
/** The state as a sentence. Used for the visible text and the announcement. */
|
|
21
|
+
function describeSyncState(state, pending = 0, error) {
|
|
22
|
+
switch (state) {
|
|
23
|
+
case "synced": return "All changes saved";
|
|
24
|
+
case "pending": return `${changes(pending)} waiting to save`;
|
|
25
|
+
case "syncing": return pending > 0 ? `Saving ${changes(pending)}…` : "Saving…";
|
|
26
|
+
case "offline": return pending > 0 ? `Offline. ${changes(pending)} will save when you're back online.` : "Offline. Changes will save when you're back online.";
|
|
27
|
+
case "error": return pending > 0 ? `Could not save ${changes(pending)}. ${error ?? ""}`.trim() : `Could not save. ${error ?? ""}`.trim();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function subscribeOnline(callback) {
|
|
31
|
+
window.addEventListener("online", callback);
|
|
32
|
+
window.addEventListener("offline", callback);
|
|
33
|
+
return () => {
|
|
34
|
+
window.removeEventListener("online", callback);
|
|
35
|
+
window.removeEventListener("offline", callback);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const readOnline = () => navigator.onLine;
|
|
39
|
+
const readOnlineOnServer = () => true;
|
|
40
|
+
/** Whether the browser believes it has a network. False is reliable; true is not. */
|
|
41
|
+
function useOnlineStatus() {
|
|
42
|
+
return useSyncExternalStore(subscribeOnline, readOnline, readOnlineOnServer);
|
|
43
|
+
}
|
|
44
|
+
function SyncStatus({ className, online: onlineProp, pending = 0, syncing = false, error, lastSyncedAt, locale, onRetry, announce = true, ...props }) {
|
|
45
|
+
const browserOnline = useOnlineStatus();
|
|
46
|
+
const state = syncState({
|
|
47
|
+
online: onlineProp ?? browserOnline,
|
|
48
|
+
pending,
|
|
49
|
+
syncing,
|
|
50
|
+
error
|
|
51
|
+
});
|
|
52
|
+
const text = describeSyncState(state, pending, error);
|
|
53
|
+
const [seen, setSeen] = useState(state);
|
|
54
|
+
const [announcement, setAnnouncement] = useState("");
|
|
55
|
+
const [recovering, setRecovering] = useState(false);
|
|
56
|
+
if (seen !== state) {
|
|
57
|
+
const from = seen;
|
|
58
|
+
setSeen(state);
|
|
59
|
+
if (state === "offline" || state === "error") {
|
|
60
|
+
setAnnouncement(text);
|
|
61
|
+
setRecovering(false);
|
|
62
|
+
} else if (from === "offline") setAnnouncement(pending > 0 ? `Back online. Saving ${changes(pending)}.` : "Back online.");
|
|
63
|
+
else if ((from === "error" || recovering) && state === "synced") {
|
|
64
|
+
setAnnouncement("Saved.");
|
|
65
|
+
setRecovering(false);
|
|
66
|
+
} else if (from === "error") setRecovering(true);
|
|
67
|
+
}
|
|
68
|
+
const savedAt = lastSyncedAt !== void 0 ? new Intl.DateTimeFormat(locale, { timeStyle: "short" }).format(lastSyncedAt) : null;
|
|
69
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
70
|
+
"data-slot": "sync-status",
|
|
71
|
+
"data-state": state,
|
|
72
|
+
className: cn("flex flex-wrap items-center gap-x-2 gap-y-1 text-xs", className),
|
|
73
|
+
...props,
|
|
74
|
+
children: [
|
|
75
|
+
/* @__PURE__ */ jsx("span", {
|
|
76
|
+
"aria-hidden": "true",
|
|
77
|
+
className: cn("size-2 shrink-0 rounded-full", state === "synced" && "bg-success", state === "pending" && "bg-muted-foreground", state === "syncing" && "animate-pulse-soft bg-info", state === "offline" && "bg-warning", state === "error" && "bg-destructive")
|
|
78
|
+
}),
|
|
79
|
+
/* @__PURE__ */ jsxs("span", {
|
|
80
|
+
"data-slot": "sync-status-text",
|
|
81
|
+
className: cn(state === "offline" && "text-warning", state === "error" && "text-destructive", (state === "synced" || state === "pending" || state === "syncing") && "text-muted-foreground"),
|
|
82
|
+
children: [text, savedAt && state === "synced" ? ` at ${savedAt}` : null]
|
|
83
|
+
}),
|
|
84
|
+
state === "error" && onRetry ? /* @__PURE__ */ jsx(Button, {
|
|
85
|
+
variant: "outline",
|
|
86
|
+
size: "sm",
|
|
87
|
+
className: "h-6 px-2 text-xs",
|
|
88
|
+
onClick: onRetry,
|
|
89
|
+
children: "Retry"
|
|
90
|
+
}) : null,
|
|
91
|
+
/* @__PURE__ */ jsx("span", {
|
|
92
|
+
role: "status",
|
|
93
|
+
"aria-live": announce ? "polite" : "off",
|
|
94
|
+
className: "sr-only",
|
|
95
|
+
children: announce ? announcement : ""
|
|
96
|
+
})
|
|
97
|
+
]
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
export { SyncStatus, describeSyncState, syncState, useOnlineStatus };
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=sync-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-status.js","names":[],"sources":["../../../src/components/sync-status/sync-status.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState, useSyncExternalStore, type ComponentPropsWithRef } from \"react\";\n\nimport { Button } from \"@/components/button\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * \"Offline — 3 changes will sync when you're back.\"\n *\n * Linear, Notion, Figma and Google Docs each built this, and nothing in a\n * component library touches network state at all. It is small, and it is\n * the difference between an app that loses work and one that says it is\n * holding it: the moment a request fails, the reader needs to know whether\n * to keep typing, and whether what they typed a minute ago is anywhere.\n *\n * Two things this gets right that the obvious version gets wrong.\n *\n * `navigator.onLine` is believed in one direction only. False is reliable —\n * the interface is down. True means only that there is an interface, not\n * that the server is reachable, so an application's own failed request is\n * the real signal and takes precedence. The hook is exported for the rest\n * of the app to share the same reading.\n *\n * Announcements are for transitions, not states. Every save flips\n * \"Saving…\" to \"Saved\", and a live region on that text narrates the whole\n * session. So the visible text is not live; a separate region says something\n * only when the situation changes — went offline, came back, could not sync —\n * which is when a reader who is typing needs to be told.\n */\n\nexport type SyncState = \"synced\" | \"pending\" | \"syncing\" | \"offline\" | \"error\";\n\nexport interface SyncInput {\n /** From `useOnlineStatus`, or your own reading. */\n online: boolean;\n /** Changes waiting to be sent. */\n pending?: number;\n /** A send is in flight. */\n syncing?: boolean;\n /** The last send failed, in the reader's language. Takes precedence over `online`. */\n error?: string | null;\n}\n\n/**\n * Which state the inputs amount to. Error beats offline beats syncing beats\n * pending beats synced: each one is the thing the reader most needs to know\n * given the others.\n */\nexport function syncState({\n online,\n pending = 0,\n syncing = false,\n error,\n}: SyncInput): SyncState {\n if (error) return \"error\";\n if (!online) return \"offline\";\n if (syncing) return \"syncing\";\n if (pending > 0) return \"pending\";\n return \"synced\";\n}\n\nconst changes = (n: number) => `${String(n)} ${n === 1 ? \"change\" : \"changes\"}`;\n\n/** The state as a sentence. Used for the visible text and the announcement. */\nexport function describeSyncState(\n state: SyncState,\n pending = 0,\n error?: string | null,\n): string {\n switch (state) {\n case \"synced\":\n return \"All changes saved\";\n case \"pending\":\n return `${changes(pending)} waiting to save`;\n case \"syncing\":\n return pending > 0 ? `Saving ${changes(pending)}…` : \"Saving…\";\n case \"offline\":\n return pending > 0\n ? `Offline. ${changes(pending)} will save when you're back online.`\n : \"Offline. Changes will save when you're back online.\";\n case \"error\":\n return pending > 0\n ? `Could not save ${changes(pending)}. ${error ?? \"\"}`.trim()\n : `Could not save. ${error ?? \"\"}`.trim();\n }\n}\n\n/* The browser's reading of the network, as an external store. The server\n snapshot is \"online\": a page rendered on the server has no interface to\n report, and rendering \"offline\" into HTML would be a lie until hydration. */\nfunction subscribeOnline(callback: () => void) {\n window.addEventListener(\"online\", callback);\n window.addEventListener(\"offline\", callback);\n return () => {\n window.removeEventListener(\"online\", callback);\n window.removeEventListener(\"offline\", callback);\n };\n}\nconst readOnline = () => navigator.onLine;\nconst readOnlineOnServer = () => true;\n\n/** Whether the browser believes it has a network. False is reliable; true is not. */\nexport function useOnlineStatus(): boolean {\n return useSyncExternalStore(subscribeOnline, readOnline, readOnlineOnServer);\n}\n\nexport interface SyncStatusProps\n extends Omit<ComponentPropsWithRef<\"div\">, \"children\">, Omit<SyncInput, \"online\"> {\n /** Omit to read the browser's own status. */\n online?: boolean;\n /** When the last save succeeded. Shown as a time, not a relative phrase, so no clock is needed. */\n lastSyncedAt?: Date;\n locale?: string;\n /** Offered when a save failed. */\n onRetry?: () => void;\n /**\n * Announce transitions. On by default, because going offline is the one\n * thing a reader who is typing has to be told; syncing itself never\n * announces, whichever way this is set.\n */\n announce?: boolean;\n}\n\nexport function SyncStatus({\n className,\n online: onlineProp,\n pending = 0,\n syncing = false,\n error,\n lastSyncedAt,\n locale,\n onRetry,\n announce = true,\n ...props\n}: SyncStatusProps) {\n const browserOnline = useOnlineStatus();\n const online = onlineProp ?? browserOnline;\n const state = syncState({ online, pending, syncing, error });\n const text = describeSyncState(state, pending, error);\n\n // Said once, when the situation changes. Adjusted during render so the\n // announcement lands in the same commit as the state it describes. Syncing\n // and pending are the ordinary churn of saving and are never announced.\n const [seen, setSeen] = useState(state);\n const [announcement, setAnnouncement] = useState(\"\");\n // A failure is over when a save lands, not when the retry starts, so the\n // recovery is remembered across the syncing in between.\n const [recovering, setRecovering] = useState(false);\n if (seen !== state) {\n const from = seen;\n setSeen(state);\n if (state === \"offline\" || state === \"error\") {\n setAnnouncement(text);\n setRecovering(false);\n } else if (from === \"offline\") {\n setAnnouncement(\n pending > 0 ? `Back online. Saving ${changes(pending)}.` : \"Back online.\",\n );\n } else if ((from === \"error\" || recovering) && state === \"synced\") {\n setAnnouncement(\"Saved.\");\n setRecovering(false);\n } else if (from === \"error\") {\n setRecovering(true);\n }\n }\n\n const savedAt =\n lastSyncedAt !== undefined\n ? new Intl.DateTimeFormat(locale, { timeStyle: \"short\" }).format(lastSyncedAt)\n : null;\n\n return (\n <div\n data-slot=\"sync-status\"\n data-state={state}\n className={cn(\"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs\", className)}\n {...props}\n >\n {/* The dot is decoration. The word is the status. */}\n <span\n aria-hidden=\"true\"\n className={cn(\n \"size-2 shrink-0 rounded-full\",\n state === \"synced\" && \"bg-success\",\n state === \"pending\" && \"bg-muted-foreground\",\n state === \"syncing\" && \"animate-pulse-soft bg-info\",\n state === \"offline\" && \"bg-warning\",\n state === \"error\" && \"bg-destructive\",\n )}\n />\n <span\n data-slot=\"sync-status-text\"\n className={cn(\n state === \"offline\" && \"text-warning\",\n state === \"error\" && \"text-destructive\",\n (state === \"synced\" || state === \"pending\" || state === \"syncing\") &&\n \"text-muted-foreground\",\n )}\n >\n {text}\n {savedAt && state === \"synced\" ? ` at ${savedAt}` : null}\n </span>\n\n {state === \"error\" && onRetry ? (\n <Button variant=\"outline\" size=\"sm\" className=\"h-6 px-2 text-xs\" onClick={onRetry}>\n Retry\n </Button>\n ) : null}\n\n {/* Present from the start, so the first transition is heard. */}\n <span role=\"status\" aria-live={announce ? \"polite\" : \"off\"} className=\"sr-only\">\n {announce ? announcement : \"\"}\n </span>\n </div>\n );\n}\n"],"mappings":"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"}
|