@carefully-built/cli 0.1.0 → 0.1.2
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 +148 -7
- package/dist/index.mjs +71 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/registry/ui/avatar/manifest.json +33 -0
- package/registry/ui/avatar/primitives/avatar.tsx +64 -0
- package/registry/ui/avatar/utils/cn.ts +6 -0
- package/registry/ui/button/manifest.json +24 -5
- package/registry/ui/button/utils/cn.ts +6 -0
- package/registry/ui/calendar/manifest.json +35 -0
- package/registry/ui/calendar/primitives/button.tsx +89 -0
- package/registry/ui/calendar/primitives/calendar.tsx +68 -0
- package/registry/ui/calendar/utils/cn.ts +6 -0
- package/registry/ui/card/manifest.json +36 -0
- package/registry/ui/card/primitives/card.tsx +80 -0
- package/registry/ui/card/utils/cn.ts +6 -0
- package/registry/ui/chip/manifest.json +36 -0
- package/registry/ui/chip/primitives/chip-utils.ts +10 -0
- package/registry/ui/chip/primitives/chip.tsx +74 -0
- package/registry/ui/chip/utils/cn.ts +6 -0
- package/registry/ui/chip-utils/manifest.json +33 -0
- package/registry/ui/chip-utils/primitives/chip-utils.ts +10 -0
- package/registry/ui/chip-utils/utils/cn.ts +6 -0
- package/registry/ui/date-display/manifest.json +33 -0
- package/registry/ui/date-display/utils/cn.ts +6 -0
- package/registry/ui/date-display/utils/date-display.ts +61 -0
- package/registry/ui/dialog/manifest.json +43 -0
- package/registry/ui/dialog/primitives/button.tsx +89 -0
- package/registry/ui/dialog/primitives/dialog.tsx +147 -0
- package/registry/ui/dialog/utils/cn.ts +6 -0
- package/registry/ui/display-date/manifest.json +36 -0
- package/registry/ui/display-date/primitives/display-date.tsx +20 -0
- package/registry/ui/display-date/utils/cn.ts +6 -0
- package/registry/ui/display-date/utils/date-display.ts +61 -0
- package/registry/ui/drawer/manifest.json +37 -0
- package/registry/ui/drawer/primitives/drawer.tsx +99 -0
- package/registry/ui/drawer/utils/cn.ts +6 -0
- package/registry/ui/dropdown-menu/manifest.json +37 -0
- package/registry/ui/dropdown-menu/primitives/dropdown-menu.tsx +140 -0
- package/registry/ui/dropdown-menu/utils/cn.ts +6 -0
- package/registry/ui/empty-state/empty-state/collection-empty-state.ts +29 -0
- package/registry/ui/empty-state/empty-state/empty-state-card.tsx +72 -0
- package/registry/ui/empty-state/empty-state/index.ts +8 -0
- package/registry/ui/empty-state/empty-state/initial-empty-state.tsx +36 -0
- package/registry/ui/empty-state/empty-state/no-results-state.tsx +20 -0
- package/registry/ui/empty-state/manifest.json +63 -0
- package/registry/ui/empty-state/primitives/button.tsx +89 -0
- package/registry/ui/empty-state/primitives/card.tsx +80 -0
- package/registry/ui/empty-state/utils/cn.ts +6 -0
- package/registry/ui/error-page/error-page/error-code.tsx +16 -0
- package/registry/ui/error-page/error-page/error-page-content.ts +75 -0
- package/registry/ui/error-page/error-page/index.ts +19 -0
- package/registry/ui/error-page/error-page/posthog-error-capture.ts +83 -0
- package/registry/ui/error-page/error-page/saas-error-page.tsx +146 -0
- package/registry/ui/error-page/manifest.json +64 -0
- package/registry/ui/error-page/primitives/button.tsx +89 -0
- package/registry/ui/error-page/utils/cn.ts +6 -0
- package/registry/ui/field-detail-row/manifest.json +32 -0
- package/registry/ui/field-detail-row/primitives/field-detail-row.tsx +28 -0
- package/registry/ui/field-detail-row/utils/cn.ts +6 -0
- package/registry/ui/file-dropzone/manifest.json +35 -0
- package/registry/ui/file-dropzone/primitives/button.tsx +89 -0
- package/registry/ui/file-dropzone/primitives/file-dropzone.tsx +236 -0
- package/registry/ui/file-dropzone/utils/cn.ts +6 -0
- package/registry/ui/help-info-button/manifest.json +72 -0
- package/registry/ui/help-info-button/overlays/responsive-sheet.footer.tsx +88 -0
- package/registry/ui/help-info-button/overlays/responsive-sheet.layouts.tsx +207 -0
- package/registry/ui/help-info-button/overlays/responsive-sheet.shortcuts.ts +103 -0
- package/registry/ui/help-info-button/overlays/responsive-sheet.tsx +132 -0
- package/registry/ui/help-info-button/primitives/button.tsx +89 -0
- package/registry/ui/help-info-button/primitives/drawer.tsx +99 -0
- package/registry/ui/help-info-button/primitives/help-info-button.tsx +63 -0
- package/registry/ui/help-info-button/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/help-info-button/primitives/sheet.tsx +103 -0
- package/registry/ui/help-info-button/primitives/tooltip.tsx +57 -0
- package/registry/ui/help-info-button/utils/cn.ts +6 -0
- package/registry/ui/help-info-button/utils/use-media-query.ts +28 -0
- package/registry/ui/input/manifest.json +31 -0
- package/registry/ui/input/primitives/input.tsx +19 -0
- package/registry/ui/input/utils/cn.ts +6 -0
- package/registry/ui/keyboard-shortcut-hint/manifest.json +32 -0
- package/registry/ui/keyboard-shortcut-hint/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/keyboard-shortcut-hint/utils/cn.ts +6 -0
- package/registry/ui/label/manifest.json +31 -0
- package/registry/ui/label/primitives/label.tsx +21 -0
- package/registry/ui/label/utils/cn.ts +6 -0
- package/registry/ui/pagination/manifest.json +36 -0
- package/registry/ui/pagination/primitives/button.tsx +89 -0
- package/registry/ui/pagination/primitives/pagination.tsx +143 -0
- package/registry/ui/pagination/utils/cn.ts +6 -0
- package/registry/ui/popover/manifest.json +33 -0
- package/registry/ui/popover/primitives/popover.tsx +46 -0
- package/registry/ui/popover/utils/cn.ts +6 -0
- package/registry/ui/responsive-sheet/manifest.json +66 -0
- package/registry/ui/responsive-sheet/overlays/responsive-sheet.footer.tsx +88 -0
- package/registry/ui/responsive-sheet/overlays/responsive-sheet.layouts.tsx +207 -0
- package/registry/ui/responsive-sheet/overlays/responsive-sheet.shortcuts.ts +103 -0
- package/registry/ui/responsive-sheet/overlays/responsive-sheet.tsx +132 -0
- package/registry/ui/responsive-sheet/primitives/button.tsx +89 -0
- package/registry/ui/responsive-sheet/primitives/drawer.tsx +99 -0
- package/registry/ui/responsive-sheet/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/responsive-sheet/primitives/sheet.tsx +103 -0
- package/registry/ui/responsive-sheet/utils/cn.ts +6 -0
- package/registry/ui/responsive-sheet/utils/use-media-query.ts +28 -0
- package/registry/ui/responsive-sheet.footer/manifest.json +40 -0
- package/registry/ui/responsive-sheet.footer/overlays/responsive-sheet.footer.tsx +88 -0
- package/registry/ui/responsive-sheet.footer/primitives/button.tsx +89 -0
- package/registry/ui/responsive-sheet.footer/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/responsive-sheet.footer/utils/cn.ts +6 -0
- package/registry/ui/responsive-sheet.shortcuts/manifest.json +34 -0
- package/registry/ui/responsive-sheet.shortcuts/overlays/responsive-sheet.shortcuts.ts +103 -0
- package/registry/ui/responsive-sheet.shortcuts/utils/cn.ts +6 -0
- package/registry/ui/scroll-fade-area/manifest.json +31 -0
- package/registry/ui/scroll-fade-area/primitives/scroll-fade-area.tsx +295 -0
- package/registry/ui/scroll-fade-area/utils/cn.ts +6 -0
- package/registry/ui/search/manifest.json +35 -0
- package/registry/ui/search/utils/cn.ts +6 -0
- package/registry/ui/search/utils/search.ts +227 -0
- package/registry/ui/searchable-select/manifest.json +48 -0
- package/registry/ui/searchable-select/primitives/input.tsx +19 -0
- package/registry/ui/searchable-select/search/searchable-select-position.ts +95 -0
- package/registry/ui/searchable-select/search/searchable-select.tsx +431 -0
- package/registry/ui/searchable-select/utils/cn.ts +6 -0
- package/registry/ui/searchable-select/utils/search.ts +227 -0
- package/registry/ui/searchable-select-position/manifest.json +32 -0
- package/registry/ui/searchable-select-position/search/searchable-select-position.ts +95 -0
- package/registry/ui/searchable-select-position/utils/cn.ts +6 -0
- package/registry/ui/segmented-toggle/manifest.json +41 -0
- package/registry/ui/segmented-toggle/primitives/scroll-fade-area.tsx +295 -0
- package/registry/ui/segmented-toggle/primitives/segmented-toggle.tsx +106 -0
- package/registry/ui/segmented-toggle/primitives/tabs.tsx +97 -0
- package/registry/ui/segmented-toggle/utils/cn.ts +6 -0
- package/registry/ui/select/manifest.json +37 -0
- package/registry/ui/select/primitives/select.tsx +142 -0
- package/registry/ui/select/utils/cn.ts +6 -0
- package/registry/ui/sheet/manifest.json +39 -0
- package/registry/ui/sheet/primitives/button.tsx +89 -0
- package/registry/ui/sheet/primitives/sheet.tsx +103 -0
- package/registry/ui/sheet/utils/cn.ts +6 -0
- package/registry/ui/skeleton/manifest.json +31 -0
- package/registry/ui/skeleton/primitives/skeleton.tsx +13 -0
- package/registry/ui/skeleton/utils/cn.ts +6 -0
- package/registry/ui/smart-table/manifest.json +115 -0
- package/registry/ui/smart-table/primitives/button.tsx +89 -0
- package/registry/ui/smart-table/primitives/card.tsx +80 -0
- package/registry/ui/smart-table/primitives/display-date.tsx +20 -0
- package/registry/ui/smart-table/primitives/pagination.tsx +143 -0
- package/registry/ui/smart-table/primitives/skeleton.tsx +13 -0
- package/registry/ui/smart-table/primitives/table.tsx +92 -0
- package/registry/ui/smart-table/primitives/tooltip.tsx +57 -0
- package/registry/ui/smart-table/smart-table/DesktopView.tsx +343 -0
- package/registry/ui/smart-table/smart-table/MobileView.tsx +170 -0
- package/registry/ui/smart-table/smart-table/SmartTable.tsx +85 -0
- package/registry/ui/smart-table/smart-table/SmartTableActions.tsx +71 -0
- package/registry/ui/smart-table/smart-table/TruncatedContent.tsx +147 -0
- package/registry/ui/smart-table/smart-table/index.ts +15 -0
- package/registry/ui/smart-table/smart-table/sorting.ts +148 -0
- package/registry/ui/smart-table/smart-table/truncated-content.utils.ts +22 -0
- package/registry/ui/smart-table/smart-table/types.ts +95 -0
- package/registry/ui/smart-table/smart-table/utils.ts +150 -0
- package/registry/ui/smart-table/utils/cn.ts +6 -0
- package/registry/ui/smart-table/utils/date-display.ts +61 -0
- package/registry/ui/smart-table/utils/use-media-query.ts +28 -0
- package/registry/ui/switch/manifest.json +31 -0
- package/registry/ui/switch/primitives/switch.tsx +31 -0
- package/registry/ui/switch/utils/cn.ts +6 -0
- package/registry/ui/table/manifest.json +38 -0
- package/registry/ui/table/primitives/table.tsx +92 -0
- package/registry/ui/table/utils/cn.ts +6 -0
- package/registry/ui/table-toolbar/manifest.json +93 -0
- package/registry/ui/table-toolbar/overlays/responsive-sheet.footer.tsx +88 -0
- package/registry/ui/table-toolbar/overlays/responsive-sheet.layouts.tsx +207 -0
- package/registry/ui/table-toolbar/overlays/responsive-sheet.shortcuts.ts +103 -0
- package/registry/ui/table-toolbar/overlays/responsive-sheet.tsx +132 -0
- package/registry/ui/table-toolbar/primitives/button.tsx +89 -0
- package/registry/ui/table-toolbar/primitives/drawer.tsx +99 -0
- package/registry/ui/table-toolbar/primitives/input.tsx +19 -0
- package/registry/ui/table-toolbar/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/table-toolbar/primitives/sheet.tsx +103 -0
- package/registry/ui/table-toolbar/search/searchable-select-position.ts +95 -0
- package/registry/ui/table-toolbar/search/searchable-select.tsx +431 -0
- package/registry/ui/table-toolbar/table-toolbar/index.ts +9 -0
- package/registry/ui/table-toolbar/table-toolbar/table-toolbar.tsx +552 -0
- package/registry/ui/table-toolbar/utils/cn.ts +6 -0
- package/registry/ui/table-toolbar/utils/search.ts +227 -0
- package/registry/ui/table-toolbar/utils/use-media-query.ts +28 -0
- package/registry/ui/tabs/manifest.json +40 -0
- package/registry/ui/tabs/primitives/scroll-fade-area.tsx +295 -0
- package/registry/ui/tabs/primitives/tabs.tsx +97 -0
- package/registry/ui/tabs/utils/cn.ts +6 -0
- package/registry/ui/textarea/manifest.json +31 -0
- package/registry/ui/textarea/primitives/textarea.tsx +18 -0
- package/registry/ui/textarea/utils/cn.ts +6 -0
- package/registry/ui/tooltip/manifest.json +34 -0
- package/registry/ui/tooltip/primitives/tooltip.tsx +57 -0
- package/registry/ui/tooltip/utils/cn.ts +6 -0
- package/registry/ui/use-media-query/manifest.json +32 -0
- package/registry/ui/use-media-query/utils/cn.ts +6 -0
- package/registry/ui/use-media-query/utils/use-media-query.ts +28 -0
- package/registry/ui/user-picker/manifest.json +52 -0
- package/registry/ui/user-picker/primitives/avatar.tsx +64 -0
- package/registry/ui/user-picker/primitives/button.tsx +89 -0
- package/registry/ui/user-picker/primitives/input.tsx +19 -0
- package/registry/ui/user-picker/primitives/popover.tsx +46 -0
- package/registry/ui/user-picker/primitives/user-picker-utils.ts +113 -0
- package/registry/ui/user-picker/primitives/user-picker.tsx +226 -0
- package/registry/ui/user-picker/utils/cn.ts +6 -0
- package/registry/ui/user-picker-utils/manifest.json +38 -0
- package/registry/ui/user-picker-utils/primitives/user-picker-utils.ts +113 -0
- package/registry/ui/user-picker-utils/utils/cn.ts +6 -0
- package/registry/ui/button/cn.ts +0 -6
- /package/registry/ui/button/{button.tsx → primitives/button.tsx} +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CornerDownLeft } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Button } from '@/components/ui/button';
|
|
8
|
+
import { KeyboardKeycap, ShortcutModifierKeycap } from '@/components/ui/keyboard-shortcut-hint';
|
|
9
|
+
|
|
10
|
+
export function DesktopConfirmShortcutHint({
|
|
11
|
+
desktopModifierLabel,
|
|
12
|
+
}: {
|
|
13
|
+
readonly desktopModifierLabel: string;
|
|
14
|
+
}): React.ReactElement {
|
|
15
|
+
const keycapClassName = 'border-primary-foreground/20 bg-primary-foreground/10';
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<span className="text-primary-foreground/70 inline-flex items-center gap-1">
|
|
19
|
+
<ShortcutModifierKeycap modifierLabel={desktopModifierLabel} className={keycapClassName} />
|
|
20
|
+
<KeyboardKeycap className={keycapClassName}>
|
|
21
|
+
<CornerDownLeft className="size-[10px]" />
|
|
22
|
+
</KeyboardKeycap>
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SheetActionFooterProps {
|
|
28
|
+
readonly footer?: ReactNode;
|
|
29
|
+
readonly onCancel?: () => void;
|
|
30
|
+
readonly cancelLabel: ReactNode;
|
|
31
|
+
readonly onConfirm?: () => void;
|
|
32
|
+
readonly confirmLabel: ReactNode;
|
|
33
|
+
readonly confirmDisabled: boolean;
|
|
34
|
+
readonly confirmLoading: boolean;
|
|
35
|
+
readonly desktopConfirmShortcutEnabled?: boolean;
|
|
36
|
+
readonly desktopModifierLabel?: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function SheetActionFooter({
|
|
40
|
+
footer,
|
|
41
|
+
onCancel,
|
|
42
|
+
cancelLabel,
|
|
43
|
+
onConfirm,
|
|
44
|
+
confirmLabel,
|
|
45
|
+
confirmDisabled,
|
|
46
|
+
confirmLoading,
|
|
47
|
+
desktopConfirmShortcutEnabled = false,
|
|
48
|
+
desktopModifierLabel = null,
|
|
49
|
+
}: SheetActionFooterProps): React.ReactNode {
|
|
50
|
+
if (footer) return footer;
|
|
51
|
+
|
|
52
|
+
if (!onCancel && !onConfirm) return null;
|
|
53
|
+
|
|
54
|
+
const actionCount = Number(Boolean(onCancel)) + Number(Boolean(onConfirm));
|
|
55
|
+
const footerButtonClassName = actionCount === 1 ? 'w-full' : 'min-w-0 flex-1';
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex w-full flex-nowrap items-center gap-2">
|
|
59
|
+
{onCancel ? (
|
|
60
|
+
<Button
|
|
61
|
+
type="button"
|
|
62
|
+
variant="outline"
|
|
63
|
+
onClick={onCancel}
|
|
64
|
+
className={footerButtonClassName}
|
|
65
|
+
>
|
|
66
|
+
{cancelLabel}
|
|
67
|
+
</Button>
|
|
68
|
+
) : null}
|
|
69
|
+
{onConfirm ? (
|
|
70
|
+
<Button
|
|
71
|
+
type="button"
|
|
72
|
+
onClick={onConfirm}
|
|
73
|
+
disabled={confirmDisabled}
|
|
74
|
+
className={`${footerButtonClassName} relative`}
|
|
75
|
+
>
|
|
76
|
+
<span className="inline-flex w-full items-center justify-center">
|
|
77
|
+
<span>{confirmLoading ? 'Saving...' : confirmLabel}</span>
|
|
78
|
+
{desktopConfirmShortcutEnabled && desktopModifierLabel ? (
|
|
79
|
+
<span className="absolute top-1/2 right-2 -translate-y-1/2">
|
|
80
|
+
<DesktopConfirmShortcutHint desktopModifierLabel={desktopModifierLabel} />
|
|
81
|
+
</span>
|
|
82
|
+
) : null}
|
|
83
|
+
</span>
|
|
84
|
+
</Button>
|
|
85
|
+
) : null}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { SheetOutsideInteractionGuard } from '@/components/ui/responsive-sheet';
|
|
6
|
+
import type { ResponsiveSheetClassNames } from '@/components/ui/responsive-sheet';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Drawer,
|
|
10
|
+
DrawerContent,
|
|
11
|
+
DrawerDescription,
|
|
12
|
+
DrawerHeader,
|
|
13
|
+
DrawerTitle,
|
|
14
|
+
} from '@/components/ui/drawer';
|
|
15
|
+
import {
|
|
16
|
+
Sheet,
|
|
17
|
+
SheetContent,
|
|
18
|
+
SheetDescription,
|
|
19
|
+
SheetHeader,
|
|
20
|
+
SheetTitle,
|
|
21
|
+
} from '@/components/ui/sheet';
|
|
22
|
+
import { cn } from '@/lib/utils';
|
|
23
|
+
|
|
24
|
+
interface SheetDescriptionBlockProps {
|
|
25
|
+
readonly title: ReactNode;
|
|
26
|
+
readonly description?: ReactNode;
|
|
27
|
+
readonly classes?: ResponsiveSheetClassNames;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function SheetDescriptionBlock({
|
|
31
|
+
title,
|
|
32
|
+
description,
|
|
33
|
+
classes,
|
|
34
|
+
}: SheetDescriptionBlockProps): React.ReactElement {
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<SheetTitle className={classes?.title}>{title}</SheetTitle>
|
|
38
|
+
{description ? (
|
|
39
|
+
<SheetDescription className={classes?.description}>{description}</SheetDescription>
|
|
40
|
+
) : null}
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface SharedSheetLayoutProps {
|
|
46
|
+
readonly open: boolean;
|
|
47
|
+
readonly onOpenChange: (open: boolean) => void;
|
|
48
|
+
readonly modal: boolean;
|
|
49
|
+
readonly outsideInteractionGuard?: SheetOutsideInteractionGuard;
|
|
50
|
+
readonly title: ReactNode;
|
|
51
|
+
readonly description?: ReactNode;
|
|
52
|
+
readonly children: ReactNode;
|
|
53
|
+
readonly footer: ReactNode;
|
|
54
|
+
readonly mobileDrawerContentClassName?: string;
|
|
55
|
+
readonly contentClassName?: string;
|
|
56
|
+
readonly footerClassName?: string;
|
|
57
|
+
readonly classes?: ResponsiveSheetClassNames;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function shouldPreventOutsideInteraction(
|
|
61
|
+
target: EventTarget | null,
|
|
62
|
+
guard?: SheetOutsideInteractionGuard,
|
|
63
|
+
): boolean {
|
|
64
|
+
const element =
|
|
65
|
+
target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
|
66
|
+
|
|
67
|
+
if (!element) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (element.closest('[data-searchable-select-content]')) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return guard?.selectors.some((selector) => element.closest(selector)) ?? false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type MobileSheetLayoutProps = SharedSheetLayoutProps;
|
|
79
|
+
|
|
80
|
+
export function MobileSheetLayout({
|
|
81
|
+
open,
|
|
82
|
+
onOpenChange,
|
|
83
|
+
modal,
|
|
84
|
+
outsideInteractionGuard,
|
|
85
|
+
title,
|
|
86
|
+
description,
|
|
87
|
+
children,
|
|
88
|
+
footer,
|
|
89
|
+
mobileDrawerContentClassName,
|
|
90
|
+
contentClassName,
|
|
91
|
+
footerClassName,
|
|
92
|
+
classes,
|
|
93
|
+
}: MobileSheetLayoutProps): React.ReactElement {
|
|
94
|
+
return (
|
|
95
|
+
<Drawer open={open} onOpenChange={onOpenChange} modal={modal}>
|
|
96
|
+
<DrawerContent
|
|
97
|
+
aria-describedby={description ? undefined : 'responsive-sheet-description-empty'}
|
|
98
|
+
className={cn(
|
|
99
|
+
'px-4 pb-[calc(env(safe-area-inset-bottom)+20px)]',
|
|
100
|
+
mobileDrawerContentClassName,
|
|
101
|
+
classes?.mobileContent,
|
|
102
|
+
)}
|
|
103
|
+
onInteractOutside={(event) => {
|
|
104
|
+
if (shouldPreventOutsideInteraction(event.target, outsideInteractionGuard)) {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
}
|
|
107
|
+
}}
|
|
108
|
+
onPointerDownOutside={(event) => {
|
|
109
|
+
if (shouldPreventOutsideInteraction(event.target, outsideInteractionGuard)) {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
}
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<DrawerHeader className={cn('px-0 pb-4', classes?.header)}>
|
|
115
|
+
<DrawerTitle className={classes?.title}>{title}</DrawerTitle>
|
|
116
|
+
{description ? (
|
|
117
|
+
<DrawerDescription className={classes?.description}>{description}</DrawerDescription>
|
|
118
|
+
) : (
|
|
119
|
+
<DrawerDescription id="responsive-sheet-description-empty" className="sr-only">
|
|
120
|
+
Dialog
|
|
121
|
+
</DrawerDescription>
|
|
122
|
+
)}
|
|
123
|
+
</DrawerHeader>
|
|
124
|
+
<div className="flex min-h-0 flex-1 flex-col gap-4">
|
|
125
|
+
<div
|
|
126
|
+
className={cn(
|
|
127
|
+
'-mx-1 flex-1 overflow-y-auto px-1 pb-3 [scrollbar-gutter:stable]',
|
|
128
|
+
contentClassName,
|
|
129
|
+
classes?.body,
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</div>
|
|
134
|
+
{footer ? (
|
|
135
|
+
<div className={cn('shrink-0 border-t pt-4 pb-3', footerClassName, classes?.footer)}>
|
|
136
|
+
{footer}
|
|
137
|
+
</div>
|
|
138
|
+
) : null}
|
|
139
|
+
</div>
|
|
140
|
+
</DrawerContent>
|
|
141
|
+
</Drawer>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface DesktopSheetLayoutProps extends SharedSheetLayoutProps {
|
|
146
|
+
readonly width: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function DesktopSheetLayout({
|
|
150
|
+
open,
|
|
151
|
+
onOpenChange,
|
|
152
|
+
modal,
|
|
153
|
+
outsideInteractionGuard,
|
|
154
|
+
title,
|
|
155
|
+
description,
|
|
156
|
+
children,
|
|
157
|
+
footer,
|
|
158
|
+
width,
|
|
159
|
+
contentClassName,
|
|
160
|
+
footerClassName,
|
|
161
|
+
classes,
|
|
162
|
+
}: DesktopSheetLayoutProps): React.ReactElement {
|
|
163
|
+
return (
|
|
164
|
+
<Sheet open={open} onOpenChange={onOpenChange} modal={modal}>
|
|
165
|
+
<SheetContent
|
|
166
|
+
aria-describedby={description ? undefined : 'responsive-sheet-description-empty'}
|
|
167
|
+
style={{ width: `${String(width)}px`, maxWidth: '85vw' }}
|
|
168
|
+
className={cn('flex flex-col gap-0 p-0', classes?.desktopContent)}
|
|
169
|
+
onInteractOutside={(event) => {
|
|
170
|
+
if (shouldPreventOutsideInteraction(event.target, outsideInteractionGuard)) {
|
|
171
|
+
event.preventDefault();
|
|
172
|
+
}
|
|
173
|
+
}}
|
|
174
|
+
onPointerDownOutside={(event) => {
|
|
175
|
+
if (shouldPreventOutsideInteraction(event.target, outsideInteractionGuard)) {
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
}
|
|
178
|
+
}}
|
|
179
|
+
>
|
|
180
|
+
<SheetHeader className={cn('border-b px-4 py-4', classes?.header)}>
|
|
181
|
+
<SheetDescriptionBlock title={title} description={description} classes={classes} />
|
|
182
|
+
</SheetHeader>
|
|
183
|
+
{description ? null : (
|
|
184
|
+
<SheetDescription id="responsive-sheet-description-empty" className="sr-only">
|
|
185
|
+
Dialog
|
|
186
|
+
</SheetDescription>
|
|
187
|
+
)}
|
|
188
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
189
|
+
<div
|
|
190
|
+
className={cn(
|
|
191
|
+
'flex-1 overflow-x-visible overflow-y-auto px-4 pt-4 pb-6 [scrollbar-gutter:stable]',
|
|
192
|
+
contentClassName,
|
|
193
|
+
classes?.body,
|
|
194
|
+
)}
|
|
195
|
+
>
|
|
196
|
+
{children}
|
|
197
|
+
</div>
|
|
198
|
+
{footer ? (
|
|
199
|
+
<div className={cn('border-t px-4 py-4', footerClassName, classes?.footer)}>
|
|
200
|
+
{footer}
|
|
201
|
+
</div>
|
|
202
|
+
) : null}
|
|
203
|
+
</div>
|
|
204
|
+
</SheetContent>
|
|
205
|
+
</Sheet>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface NavigatorWithUserAgentData extends Navigator {
|
|
4
|
+
readonly userAgentData?: {
|
|
5
|
+
readonly platform?: string;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getDesktopShortcutModifierLabel(): string {
|
|
10
|
+
if (typeof navigator === "undefined") {
|
|
11
|
+
return "Ctrl";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const navigatorWithUserAgentData = navigator as NavigatorWithUserAgentData;
|
|
15
|
+
const platform =
|
|
16
|
+
navigatorWithUserAgentData.userAgentData?.platform ?? navigator.userAgent;
|
|
17
|
+
|
|
18
|
+
return /Mac|iPhone|iPad|iPod/i.test(platform) ? "Cmd" : "Ctrl";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isAllowedConfirmShortcutEvent(
|
|
22
|
+
event: KeyboardEvent,
|
|
23
|
+
desktopModifierLabel: string,
|
|
24
|
+
): boolean {
|
|
25
|
+
if (event.key !== "Enter" || event.repeat || event.isComposing) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const expectsMetaKey = desktopModifierLabel === "Cmd";
|
|
30
|
+
const usedExpectedModifier = expectsMetaKey ? event.metaKey : event.ctrlKey;
|
|
31
|
+
const usedOtherModifier = expectsMetaKey ? event.ctrlKey : event.metaKey;
|
|
32
|
+
const usedShiftModifier = event.shiftKey;
|
|
33
|
+
const usedAltModifier = event.altKey;
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
!usedExpectedModifier ||
|
|
37
|
+
usedOtherModifier ||
|
|
38
|
+
usedShiftModifier ||
|
|
39
|
+
usedAltModifier
|
|
40
|
+
) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useDesktopShortcutModifierLabel(
|
|
48
|
+
enabled: boolean,
|
|
49
|
+
): string | null {
|
|
50
|
+
const [desktopModifierLabel, setDesktopModifierLabel] = useState<
|
|
51
|
+
string | null
|
|
52
|
+
>(null);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!enabled) {
|
|
56
|
+
setDesktopModifierLabel(null);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setDesktopModifierLabel(getDesktopShortcutModifierLabel());
|
|
61
|
+
}, [enabled]);
|
|
62
|
+
|
|
63
|
+
return desktopModifierLabel;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface UseDesktopConfirmShortcutOptions {
|
|
67
|
+
readonly open: boolean;
|
|
68
|
+
readonly enabled: boolean;
|
|
69
|
+
readonly confirmDisabled: boolean;
|
|
70
|
+
readonly confirmLoading: boolean;
|
|
71
|
+
readonly onConfirm?: () => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function useDesktopConfirmShortcut({
|
|
75
|
+
open,
|
|
76
|
+
enabled,
|
|
77
|
+
confirmDisabled,
|
|
78
|
+
confirmLoading,
|
|
79
|
+
onConfirm,
|
|
80
|
+
}: UseDesktopConfirmShortcutOptions): void {
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!open || !enabled || !onConfirm || confirmDisabled || confirmLoading) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const desktopModifierLabel = getDesktopShortcutModifierLabel();
|
|
87
|
+
|
|
88
|
+
const handleKeyDown = (event: KeyboardEvent): void => {
|
|
89
|
+
if (!isAllowedConfirmShortcutEvent(event, desktopModifierLabel)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
onConfirm();
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
98
|
+
|
|
99
|
+
return () => {
|
|
100
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
101
|
+
};
|
|
102
|
+
}, [confirmDisabled, confirmLoading, enabled, onConfirm, open]);
|
|
103
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import { SheetActionFooter } from '@/components/ui/responsive-sheet.footer';
|
|
6
|
+
import { DesktopSheetLayout, MobileSheetLayout } from '@/components/ui/responsive-sheet.layouts';
|
|
7
|
+
import {
|
|
8
|
+
useDesktopConfirmShortcut,
|
|
9
|
+
useDesktopShortcutModifierLabel,
|
|
10
|
+
} from '@/components/ui/responsive-sheet.shortcuts';
|
|
11
|
+
import { cn } from '@/lib/utils';
|
|
12
|
+
import { useIsMobile } from '@/components/ui/use-media-query';
|
|
13
|
+
|
|
14
|
+
export interface SheetOutsideInteractionGuard {
|
|
15
|
+
readonly selectors: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ResponsiveSheetClassNames {
|
|
19
|
+
readonly desktopContent?: string;
|
|
20
|
+
readonly mobileContent?: string;
|
|
21
|
+
readonly header?: string;
|
|
22
|
+
readonly body?: string;
|
|
23
|
+
readonly footer?: string;
|
|
24
|
+
readonly title?: string;
|
|
25
|
+
readonly description?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ResponsiveSheetProps {
|
|
29
|
+
readonly open: boolean;
|
|
30
|
+
readonly onOpenChange: (open: boolean) => void;
|
|
31
|
+
readonly title: ReactNode;
|
|
32
|
+
readonly description?: ReactNode;
|
|
33
|
+
readonly children: ReactNode;
|
|
34
|
+
readonly footer?: ReactNode;
|
|
35
|
+
readonly onCancel?: () => void;
|
|
36
|
+
readonly cancelLabel?: ReactNode;
|
|
37
|
+
readonly onConfirm?: () => void;
|
|
38
|
+
readonly confirmLabel?: ReactNode;
|
|
39
|
+
readonly confirmDisabled?: boolean;
|
|
40
|
+
readonly confirmLoading?: boolean;
|
|
41
|
+
readonly width?: number;
|
|
42
|
+
readonly modal?: boolean;
|
|
43
|
+
readonly outsideInteractionGuard?: SheetOutsideInteractionGuard;
|
|
44
|
+
readonly enableDesktopConfirmShortcut?: boolean;
|
|
45
|
+
readonly mobileDrawerContentClassName?: string;
|
|
46
|
+
readonly className?: string;
|
|
47
|
+
readonly contentClassName?: string;
|
|
48
|
+
readonly footerClassName?: string;
|
|
49
|
+
readonly classes?: ResponsiveSheetClassNames;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function ResponsiveSheet({
|
|
53
|
+
open,
|
|
54
|
+
onOpenChange,
|
|
55
|
+
title,
|
|
56
|
+
description,
|
|
57
|
+
children,
|
|
58
|
+
footer,
|
|
59
|
+
onCancel,
|
|
60
|
+
cancelLabel = 'Cancel',
|
|
61
|
+
onConfirm,
|
|
62
|
+
confirmLabel = 'Save',
|
|
63
|
+
confirmDisabled = false,
|
|
64
|
+
confirmLoading = false,
|
|
65
|
+
width = 550,
|
|
66
|
+
modal = true,
|
|
67
|
+
outsideInteractionGuard,
|
|
68
|
+
enableDesktopConfirmShortcut = true,
|
|
69
|
+
mobileDrawerContentClassName,
|
|
70
|
+
className,
|
|
71
|
+
contentClassName,
|
|
72
|
+
footerClassName,
|
|
73
|
+
classes,
|
|
74
|
+
}: ResponsiveSheetProps): React.ReactElement {
|
|
75
|
+
const isMobile = useIsMobile();
|
|
76
|
+
const desktopConfirmShortcutEnabled =
|
|
77
|
+
!isMobile && enableDesktopConfirmShortcut && Boolean(onConfirm);
|
|
78
|
+
const desktopModifierLabel = useDesktopShortcutModifierLabel(desktopConfirmShortcutEnabled);
|
|
79
|
+
|
|
80
|
+
useDesktopConfirmShortcut({
|
|
81
|
+
open,
|
|
82
|
+
enabled: desktopConfirmShortcutEnabled,
|
|
83
|
+
confirmDisabled,
|
|
84
|
+
confirmLoading,
|
|
85
|
+
onConfirm: onConfirm
|
|
86
|
+
? () => {
|
|
87
|
+
onConfirm();
|
|
88
|
+
}
|
|
89
|
+
: undefined,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const resolvedFooter = (
|
|
93
|
+
<SheetActionFooter
|
|
94
|
+
footer={footer}
|
|
95
|
+
onCancel={onCancel}
|
|
96
|
+
cancelLabel={cancelLabel}
|
|
97
|
+
onConfirm={onConfirm}
|
|
98
|
+
confirmLabel={confirmLabel}
|
|
99
|
+
confirmDisabled={confirmDisabled}
|
|
100
|
+
confirmLoading={confirmLoading}
|
|
101
|
+
desktopConfirmShortcutEnabled={desktopConfirmShortcutEnabled}
|
|
102
|
+
desktopModifierLabel={desktopModifierLabel}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
const hasFooter = [footer, onCancel, onConfirm].some(
|
|
106
|
+
(value) => value !== null && value !== undefined,
|
|
107
|
+
);
|
|
108
|
+
const sharedLayoutProps = {
|
|
109
|
+
open,
|
|
110
|
+
onOpenChange,
|
|
111
|
+
modal,
|
|
112
|
+
outsideInteractionGuard,
|
|
113
|
+
title,
|
|
114
|
+
description,
|
|
115
|
+
footer: hasFooter ? resolvedFooter : null,
|
|
116
|
+
children,
|
|
117
|
+
contentClassName,
|
|
118
|
+
footerClassName,
|
|
119
|
+
mobileDrawerContentClassName,
|
|
120
|
+
classes: {
|
|
121
|
+
...classes,
|
|
122
|
+
desktopContent: cn(className, classes?.desktopContent),
|
|
123
|
+
mobileContent: cn(className, mobileDrawerContentClassName, classes?.mobileContent),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return isMobile ? (
|
|
128
|
+
<MobileSheetLayout {...sharedLayoutProps} />
|
|
129
|
+
) : (
|
|
130
|
+
<DesktopSheetLayout {...sharedLayoutProps} width={width} />
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva } from "class-variance-authority";
|
|
3
|
+
import { Slot } from "radix-ui";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"group/button inline-flex shrink-0 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
"bg-primary text-primary-foreground hover:brightness-90 [a]:hover:bg-primary/80",
|
|
14
|
+
outline:
|
|
15
|
+
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
16
|
+
secondary:
|
|
17
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
18
|
+
ghost:
|
|
19
|
+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
20
|
+
destructive:
|
|
21
|
+
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
|
22
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
23
|
+
},
|
|
24
|
+
size: {
|
|
25
|
+
default:
|
|
26
|
+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
27
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
28
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
29
|
+
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
30
|
+
icon: "size-8",
|
|
31
|
+
"icon-xs":
|
|
32
|
+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
33
|
+
"icon-sm":
|
|
34
|
+
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
35
|
+
"icon-lg": "size-9",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
defaultVariants: {
|
|
39
|
+
variant: "default",
|
|
40
|
+
size: "default",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
type ButtonVariant =
|
|
46
|
+
| "default"
|
|
47
|
+
| "outline"
|
|
48
|
+
| "secondary"
|
|
49
|
+
| "ghost"
|
|
50
|
+
| "destructive"
|
|
51
|
+
| "link";
|
|
52
|
+
type ButtonSize =
|
|
53
|
+
| "default"
|
|
54
|
+
| "xs"
|
|
55
|
+
| "sm"
|
|
56
|
+
| "lg"
|
|
57
|
+
| "icon"
|
|
58
|
+
| "icon-xs"
|
|
59
|
+
| "icon-sm"
|
|
60
|
+
| "icon-lg";
|
|
61
|
+
|
|
62
|
+
interface ButtonProps extends React.ComponentProps<"button"> {
|
|
63
|
+
readonly asChild?: boolean;
|
|
64
|
+
readonly size?: ButtonSize;
|
|
65
|
+
readonly variant?: ButtonVariant;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Button({
|
|
69
|
+
className,
|
|
70
|
+
variant = "default",
|
|
71
|
+
size = "default",
|
|
72
|
+
asChild = false,
|
|
73
|
+
...props
|
|
74
|
+
}: ButtonProps) {
|
|
75
|
+
const Comp = asChild ? Slot.Root : "button";
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Comp
|
|
79
|
+
data-slot="button"
|
|
80
|
+
data-variant={variant}
|
|
81
|
+
data-size={size}
|
|
82
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { Button, buttonVariants };
|
|
89
|
+
export type { ButtonProps, ButtonSize, ButtonVariant };
|