@miragon/modeler-shell 0.0.0-stage → 0.2.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/API.md +2869 -0
- package/LICENSE +21 -0
- package/README.md +566 -2
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/dist/appearance-BBaM_4bD.d.ts +323 -0
- package/dist/appearance-D3FUcbu6.d.cts +53 -0
- package/dist/appearance-D3FUcbu6.d.ts +53 -0
- package/dist/appearance-XX3JC3pI.d.cts +323 -0
- package/dist/chunk-66HCSJNY.js +2792 -0
- package/dist/chunk-66HCSJNY.js.map +1 -0
- package/dist/chunk-KYDG5DMX.js +136 -0
- package/dist/chunk-KYDG5DMX.js.map +1 -0
- package/dist/chunk-N4QXFF3A.js +23 -0
- package/dist/chunk-N4QXFF3A.js.map +1 -0
- package/dist/chunk-OADW3GPA.js +785 -0
- package/dist/chunk-OADW3GPA.js.map +1 -0
- package/dist/core.cjs +873 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +246 -0
- package/dist/core.d.ts +246 -0
- package/dist/core.js +131 -0
- package/dist/core.js.map +1 -0
- package/dist/diagram-js.cjs +1147 -0
- package/dist/diagram-js.cjs.map +1 -0
- package/dist/diagram-js.d.cts +170 -0
- package/dist/diagram-js.d.ts +170 -0
- package/dist/diagram-js.js +879 -0
- package/dist/diagram-js.js.map +1 -0
- package/dist/index.cjs +3632 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +143 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol-DhYBakWL.d.cts +15 -0
- package/dist/protocol-IeW8bf4f.d.ts +15 -0
- package/dist/shell.css +1603 -0
- package/dist/testing-vscode-stub.cjs +332 -0
- package/dist/testing-vscode-stub.cjs.map +1 -0
- package/dist/testing-vscode-stub.d.cts +65 -0
- package/dist/testing-vscode-stub.d.ts +65 -0
- package/dist/testing-vscode-stub.js +154 -0
- package/dist/testing-vscode-stub.js.map +1 -0
- package/dist/testing.cjs +4663 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +231 -0
- package/dist/testing.d.ts +231 -0
- package/dist/testing.js +4483 -0
- package/dist/testing.js.map +1 -0
- package/dist/toasts-Bp0REqdq.d.cts +142 -0
- package/dist/toasts-Bp0REqdq.d.ts +142 -0
- package/dist/types-CnRrINRs.d.cts +445 -0
- package/dist/types-PuDZczzy.d.ts +445 -0
- package/dist/vscode-extension.cjs +215 -0
- package/dist/vscode-extension.cjs.map +1 -0
- package/dist/vscode-extension.d.cts +103 -0
- package/dist/vscode-extension.d.ts +103 -0
- package/dist/vscode-extension.js +24 -0
- package/dist/vscode-extension.js.map +1 -0
- package/dist/vscode-webview.cjs +3561 -0
- package/dist/vscode-webview.cjs.map +1 -0
- package/dist/vscode-webview.d.cts +84 -0
- package/dist/vscode-webview.d.ts +84 -0
- package/dist/vscode-webview.js +259 -0
- package/dist/vscode-webview.js.map +1 -0
- package/package.json +145 -4
- package/src/core/appearance.ts +104 -0
- package/src/core/feedback/appearance.ts +141 -0
- package/src/core/feedback/problems.ts +23 -0
- package/src/core/feedback/toasts.ts +118 -0
- package/src/core/fit.ts +220 -0
- package/src/core/index.ts +127 -0
- package/src/core/keymap.ts +295 -0
- package/src/core/layout.ts +69 -0
- package/src/core/messages.ts +159 -0
- package/src/core/sizes.ts +85 -0
- package/src/core/types.ts +106 -0
- package/src/core/web-chrome/bottom-row.ts +37 -0
- package/src/core/web-chrome/drop.ts +78 -0
- package/src/core/web-chrome/top-row.ts +66 -0
- package/src/core/zoom.ts +74 -0
- package/src/core.ts +2 -0
- package/src/diagram-js/connect.ts +75 -0
- package/src/diagram-js/index.ts +16 -0
- package/src/diagram-js/module.ts +20 -0
- package/src/diagram-js/palette-skin.ts +267 -0
- package/src/diagram-js/palette.css +269 -0
- package/src/diagram-js/service.ts +243 -0
- package/src/diagram-js/types.ts +67 -0
- package/src/diagram-js/viewport.ts +583 -0
- package/src/diagram-js.ts +2 -0
- package/src/dom/buttons.css +124 -0
- package/src/dom/element.ts +51 -0
- package/src/dom/feedback/appearance.ts +184 -0
- package/src/dom/feedback/feedback.css +435 -0
- package/src/dom/feedback/problems-popover.ts +287 -0
- package/src/dom/feedback/toasts.ts +431 -0
- package/src/dom/hint.ts +51 -0
- package/src/dom/icons.ts +309 -0
- package/src/dom/index.ts +42 -0
- package/src/dom/inspector.ts +35 -0
- package/src/dom/keys.ts +67 -0
- package/src/dom/ledger.ts +87 -0
- package/src/dom/legend.ts +85 -0
- package/src/dom/menu.ts +404 -0
- package/src/dom/mount.ts +731 -0
- package/src/dom/problems.ts +73 -0
- package/src/dom/shell-dom.css +969 -0
- package/src/dom/status.ts +72 -0
- package/src/dom/tooltip.ts +237 -0
- package/src/dom/types.ts +459 -0
- package/src/dom/web/drop-zone.ts +181 -0
- package/src/dom/web/empty-state.ts +144 -0
- package/src/dom/web/legal.ts +75 -0
- package/src/dom/web/menu-groups.ts +69 -0
- package/src/dom/web/primary.ts +124 -0
- package/src/dom/web/web.css +386 -0
- package/src/dom/zoom-panel.ts +154 -0
- package/src/index.ts +3 -0
- package/src/shell.css +9 -0
- package/src/testing/case-tools.ts +164 -0
- package/src/testing/cases.ts +1077 -0
- package/src/testing/checks.ts +868 -0
- package/src/testing/facts.ts +641 -0
- package/src/testing/geometry.ts +652 -0
- package/src/testing/harness.ts +187 -0
- package/src/testing/index.ts +19 -0
- package/src/testing/page-tools.ts +457 -0
- package/src/testing/page.ts +27 -0
- package/src/testing/vscode-stub.ts +237 -0
- package/src/testing/web-cases.ts +913 -0
- package/src/testing/web-checks.ts +1049 -0
- package/src/testing/web-facts.ts +713 -0
- package/src/testing-vscode-stub.ts +2 -0
- package/src/testing.ts +2 -0
- package/src/vscode/extension/html.ts +107 -0
- package/src/vscode/extension/index.ts +8 -0
- package/src/vscode/extension/reveal.ts +9 -0
- package/src/vscode/extension/theme-bridge.ts +85 -0
- package/src/vscode/extension/types.ts +36 -0
- package/src/vscode/protocol.ts +31 -0
- package/src/vscode/webview/appearance.ts +141 -0
- package/src/vscode/webview/index.ts +8 -0
- package/src/vscode/webview/mount.ts +88 -0
- package/src/vscode/webview/state.ts +116 -0
- package/src/vscode-extension.ts +2 -0
- package/src/vscode-webview.ts +2 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { ariaKeyShortcutsFromLabel, type ShellMessages } from "../../core/index.js";
|
|
2
|
+
import { createElement } from "../element.js";
|
|
3
|
+
import type { EmptyStateOptions, FileMenuAction } from "../types.js";
|
|
4
|
+
|
|
5
|
+
export interface EmptyStateContext {
|
|
6
|
+
ownerDocument: Document;
|
|
7
|
+
messages: ShellMessages;
|
|
8
|
+
options: EmptyStateOptions;
|
|
9
|
+
/** The drop hint under the buttons; `undefined` without a drop zone. */
|
|
10
|
+
dropHint: string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* The File group's "Open…": the hint offers it as a text link, the keyboard's way to a file
|
|
13
|
+
* while the start screen hides the menu and a drop needs a pointer. `undefined` without one.
|
|
14
|
+
*/
|
|
15
|
+
open?: FileMenuAction | undefined;
|
|
16
|
+
/** `id` of the title, the section's `aria-labelledby`. */
|
|
17
|
+
titleId: string;
|
|
18
|
+
/** Runs before `onNewDiagram` / `onShowExample`: the shell hides the card and moves the focus. */
|
|
19
|
+
onLeave(): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EmptyStateController {
|
|
23
|
+
/** The `<section data-chrome="empty-state">`. */
|
|
24
|
+
readonly element: HTMLElement;
|
|
25
|
+
isShown(): boolean;
|
|
26
|
+
setShown(shown: boolean): void;
|
|
27
|
+
/** Focuses "New diagram", the card's first button. */
|
|
28
|
+
focus(): void;
|
|
29
|
+
destroy(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The Komet's size on the card (guide §8). */
|
|
33
|
+
const KOMET_SIZE = "64";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `<section class="cd-shell-empty-state" data-chrome="empty-state" aria-labelledby>` (guide §8):
|
|
37
|
+
* centred in the container, `min(400px, 100cqw − 24px)` wide, glass, `shadow-2`, radius `lg`,
|
|
38
|
+
* padding `32px 40px`, content centred with 16px gaps: `<img src=komet width=64 height=64
|
|
39
|
+
* alt="">`, `<h1>` title 21px/600, help 16px/1.6 `--cd-ui-text-leise`, then the buttons side by
|
|
40
|
+
* side with 8px gap, 36px high (`cd-shell-button`): primary "New diagram" first, ghost "Show
|
|
41
|
+
* example", then the drop hint 13px `--cd-ui-text-leise`. With the File group's "Open…" the hint
|
|
42
|
+
* ends in a text link `messages.openFileLink` in `--cd-ui-link` (and its shortcut chip in Geist
|
|
43
|
+
* Mono): not a third action, the keyboard's way to open a file. Opening keeps the card until the
|
|
44
|
+
* modeler leaves it (`setEmptyState(false)` after the file loaded). Layer 20, `hidden` until
|
|
45
|
+
* `setShown(true)`.
|
|
46
|
+
*/
|
|
47
|
+
export function createEmptyState(context: EmptyStateContext): EmptyStateController {
|
|
48
|
+
const { ownerDocument, messages, options, dropHint, open, titleId, onLeave } = context;
|
|
49
|
+
|
|
50
|
+
const button = (variant: "primary" | "ghost", label: string, run: () => void): HTMLButtonElement => {
|
|
51
|
+
const element = createElement(
|
|
52
|
+
ownerDocument,
|
|
53
|
+
"button",
|
|
54
|
+
{ type: "button", class: "cd-shell-button", "data-variant": variant },
|
|
55
|
+
label,
|
|
56
|
+
);
|
|
57
|
+
element.addEventListener("click", () => {
|
|
58
|
+
onLeave();
|
|
59
|
+
run();
|
|
60
|
+
});
|
|
61
|
+
return element;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const onShowExample = options.onShowExample;
|
|
65
|
+
const newDiagram = button("primary", messages.newDiagram, () => options.onNewDiagram());
|
|
66
|
+
const element = createElement(
|
|
67
|
+
ownerDocument,
|
|
68
|
+
"section",
|
|
69
|
+
{ class: "cd-shell-empty-state", "data-chrome": "empty-state", "aria-labelledby": titleId },
|
|
70
|
+
// Dragging the mark would start an image drag that some browsers carry as a file.
|
|
71
|
+
createElement(ownerDocument, "img", {
|
|
72
|
+
class: "cd-shell-empty-state-mark",
|
|
73
|
+
src: options.komet,
|
|
74
|
+
width: KOMET_SIZE,
|
|
75
|
+
height: KOMET_SIZE,
|
|
76
|
+
alt: "",
|
|
77
|
+
draggable: "false",
|
|
78
|
+
}),
|
|
79
|
+
createElement(ownerDocument, "h1", { class: "cd-shell-empty-state-title", id: titleId }, options.title),
|
|
80
|
+
createElement(ownerDocument, "p", { class: "cd-shell-empty-state-text" }, options.text),
|
|
81
|
+
createElement(
|
|
82
|
+
ownerDocument,
|
|
83
|
+
"div",
|
|
84
|
+
{ class: "cd-shell-empty-state-actions" },
|
|
85
|
+
newDiagram,
|
|
86
|
+
onShowExample && button("ghost", messages.showExample, () => onShowExample()),
|
|
87
|
+
),
|
|
88
|
+
hintParagraph(),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
function hintParagraph(): HTMLElement | undefined {
|
|
92
|
+
const hasDropHint = dropHint !== undefined && dropHint !== "";
|
|
93
|
+
if (!hasDropHint && !open) return undefined;
|
|
94
|
+
const paragraph = createElement(ownerDocument, "p", { class: "cd-shell-empty-state-hint" });
|
|
95
|
+
if (hasDropHint) paragraph.append(dropHint);
|
|
96
|
+
if (!open) return paragraph;
|
|
97
|
+
const shortcut = open.shortcut?.trim();
|
|
98
|
+
const link = createElement(
|
|
99
|
+
ownerDocument,
|
|
100
|
+
"button",
|
|
101
|
+
{
|
|
102
|
+
type: "button",
|
|
103
|
+
class: "cd-shell-empty-state-open",
|
|
104
|
+
"aria-keyshortcuts": shortcut ? ariaKeyShortcutsFromLabel(shortcut) : undefined,
|
|
105
|
+
"aria-haspopup": open.dialog ? "dialog" : undefined,
|
|
106
|
+
},
|
|
107
|
+
messages.openFileLink,
|
|
108
|
+
);
|
|
109
|
+
link.addEventListener("click", () => open.onSelect());
|
|
110
|
+
if (hasDropHint) {
|
|
111
|
+
paragraph.append(" ", createElement(ownerDocument, "span", { "aria-hidden": "true" }, "·"), " ");
|
|
112
|
+
}
|
|
113
|
+
// The link and its chip wrap as one: a chip alone on a line names no action.
|
|
114
|
+
const openItem = createElement(ownerDocument, "span", { class: "cd-shell-empty-state-open-item" }, link);
|
|
115
|
+
if (shortcut) {
|
|
116
|
+
openItem.append(
|
|
117
|
+
" ",
|
|
118
|
+
createElement(
|
|
119
|
+
ownerDocument,
|
|
120
|
+
"kbd",
|
|
121
|
+
{ class: "cd-shell-empty-state-key", "aria-hidden": "true" },
|
|
122
|
+
shortcut,
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
paragraph.append(openItem);
|
|
127
|
+
return paragraph;
|
|
128
|
+
}
|
|
129
|
+
element.hidden = true;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
element,
|
|
133
|
+
isShown: () => !element.hidden,
|
|
134
|
+
setShown(shown) {
|
|
135
|
+
element.hidden = !shown;
|
|
136
|
+
},
|
|
137
|
+
focus() {
|
|
138
|
+
newDiagram.focus({ preventScroll: true });
|
|
139
|
+
},
|
|
140
|
+
destroy() {
|
|
141
|
+
element.remove();
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ShellMessages } from "../../core/index.js";
|
|
2
|
+
import { createElement } from "../element.js";
|
|
3
|
+
import type { LegalLineOptions } from "../types.js";
|
|
4
|
+
|
|
5
|
+
/** The Miragon legal pages (corporate-identity, skill docs-site, `miragon/legal.ts`). */
|
|
6
|
+
export const MIRAGON_HOME = "https://www.miragon.io/";
|
|
7
|
+
export const MIRAGON_IMPRESSUM = "https://www.miragon.io/impressum/";
|
|
8
|
+
export const MIRAGON_DATENSCHUTZ = "https://www.miragon.io/datenschutz/";
|
|
9
|
+
|
|
10
|
+
export interface LegalLineContext {
|
|
11
|
+
ownerDocument: Document;
|
|
12
|
+
messages: ShellMessages;
|
|
13
|
+
options: LegalLineOptions;
|
|
14
|
+
/** `cd-shell-legal-N`: the prefix of the id the links' `aria-describedby` names. */
|
|
15
|
+
idPrefix: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface LegalLineController {
|
|
19
|
+
/** The `<nav data-chrome="legal">`. */
|
|
20
|
+
readonly element: HTMLElement;
|
|
21
|
+
destroy(): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `<nav class="cd-shell-legal" data-chrome="legal" aria-label="Legal">` (guide §5.10, §2): one
|
|
26
|
+
* line, 13px, line height 20px, at `left: 12px; bottom: 12px`, glass without border and shadow,
|
|
27
|
+
* padding `0 8px`, radius `sm`, layer 20; in a container too narrow for it, it wraps between its
|
|
28
|
+
* entries and grows upwards. Links in `--cd-ui-link`, `target="_blank" rel="noopener noreferrer"`
|
|
29
|
+
* and `aria-describedby` a hidden `messages.opensInNewTab`, each after the first in a
|
|
30
|
+
* `<span class="cd-shell-legal-item">` with its separator `<span aria-hidden="true">·</span>`:
|
|
31
|
+
* `messages.copyright(year)` → `MIRAGON_HOME`, "Impressum" → `MIRAGON_IMPRESSUM` and "Datenschutz"
|
|
32
|
+
* → `MIRAGON_DATENSCHUTZ` (both `lang="de"`, never translated), then `options.links`.
|
|
33
|
+
*/
|
|
34
|
+
export function createLegalLine(context: LegalLineContext): LegalLineController {
|
|
35
|
+
const { ownerDocument, messages, options } = context;
|
|
36
|
+
const year = options.year ?? new Date().getFullYear();
|
|
37
|
+
|
|
38
|
+
// Every link opens a new tab; screen readers hear that from one shared description.
|
|
39
|
+
const newTabId = `${context.idPrefix}-new-tab`;
|
|
40
|
+
const link = (label: string, href: string, lang?: string): HTMLAnchorElement =>
|
|
41
|
+
createElement(
|
|
42
|
+
ownerDocument,
|
|
43
|
+
"a",
|
|
44
|
+
{ href, lang, target: "_blank", rel: "noopener noreferrer", "aria-describedby": newTabId },
|
|
45
|
+
label,
|
|
46
|
+
);
|
|
47
|
+
const separator = (): HTMLElement => createElement(ownerDocument, "span", { "aria-hidden": "true" }, "·");
|
|
48
|
+
const item = (anchor: HTMLAnchorElement): HTMLElement =>
|
|
49
|
+
createElement(ownerDocument, "span", { class: "cd-shell-legal-item" }, separator(), anchor);
|
|
50
|
+
|
|
51
|
+
// The legal pages are German whatever the tool's language (guide §2), so their names are too.
|
|
52
|
+
const links = [
|
|
53
|
+
link(messages.copyright(year), MIRAGON_HOME),
|
|
54
|
+
link("Impressum", MIRAGON_IMPRESSUM, "de"),
|
|
55
|
+
link("Datenschutz", MIRAGON_DATENSCHUTZ, "de"),
|
|
56
|
+
...(options.links ?? []).map((extra) => link(extra.label, extra.href, extra.lang)),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const element = createElement(ownerDocument, "nav", {
|
|
60
|
+
class: "cd-shell-legal",
|
|
61
|
+
"data-chrome": "legal",
|
|
62
|
+
"aria-label": messages.legal,
|
|
63
|
+
});
|
|
64
|
+
element.append(
|
|
65
|
+
...links.map((anchor, index) => (index === 0 ? anchor : item(anchor))),
|
|
66
|
+
createElement(ownerDocument, "span", { id: newTabId, hidden: "" }, messages.opensInNewTab),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
element,
|
|
71
|
+
destroy() {
|
|
72
|
+
element.remove();
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { parseAppearanceChoice, type ShellMessages } from "../../core/index.js";
|
|
2
|
+
import type { ShellIconName } from "../icons.js";
|
|
3
|
+
import type { AppearanceHandle, FileMenuAction, FileMenuOptions, MenuEntry } from "../types.js";
|
|
4
|
+
|
|
5
|
+
interface FileEntrySpec {
|
|
6
|
+
option: keyof FileMenuOptions;
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
icon: ShellIconName | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function fileEntry(spec: FileEntrySpec, action: FileMenuAction): MenuEntry {
|
|
13
|
+
const common = {
|
|
14
|
+
id: spec.id,
|
|
15
|
+
label: spec.label,
|
|
16
|
+
...(spec.icon ? { icon: spec.icon } : {}),
|
|
17
|
+
...(action.disabled ? { disabled: action.disabled } : {}),
|
|
18
|
+
...(action.shortcut ? { shortcut: action.shortcut } : {}),
|
|
19
|
+
onSelect: () => action.onSelect(),
|
|
20
|
+
};
|
|
21
|
+
return action.dialog ? { kind: "dialog", ...common } : { kind: "action", ...common };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The File group (guide §5.4, §2), first in the menu: "New diagram" (`file-plus`, id
|
|
26
|
+
* `new-diagram`), "Open…" (`folder-open`, `open`), "Show example" (no icon, `show-example`),
|
|
27
|
+
* "Import…" (`import`, `import`), in this order and only those `options` gives. `action` entries
|
|
28
|
+
* (`dialog` with `FileMenuAction.dialog`) with the action's `shortcut` and `disabled`. Never
|
|
29
|
+
* locked on a read-only canvas.
|
|
30
|
+
*/
|
|
31
|
+
export function fileMenuEntries(options: FileMenuOptions, messages: ShellMessages): readonly MenuEntry[] {
|
|
32
|
+
const specs: readonly FileEntrySpec[] = [
|
|
33
|
+
{ option: "newDiagram", id: "new-diagram", label: messages.newDiagram, icon: "file-plus" },
|
|
34
|
+
{ option: "open", id: "open", label: messages.openFile, icon: "folder-open" },
|
|
35
|
+
// §5.14 gives "Show example" no icon.
|
|
36
|
+
{ option: "showExample", id: "show-example", label: messages.showExample, icon: undefined },
|
|
37
|
+
{ option: "import", id: "import", label: messages.importFile, icon: "import" },
|
|
38
|
+
];
|
|
39
|
+
return specs.flatMap((spec) => {
|
|
40
|
+
const action = options[spec.option];
|
|
41
|
+
return action ? [fileEntry(spec, action)] : [];
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The Appearance group (guide §5.4, §13.3) between Export and Help: one radio group, id
|
|
47
|
+
* `appearance`, label `messages.appearance`, options `system`, `light`, `dark` labelled
|
|
48
|
+
* `messages.appearanceSystem` / `Light` / `Dark`, value `appearance.choice()`, selecting calls
|
|
49
|
+
* `appearance.setChoice`. Never locked. The shell asks for it only when `themes` includes `"dark"`.
|
|
50
|
+
*/
|
|
51
|
+
export function appearanceMenuEntries(
|
|
52
|
+
appearance: AppearanceHandle,
|
|
53
|
+
messages: ShellMessages,
|
|
54
|
+
): readonly MenuEntry[] {
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
kind: "radio",
|
|
58
|
+
id: "appearance",
|
|
59
|
+
label: messages.appearance,
|
|
60
|
+
options: [
|
|
61
|
+
{ value: "system", label: messages.appearanceSystem },
|
|
62
|
+
{ value: "light", label: messages.appearanceLight },
|
|
63
|
+
{ value: "dark", label: messages.appearanceDark },
|
|
64
|
+
],
|
|
65
|
+
value: () => appearance.choice(),
|
|
66
|
+
onSelect: (value) => appearance.setChoice(parseAppearanceChoice(value)),
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { SHELL, type PrimaryWidths, type ShellMessages } from "../../core/index.js";
|
|
2
|
+
import { createElement, toggleAttribute } from "../element.js";
|
|
3
|
+
import { iconElement } from "../icons.js";
|
|
4
|
+
import type { TooltipController } from "../tooltip.js";
|
|
5
|
+
import type { PrimaryActionHandle, PrimaryActionOptions } from "../types.js";
|
|
6
|
+
|
|
7
|
+
export interface PrimaryActionContext {
|
|
8
|
+
ownerDocument: Document;
|
|
9
|
+
messages: ShellMessages;
|
|
10
|
+
tooltip: TooltipController;
|
|
11
|
+
options: PrimaryActionOptions;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PrimaryActionController extends PrimaryActionHandle {
|
|
15
|
+
/**
|
|
16
|
+
* Border-box widths with and without the label for `decideTopRow`, measured also while the label
|
|
17
|
+
* is collapsed (the label's width does not change with the state), cached until `refresh()`.
|
|
18
|
+
*/
|
|
19
|
+
widths(): PrimaryWidths;
|
|
20
|
+
/** Icon only (`true`) or icon and label; the `aria-label` stays, the tooltip shows only while collapsed. */
|
|
21
|
+
setCollapsed(collapsed: boolean): void;
|
|
22
|
+
destroy(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** On the button while it shows its icon only: the stylesheet's 40 × 40 (the root mirrors it as `data-primary`). */
|
|
26
|
+
const COLLAPSED_ATTRIBUTE = "data-collapsed";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `<button type="button" class="cd-shell-primary cd-shell-button" data-variant="primary"
|
|
30
|
+
* data-chrome="primary" aria-label="Share">` with the 16px icon and `<span
|
|
31
|
+
* class="cd-shell-primary-label">` (guide §5.7): 40px high at `top: 12px; right: 12px`, layer 30,
|
|
32
|
+
* blue (`dom/buttons.css`), hover lift. Collapsed: 40 × 40, label hidden, chrome tooltip with the
|
|
33
|
+
* label (no `aria-describedby`, it repeats the name). `disabled()` → `aria-disabled`, no action.
|
|
34
|
+
*/
|
|
35
|
+
export function createPrimaryAction(context: PrimaryActionContext): PrimaryActionController {
|
|
36
|
+
const { ownerDocument, messages, tooltip, options } = context;
|
|
37
|
+
const label = options.label ?? messages.share;
|
|
38
|
+
const labelElement = createElement(ownerDocument, "span", { class: "cd-shell-primary-label" }, label);
|
|
39
|
+
const element = createElement(
|
|
40
|
+
ownerDocument,
|
|
41
|
+
"button",
|
|
42
|
+
{
|
|
43
|
+
type: "button",
|
|
44
|
+
class: "cd-shell-primary cd-shell-button",
|
|
45
|
+
"data-variant": "primary",
|
|
46
|
+
"data-chrome": "primary",
|
|
47
|
+
"aria-label": label,
|
|
48
|
+
},
|
|
49
|
+
iconElement(options.icon ?? "share-2"),
|
|
50
|
+
labelElement,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
let collapsed = false;
|
|
54
|
+
let disabled = false;
|
|
55
|
+
let labelledWidth: number | undefined;
|
|
56
|
+
|
|
57
|
+
function readDisabled(): void {
|
|
58
|
+
disabled = options.disabled?.() ?? false;
|
|
59
|
+
toggleAttribute(element, "aria-disabled", disabled ? "true" : null);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function showCollapsed(state: boolean): void {
|
|
63
|
+
labelElement.hidden = state;
|
|
64
|
+
toggleAttribute(element, COLLAPSED_ATTRIBUTE, state ? "" : null);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The labelled width; while collapsed the label shows for one synchronous layout, never painted. */
|
|
68
|
+
function measureLabelledWidth(): number {
|
|
69
|
+
if (!collapsed) return element.offsetWidth;
|
|
70
|
+
showCollapsed(false);
|
|
71
|
+
const width = element.offsetWidth;
|
|
72
|
+
showCollapsed(true);
|
|
73
|
+
return width;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isEngaged(): boolean {
|
|
77
|
+
if (ownerDocument.activeElement === element) return true;
|
|
78
|
+
try {
|
|
79
|
+
return element.matches(":hover");
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const onClick = (event: MouseEvent): void => {
|
|
86
|
+
if (disabled) {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
options.onSelect();
|
|
91
|
+
};
|
|
92
|
+
element.addEventListener("click", onClick);
|
|
93
|
+
const detachTooltip = tooltip.attach(element, () => (collapsed ? { label } : null));
|
|
94
|
+
readDisabled();
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
element,
|
|
98
|
+
isCollapsed: () => collapsed,
|
|
99
|
+
refresh() {
|
|
100
|
+
readDisabled();
|
|
101
|
+
labelledWidth = undefined;
|
|
102
|
+
},
|
|
103
|
+
widths() {
|
|
104
|
+
// Measured fresh while the label shows (a late web font widens it), from the cache while collapsed.
|
|
105
|
+
if (!collapsed || labelledWidth === undefined) {
|
|
106
|
+
const measured = measureLabelledWidth();
|
|
107
|
+
if (measured > 0) labelledWidth = measured;
|
|
108
|
+
}
|
|
109
|
+
return { withLabel: labelledWidth ?? 0, iconOnly: SHELL.row };
|
|
110
|
+
},
|
|
111
|
+
setCollapsed(next) {
|
|
112
|
+
if (next === collapsed) return;
|
|
113
|
+
collapsed = next;
|
|
114
|
+
showCollapsed(next);
|
|
115
|
+
// An expanded button names itself; a tooltip still showing for it would only repeat the label.
|
|
116
|
+
if (!next && isEngaged()) tooltip.hide();
|
|
117
|
+
},
|
|
118
|
+
destroy() {
|
|
119
|
+
detachTooltip();
|
|
120
|
+
element.removeEventListener("click", onClick);
|
|
121
|
+
element.remove();
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|