pika-ux 1.0.0-beta.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 +9 -0
- package/dist/icon-generator/generate-icon-ts-indices.js +78 -0
- package/dist/shadcn-postinstall/index.js +114 -0
- package/package.json +102 -0
- package/readme.md +50 -0
- package/scripts/setup.js +100 -0
- package/src/App.svelte +51 -0
- package/src/app.css +349 -0
- package/src/icons/ci/index.d.ts +5009 -0
- package/src/icons/lucide/index.d.ts +11274 -0
- package/src/index.ts +23 -0
- package/src/lib/docsite/Navigation.svelte +77 -0
- package/src/lib/docsite/pages/Colors.svelte +35 -0
- package/src/lib/docsite/pages/Components.svelte +50 -0
- package/src/lib/docsite/pages/GettingStarted.svelte +21 -0
- package/src/lib/docsite/pages/Icons.svelte +22 -0
- package/src/lib/docsite/pages/components/Button.svelte +40 -0
- package/src/main.ts +9 -0
- package/src/pika/chip/chip.svelte +95 -0
- package/src/pika/chip/index.ts +1 -0
- package/src/pika/combobox/combobox-types.ts +5 -0
- package/src/pika/combobox/combobox.svelte +221 -0
- package/src/pika/combobox/index.ts +2 -0
- package/src/pika/confirm-dialog/confirm-dialog.svelte +48 -0
- package/src/pika/confirm-dialog/index.ts +1 -0
- package/src/pika/copy-button/copy-button.svelte +134 -0
- package/src/pika/copy-button/index.ts +1 -0
- package/src/pika/create-copy-link-button/create-copy-link-button.svelte +133 -0
- package/src/pika/create-copy-link-button/index.ts +1 -0
- package/src/pika/date-picker/date-picker.svelte +33 -0
- package/src/pika/date-picker/index.ts +1 -0
- package/src/pika/date-range-picker/date-range-picker.svelte +48 -0
- package/src/pika/date-range-picker/index.ts +1 -0
- package/src/pika/date-time-picker/date-time-picker.svelte +336 -0
- package/src/pika/date-time-picker/index.ts +1 -0
- package/src/pika/expandable-container/expandable-container.svelte +155 -0
- package/src/pika/expandable-container/index.ts +1 -0
- package/src/pika/index.ts +29 -0
- package/src/pika/list/index.ts +2 -0
- package/src/pika/list/list-types.ts +5 -0
- package/src/pika/list/list.svelte +349 -0
- package/src/pika/markdown-editor/github.scss +87 -0
- package/src/pika/markdown-editor/index.ts +1 -0
- package/src/pika/markdown-editor/markdown-editor.svelte +44 -0
- package/src/pika/permanent-toast/index.ts +1 -0
- package/src/pika/permanent-toast/permanent-toast.svelte +47 -0
- package/src/pika/pika-alert/index.ts +1 -0
- package/src/pika/pika-alert/pika-alert.svelte +53 -0
- package/src/pika/pika-badge/index.ts +1 -0
- package/src/pika/pika-badge/pika-badge.svelte +61 -0
- package/src/pika/pika-table/index.ts +7 -0
- package/src/pika/pika-table/pika-table-cell.svelte +9 -0
- package/src/pika/pika-table/pika-table-checkbox.svelte +8 -0
- package/src/pika/pika-table/pika-table-column-header.svelte +88 -0
- package/src/pika/pika-table/pika-table-faceted-filter.svelte +109 -0
- package/src/pika/pika-table/pika-table-pagination.svelte +95 -0
- package/src/pika/pika-table/pika-table-row-actions.svelte +58 -0
- package/src/pika/pika-table/pika-table-toolbar.svelte +88 -0
- package/src/pika/pika-table/pika-table-view-options.svelte +35 -0
- package/src/pika/pika-table/pika-table.svelte +295 -0
- package/src/pika/pika-table/types.ts +106 -0
- package/src/pika/pika-tabs/index.ts +18 -0
- package/src/pika/pika-tabs/tabs-content.svelte +16 -0
- package/src/pika/pika-tabs/tabs-list.svelte +12 -0
- package/src/pika/pika-tabs/tabs-trigger.svelte +23 -0
- package/src/pika/popup-help/index.ts +1 -0
- package/src/pika/popup-help/popup-help.svelte +33 -0
- package/src/pika/simple-dropdown/index.ts +2 -0
- package/src/pika/simple-dropdown/simple-dropdown-types.ts +5 -0
- package/src/pika/simple-dropdown/simple-dropdown.svelte +288 -0
- package/src/pika/slideout/constants.ts +5 -0
- package/src/pika/slideout/context.svelte.ts +110 -0
- package/src/pika/slideout/index.ts +19 -0
- package/src/pika/slideout/slideout-content.svelte +36 -0
- package/src/pika/slideout/slideout-panel.svelte +126 -0
- package/src/pika/slideout/slideout-provider.svelte +49 -0
- package/src/pika/slideout/slideout-rail.svelte.die +69 -0
- package/src/pika/slideout/slideout.svelte +33 -0
- package/src/pika/slideout/slideout.svelte.old +113 -0
- package/src/pika/text-wave-shimmer/index.ts +1 -0
- package/src/pika/text-wave-shimmer/text-wave-shimmer.svelte +43 -0
- package/src/pika/tooltip-plus/index.ts +1 -0
- package/src/pika/tooltip-plus/tooltip-plus.svelte +42 -0
- package/src/shadcn/.DS_Store +0 -0
- package/src/shadcn/alert/alert-description.svelte +11 -0
- package/src/shadcn/alert/alert-title.svelte +24 -0
- package/src/shadcn/alert/alert.svelte +39 -0
- package/src/shadcn/alert/index.ts +14 -0
- package/src/shadcn/avatar/avatar-fallback.svelte +13 -0
- package/src/shadcn/avatar/avatar-image.svelte +13 -0
- package/src/shadcn/avatar/avatar.svelte +19 -0
- package/src/shadcn/avatar/index.ts +13 -0
- package/src/shadcn/badge/badge.svelte +48 -0
- package/src/shadcn/badge/index.ts +2 -0
- package/src/shadcn/breadcrumb/breadcrumb-ellipsis.svelte +12 -0
- package/src/shadcn/breadcrumb/breadcrumb-item.svelte +20 -0
- package/src/shadcn/breadcrumb/breadcrumb-link.svelte +31 -0
- package/src/shadcn/breadcrumb/breadcrumb-list.svelte +20 -0
- package/src/shadcn/breadcrumb/breadcrumb-page.svelte +23 -0
- package/src/shadcn/breadcrumb/breadcrumb-separator.svelte +15 -0
- package/src/shadcn/breadcrumb/breadcrumb.svelte +15 -0
- package/src/shadcn/breadcrumb/index.ts +25 -0
- package/src/shadcn/button/button.svelte +81 -0
- package/src/shadcn/button/index.ts +17 -0
- package/src/shadcn/calendar/calendar-caption.svelte +76 -0
- package/src/shadcn/calendar/calendar-cell.svelte +19 -0
- package/src/shadcn/calendar/calendar-day.svelte +31 -0
- package/src/shadcn/calendar/calendar-grid-body.svelte +12 -0
- package/src/shadcn/calendar/calendar-grid-head.svelte +12 -0
- package/src/shadcn/calendar/calendar-grid-row.svelte +12 -0
- package/src/shadcn/calendar/calendar-grid.svelte +16 -0
- package/src/shadcn/calendar/calendar-head-cell.svelte +16 -0
- package/src/shadcn/calendar/calendar-header.svelte +16 -0
- package/src/shadcn/calendar/calendar-heading.svelte +12 -0
- package/src/shadcn/calendar/calendar-month-select.svelte +25 -0
- package/src/shadcn/calendar/calendar-month.svelte +15 -0
- package/src/shadcn/calendar/calendar-months.svelte +20 -0
- package/src/shadcn/calendar/calendar-nav.svelte +19 -0
- package/src/shadcn/calendar/calendar-next-button.svelte +19 -0
- package/src/shadcn/calendar/calendar-prev-button.svelte +19 -0
- package/src/shadcn/calendar/calendar-year-select.svelte +25 -0
- package/src/shadcn/calendar/calendar.svelte +61 -0
- package/src/shadcn/calendar/index.ts +30 -0
- package/src/shadcn/card/card-content.svelte +16 -0
- package/src/shadcn/card/card-description.svelte +16 -0
- package/src/shadcn/card/card-footer.svelte +16 -0
- package/src/shadcn/card/card-header.svelte +16 -0
- package/src/shadcn/card/card-title.svelte +25 -0
- package/src/shadcn/card/card.svelte +20 -0
- package/src/shadcn/card/index.ts +22 -0
- package/src/shadcn/carousel/carousel-content.svelte +39 -0
- package/src/shadcn/carousel/carousel-item.svelte +26 -0
- package/src/shadcn/carousel/carousel-next.svelte +30 -0
- package/src/shadcn/carousel/carousel-previous.svelte +30 -0
- package/src/shadcn/carousel/carousel.svelte +88 -0
- package/src/shadcn/carousel/context.ts +51 -0
- package/src/shadcn/carousel/index.ts +19 -0
- package/src/shadcn/checkbox/checkbox.svelte +36 -0
- package/src/shadcn/checkbox/index.ts +6 -0
- package/src/shadcn/collapsible/collapsible-content.svelte +7 -0
- package/src/shadcn/collapsible/collapsible-trigger.svelte +7 -0
- package/src/shadcn/collapsible/collapsible.svelte +11 -0
- package/src/shadcn/collapsible/index.ts +13 -0
- package/src/shadcn/command/command-dialog.svelte +40 -0
- package/src/shadcn/command/command-empty.svelte +13 -0
- package/src/shadcn/command/command-group.svelte +30 -0
- package/src/shadcn/command/command-input.svelte +21 -0
- package/src/shadcn/command/command-item.svelte +16 -0
- package/src/shadcn/command/command-link-item.svelte +16 -0
- package/src/shadcn/command/command-list.svelte +13 -0
- package/src/shadcn/command/command-separator.svelte +13 -0
- package/src/shadcn/command/command-shortcut.svelte +20 -0
- package/src/shadcn/command/command.svelte +19 -0
- package/src/shadcn/command/index.ts +40 -0
- package/src/shadcn/data-table/data-table.svelte.ts +141 -0
- package/src/shadcn/data-table/flex-render.svelte +36 -0
- package/src/shadcn/data-table/index.ts +3 -0
- package/src/shadcn/data-table/render-helpers.ts +111 -0
- package/src/shadcn/dialog/dialog-close.svelte +7 -0
- package/src/shadcn/dialog/dialog-content.svelte +43 -0
- package/src/shadcn/dialog/dialog-description.svelte +13 -0
- package/src/shadcn/dialog/dialog-footer.svelte +20 -0
- package/src/shadcn/dialog/dialog-header.svelte +20 -0
- package/src/shadcn/dialog/dialog-overlay.svelte +16 -0
- package/src/shadcn/dialog/dialog-title.svelte +13 -0
- package/src/shadcn/dialog/dialog-trigger.svelte +7 -0
- package/src/shadcn/dialog/index.ts +37 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-checkbox-item.svelte +41 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-content.svelte +27 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-item.svelte +27 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-label.svelte +24 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-radio-item.svelte +26 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-separator.svelte +13 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-sub-content.svelte +16 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/src/shadcn/dropdown-menu/index.ts +49 -0
- package/src/shadcn/index.ts +40 -0
- package/src/shadcn/input/index.ts +7 -0
- package/src/shadcn/input/input.svelte +51 -0
- package/src/shadcn/is-mobile.svelte.ts +9 -0
- package/src/shadcn/label/index.ts +7 -0
- package/src/shadcn/label/label.svelte +16 -0
- package/src/shadcn/popover/index.ts +17 -0
- package/src/shadcn/popover/popover-content.svelte +29 -0
- package/src/shadcn/popover/popover-trigger.svelte +8 -0
- package/src/shadcn/radio-group/index.ts +10 -0
- package/src/shadcn/radio-group/radio-group-item.svelte +25 -0
- package/src/shadcn/radio-group/radio-group.svelte +19 -0
- package/src/shadcn/range-calendar/index.ts +30 -0
- package/src/shadcn/range-calendar/range-calendar-cell.svelte +19 -0
- package/src/shadcn/range-calendar/range-calendar-day.svelte +35 -0
- package/src/shadcn/range-calendar/range-calendar-grid-body.svelte +12 -0
- package/src/shadcn/range-calendar/range-calendar-grid-head.svelte +12 -0
- package/src/shadcn/range-calendar/range-calendar-grid-row.svelte +12 -0
- package/src/shadcn/range-calendar/range-calendar-grid.svelte +16 -0
- package/src/shadcn/range-calendar/range-calendar-head-cell.svelte +16 -0
- package/src/shadcn/range-calendar/range-calendar-header.svelte +16 -0
- package/src/shadcn/range-calendar/range-calendar-heading.svelte +16 -0
- package/src/shadcn/range-calendar/range-calendar-months.svelte +20 -0
- package/src/shadcn/range-calendar/range-calendar-next-button.svelte +18 -0
- package/src/shadcn/range-calendar/range-calendar-prev-button.svelte +18 -0
- package/src/shadcn/range-calendar/range-calendar.svelte +57 -0
- package/src/shadcn/resizable/index.ts +13 -0
- package/src/shadcn/resizable/resizable-handle.svelte +30 -0
- package/src/shadcn/resizable/resizable-pane-group.svelte +22 -0
- package/src/shadcn/scroll-area/index.ts +10 -0
- package/src/shadcn/scroll-area/scroll-area-scrollbar.svelte +28 -0
- package/src/shadcn/scroll-area/scroll-area.svelte +35 -0
- package/src/shadcn/select/index.ts +37 -0
- package/src/shadcn/select/select-content.svelte +38 -0
- package/src/shadcn/select/select-group-heading.svelte +21 -0
- package/src/shadcn/select/select-group.svelte +7 -0
- package/src/shadcn/select/select-item.svelte +31 -0
- package/src/shadcn/select/select-label.svelte +20 -0
- package/src/shadcn/select/select-scroll-down-button.svelte +11 -0
- package/src/shadcn/select/select-scroll-up-button.svelte +11 -0
- package/src/shadcn/select/select-separator.svelte +14 -0
- package/src/shadcn/select/select-trigger.svelte +30 -0
- package/src/shadcn/separator/index.ts +7 -0
- package/src/shadcn/separator/separator.svelte +16 -0
- package/src/shadcn/sheet/index.ts +36 -0
- package/src/shadcn/sheet/sheet-close.svelte +7 -0
- package/src/shadcn/sheet/sheet-content.svelte +66 -0
- package/src/shadcn/sheet/sheet-description.svelte +13 -0
- package/src/shadcn/sheet/sheet-footer.svelte +15 -0
- package/src/shadcn/sheet/sheet-header.svelte +15 -0
- package/src/shadcn/sheet/sheet-overlay.svelte +16 -0
- package/src/shadcn/sheet/sheet-title.svelte +13 -0
- package/src/shadcn/sheet/sheet-trigger.svelte +7 -0
- package/src/shadcn/sidebar/constants.ts +6 -0
- package/src/shadcn/sidebar/context.svelte.ts +80 -0
- package/src/shadcn/sidebar/index.ts +75 -0
- package/src/shadcn/sidebar/sidebar-content.svelte +24 -0
- package/src/shadcn/sidebar/sidebar-footer.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-group-action.svelte +36 -0
- package/src/shadcn/sidebar/sidebar-group-content.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-group-label.svelte +34 -0
- package/src/shadcn/sidebar/sidebar-group.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-header.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-input.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-inset.svelte +24 -0
- package/src/shadcn/sidebar/sidebar-menu-action.svelte +43 -0
- package/src/shadcn/sidebar/sidebar-menu-badge.svelte +29 -0
- package/src/shadcn/sidebar/sidebar-menu-button.svelte +101 -0
- package/src/shadcn/sidebar/sidebar-menu-item.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-menu-skeleton.svelte +36 -0
- package/src/shadcn/sidebar/sidebar-menu-sub-button.svelte +43 -0
- package/src/shadcn/sidebar/sidebar-menu-sub-item.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-menu-sub.svelte +25 -0
- package/src/shadcn/sidebar/sidebar-menu.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-provider.svelte +46 -0
- package/src/shadcn/sidebar/sidebar-rail.svelte +36 -0
- package/src/shadcn/sidebar/sidebar-separator.svelte +15 -0
- package/src/shadcn/sidebar/sidebar-trigger.svelte +35 -0
- package/src/shadcn/sidebar/sidebar.svelte +94 -0
- package/src/shadcn/skeleton/index.ts +7 -0
- package/src/shadcn/skeleton/skeleton.svelte +17 -0
- package/src/shadcn/slider/index.ts +7 -0
- package/src/shadcn/slider/slider.svelte +44 -0
- package/src/shadcn/sonner/index.ts +1 -0
- package/src/shadcn/sonner/sonner.svelte +13 -0
- package/src/shadcn/switch/index.ts +7 -0
- package/src/shadcn/switch/switch.svelte +27 -0
- package/src/shadcn/table/index.ts +28 -0
- package/src/shadcn/table/table-body.svelte +15 -0
- package/src/shadcn/table/table-caption.svelte +20 -0
- package/src/shadcn/table/table-cell.svelte +20 -0
- package/src/shadcn/table/table-footer.svelte +20 -0
- package/src/shadcn/table/table-head.svelte +23 -0
- package/src/shadcn/table/table-header.svelte +15 -0
- package/src/shadcn/table/table-row.svelte +23 -0
- package/src/shadcn/table/table.svelte +17 -0
- package/src/shadcn/tabs/index.ts +18 -0
- package/src/shadcn/tabs/tabs-content.svelte +21 -0
- package/src/shadcn/tabs/tabs-list.svelte +19 -0
- package/src/shadcn/tabs/tabs-trigger.svelte +21 -0
- package/src/shadcn/textarea/index.ts +7 -0
- package/src/shadcn/textarea/textarea.svelte +22 -0
- package/src/shadcn/toggle/index.ts +13 -0
- package/src/shadcn/toggle/toggle.svelte +51 -0
- package/src/shadcn/toggle-group/index.ts +10 -0
- package/src/shadcn/toggle-group/toggle-group-item.svelte +30 -0
- package/src/shadcn/toggle-group/toggle-group.svelte +41 -0
- package/src/shadcn/tooltip/index.ts +21 -0
- package/src/shadcn/tooltip/tooltip-content.svelte +47 -0
- package/src/shadcn/tooltip/tooltip-trigger.svelte +7 -0
- package/src/shadcn/utils.ts +14 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
import Check from '$icons/lucide/check';
|
|
3
|
+
import ChevronsUpDown from '$icons/lucide/chevrons-up-down';
|
|
4
|
+
import X from '$icons/lucide/x';
|
|
5
|
+
import { Button } from '../../shadcn/button';
|
|
6
|
+
import * as Command from '../../shadcn/command';
|
|
7
|
+
import * as Popover from '../../shadcn/popover';
|
|
8
|
+
import { cn } from '../../shadcn/utils';
|
|
9
|
+
import indefinite from 'indefinite';
|
|
10
|
+
import plur from 'plur';
|
|
11
|
+
import { tick } from 'svelte';
|
|
12
|
+
import type { SimpleDropdownMapping } from './simple-dropdown-types';
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
value = $bindable(),
|
|
16
|
+
mapping,
|
|
17
|
+
options,
|
|
18
|
+
inputPlaceholder,
|
|
19
|
+
searchPlaceholder,
|
|
20
|
+
optionTypeName = 'option',
|
|
21
|
+
// We will figure out the plural form of the data type name using the plur library if not provided
|
|
22
|
+
optionTypeNamePlural,
|
|
23
|
+
onValueChanged,
|
|
24
|
+
wrapperClasses = '',
|
|
25
|
+
buttonClasses = '',
|
|
26
|
+
popupWidthClasses = '',
|
|
27
|
+
loading = false,
|
|
28
|
+
showValueInListEntries = false,
|
|
29
|
+
disabled = false,
|
|
30
|
+
allowArbitraryValues,
|
|
31
|
+
dontShowSearchInput = false,
|
|
32
|
+
allowClear = false,
|
|
33
|
+
multiSelect = false
|
|
34
|
+
}: {
|
|
35
|
+
value: T | undefined | T[];
|
|
36
|
+
mapping: SimpleDropdownMapping<T>;
|
|
37
|
+
options: T[] | undefined;
|
|
38
|
+
inputPlaceholder?: string;
|
|
39
|
+
searchPlaceholder?: string;
|
|
40
|
+
onValueChanged?: (value: T | undefined | T[]) => void;
|
|
41
|
+
wrapperClasses?: string;
|
|
42
|
+
buttonClasses?: string;
|
|
43
|
+
// This is the name of the type of data in the dropdown that a user will understand
|
|
44
|
+
optionTypeName?: string;
|
|
45
|
+
optionTypeNamePlural?: string;
|
|
46
|
+
loading?: boolean;
|
|
47
|
+
showValueInListEntries?: boolean;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
dontShowSearchInput?: boolean;
|
|
50
|
+
popupWidthClasses?: string;
|
|
51
|
+
allowClear?: boolean;
|
|
52
|
+
multiSelect?: boolean;
|
|
53
|
+
allowArbitraryValues?: {
|
|
54
|
+
convertValueToType: (arbitraryValue: string) => T;
|
|
55
|
+
};
|
|
56
|
+
} = $props();
|
|
57
|
+
|
|
58
|
+
$effect(() => {
|
|
59
|
+
// Throw an exception if there is an options array and if all values are not unique
|
|
60
|
+
if (options && options.length > 0 && new Set(options.map((opt) => getValue(opt))).size !== options.length) {
|
|
61
|
+
throw new Error(`All values in the options (returned from your mappings.getValue fn) array must be unique: ${JSON.stringify(options)}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate that value is compatible with multiSelect mode
|
|
65
|
+
if (multiSelect && value !== undefined && !Array.isArray(value)) {
|
|
66
|
+
throw new Error(`When multiSelect is true, value must be undefined or an array. Received: ${typeof value}`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const getValue = (item: T) => mapping.value(item);
|
|
71
|
+
const getLabel = (item: T) => mapping.label(item);
|
|
72
|
+
const getSecondaryLabel = (item: T) => mapping.secondaryLabel?.(item);
|
|
73
|
+
|
|
74
|
+
// Helper functions for multi-select
|
|
75
|
+
const selectedValues = $derived(multiSelect ? (value as T[]) || [] : value ? [value as T] : []);
|
|
76
|
+
|
|
77
|
+
const isSelected = (option: T) => {
|
|
78
|
+
return selectedValues.some((selected) => getValue(selected) === getValue(option));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const toggleSelection = (option: T) => {
|
|
82
|
+
if (!multiSelect) {
|
|
83
|
+
// Single select mode
|
|
84
|
+
const optionValue = getValue(option);
|
|
85
|
+
if (!value || getValue(value as T) !== optionValue) {
|
|
86
|
+
value = option;
|
|
87
|
+
if (onValueChanged) onValueChanged(value);
|
|
88
|
+
}
|
|
89
|
+
closeAndFocusTrigger();
|
|
90
|
+
} else {
|
|
91
|
+
// Multi-select mode
|
|
92
|
+
const currentSelected = (value as T[]) || [];
|
|
93
|
+
const optionValue = getValue(option);
|
|
94
|
+
const isCurrentlySelected = currentSelected.some((selected) => getValue(selected) === optionValue);
|
|
95
|
+
|
|
96
|
+
if (isCurrentlySelected) {
|
|
97
|
+
// Remove from selection
|
|
98
|
+
const newSelection = currentSelected.filter((selected) => getValue(selected) !== optionValue);
|
|
99
|
+
value = newSelection;
|
|
100
|
+
} else {
|
|
101
|
+
// Add to selection
|
|
102
|
+
const newSelection = [...currentSelected, option];
|
|
103
|
+
value = newSelection;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (onValueChanged) onValueChanged(value);
|
|
107
|
+
// Don't close popup in multi-select mode
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const plurarFormOfOptionTypeName = $derived(optionTypeNamePlural ?? plur(optionTypeName));
|
|
112
|
+
const optionTypeNamePrecededByArticle = $derived(indefinite(optionTypeName));
|
|
113
|
+
const selectAnOptionText = $derived(`Select ${optionTypeNamePrecededByArticle}...`);
|
|
114
|
+
let open = $state(false);
|
|
115
|
+
let triggerRef = $state<HTMLButtonElement>(null!);
|
|
116
|
+
let searchValue = $state('');
|
|
117
|
+
|
|
118
|
+
const labelToDisplayInButton = $derived.by(() => {
|
|
119
|
+
if (multiSelect) {
|
|
120
|
+
const selected = (value as T[]) || [];
|
|
121
|
+
if (selected.length === 0) {
|
|
122
|
+
return inputPlaceholder ?? `Select ${plurarFormOfOptionTypeName}...`;
|
|
123
|
+
} else {
|
|
124
|
+
return selected.map((item) => getLabel(item)).join(', ');
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
if (!value) return inputPlaceholder ?? selectAnOptionText;
|
|
128
|
+
return getLabel(value as T);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Add the current value to the options if it's not already in the options
|
|
133
|
+
const normalizedOptions = $derived(options ? [...options] : []);
|
|
134
|
+
|
|
135
|
+
// Show all options when dropdown opens, optionally filter by search text
|
|
136
|
+
const visibleOptions = $derived.by(() => {
|
|
137
|
+
if (loading) {
|
|
138
|
+
return []; // Don't show any options if we are loading
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!searchValue.trim()) {
|
|
142
|
+
return normalizedOptions; // Show all when no search text
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Filter options by search text for better UX
|
|
146
|
+
return normalizedOptions.filter((option) => getLabel(option).toLowerCase().includes(searchValue.toLowerCase()));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// We want to refocus the trigger button when the user selects
|
|
150
|
+
// an item from the list so users can continue navigating the
|
|
151
|
+
// rest of the form with the keyboard.
|
|
152
|
+
function closeAndFocusTrigger() {
|
|
153
|
+
open = false;
|
|
154
|
+
tick().then(() => {
|
|
155
|
+
triggerRef.focus();
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Handle input changes for arbitrary values
|
|
160
|
+
function handleInputChange(e: Event) {
|
|
161
|
+
const inputValue = (e.target as HTMLInputElement).value;
|
|
162
|
+
|
|
163
|
+
if (allowArbitraryValues && !multiSelect) {
|
|
164
|
+
// Convert the string to type T using the user's conversion function
|
|
165
|
+
const convertedValue = allowArbitraryValues.convertValueToType(inputValue);
|
|
166
|
+
value = convertedValue;
|
|
167
|
+
if (onValueChanged) onValueChanged(value);
|
|
168
|
+
}
|
|
169
|
+
// If arbitrary values not allowed, searchValue is just for filtering
|
|
170
|
+
// Note: Arbitrary values with multi-select mode not supported yet
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle Enter key for arbitrary values
|
|
174
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
175
|
+
if (e.key === 'Enter' && allowArbitraryValues && searchValue.trim() && !multiSelect) {
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
// Convert the arbitrary value and set it
|
|
178
|
+
const convertedValue = allowArbitraryValues.convertValueToType(searchValue.trim());
|
|
179
|
+
value = convertedValue;
|
|
180
|
+
if (onValueChanged) onValueChanged(value);
|
|
181
|
+
searchValue = ''; // Clear the search input
|
|
182
|
+
closeAndFocusTrigger();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Handle clear button click
|
|
187
|
+
function handleClear(e: Event) {
|
|
188
|
+
e.stopPropagation();
|
|
189
|
+
value = undefined;
|
|
190
|
+
if (onValueChanged) onValueChanged(value);
|
|
191
|
+
}
|
|
192
|
+
</script>
|
|
193
|
+
|
|
194
|
+
<div class="flex items-center ${wrapperClasses} gap-2">
|
|
195
|
+
<Popover.Root bind:open>
|
|
196
|
+
<Popover.Trigger bind:ref={triggerRef} class="flex-1">
|
|
197
|
+
{#snippet child({ props })}
|
|
198
|
+
<Button variant="outline" {...props} class={`flex items-center justify-between w-full ${buttonClasses}`} role="combobox" aria-expanded={open} {disabled}>
|
|
199
|
+
<span class="flex-1 flex items-center min-w-0">
|
|
200
|
+
<span class={cn('text-left truncate min-w-0 flex-1 w-0', !value && 'text-muted-foreground')}>{labelToDisplayInButton}</span>
|
|
201
|
+
{#if showValueInListEntries && value && !multiSelect}
|
|
202
|
+
<span class="text-xs flex min-w-0 text-muted-foreground/70 font-mono truncate ml-1">
|
|
203
|
+
({getValue(value as T)})
|
|
204
|
+
</span>
|
|
205
|
+
{/if}
|
|
206
|
+
</span>
|
|
207
|
+
|
|
208
|
+
<ChevronsUpDown class="ml-2 shrink-0 opacity-50" />
|
|
209
|
+
</Button>
|
|
210
|
+
{/snippet}
|
|
211
|
+
</Popover.Trigger>
|
|
212
|
+
<Popover.Content class={cn('p-0', popupWidthClasses)}>
|
|
213
|
+
<Command.Root shouldFilter={false} class="">
|
|
214
|
+
{#if !dontShowSearchInput}
|
|
215
|
+
<Command.Input
|
|
216
|
+
bind:value={searchValue}
|
|
217
|
+
oninput={handleInputChange}
|
|
218
|
+
onkeydown={handleKeyDown}
|
|
219
|
+
placeholder={searchPlaceholder ?? `Search ${plurarFormOfOptionTypeName}...`}
|
|
220
|
+
class="h-9"
|
|
221
|
+
/>
|
|
222
|
+
{/if}
|
|
223
|
+
<Command.List>
|
|
224
|
+
{#if loading}
|
|
225
|
+
<Command.Loading>
|
|
226
|
+
<div class="flex items-center justify-center py-6 text-sm text-muted-foreground">
|
|
227
|
+
<div class="flex items-center gap-2">
|
|
228
|
+
<div class="h-4 w-4 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent"></div>
|
|
229
|
+
Loading {plurarFormOfOptionTypeName}...
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</Command.Loading>
|
|
233
|
+
{:else}
|
|
234
|
+
<Command.Empty>
|
|
235
|
+
{#if allowArbitraryValues && searchValue.trim()}
|
|
236
|
+
Press Enter to use "{searchValue}"
|
|
237
|
+
{:else}
|
|
238
|
+
No {optionTypeName} found.
|
|
239
|
+
{/if}
|
|
240
|
+
</Command.Empty>
|
|
241
|
+
{/if}
|
|
242
|
+
<Command.Group value={plurarFormOfOptionTypeName}>
|
|
243
|
+
{#key visibleOptions}
|
|
244
|
+
{#each visibleOptions as option (getValue(option))}
|
|
245
|
+
<Command.Item
|
|
246
|
+
value={getValue(option)}
|
|
247
|
+
onSelect={() => {
|
|
248
|
+
toggleSelection(option);
|
|
249
|
+
}}
|
|
250
|
+
class={cn('flex items-start gap-2 px-2 py-2', (getSecondaryLabel(option) || showValueInListEntries) && 'py-2.5 min-h-[3rem]')}
|
|
251
|
+
>
|
|
252
|
+
<Check class={cn('mt-1 flex-shrink-0', !isSelected(option) && 'text-transparent')} />
|
|
253
|
+
<div class="flex-1 min-w-0">
|
|
254
|
+
<!-- Primary label -->
|
|
255
|
+
<div class="font-medium text-sm leading-tight truncate">
|
|
256
|
+
{getLabel(option)}
|
|
257
|
+
{#if showValueInListEntries}
|
|
258
|
+
<span class="text-xs text-muted-foreground/70 font-mono truncate ml-1">
|
|
259
|
+
({getValue(option)})
|
|
260
|
+
</span>
|
|
261
|
+
{/if}
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<!-- Secondary and tertiary info in a row -->
|
|
265
|
+
{#if getSecondaryLabel(option)}
|
|
266
|
+
<div class="flex items-center gap-2 mt-0.5">
|
|
267
|
+
{#if getSecondaryLabel(option)}
|
|
268
|
+
<span class="text-xs text-muted-foreground">
|
|
269
|
+
{getSecondaryLabel(option)}
|
|
270
|
+
</span>
|
|
271
|
+
{/if}
|
|
272
|
+
</div>
|
|
273
|
+
{/if}
|
|
274
|
+
</div>
|
|
275
|
+
</Command.Item>
|
|
276
|
+
{/each}
|
|
277
|
+
{/key}
|
|
278
|
+
</Command.Group>
|
|
279
|
+
</Command.List>
|
|
280
|
+
</Command.Root>
|
|
281
|
+
</Popover.Content>
|
|
282
|
+
</Popover.Root>
|
|
283
|
+
{#if allowClear && !disabled}
|
|
284
|
+
<Button variant="ghost" size="icon" class="h-4 w-4 text-muted-foreground " onclick={handleClear}>
|
|
285
|
+
<X />
|
|
286
|
+
</Button>
|
|
287
|
+
{/if}
|
|
288
|
+
</div>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { IsMobile } from '../../shadcn/is-mobile.svelte.js';
|
|
2
|
+
import { getContext, setContext } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Getter<T> = () => T;
|
|
5
|
+
|
|
6
|
+
export type SlideoutStateProps = {
|
|
7
|
+
/**
|
|
8
|
+
* A getter function that returns the current open state of the slideout.
|
|
9
|
+
* We use a getter function here to support `bind:open` on the provider component.
|
|
10
|
+
*/
|
|
11
|
+
open: Getter<boolean>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A function that sets the open state of the slideout.
|
|
15
|
+
*/
|
|
16
|
+
setOpen: (open: boolean) => void;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The side that the slideout appears on
|
|
20
|
+
*/
|
|
21
|
+
side?: 'left' | 'right';
|
|
22
|
+
|
|
23
|
+
initialWidth?: number; // Allow setting initial width
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class SlideoutState {
|
|
27
|
+
readonly props: SlideoutStateProps;
|
|
28
|
+
open = $derived.by(() => this.props.open());
|
|
29
|
+
openMobile = $state(false);
|
|
30
|
+
setOpen: SlideoutStateProps['setOpen'];
|
|
31
|
+
side = $state<'left' | 'right'>('right');
|
|
32
|
+
panelWidth = $state(320);
|
|
33
|
+
isDragging = $state(false);
|
|
34
|
+
#isMobile: IsMobile;
|
|
35
|
+
state = $derived.by(() => (this.open ? 'expanded' : 'collapsed'));
|
|
36
|
+
widthBeforeMaximized: number | undefined = $state(undefined);
|
|
37
|
+
isMaximized = $state(false);
|
|
38
|
+
isAnimating = $state(false);
|
|
39
|
+
|
|
40
|
+
constructor(props: SlideoutStateProps) {
|
|
41
|
+
this.setOpen = props.setOpen;
|
|
42
|
+
this.#isMobile = new IsMobile();
|
|
43
|
+
this.props = props;
|
|
44
|
+
this.side = props.side || 'right';
|
|
45
|
+
this.panelWidth = props.initialWidth || 320;
|
|
46
|
+
|
|
47
|
+
$effect(() => {
|
|
48
|
+
this.openMobile = this.open;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Convenience getter for checking if mobile
|
|
53
|
+
get isMobile() {
|
|
54
|
+
return this.#isMobile.current;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
toggleMaximize = () => {
|
|
58
|
+
if (this.isMobile) return;
|
|
59
|
+
|
|
60
|
+
this.isMaximized = !this.isMaximized;
|
|
61
|
+
if (this.isMaximized) {
|
|
62
|
+
this.widthBeforeMaximized = this.panelWidth;
|
|
63
|
+
} else {
|
|
64
|
+
this.panelWidth = this.widthBeforeMaximized || 320;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
setPanelWidth = (width: number) => {
|
|
69
|
+
// Apply constraints
|
|
70
|
+
this.panelWidth = Math.max(200, Math.min(window.innerWidth * 0.8, width)); // Example: max 80% viewport
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
setIsDragging = (dragging: boolean) => {
|
|
74
|
+
this.isDragging = dragging;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Event handler for keyboard shortcuts
|
|
78
|
+
handleShortcutKeydown = (e: KeyboardEvent) => {
|
|
79
|
+
// Add keyboard shortcut handling here if needed
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
setOpenMobile = (value: boolean) => {
|
|
83
|
+
this.openMobile = value;
|
|
84
|
+
this.setOpen(value);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
toggle = () => {
|
|
88
|
+
return this.#isMobile.current ? (this.openMobile = !this.openMobile) : this.setOpen(!this.open);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const SYMBOL_KEY = 'scn-slideout';
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Instantiates a new `SlideoutState` instance and sets it in the context.
|
|
96
|
+
*
|
|
97
|
+
* @param props The constructor props for the `SlideoutState` class.
|
|
98
|
+
* @returns The `SlideoutState` instance.
|
|
99
|
+
*/
|
|
100
|
+
export function setSlideout(props: SlideoutStateProps): SlideoutState {
|
|
101
|
+
return setContext(Symbol.for(SYMBOL_KEY), new SlideoutState(props));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Retrieves the `SlideoutState` instance from the context.
|
|
106
|
+
* @returns The `SlideoutState` instance.
|
|
107
|
+
*/
|
|
108
|
+
export function useSlideout(): SlideoutState {
|
|
109
|
+
return getContext(Symbol.for(SYMBOL_KEY));
|
|
110
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useSlideout } from './context.svelte.js';
|
|
2
|
+
import Content from './slideout-content.svelte';
|
|
3
|
+
import Panel from './slideout-panel.svelte';
|
|
4
|
+
import Provider from './slideout-provider.svelte';
|
|
5
|
+
import Root from './slideout.svelte';
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
Content,
|
|
9
|
+
Panel,
|
|
10
|
+
Provider,
|
|
11
|
+
Root,
|
|
12
|
+
// Aliased exports
|
|
13
|
+
Root as Slideout,
|
|
14
|
+
Content as SlideoutContent,
|
|
15
|
+
Panel as SlideoutPanel,
|
|
16
|
+
Provider as SlideoutProvider,
|
|
17
|
+
// Hooks
|
|
18
|
+
useSlideout
|
|
19
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../shadcn/utils.js';
|
|
3
|
+
import type { WithElementRef } from 'bits-ui';
|
|
4
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
5
|
+
import { useSlideout } from './context.svelte.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
ref = $bindable(null),
|
|
9
|
+
class: className,
|
|
10
|
+
children,
|
|
11
|
+
...restProps
|
|
12
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
13
|
+
|
|
14
|
+
const slideout = useSlideout();
|
|
15
|
+
const isPanelOpen = $derived(slideout.open || (slideout.isMobile && slideout.openMobile));
|
|
16
|
+
|
|
17
|
+
// add max-hieght: 100vh (max-h-screen)
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div
|
|
21
|
+
bind:this={ref}
|
|
22
|
+
data-slideout="content"
|
|
23
|
+
class={cn(
|
|
24
|
+
// 'flex flex-col h-full flex-1 overflow-auto transition-all duration-300 ease-in-out',
|
|
25
|
+
'max-h-screen flex flex-col h-full flex-1 overflow-auto transition-all duration-300 ease-in-out min-w-0', // Added min-w-0
|
|
26
|
+
// slideout.isMobile && 'data-[panel-open=true]:hidden',
|
|
27
|
+
// You might hide it explicitly on mobile, or let flexbox handle it (panel width 100%)
|
|
28
|
+
slideout.isMobile && isPanelOpen && 'hidden', // Keep explicit hide for mobile clarity
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
data-panel-open={isPanelOpen}
|
|
32
|
+
{...restProps}
|
|
33
|
+
>
|
|
34
|
+
{@render children?.()}
|
|
35
|
+
</div>
|
|
36
|
+
<!-- data-panel-open={slideout.open || (slideout.isMobile && slideout.openMobile)} -->
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../shadcn/utils.js';
|
|
3
|
+
import type { WithElementRef } from 'bits-ui';
|
|
4
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
5
|
+
import { useSlideout } from './context.svelte.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
ref = $bindable(null),
|
|
9
|
+
class: className,
|
|
10
|
+
children,
|
|
11
|
+
...restProps
|
|
12
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
13
|
+
|
|
14
|
+
// Get the slideout context
|
|
15
|
+
const slideout = useSlideout();
|
|
16
|
+
|
|
17
|
+
// State for drag operations
|
|
18
|
+
let startX = $state(0);
|
|
19
|
+
let startWidth = $state(slideout.panelWidth);
|
|
20
|
+
|
|
21
|
+
// Reference to the rail element
|
|
22
|
+
let railRef = $state<HTMLButtonElement | null>(null);
|
|
23
|
+
|
|
24
|
+
// Handle drag start
|
|
25
|
+
function handleDragStart(e: MouseEvent) {
|
|
26
|
+
slideout.setIsDragging(true);
|
|
27
|
+
startX = e.clientX;
|
|
28
|
+
startWidth = slideout.panelWidth;
|
|
29
|
+
|
|
30
|
+
// Add event listeners for dragging
|
|
31
|
+
window.addEventListener('mousemove', handleDragMove);
|
|
32
|
+
window.addEventListener('mouseup', handleDragEnd);
|
|
33
|
+
|
|
34
|
+
// Prevent text selection while dragging
|
|
35
|
+
document.body.style.userSelect = 'none';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Handle drag move
|
|
39
|
+
function handleDragMove(e: MouseEvent) {
|
|
40
|
+
if (!slideout.isDragging) return;
|
|
41
|
+
|
|
42
|
+
const dx = e.clientX - startX;
|
|
43
|
+
const newWidth = slideout.side === 'right' ? startWidth - dx : startWidth + dx;
|
|
44
|
+
|
|
45
|
+
// Limit width between reasonable values
|
|
46
|
+
// currentWidth = Math.max(200, Math.min(600, newWidth));
|
|
47
|
+
slideout.setPanelWidth(newWidth);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle drag end
|
|
51
|
+
function handleDragEnd() {
|
|
52
|
+
slideout.setIsDragging(false);
|
|
53
|
+
|
|
54
|
+
// Remove event listeners
|
|
55
|
+
window.removeEventListener('mousemove', handleDragMove);
|
|
56
|
+
window.removeEventListener('mouseup', handleDragEnd);
|
|
57
|
+
|
|
58
|
+
// Restore text selection
|
|
59
|
+
document.body.style.userSelect = '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Cleanup on component destruction
|
|
63
|
+
function cleanup() {
|
|
64
|
+
window.removeEventListener('mousemove', handleDragMove);
|
|
65
|
+
window.removeEventListener('mouseup', handleDragEnd);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const effectiveWidth = $derived.by(() => {
|
|
69
|
+
const isOpen = slideout.open || (slideout.isMobile && slideout.openMobile);
|
|
70
|
+
if (!isOpen) {
|
|
71
|
+
return '0px'; // Use 0 width when closed
|
|
72
|
+
}
|
|
73
|
+
if (slideout.isMobile || slideout.isMaximized) {
|
|
74
|
+
return '100%'; // Full width on mobile when open or when maximized
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return `${slideout.panelWidth}px`; // Use context width on desktop when open
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// add h-screen
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<svelte:window on:beforeunload={cleanup} />
|
|
84
|
+
<div
|
|
85
|
+
bind:this={ref}
|
|
86
|
+
data-slideout="panel"
|
|
87
|
+
data-side={slideout.side}
|
|
88
|
+
data-state={slideout.state}
|
|
89
|
+
style="width: {effectiveWidth}"
|
|
90
|
+
ontransitionstart={() => (slideout.isAnimating = true)}
|
|
91
|
+
ontransitionend={() => (slideout.isAnimating = false)}
|
|
92
|
+
ontransitioncancel={() => (slideout.isAnimating = false)}
|
|
93
|
+
class={cn(
|
|
94
|
+
'h-screen flex-shrink-0 overflow-auto relative flex flex-col',
|
|
95
|
+
'transition-all duration-300 ease-in-out',
|
|
96
|
+
// Hide rail when collapsed or on mobile or when maximied unless specified otherwise
|
|
97
|
+
(slideout.state === 'collapsed' || slideout.isMobile || slideout.isMaximized) &&
|
|
98
|
+
'[&>[data-slideout=rail]]:hidden',
|
|
99
|
+
className
|
|
100
|
+
)}
|
|
101
|
+
{...restProps}
|
|
102
|
+
>
|
|
103
|
+
<!-- Panel content -->
|
|
104
|
+
<div class="flex-1 overflow-auto min-w-0 h-full">
|
|
105
|
+
{@render children?.()}
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<!-- Rail/handle for resizing - integrated directly into panel -->
|
|
109
|
+
<button
|
|
110
|
+
bind:this={railRef}
|
|
111
|
+
data-slideout="rail"
|
|
112
|
+
aria-label="Resize Slideout"
|
|
113
|
+
tabIndex={-1}
|
|
114
|
+
onmousedown={handleDragStart}
|
|
115
|
+
title="Resize Slideout"
|
|
116
|
+
class={cn(
|
|
117
|
+
'after:bg-sidebar-accent hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
|
|
118
|
+
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
|
|
119
|
+
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
|
120
|
+
'group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
|
|
121
|
+
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2 [[data-side=right][data-collapsible=offcanvas]_&]:-left-2'
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
<!-- Optional: Add visual indicator for the rail for drag drop resize -->
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../shadcn/utils.js';
|
|
3
|
+
import type { WithElementRef } from 'bits-ui';
|
|
4
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
5
|
+
import { SLIDEOUT_DEFAULT_WIDTH, SLIDEOUT_WIDTH, SLIDEOUT_WIDTH_ICON } from './constants.js';
|
|
6
|
+
import { setSlideout } from './context.svelte.js';
|
|
7
|
+
|
|
8
|
+
interface Props extends WithElementRef<HTMLAttributes<HTMLDivElement>> {
|
|
9
|
+
open?: boolean;
|
|
10
|
+
onOpenChange?: (open: boolean) => void;
|
|
11
|
+
side?: 'left' | 'right';
|
|
12
|
+
initialWidth?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
ref = $bindable(null),
|
|
17
|
+
open = $bindable(false),
|
|
18
|
+
onOpenChange = () => {},
|
|
19
|
+
side = 'right',
|
|
20
|
+
initialWidth = SLIDEOUT_DEFAULT_WIDTH,
|
|
21
|
+
class: className,
|
|
22
|
+
style,
|
|
23
|
+
children,
|
|
24
|
+
...restProps
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
|
|
27
|
+
const slideout = setSlideout({
|
|
28
|
+
open: () => open,
|
|
29
|
+
setOpen: (value: boolean) => {
|
|
30
|
+
open = value;
|
|
31
|
+
onOpenChange(value);
|
|
32
|
+
},
|
|
33
|
+
side,
|
|
34
|
+
initialWidth,
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<svelte:window onkeydown={slideout.handleShortcutKeydown} />
|
|
39
|
+
|
|
40
|
+
<div
|
|
41
|
+
style="--slideout-width: {SLIDEOUT_WIDTH}; --slideout-width-icon: {SLIDEOUT_WIDTH_ICON}; {style}"
|
|
42
|
+
class={cn('group/slideout-wrapper flex h-full w-full overflow-hidden', className)}
|
|
43
|
+
data-side={side}
|
|
44
|
+
data-state={slideout.state}
|
|
45
|
+
bind:this={ref}
|
|
46
|
+
{...restProps}
|
|
47
|
+
>
|
|
48
|
+
{@render children?.()}
|
|
49
|
+
</div>
|