@nanisoft/prism-ui 0.10.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/dist/.tsbuildinfo +1 -1
- package/dist/blocks/app-shell-01/app-shell.js +4 -4
- package/dist/blocks/app-shell-01/index.d.ts +2 -2
- package/dist/blocks/app-shell-01/index.js +1 -1
- package/dist/blocks/auth-form-01/auth-form.d.ts +11 -1
- package/dist/blocks/auth-form-01/auth-form.d.ts.map +1 -1
- package/dist/blocks/auth-form-01/auth-form.js +11 -8
- package/dist/blocks/auth-form-01/index.d.ts +2 -2
- package/dist/blocks/auth-form-01/index.js +1 -1
- package/dist/blocks/cta-01/cta.d.ts +26 -21
- package/dist/blocks/cta-01/cta.d.ts.map +1 -1
- package/dist/blocks/cta-01/cta.js +2 -2
- package/dist/blocks/cta-01/index.d.ts +2 -2
- package/dist/blocks/cta-01/index.js +1 -1
- package/dist/blocks/data-table-01/data-table.js +7 -7
- package/dist/blocks/data-table-01/index.d.ts +2 -2
- package/dist/blocks/data-table-01/index.js +1 -1
- package/dist/blocks/empty-state-01/empty-state.d.ts +143 -0
- package/dist/blocks/empty-state-01/empty-state.d.ts.map +1 -0
- package/dist/blocks/empty-state-01/empty-state.js +104 -0
- package/dist/blocks/empty-state-01/index.d.ts +3 -0
- package/dist/blocks/empty-state-01/index.d.ts.map +1 -0
- package/dist/blocks/empty-state-01/index.js +1 -0
- package/dist/blocks/feature-grid-01/feature-grid.d.ts +28 -1
- package/dist/blocks/feature-grid-01/feature-grid.d.ts.map +1 -1
- package/dist/blocks/feature-grid-01/feature-grid.js +34 -3
- package/dist/blocks/feature-grid-01/index.d.ts +2 -2
- package/dist/blocks/feature-grid-01/index.js +1 -1
- package/dist/blocks/hero-01/hero.d.ts +35 -15
- package/dist/blocks/hero-01/hero.d.ts.map +1 -1
- package/dist/blocks/hero-01/hero.js +14 -9
- package/dist/blocks/hero-01/index.d.ts +2 -1
- package/dist/blocks/hero-01/index.d.ts.map +1 -1
- package/dist/blocks/hero-01/index.js +1 -1
- package/dist/blocks/index.d.ts +20 -20
- package/dist/blocks/index.js +19 -19
- package/dist/blocks/instrument-panel-01/index.d.ts +2 -2
- package/dist/blocks/instrument-panel-01/index.js +1 -1
- package/dist/blocks/instrument-panel-01/instrument-panel.js +1 -1
- package/dist/blocks/logo-strip-01/index.d.ts +2 -2
- package/dist/blocks/logo-strip-01/index.js +1 -1
- package/dist/blocks/logo-strip-01/logo-strip.d.ts +1 -1
- package/dist/blocks/logo-strip-01/logo-strip.js +2 -2
- package/dist/blocks/note-grid-01/index.d.ts +2 -2
- package/dist/blocks/note-grid-01/index.js +1 -1
- package/dist/blocks/note-grid-01/note-grid.d.ts +1 -1
- package/dist/blocks/note-grid-01/note-grid.js +1 -1
- package/dist/blocks/page-header-01/index.d.ts +2 -2
- package/dist/blocks/page-header-01/index.js +1 -1
- package/dist/blocks/page-header-01/page-header.d.ts +1 -1
- package/dist/blocks/page-header-01/page-header.js +4 -4
- package/dist/blocks/pricing-01/index.d.ts +1 -1
- package/dist/blocks/pricing-01/index.js +1 -1
- package/dist/blocks/pricing-01/pricing.d.ts +8 -1
- package/dist/blocks/pricing-01/pricing.d.ts.map +1 -1
- package/dist/blocks/pricing-01/pricing.js +15 -5
- package/dist/blocks/process-flow-01/index.d.ts +3 -0
- package/dist/blocks/process-flow-01/index.d.ts.map +1 -0
- package/dist/blocks/process-flow-01/index.js +1 -0
- package/dist/blocks/process-flow-01/process-flow.d.ts +118 -0
- package/dist/blocks/process-flow-01/process-flow.d.ts.map +1 -0
- package/dist/blocks/process-flow-01/process-flow.js +71 -0
- package/dist/blocks/process-rail-01/index.d.ts +2 -2
- package/dist/blocks/process-rail-01/index.js +1 -1
- package/dist/blocks/process-rail-01/process-rail.d.ts +1 -1
- package/dist/blocks/process-rail-01/process-rail.js +2 -2
- package/dist/blocks/product-grid-01/index.d.ts +2 -2
- package/dist/blocks/product-grid-01/index.js +1 -1
- package/dist/blocks/product-grid-01/product-grid.d.ts +37 -5
- package/dist/blocks/product-grid-01/product-grid.d.ts.map +1 -1
- package/dist/blocks/product-grid-01/product-grid.js +20 -6
- package/dist/blocks/run-console-01/index.d.ts +3 -0
- package/dist/blocks/run-console-01/index.d.ts.map +1 -0
- package/dist/blocks/run-console-01/index.js +1 -0
- package/dist/blocks/run-console-01/run-console.d.ts +111 -0
- package/dist/blocks/run-console-01/run-console.d.ts.map +1 -0
- package/dist/blocks/run-console-01/run-console.js +48 -0
- package/dist/blocks/settings-panel-01/index.d.ts +2 -2
- package/dist/blocks/settings-panel-01/index.js +1 -1
- package/dist/blocks/settings-panel-01/settings-panel.d.ts +11 -1
- package/dist/blocks/settings-panel-01/settings-panel.d.ts.map +1 -1
- package/dist/blocks/settings-panel-01/settings-panel.js +15 -10
- package/dist/blocks/site-footer/index.d.ts +2 -2
- package/dist/blocks/site-footer/index.js +1 -1
- package/dist/blocks/site-footer/site-footer.d.ts +1 -1
- package/dist/blocks/site-footer/site-footer.js +2 -2
- package/dist/blocks/site-header/index.d.ts +2 -2
- package/dist/blocks/site-header/index.js +1 -1
- package/dist/blocks/site-header/site-header.d.ts +2 -2
- package/dist/blocks/site-header/site-header.js +3 -3
- package/dist/blocks/site-navbar/index.d.ts +3 -0
- package/dist/blocks/site-navbar/index.d.ts.map +1 -0
- package/dist/blocks/site-navbar/index.js +1 -0
- package/dist/blocks/site-navbar/mobile-nav.d.ts +48 -0
- package/dist/blocks/site-navbar/mobile-nav.d.ts.map +1 -0
- package/dist/blocks/site-navbar/mobile-nav.js +35 -0
- package/dist/blocks/site-navbar/mode-toggle.d.ts +39 -0
- package/dist/blocks/site-navbar/mode-toggle.d.ts.map +1 -0
- package/dist/blocks/site-navbar/mode-toggle.js +39 -0
- package/dist/blocks/site-navbar/navbar-controls.d.ts +79 -0
- package/dist/blocks/site-navbar/navbar-controls.d.ts.map +1 -0
- package/dist/blocks/site-navbar/navbar-controls.js +40 -0
- package/dist/blocks/site-navbar/search-entry.d.ts +29 -0
- package/dist/blocks/site-navbar/search-entry.d.ts.map +1 -0
- package/dist/blocks/site-navbar/search-entry.js +52 -0
- package/dist/blocks/site-navbar/site-navbar.d.ts +212 -0
- package/dist/blocks/site-navbar/site-navbar.d.ts.map +1 -0
- package/dist/blocks/site-navbar/site-navbar.js +61 -0
- package/dist/blocks/site-navbar/sites-menu.d.ts +40 -0
- package/dist/blocks/site-navbar/sites-menu.d.ts.map +1 -0
- package/dist/blocks/site-navbar/sites-menu.js +48 -0
- package/dist/blocks/site-navbar/theme-menu.d.ts +47 -0
- package/dist/blocks/site-navbar/theme-menu.d.ts.map +1 -0
- package/dist/blocks/site-navbar/theme-menu.js +55 -0
- package/dist/blocks/stack-grid-01/index.d.ts +2 -2
- package/dist/blocks/stack-grid-01/index.js +1 -1
- package/dist/blocks/stack-grid-01/stack-grid.d.ts +42 -11
- package/dist/blocks/stack-grid-01/stack-grid.d.ts.map +1 -1
- package/dist/blocks/stack-grid-01/stack-grid.js +15 -7
- package/dist/blocks/stats-01/index.d.ts +9 -1
- package/dist/blocks/stats-01/index.d.ts.map +1 -1
- package/dist/blocks/stats-01/index.js +8 -1
- package/dist/blocks/stats-01/stats.d.ts +1 -1
- package/dist/blocks/stats-01/stats.js +2 -2
- package/dist/blocks/status-ledger-01/index.d.ts +2 -2
- package/dist/blocks/status-ledger-01/index.js +1 -1
- package/dist/blocks/status-ledger-01/status-ledger.d.ts +1 -1
- package/dist/blocks/status-ledger-01/status-ledger.js +2 -2
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +473 -1
- package/dist/components/index.d.ts +104 -66
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +74 -40
- package/dist/components/ui/accordion.js +1 -1
- package/dist/components/ui/alert-dialog.d.ts +119 -0
- package/dist/components/ui/alert-dialog.d.ts.map +1 -0
- package/dist/components/ui/alert-dialog.js +82 -0
- package/dist/components/ui/alert.js +1 -1
- package/dist/components/ui/aspect-ratio.d.ts +60 -0
- package/dist/components/ui/aspect-ratio.d.ts.map +1 -0
- package/dist/components/ui/aspect-ratio.js +61 -0
- package/dist/components/ui/avatar.js +1 -1
- package/dist/components/ui/badge.js +1 -1
- package/dist/components/ui/breadcrumb.js +1 -1
- package/dist/components/ui/button-group.d.ts +68 -0
- package/dist/components/ui/button-group.d.ts.map +1 -0
- package/dist/components/ui/button-group.js +54 -0
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/calendar.d.ts +158 -0
- package/dist/components/ui/calendar.d.ts.map +1 -0
- package/dist/components/ui/calendar.js +216 -0
- package/dist/components/ui/card.d.ts +18 -6
- package/dist/components/ui/card.d.ts.map +1 -1
- package/dist/components/ui/card.js +23 -8
- package/dist/components/ui/carousel.d.ts +118 -0
- package/dist/components/ui/carousel.d.ts.map +1 -0
- package/dist/components/ui/carousel.js +152 -0
- package/dist/components/ui/chart-frame.d.ts +216 -0
- package/dist/components/ui/chart-frame.d.ts.map +1 -0
- package/dist/components/ui/chart-frame.js +286 -0
- package/dist/components/ui/checkbox.js +1 -1
- package/dist/components/ui/collapsible.d.ts +97 -0
- package/dist/components/ui/collapsible.d.ts.map +1 -0
- package/dist/components/ui/collapsible.js +54 -0
- package/dist/components/ui/combobox.d.ts +141 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +243 -0
- package/dist/components/ui/command-palette.d.ts +106 -0
- package/dist/components/ui/command-palette.d.ts.map +1 -0
- package/dist/components/ui/command-palette.js +176 -0
- package/dist/components/ui/command.d.ts +82 -0
- package/dist/components/ui/command.d.ts.map +1 -0
- package/dist/components/ui/command.js +37 -0
- package/dist/components/ui/context-menu.d.ts +151 -0
- package/dist/components/ui/context-menu.d.ts.map +1 -0
- package/dist/components/ui/context-menu.js +97 -0
- package/dist/components/ui/cta-link.js +1 -1
- package/dist/components/ui/date-picker.d.ts +111 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -0
- package/dist/components/ui/date-picker.js +74 -0
- package/dist/components/ui/diagram.d.ts +84 -15
- package/dist/components/ui/diagram.d.ts.map +1 -1
- package/dist/components/ui/diagram.js +77 -7
- package/dist/components/ui/dialog.js +1 -1
- package/dist/components/ui/diff.d.ts +105 -0
- package/dist/components/ui/diff.d.ts.map +1 -0
- package/dist/components/ui/diff.js +113 -0
- package/dist/components/ui/dropdown-menu.d.ts +18 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/components/ui/dropdown-menu.js +1 -1
- package/dist/components/ui/fact-list.js +1 -1
- package/dist/components/ui/field.js +1 -1
- package/dist/components/ui/form.d.ts +151 -0
- package/dist/components/ui/form.d.ts.map +1 -0
- package/dist/components/ui/form.js +73 -0
- package/dist/components/ui/hover-card.d.ts +105 -0
- package/dist/components/ui/hover-card.d.ts.map +1 -0
- package/dist/components/ui/hover-card.js +49 -0
- package/dist/components/ui/input-group.d.ts +99 -0
- package/dist/components/ui/input-group.d.ts.map +1 -0
- package/dist/components/ui/input-group.js +83 -0
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/item.d.ts +113 -0
- package/dist/components/ui/item.d.ts.map +1 -0
- package/dist/components/ui/item.js +61 -0
- package/dist/components/ui/kbd.js +1 -1
- package/dist/components/ui/label.d.ts +58 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +29 -0
- package/dist/components/ui/live-region.d.ts +9 -0
- package/dist/components/ui/live-region.d.ts.map +1 -1
- package/dist/components/ui/live-region.js +10 -2
- package/dist/components/ui/mark.js +1 -1
- package/dist/components/ui/menubar.d.ts +197 -0
- package/dist/components/ui/menubar.d.ts.map +1 -0
- package/dist/components/ui/menubar.js +109 -0
- package/dist/components/ui/meter.d.ts +121 -0
- package/dist/components/ui/meter.d.ts.map +1 -0
- package/dist/components/ui/meter.js +82 -0
- package/dist/components/ui/native-select.d.ts +72 -0
- package/dist/components/ui/native-select.d.ts.map +1 -0
- package/dist/components/ui/native-select.js +55 -0
- package/dist/components/ui/navigation-menu.d.ts +186 -0
- package/dist/components/ui/navigation-menu.d.ts.map +1 -0
- package/dist/components/ui/navigation-menu.js +94 -0
- package/dist/components/ui/number-field.d.ts +111 -0
- package/dist/components/ui/number-field.d.ts.map +1 -0
- package/dist/components/ui/number-field.js +84 -0
- package/dist/components/ui/one-time-code.d.ts +127 -0
- package/dist/components/ui/one-time-code.d.ts.map +1 -0
- package/dist/components/ui/one-time-code.js +57 -0
- package/dist/components/ui/pagination.js +1 -1
- package/dist/components/ui/popover.js +1 -1
- package/dist/components/ui/product-mark.d.ts +1 -1
- package/dist/components/ui/product-mark.js +1 -1
- package/dist/components/ui/product-switcher.d.ts +1 -1
- package/dist/components/ui/product-switcher.js +2 -2
- package/dist/components/ui/progress.js +1 -1
- package/dist/components/ui/prose.js +1 -1
- package/dist/components/ui/pulse-graph.js +1 -1
- package/dist/components/ui/pulse-series.js +1 -1
- package/dist/components/ui/radio-group.js +1 -1
- package/dist/components/ui/resizable.d.ts +129 -0
- package/dist/components/ui/resizable.d.ts.map +1 -0
- package/dist/components/ui/resizable.js +205 -0
- package/dist/components/ui/scroll-area.d.ts +97 -0
- package/dist/components/ui/scroll-area.d.ts.map +1 -0
- package/dist/components/ui/scroll-area.js +68 -0
- package/dist/components/ui/search-dialog.d.ts +139 -0
- package/dist/components/ui/search-dialog.d.ts.map +1 -0
- package/dist/components/ui/search-dialog.js +178 -0
- package/dist/components/ui/section.d.ts +26 -0
- package/dist/components/ui/section.d.ts.map +1 -1
- package/dist/components/ui/section.js +32 -1
- package/dist/components/ui/select.js +1 -1
- package/dist/components/ui/separator.js +1 -1
- package/dist/components/ui/sheet.d.ts +103 -0
- package/dist/components/ui/sheet.d.ts.map +1 -0
- package/dist/components/ui/sheet.js +58 -0
- package/dist/components/ui/sidebar.d.ts +228 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sidebar.js +172 -0
- package/dist/components/ui/signal-field.js +1 -1
- package/dist/components/ui/skeleton.js +1 -1
- package/dist/components/ui/slider.js +1 -1
- package/dist/components/ui/spinner.d.ts +79 -0
- package/dist/components/ui/spinner.d.ts.map +1 -0
- package/dist/components/ui/spinner.js +79 -0
- package/dist/components/ui/switch.js +1 -1
- package/dist/components/ui/table-sort.d.ts +97 -0
- package/dist/components/ui/table-sort.d.ts.map +1 -0
- package/dist/components/ui/table-sort.js +102 -0
- package/dist/components/ui/table.js +1 -1
- package/dist/components/ui/tabs.js +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/timeline.d.ts +106 -0
- package/dist/components/ui/timeline.d.ts.map +1 -0
- package/dist/components/ui/timeline.js +111 -0
- package/dist/components/ui/toast.d.ts +219 -0
- package/dist/components/ui/toast.d.ts.map +1 -0
- package/dist/components/ui/toast.js +270 -0
- package/dist/components/ui/toggle-group.d.ts +118 -0
- package/dist/components/ui/toggle-group.d.ts.map +1 -0
- package/dist/components/ui/toggle-group.js +194 -0
- package/dist/components/ui/toggle.d.ts +73 -0
- package/dist/components/ui/toggle.d.ts.map +1 -0
- package/dist/components/ui/toggle.js +44 -0
- package/dist/components/ui/tooltip.js +1 -1
- package/dist/components/ui/tree.js +1 -1
- package/dist/components/ui/typography.js +1 -1
- package/dist/fonts/Inter-OFL.txt +93 -93
- package/dist/index.d.ts +72 -70
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +61 -60
- package/dist/lib/rank.d.ts +69 -0
- package/dist/lib/rank.d.ts.map +1 -0
- package/dist/lib/rank.js +77 -0
- package/dist/live/index.d.ts +22 -0
- package/dist/live/index.d.ts.map +1 -0
- package/dist/live/index.js +20 -0
- package/dist/live/run-stream-01/index.d.ts +3 -0
- package/dist/live/run-stream-01/index.d.ts.map +1 -0
- package/dist/live/run-stream-01/index.js +1 -0
- package/dist/live/run-stream-01/run-stream.d.ts +125 -0
- package/dist/live/run-stream-01/run-stream.d.ts.map +1 -0
- package/dist/live/run-stream-01/run-stream.js +111 -0
- package/dist/pages/auth-page/index.d.ts +15 -2
- package/dist/pages/auth-page/index.d.ts.map +1 -1
- package/dist/pages/auth-page/index.js +8 -4
- package/dist/pages/blog-post-page/index.js +5 -5
- package/dist/pages/dashboard-page/index.d.ts +4 -4
- package/dist/pages/dashboard-page/index.js +4 -4
- package/dist/pages/docs-shell/docs-shell.d.ts +37 -4
- package/dist/pages/docs-shell/docs-shell.d.ts.map +1 -1
- package/dist/pages/docs-shell/docs-shell.js +107 -16
- package/dist/pages/docs-shell/index.d.ts +2 -2
- package/dist/pages/docs-shell/index.js +1 -1
- package/dist/pages/index.d.ts +14 -14
- package/dist/pages/index.js +7 -7
- package/dist/pages/marketing-page/index.d.ts +6 -6
- package/dist/pages/marketing-page/index.js +5 -5
- package/dist/pages/not-found-page/index.js +4 -4
- package/dist/pages/settings-page/index.d.ts +3 -3
- package/dist/pages/settings-page/index.js +4 -4
- package/dist/provider/index.d.ts +4 -4
- package/dist/provider/index.js +2 -2
- package/dist/provider/provider.d.ts +1 -1
- package/dist/provider/provider.js +1 -1
- package/dist/provider/theme-script.d.ts +1 -1
- package/dist/provider/theme-script.js +1 -1
- package/dist/styles.css +689 -0
- package/package.json +9 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { cn } from '../../lib/utils.js';
|
|
6
|
+
/** `YYYY-MM-DD` in local time, which is the only date format a form should post. */
|
|
7
|
+
function iso(date) {
|
|
8
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
9
|
+
}
|
|
10
|
+
const addDays = (date, days) => new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
|
|
11
|
+
const startOfMonth = (date) => new Date(date.getFullYear(), date.getMonth(), 1);
|
|
12
|
+
const sameMonth = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
|
|
13
|
+
const sameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
14
|
+
/**
|
|
15
|
+
* The same day of the month, `months` along.
|
|
16
|
+
*
|
|
17
|
+
* The day is clamped to the length of the month it lands in, so stepping back from
|
|
18
|
+
* the 31st lands on the 30th, the 28th or the 29th rather than rolling into the
|
|
19
|
+
* month before. A calendar that rolled would move the focus to a different month
|
|
20
|
+
* than the reader asked for, and the date would be wrong by a month with nothing
|
|
21
|
+
* on screen to say so.
|
|
22
|
+
*/
|
|
23
|
+
const shiftMonth = (date, months) => {
|
|
24
|
+
const target = new Date(date.getFullYear(), date.getMonth() + months, 1);
|
|
25
|
+
const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
|
|
26
|
+
return new Date(target.getFullYear(), target.getMonth(), Math.min(date.getDate(), lastDay));
|
|
27
|
+
};
|
|
28
|
+
const DAY_CELL =
|
|
29
|
+
// `transition-colors` is a property list, which Tailwind owns; the duration and
|
|
30
|
+
// the easing are the motion tokens, and neither is written here as a number.
|
|
31
|
+
'text-foreground hover:bg-accent hover:text-accent-foreground flex h-9 w-full cursor-pointer items-center justify-center rounded-md text-sm tabular-nums outline-none transition-colors duration-fast ease-out focus-visible:ring-ring focus-visible:ring-[3px]';
|
|
32
|
+
/**
|
|
33
|
+
* How a day cell looks.
|
|
34
|
+
*
|
|
35
|
+
* `selected` states a fill and therefore states its own ink. `today` does not set
|
|
36
|
+
* a fill, so it does not set an ink: it is a ring inside the cell and a weight,
|
|
37
|
+
* and a ring has no foreground to keep in step with the two states above it.
|
|
38
|
+
*/
|
|
39
|
+
const DAY_STATE = {
|
|
40
|
+
selected: 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground font-semibold',
|
|
41
|
+
today: 'ring-ring ring-1 ring-inset font-semibold',
|
|
42
|
+
unavailable: 'text-muted-foreground aria-disabled:hover:bg-transparent',
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* A month grid for picking one date.
|
|
46
|
+
*
|
|
47
|
+
* **It is built here because Base UI does not ship one.** The package at 1.8.0
|
|
48
|
+
* has a combobox, a number field, an OTP field and a field, and no calendar, so
|
|
49
|
+
* there is nothing to compose and everything here is the Component's own. That is
|
|
50
|
+
* stated rather than implied because "the docs show one" is not evidence, and a
|
|
51
|
+
* wrapper around a module that is not in the installed package is the failure this
|
|
52
|
+
* Component could have shipped.
|
|
53
|
+
*
|
|
54
|
+
* **The grid is always six weeks tall.** A month that needs five rows and a month
|
|
55
|
+
* that needs six would otherwise resize as the reader pages through, which moves
|
|
56
|
+
* the dates under the pointer and under the highlight: a reader who aims at the
|
|
57
|
+
* 24th after reading its position in a five-row month clicks the 31st in a
|
|
58
|
+
* six-row one. The empty cells at the edges are marked `aria-hidden` rather than
|
|
59
|
+
* filled with the neighbouring month's dates, because a date from another month in
|
|
60
|
+
* this grid is a date the reader did not come for.
|
|
61
|
+
*
|
|
62
|
+
* **Every date, weekday name and month name is the caller's.** The caption, the
|
|
63
|
+
* seven column headings and the month the reader moves to are all reader-facing
|
|
64
|
+
* text in the reader's language, and the Component has no business carrying a
|
|
65
|
+
* table of them. What it does carry is the geometry: which column a date falls in
|
|
66
|
+
* and which day it sits on.
|
|
67
|
+
*
|
|
68
|
+
* **One cell is in the tab order, and the arrows move it.** The roving tab stop
|
|
69
|
+
* sits on the chosen date, or on today, or on the first of the month, so a reader
|
|
70
|
+
* who tabs in lands on the date they care about rather than on the 1st. The arrows
|
|
71
|
+
* step a day and a week, Home and End step to the ends of the week, and PageUp and
|
|
72
|
+
* PageDown step a month, with Shift for a year. Moving past the end of a month
|
|
73
|
+
* changes the month on show and carries the focus with it, so paging never leaves
|
|
74
|
+
* the highlight on a cell that is no longer drawn.
|
|
75
|
+
*
|
|
76
|
+
* **A disabled date is drawn, reachable and refused.** It stays in the grid and
|
|
77
|
+
* stays in the arrow path, because a date that is simply missing is
|
|
78
|
+
* indistinguishable from a grid that failed to render that week, and a reader who
|
|
79
|
+
* cannot reach it cannot tell a fully booked day from a bug. Enter, Space and a
|
|
80
|
+
* click on it do nothing, and it announces as unavailable. Explain the reason in
|
|
81
|
+
* text beside the field; a greyed cell on its own says only that it is not
|
|
82
|
+
* available.
|
|
83
|
+
*
|
|
84
|
+
* **The caption is a live region.** Paging a month changes the month with no other
|
|
85
|
+
* visible change for a reader who is on the grid, so the caption announces itself
|
|
86
|
+
* when it changes.
|
|
87
|
+
*/
|
|
88
|
+
function Calendar({ label, monthLabel, weekdayLabels, firstWeekday = 0, previousLabel, nextLabel, month, onMonthChange, value = null, onValueChange, isDateDisabled, dayLabel, today, disabled = false, name, form, className, }) {
|
|
89
|
+
const gridRef = useRef(null);
|
|
90
|
+
// `null` means "wherever the reader would expect to land", which is derived
|
|
91
|
+
// rather than stored, so an external change to `value` or `month` moves the tab
|
|
92
|
+
// stop without this Component having to watch for it.
|
|
93
|
+
const [movedTo, setMovedTo] = useState(null);
|
|
94
|
+
const isUnavailable = useMemo(() => (date) => disabled || (isDateDisabled?.(date) ?? false), [disabled, isDateDisabled]);
|
|
95
|
+
/**
|
|
96
|
+
* Six weeks from the Sunday, or the reader's first day, on or before the first
|
|
97
|
+
* of the month.
|
|
98
|
+
*/
|
|
99
|
+
const cells = useMemo(() => {
|
|
100
|
+
const first = startOfMonth(month);
|
|
101
|
+
const offset = (first.getDay() - firstWeekday + 7) % 7;
|
|
102
|
+
const start = addDays(first, -offset);
|
|
103
|
+
return Array.from({ length: 42 }, (_, index) => addDays(start, index));
|
|
104
|
+
}, [month, firstWeekday]);
|
|
105
|
+
/**
|
|
106
|
+
* Where a reader who tabs into the grid lands: the chosen date, else today, else
|
|
107
|
+
* the first of the month. A month on show is never empty, so there is always an
|
|
108
|
+
* answer and the grid is always reachable by one Tab.
|
|
109
|
+
*/
|
|
110
|
+
const landing = useMemo(() => {
|
|
111
|
+
if (value !== null && sameMonth(value, month))
|
|
112
|
+
return value;
|
|
113
|
+
if (today !== undefined && sameMonth(today, month))
|
|
114
|
+
return today;
|
|
115
|
+
return startOfMonth(month);
|
|
116
|
+
}, [value, today, month]);
|
|
117
|
+
const focused = movedTo !== null && sameMonth(movedTo, month) ? movedTo : landing;
|
|
118
|
+
/** Focus follows a move, because a move that changed month draws a new cell. */
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (movedTo === null)
|
|
121
|
+
return;
|
|
122
|
+
gridRef.current?.querySelector(`[data-date="${iso(movedTo)}"]`)?.focus();
|
|
123
|
+
}, [movedTo, month]);
|
|
124
|
+
/** Moves the tab stop, and the month on show when the move crosses a month. */
|
|
125
|
+
const goTo = (date) => {
|
|
126
|
+
setMovedTo(date);
|
|
127
|
+
if (!sameMonth(date, month))
|
|
128
|
+
onMonthChange(startOfMonth(date));
|
|
129
|
+
};
|
|
130
|
+
const choose = (date) => {
|
|
131
|
+
// The tab stop follows whatever the reader last operated, so a click leaves the
|
|
132
|
+
// grid in the state the reader is now looking at rather than snapping the stop
|
|
133
|
+
// back to the landing cell.
|
|
134
|
+
setMovedTo(date);
|
|
135
|
+
if (isUnavailable(date))
|
|
136
|
+
return;
|
|
137
|
+
onValueChange?.(date);
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* The keys on one day cell.
|
|
141
|
+
*
|
|
142
|
+
* The date comes from the cell the event fired on rather than from state,
|
|
143
|
+
* because a reader may have reached a cell by Tab or by a click and the tab stop
|
|
144
|
+
* is the Component's own decision. Deriving the move from state would step from
|
|
145
|
+
* wherever the tab stop happened to be, which is a cell the reader is not on.
|
|
146
|
+
*/
|
|
147
|
+
const onDayKeyDown = (event, date) => {
|
|
148
|
+
const row = Math.floor(cells.findIndex((entry) => sameDay(entry, date)) / 7);
|
|
149
|
+
const first = cells[row * 7];
|
|
150
|
+
const last = first === undefined ? null : addDays(first, 6);
|
|
151
|
+
// Enter by `key`, Space by `code`. The space bar's `key` has been reported as a
|
|
152
|
+
// space, as "Spacebar" and as "Space" by different engines over the years, and
|
|
153
|
+
// its `code` has been the same in all of them, so this is the one activation key
|
|
154
|
+
// whose physical position is the more reliable half of the event.
|
|
155
|
+
if (event.key === 'Enter' || event.code === 'Space') {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
choose(date);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
switch (event.key) {
|
|
161
|
+
case 'ArrowLeft':
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
goTo(addDays(date, -1));
|
|
164
|
+
return;
|
|
165
|
+
case 'ArrowRight':
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
goTo(addDays(date, 1));
|
|
168
|
+
return;
|
|
169
|
+
case 'ArrowUp':
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
goTo(addDays(date, -7));
|
|
172
|
+
return;
|
|
173
|
+
case 'ArrowDown':
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
goTo(addDays(date, 7));
|
|
176
|
+
return;
|
|
177
|
+
case 'Home':
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
if (first !== undefined)
|
|
180
|
+
goTo(first);
|
|
181
|
+
return;
|
|
182
|
+
case 'End':
|
|
183
|
+
event.preventDefault();
|
|
184
|
+
if (last !== null)
|
|
185
|
+
goTo(last);
|
|
186
|
+
return;
|
|
187
|
+
case 'PageUp':
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
goTo(shiftMonth(date, event.shiftKey ? -12 : -1));
|
|
190
|
+
return;
|
|
191
|
+
case 'PageDown':
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
goTo(shiftMonth(date, event.shiftKey ? 12 : 1));
|
|
194
|
+
return;
|
|
195
|
+
default:
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const focusedIso = iso(focused);
|
|
199
|
+
const chosenIso = value === null ? null : iso(value);
|
|
200
|
+
const todayIso = today === undefined ? null : iso(today);
|
|
201
|
+
return (_jsxs("div", { "data-slot": "calendar", className: cn('bg-popover text-popover-foreground w-64 rounded-md border p-3 shadow-md', className), children: [_jsxs("div", { className: "mb-2 flex items-center justify-between gap-1", children: [_jsx("button", { type: "button", "data-slot": "calendar-previous", "aria-label": previousLabel, disabled: disabled, onClick: () => onMonthChange(shiftMonth(month, -1)), className: cn('text-muted-foreground hover:bg-accent hover:text-accent-foreground inline-flex size-8 items-center justify-center rounded-md outline-none', 'transition-colors duration-fast ease-out focus-visible:ring-ring focus-visible:ring-[3px]', 'disabled:pointer-events-none disabled:opacity-50'), children: _jsx(ChevronLeftIcon, { className: "size-4" }) }), _jsx("div", { "data-slot": "calendar-caption", "aria-live": "polite", className: "text-foreground text-sm font-medium tabular-nums", children: monthLabel }), _jsx("button", { type: "button", "data-slot": "calendar-next", "aria-label": nextLabel, disabled: disabled, onClick: () => onMonthChange(shiftMonth(month, 1)), className: cn('text-muted-foreground hover:bg-accent hover:text-accent-foreground inline-flex size-8 items-center justify-center rounded-md outline-none', 'transition-colors duration-fast ease-out focus-visible:ring-ring focus-visible:ring-[3px]', 'disabled:pointer-events-none disabled:opacity-50'), children: _jsx(ChevronRightIcon, { className: "size-4" }) })] }), _jsx("div", { ref: gridRef, children: _jsxs("table", { "data-slot": "calendar-grid", role: "grid", "aria-label": label, className: "w-full border-collapse tabular-nums", children: [_jsx("thead", { children: _jsx("tr", { children: weekdayLabels.map((weekday, column) => (_jsx("th", { scope: "col", "data-slot": "calendar-weekday", className: "text-muted-foreground flex h-8 items-center justify-center text-xs font-medium", children: weekday }, `${weekday}-${column}`))) }) }), _jsx("tbody", { children: Array.from({ length: 6 }, (_, row) => (_jsx("tr", { children: cells.slice(row * 7, row * 7 + 7).map((date) => {
|
|
202
|
+
const key = iso(date);
|
|
203
|
+
if (!sameMonth(date, month)) {
|
|
204
|
+
// The cells that pad the month out. They are hidden from the
|
|
205
|
+
// accessibility tree rather than filled with the neighbouring
|
|
206
|
+
// month's dates, because a date from another month drawn in
|
|
207
|
+
// this grid is a date the reader did not come for.
|
|
208
|
+
return (_jsx("td", { role: "gridcell", "aria-hidden": "true", className: "p-0" }, key));
|
|
209
|
+
}
|
|
210
|
+
const unavailable = isUnavailable(date);
|
|
211
|
+
const isSelected = key === chosenIso;
|
|
212
|
+
const isToday = key === todayIso;
|
|
213
|
+
return (_jsx("td", { role: "gridcell", "data-slot": "calendar-day", "data-date": key, tabIndex: key === focusedIso ? 0 : -1, "aria-selected": isSelected, "aria-current": isToday ? 'date' : undefined, "aria-disabled": unavailable || undefined, ...(dayLabel === undefined ? null : { 'aria-label': dayLabel(date) }), onClick: () => choose(date), onKeyDown: (event) => onDayKeyDown(event, date), className: cn(DAY_CELL, unavailable && DAY_STATE.unavailable, isToday && !isSelected && DAY_STATE.today, isSelected && DAY_STATE.selected), children: date.getDate() }, key));
|
|
214
|
+
}) }, row))) })] }) }), name === undefined ? null : (_jsx("input", { type: "hidden", name: name, ...(form === undefined ? null : { form }), value: chosenIso ?? '', readOnly: true }))] }));
|
|
215
|
+
}
|
|
216
|
+
export { Calendar };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentProps } from 'react';
|
|
2
|
+
import type { HeadingElement } from './typography.js';
|
|
2
3
|
/**
|
|
3
4
|
* A bordered surface for a small group of related content.
|
|
4
5
|
*
|
|
@@ -18,13 +19,24 @@ declare function CardHeader({ className, ...props }: ComponentProps<'div'>): imp
|
|
|
18
19
|
/**
|
|
19
20
|
* The card's title: the one line that names the group of related content.
|
|
20
21
|
*
|
|
21
|
-
* Renders a `div`
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* Renders a `div` by default, because a card's title is not always the heading of
|
|
23
|
+
* the region it sits in: four cards in a grid are four titles under one section
|
|
24
|
+
* heading, and four `h3`s under one `h2` is right while four `h2`s is four
|
|
25
|
+
* sections. So the element is a decision the caller makes with `as`, and a Block
|
|
26
|
+
* that draws a set of titled cards passes `childLevel(headingLevel)` rather than a
|
|
27
|
+
* literal, so the titles follow the section when the block is composed one level
|
|
28
|
+
* deeper than it was written for.
|
|
29
|
+
*
|
|
30
|
+
* An earlier version of this note told a caller to pass Prism's `Heading` inside
|
|
31
|
+
* instead, and that was wrong. `Heading` is a step of the *type* scale and its
|
|
32
|
+
* floor is `lg`; a card title sits at body size, so the only way to use it was to
|
|
33
|
+
* override the size back down, which asks a type-scale component to have no opinion
|
|
34
|
+
* about type. The element and the visual step are separate questions and this is the
|
|
35
|
+
* one that answers the element.
|
|
26
36
|
*/
|
|
27
|
-
declare function CardTitle({ className, ...props }: ComponentProps<'
|
|
37
|
+
declare function CardTitle({ className, as: Tag, ...props }: ComponentProps<'h2'> & {
|
|
38
|
+
as?: HeadingElement | 'div';
|
|
39
|
+
}): import("react").JSX.Element;
|
|
28
40
|
/**
|
|
29
41
|
* The supporting line under a card's title: one sentence about what the card
|
|
30
42
|
* holds.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../../src/components/ui/card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../../src/components/ui/card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAe,MAAM,OAAO,CAAA;AAExD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAIlD;;;;;;GAMG;AACH,iBAAS,IAAI,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,cAAc,CAAC,KAAK,CAAC,+BAW3D;AAED;;;;;;GAMG;AACH,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,cAAc,CAAC,KAAK,CAAC,+BAWjE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,iBAAS,SAAS,CAAC,EACjB,SAAS,EACT,EAAE,EAAE,GAAW,EACf,GAAG,KAAK,EACT,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG;IAAE,EAAE,CAAC,EAAE,cAAc,GAAG,KAAK,CAAA;CAAE,+BAcxD;AAED;;;GAGG;AACH,iBAAS,eAAe,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,cAAc,CAAC,KAAK,CAAC,+BAQtE;AAED,wEAAwE;AACxE,iBAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,cAAc,CAAC,KAAK,CAAC,+BAElE;AAED;;;GAGG;AACH,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,cAAc,CAAC,KAAK,CAAC,+BAQjE;AAED,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,CAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { cn } from '../../lib/utils';
|
|
2
|
+
import { cn } from '../../lib/utils.js';
|
|
3
3
|
/**
|
|
4
4
|
* A bordered surface for a small group of related content.
|
|
5
5
|
*
|
|
@@ -23,14 +23,29 @@ function CardHeader({ className, ...props }) {
|
|
|
23
23
|
/**
|
|
24
24
|
* The card's title: the one line that names the group of related content.
|
|
25
25
|
*
|
|
26
|
-
* Renders a `div`
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* Renders a `div` by default, because a card's title is not always the heading of
|
|
27
|
+
* the region it sits in: four cards in a grid are four titles under one section
|
|
28
|
+
* heading, and four `h3`s under one `h2` is right while four `h2`s is four
|
|
29
|
+
* sections. So the element is a decision the caller makes with `as`, and a Block
|
|
30
|
+
* that draws a set of titled cards passes `childLevel(headingLevel)` rather than a
|
|
31
|
+
* literal, so the titles follow the section when the block is composed one level
|
|
32
|
+
* deeper than it was written for.
|
|
33
|
+
*
|
|
34
|
+
* An earlier version of this note told a caller to pass Prism's `Heading` inside
|
|
35
|
+
* instead, and that was wrong. `Heading` is a step of the *type* scale and its
|
|
36
|
+
* floor is `lg`; a card title sits at body size, so the only way to use it was to
|
|
37
|
+
* override the size back down, which asks a type-scale component to have no opinion
|
|
38
|
+
* about type. The element and the visual step are separate questions and this is the
|
|
39
|
+
* one that answers the element.
|
|
31
40
|
*/
|
|
32
|
-
function CardTitle({ className, ...props }) {
|
|
33
|
-
|
|
41
|
+
function CardTitle({ className, as: Tag = 'div', ...props }) {
|
|
42
|
+
// The same cast `Heading` makes, and for the same reason: the element is chosen
|
|
43
|
+
// at runtime, so the union of the six headings and a `div` is not one prop type.
|
|
44
|
+
// The props are typed from `h2` rather than `div` because every prop a caller
|
|
45
|
+
// passes to a card title is valid on a heading, and typing from `h2` would stop a
|
|
46
|
+
// caller passing one that only a `div` takes.
|
|
47
|
+
const Element = Tag;
|
|
48
|
+
return (_jsx(Element, { "data-slot": "card-title", className: cn('font-semibold leading-none', className), ...props }));
|
|
34
49
|
}
|
|
35
50
|
/**
|
|
36
51
|
* The supporting line under a card's title: one sentence about what the card
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
/** One slide, and the words a reader is given for it. */
|
|
3
|
+
export type CarouselSlide = {
|
|
4
|
+
/** A stable key. A slide's key must not change as the reader moves. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** The slide's content. */
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* The accessible name of this slide.
|
|
10
|
+
*
|
|
11
|
+
* A carousel hides its content, so this is often the only way a reader knows
|
|
12
|
+
* which slide they are on before they read it. It is required for the same
|
|
13
|
+
* reason a region's name is: it is the caller's word, and a slide announced only
|
|
14
|
+
* by its content is a slide a reader cannot skip past by name.
|
|
15
|
+
*/
|
|
16
|
+
label: string;
|
|
17
|
+
};
|
|
18
|
+
/** The props the Carousel accepts. */
|
|
19
|
+
export interface CarouselProps extends Omit<ComponentProps<'div'>, 'children'> {
|
|
20
|
+
/** The slides, in the order a reader meets them. */
|
|
21
|
+
slides: readonly CarouselSlide[];
|
|
22
|
+
/**
|
|
23
|
+
* The accessible name of the carousel.
|
|
24
|
+
*
|
|
25
|
+
* Required rather than defaulted. A carousel is a region, and a page with two is
|
|
26
|
+
* a page where a reader cannot tell which set of slides they are in. The name is
|
|
27
|
+
* the caller's word.
|
|
28
|
+
*/
|
|
29
|
+
label: string;
|
|
30
|
+
/**
|
|
31
|
+
* The words read for a slide position, given its one-based index and the total.
|
|
32
|
+
*
|
|
33
|
+
* Required, and required as a function rather than a string, because a position
|
|
34
|
+
* has to be a sentence and the sentence is different in every language. It is
|
|
35
|
+
* also the answer to the only real defect a carousel has: one with no position
|
|
36
|
+
* indicator is a guessing game, because a reader who has been moved three times
|
|
37
|
+
* with nothing telling them where they are has no way to know how much is left.
|
|
38
|
+
*
|
|
39
|
+
* The function receives the index of the slide the control moves *to*, so the
|
|
40
|
+
* words always describe where the reader will be rather than where they are.
|
|
41
|
+
*/
|
|
42
|
+
position: (index: number, total: number) => string;
|
|
43
|
+
/**
|
|
44
|
+
* What a reader is told when there is nothing to show.
|
|
45
|
+
*
|
|
46
|
+
* A carousel with no slides is a frame with controls in it that move nowhere, so
|
|
47
|
+
* the case is authored rather than left to a caller to notice.
|
|
48
|
+
*/
|
|
49
|
+
empty?: ReactNode;
|
|
50
|
+
/** The words read for the control that moves back. @defaultValue 'Previous slide' */
|
|
51
|
+
previousLabel?: string;
|
|
52
|
+
/** The words read for the control that moves forward. @defaultValue 'Next slide' */
|
|
53
|
+
nextLabel?: string;
|
|
54
|
+
/**
|
|
55
|
+
* The slide to show first.
|
|
56
|
+
*
|
|
57
|
+
* Omit it and the first slide is shown. It is a prop because a carousel dropped
|
|
58
|
+
* into the middle of a page is often a deep link's target, and a Component that
|
|
59
|
+
* always opened on slide one would show the reader a different slide from the
|
|
60
|
+
* one the address names.
|
|
61
|
+
*/
|
|
62
|
+
initialIndex?: number;
|
|
63
|
+
/** The slide the reader is on, when the caller owns it. */
|
|
64
|
+
index?: number;
|
|
65
|
+
/** Called with the index of the slide the reader moves to. */
|
|
66
|
+
onIndexChange?: (index: number) => void;
|
|
67
|
+
/** Layout only. */
|
|
68
|
+
className?: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A set of slides with controls that move between them.
|
|
72
|
+
*
|
|
73
|
+
* **A carousel hides content, so it must never be the only route to it.** That is
|
|
74
|
+
* the decision worth making, and it is a constraint on the caller as much as on
|
|
75
|
+
* this Component. Everything a carousel shows, a reader must be able to reach some
|
|
76
|
+
* other way: a tab, a section below it, a list. A carousel used as the only
|
|
77
|
+
* presentation of a set of images is a set of images a reader on a keyboard, on a
|
|
78
|
+
* screen reader, or with the controls hidden by a stylesheet cannot see at all,
|
|
79
|
+
* and the Component cannot detect that case. So the rule is here, in the
|
|
80
|
+
* documentation, where a caller meets it, rather than assumed.
|
|
81
|
+
*
|
|
82
|
+
* **The controls say where the reader is, and the position is a sentence the
|
|
83
|
+
* caller writes.** `position` is required and is a function of the index and the
|
|
84
|
+
* total, so a reader is told "3 of 7" rather than being left to infer it. A
|
|
85
|
+
* carousel with no position indicator is a guessing game: a reader moved three
|
|
86
|
+
* times with nothing counting has no way to know whether there are three slides
|
|
87
|
+
* left or three hundred, and the only way to find out is to keep pressing. The
|
|
88
|
+
* current position is also in `aria-live="polite"` on the position, because a
|
|
89
|
+
* carousel that moves silently under a reader who did not press anything is a
|
|
90
|
+
* carousel a reader cannot follow.
|
|
91
|
+
*
|
|
92
|
+
* **One slide is the whole content, with no controls.** A carousel of one slide is
|
|
93
|
+
* a picture with two buttons that do nothing, and it is the state a filtered or
|
|
94
|
+
* partially-loaded set passes through. The controls are absent rather than
|
|
95
|
+
* disabled, and the position says 1 of 1, because "there is one slide and you are
|
|
96
|
+
* on it" is a true and useful thing to tell a reader.
|
|
97
|
+
*
|
|
98
|
+
* **No slide is authored rather than rendered as an empty frame.** Zero slides is
|
|
99
|
+
* a state a caller reaches before their data arrives or after a filter excludes
|
|
100
|
+
* everything, and a region with an empty track and two dead controls is a gap
|
|
101
|
+
* rather than a state.
|
|
102
|
+
*
|
|
103
|
+
* **It is operable from the keyboard, and the arrow keys do what a reader
|
|
104
|
+
* expects.** Left and Right move between slides when the carousel has focus, Home
|
|
105
|
+
* and End jump to the first and last, and the controls are real buttons so Tab
|
|
106
|
+
* reaches them and Enter and Space press them. Nothing here is a div with a
|
|
107
|
+
* handler, which is the version a consumer cannot assemble for themselves because
|
|
108
|
+
* every one of those keys is a chance to get one wrong.
|
|
109
|
+
*
|
|
110
|
+
* **The slide transition is a state change and not an entrance.** The track moves
|
|
111
|
+
* with `duration-base ease-out`, which is the reader's own press being reported
|
|
112
|
+
* back, and there is no fade on mount and no motion a reader did not ask for.
|
|
113
|
+
*
|
|
114
|
+
* It is a client Component, because a carousel holds a position and answers keys.
|
|
115
|
+
*/
|
|
116
|
+
declare function Carousel({ slides, label, position, className, empty, previousLabel, nextLabel, initialIndex, index, onIndexChange, ...props }: CarouselProps): import("react").JSX.Element;
|
|
117
|
+
export { Carousel };
|
|
118
|
+
//# sourceMappingURL=carousel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"carousel.d.ts","sourceRoot":"","sources":["../../../src/components/ui/carousel.tsx"],"names":[],"mappings":"AAGA,OAAO,EAML,KAAK,cAAc,EAEnB,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AAId,yDAAyD;AACzD,MAAM,MAAM,aAAa,GAAG;IAC1B,uEAAuE;IACvE,EAAE,EAAE,MAAM,CAAA;IACV,2BAA2B;IAC3B,QAAQ,EAAE,SAAS,CAAA;IACnB;;;;;;;OAOG;IACH,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,sCAAsC;AACtC,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC;IAC5E,oDAAoD;IACpD,MAAM,EAAE,SAAS,aAAa,EAAE,CAAA;IAChC;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;;;;;;OAWG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAClD;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,qFAAqF;IACrF,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,mBAAmB;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,iBAAS,QAAQ,CAAC,EAChB,MAAM,EACN,KAAK,EACL,QAAQ,EACR,SAAS,EACT,KAAK,EACL,aAAgC,EAChC,SAAwB,EACxB,YAAgB,EAChB,KAAK,EACL,aAAa,EACb,GAAG,KAAK,EACT,EAAE,aAAa,+BAgNf;AASD,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
|
4
|
+
import { useCallback, useEffect, useId, useRef, useState, } from 'react';
|
|
5
|
+
import { cn } from '../../lib/utils.js';
|
|
6
|
+
/**
|
|
7
|
+
* A set of slides with controls that move between them.
|
|
8
|
+
*
|
|
9
|
+
* **A carousel hides content, so it must never be the only route to it.** That is
|
|
10
|
+
* the decision worth making, and it is a constraint on the caller as much as on
|
|
11
|
+
* this Component. Everything a carousel shows, a reader must be able to reach some
|
|
12
|
+
* other way: a tab, a section below it, a list. A carousel used as the only
|
|
13
|
+
* presentation of a set of images is a set of images a reader on a keyboard, on a
|
|
14
|
+
* screen reader, or with the controls hidden by a stylesheet cannot see at all,
|
|
15
|
+
* and the Component cannot detect that case. So the rule is here, in the
|
|
16
|
+
* documentation, where a caller meets it, rather than assumed.
|
|
17
|
+
*
|
|
18
|
+
* **The controls say where the reader is, and the position is a sentence the
|
|
19
|
+
* caller writes.** `position` is required and is a function of the index and the
|
|
20
|
+
* total, so a reader is told "3 of 7" rather than being left to infer it. A
|
|
21
|
+
* carousel with no position indicator is a guessing game: a reader moved three
|
|
22
|
+
* times with nothing counting has no way to know whether there are three slides
|
|
23
|
+
* left or three hundred, and the only way to find out is to keep pressing. The
|
|
24
|
+
* current position is also in `aria-live="polite"` on the position, because a
|
|
25
|
+
* carousel that moves silently under a reader who did not press anything is a
|
|
26
|
+
* carousel a reader cannot follow.
|
|
27
|
+
*
|
|
28
|
+
* **One slide is the whole content, with no controls.** A carousel of one slide is
|
|
29
|
+
* a picture with two buttons that do nothing, and it is the state a filtered or
|
|
30
|
+
* partially-loaded set passes through. The controls are absent rather than
|
|
31
|
+
* disabled, and the position says 1 of 1, because "there is one slide and you are
|
|
32
|
+
* on it" is a true and useful thing to tell a reader.
|
|
33
|
+
*
|
|
34
|
+
* **No slide is authored rather than rendered as an empty frame.** Zero slides is
|
|
35
|
+
* a state a caller reaches before their data arrives or after a filter excludes
|
|
36
|
+
* everything, and a region with an empty track and two dead controls is a gap
|
|
37
|
+
* rather than a state.
|
|
38
|
+
*
|
|
39
|
+
* **It is operable from the keyboard, and the arrow keys do what a reader
|
|
40
|
+
* expects.** Left and Right move between slides when the carousel has focus, Home
|
|
41
|
+
* and End jump to the first and last, and the controls are real buttons so Tab
|
|
42
|
+
* reaches them and Enter and Space press them. Nothing here is a div with a
|
|
43
|
+
* handler, which is the version a consumer cannot assemble for themselves because
|
|
44
|
+
* every one of those keys is a chance to get one wrong.
|
|
45
|
+
*
|
|
46
|
+
* **The slide transition is a state change and not an entrance.** The track moves
|
|
47
|
+
* with `duration-base ease-out`, which is the reader's own press being reported
|
|
48
|
+
* back, and there is no fade on mount and no motion a reader did not ask for.
|
|
49
|
+
*
|
|
50
|
+
* It is a client Component, because a carousel holds a position and answers keys.
|
|
51
|
+
*/
|
|
52
|
+
function Carousel({ slides, label, position, className, empty, previousLabel = 'Previous slide', nextLabel = 'Next slide', initialIndex = 0, index, onIndexChange, ...props }) {
|
|
53
|
+
const [uncontrolled, setUncontrolled] = useState(() => clamp(initialIndex, slides.length));
|
|
54
|
+
const controlled = index !== undefined;
|
|
55
|
+
const at = controlled ? clamp(index, slides.length) : uncontrolled;
|
|
56
|
+
const total = slides.length;
|
|
57
|
+
const set = useCallback((next) => {
|
|
58
|
+
const bounded = clamp(next, total);
|
|
59
|
+
if (!controlled)
|
|
60
|
+
setUncontrolled(bounded);
|
|
61
|
+
onIndexChange?.(bounded);
|
|
62
|
+
}, [controlled, onIndexChange, total]);
|
|
63
|
+
const labelId = useId();
|
|
64
|
+
const root = useRef(null);
|
|
65
|
+
const previous = useRef(null);
|
|
66
|
+
const next = useRef(null);
|
|
67
|
+
const atFirst = at <= 0;
|
|
68
|
+
const atLast = at >= total - 1;
|
|
69
|
+
// The reader's own arrow keys, which are the reason this is a Component rather
|
|
70
|
+
// than a stack of divs with two buttons beside them.
|
|
71
|
+
const onKeyDown = (event) => {
|
|
72
|
+
const keys = ['ArrowLeft', 'ArrowRight', 'Home', 'End'];
|
|
73
|
+
if (!keys.includes(event.key))
|
|
74
|
+
return;
|
|
75
|
+
if (total <= 1)
|
|
76
|
+
return;
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
if (event.key === 'Home')
|
|
79
|
+
set(0);
|
|
80
|
+
else if (event.key === 'End')
|
|
81
|
+
set(total - 1);
|
|
82
|
+
else
|
|
83
|
+
set(at + (event.key === 'ArrowRight' ? 1 : -1));
|
|
84
|
+
};
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
// A carousel that has lost slides must not leave the reader past the end, and
|
|
87
|
+
// the failure is silent: the track is translated to an offset with no slide
|
|
88
|
+
// there, so the reader is looking at blank space in a carousel that is
|
|
89
|
+
// working perfectly well.
|
|
90
|
+
if (at > total - 1)
|
|
91
|
+
set(total - 1);
|
|
92
|
+
}, [at, set, total]);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
// A control that has just been disabled must not keep the reader's focus.
|
|
95
|
+
//
|
|
96
|
+
// A disabled button is out of the tab order but it does not give up focus, so
|
|
97
|
+
// a reader who pressed "next" on the last-but-one slide is left focused on a
|
|
98
|
+
// control that can no longer answer anything: not Enter, not Space, and not
|
|
99
|
+
// the arrow keys this Component listens for on the way past. The reader is
|
|
100
|
+
// stranded inside the carousel with no working control under their fingers and
|
|
101
|
+
// no way to know why. So focus is handed to the sibling that is still live,
|
|
102
|
+
// which is the same thing a browser does when focus lands on something that
|
|
103
|
+
// has become inert.
|
|
104
|
+
const active = document.activeElement;
|
|
105
|
+
if (!(active instanceof HTMLButtonElement))
|
|
106
|
+
return;
|
|
107
|
+
if (active !== previous.current && active !== next.current)
|
|
108
|
+
return;
|
|
109
|
+
if (active.disabled !== true)
|
|
110
|
+
return;
|
|
111
|
+
const live = active === previous.current ? next.current : previous.current;
|
|
112
|
+
if (live === null || live.disabled)
|
|
113
|
+
return;
|
|
114
|
+
live.focus();
|
|
115
|
+
});
|
|
116
|
+
if (total === 0) {
|
|
117
|
+
// A region with an empty track and two dead controls is a gap rather than a
|
|
118
|
+
// state, so the empty case is authored.
|
|
119
|
+
return (_jsx("div", { "data-slot": "carousel-empty", className: cn('text-muted-foreground text-sm', className), children: empty ?? null }));
|
|
120
|
+
}
|
|
121
|
+
return (_jsxs("div", { ref: root, "data-slot": "carousel",
|
|
122
|
+
// A carousel is a region, and it is named, because a page with two of them
|
|
123
|
+
// is a page where a reader cannot tell which set of slides they are in.
|
|
124
|
+
role: "region", "aria-roledescription": "carousel", "aria-label": label, onKeyDown: onKeyDown, className: cn('flex flex-col gap-3', className), ...props, children: [_jsx("div", { "data-slot": "carousel-viewport", className: "overflow-hidden", style: {
|
|
125
|
+
// A track width of one slide each is what makes the translate a whole
|
|
126
|
+
// slide, and a negative margin of the track's own width is what puts the
|
|
127
|
+
// first slide at zero. Both are the layout rather than a value, so they
|
|
128
|
+
// are the two numbers this Component is allowed to hold.
|
|
129
|
+
width: `${100 / total}%`,
|
|
130
|
+
marginInlineStart: `${-(100 * (total - 1)) / total}%`,
|
|
131
|
+
transform: `translateX(${(at * 100) / total}%)`,
|
|
132
|
+
}, children: _jsx("div", { "data-slot": "carousel-track",
|
|
133
|
+
// A state change, reported with a token. There is no fade on mount and no
|
|
134
|
+
// motion a reader did not ask for, because there is no decorative or
|
|
135
|
+
// entrance animation in this system.
|
|
136
|
+
className: "flex transition-transform duration-base ease-out", style: { width: `${100 * total}%` }, children: slides.map((slide, slideAt) => {
|
|
137
|
+
const current = slideAt === at;
|
|
138
|
+
return (_jsx("div", { "data-slot": "carousel-slide", "data-current": current ? 'true' : undefined, id: `${labelId}-slide-${slideAt}`, role: "group", "aria-label": slide.label, "aria-roledescription": "slide", "aria-hidden": current ? undefined : true, className: "w-full shrink-0", children: slide.children }, slide.id));
|
|
139
|
+
}) }) }), total > 1 ? (_jsxs("div", { "data-slot": "carousel-controls", className: "flex items-center justify-between gap-3", children: [_jsx("button", { type: "button", ref: previous, "data-slot": "carousel-previous", "aria-label": previousLabel, disabled: atFirst, onClick: () => set(at - 1), className: cn('border-input bg-background text-foreground inline-flex size-9 items-center justify-center rounded-md border outline-none', 'transition-colors duration-fast ease-out', 'hover:bg-accent hover:text-accent-foreground', 'focus-visible:ring-ring focus-visible:ring-[3px]', 'disabled:pointer-events-none disabled:opacity-50'), children: _jsx(ChevronLeftIcon, { className: "size-4", "aria-hidden": "true" }) }), _jsx("p", { "data-slot": "carousel-position", "aria-live": "polite", className: "text-muted-foreground text-sm tabular-nums", children: position(at + 1, total) }), _jsx("button", { type: "button", ref: next, "data-slot": "carousel-next", "aria-label": nextLabel, disabled: atLast, onClick: () => set(at + 1), className: cn('border-input bg-background text-foreground inline-flex size-9 items-center justify-center rounded-md border outline-none', 'transition-colors duration-fast ease-out', 'hover:bg-accent hover:text-accent-foreground', 'focus-visible:ring-ring focus-visible:ring-[3px]', 'disabled:pointer-events-none disabled:opacity-50'), children: _jsx(ChevronRightIcon, { className: "size-4", "aria-hidden": "true" }) })] })) : (
|
|
140
|
+
// One slide is the whole content, and the position still says so rather
|
|
141
|
+
// than the reader being left to infer that there is nothing to move to.
|
|
142
|
+
_jsx("p", { "data-slot": "carousel-position", "aria-live": "polite", className: "text-muted-foreground text-sm tabular-nums", children: position(1, 1) }))] }));
|
|
143
|
+
}
|
|
144
|
+
/** An index inside the set, so a caller cannot move the reader past either end. */
|
|
145
|
+
function clamp(value, total) {
|
|
146
|
+
if (!Number.isFinite(value))
|
|
147
|
+
return 0;
|
|
148
|
+
if (total === 0)
|
|
149
|
+
return 0;
|
|
150
|
+
return Math.min(Math.max(Math.trunc(value), 0), total - 1);
|
|
151
|
+
}
|
|
152
|
+
export { Carousel };
|