@adea-ai/ui 0.145.0 → 0.145.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/composites/workspace-switcher/index.js +1 -1
- package/dist/components/composites/workspace-switcher/workspace-reorder-dialog.d.ts.map +1 -1
- package/dist/components/composites/workspace-switcher/workspace-reorder-dialog.js +8 -3
- package/dist/components/composites/workspace-switcher/workspace-reorder-dialog.js.map +1 -1
- package/dist/components/composites/workspace-switcher/workspace-switcher.d.ts +12 -2
- package/dist/components/composites/workspace-switcher/workspace-switcher.d.ts.map +1 -1
- package/dist/components/composites/workspace-switcher/workspace-switcher.js +67 -10
- package/dist/components/composites/workspace-switcher/workspace-switcher.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/r/src/components/composites/workspace-switcher/workspace-reorder-dialog.tsx +11 -5
- package/dist/r/src/components/composites/workspace-switcher/workspace-switcher.tsx +90 -11
- package/package.json +1 -1
- package/src/components/composites/workspace-switcher/workspace-reorder-dialog.tsx +11 -5
- package/src/components/composites/workspace-switcher/workspace-switcher.tsx +90 -11
|
@@ -1,4 +1,4 @@
|
|
|
1
|
+
import { WorkspaceSwitcher } from "./workspace-switcher.js";
|
|
1
2
|
import { moveTarget, moveWorkspace } from "./workspace-order.js";
|
|
2
3
|
import { WorkspaceReorderDialog } from "./workspace-reorder-dialog.js";
|
|
3
|
-
import { WorkspaceSwitcher } from "./workspace-switcher.js";
|
|
4
4
|
export { WorkspaceReorderDialog, WorkspaceSwitcher, moveTarget, moveWorkspace };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-reorder-dialog.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/workspace-switcher/workspace-reorder-dialog.tsx"],"names":[],"mappings":"AACA,OAAO,EAAwD,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AAe9F,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAA;IACjC,UAAU,EAAE,SAAS,oBAAoB,EAAE,CAAA;IAC3C,2FAA2F;IAC3F,SAAS,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0FAA0F;IAC1F,eAAe,CAAC,EAAE,QAAQ,CAAC,WAAW,GAAG,SAAS,CAAC,CAAA;CACpD,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,2BAA2B,
|
|
1
|
+
{"version":3,"file":"workspace-reorder-dialog.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/workspace-switcher/workspace-reorder-dialog.tsx"],"names":[],"mappings":"AACA,OAAO,EAAwD,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AAe9F,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAA;IACjC,UAAU,EAAE,SAAS,oBAAoB,EAAE,CAAA;IAC3C,2FAA2F;IAC3F,SAAS,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0FAA0F;IAC1F,eAAe,CAAC,EAAE,QAAQ,CAAC,WAAW,GAAG,SAAS,CAAC,CAAA;CACpD,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,2BAA2B,kCAmKxE"}
|
|
@@ -50,13 +50,18 @@ function WorkspaceReorderDialog(props) {
|
|
|
50
50
|
});
|
|
51
51
|
const focusMove = (owner, id, direction) => {
|
|
52
52
|
if (frame !== void 0) cancelAnimationFrame(frame);
|
|
53
|
-
|
|
54
|
-
frame = void 0;
|
|
53
|
+
const restore = () => {
|
|
55
54
|
if (disposed || owner !== session || !props.open) return;
|
|
56
55
|
const row = list?.querySelector(`[data-reorder-id="${CSS.escape(id)}"]`);
|
|
57
56
|
const wanted = row?.querySelector(`[data-move="${direction}"]`);
|
|
58
57
|
const other = row?.querySelector(`[data-move="${direction === "up" ? "down" : "up"}"]`);
|
|
59
|
-
|
|
58
|
+
const target = wanted?.getAttribute("aria-disabled") === "true" ? other : wanted;
|
|
59
|
+
if (target && document.activeElement !== target) target.focus({ preventScroll: true });
|
|
60
|
+
};
|
|
61
|
+
restore();
|
|
62
|
+
frame = requestAnimationFrame(() => {
|
|
63
|
+
frame = void 0;
|
|
64
|
+
restore();
|
|
60
65
|
});
|
|
61
66
|
};
|
|
62
67
|
const move = async (item, direction) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-reorder-dialog.js","names":["ArrowDown","ArrowUp","For","Show","createEffect","createSignal","on","onCleanup","Accessor","Button","Dialog","DialogClose","DialogContent","DialogDescription","DialogFooter","DialogHeader","DialogTitle","createDialogFocusRestoration","ActionButton","moveTarget","moveWorkspace","WorkspaceMoveDirection","WorkspaceReorderItem","id","name","fixed","WorkspaceReorderDialogProps","open","onOpenChange","workspaces","onReorder","ids","Promise","title","restoreFocusRef","HTMLElement","WorkspaceReorderDialog","props","status","setStatus","error","setError","pending","setPending","list","HTMLOListElement","session","disposed","frame","undefined","cancelAnimationFrame","defer","focus","focusMove","owner","direction","requestAnimationFrame","row","querySelector","CSS","escape","wanted","other","target","getAttribute","preventScroll","move","item","next","current","indexOf","length","_$createComponent","children","onKeyDown","event","KeyboardEvent","key","defaultPrevented","preventDefault","onOpenAutoFocus","onCloseAutoFocus","_el$","_tmpl$","_$use","el","_$insert","each","_el$4","_tmpl$4","_el$5","firstChild","when","fallback","_tmpl$5","const","variant","size","aria-label","tooltip","disabled","onClick","_$effect","_$setAttribute","_el$2","_tmpl$2","_el$3","_tmpl$3","as","_$createComponent","_$setAttribute"],"sources":["../../../../src/components/composites/workspace-switcher/workspace-reorder-dialog.tsx"],"sourcesContent":["import { ArrowDown, ArrowUp } from 'lucide-solid'\nimport { For, Show, createEffect, createSignal, on, onCleanup, type Accessor } from 'solid-js'\nimport { Button } from '../../ui/button'\nimport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../../ui/dialog'\nimport { createDialogFocusRestoration } from '../../ui/dialog/dialog'\nimport { ActionButton } from '../action-button'\nimport { moveTarget, moveWorkspace, type WorkspaceMoveDirection } from './workspace-order'\n\nexport type WorkspaceReorderItem = {\n id: string\n name: string\n fixed?: boolean\n}\n\nexport type WorkspaceReorderDialogProps = {\n open: boolean\n onOpenChange(open: boolean): void\n workspaces: readonly WorkspaceReorderItem[]\n /** Receives the full id order after one move. May be async; a rejection shows an error. */\n onReorder(ids: readonly string[]): void | Promise<void>\n title?: string\n /** Where focus returns on close; the menu item that opened the dialog is gone by then. */\n restoreFocusRef?: Accessor<HTMLElement | undefined>\n}\n\n/**\n * WorkspaceReorderDialog.\n *\n * Ordering by buttons, not by dragging, so it works from the keyboard and with\n * a screen reader: every row has a labelled Move up and Move down. Each move is\n * reported to the host straight away (there is no Save), the moved row keeps\n * focus on the control that was used, and the new position is announced.\n *\n * One save runs at a time, even across a close and reopen: the moves stay\n * disabled until the host's promise settles. A save that finishes after the\n * dialog has closed reports nothing here (the host owns any failure it wants\n * to surface) and never moves focus.\n */\nexport function WorkspaceReorderDialog(props: WorkspaceReorderDialogProps) {\n const [status, setStatus] = createSignal('')\n const [error, setError] = createSignal('')\n const [pending, setPending] = createSignal(false)\n let list: HTMLOListElement | undefined\n // Each open and each close starts a new session. A save belongs to the session\n // it began in; once the dialog has closed (or closed and reopened) its result\n // no longer speaks for what is on screen, so it must not touch the status\n // line or focus. `pending` is deliberately not reset with the session: the\n // host's save is still running, so the next move waits for it instead of\n // running a second save beside it.\n let session = 0\n let disposed = false\n let frame: number | undefined\n\n createEffect(\n on(\n () => props.open,\n () => {\n session += 1\n if (frame !== undefined) cancelAnimationFrame(frame)\n frame = undefined\n setStatus('')\n setError('')\n },\n { defer: true }\n )\n )\n onCleanup(() => {\n disposed = true\n session += 1\n if (frame !== undefined) cancelAnimationFrame(frame)\n })\n\n const focus = createDialogFocusRestoration({\n open: () => props.open,\n restoreFocusRef: props.restoreFocusRef,\n })\n\n const focusMove = (owner: number, id: string, direction: WorkspaceMoveDirection) => {\n // The host re-renders the rows with the new order; focus the same control\n // once it exists, or its sibling when the move reached an edge.\n if (frame !== undefined) cancelAnimationFrame(frame)\n frame = requestAnimationFrame(() => {\n frame = undefined\n if (disposed || owner !== session || !props.open) return\n const row = list?.querySelector<HTMLElement>(`[data-reorder-id=\"${CSS.escape(id)}\"]`)\n const wanted = row?.querySelector<HTMLElement>(`[data-move=\"${direction}\"]`)\n const other = row?.querySelector<HTMLElement>(\n `[data-move=\"${direction === 'up' ? 'down' : 'up'}\"]`\n )\n const target = wanted?.getAttribute('aria-disabled') === 'true' ? other : wanted\n target?.focus({ preventScroll: true })\n })\n }\n\n const move = async (item: WorkspaceReorderItem, direction: WorkspaceMoveDirection) => {\n if (pending()) return\n const next = moveWorkspace(props.workspaces, item.id, direction)\n if (!next) return\n const owner = session\n const current = () => !disposed && owner === session && props.open\n setError('')\n setPending(true)\n try {\n await props.onReorder(next)\n if (current())\n setStatus(`Moved ${item.name} to position ${next.indexOf(item.id) + 1} of ${next.length}.`)\n } catch {\n if (current()) {\n setStatus('')\n setError('Could not save the order. Try again.')\n }\n } finally {\n if (!disposed) setPending(false)\n if (current()) focusMove(owner, item.id, direction)\n }\n }\n\n return (\n <Dialog open={props.open} onOpenChange={props.onOpenChange}>\n <DialogContent\n class=\"w-full max-w-sm\"\n onKeyDown={(event: KeyboardEvent) => {\n // Tooltip help on a focused move control would otherwise take the first Escape.\n if (event.key === 'Escape' && !event.defaultPrevented) {\n event.preventDefault()\n props.onOpenChange(false)\n }\n }}\n onOpenAutoFocus={focus.onOpenAutoFocus}\n onCloseAutoFocus={focus.onCloseAutoFocus}\n >\n <DialogHeader>\n <DialogTitle>{props.title ?? 'Reorder workspaces'}</DialogTitle>\n <DialogDescription>\n Move a workspace up or down. The order is saved as you go.\n </DialogDescription>\n </DialogHeader>\n <ol\n ref={(el) => (list = el)}\n class=\"flex flex-col gap-1\"\n aria-label=\"Workspace order\"\n aria-busy={pending()}\n >\n <For each={props.workspaces}>\n {(item) => (\n <li\n data-reorder-id={item.id}\n class=\"flex items-center gap-2 rounded-md border border-border px-3 py-1.5\"\n >\n <span class=\"min-w-0 flex-1 truncate text-sm\">{item.name}</span>\n <Show\n when={!item.fixed}\n fallback={<span class=\"text-xs text-muted-foreground\">Always first</span>}\n >\n <For each={['up', 'down'] as const}>\n {(direction) => (\n <ActionButton\n variant=\"ghost\"\n size=\"icon-sm\"\n data-move={direction}\n aria-label={`Move ${item.name} ${direction}`}\n tooltip={`Move ${direction}`}\n disabled={\n pending() ||\n moveTarget(props.workspaces, item.id, direction) === undefined\n }\n onClick={() => void move(item, direction)}\n >\n <Show when={direction === 'up'} fallback={<ArrowDown aria-hidden=\"true\" />}>\n <ArrowUp aria-hidden=\"true\" />\n </Show>\n </ActionButton>\n )}\n </For>\n </Show>\n </li>\n )}\n </For>\n </ol>\n <p role=\"status\" aria-live=\"polite\" class=\"min-h-5 text-xs text-muted-foreground\">\n {status()}\n </p>\n <Show when={error()}>\n <p role=\"alert\" class=\"text-xs text-destructive\">\n {error()}\n </p>\n </Show>\n <DialogFooter>\n <DialogClose as={Button} variant=\"outline\" size=\"sm\">\n Done\n </DialogClose>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"workspace-reorder-dialog.js","names":["ArrowDown","ArrowUp","For","Show","createEffect","createSignal","on","onCleanup","Accessor","Button","Dialog","DialogClose","DialogContent","DialogDescription","DialogFooter","DialogHeader","DialogTitle","createDialogFocusRestoration","ActionButton","moveTarget","moveWorkspace","WorkspaceMoveDirection","WorkspaceReorderItem","id","name","fixed","WorkspaceReorderDialogProps","open","onOpenChange","workspaces","onReorder","ids","Promise","title","restoreFocusRef","HTMLElement","WorkspaceReorderDialog","props","status","setStatus","error","setError","pending","setPending","list","HTMLOListElement","session","disposed","frame","undefined","cancelAnimationFrame","defer","focus","focusMove","owner","direction","restore","row","querySelector","CSS","escape","wanted","other","target","getAttribute","document","activeElement","preventScroll","requestAnimationFrame","move","item","next","current","indexOf","length","_$createComponent","children","onKeyDown","event","KeyboardEvent","key","defaultPrevented","preventDefault","onOpenAutoFocus","onCloseAutoFocus","_el$","_tmpl$","_$use","el","_$insert","each","_el$4","_tmpl$4","_el$5","firstChild","when","fallback","_tmpl$5","const","variant","size","aria-label","tooltip","disabled","onClick","_$effect","_$setAttribute","_el$2","_tmpl$2","_el$3","_tmpl$3","as","_$createComponent","_$setAttribute"],"sources":["../../../../src/components/composites/workspace-switcher/workspace-reorder-dialog.tsx"],"sourcesContent":["import { ArrowDown, ArrowUp } from 'lucide-solid'\nimport { For, Show, createEffect, createSignal, on, onCleanup, type Accessor } from 'solid-js'\nimport { Button } from '../../ui/button'\nimport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../../ui/dialog'\nimport { createDialogFocusRestoration } from '../../ui/dialog/dialog'\nimport { ActionButton } from '../action-button'\nimport { moveTarget, moveWorkspace, type WorkspaceMoveDirection } from './workspace-order'\n\nexport type WorkspaceReorderItem = {\n id: string\n name: string\n fixed?: boolean\n}\n\nexport type WorkspaceReorderDialogProps = {\n open: boolean\n onOpenChange(open: boolean): void\n workspaces: readonly WorkspaceReorderItem[]\n /** Receives the full id order after one move. May be async; a rejection shows an error. */\n onReorder(ids: readonly string[]): void | Promise<void>\n title?: string\n /** Where focus returns on close; the menu item that opened the dialog is gone by then. */\n restoreFocusRef?: Accessor<HTMLElement | undefined>\n}\n\n/**\n * WorkspaceReorderDialog.\n *\n * Ordering by buttons, not by dragging, so it works from the keyboard and with\n * a screen reader: every row has a labelled Move up and Move down. Each move is\n * reported to the host straight away (there is no Save), the moved row keeps\n * focus on the control that was used, and the new position is announced.\n *\n * One save runs at a time, even across a close and reopen: the moves stay\n * disabled until the host's promise settles. A save that finishes after the\n * dialog has closed reports nothing here (the host owns any failure it wants\n * to surface) and never moves focus.\n */\nexport function WorkspaceReorderDialog(props: WorkspaceReorderDialogProps) {\n const [status, setStatus] = createSignal('')\n const [error, setError] = createSignal('')\n const [pending, setPending] = createSignal(false)\n let list: HTMLOListElement | undefined\n // Each open and each close starts a new session. A save belongs to the session\n // it began in; once the dialog has closed (or closed and reopened) its result\n // no longer speaks for what is on screen, so it must not touch the status\n // line or focus. `pending` is deliberately not reset with the session: the\n // host's save is still running, so the next move waits for it instead of\n // running a second save beside it.\n let session = 0\n let disposed = false\n let frame: number | undefined\n\n createEffect(\n on(\n () => props.open,\n () => {\n session += 1\n if (frame !== undefined) cancelAnimationFrame(frame)\n frame = undefined\n setStatus('')\n setError('')\n },\n { defer: true }\n )\n )\n onCleanup(() => {\n disposed = true\n session += 1\n if (frame !== undefined) cancelAnimationFrame(frame)\n })\n\n const focus = createDialogFocusRestoration({\n open: () => props.open,\n restoreFocusRef: props.restoreFocusRef,\n })\n\n const focusMove = (owner: number, id: string, direction: WorkspaceMoveDirection) => {\n // The host re-renders the rows with the new order, which replaces the control\n // that had focus; focus the same control once it exists, or its sibling when\n // the move reached an edge. Try at once so keys pressed right after a save\n // still reach the dialog, and again next frame for a host that renders later.\n if (frame !== undefined) cancelAnimationFrame(frame)\n const restore = () => {\n if (disposed || owner !== session || !props.open) return\n const row = list?.querySelector<HTMLElement>(`[data-reorder-id=\"${CSS.escape(id)}\"]`)\n const wanted = row?.querySelector<HTMLElement>(`[data-move=\"${direction}\"]`)\n const other = row?.querySelector<HTMLElement>(\n `[data-move=\"${direction === 'up' ? 'down' : 'up'}\"]`\n )\n const target = wanted?.getAttribute('aria-disabled') === 'true' ? other : wanted\n if (target && document.activeElement !== target) target.focus({ preventScroll: true })\n }\n restore()\n frame = requestAnimationFrame(() => {\n frame = undefined\n restore()\n })\n }\n\n const move = async (item: WorkspaceReorderItem, direction: WorkspaceMoveDirection) => {\n if (pending()) return\n const next = moveWorkspace(props.workspaces, item.id, direction)\n if (!next) return\n const owner = session\n const current = () => !disposed && owner === session && props.open\n setError('')\n setPending(true)\n try {\n await props.onReorder(next)\n if (current())\n setStatus(`Moved ${item.name} to position ${next.indexOf(item.id) + 1} of ${next.length}.`)\n } catch {\n if (current()) {\n setStatus('')\n setError('Could not save the order. Try again.')\n }\n } finally {\n if (!disposed) setPending(false)\n if (current()) focusMove(owner, item.id, direction)\n }\n }\n\n return (\n <Dialog open={props.open} onOpenChange={props.onOpenChange}>\n <DialogContent\n class=\"w-full max-w-sm\"\n onKeyDown={(event: KeyboardEvent) => {\n // Tooltip help on a focused move control would otherwise take the first Escape.\n if (event.key === 'Escape' && !event.defaultPrevented) {\n event.preventDefault()\n props.onOpenChange(false)\n }\n }}\n onOpenAutoFocus={focus.onOpenAutoFocus}\n onCloseAutoFocus={focus.onCloseAutoFocus}\n >\n <DialogHeader>\n <DialogTitle>{props.title ?? 'Reorder workspaces'}</DialogTitle>\n <DialogDescription>\n Move a workspace up or down. The order is saved as you go.\n </DialogDescription>\n </DialogHeader>\n <ol\n ref={(el) => (list = el)}\n class=\"flex flex-col gap-1\"\n aria-label=\"Workspace order\"\n aria-busy={pending()}\n >\n <For each={props.workspaces}>\n {(item) => (\n <li\n data-reorder-id={item.id}\n class=\"flex items-center gap-2 rounded-md border border-border px-3 py-1.5\"\n >\n <span class=\"min-w-0 flex-1 truncate text-sm\">{item.name}</span>\n <Show\n when={!item.fixed}\n fallback={<span class=\"text-xs text-muted-foreground\">Always first</span>}\n >\n <For each={['up', 'down'] as const}>\n {(direction) => (\n <ActionButton\n variant=\"ghost\"\n size=\"icon-sm\"\n data-move={direction}\n aria-label={`Move ${item.name} ${direction}`}\n tooltip={`Move ${direction}`}\n disabled={\n pending() ||\n moveTarget(props.workspaces, item.id, direction) === undefined\n }\n onClick={() => void move(item, direction)}\n >\n <Show when={direction === 'up'} fallback={<ArrowDown aria-hidden=\"true\" />}>\n <ArrowUp aria-hidden=\"true\" />\n </Show>\n </ActionButton>\n )}\n </For>\n </Show>\n </li>\n )}\n </For>\n </ol>\n <p role=\"status\" aria-live=\"polite\" class=\"min-h-5 text-xs text-muted-foreground\">\n {status()}\n </p>\n <Show when={error()}>\n <p role=\"alert\" class=\"text-xs text-destructive\">\n {error()}\n </p>\n </Show>\n <DialogFooter>\n <DialogClose as={Button} variant=\"outline\" size=\"sm\">\n Done\n </DialogClose>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":"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"}
|
|
@@ -17,8 +17,16 @@ import type { JSX } from 'solid-js';
|
|
|
17
17
|
export type WorkspaceSwitcherItem = {
|
|
18
18
|
id: string;
|
|
19
19
|
name: string;
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The workspace's mark, drawn in the trigger and in its row. Decorative.
|
|
22
|
+
*
|
|
23
|
+
* A DOM node can sit in one place, so the current workspace's mark would have
|
|
24
|
+
* to be in the trigger and its menu row at once. Pass a function to draw a
|
|
25
|
+
* fresh mark in each place (the preferred form). A node built once is also
|
|
26
|
+
* safe: the trigger keeps it and the current row gets a copy, which carries
|
|
27
|
+
* no event handlers or reactive bindings, so keep marks static.
|
|
28
|
+
*/
|
|
29
|
+
mark: JSX.Element | (() => JSX.Element);
|
|
22
30
|
/** A quiet trailing readout in the row, e.g. an unread count. Optional. */
|
|
23
31
|
detail?: JSX.Element;
|
|
24
32
|
/** Pinned to the top of the order, e.g. Home: it cannot be moved and nothing moves above it. */
|
|
@@ -42,6 +50,8 @@ export type WorkspaceSwitcherProps = {
|
|
|
42
50
|
*/
|
|
43
51
|
onReorder?(ids: readonly string[]): void | Promise<void>;
|
|
44
52
|
reorderLabel?: string;
|
|
53
|
+
/** Replaces the "Reorder workspaces…" item when the dialog could not be loaded. */
|
|
54
|
+
reorderErrorLabel?: string;
|
|
45
55
|
/** The menu's heading; also the trigger's accessible-name prefix. */
|
|
46
56
|
label?: string;
|
|
47
57
|
/** Shown while no workspace is current. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-switcher.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/workspace-switcher/workspace-switcher.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"workspace-switcher.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/workspace-switcher/workspace-switcher.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAsBnC;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;;;OAQG;IACH,IAAI,EAAE,GAAG,CAAC,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,OAAO,CAAC,CAAA;IACvC,2EAA2E;IAC3E,MAAM,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACpB,gGAAgG;IAChG,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,EAAE,SAAS,qBAAqB,EAAE,CAAA;IAC5C,kCAAkC;IAClC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;IACzB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,oDAAoD;IACpD,QAAQ,CAAC,IAAI,IAAI,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4FAA4F;IAC5F,QAAQ,CAAC,IAAI,IAAI,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,SAAS,CAAC,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAWD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,eAkJ9D"}
|
|
@@ -1,27 +1,75 @@
|
|
|
1
1
|
import { cn } from "../../../lib/utils.js";
|
|
2
2
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../../ui/dropdown-menu/dropdown-menu.js";
|
|
3
3
|
import { ActionButton } from "../action-button/action-button.js";
|
|
4
|
-
import {
|
|
5
|
-
import { createComponent, insert, memo, template } from "solid-js/web";
|
|
4
|
+
import { Dynamic, createComponent, insert, memo, template } from "solid-js/web";
|
|
6
5
|
import { ArrowDownUp, ChevronDown, Plus, Settings } from "lucide-solid";
|
|
7
|
-
import { For, Show, createSignal } from "solid-js";
|
|
6
|
+
import { For, Show, createSignal, onCleanup } from "solid-js";
|
|
8
7
|
//#region src/components/composites/workspace-switcher/workspace-switcher.tsx
|
|
9
8
|
var _tmpl$ = /*#__PURE__*/ template(`<span aria-hidden=true class="flex shrink-0">`);
|
|
10
9
|
var _tmpl$2 = /*#__PURE__*/ template(`<span class="min-w-0 max-w-40 truncate">`);
|
|
11
10
|
var _tmpl$3 = /*#__PURE__*/ template(`<span class="min-w-0 flex-1 truncate">`);
|
|
11
|
+
var loadReorderDialog = () => import("./workspace-reorder-dialog.js");
|
|
12
|
+
/**
|
|
13
|
+
* Draws one placement of a mark. A function draws anew each time; a node that is
|
|
14
|
+
* already placed elsewhere is copied so the original stays where it is.
|
|
15
|
+
*/
|
|
16
|
+
function drawMark(mark, copy) {
|
|
17
|
+
if (typeof mark === "function") return mark();
|
|
18
|
+
return copy && mark instanceof Node ? mark.cloneNode(true) : mark;
|
|
19
|
+
}
|
|
12
20
|
function WorkspaceSwitcher(props) {
|
|
13
21
|
const [open, setOpen] = createSignal(false);
|
|
14
22
|
const [reordering, setReordering] = createSignal(false);
|
|
23
|
+
const [reorderDialog, setReorderDialog] = createSignal();
|
|
24
|
+
const [reorderFailed, setReorderFailed] = createSignal(false);
|
|
25
|
+
let cancelPending;
|
|
15
26
|
let trigger;
|
|
16
27
|
const current = () => props.workspaces.find((item) => item.id === props.value);
|
|
17
28
|
const label = () => props.label ?? "Workspaces";
|
|
18
29
|
const canReorder = () => props.onReorder !== void 0 && props.workspaces.filter((item) => !item.fixed).length > 1;
|
|
30
|
+
const loadDialog = () => loadReorderDialog().then((module) => {
|
|
31
|
+
setReorderDialog(() => module.WorkspaceReorderDialog);
|
|
32
|
+
return true;
|
|
33
|
+
}, () => {
|
|
34
|
+
setReorderFailed(true);
|
|
35
|
+
return false;
|
|
36
|
+
});
|
|
37
|
+
const warm = (next) => {
|
|
38
|
+
setOpen(next);
|
|
39
|
+
if (next && canReorder() && !reorderDialog()) loadDialog();
|
|
40
|
+
};
|
|
41
|
+
const requestReorder = () => {
|
|
42
|
+
cancelPending?.();
|
|
43
|
+
if (reorderDialog()) {
|
|
44
|
+
setReordering(true);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let waiting = true;
|
|
48
|
+
const stop = () => {
|
|
49
|
+
waiting = false;
|
|
50
|
+
document.removeEventListener("keydown", onKey, true);
|
|
51
|
+
document.removeEventListener("pointerdown", stop, true);
|
|
52
|
+
if (cancelPending === stop) cancelPending = void 0;
|
|
53
|
+
};
|
|
54
|
+
const onKey = (event) => {
|
|
55
|
+
if (event.key === "Escape") stop();
|
|
56
|
+
};
|
|
57
|
+
document.addEventListener("keydown", onKey, true);
|
|
58
|
+
document.addEventListener("pointerdown", stop, true);
|
|
59
|
+
cancelPending = stop;
|
|
60
|
+
loadDialog().then((loaded) => {
|
|
61
|
+
const stillWanted = waiting;
|
|
62
|
+
stop();
|
|
63
|
+
if (loaded && stillWanted) setReordering(true);
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
onCleanup(() => cancelPending?.());
|
|
19
67
|
return [createComponent(DropdownMenu, {
|
|
20
68
|
modal: false,
|
|
21
69
|
get open() {
|
|
22
70
|
return open();
|
|
23
71
|
},
|
|
24
|
-
onOpenChange:
|
|
72
|
+
onOpenChange: warm,
|
|
25
73
|
get children() {
|
|
26
74
|
return [createComponent(DropdownMenuTrigger, {
|
|
27
75
|
as: ActionButton,
|
|
@@ -45,7 +93,10 @@ function WorkspaceSwitcher(props) {
|
|
|
45
93
|
return [
|
|
46
94
|
(() => {
|
|
47
95
|
var _el$ = _tmpl$();
|
|
48
|
-
insert(_el$, () =>
|
|
96
|
+
insert(_el$, (() => {
|
|
97
|
+
var _c$ = memo(() => !!current());
|
|
98
|
+
return () => _c$() && drawMark(current().mark, false);
|
|
99
|
+
})());
|
|
49
100
|
return _el$;
|
|
50
101
|
})(),
|
|
51
102
|
(() => {
|
|
@@ -93,7 +144,7 @@ function WorkspaceSwitcher(props) {
|
|
|
93
144
|
return [
|
|
94
145
|
(() => {
|
|
95
146
|
var _el$3 = _tmpl$();
|
|
96
|
-
insert(_el$3, () => item.mark);
|
|
147
|
+
insert(_el$3, () => drawMark(item.mark, item.id === props.value));
|
|
97
148
|
return _el$3;
|
|
98
149
|
})(),
|
|
99
150
|
(() => {
|
|
@@ -141,9 +192,12 @@ function WorkspaceSwitcher(props) {
|
|
|
141
192
|
},
|
|
142
193
|
get children() {
|
|
143
194
|
return createComponent(DropdownMenuItem, {
|
|
144
|
-
|
|
195
|
+
get disabled() {
|
|
196
|
+
return reorderFailed();
|
|
197
|
+
},
|
|
198
|
+
onSelect: requestReorder,
|
|
145
199
|
get children() {
|
|
146
|
-
return [createComponent(ArrowDownUp, { "aria-hidden": "true" }), memo(() => props.reorderLabel ?? "Reorder workspaces…")];
|
|
200
|
+
return [createComponent(ArrowDownUp, { "aria-hidden": "true" }), memo(() => memo(() => !!reorderFailed())() ? props.reorderErrorLabel ?? "Couldn’t load reordering. Reload to try again." : props.reorderLabel ?? "Reorder workspaces…")];
|
|
147
201
|
}
|
|
148
202
|
});
|
|
149
203
|
}
|
|
@@ -169,9 +223,12 @@ function WorkspaceSwitcher(props) {
|
|
|
169
223
|
}
|
|
170
224
|
}), createComponent(Show, {
|
|
171
225
|
get when() {
|
|
172
|
-
return props.onReorder;
|
|
226
|
+
return memo(() => !!reorderDialog())() && props.onReorder;
|
|
173
227
|
},
|
|
174
|
-
children: (onReorder) => createComponent(
|
|
228
|
+
children: (onReorder) => createComponent(Dynamic, {
|
|
229
|
+
get component() {
|
|
230
|
+
return reorderDialog();
|
|
231
|
+
},
|
|
175
232
|
get open() {
|
|
176
233
|
return reordering();
|
|
177
234
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-switcher.js","names":["ArrowDownUp","ChevronDown","Plus","Settings","JSX","For","Show","createSignal","cn","DropdownMenu","DropdownMenuContent","DropdownMenuGroup","DropdownMenuItem","DropdownMenuLabel","DropdownMenuRadioGroup","DropdownMenuRadioItem","DropdownMenuSeparator","DropdownMenuTrigger","ActionButton","WorkspaceReorderDialog","WorkspaceSwitcherItem","id","name","mark","Element","detail","fixed","WorkspaceSwitcherProps","workspaces","value","onValueChange","onCreate","createLabel","onManage","manageLabel","onReorder","ids","Promise","reorderLabel","label","placeholder","disabled","class","WorkspaceSwitcher","props","open","setOpen","reordering","setReordering","trigger","HTMLElement","current","find","item","canReorder","undefined","filter","length","_$createComponent","modal","onOpenChange","children","as","variant","size","ref","el","tooltip","aria-label","_el$","_tmpl$","_$insert","_el$2","_tmpl$2","align","sideOffset","onChange","String","each","closeOnSelect","data-workspace-id","_el$3","_el$4","_tmpl$3","when","onSelect","_$memo","restoreFocusRef","_$createComponent","_$memo"],"sources":["../../../../src/components/composites/workspace-switcher/workspace-switcher.tsx"],"sourcesContent":["import { ArrowDownUp, ChevronDown, Plus, Settings } from 'lucide-solid'\nimport type { JSX } from 'solid-js'\nimport { For, Show, createSignal } from 'solid-js'\nimport { cn } from '#lib/utils'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../../ui/dropdown-menu'\nimport { ActionButton } from '../action-button'\nimport { WorkspaceReorderDialog } from './workspace-reorder-dialog'\n\n/**\n * WorkspaceSwitcher.\n *\n * The workspace picker that sits in the top bar beside the sidebar toggle. One\n * ghost button shows the current workspace's mark and name; pressing it opens a\n * radio list of the others. Choosing one only changes what the window is\n * looking at: the switcher reports the id and the host decides what that means,\n * which is why it carries no status of its own and never closes, restarts or\n * moves anything.\n *\n * It is deliberately the simple interaction (a single menu, not a nested\n * accordion): the sidebar below it already shows what is inside the chosen\n * workspace, so the switcher's only job is to name the workspace and let the\n * user change it.\n */\nexport type WorkspaceSwitcherItem = {\n id: string\n name: string\n /** The workspace's mark, drawn in the trigger and its row. Decorative. */\n mark: JSX.Element\n /** A quiet trailing readout in the row, e.g. an unread count. Optional. */\n detail?: JSX.Element\n /** Pinned to the top of the order, e.g. Home: it cannot be moved and nothing moves above it. */\n fixed?: boolean\n}\n\nexport type WorkspaceSwitcherProps = {\n workspaces: readonly WorkspaceSwitcherItem[]\n /** The current workspace's id. */\n value: string | undefined\n onValueChange(id: string): void\n /** Offered at the foot of the menu when present. */\n onCreate?(): void\n createLabel?: string\n /** Offered beside the create action when present: open the current workspace's settings. */\n onManage?(): void\n manageLabel?: string\n /**\n * Offered when present and at least two workspaces can move: opens a dialog\n * with Move up and Move down on each row. Receives the full id order after\n * each move; the host saves it and passes the workspaces back in that order.\n */\n onReorder?(ids: readonly string[]): void | Promise<void>\n reorderLabel?: string\n /** The menu's heading; also the trigger's accessible-name prefix. */\n label?: string\n /** Shown while no workspace is current. */\n placeholder?: string\n disabled?: boolean\n class?: string\n}\n\nexport function WorkspaceSwitcher(props: WorkspaceSwitcherProps) {\n const [open, setOpen] = createSignal(false)\n const [reordering, setReordering] = createSignal(false)\n let trigger: HTMLElement | undefined\n const current = () => props.workspaces.find((item) => item.id === props.value)\n const label = () => props.label ?? 'Workspaces'\n const canReorder = () =>\n props.onReorder !== undefined && props.workspaces.filter((item) => !item.fixed).length > 1\n\n return (\n <>\n <DropdownMenu modal={false} open={open()} onOpenChange={setOpen}>\n <DropdownMenuTrigger\n as={ActionButton}\n variant=\"ghost\"\n size=\"sm\"\n disabled={props.disabled}\n ref={(el: HTMLElement) => (trigger = el)}\n data-slot=\"workspace-switcher\"\n tooltip={`Workspace: ${current()?.name ?? props.placeholder ?? 'Choose workspace'}`}\n aria-label={`Switch workspace, current: ${current()?.name ?? 'none'}`}\n class={cn('max-w-full min-w-0 shrink', props.class)}\n >\n <span aria-hidden=\"true\" class=\"flex shrink-0\">\n {current()?.mark}\n </span>\n <span class=\"min-w-0 max-w-40 truncate\">\n {current()?.name ?? props.placeholder ?? 'Choose workspace'}\n </span>\n <ChevronDown aria-hidden=\"true\" class=\"size-3.5 shrink-0 text-muted-foreground\" />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" sideOffset={6} class=\"min-w-56\">\n <DropdownMenuGroup>\n <DropdownMenuLabel>{label()}</DropdownMenuLabel>\n <DropdownMenuRadioGroup\n value={props.value}\n onChange={(value) => {\n setOpen(false)\n const id = String(value)\n if (id !== props.value) props.onValueChange(id)\n }}\n >\n <For each={props.workspaces}>\n {(item) => (\n <DropdownMenuRadioItem value={item.id} closeOnSelect data-workspace-id={item.id}>\n <span aria-hidden=\"true\" class=\"flex shrink-0\">\n {item.mark}\n </span>\n <span class=\"min-w-0 flex-1 truncate\">{item.name}</span>\n <Show when={item.detail}>{item.detail}</Show>\n </DropdownMenuRadioItem>\n )}\n </For>\n </DropdownMenuRadioGroup>\n </DropdownMenuGroup>\n <Show when={props.onCreate || props.onManage || canReorder()}>\n <DropdownMenuSeparator />\n <Show when={props.onCreate}>\n <DropdownMenuItem onSelect={() => props.onCreate?.()}>\n <Plus aria-hidden=\"true\" />\n {props.createLabel ?? 'New workspace'}\n </DropdownMenuItem>\n </Show>\n <Show when={canReorder()}>\n <DropdownMenuItem onSelect={() => setReordering(true)}>\n <ArrowDownUp aria-hidden=\"true\" />\n {props.reorderLabel ?? 'Reorder workspaces…'}\n </DropdownMenuItem>\n </Show>\n <Show when={props.onManage}>\n <DropdownMenuItem onSelect={() => props.onManage?.()}>\n <Settings aria-hidden=\"true\" />\n {props.manageLabel ?? 'Workspace settings…'}\n </DropdownMenuItem>\n </Show>\n </Show>\n </DropdownMenuContent>\n </DropdownMenu>\n <Show when={props.onReorder}>\n {(onReorder) => (\n <WorkspaceReorderDialog\n open={reordering()}\n onOpenChange={setReordering}\n workspaces={props.workspaces}\n onReorder={onReorder()}\n restoreFocusRef={() => trigger}\n />\n )}\n </Show>\n </>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"workspace-switcher.js","names":["ArrowDownUp","ChevronDown","Plus","Settings","JSX","Dynamic","For","Show","createSignal","onCleanup","cn","DropdownMenu","DropdownMenuContent","DropdownMenuGroup","DropdownMenuItem","DropdownMenuLabel","DropdownMenuRadioGroup","DropdownMenuRadioItem","DropdownMenuSeparator","DropdownMenuTrigger","ActionButton","loadReorderDialog","ReorderDialog","Awaited","ReturnType","WorkspaceSwitcherItem","id","name","mark","Element","detail","fixed","WorkspaceSwitcherProps","workspaces","value","onValueChange","onCreate","createLabel","onManage","manageLabel","onReorder","ids","Promise","reorderLabel","reorderErrorLabel","label","placeholder","disabled","class","drawMark","copy","Node","cloneNode","WorkspaceSwitcher","props","open","setOpen","reordering","setReordering","reorderDialog","setReorderDialog","reorderFailed","setReorderFailed","cancelPending","trigger","HTMLElement","current","find","item","canReorder","undefined","filter","length","loadDialog","then","module","WorkspaceReorderDialog","warm","next","requestReorder","waiting","stop","document","removeEventListener","onKey","event","KeyboardEvent","key","addEventListener","loaded","stillWanted","_$createComponent","modal","onOpenChange","children","as","variant","size","ref","el","tooltip","aria-label","_el$","_tmpl$","_$insert","_c$","_$memo","_el$2","_tmpl$2","align","sideOffset","onChange","String","each","closeOnSelect","data-workspace-id","_el$3","_el$4","_tmpl$3","when","onSelect","component","restoreFocusRef","_$createComponent","_$memo"],"sources":["../../../../src/components/composites/workspace-switcher/workspace-switcher.tsx"],"sourcesContent":["import { ArrowDownUp, ChevronDown, Plus, Settings } from 'lucide-solid'\nimport type { JSX } from 'solid-js'\nimport { Dynamic } from 'solid-js/web'\nimport { For, Show, createSignal, onCleanup } from 'solid-js'\nimport { cn } from '#lib/utils'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../../ui/dropdown-menu'\nimport { ActionButton } from '../action-button'\n\n// The dialog is only reachable from a menu item, so it stays out of the code\n// that ships with the switcher and loads on demand.\nconst loadReorderDialog = () => import('./workspace-reorder-dialog')\ntype ReorderDialog = Awaited<ReturnType<typeof loadReorderDialog>>['WorkspaceReorderDialog']\n\n/**\n * WorkspaceSwitcher.\n *\n * The workspace picker that sits in the top bar beside the sidebar toggle. One\n * ghost button shows the current workspace's mark and name; pressing it opens a\n * radio list of the others. Choosing one only changes what the window is\n * looking at: the switcher reports the id and the host decides what that means,\n * which is why it carries no status of its own and never closes, restarts or\n * moves anything.\n *\n * It is deliberately the simple interaction (a single menu, not a nested\n * accordion): the sidebar below it already shows what is inside the chosen\n * workspace, so the switcher's only job is to name the workspace and let the\n * user change it.\n */\nexport type WorkspaceSwitcherItem = {\n id: string\n name: string\n /**\n * The workspace's mark, drawn in the trigger and in its row. Decorative.\n *\n * A DOM node can sit in one place, so the current workspace's mark would have\n * to be in the trigger and its menu row at once. Pass a function to draw a\n * fresh mark in each place (the preferred form). A node built once is also\n * safe: the trigger keeps it and the current row gets a copy, which carries\n * no event handlers or reactive bindings, so keep marks static.\n */\n mark: JSX.Element | (() => JSX.Element)\n /** A quiet trailing readout in the row, e.g. an unread count. Optional. */\n detail?: JSX.Element\n /** Pinned to the top of the order, e.g. Home: it cannot be moved and nothing moves above it. */\n fixed?: boolean\n}\n\nexport type WorkspaceSwitcherProps = {\n workspaces: readonly WorkspaceSwitcherItem[]\n /** The current workspace's id. */\n value: string | undefined\n onValueChange(id: string): void\n /** Offered at the foot of the menu when present. */\n onCreate?(): void\n createLabel?: string\n /** Offered beside the create action when present: open the current workspace's settings. */\n onManage?(): void\n manageLabel?: string\n /**\n * Offered when present and at least two workspaces can move: opens a dialog\n * with Move up and Move down on each row. Receives the full id order after\n * each move; the host saves it and passes the workspaces back in that order.\n */\n onReorder?(ids: readonly string[]): void | Promise<void>\n reorderLabel?: string\n /** Replaces the \"Reorder workspaces…\" item when the dialog could not be loaded. */\n reorderErrorLabel?: string\n /** The menu's heading; also the trigger's accessible-name prefix. */\n label?: string\n /** Shown while no workspace is current. */\n placeholder?: string\n disabled?: boolean\n class?: string\n}\n\n/**\n * Draws one placement of a mark. A function draws anew each time; a node that is\n * already placed elsewhere is copied so the original stays where it is.\n */\nfunction drawMark(mark: WorkspaceSwitcherItem['mark'], copy: boolean): JSX.Element {\n if (typeof mark === 'function') return mark()\n return copy && mark instanceof Node ? mark.cloneNode(true) : mark\n}\n\nexport function WorkspaceSwitcher(props: WorkspaceSwitcherProps) {\n const [open, setOpen] = createSignal(false)\n const [reordering, setReordering] = createSignal(false)\n const [reorderDialog, setReorderDialog] = createSignal<ReorderDialog>()\n const [reorderFailed, setReorderFailed] = createSignal(false)\n let cancelPending: (() => void) | undefined\n let trigger: HTMLElement | undefined\n const current = () => props.workspaces.find((item) => item.id === props.value)\n const label = () => props.label ?? 'Workspaces'\n const canReorder = () =>\n props.onReorder !== undefined && props.workspaces.filter((item) => !item.fixed).length > 1\n\n // Fetch the dialog's code. Safe to call early and often: the browser keeps one\n // copy, and a failure is remembered here instead of surfacing as an unhandled\n // rejection, so the switcher keeps working without reordering.\n const loadDialog = () =>\n loadReorderDialog().then(\n (module) => {\n setReorderDialog(() => module.WorkspaceReorderDialog)\n return true\n },\n () => {\n setReorderFailed(true)\n return false\n }\n )\n // Warm the chunk while the menu is open, so choosing the item rarely waits.\n const warm = (next: boolean) => {\n setOpen(next)\n if (next && canReorder() && !reorderDialog()) void loadDialog()\n }\n // Choosing the item while the code is still arriving must not open the dialog\n // late: nothing on screen can take Escape or an outside click yet, so either\n // of them withdraws the request, and the dialog opens only if the user waits.\n const requestReorder = () => {\n cancelPending?.()\n if (reorderDialog()) {\n setReordering(true)\n return\n }\n let waiting = true\n const stop = () => {\n waiting = false\n document.removeEventListener('keydown', onKey, true)\n document.removeEventListener('pointerdown', stop, true)\n if (cancelPending === stop) cancelPending = undefined\n }\n const onKey = (event: KeyboardEvent) => {\n if (event.key === 'Escape') stop()\n }\n document.addEventListener('keydown', onKey, true)\n document.addEventListener('pointerdown', stop, true)\n cancelPending = stop\n void loadDialog().then((loaded) => {\n const stillWanted = waiting\n stop()\n if (loaded && stillWanted) setReordering(true)\n })\n }\n onCleanup(() => cancelPending?.())\n\n return (\n <>\n <DropdownMenu modal={false} open={open()} onOpenChange={warm}>\n <DropdownMenuTrigger\n as={ActionButton}\n variant=\"ghost\"\n size=\"sm\"\n disabled={props.disabled}\n ref={(el: HTMLElement) => (trigger = el)}\n data-slot=\"workspace-switcher\"\n tooltip={`Workspace: ${current()?.name ?? props.placeholder ?? 'Choose workspace'}`}\n aria-label={`Switch workspace, current: ${current()?.name ?? 'none'}`}\n class={cn('max-w-full min-w-0 shrink', props.class)}\n >\n <span aria-hidden=\"true\" class=\"flex shrink-0\">\n {current() && drawMark(current()!.mark, false)}\n </span>\n <span class=\"min-w-0 max-w-40 truncate\">\n {current()?.name ?? props.placeholder ?? 'Choose workspace'}\n </span>\n <ChevronDown aria-hidden=\"true\" class=\"size-3.5 shrink-0 text-muted-foreground\" />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" sideOffset={6} class=\"min-w-56\">\n <DropdownMenuGroup>\n <DropdownMenuLabel>{label()}</DropdownMenuLabel>\n <DropdownMenuRadioGroup\n value={props.value}\n onChange={(value) => {\n setOpen(false)\n const id = String(value)\n if (id !== props.value) props.onValueChange(id)\n }}\n >\n <For each={props.workspaces}>\n {(item) => (\n <DropdownMenuRadioItem value={item.id} closeOnSelect data-workspace-id={item.id}>\n <span aria-hidden=\"true\" class=\"flex shrink-0\">\n {drawMark(item.mark, item.id === props.value)}\n </span>\n <span class=\"min-w-0 flex-1 truncate\">{item.name}</span>\n <Show when={item.detail}>{item.detail}</Show>\n </DropdownMenuRadioItem>\n )}\n </For>\n </DropdownMenuRadioGroup>\n </DropdownMenuGroup>\n <Show when={props.onCreate || props.onManage || canReorder()}>\n <DropdownMenuSeparator />\n <Show when={props.onCreate}>\n <DropdownMenuItem onSelect={() => props.onCreate?.()}>\n <Plus aria-hidden=\"true\" />\n {props.createLabel ?? 'New workspace'}\n </DropdownMenuItem>\n </Show>\n <Show when={canReorder()}>\n <DropdownMenuItem disabled={reorderFailed()} onSelect={requestReorder}>\n <ArrowDownUp aria-hidden=\"true\" />\n {reorderFailed()\n ? (props.reorderErrorLabel ?? 'Couldn’t load reordering. Reload to try again.')\n : (props.reorderLabel ?? 'Reorder workspaces…')}\n </DropdownMenuItem>\n </Show>\n <Show when={props.onManage}>\n <DropdownMenuItem onSelect={() => props.onManage?.()}>\n <Settings aria-hidden=\"true\" />\n {props.manageLabel ?? 'Workspace settings…'}\n </DropdownMenuItem>\n </Show>\n </Show>\n </DropdownMenuContent>\n </DropdownMenu>\n <Show when={reorderDialog() && props.onReorder}>\n {(onReorder) => (\n <Dynamic\n component={reorderDialog()}\n open={reordering()}\n onOpenChange={setReordering}\n workspaces={props.workspaces}\n onReorder={onReorder()}\n restoreFocusRef={() => trigger}\n />\n )}\n </Show>\n </>\n )\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -189,9 +189,9 @@ import { UpdateChannelControl } from "./components/composites/update-dialog/chan
|
|
|
189
189
|
import { AboutDialog } from "./components/composites/about-dialog/about-dialog.js";
|
|
190
190
|
import { HelpCenter } from "./components/composites/help-center/help-center.js";
|
|
191
191
|
import { WorkspaceMark } from "./components/composites/workspace-mark/workspace-mark.js";
|
|
192
|
+
import { WorkspaceSwitcher } from "./components/composites/workspace-switcher/workspace-switcher.js";
|
|
192
193
|
import { moveTarget, moveWorkspace } from "./components/composites/workspace-switcher/workspace-order.js";
|
|
193
194
|
import { WorkspaceReorderDialog } from "./components/composites/workspace-switcher/workspace-reorder-dialog.js";
|
|
194
|
-
import { WorkspaceSwitcher } from "./components/composites/workspace-switcher/workspace-switcher.js";
|
|
195
195
|
import { MARK_IMAGE_MAX_DATA_URL_LENGTH, MARK_IMAGE_MAX_SOURCE_BYTES, MARK_IMAGE_SIZE, MarkImageError, centredSquare, checkMarkImageFile, isMarkImageDataUrl, markImageFromFile, markImageProblemCopy } from "./components/composites/icon-picker/image-data-url.js";
|
|
196
196
|
import { isShellIconKey, resolveShellIcon, shellIconEntries, shellIconLabel } from "./components/composites/icon-picker/shell-icons.js";
|
|
197
197
|
import { IconPicker, IconPickerDropdown } from "./components/composites/icon-picker/icon-picker.js";
|
|
@@ -84,11 +84,12 @@ export function WorkspaceReorderDialog(props: WorkspaceReorderDialogProps) {
|
|
|
84
84
|
})
|
|
85
85
|
|
|
86
86
|
const focusMove = (owner: number, id: string, direction: WorkspaceMoveDirection) => {
|
|
87
|
-
// The host re-renders the rows with the new order
|
|
88
|
-
// once it exists, or its sibling when
|
|
87
|
+
// The host re-renders the rows with the new order, which replaces the control
|
|
88
|
+
// that had focus; focus the same control once it exists, or its sibling when
|
|
89
|
+
// the move reached an edge. Try at once so keys pressed right after a save
|
|
90
|
+
// still reach the dialog, and again next frame for a host that renders later.
|
|
89
91
|
if (frame !== undefined) cancelAnimationFrame(frame)
|
|
90
|
-
|
|
91
|
-
frame = undefined
|
|
92
|
+
const restore = () => {
|
|
92
93
|
if (disposed || owner !== session || !props.open) return
|
|
93
94
|
const row = list?.querySelector<HTMLElement>(`[data-reorder-id="${CSS.escape(id)}"]`)
|
|
94
95
|
const wanted = row?.querySelector<HTMLElement>(`[data-move="${direction}"]`)
|
|
@@ -96,7 +97,12 @@ export function WorkspaceReorderDialog(props: WorkspaceReorderDialogProps) {
|
|
|
96
97
|
`[data-move="${direction === 'up' ? 'down' : 'up'}"]`
|
|
97
98
|
)
|
|
98
99
|
const target = wanted?.getAttribute('aria-disabled') === 'true' ? other : wanted
|
|
99
|
-
target
|
|
100
|
+
if (target && document.activeElement !== target) target.focus({ preventScroll: true })
|
|
101
|
+
}
|
|
102
|
+
restore()
|
|
103
|
+
frame = requestAnimationFrame(() => {
|
|
104
|
+
frame = undefined
|
|
105
|
+
restore()
|
|
100
106
|
})
|
|
101
107
|
}
|
|
102
108
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ArrowDownUp, ChevronDown, Plus, Settings } from 'lucide-solid'
|
|
2
2
|
import type { JSX } from 'solid-js'
|
|
3
|
-
import {
|
|
3
|
+
import { Dynamic } from 'solid-js/web'
|
|
4
|
+
import { For, Show, createSignal, onCleanup } from 'solid-js'
|
|
4
5
|
import { cn } from '../../../lib/utils'
|
|
5
6
|
import {
|
|
6
7
|
DropdownMenu,
|
|
@@ -14,7 +15,11 @@ import {
|
|
|
14
15
|
DropdownMenuTrigger,
|
|
15
16
|
} from '../../ui/dropdown-menu'
|
|
16
17
|
import { ActionButton } from '../action-button'
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
// The dialog is only reachable from a menu item, so it stays out of the code
|
|
20
|
+
// that ships with the switcher and loads on demand.
|
|
21
|
+
const loadReorderDialog = () => import('./workspace-reorder-dialog')
|
|
22
|
+
type ReorderDialog = Awaited<ReturnType<typeof loadReorderDialog>>['WorkspaceReorderDialog']
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
25
|
* WorkspaceSwitcher.
|
|
@@ -34,8 +39,16 @@ import { WorkspaceReorderDialog } from './workspace-reorder-dialog'
|
|
|
34
39
|
export type WorkspaceSwitcherItem = {
|
|
35
40
|
id: string
|
|
36
41
|
name: string
|
|
37
|
-
/**
|
|
38
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The workspace's mark, drawn in the trigger and in its row. Decorative.
|
|
44
|
+
*
|
|
45
|
+
* A DOM node can sit in one place, so the current workspace's mark would have
|
|
46
|
+
* to be in the trigger and its menu row at once. Pass a function to draw a
|
|
47
|
+
* fresh mark in each place (the preferred form). A node built once is also
|
|
48
|
+
* safe: the trigger keeps it and the current row gets a copy, which carries
|
|
49
|
+
* no event handlers or reactive bindings, so keep marks static.
|
|
50
|
+
*/
|
|
51
|
+
mark: JSX.Element | (() => JSX.Element)
|
|
39
52
|
/** A quiet trailing readout in the row, e.g. an unread count. Optional. */
|
|
40
53
|
detail?: JSX.Element
|
|
41
54
|
/** Pinned to the top of the order, e.g. Home: it cannot be moved and nothing moves above it. */
|
|
@@ -60,6 +73,8 @@ export type WorkspaceSwitcherProps = {
|
|
|
60
73
|
*/
|
|
61
74
|
onReorder?(ids: readonly string[]): void | Promise<void>
|
|
62
75
|
reorderLabel?: string
|
|
76
|
+
/** Replaces the "Reorder workspaces…" item when the dialog could not be loaded. */
|
|
77
|
+
reorderErrorLabel?: string
|
|
63
78
|
/** The menu's heading; also the trigger's accessible-name prefix. */
|
|
64
79
|
label?: string
|
|
65
80
|
/** Shown while no workspace is current. */
|
|
@@ -68,18 +83,79 @@ export type WorkspaceSwitcherProps = {
|
|
|
68
83
|
class?: string
|
|
69
84
|
}
|
|
70
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Draws one placement of a mark. A function draws anew each time; a node that is
|
|
88
|
+
* already placed elsewhere is copied so the original stays where it is.
|
|
89
|
+
*/
|
|
90
|
+
function drawMark(mark: WorkspaceSwitcherItem['mark'], copy: boolean): JSX.Element {
|
|
91
|
+
if (typeof mark === 'function') return mark()
|
|
92
|
+
return copy && mark instanceof Node ? mark.cloneNode(true) : mark
|
|
93
|
+
}
|
|
94
|
+
|
|
71
95
|
export function WorkspaceSwitcher(props: WorkspaceSwitcherProps) {
|
|
72
96
|
const [open, setOpen] = createSignal(false)
|
|
73
97
|
const [reordering, setReordering] = createSignal(false)
|
|
98
|
+
const [reorderDialog, setReorderDialog] = createSignal<ReorderDialog>()
|
|
99
|
+
const [reorderFailed, setReorderFailed] = createSignal(false)
|
|
100
|
+
let cancelPending: (() => void) | undefined
|
|
74
101
|
let trigger: HTMLElement | undefined
|
|
75
102
|
const current = () => props.workspaces.find((item) => item.id === props.value)
|
|
76
103
|
const label = () => props.label ?? 'Workspaces'
|
|
77
104
|
const canReorder = () =>
|
|
78
105
|
props.onReorder !== undefined && props.workspaces.filter((item) => !item.fixed).length > 1
|
|
79
106
|
|
|
107
|
+
// Fetch the dialog's code. Safe to call early and often: the browser keeps one
|
|
108
|
+
// copy, and a failure is remembered here instead of surfacing as an unhandled
|
|
109
|
+
// rejection, so the switcher keeps working without reordering.
|
|
110
|
+
const loadDialog = () =>
|
|
111
|
+
loadReorderDialog().then(
|
|
112
|
+
(module) => {
|
|
113
|
+
setReorderDialog(() => module.WorkspaceReorderDialog)
|
|
114
|
+
return true
|
|
115
|
+
},
|
|
116
|
+
() => {
|
|
117
|
+
setReorderFailed(true)
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
// Warm the chunk while the menu is open, so choosing the item rarely waits.
|
|
122
|
+
const warm = (next: boolean) => {
|
|
123
|
+
setOpen(next)
|
|
124
|
+
if (next && canReorder() && !reorderDialog()) void loadDialog()
|
|
125
|
+
}
|
|
126
|
+
// Choosing the item while the code is still arriving must not open the dialog
|
|
127
|
+
// late: nothing on screen can take Escape or an outside click yet, so either
|
|
128
|
+
// of them withdraws the request, and the dialog opens only if the user waits.
|
|
129
|
+
const requestReorder = () => {
|
|
130
|
+
cancelPending?.()
|
|
131
|
+
if (reorderDialog()) {
|
|
132
|
+
setReordering(true)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
let waiting = true
|
|
136
|
+
const stop = () => {
|
|
137
|
+
waiting = false
|
|
138
|
+
document.removeEventListener('keydown', onKey, true)
|
|
139
|
+
document.removeEventListener('pointerdown', stop, true)
|
|
140
|
+
if (cancelPending === stop) cancelPending = undefined
|
|
141
|
+
}
|
|
142
|
+
const onKey = (event: KeyboardEvent) => {
|
|
143
|
+
if (event.key === 'Escape') stop()
|
|
144
|
+
}
|
|
145
|
+
document.addEventListener('keydown', onKey, true)
|
|
146
|
+
document.addEventListener('pointerdown', stop, true)
|
|
147
|
+
cancelPending = stop
|
|
148
|
+
void loadDialog().then((loaded) => {
|
|
149
|
+
const stillWanted = waiting
|
|
150
|
+
stop()
|
|
151
|
+
if (loaded && stillWanted) setReordering(true)
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
onCleanup(() => cancelPending?.())
|
|
155
|
+
|
|
80
156
|
return (
|
|
81
157
|
<>
|
|
82
|
-
<DropdownMenu modal={false} open={open()} onOpenChange={
|
|
158
|
+
<DropdownMenu modal={false} open={open()} onOpenChange={warm}>
|
|
83
159
|
<DropdownMenuTrigger
|
|
84
160
|
as={ActionButton}
|
|
85
161
|
variant="ghost"
|
|
@@ -92,7 +168,7 @@ export function WorkspaceSwitcher(props: WorkspaceSwitcherProps) {
|
|
|
92
168
|
class={cn('max-w-full min-w-0 shrink', props.class)}
|
|
93
169
|
>
|
|
94
170
|
<span aria-hidden="true" class="flex shrink-0">
|
|
95
|
-
{current()
|
|
171
|
+
{current() && drawMark(current()!.mark, false)}
|
|
96
172
|
</span>
|
|
97
173
|
<span class="min-w-0 max-w-40 truncate">
|
|
98
174
|
{current()?.name ?? props.placeholder ?? 'Choose workspace'}
|
|
@@ -114,7 +190,7 @@ export function WorkspaceSwitcher(props: WorkspaceSwitcherProps) {
|
|
|
114
190
|
{(item) => (
|
|
115
191
|
<DropdownMenuRadioItem value={item.id} closeOnSelect data-workspace-id={item.id}>
|
|
116
192
|
<span aria-hidden="true" class="flex shrink-0">
|
|
117
|
-
{item.mark}
|
|
193
|
+
{drawMark(item.mark, item.id === props.value)}
|
|
118
194
|
</span>
|
|
119
195
|
<span class="min-w-0 flex-1 truncate">{item.name}</span>
|
|
120
196
|
<Show when={item.detail}>{item.detail}</Show>
|
|
@@ -132,9 +208,11 @@ export function WorkspaceSwitcher(props: WorkspaceSwitcherProps) {
|
|
|
132
208
|
</DropdownMenuItem>
|
|
133
209
|
</Show>
|
|
134
210
|
<Show when={canReorder()}>
|
|
135
|
-
<DropdownMenuItem
|
|
211
|
+
<DropdownMenuItem disabled={reorderFailed()} onSelect={requestReorder}>
|
|
136
212
|
<ArrowDownUp aria-hidden="true" />
|
|
137
|
-
{
|
|
213
|
+
{reorderFailed()
|
|
214
|
+
? (props.reorderErrorLabel ?? 'Couldn’t load reordering. Reload to try again.')
|
|
215
|
+
: (props.reorderLabel ?? 'Reorder workspaces…')}
|
|
138
216
|
</DropdownMenuItem>
|
|
139
217
|
</Show>
|
|
140
218
|
<Show when={props.onManage}>
|
|
@@ -146,9 +224,10 @@ export function WorkspaceSwitcher(props: WorkspaceSwitcherProps) {
|
|
|
146
224
|
</Show>
|
|
147
225
|
</DropdownMenuContent>
|
|
148
226
|
</DropdownMenu>
|
|
149
|
-
<Show when={props.onReorder}>
|
|
227
|
+
<Show when={reorderDialog() && props.onReorder}>
|
|
150
228
|
{(onReorder) => (
|
|
151
|
-
<
|
|
229
|
+
<Dynamic
|
|
230
|
+
component={reorderDialog()}
|
|
152
231
|
open={reordering()}
|
|
153
232
|
onOpenChange={setReordering}
|
|
154
233
|
workspaces={props.workspaces}
|
package/package.json
CHANGED
|
@@ -84,11 +84,12 @@ export function WorkspaceReorderDialog(props: WorkspaceReorderDialogProps) {
|
|
|
84
84
|
})
|
|
85
85
|
|
|
86
86
|
const focusMove = (owner: number, id: string, direction: WorkspaceMoveDirection) => {
|
|
87
|
-
// The host re-renders the rows with the new order
|
|
88
|
-
// once it exists, or its sibling when
|
|
87
|
+
// The host re-renders the rows with the new order, which replaces the control
|
|
88
|
+
// that had focus; focus the same control once it exists, or its sibling when
|
|
89
|
+
// the move reached an edge. Try at once so keys pressed right after a save
|
|
90
|
+
// still reach the dialog, and again next frame for a host that renders later.
|
|
89
91
|
if (frame !== undefined) cancelAnimationFrame(frame)
|
|
90
|
-
|
|
91
|
-
frame = undefined
|
|
92
|
+
const restore = () => {
|
|
92
93
|
if (disposed || owner !== session || !props.open) return
|
|
93
94
|
const row = list?.querySelector<HTMLElement>(`[data-reorder-id="${CSS.escape(id)}"]`)
|
|
94
95
|
const wanted = row?.querySelector<HTMLElement>(`[data-move="${direction}"]`)
|
|
@@ -96,7 +97,12 @@ export function WorkspaceReorderDialog(props: WorkspaceReorderDialogProps) {
|
|
|
96
97
|
`[data-move="${direction === 'up' ? 'down' : 'up'}"]`
|
|
97
98
|
)
|
|
98
99
|
const target = wanted?.getAttribute('aria-disabled') === 'true' ? other : wanted
|
|
99
|
-
target
|
|
100
|
+
if (target && document.activeElement !== target) target.focus({ preventScroll: true })
|
|
101
|
+
}
|
|
102
|
+
restore()
|
|
103
|
+
frame = requestAnimationFrame(() => {
|
|
104
|
+
frame = undefined
|
|
105
|
+
restore()
|
|
100
106
|
})
|
|
101
107
|
}
|
|
102
108
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ArrowDownUp, ChevronDown, Plus, Settings } from 'lucide-solid'
|
|
2
2
|
import type { JSX } from 'solid-js'
|
|
3
|
-
import {
|
|
3
|
+
import { Dynamic } from 'solid-js/web'
|
|
4
|
+
import { For, Show, createSignal, onCleanup } from 'solid-js'
|
|
4
5
|
import { cn } from '#lib/utils'
|
|
5
6
|
import {
|
|
6
7
|
DropdownMenu,
|
|
@@ -14,7 +15,11 @@ import {
|
|
|
14
15
|
DropdownMenuTrigger,
|
|
15
16
|
} from '../../ui/dropdown-menu'
|
|
16
17
|
import { ActionButton } from '../action-button'
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
// The dialog is only reachable from a menu item, so it stays out of the code
|
|
20
|
+
// that ships with the switcher and loads on demand.
|
|
21
|
+
const loadReorderDialog = () => import('./workspace-reorder-dialog')
|
|
22
|
+
type ReorderDialog = Awaited<ReturnType<typeof loadReorderDialog>>['WorkspaceReorderDialog']
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
25
|
* WorkspaceSwitcher.
|
|
@@ -34,8 +39,16 @@ import { WorkspaceReorderDialog } from './workspace-reorder-dialog'
|
|
|
34
39
|
export type WorkspaceSwitcherItem = {
|
|
35
40
|
id: string
|
|
36
41
|
name: string
|
|
37
|
-
/**
|
|
38
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The workspace's mark, drawn in the trigger and in its row. Decorative.
|
|
44
|
+
*
|
|
45
|
+
* A DOM node can sit in one place, so the current workspace's mark would have
|
|
46
|
+
* to be in the trigger and its menu row at once. Pass a function to draw a
|
|
47
|
+
* fresh mark in each place (the preferred form). A node built once is also
|
|
48
|
+
* safe: the trigger keeps it and the current row gets a copy, which carries
|
|
49
|
+
* no event handlers or reactive bindings, so keep marks static.
|
|
50
|
+
*/
|
|
51
|
+
mark: JSX.Element | (() => JSX.Element)
|
|
39
52
|
/** A quiet trailing readout in the row, e.g. an unread count. Optional. */
|
|
40
53
|
detail?: JSX.Element
|
|
41
54
|
/** Pinned to the top of the order, e.g. Home: it cannot be moved and nothing moves above it. */
|
|
@@ -60,6 +73,8 @@ export type WorkspaceSwitcherProps = {
|
|
|
60
73
|
*/
|
|
61
74
|
onReorder?(ids: readonly string[]): void | Promise<void>
|
|
62
75
|
reorderLabel?: string
|
|
76
|
+
/** Replaces the "Reorder workspaces…" item when the dialog could not be loaded. */
|
|
77
|
+
reorderErrorLabel?: string
|
|
63
78
|
/** The menu's heading; also the trigger's accessible-name prefix. */
|
|
64
79
|
label?: string
|
|
65
80
|
/** Shown while no workspace is current. */
|
|
@@ -68,18 +83,79 @@ export type WorkspaceSwitcherProps = {
|
|
|
68
83
|
class?: string
|
|
69
84
|
}
|
|
70
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Draws one placement of a mark. A function draws anew each time; a node that is
|
|
88
|
+
* already placed elsewhere is copied so the original stays where it is.
|
|
89
|
+
*/
|
|
90
|
+
function drawMark(mark: WorkspaceSwitcherItem['mark'], copy: boolean): JSX.Element {
|
|
91
|
+
if (typeof mark === 'function') return mark()
|
|
92
|
+
return copy && mark instanceof Node ? mark.cloneNode(true) : mark
|
|
93
|
+
}
|
|
94
|
+
|
|
71
95
|
export function WorkspaceSwitcher(props: WorkspaceSwitcherProps) {
|
|
72
96
|
const [open, setOpen] = createSignal(false)
|
|
73
97
|
const [reordering, setReordering] = createSignal(false)
|
|
98
|
+
const [reorderDialog, setReorderDialog] = createSignal<ReorderDialog>()
|
|
99
|
+
const [reorderFailed, setReorderFailed] = createSignal(false)
|
|
100
|
+
let cancelPending: (() => void) | undefined
|
|
74
101
|
let trigger: HTMLElement | undefined
|
|
75
102
|
const current = () => props.workspaces.find((item) => item.id === props.value)
|
|
76
103
|
const label = () => props.label ?? 'Workspaces'
|
|
77
104
|
const canReorder = () =>
|
|
78
105
|
props.onReorder !== undefined && props.workspaces.filter((item) => !item.fixed).length > 1
|
|
79
106
|
|
|
107
|
+
// Fetch the dialog's code. Safe to call early and often: the browser keeps one
|
|
108
|
+
// copy, and a failure is remembered here instead of surfacing as an unhandled
|
|
109
|
+
// rejection, so the switcher keeps working without reordering.
|
|
110
|
+
const loadDialog = () =>
|
|
111
|
+
loadReorderDialog().then(
|
|
112
|
+
(module) => {
|
|
113
|
+
setReorderDialog(() => module.WorkspaceReorderDialog)
|
|
114
|
+
return true
|
|
115
|
+
},
|
|
116
|
+
() => {
|
|
117
|
+
setReorderFailed(true)
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
// Warm the chunk while the menu is open, so choosing the item rarely waits.
|
|
122
|
+
const warm = (next: boolean) => {
|
|
123
|
+
setOpen(next)
|
|
124
|
+
if (next && canReorder() && !reorderDialog()) void loadDialog()
|
|
125
|
+
}
|
|
126
|
+
// Choosing the item while the code is still arriving must not open the dialog
|
|
127
|
+
// late: nothing on screen can take Escape or an outside click yet, so either
|
|
128
|
+
// of them withdraws the request, and the dialog opens only if the user waits.
|
|
129
|
+
const requestReorder = () => {
|
|
130
|
+
cancelPending?.()
|
|
131
|
+
if (reorderDialog()) {
|
|
132
|
+
setReordering(true)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
let waiting = true
|
|
136
|
+
const stop = () => {
|
|
137
|
+
waiting = false
|
|
138
|
+
document.removeEventListener('keydown', onKey, true)
|
|
139
|
+
document.removeEventListener('pointerdown', stop, true)
|
|
140
|
+
if (cancelPending === stop) cancelPending = undefined
|
|
141
|
+
}
|
|
142
|
+
const onKey = (event: KeyboardEvent) => {
|
|
143
|
+
if (event.key === 'Escape') stop()
|
|
144
|
+
}
|
|
145
|
+
document.addEventListener('keydown', onKey, true)
|
|
146
|
+
document.addEventListener('pointerdown', stop, true)
|
|
147
|
+
cancelPending = stop
|
|
148
|
+
void loadDialog().then((loaded) => {
|
|
149
|
+
const stillWanted = waiting
|
|
150
|
+
stop()
|
|
151
|
+
if (loaded && stillWanted) setReordering(true)
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
onCleanup(() => cancelPending?.())
|
|
155
|
+
|
|
80
156
|
return (
|
|
81
157
|
<>
|
|
82
|
-
<DropdownMenu modal={false} open={open()} onOpenChange={
|
|
158
|
+
<DropdownMenu modal={false} open={open()} onOpenChange={warm}>
|
|
83
159
|
<DropdownMenuTrigger
|
|
84
160
|
as={ActionButton}
|
|
85
161
|
variant="ghost"
|
|
@@ -92,7 +168,7 @@ export function WorkspaceSwitcher(props: WorkspaceSwitcherProps) {
|
|
|
92
168
|
class={cn('max-w-full min-w-0 shrink', props.class)}
|
|
93
169
|
>
|
|
94
170
|
<span aria-hidden="true" class="flex shrink-0">
|
|
95
|
-
{current()
|
|
171
|
+
{current() && drawMark(current()!.mark, false)}
|
|
96
172
|
</span>
|
|
97
173
|
<span class="min-w-0 max-w-40 truncate">
|
|
98
174
|
{current()?.name ?? props.placeholder ?? 'Choose workspace'}
|
|
@@ -114,7 +190,7 @@ export function WorkspaceSwitcher(props: WorkspaceSwitcherProps) {
|
|
|
114
190
|
{(item) => (
|
|
115
191
|
<DropdownMenuRadioItem value={item.id} closeOnSelect data-workspace-id={item.id}>
|
|
116
192
|
<span aria-hidden="true" class="flex shrink-0">
|
|
117
|
-
{item.mark}
|
|
193
|
+
{drawMark(item.mark, item.id === props.value)}
|
|
118
194
|
</span>
|
|
119
195
|
<span class="min-w-0 flex-1 truncate">{item.name}</span>
|
|
120
196
|
<Show when={item.detail}>{item.detail}</Show>
|
|
@@ -132,9 +208,11 @@ export function WorkspaceSwitcher(props: WorkspaceSwitcherProps) {
|
|
|
132
208
|
</DropdownMenuItem>
|
|
133
209
|
</Show>
|
|
134
210
|
<Show when={canReorder()}>
|
|
135
|
-
<DropdownMenuItem
|
|
211
|
+
<DropdownMenuItem disabled={reorderFailed()} onSelect={requestReorder}>
|
|
136
212
|
<ArrowDownUp aria-hidden="true" />
|
|
137
|
-
{
|
|
213
|
+
{reorderFailed()
|
|
214
|
+
? (props.reorderErrorLabel ?? 'Couldn’t load reordering. Reload to try again.')
|
|
215
|
+
: (props.reorderLabel ?? 'Reorder workspaces…')}
|
|
138
216
|
</DropdownMenuItem>
|
|
139
217
|
</Show>
|
|
140
218
|
<Show when={props.onManage}>
|
|
@@ -146,9 +224,10 @@ export function WorkspaceSwitcher(props: WorkspaceSwitcherProps) {
|
|
|
146
224
|
</Show>
|
|
147
225
|
</DropdownMenuContent>
|
|
148
226
|
</DropdownMenu>
|
|
149
|
-
<Show when={props.onReorder}>
|
|
227
|
+
<Show when={reorderDialog() && props.onReorder}>
|
|
150
228
|
{(onReorder) => (
|
|
151
|
-
<
|
|
229
|
+
<Dynamic
|
|
230
|
+
component={reorderDialog()}
|
|
152
231
|
open={reordering()}
|
|
153
232
|
onOpenChange={setReordering}
|
|
154
233
|
workspaces={props.workspaces}
|