@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
package/src/Pill.tsx
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReactNode, Ref } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
|
|
4
|
+
/* What the label says about the thing it names, drawn as its ground. */
|
|
5
|
+
const TONES = {
|
|
6
|
+
muted: 'bg-surface-sunken text-text-muted',
|
|
7
|
+
/* Lifted off a sunken header, for a label that should stand out as a control. */
|
|
8
|
+
raised: 'bg-surface-active text-text-muted',
|
|
9
|
+
idle: 'bg-status-idle/15 text-status-idle',
|
|
10
|
+
needsYou: 'bg-status-needs-you/15 text-status-needs-you',
|
|
11
|
+
error: 'bg-status-error/15 text-status-error',
|
|
12
|
+
accent: 'bg-accent-soft text-accent'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/* A pill rides in a header or a row; a tag sits at the end of a line of prose, tighter and bolder. */
|
|
16
|
+
const SHAPES = {
|
|
17
|
+
pill: 'rounded-full px-2 py-0.5',
|
|
18
|
+
tag: 'rounded-md px-1.5 py-0.5 font-medium'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface PillProps {
|
|
22
|
+
icon?: ReactNode;
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
tone?: keyof typeof TONES;
|
|
25
|
+
shape?: keyof typeof SHAPES;
|
|
26
|
+
/* A branch name or a count reads better in the monospace face. */
|
|
27
|
+
mono?: boolean;
|
|
28
|
+
/* With a handler the pill is a button; without one it is a label. */
|
|
29
|
+
onClick?: () => void;
|
|
30
|
+
/* Makes the button a toggle, for a switch of what a surface shows. */
|
|
31
|
+
pressed?: boolean;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
className?: string;
|
|
34
|
+
ref?: Ref<HTMLElement>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* The small rounded label in a header, a sidebar row or a settings line: a count, a branch, a status. */
|
|
38
|
+
export function Pill({ icon, children, tone = 'muted', shape = 'pill', mono = false, onClick, pressed, disabled, className, ref }: PillProps) {
|
|
39
|
+
const shared = clsx('inline-flex shrink-0 items-center gap-1 text-xs', SHAPES[shape], TONES[tone], mono && 'font-mono', className);
|
|
40
|
+
if (!onClick) {
|
|
41
|
+
return (
|
|
42
|
+
<span ref={ref} className={shared}>
|
|
43
|
+
{icon}
|
|
44
|
+
{children}
|
|
45
|
+
</span>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return (
|
|
49
|
+
<button
|
|
50
|
+
ref={ref as Ref<HTMLButtonElement>}
|
|
51
|
+
type="button"
|
|
52
|
+
aria-pressed={pressed}
|
|
53
|
+
disabled={disabled}
|
|
54
|
+
className={clsx(shared, 'enabled:hover:text-text disabled:opacity-50 aria-pressed:bg-surface-active aria-pressed:text-text')}
|
|
55
|
+
onClick={onClick}
|
|
56
|
+
>
|
|
57
|
+
{icon}
|
|
58
|
+
{children}
|
|
59
|
+
</button>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { ComponentProps, MouseEvent, ReactNode, Ref } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { ChevronDown, History } from 'lucide-react';
|
|
5
|
+
import { Icon } from './Icon.tsx';
|
|
6
|
+
import * as Menu from './menu/index.parts.ts';
|
|
7
|
+
import { ProjectSwitcherRow } from './project-switcher-row.tsx';
|
|
8
|
+
|
|
9
|
+
export interface ProjectSwitcherItem {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
icon?: ReactNode;
|
|
13
|
+
/* The folder or other details, shown in a tooltip so the row stays on one line. */
|
|
14
|
+
description?: ReactNode;
|
|
15
|
+
/* A machine or workspace name at the end of the row. */
|
|
16
|
+
hint?: ReactNode;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/* A stale or disconnected project can still be selected to try opening it. */
|
|
19
|
+
muted?: boolean;
|
|
20
|
+
/* Menu items for the submenu beside this project's row. */
|
|
21
|
+
actions?: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ProjectSwitcherProps<Item extends ProjectSwitcherItem = ProjectSwitcherItem> {
|
|
25
|
+
current: ProjectSwitcherItem | null;
|
|
26
|
+
projects: readonly Item[];
|
|
27
|
+
recentProjects?: readonly Item[];
|
|
28
|
+
onSelect(project: Item, event: MouseEvent<HTMLElement>): void;
|
|
29
|
+
/* Before the current project's icon, such as the machine it runs on. */
|
|
30
|
+
leading?: ReactNode;
|
|
31
|
+
/* Menu items in a group of their own between the open projects and Recent projects. */
|
|
32
|
+
before?: ReactNode;
|
|
33
|
+
/* Menu items in one group with Recent projects, such as opening a folder or creating a project. */
|
|
34
|
+
children?: ReactNode;
|
|
35
|
+
label?: string;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
open?: boolean;
|
|
38
|
+
defaultOpen?: boolean;
|
|
39
|
+
onOpenChange?: ComponentProps<typeof Menu.Root>['onOpenChange'];
|
|
40
|
+
className?: string;
|
|
41
|
+
ref?: Ref<HTMLButtonElement>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ProjectSwitcher<Item extends ProjectSwitcherItem>({
|
|
45
|
+
current,
|
|
46
|
+
projects,
|
|
47
|
+
recentProjects,
|
|
48
|
+
onSelect,
|
|
49
|
+
leading,
|
|
50
|
+
before,
|
|
51
|
+
children,
|
|
52
|
+
label,
|
|
53
|
+
disabled,
|
|
54
|
+
open,
|
|
55
|
+
defaultOpen,
|
|
56
|
+
onOpenChange,
|
|
57
|
+
className,
|
|
58
|
+
ref
|
|
59
|
+
}: ProjectSwitcherProps<Item>) {
|
|
60
|
+
const { t } = useTranslation('ui');
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Menu.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
|
|
64
|
+
<Menu.Trigger
|
|
65
|
+
ref={ref}
|
|
66
|
+
disabled={disabled}
|
|
67
|
+
aria-label={label ?? current?.name ?? t('projectSwitcher.label')}
|
|
68
|
+
className={clsx('project-switcher-trigger', className)}
|
|
69
|
+
>
|
|
70
|
+
{leading}
|
|
71
|
+
{current?.icon}
|
|
72
|
+
<span className="truncate text-sm font-medium text-text">{current?.name ?? t('projectSwitcher.label')}</span>
|
|
73
|
+
<Icon icon={ChevronDown} size={14} className="shrink-0 text-text-muted" />
|
|
74
|
+
</Menu.Trigger>
|
|
75
|
+
<Menu.Popup className="min-w-60">
|
|
76
|
+
<ProjectSwitcherGroups projects={projects} recentProjects={recentProjects} before={before} onSelect={onSelect}>
|
|
77
|
+
{children}
|
|
78
|
+
</ProjectSwitcherGroups>
|
|
79
|
+
</Menu.Popup>
|
|
80
|
+
</Menu.Root>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* The rows of the popup, apart from it so they render without an open menu. */
|
|
85
|
+
export function ProjectSwitcherGroups<Item extends ProjectSwitcherItem>({
|
|
86
|
+
projects,
|
|
87
|
+
recentProjects = [],
|
|
88
|
+
before,
|
|
89
|
+
children,
|
|
90
|
+
onSelect
|
|
91
|
+
}: Pick<ProjectSwitcherProps<Item>, 'projects' | 'recentProjects' | 'before' | 'children' | 'onSelect'>) {
|
|
92
|
+
const { t } = useTranslation('ui');
|
|
93
|
+
|
|
94
|
+
// The theme hides a separator at the top of a menu or against another, so a group that draws
|
|
95
|
+
// nothing leaves no line behind.
|
|
96
|
+
return (
|
|
97
|
+
<>
|
|
98
|
+
{projects.map((project) => (
|
|
99
|
+
<ProjectSwitcherRow key={project.id} project={project} onSelect={onSelect} />
|
|
100
|
+
))}
|
|
101
|
+
{before && (
|
|
102
|
+
<>
|
|
103
|
+
<Menu.Separator />
|
|
104
|
+
{before}
|
|
105
|
+
</>
|
|
106
|
+
)}
|
|
107
|
+
{(recentProjects.length > 0 || children) && (
|
|
108
|
+
<>
|
|
109
|
+
<Menu.Separator />
|
|
110
|
+
{recentProjects.length > 0 && (
|
|
111
|
+
<Menu.SubmenuRoot>
|
|
112
|
+
<Menu.SubmenuTrigger>
|
|
113
|
+
<Icon icon={History} size={14} /> {t('projectSwitcher.recent')}
|
|
114
|
+
</Menu.SubmenuTrigger>
|
|
115
|
+
<Menu.Popup className="min-w-60">
|
|
116
|
+
{recentProjects.map((project) => (
|
|
117
|
+
<ProjectSwitcherRow key={project.id} project={project} onSelect={onSelect} />
|
|
118
|
+
))}
|
|
119
|
+
</Menu.Popup>
|
|
120
|
+
</Menu.SubmenuRoot>
|
|
121
|
+
)}
|
|
122
|
+
{children}
|
|
123
|
+
</>
|
|
124
|
+
)}
|
|
125
|
+
</>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useState, type ReactNode, type Ref } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import type { LucideIcon } from 'lucide-react';
|
|
4
|
+
import { Button } from './Button.tsx';
|
|
5
|
+
import { DialogDescription, DialogFooter, DialogPopup, DialogRoot, DialogTitle } from './dialog/parts.tsx';
|
|
6
|
+
import { Field, FormError } from './Field.tsx';
|
|
7
|
+
import { Icon } from './Icon.tsx';
|
|
8
|
+
import { Input, TextArea } from './Input.tsx';
|
|
9
|
+
import { useAsyncAction } from './useAsyncAction.ts';
|
|
10
|
+
|
|
11
|
+
export interface PromptDialogProps {
|
|
12
|
+
open: boolean;
|
|
13
|
+
title: string;
|
|
14
|
+
/* The mark beside the title, for a dialog that names the thing it is about. */
|
|
15
|
+
titleIcon?: LucideIcon;
|
|
16
|
+
description?: ReactNode;
|
|
17
|
+
/* A one line field the answer is typed in; without it the dialog is a confirm and nothing else. */
|
|
18
|
+
field?: {
|
|
19
|
+
/* Drawn above the field. Without one only a screen reader hears the name, from `ariaLabel`. */
|
|
20
|
+
label?: string;
|
|
21
|
+
ariaLabel?: string;
|
|
22
|
+
initial?: string;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
/* A branch, a path or anything else read back letter by letter. */
|
|
25
|
+
mono?: boolean;
|
|
26
|
+
maxLength?: number;
|
|
27
|
+
};
|
|
28
|
+
/* A second, taller field under it, for the body of a pull request. */
|
|
29
|
+
area?: { label: string; initial?: string; placeholder?: string };
|
|
30
|
+
confirmLabel: string;
|
|
31
|
+
/* What the button says while the step runs, for one slow enough that a quiet button is not enough. */
|
|
32
|
+
confirmBusyLabel?: string;
|
|
33
|
+
confirmIcon?: LucideIcon;
|
|
34
|
+
danger?: boolean;
|
|
35
|
+
/* The caller runs the step somewhere else and says when it is busy; a step this dialog awaits says so itself. */
|
|
36
|
+
busy?: boolean;
|
|
37
|
+
/* Lets the field be confirmed empty, for a list that may be cleared or a name with a default. */
|
|
38
|
+
allowEmpty?: boolean;
|
|
39
|
+
/* A reason this question cannot be answered at all, beside the one an empty field is. */
|
|
40
|
+
confirmDisabled?: boolean;
|
|
41
|
+
/* A second way out next to the confirm, such as merging before removing. */
|
|
42
|
+
secondary?: { label: string; onClick(): void };
|
|
43
|
+
/* Over another dialog, so what is under it is dimmed a second time. */
|
|
44
|
+
nested?: boolean;
|
|
45
|
+
/* Whatever this question needs beyond a field, between the description and the buttons. */
|
|
46
|
+
children?: ReactNode;
|
|
47
|
+
/* Told what was typed. A rejection stays on screen as the reason and leaves the dialog open. */
|
|
48
|
+
onConfirm(value: string, body: string): void | Promise<unknown>;
|
|
49
|
+
/* What the failure line reads when the rejection carries no sentence of its own. */
|
|
50
|
+
fallbackMessage?: string;
|
|
51
|
+
/* Only ever told `false`: Cancel, Escape or a click outside. The dialog never closes itself. */
|
|
52
|
+
onOpenChange(open: boolean): void;
|
|
53
|
+
/* Extra on the popup. */
|
|
54
|
+
className?: string;
|
|
55
|
+
ref?: Ref<HTMLDivElement>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
* The one dialog a question is asked in: a name to type, or a warning to agree with. Every confirm
|
|
60
|
+
* and every rename shares it, so none of them grows a layout of its own. It never closes itself,
|
|
61
|
+
* because what a confirm leads to is the caller's business: an action that asks a second question
|
|
62
|
+
* would be shut out by a close of its own.
|
|
63
|
+
*/
|
|
64
|
+
export function PromptDialog({
|
|
65
|
+
open,
|
|
66
|
+
title,
|
|
67
|
+
titleIcon,
|
|
68
|
+
description,
|
|
69
|
+
field,
|
|
70
|
+
area,
|
|
71
|
+
confirmLabel,
|
|
72
|
+
confirmBusyLabel,
|
|
73
|
+
confirmIcon,
|
|
74
|
+
danger = false,
|
|
75
|
+
busy = false,
|
|
76
|
+
allowEmpty = false,
|
|
77
|
+
confirmDisabled = false,
|
|
78
|
+
secondary,
|
|
79
|
+
nested = false,
|
|
80
|
+
children,
|
|
81
|
+
onConfirm,
|
|
82
|
+
fallbackMessage,
|
|
83
|
+
onOpenChange,
|
|
84
|
+
className,
|
|
85
|
+
ref
|
|
86
|
+
}: PromptDialogProps) {
|
|
87
|
+
const { t } = useTranslation('ui');
|
|
88
|
+
/* The dialog stays mounted between questions, so every opening starts from what it was handed.
|
|
89
|
+
The token is what it was handed, so a new question resets the fields in the same render that
|
|
90
|
+
shows it and not in a second one after. */
|
|
91
|
+
const token = open ? `${field?.initial ?? ''}\u0000${area?.initial ?? ''}` : '';
|
|
92
|
+
const [draft, setDraft] = useState({ token, value: field?.initial ?? '', body: area?.initial ?? '' });
|
|
93
|
+
const step = useAsyncAction(fallbackMessage);
|
|
94
|
+
if (draft.token !== token) {
|
|
95
|
+
setDraft({ token, value: field?.initial ?? '', body: area?.initial ?? '' });
|
|
96
|
+
step.clear();
|
|
97
|
+
}
|
|
98
|
+
const value = draft.value;
|
|
99
|
+
const body = draft.body;
|
|
100
|
+
const working = busy || step.busy;
|
|
101
|
+
const setValue = (next: string): void => setDraft({ ...draft, value: next });
|
|
102
|
+
const setBody = (next: string): void => setDraft({ ...draft, body: next });
|
|
103
|
+
|
|
104
|
+
const submit = (): void => {
|
|
105
|
+
if (!working && !confirmDisabled && (field === undefined || allowEmpty || value.trim() !== '')) {
|
|
106
|
+
void step.run(async () => await onConfirm(value.trim(), body.trim()));
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const close = (): void => onOpenChange(false);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<DialogRoot open={open} onOpenChange={(next) => !next && close()}>
|
|
114
|
+
<DialogPopup ref={ref} size="sm" nested={nested} className={className}>
|
|
115
|
+
<DialogTitle className="flex items-center gap-2 break-words">
|
|
116
|
+
{titleIcon && <Icon icon={titleIcon} size={16} />}
|
|
117
|
+
{title}
|
|
118
|
+
</DialogTitle>
|
|
119
|
+
{description !== undefined && <DialogDescription className="mt-1 break-words">{description}</DialogDescription>}
|
|
120
|
+
{field !== undefined && (
|
|
121
|
+
<Field label={field.label} className="mt-4">
|
|
122
|
+
<Input
|
|
123
|
+
autoFocus
|
|
124
|
+
mono={field.mono === true}
|
|
125
|
+
aria-label={field.ariaLabel}
|
|
126
|
+
spellCheck={false}
|
|
127
|
+
placeholder={field.placeholder}
|
|
128
|
+
maxLength={field.maxLength}
|
|
129
|
+
value={value}
|
|
130
|
+
onChange={(event) => setValue(event.target.value)}
|
|
131
|
+
onKeyDown={(event) => {
|
|
132
|
+
// Whatever listens on the window has keys of its own, and a name may hold any of them.
|
|
133
|
+
event.stopPropagation();
|
|
134
|
+
if (event.key === 'Enter') {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
submit();
|
|
137
|
+
}
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
</Field>
|
|
141
|
+
)}
|
|
142
|
+
{area !== undefined && (
|
|
143
|
+
<Field label={area.label} className="mt-3">
|
|
144
|
+
<TextArea rows={5} spellCheck={false} placeholder={area.placeholder} value={body} onChange={(event) => setBody(event.target.value)} />
|
|
145
|
+
</Field>
|
|
146
|
+
)}
|
|
147
|
+
{children}
|
|
148
|
+
{step.failure !== null && <FormError className="mt-2 break-words">{step.failure}</FormError>}
|
|
149
|
+
<DialogFooter>
|
|
150
|
+
<Button onClick={close}>{t('action.cancel')}</Button>
|
|
151
|
+
{secondary !== undefined && (
|
|
152
|
+
<Button disabled={working} onClick={secondary.onClick}>
|
|
153
|
+
{secondary.label}
|
|
154
|
+
</Button>
|
|
155
|
+
)}
|
|
156
|
+
<Button
|
|
157
|
+
variant={danger ? 'danger' : 'primary'}
|
|
158
|
+
disabled={working || confirmDisabled || (field !== undefined && !allowEmpty && value.trim() === '')}
|
|
159
|
+
onClick={submit}
|
|
160
|
+
>
|
|
161
|
+
{confirmIcon && <Icon icon={confirmIcon} size={12} />}
|
|
162
|
+
{working && confirmBusyLabel !== undefined ? confirmBusyLabel : confirmLabel}
|
|
163
|
+
</Button>
|
|
164
|
+
</DialogFooter>
|
|
165
|
+
</DialogPopup>
|
|
166
|
+
</DialogRoot>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { useRender } from '@base-ui-components/react/use-render';
|
|
3
|
+
|
|
4
|
+
/* A label takes the line height of the row it sits in, which is why the size carries `/[inherit]`. */
|
|
5
|
+
const SECTION_LABEL = 'text-xs/[inherit] font-medium text-text-faint';
|
|
6
|
+
|
|
7
|
+
export type SectionLabelProps = useRender.ComponentProps<'span'>;
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* The small label above a group of rows outside a popup: a sidebar's groups, a palette's sections,
|
|
11
|
+
* the name of a field. Always sentence case. A `<span>` unless `render` makes it a heading or a label.
|
|
12
|
+
*/
|
|
13
|
+
export function SectionLabel({ render, className, ref, ...props }: SectionLabelProps) {
|
|
14
|
+
return useRender({
|
|
15
|
+
render,
|
|
16
|
+
ref,
|
|
17
|
+
defaultTagName: 'span',
|
|
18
|
+
props: { ...props, className: clsx(SECTION_LABEL, className) }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type Ref } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { mergeRefs } from './merge-refs.ts';
|
|
4
|
+
import { Tooltip } from './Tooltip.tsx';
|
|
5
|
+
import { useMeasuredWidth } from './useMeasuredWidth.ts';
|
|
6
|
+
|
|
7
|
+
export interface SegmentBarPart {
|
|
8
|
+
/* Its length, in the unit of the bar's `range`. */
|
|
9
|
+
value: number;
|
|
10
|
+
/* Where it starts, in the unit of the bar's `range`. Defaults to the end of the part before, or 0 for the first;
|
|
11
|
+
a later start leaves the stretch in between empty. Parts stay in order. */
|
|
12
|
+
start?: number;
|
|
13
|
+
/* Drawn in the part where there is room for it, and always read by a screen reader. */
|
|
14
|
+
label?: string;
|
|
15
|
+
/* Any CSS color, such as `var(--positive)`, in place of the theme's. It wins over the accent of `current`. */
|
|
16
|
+
color?: string;
|
|
17
|
+
/* The part a person is at, such as the section under the playhead. */
|
|
18
|
+
current?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SegmentBarProps {
|
|
22
|
+
parts: readonly SegmentBarPart[];
|
|
23
|
+
/* The stretch the bar shows, in the unit of the parts: the rest of a whole, or the visible part of a zoomed timeline.
|
|
24
|
+
Defaults to 0 up to the end of the last part. */
|
|
25
|
+
range?: readonly [from: number, to: number];
|
|
26
|
+
/* `md` draws each label in its part, `sm` is a thin bar that shows its labels only as a tooltip. */
|
|
27
|
+
size?: 'sm' | 'md';
|
|
28
|
+
/* Makes every part a button, which calls this with its index in `parts`. */
|
|
29
|
+
onSelect?(index: number): void;
|
|
30
|
+
/* The accessible name of the whole. */
|
|
31
|
+
label?: string;
|
|
32
|
+
className?: string;
|
|
33
|
+
ref?: Ref<HTMLOListElement>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* A whole split into parts, each as wide as it lasts, so they compare at a glance. A part starts exactly at its
|
|
37
|
+
place; the gap between two parts that touch comes out of the one before. */
|
|
38
|
+
export function SegmentBar({ parts, range, size = 'md', onSelect, label, className, ref }: SegmentBarProps) {
|
|
39
|
+
const [measure, width] = useMeasuredWidth();
|
|
40
|
+
const rootRef = useMemo(() => mergeRefs(ref, measure), [ref, measure]);
|
|
41
|
+
const labels = useRef<(HTMLSpanElement | null)[]>([]);
|
|
42
|
+
const [cut, setCut] = useState<readonly boolean[]>([]);
|
|
43
|
+
|
|
44
|
+
let end = 0;
|
|
45
|
+
const placed = parts.map((part, index) => {
|
|
46
|
+
const from = part.start ?? end;
|
|
47
|
+
end = from + Math.max(0, part.value);
|
|
48
|
+
return { part, index, from, to: end };
|
|
49
|
+
});
|
|
50
|
+
const [from, to] = range ?? [0, end];
|
|
51
|
+
const span = to - from;
|
|
52
|
+
const visible = span > 0 ? placed.filter((placement) => placement.to > from && placement.from < to) : [];
|
|
53
|
+
// Parts without a name say nothing a screen reader could use; the element around the bar names it, as a meter does.
|
|
54
|
+
// Parts a person can press stay reachable, since a hidden list must not hold focusable buttons.
|
|
55
|
+
const hidden = onSelect === undefined && !parts.some((part) => part.label !== undefined);
|
|
56
|
+
|
|
57
|
+
// A label that stops fitting after a resize or a new range gets its tooltip.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const next = labels.current.slice(0, visible.length).map((element) => element !== null && element.scrollWidth > element.clientWidth);
|
|
60
|
+
setCut((current) => (current.length === next.length && current.every((value, i) => value === next[i]) ? current : next));
|
|
61
|
+
}, [width, parts, from, to, size, visible.length]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<ol
|
|
65
|
+
ref={rootRef}
|
|
66
|
+
aria-label={hidden ? undefined : label}
|
|
67
|
+
aria-hidden={hidden ? true : undefined}
|
|
68
|
+
className={clsx('relative min-w-0', size === 'sm' ? 'h-1.5' : 'h-7', className)}
|
|
69
|
+
>
|
|
70
|
+
{visible.map(({ part, index, from: partFrom, to: partTo }, i) => {
|
|
71
|
+
const left = (Math.max(partFrom, from) - from) / span;
|
|
72
|
+
const right = (Math.min(partTo, to) - from) / span;
|
|
73
|
+
const next = visible[i + 1];
|
|
74
|
+
// Less than a pixel apart they touch, whatever rounding the caller's starts carry.
|
|
75
|
+
const touches = next !== undefined && ((next.from - partTo) / span) * width < 1;
|
|
76
|
+
const fill = clsx(
|
|
77
|
+
'flex size-full min-w-0 items-center',
|
|
78
|
+
size === 'sm' ? 'rounded-full' : 'rounded-sm px-1.5 text-left text-xs',
|
|
79
|
+
part.current && (size === 'sm' ? 'bg-accent' : 'bg-accent-soft text-text'),
|
|
80
|
+
!part.current && part.color === undefined && 'bg-surface-active',
|
|
81
|
+
!part.current && size === 'md' && 'text-text-muted',
|
|
82
|
+
onSelect !== undefined && 'cursor-pointer hover:opacity-80'
|
|
83
|
+
);
|
|
84
|
+
const text = (
|
|
85
|
+
<span
|
|
86
|
+
ref={(element) => {
|
|
87
|
+
labels.current[i] = element;
|
|
88
|
+
}}
|
|
89
|
+
className={size === 'sm' ? 'sr-only' : 'min-w-0 truncate'}
|
|
90
|
+
>
|
|
91
|
+
{part.label}
|
|
92
|
+
</span>
|
|
93
|
+
);
|
|
94
|
+
const body =
|
|
95
|
+
onSelect === undefined ? (
|
|
96
|
+
<span className={fill} style={{ backgroundColor: part.color }} aria-current={part.current ? true : undefined}>
|
|
97
|
+
{text}
|
|
98
|
+
</span>
|
|
99
|
+
) : (
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
className={fill}
|
|
103
|
+
style={{ backgroundColor: part.color }}
|
|
104
|
+
aria-current={part.current ? true : undefined}
|
|
105
|
+
onClick={() => onSelect(index)}
|
|
106
|
+
>
|
|
107
|
+
{text}
|
|
108
|
+
</button>
|
|
109
|
+
);
|
|
110
|
+
return (
|
|
111
|
+
<li
|
|
112
|
+
key={index}
|
|
113
|
+
className={clsx('absolute inset-y-0 focus-within:z-10', touches && 'pr-0.5')}
|
|
114
|
+
style={{ left: `${left * 100}%`, width: `${(right - left) * 100}%` }}
|
|
115
|
+
>
|
|
116
|
+
{part.label === undefined ? (
|
|
117
|
+
body
|
|
118
|
+
) : (
|
|
119
|
+
<Tooltip label={part.label} disabled={size === 'md' && !cut[i]}>
|
|
120
|
+
{body}
|
|
121
|
+
</Tooltip>
|
|
122
|
+
)}
|
|
123
|
+
</li>
|
|
124
|
+
);
|
|
125
|
+
})}
|
|
126
|
+
</ol>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Ref } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import type { LucideIcon } from 'lucide-react';
|
|
4
|
+
import { Icon } from './Icon.tsx';
|
|
5
|
+
|
|
6
|
+
export interface SegmentedOption<T extends string> {
|
|
7
|
+
id: T;
|
|
8
|
+
label: string;
|
|
9
|
+
icon?: LucideIcon;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SegmentedProps<T extends string> {
|
|
13
|
+
value: T;
|
|
14
|
+
onValueChange(id: T): void;
|
|
15
|
+
options: readonly SegmentedOption<T>[];
|
|
16
|
+
/* The accessible name of the group. */
|
|
17
|
+
label: string;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
className?: string;
|
|
20
|
+
ref?: Ref<HTMLDivElement>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* One of a few options side by side in one sunken track, the picked one lifted out of it. */
|
|
24
|
+
export function Segmented<T extends string>({ value, onValueChange, options, label, disabled, className, ref }: SegmentedProps<T>) {
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
ref={ref}
|
|
28
|
+
className={clsx('flex h-8 w-fit items-center rounded-lg bg-surface-sunken p-0.5 text-xs font-medium', className)}
|
|
29
|
+
role="radiogroup"
|
|
30
|
+
aria-label={label}
|
|
31
|
+
>
|
|
32
|
+
{options.map((option) => (
|
|
33
|
+
<button
|
|
34
|
+
key={option.id}
|
|
35
|
+
type="button"
|
|
36
|
+
role="radio"
|
|
37
|
+
aria-checked={value === option.id}
|
|
38
|
+
disabled={disabled}
|
|
39
|
+
className={clsx(
|
|
40
|
+
'flex h-7 items-center gap-1.5 rounded-md px-3 transition-colors disabled:opacity-50',
|
|
41
|
+
value === option.id ? 'bg-surface-raised text-text shadow-raised' : 'text-text-muted hover:text-text'
|
|
42
|
+
)}
|
|
43
|
+
onClick={() => onValueChange(option.id)}
|
|
44
|
+
>
|
|
45
|
+
{option.icon && <Icon icon={option.icon} size={14} />}
|
|
46
|
+
{option.label}
|
|
47
|
+
</button>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|