@adecore/ui 0.14.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 -0
- package/README.md +83 -0
- package/dist/AccentSwatches.d.ts +15 -0
- package/dist/AccentSwatches.js +21 -0
- package/dist/Banner.d.ts +12 -0
- package/dist/Banner.js +18 -0
- package/dist/Button.d.ts +9 -0
- package/dist/Button.js +28 -0
- package/dist/ButtonGroup.d.ts +3 -0
- package/dist/ButtonGroup.js +11 -0
- package/dist/Checkbox.d.ts +11 -0
- package/dist/Checkbox.js +9 -0
- package/dist/ChoiceCards.d.ts +23 -0
- package/dist/ChoiceCards.js +21 -0
- package/dist/CloseButton.d.ts +15 -0
- package/dist/CloseButton.js +11 -0
- package/dist/ColorSwatch.d.ts +14 -0
- package/dist/ColorSwatch.js +28 -0
- package/dist/ColumnResizeHandle.d.ts +9 -0
- package/dist/ColumnResizeHandle.js +12 -0
- package/dist/DisabledReason.d.ts +6 -0
- package/dist/DisabledReason.js +8 -0
- package/dist/DockShell.d.ts +9 -0
- package/dist/DockShell.js +49 -0
- package/dist/EmptyState.d.ts +16 -0
- package/dist/EmptyState.js +11 -0
- package/dist/ErrorBoundary.d.ts +24 -0
- package/dist/ErrorBoundary.js +47 -0
- package/dist/Field.d.ts +17 -0
- package/dist/Field.js +48 -0
- package/dist/FileIcon.d.ts +8 -0
- package/dist/FileIcon.js +15 -0
- package/dist/Icon.d.ts +9 -0
- package/dist/Icon.js +12 -0
- package/dist/IconButton.d.ts +23 -0
- package/dist/IconButton.js +25 -0
- package/dist/IconPicker.d.ts +17 -0
- package/dist/IconPicker.js +93 -0
- package/dist/Input.d.ts +14 -0
- package/dist/Input.js +25 -0
- package/dist/Kbd.d.ts +23 -0
- package/dist/Kbd.js +20 -0
- package/dist/ListRow.d.ts +10 -0
- package/dist/ListRow.js +20 -0
- package/dist/Meter.d.ts +12 -0
- package/dist/Meter.js +7 -0
- package/dist/PanelEmpty.d.ts +16 -0
- package/dist/PanelEmpty.js +7 -0
- package/dist/PanelHeader.d.ts +6 -0
- package/dist/PanelHeader.js +20 -0
- package/dist/Pill.d.ts +27 -0
- package/dist/Pill.js +25 -0
- package/dist/ProjectSwitcher.d.ts +30 -0
- package/dist/ProjectSwitcher.js +18 -0
- package/dist/PromptDialog.d.ts +40 -0
- package/dist/PromptDialog.js +47 -0
- package/dist/SectionLabel.d.ts +3 -0
- package/dist/SectionLabel.js +16 -0
- package/dist/SegmentBar.d.ts +18 -0
- package/dist/SegmentBar.js +44 -0
- package/dist/Segmented.d.ts +17 -0
- package/dist/Segmented.js +7 -0
- package/dist/Select.d.ts +27 -0
- package/dist/Select.js +34 -0
- package/dist/Separator.d.ts +7 -0
- package/dist/Separator.js +8 -0
- package/dist/ShortcutHints.d.ts +1 -0
- package/dist/ShortcutHints.js +94 -0
- package/dist/Skeleton.d.ts +6 -0
- package/dist/Skeleton.js +6 -0
- package/dist/SlidingColumn.d.ts +19 -0
- package/dist/SlidingColumn.js +41 -0
- package/dist/Spinner.d.ts +6 -0
- package/dist/Spinner.js +7 -0
- package/dist/Stepper.d.ts +14 -0
- package/dist/Stepper.js +18 -0
- package/dist/Surface.d.ts +3 -0
- package/dist/Surface.js +16 -0
- package/dist/Switch.d.ts +10 -0
- package/dist/Switch.js +7 -0
- package/dist/TextMenu.d.ts +5 -0
- package/dist/TextMenu.js +31 -0
- package/dist/Tile.d.ts +11 -0
- package/dist/Tile.js +10 -0
- package/dist/Toasts.d.ts +9 -0
- package/dist/Toasts.js +58 -0
- package/dist/Tooltip.d.ts +16 -0
- package/dist/Tooltip.js +21 -0
- package/dist/UIProvider.d.ts +9 -0
- package/dist/UIProvider.js +26 -0
- package/dist/Waveform.d.ts +16 -0
- package/dist/Waveform.js +123 -0
- package/dist/Wipe.d.ts +12 -0
- package/dist/Wipe.js +47 -0
- package/dist/ZoomControls.d.ts +30 -0
- package/dist/ZoomControls.js +16 -0
- package/dist/class-name.d.ts +2 -0
- package/dist/class-name.js +3 -0
- package/dist/clipboard.d.ts +2 -0
- package/dist/clipboard.js +16 -0
- package/dist/context-menu/index.parts.d.ts +1 -0
- package/dist/context-menu/index.parts.js +2 -0
- package/dist/dialog/index.parts.d.ts +1 -0
- package/dist/dialog/index.parts.js +1 -0
- package/dist/dialog/parts.d.ts +36 -0
- package/dist/dialog/parts.js +55 -0
- package/dist/dialog-layer.d.ts +1 -0
- package/dist/dialog-layer.js +27 -0
- package/dist/error-boundary.d.ts +4 -0
- package/dist/error-boundary.js +34 -0
- package/dist/error-message.d.ts +1 -0
- package/dist/error-message.js +4 -0
- package/dist/field-context.d.ts +7 -0
- package/dist/field-context.js +4 -0
- package/dist/file-icon.d.ts +4 -0
- package/dist/file-icon.js +46 -0
- package/dist/floating.d.ts +5 -0
- package/dist/floating.js +6 -0
- package/dist/format/datetime.d.ts +11 -0
- package/dist/format/datetime.js +50 -0
- package/dist/format/duration.d.ts +5 -0
- package/dist/format/duration.js +76 -0
- package/dist/format/index.d.ts +6 -0
- package/dist/format/index.js +7 -0
- package/dist/format/locale.d.ts +17 -0
- package/dist/format/locale.js +105 -0
- package/dist/format/number.d.ts +8 -0
- package/dist/format/number.js +72 -0
- package/dist/format/regions.d.ts +6 -0
- package/dist/format/regions.js +15 -0
- package/dist/format/time-zone.d.ts +1 -0
- package/dist/format/time-zone.js +2 -0
- package/dist/icon-button-size.d.ts +3 -0
- package/dist/icon-button-size.js +3 -0
- package/dist/icon-picker.d.ts +23 -0
- package/dist/icon-picker.js +80 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +74 -0
- package/dist/lazy.d.ts +12 -0
- package/dist/lazy.js +104 -0
- package/dist/locales/en.json +59 -0
- package/dist/locales/nl.json +59 -0
- package/dist/locales.d.ts +4 -0
- package/dist/locales.js +17 -0
- package/dist/menu/index.parts.d.ts +1 -0
- package/dist/menu/index.parts.js +1 -0
- package/dist/menu/parts.d.ts +67 -0
- package/dist/menu/parts.js +113 -0
- package/dist/merge-refs.d.ts +2 -0
- package/dist/merge-refs.js +11 -0
- package/dist/modality.d.ts +1 -0
- package/dist/modality.js +29 -0
- package/dist/platform.d.ts +2 -0
- package/dist/platform.js +9 -0
- package/dist/popover/index.parts.d.ts +1 -0
- package/dist/popover/index.parts.js +1 -0
- package/dist/popover/parts.d.ts +26 -0
- package/dist/popover/parts.js +28 -0
- package/dist/prefetch.d.ts +21 -0
- package/dist/prefetch.js +99 -0
- package/dist/preview-card/index.parts.d.ts +1 -0
- package/dist/preview-card/index.parts.js +1 -0
- package/dist/project-switcher-row.d.ts +6 -0
- package/dist/project-switcher-row.js +16 -0
- package/dist/selection.d.ts +2 -0
- package/dist/selection.js +22 -0
- package/dist/seti-icons.d.ts +5 -0
- package/dist/seti-icons.js +445 -0
- package/dist/settings/ConfirmDialog.d.ts +9 -0
- package/dist/settings/ConfirmDialog.js +9 -0
- package/dist/settings/DetailHeader.d.ts +10 -0
- package/dist/settings/DetailHeader.js +6 -0
- package/dist/settings/MasterDetail.d.ts +24 -0
- package/dist/settings/MasterDetail.js +16 -0
- package/dist/settings/SettingsDialog.d.ts +46 -0
- package/dist/settings/SettingsDialog.js +96 -0
- package/dist/settings/SettingsRow.d.ts +22 -0
- package/dist/settings/SettingsRow.js +30 -0
- package/dist/settings/SettingsSection.d.ts +14 -0
- package/dist/settings/SettingsSection.js +8 -0
- package/dist/settings/index.d.ts +7 -0
- package/dist/settings/index.js +8 -0
- package/dist/settings/target.d.ts +7 -0
- package/dist/settings/target.js +3 -0
- package/dist/shortcut-hints.d.ts +38 -0
- package/dist/shortcut-hints.js +81 -0
- package/dist/shortcut.d.ts +32 -0
- package/dist/shortcut.js +137 -0
- package/dist/testing/dedupe.d.ts +1 -0
- package/dist/testing/dedupe.js +73 -0
- package/dist/testing/fake-source.d.ts +8 -0
- package/dist/testing/fake-source.js +15 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +2 -0
- package/dist/theme.css +562 -0
- package/dist/toast-store.d.ts +38 -0
- package/dist/toast-store.js +61 -0
- package/dist/useAsyncAction.d.ts +8 -0
- package/dist/useAsyncAction.js +23 -0
- package/dist/useColumnResize.d.ts +14 -0
- package/dist/useColumnResize.js +49 -0
- package/dist/useContentSize.d.ts +5 -0
- package/dist/useContentSize.js +21 -0
- package/dist/useMeasuredWidth.d.ts +1 -0
- package/dist/useMeasuredWidth.js +6 -0
- package/dist/useNow.d.ts +3 -0
- package/dist/useNow.js +41 -0
- package/dist/wipe-split.d.ts +7 -0
- package/dist/wipe-split.js +20 -0
- package/dist/zoom.d.ts +3 -0
- package/dist/zoom.js +6 -0
- package/package.json +92 -0
- package/src/AccentSwatches.tsx +73 -0
- package/src/Banner.tsx +43 -0
- package/src/Button.tsx +55 -0
- package/src/ButtonGroup.tsx +14 -0
- package/src/Checkbox.tsx +44 -0
- package/src/ChoiceCards.tsx +108 -0
- package/src/CloseButton.tsx +21 -0
- package/src/ColorSwatch.tsx +41 -0
- package/src/ColumnResizeHandle.tsx +23 -0
- package/src/DisabledReason.tsx +23 -0
- package/src/DockShell.tsx +76 -0
- package/src/EmptyState.tsx +48 -0
- package/src/ErrorBoundary.tsx +116 -0
- package/src/Field.tsx +116 -0
- package/src/FileIcon.tsx +27 -0
- package/src/Icon.tsx +22 -0
- package/src/IconButton.tsx +84 -0
- package/src/IconPicker.tsx +257 -0
- package/src/Input.tsx +78 -0
- package/src/Kbd.tsx +67 -0
- package/src/ListRow.tsx +26 -0
- package/src/Meter.tsx +41 -0
- package/src/PanelEmpty.tsx +37 -0
- package/src/PanelHeader.tsx +31 -0
- package/src/Pill.tsx +61 -0
- package/src/ProjectSwitcher.tsx +127 -0
- package/src/PromptDialog.tsx +168 -0
- package/src/SectionLabel.tsx +20 -0
- package/src/SegmentBar.tsx +128 -0
- package/src/Segmented.tsx +51 -0
- package/src/Select.tsx +135 -0
- package/src/Separator.tsx +15 -0
- package/src/ShortcutHints.tsx +118 -0
- package/src/Skeleton.tsx +13 -0
- package/src/SlidingColumn.tsx +91 -0
- package/src/Spinner.tsx +32 -0
- package/src/Stepper.tsx +67 -0
- package/src/Surface.tsx +20 -0
- package/src/Switch.tsx +32 -0
- package/src/TextMenu.tsx +59 -0
- package/src/Tile.tsx +49 -0
- package/src/Toasts.tsx +151 -0
- package/src/Tooltip.tsx +70 -0
- package/src/UIProvider.tsx +41 -0
- package/src/Waveform.tsx +199 -0
- package/src/Wipe.tsx +99 -0
- package/src/ZoomControls.tsx +99 -0
- package/src/class-name.ts +8 -0
- package/src/clipboard.ts +17 -0
- package/src/context-menu/index.parts.ts +21 -0
- package/src/dialog/index.parts.ts +10 -0
- package/src/dialog/parts.tsx +112 -0
- package/src/dialog-layer.ts +31 -0
- package/src/error-boundary.ts +40 -0
- package/src/error-message.ts +5 -0
- package/src/field-context.ts +12 -0
- package/src/file-icon.ts +52 -0
- package/src/floating.ts +9 -0
- package/src/format/datetime.ts +63 -0
- package/src/format/duration.ts +83 -0
- package/src/format/index.ts +20 -0
- package/src/format/locale.ts +153 -0
- package/src/format/number.ts +87 -0
- package/src/format/regions.ts +20 -0
- package/src/format/time-zone.ts +2 -0
- package/src/icon-button-size.ts +6 -0
- package/src/icon-picker.ts +112 -0
- package/src/index.ts +103 -0
- package/src/lazy.tsx +124 -0
- package/src/locales/en.json +59 -0
- package/src/locales/nl.json +59 -0
- package/src/locales.ts +21 -0
- package/src/menu/index.parts.ts +20 -0
- package/src/menu/parts.tsx +296 -0
- package/src/merge-refs.ts +14 -0
- package/src/modality.ts +37 -0
- package/src/platform.ts +10 -0
- package/src/popover/index.parts.ts +8 -0
- package/src/popover/parts.tsx +119 -0
- package/src/prefetch.ts +113 -0
- package/src/preview-card/index.parts.ts +1 -0
- package/src/project-switcher-row.tsx +51 -0
- package/src/selection.ts +24 -0
- package/src/seti-icons.ts +451 -0
- package/src/settings/ConfirmDialog.tsx +30 -0
- package/src/settings/DetailHeader.tsx +25 -0
- package/src/settings/MasterDetail.tsx +67 -0
- package/src/settings/SettingsDialog.tsx +342 -0
- package/src/settings/SettingsRow.tsx +89 -0
- package/src/settings/SettingsSection.tsx +46 -0
- package/src/settings/index.ts +16 -0
- package/src/settings/target.ts +12 -0
- package/src/shortcut-hints.ts +117 -0
- package/src/shortcut.ts +170 -0
- package/src/testing/dedupe.ts +93 -0
- package/src/testing/fake-source.ts +21 -0
- package/src/testing/index.ts +3 -0
- package/src/theme.css +562 -0
- package/src/toast-store.ts +120 -0
- package/src/useAsyncAction.ts +32 -0
- package/src/useColumnResize.ts +75 -0
- package/src/useContentSize.ts +27 -0
- package/src/useMeasuredWidth.ts +7 -0
- package/src/useNow.ts +49 -0
- package/src/wipe-split.ts +24 -0
- package/src/zoom.ts +9 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { createContext, useContext, type ComponentProps, type ReactNode, type Ref } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { Check, ChevronRight, type LucideIcon } from 'lucide-react';
|
|
4
|
+
import { ContextMenu as BaseContextMenu } from '@base-ui-components/react/context-menu';
|
|
5
|
+
import { Menu as BaseMenu } from '@base-ui-components/react/menu';
|
|
6
|
+
import { Separator as BaseSeparator } from '@base-ui-components/react/separator';
|
|
7
|
+
import { useRender } from '@base-ui-components/react/use-render';
|
|
8
|
+
import { withClass } from '../class-name.ts';
|
|
9
|
+
import { Icon } from '../Icon.tsx';
|
|
10
|
+
import { Tooltip } from '../Tooltip.tsx';
|
|
11
|
+
|
|
12
|
+
/* Where a popup opens from decides where it goes when a call site says nothing. */
|
|
13
|
+
type MenuKind = 'menu' | 'context' | 'submenu';
|
|
14
|
+
|
|
15
|
+
const MenuKindContext = createContext<MenuKind>('menu');
|
|
16
|
+
|
|
17
|
+
type PositionerProps = ComponentProps<typeof BaseMenu.Positioner>;
|
|
18
|
+
|
|
19
|
+
type Placement = Pick<
|
|
20
|
+
PositionerProps,
|
|
21
|
+
'side' | 'align' | 'sideOffset' | 'alignOffset' | 'collisionPadding' | 'collisionBoundary' | 'collisionAvoidance' | 'anchor' | 'sticky' | 'positionMethod'
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* A menu hangs under its trigger, lined up with its start, unless there is no room. A context menu
|
|
26
|
+
* opens at the pointer, which its positioner already knows. A submenu sits beside its row, pulled
|
|
27
|
+
* up by the popup's own padding so its first row lines up with the row that opened it.
|
|
28
|
+
*/
|
|
29
|
+
const PLACEMENT: Record<MenuKind, Placement> = {
|
|
30
|
+
menu: { side: 'bottom', align: 'start', sideOffset: 6 },
|
|
31
|
+
context: {},
|
|
32
|
+
submenu: { sideOffset: 4, alignOffset: -4 }
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/* The label above a group of rows. It takes the line height of the row it sits in. */
|
|
36
|
+
export const MENU_LABEL = 'px-2.5 pt-1.5 pb-0.5 text-xs/[inherit] text-text-faint';
|
|
37
|
+
|
|
38
|
+
/* A trailing hint in a row: what the item does to something else, never a shortcut. A shortcut stays a `Kbd`, which `.menu-item` already pushes to the right. */
|
|
39
|
+
const MENU_HINT = 'ml-auto pl-3 text-xs/[inherit] text-text-faint';
|
|
40
|
+
|
|
41
|
+
/* The hairline between two groups of rows. It runs the whole width of the popup, which is what the
|
|
42
|
+
negative margin buys back from its 4px of padding, and it is softer than a border a surface ends with. */
|
|
43
|
+
const MENU_SEPARATOR = '-mx-1 my-1 h-px bg-border-soft';
|
|
44
|
+
|
|
45
|
+
export function MenuRoot(props: ComponentProps<typeof BaseMenu.Root>) {
|
|
46
|
+
return (
|
|
47
|
+
<MenuKindContext value="menu">
|
|
48
|
+
<BaseMenu.Root {...props} />
|
|
49
|
+
</MenuKindContext>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function ContextMenuRoot(props: ComponentProps<typeof BaseContextMenu.Root>) {
|
|
54
|
+
return (
|
|
55
|
+
<MenuKindContext value="context">
|
|
56
|
+
<BaseContextMenu.Root {...props} />
|
|
57
|
+
</MenuKindContext>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function MenuSubmenuRoot(props: ComponentProps<typeof BaseMenu.SubmenuRoot>) {
|
|
62
|
+
return (
|
|
63
|
+
<MenuKindContext value="submenu">
|
|
64
|
+
<BaseMenu.SubmenuRoot {...props} />
|
|
65
|
+
</MenuKindContext>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const MenuTrigger = BaseMenu.Trigger;
|
|
70
|
+
|
|
71
|
+
export const ContextMenuTrigger = BaseContextMenu.Trigger;
|
|
72
|
+
|
|
73
|
+
export type MenuPopupProps = ComponentProps<typeof BaseMenu.Popup> & Placement;
|
|
74
|
+
|
|
75
|
+
/*
|
|
76
|
+
* The shell every menu is drawn in: the portal, the layer it sits on and the gap to its trigger are
|
|
77
|
+
* decided once. The popup scrolls inside the room the window leaves it; `className` adds a minimum
|
|
78
|
+
* width or a layout of its own, such as a grid of swatches.
|
|
79
|
+
*/
|
|
80
|
+
export function MenuPopup({
|
|
81
|
+
side,
|
|
82
|
+
align,
|
|
83
|
+
sideOffset,
|
|
84
|
+
alignOffset,
|
|
85
|
+
collisionPadding,
|
|
86
|
+
collisionBoundary,
|
|
87
|
+
collisionAvoidance,
|
|
88
|
+
anchor,
|
|
89
|
+
sticky,
|
|
90
|
+
positionMethod,
|
|
91
|
+
className,
|
|
92
|
+
...props
|
|
93
|
+
}: MenuPopupProps) {
|
|
94
|
+
const defaults = PLACEMENT[useContext(MenuKindContext)];
|
|
95
|
+
return (
|
|
96
|
+
<BaseMenu.Portal>
|
|
97
|
+
<BaseMenu.Positioner
|
|
98
|
+
className="popup-positioner"
|
|
99
|
+
side={side ?? defaults.side}
|
|
100
|
+
align={align ?? defaults.align}
|
|
101
|
+
sideOffset={sideOffset ?? defaults.sideOffset}
|
|
102
|
+
alignOffset={alignOffset ?? defaults.alignOffset}
|
|
103
|
+
collisionPadding={collisionPadding}
|
|
104
|
+
collisionBoundary={collisionBoundary}
|
|
105
|
+
collisionAvoidance={collisionAvoidance}
|
|
106
|
+
anchor={anchor}
|
|
107
|
+
sticky={sticky}
|
|
108
|
+
positionMethod={positionMethod}
|
|
109
|
+
>
|
|
110
|
+
<BaseMenu.Popup className={withClass('menu-popup', className)} {...props} />
|
|
111
|
+
</BaseMenu.Positioner>
|
|
112
|
+
</BaseMenu.Portal>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
type ItemLook = {
|
|
117
|
+
/*
|
|
118
|
+
* Leaves the row out: no padding, highlight or indicator, only what makes it an item (the arrow
|
|
119
|
+
* keys, typeahead, closing the menu). For an icon button, a swatch, a link or a segment inside a
|
|
120
|
+
* menu, which draws itself and its own highlight.
|
|
121
|
+
*/
|
|
122
|
+
unstyled?: boolean;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
type MenuItemProps = ComponentProps<typeof BaseMenu.Item> & ItemLook;
|
|
126
|
+
|
|
127
|
+
export function MenuItem({ unstyled = false, className, ...props }: MenuItemProps) {
|
|
128
|
+
return <BaseMenu.Item className={unstyled ? className : withClass('menu-item', className)} {...props} />;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface MenuCheckProps {
|
|
132
|
+
/* One of several is a bare tick; on or off is a tick in an outlined box, which says the row can be off. */
|
|
133
|
+
kind: 'radio' | 'checkbox';
|
|
134
|
+
/* Only for a row that is not a radio or checkbox item, and so has no indicator of its own. */
|
|
135
|
+
checked?: boolean;
|
|
136
|
+
className?: string;
|
|
137
|
+
ref?: Ref<HTMLSpanElement>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/*
|
|
141
|
+
* The slot before the label of a row that can be checked, in a menu or a list that reads as one. It
|
|
142
|
+
* is one line of the row tall, so a row with a second line keeps the tick beside its first. Radio and
|
|
143
|
+
* checkbox items draw it themselves; a plain row that shows a state hands it `checked`.
|
|
144
|
+
*/
|
|
145
|
+
export function MenuCheck({ kind, checked, className, ref }: MenuCheckProps) {
|
|
146
|
+
const tick = <Icon icon={Check} size={kind === 'radio' ? 14 : 12} />;
|
|
147
|
+
const indicator =
|
|
148
|
+
checked !== undefined ? (
|
|
149
|
+
checked && tick
|
|
150
|
+
) : kind === 'radio' ? (
|
|
151
|
+
<BaseMenu.RadioItemIndicator className="flex">{tick}</BaseMenu.RadioItemIndicator>
|
|
152
|
+
) : (
|
|
153
|
+
<BaseMenu.CheckboxItemIndicator className="flex">{tick}</BaseMenu.CheckboxItemIndicator>
|
|
154
|
+
);
|
|
155
|
+
return (
|
|
156
|
+
<span ref={ref} className={clsx('grid h-lh w-4 shrink-0 place-items-center', className)}>
|
|
157
|
+
{kind === 'checkbox' ? <span className="grid h-4 w-4 place-items-center rounded border border-border-strong">{indicator}</span> : indicator}
|
|
158
|
+
</span>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type Indicator = {
|
|
163
|
+
/* Where the tick or the box sits: before the label, or at the end of the row, for an option that reads as a label with its control after it. */
|
|
164
|
+
indicator?: 'start' | 'end';
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
type MenuCheckboxItemProps = ComponentProps<typeof BaseMenu.CheckboxItem> & ItemLook & Indicator & { children?: ReactNode };
|
|
168
|
+
|
|
169
|
+
/* A row that is on or off. The box before its label is part of it. */
|
|
170
|
+
export function MenuCheckboxItem({ unstyled = false, indicator = 'start', className, children, ...props }: MenuCheckboxItemProps) {
|
|
171
|
+
if (unstyled) {
|
|
172
|
+
return (
|
|
173
|
+
<BaseMenu.CheckboxItem className={className} {...props}>
|
|
174
|
+
{children}
|
|
175
|
+
</BaseMenu.CheckboxItem>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return (
|
|
179
|
+
<BaseMenu.CheckboxItem className={withClass('menu-item', className)} {...props}>
|
|
180
|
+
{indicator === 'start' && <MenuCheck kind="checkbox" />}
|
|
181
|
+
{children}
|
|
182
|
+
{indicator === 'end' && <MenuCheck kind="checkbox" className="ml-auto" />}
|
|
183
|
+
</BaseMenu.CheckboxItem>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const MenuRadioGroup = BaseMenu.RadioGroup;
|
|
188
|
+
|
|
189
|
+
type MenuRadioItemProps = ComponentProps<typeof BaseMenu.RadioItem> & ItemLook & Indicator & { children?: ReactNode };
|
|
190
|
+
|
|
191
|
+
/* One of several. The tick before its label is part of it. */
|
|
192
|
+
export function MenuRadioItem({ unstyled = false, indicator = 'start', className, children, ...props }: MenuRadioItemProps) {
|
|
193
|
+
if (unstyled) {
|
|
194
|
+
return (
|
|
195
|
+
<BaseMenu.RadioItem className={className} {...props}>
|
|
196
|
+
{children}
|
|
197
|
+
</BaseMenu.RadioItem>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return (
|
|
201
|
+
<BaseMenu.RadioItem className={withClass('menu-item', className)} {...props}>
|
|
202
|
+
{indicator === 'start' && <MenuCheck kind="radio" />}
|
|
203
|
+
{children}
|
|
204
|
+
{indicator === 'end' && <MenuCheck kind="radio" className="ml-auto" />}
|
|
205
|
+
</BaseMenu.RadioItem>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
type SubmenuTriggerProps = ComponentProps<typeof BaseMenu.SubmenuTrigger> & {
|
|
210
|
+
children?: ReactNode;
|
|
211
|
+
/* The chevron at the end of the row that says it opens more. Off for a row that carries a hint there instead. */
|
|
212
|
+
chevron?: boolean;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export function MenuSubmenuTrigger({ className, children, chevron = true, ...props }: SubmenuTriggerProps) {
|
|
216
|
+
return (
|
|
217
|
+
<BaseMenu.SubmenuTrigger className={withClass('menu-item', className)} {...props}>
|
|
218
|
+
{children}
|
|
219
|
+
{chevron && <Icon icon={ChevronRight} size={14} className="ml-auto text-text-faint" />}
|
|
220
|
+
</BaseMenu.SubmenuTrigger>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export type MenuRowProps = useRender.ComponentProps<'div'>;
|
|
225
|
+
|
|
226
|
+
/*
|
|
227
|
+
* A row with buttons at its end that reads as one row. Its first item is the row, every `RowAction`
|
|
228
|
+
* and `RowSubmenuTrigger` after it an item of its own, so the arrow keys reach each.
|
|
229
|
+
*/
|
|
230
|
+
export function MenuRow({ render, className, ref, ...props }: MenuRowProps) {
|
|
231
|
+
return useRender({ render, ref, defaultTagName: 'div', props: { role: 'group', ...props, className: clsx('menu-row', className) } });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
type RowButton = {
|
|
235
|
+
icon: LucideIcon;
|
|
236
|
+
/* The button's accessible name. Unlike the `label` of an item, typing never lands on the button. */
|
|
237
|
+
label: string;
|
|
238
|
+
/* Classes for the icon rather than the button, such as a fill. The size stays the row's. */
|
|
239
|
+
iconClassName?: string;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export type MenuRowActionProps = Omit<ComponentProps<typeof BaseMenu.Item>, 'children' | 'label'> & RowButton;
|
|
243
|
+
|
|
244
|
+
/* A button in a `Menu.Row` that acts at once, named by its tooltip. `closeOnClick={false}` keeps the menu open. */
|
|
245
|
+
export function MenuRowAction({ icon, label, iconClassName, className, ...props }: MenuRowActionProps) {
|
|
246
|
+
return (
|
|
247
|
+
<Tooltip label={label} name>
|
|
248
|
+
<BaseMenu.Item className={withClass('menu-item menu-row-action', className)} {...props}>
|
|
249
|
+
<Icon icon={icon} size={14} className={iconClassName} />
|
|
250
|
+
</BaseMenu.Item>
|
|
251
|
+
</Tooltip>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export type MenuRowSubmenuTriggerProps = Omit<ComponentProps<typeof BaseMenu.SubmenuTrigger>, 'children' | 'label'> & RowButton;
|
|
256
|
+
|
|
257
|
+
/*
|
|
258
|
+
* A button in a `Menu.Row` that opens a submenu, inside a `Menu.SubmenuRoot`. It has no tooltip: the
|
|
259
|
+
* submenu opens on the same hover and says what the button holds.
|
|
260
|
+
*/
|
|
261
|
+
export function MenuRowSubmenuTrigger({ icon, label, iconClassName, className, ...props }: MenuRowSubmenuTriggerProps) {
|
|
262
|
+
return (
|
|
263
|
+
<BaseMenu.SubmenuTrigger className={withClass('menu-item menu-row-action', className)} aria-label={label} {...props}>
|
|
264
|
+
<Icon icon={icon} size={14} className={iconClassName} />
|
|
265
|
+
</BaseMenu.SubmenuTrigger>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* Also right between two groups of rows in a popover that reads as a menu. */
|
|
270
|
+
export function MenuSeparator({ className, ...props }: ComponentProps<typeof BaseSeparator>) {
|
|
271
|
+
return <BaseSeparator className={withClass(MENU_SEPARATOR, className)} {...props} />;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export const MenuGroup = BaseMenu.Group;
|
|
275
|
+
|
|
276
|
+
type MenuGroupLabelProps = ComponentProps<typeof BaseMenu.GroupLabel> & {
|
|
277
|
+
/* Leaves the label's type and padding out, for a group that lays out its own heading. */
|
|
278
|
+
unstyled?: boolean;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export function MenuGroupLabel({ unstyled = false, className, ...props }: MenuGroupLabelProps) {
|
|
282
|
+
return <BaseMenu.GroupLabel className={unstyled ? className : withClass(MENU_LABEL, className)} {...props} />;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export type MenuLabelProps = useRender.ComponentProps<'div'>;
|
|
286
|
+
|
|
287
|
+
/* A label above rows that are not a `Menu.Group` of their own, such as a row of swatches. */
|
|
288
|
+
export function MenuLabel({ render, className, ref, ...props }: MenuLabelProps) {
|
|
289
|
+
return useRender({ render, ref, defaultTagName: 'div', props: { ...props, className: clsx(MENU_LABEL, className) } });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export type MenuHintProps = useRender.ComponentProps<'span'>;
|
|
293
|
+
|
|
294
|
+
export function MenuHint({ render, className, ref, ...props }: MenuHintProps) {
|
|
295
|
+
return useRender({ render, ref, defaultTagName: 'span', props: { ...props, className: clsx(MENU_HINT, className) } });
|
|
296
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Ref, RefCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/* One callback ref that fills a component's own ref and the one its caller handed in. */
|
|
4
|
+
export const mergeRefs =
|
|
5
|
+
<T>(...refs: (Ref<T> | undefined)[]): RefCallback<T> =>
|
|
6
|
+
(node) => {
|
|
7
|
+
for (const ref of refs) {
|
|
8
|
+
if (typeof ref === 'function') {
|
|
9
|
+
ref(node);
|
|
10
|
+
} else if (ref) {
|
|
11
|
+
ref.current = node;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
package/src/modality.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
type Modality = 'keyboard' | 'pointer';
|
|
2
|
+
|
|
3
|
+
let current: Modality | null = null;
|
|
4
|
+
let started = false;
|
|
5
|
+
|
|
6
|
+
const set = (modality: Modality): void => {
|
|
7
|
+
if (current === modality) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
current = modality;
|
|
11
|
+
document.documentElement.dataset.modality = modality;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/* Which device the app was last driven with, on the root element as `data-modality`. A ring that
|
|
15
|
+
only belongs to keyboard navigation cannot ask `:focus-visible` alone. Focus that a script moves
|
|
16
|
+
(Base UI does that for the hovered menu item) keeps matching it while the browser's own keyboard
|
|
17
|
+
flag is up, which a single keystroke anywhere, a terminal included, leaves on. Once per page;
|
|
18
|
+
`UIProvider` starts it, and a second call does nothing. */
|
|
19
|
+
export function startInputModality(): void {
|
|
20
|
+
if (started) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
started = true;
|
|
24
|
+
set('pointer');
|
|
25
|
+
window.addEventListener(
|
|
26
|
+
'keydown',
|
|
27
|
+
(event) => {
|
|
28
|
+
// A modifier on its own is half of a shift-click as often as it is a keystroke.
|
|
29
|
+
if (event.key !== 'Shift' && event.key !== 'Control' && event.key !== 'Alt' && event.key !== 'Meta') {
|
|
30
|
+
set('keyboard');
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{ capture: true }
|
|
34
|
+
);
|
|
35
|
+
window.addEventListener('pointerdown', () => set('pointer'), { capture: true, passive: true });
|
|
36
|
+
window.addEventListener('pointermove', () => set('pointer'), { capture: true, passive: true });
|
|
37
|
+
}
|
package/src/platform.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* True on macOS and iOS, where a shortcut is written with ⌘ instead of Ctrl. The keyboard in question is the one in front of this page. */
|
|
2
|
+
export const isApplePlatform = (): boolean => {
|
|
3
|
+
if (typeof navigator === 'undefined') {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const hints = (navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData;
|
|
7
|
+
return applePlatformFrom(hints?.platform || navigator.platform);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const applePlatformFrom = (platform: string | undefined): boolean => /mac|iphone|ipad/i.test(platform ?? '');
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
import { Popover as BasePopover } from '@base-ui-components/react/popover';
|
|
3
|
+
import { PreviewCard as BasePreviewCard } from '@base-ui-components/react/preview-card';
|
|
4
|
+
import { withClass } from '../class-name.ts';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* The surface a popup is drawn on. `menu` is the padded card menus use, `picker` a card whose
|
|
8
|
+
* content runs to its edges (a search field on top of a list), and `plain` leaves the surface to the caller.
|
|
9
|
+
*/
|
|
10
|
+
const POPUP_VARIANT = {
|
|
11
|
+
menu: 'menu-popup',
|
|
12
|
+
picker: 'picker-popup',
|
|
13
|
+
plain: ''
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export type PopupVariant = keyof typeof POPUP_VARIANT;
|
|
17
|
+
|
|
18
|
+
type Placement = Pick<
|
|
19
|
+
ComponentProps<typeof BasePopover.Positioner>,
|
|
20
|
+
'side' | 'align' | 'sideOffset' | 'alignOffset' | 'collisionPadding' | 'collisionBoundary' | 'collisionAvoidance' | 'anchor' | 'sticky' | 'positionMethod'
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
export const PopoverRoot = BasePopover.Root;
|
|
24
|
+
|
|
25
|
+
export const PopoverTrigger = BasePopover.Trigger;
|
|
26
|
+
|
|
27
|
+
export const PopoverClose = BasePopover.Close;
|
|
28
|
+
|
|
29
|
+
export const PopoverTitle = BasePopover.Title;
|
|
30
|
+
|
|
31
|
+
export const PopoverDescription = BasePopover.Description;
|
|
32
|
+
|
|
33
|
+
export type PopoverPopupProps = ComponentProps<typeof BasePopover.Popup> &
|
|
34
|
+
Placement & {
|
|
35
|
+
variant?: PopupVariant;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/* The portal, the positioner and the popup of a popover: under its trigger and lined up with its start unless it says otherwise. */
|
|
39
|
+
export function PopoverPopup({
|
|
40
|
+
variant = 'menu',
|
|
41
|
+
side = 'bottom',
|
|
42
|
+
align = 'start',
|
|
43
|
+
sideOffset = 8,
|
|
44
|
+
alignOffset,
|
|
45
|
+
collisionPadding,
|
|
46
|
+
collisionBoundary,
|
|
47
|
+
collisionAvoidance,
|
|
48
|
+
anchor,
|
|
49
|
+
sticky,
|
|
50
|
+
positionMethod,
|
|
51
|
+
className,
|
|
52
|
+
...props
|
|
53
|
+
}: PopoverPopupProps) {
|
|
54
|
+
return (
|
|
55
|
+
<BasePopover.Portal>
|
|
56
|
+
<BasePopover.Positioner
|
|
57
|
+
className="popup-positioner"
|
|
58
|
+
side={side}
|
|
59
|
+
align={align}
|
|
60
|
+
sideOffset={sideOffset}
|
|
61
|
+
alignOffset={alignOffset}
|
|
62
|
+
collisionPadding={collisionPadding}
|
|
63
|
+
collisionBoundary={collisionBoundary}
|
|
64
|
+
collisionAvoidance={collisionAvoidance}
|
|
65
|
+
anchor={anchor}
|
|
66
|
+
sticky={sticky}
|
|
67
|
+
positionMethod={positionMethod}
|
|
68
|
+
>
|
|
69
|
+
<BasePopover.Popup className={withClass(POPUP_VARIANT[variant], className)} {...props} />
|
|
70
|
+
</BasePopover.Positioner>
|
|
71
|
+
</BasePopover.Portal>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const PreviewCardRoot = BasePreviewCard.Root;
|
|
76
|
+
|
|
77
|
+
export const PreviewCardTrigger = BasePreviewCard.Trigger;
|
|
78
|
+
|
|
79
|
+
export type PreviewCardPopupProps = ComponentProps<typeof BasePreviewCard.Popup> &
|
|
80
|
+
Placement & {
|
|
81
|
+
variant?: PopupVariant;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/* A card that opens while the pointer rests on its trigger, drawn like a popover. */
|
|
85
|
+
export function PreviewCardPopup({
|
|
86
|
+
variant = 'menu',
|
|
87
|
+
side = 'bottom',
|
|
88
|
+
align = 'start',
|
|
89
|
+
sideOffset = 8,
|
|
90
|
+
alignOffset,
|
|
91
|
+
collisionPadding,
|
|
92
|
+
collisionBoundary,
|
|
93
|
+
collisionAvoidance,
|
|
94
|
+
anchor,
|
|
95
|
+
sticky,
|
|
96
|
+
positionMethod,
|
|
97
|
+
className,
|
|
98
|
+
...props
|
|
99
|
+
}: PreviewCardPopupProps) {
|
|
100
|
+
return (
|
|
101
|
+
<BasePreviewCard.Portal>
|
|
102
|
+
<BasePreviewCard.Positioner
|
|
103
|
+
className="z-(--z-popup)"
|
|
104
|
+
side={side}
|
|
105
|
+
align={align}
|
|
106
|
+
sideOffset={sideOffset}
|
|
107
|
+
alignOffset={alignOffset}
|
|
108
|
+
collisionPadding={collisionPadding}
|
|
109
|
+
collisionBoundary={collisionBoundary}
|
|
110
|
+
collisionAvoidance={collisionAvoidance}
|
|
111
|
+
anchor={anchor}
|
|
112
|
+
sticky={sticky}
|
|
113
|
+
positionMethod={positionMethod}
|
|
114
|
+
>
|
|
115
|
+
<BasePreviewCard.Popup className={withClass(POPUP_VARIANT[variant], className)} {...props} />
|
|
116
|
+
</BasePreviewCard.Positioner>
|
|
117
|
+
</BasePreviewCard.Portal>
|
|
118
|
+
);
|
|
119
|
+
}
|
package/src/prefetch.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export type Loader = () => Promise<unknown>;
|
|
2
|
+
|
|
3
|
+
/* Calls `run` once the main thread has nothing better to do. */
|
|
4
|
+
export type WhenIdle = (run: () => void) => void;
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Loads lazy modules ahead of their first opening, one per idle moment and never two at once, so
|
|
8
|
+
* the first open resolves from the module cache. A loader registered while everything is on
|
|
9
|
+
* joins the end, which is how a lazy surface inside another lazy one gets its turn.
|
|
10
|
+
*/
|
|
11
|
+
export class Prefetcher {
|
|
12
|
+
private readonly registered: Loader[] = [];
|
|
13
|
+
private readonly queued = new Set<Loader>();
|
|
14
|
+
private readonly queue: Loader[] = [];
|
|
15
|
+
private everythingOn = false;
|
|
16
|
+
private running: Promise<void> | null = null;
|
|
17
|
+
private loading = false;
|
|
18
|
+
private readonly whenIdle: WhenIdle;
|
|
19
|
+
/* Asked on every start, since a person can turn data saving on while the page runs. */
|
|
20
|
+
private readonly allowed: () => boolean;
|
|
21
|
+
|
|
22
|
+
constructor(whenIdle: WhenIdle, allowed: () => boolean = () => true) {
|
|
23
|
+
this.whenIdle = whenIdle;
|
|
24
|
+
this.allowed = allowed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* True while a prefetch waits on its chunk, so a failure it causes is not taken for a person's. */
|
|
28
|
+
get busy(): boolean {
|
|
29
|
+
return this.loading;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
register(load: Loader): void {
|
|
33
|
+
this.registered.push(load);
|
|
34
|
+
if (this.everythingOn) {
|
|
35
|
+
this.enqueue(load);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Loads this one module, ahead of whatever is registered. */
|
|
40
|
+
prefetch(load: Loader): Promise<void> {
|
|
41
|
+
if (!this.allowed()) {
|
|
42
|
+
return Promise.resolve();
|
|
43
|
+
}
|
|
44
|
+
this.enqueue(load);
|
|
45
|
+
return this.drain();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Loads every registered module, and every one registered from now on. */
|
|
49
|
+
prefetchEverything(): Promise<void> {
|
|
50
|
+
if (!this.allowed()) {
|
|
51
|
+
return Promise.resolve();
|
|
52
|
+
}
|
|
53
|
+
this.everythingOn = true;
|
|
54
|
+
for (const load of this.registered) {
|
|
55
|
+
this.enqueue(load);
|
|
56
|
+
}
|
|
57
|
+
return this.drain();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private enqueue(load: Loader): void {
|
|
61
|
+
if (this.queued.has(load)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.queued.add(load);
|
|
65
|
+
this.queue.push(load);
|
|
66
|
+
void this.drain();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private drain(): Promise<void> {
|
|
70
|
+
// Never on an empty queue: `loadQueue` would clear `running` before it is even set.
|
|
71
|
+
if (this.running === null && this.queue.length > 0) {
|
|
72
|
+
this.running = this.loadQueue();
|
|
73
|
+
}
|
|
74
|
+
return this.running ?? Promise.resolve();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async loadQueue(): Promise<void> {
|
|
78
|
+
try {
|
|
79
|
+
for (let load = this.queue.shift(); load; load = this.queue.shift()) {
|
|
80
|
+
await new Promise<void>((resolve) => this.whenIdle(resolve));
|
|
81
|
+
this.loading = true;
|
|
82
|
+
try {
|
|
83
|
+
await load();
|
|
84
|
+
} catch {
|
|
85
|
+
// A later open tries again; a chunk a deploy removed is the app's to answer, and `busy` tells it this was only a prefetch.
|
|
86
|
+
} finally {
|
|
87
|
+
this.loading = false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} finally {
|
|
91
|
+
this.running = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Safari has no idle callback; the timeout at least lets the frame that asked finish first. */
|
|
97
|
+
const FALLBACK_DELAY_MS = 50;
|
|
98
|
+
/* A canvas that keeps drawing (a blinking cursor, a stream) may never leave an idle moment, and a prefetch that never runs helps nobody. */
|
|
99
|
+
const IDLE_TIMEOUT_MS = 2000;
|
|
100
|
+
|
|
101
|
+
const whenIdle: WhenIdle = (run) => {
|
|
102
|
+
if (typeof requestIdleCallback === 'function') {
|
|
103
|
+
requestIdleCallback(() => run(), { timeout: IDLE_TIMEOUT_MS });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
setTimeout(run, FALLBACK_DELAY_MS);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/* Not in every DOM typing yet, and only Chromium reports it. */
|
|
110
|
+
const savesData = (): boolean => (navigator as { connection?: { saveData?: boolean } }).connection?.saveData === true;
|
|
111
|
+
|
|
112
|
+
/* The one every `lazyNamed` and `lazyDialog` registers with. */
|
|
113
|
+
export const prefetcher = /* @__PURE__ */ new Prefetcher(whenIdle, () => !savesData());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PreviewCardPopup as Popup, PreviewCardRoot as Root, PreviewCardTrigger as Trigger } from '../popover/parts.tsx';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { MouseEvent } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { MoreHorizontal } from 'lucide-react';
|
|
5
|
+
import * as Menu from './menu/index.parts.ts';
|
|
6
|
+
import type { ProjectSwitcherItem } from './ProjectSwitcher.tsx';
|
|
7
|
+
import { Tooltip } from './Tooltip.tsx';
|
|
8
|
+
|
|
9
|
+
export function ProjectSwitcherRow<Item extends ProjectSwitcherItem>({
|
|
10
|
+
project,
|
|
11
|
+
onSelect
|
|
12
|
+
}: {
|
|
13
|
+
project: Item;
|
|
14
|
+
onSelect(project: Item, event: MouseEvent<HTMLElement>): void;
|
|
15
|
+
}) {
|
|
16
|
+
const { t } = useTranslation('ui');
|
|
17
|
+
const hasActions = Boolean(project.actions);
|
|
18
|
+
const item = (
|
|
19
|
+
<Menu.Item
|
|
20
|
+
className={clsx('min-w-0', project.muted && 'opacity-50')}
|
|
21
|
+
disabled={project.disabled}
|
|
22
|
+
label={project.name}
|
|
23
|
+
onClick={(event) => onSelect(project, event)}
|
|
24
|
+
>
|
|
25
|
+
{project.icon}
|
|
26
|
+
<span className="min-w-0 truncate">{project.name}</span>
|
|
27
|
+
{project.hint && <span className="ml-auto truncate pl-3 text-xs text-text-faint">{project.hint}</span>}
|
|
28
|
+
</Menu.Item>
|
|
29
|
+
);
|
|
30
|
+
const row = project.description ? (
|
|
31
|
+
<Tooltip label={project.description} side="right" sideOffset={hasActions ? 41 : undefined}>
|
|
32
|
+
{item}
|
|
33
|
+
</Tooltip>
|
|
34
|
+
) : (
|
|
35
|
+
item
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (!hasActions) {
|
|
39
|
+
return row;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Menu.Row aria-label={project.name}>
|
|
44
|
+
{row}
|
|
45
|
+
<Menu.SubmenuRoot>
|
|
46
|
+
<Menu.RowSubmenuTrigger icon={MoreHorizontal} label={t('projectSwitcher.actionsFor', { name: project.name })} />
|
|
47
|
+
<Menu.Popup className="min-w-52">{project.actions}</Menu.Popup>
|
|
48
|
+
</Menu.SubmenuRoot>
|
|
49
|
+
</Menu.Row>
|
|
50
|
+
);
|
|
51
|
+
}
|
package/src/selection.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The text selection, as a context menu has to read it. The app has one selection at a time and a
|
|
3
|
+
* menu belongs to the surface the click landed on, so both of these are scoped to an element.
|
|
4
|
+
* A selection somewhere else is no selection this menu can copy.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const selectionWithin = (element: HTMLElement | null): string => {
|
|
8
|
+
const selection = window.getSelection();
|
|
9
|
+
if (!element || !selection || selection.isCollapsed || selection.rangeCount === 0) {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
return element.contains(selection.getRangeAt(0).commonAncestorContainer) ? selection.toString() : '';
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const selectAllWithin = (element: HTMLElement | null): void => {
|
|
16
|
+
const selection = window.getSelection();
|
|
17
|
+
if (!element || !selection) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const range = document.createRange();
|
|
21
|
+
range.selectNodeContents(element);
|
|
22
|
+
selection.removeAllRanges();
|
|
23
|
+
selection.addRange(range);
|
|
24
|
+
};
|