@pindoba/astro-menu 0.0.0-alpha-20260829223243
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +12 -0
- package/index.ts +9 -0
- package/package.json +67 -0
- package/src/__test__/boot-menu.test.ts +917 -0
- package/src/__test__/runtime.test.ts +57 -0
- package/src/boot-menu.ts +598 -0
- package/src/demos/account.astro +55 -0
- package/src/demos/checkbox-radio.astro +77 -0
- package/src/demos/controlled.astro +67 -0
- package/src/demos/danger.astro +50 -0
- package/src/demos/default.astro +151 -0
- package/src/demos/index.ts +7 -0
- package/src/demos/rich-items.astro +105 -0
- package/src/demos/submenu.astro +60 -0
- package/src/env.d.ts +2 -0
- package/src/menu.astro +396 -0
- package/src/runtime.ts +102 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Menu from "../menu.astro";
|
|
3
|
+
import Stamp from "@pindoba/astro-stamp";
|
|
4
|
+
import { LogOut, ChevronDown } from "@lucide/astro";
|
|
5
|
+
import { css } from "@pindoba/styled-system/css";
|
|
6
|
+
import { stack } from "@pindoba/styled-system/patterns";
|
|
7
|
+
import type { MenuNode } from "@pindoba/core-menu";
|
|
8
|
+
|
|
9
|
+
// The non-interactive account header is a `type:"custom"` node. Astro can't
|
|
10
|
+
// carry components in `items` (they're JSON-serialized for hydration), so the
|
|
11
|
+
// content is authored as a NAMED slot keyed by the node id (`account`) — the
|
|
12
|
+
// same convention the rich-item `<id>:leading` slots use. Menu forwards it down
|
|
13
|
+
// to the ListBox via `customSlotsHtml`. The block is skipped by keyboard
|
|
14
|
+
// navigation, so arrows land straight on "Sair".
|
|
15
|
+
const items: MenuNode[] = [
|
|
16
|
+
{ type: "custom", id: "account", "aria-label": "Signed in as Demo Cubos" },
|
|
17
|
+
{ type: "separator", id: "s1" },
|
|
18
|
+
{ type: "action", id: "sign-out", label: "Sair", feedback: "danger" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const nameClass = css({ fontWeight: "bold", color: "colorPalette.text.bold" });
|
|
22
|
+
const emailClass = css({ fontSize: "sm", color: "colorPalette.text.muted" });
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
|
|
26
|
+
<Menu id="astro-menu-account" items={items} aria-label="Account menu">
|
|
27
|
+
Demo Cubos
|
|
28
|
+
<ChevronDown />
|
|
29
|
+
|
|
30
|
+
<Fragment slot="account">
|
|
31
|
+
<span class={nameClass}>Demo Cubos</span>
|
|
32
|
+
<span class={emailClass}>demo@cubos.dev</span>
|
|
33
|
+
</Fragment>
|
|
34
|
+
|
|
35
|
+
<Stamp slot="sign-out:leading" emphasis="ghost"><LogOut /></Stamp>
|
|
36
|
+
</Menu>
|
|
37
|
+
<p id="astro-menu-account-status">Last action: —</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<script>
|
|
41
|
+
function initAccountMenuDemo() {
|
|
42
|
+
const root = document.getElementById("astro-menu-account");
|
|
43
|
+
const status = document.getElementById("astro-menu-account-status");
|
|
44
|
+
if (!root || !status || root.dataset.demoWired) return;
|
|
45
|
+
root.dataset.demoWired = "true";
|
|
46
|
+
|
|
47
|
+
root.addEventListener("pindoba:menu:action", (e) => {
|
|
48
|
+
const { id } = (e as CustomEvent<{ id: string }>).detail;
|
|
49
|
+
status.textContent = `Last action: ${id === "sign-out" ? "Sair" : id}`;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
initAccountMenuDemo();
|
|
54
|
+
document.addEventListener("astro:page-load", initAccountMenuDemo);
|
|
55
|
+
</script>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Menu from "../menu.astro";
|
|
3
|
+
import { stack } from "@pindoba/styled-system/patterns";
|
|
4
|
+
import type { MenuNode } from "@pindoba/core-menu";
|
|
5
|
+
|
|
6
|
+
// NOTE (Astro): the Svelte demo's leading <Check> / spacer snippets are omitted
|
|
7
|
+
// (see default.astro). Checked/selected state is server-rendered via
|
|
8
|
+
// aria-checked and mirrored into the status line below via events.
|
|
9
|
+
const items: MenuNode[] = [
|
|
10
|
+
{
|
|
11
|
+
type: "group",
|
|
12
|
+
id: "view",
|
|
13
|
+
label: "View",
|
|
14
|
+
items: [
|
|
15
|
+
{ type: "checkbox", id: "wrap", label: "Word wrap", checked: true },
|
|
16
|
+
{ type: "checkbox", id: "minimap", label: "Minimap", checked: false },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
{ type: "separator", id: "s1" },
|
|
20
|
+
{
|
|
21
|
+
type: "radiogroup",
|
|
22
|
+
id: "sort",
|
|
23
|
+
label: "Sort by",
|
|
24
|
+
value: "name",
|
|
25
|
+
items: [
|
|
26
|
+
{ type: "radio", id: "sort-name", label: "Name", value: "name" },
|
|
27
|
+
{ type: "radio", id: "sort-date", label: "Date modified", value: "date" },
|
|
28
|
+
{ type: "radio", id: "sort-size", label: "Size", value: "size" },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
|
|
35
|
+
<Menu id="astro-menu-checkbox-radio" items={items} aria-label="View options">
|
|
36
|
+
View options
|
|
37
|
+
</Menu>
|
|
38
|
+
<p id="astro-menu-checkbox-radio-status">
|
|
39
|
+
Word wrap: on · Minimap: off · Sort: name
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<script>
|
|
44
|
+
function initCheckboxRadioMenuDemo() {
|
|
45
|
+
const root = document.getElementById("astro-menu-checkbox-radio");
|
|
46
|
+
const status = document.getElementById("astro-menu-checkbox-radio-status");
|
|
47
|
+
if (!root || !status || root.dataset.demoWired) return;
|
|
48
|
+
root.dataset.demoWired = "true";
|
|
49
|
+
|
|
50
|
+
const state = { wrap: true, minimap: false, sort: "name" };
|
|
51
|
+
const paint = () => {
|
|
52
|
+
status.textContent = `Word wrap: ${state.wrap ? "on" : "off"} · Minimap: ${
|
|
53
|
+
state.minimap ? "on" : "off"
|
|
54
|
+
} · Sort: ${state.sort}`;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
root.addEventListener("pindoba:menu:checkedchange", (e) => {
|
|
58
|
+
const { id, checked } = (
|
|
59
|
+
e as CustomEvent<{ id: string; checked: boolean }>
|
|
60
|
+
).detail;
|
|
61
|
+
if (id === "wrap") state.wrap = checked;
|
|
62
|
+
if (id === "minimap") state.minimap = checked;
|
|
63
|
+
paint();
|
|
64
|
+
});
|
|
65
|
+
root.addEventListener("pindoba:menu:valuechange", (e) => {
|
|
66
|
+
const { groupId, value } = (
|
|
67
|
+
e as CustomEvent<{ groupId: string; value: string }>
|
|
68
|
+
).detail;
|
|
69
|
+
if (groupId === "sort") state.sort = value;
|
|
70
|
+
paint();
|
|
71
|
+
});
|
|
72
|
+
paint();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
initCheckboxRadioMenuDemo();
|
|
76
|
+
document.addEventListener("astro:page-load", initCheckboxRadioMenuDemo);
|
|
77
|
+
</script>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Menu from "../menu.astro";
|
|
3
|
+
import Button from "@pindoba/astro-button";
|
|
4
|
+
import { stack } from "@pindoba/styled-system/patterns";
|
|
5
|
+
import type { MenuNode } from "@pindoba/core-menu";
|
|
6
|
+
|
|
7
|
+
// The open state is driven externally via the imperative `getMenu(root)` handle
|
|
8
|
+
// (Astro's equivalent of Svelte's bindable `open`); the menu also emits
|
|
9
|
+
// `pindoba:menu:openchange` so external UI can mirror it.
|
|
10
|
+
const items: MenuNode[] = [
|
|
11
|
+
{ type: "action", id: "new", label: "New file" },
|
|
12
|
+
{ type: "action", id: "open", label: "Open…" },
|
|
13
|
+
{ type: "action", id: "close", label: "Close" },
|
|
14
|
+
];
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
|
|
18
|
+
<Button id="astro-menu-controlled-toggle" emphasis="secondary">
|
|
19
|
+
Toggle externally
|
|
20
|
+
</Button>
|
|
21
|
+
|
|
22
|
+
<Menu id="astro-menu-controlled" items={items} aria-label="File actions">
|
|
23
|
+
Actions
|
|
24
|
+
</Menu>
|
|
25
|
+
|
|
26
|
+
<p id="astro-menu-controlled-status">Menu is closed · Last action: —</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<script>
|
|
30
|
+
import { getMenu } from "../runtime";
|
|
31
|
+
|
|
32
|
+
const LABELS: Record<string, string> = {
|
|
33
|
+
new: "New file",
|
|
34
|
+
open: "Open",
|
|
35
|
+
close: "Close",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function initControlledMenuDemo() {
|
|
39
|
+
const root = document.getElementById("astro-menu-controlled");
|
|
40
|
+
const toggle = document.getElementById("astro-menu-controlled-toggle");
|
|
41
|
+
const status = document.getElementById("astro-menu-controlled-status");
|
|
42
|
+
if (!root || !toggle || !status || root.dataset.demoWired) return;
|
|
43
|
+
root.dataset.demoWired = "true";
|
|
44
|
+
|
|
45
|
+
const state = { open: false, last: "—" };
|
|
46
|
+
const paint = () => {
|
|
47
|
+
status.textContent = `Menu is ${
|
|
48
|
+
state.open ? "open" : "closed"
|
|
49
|
+
} · Last action: ${state.last}`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
toggle.addEventListener("click", () => getMenu(root)?.toggle());
|
|
53
|
+
root.addEventListener("pindoba:menu:openchange", (e) => {
|
|
54
|
+
state.open = (e as CustomEvent<{ open: boolean }>).detail.open;
|
|
55
|
+
paint();
|
|
56
|
+
});
|
|
57
|
+
root.addEventListener("pindoba:menu:action", (e) => {
|
|
58
|
+
const { id } = (e as CustomEvent<{ id: string }>).detail;
|
|
59
|
+
state.last = LABELS[id] ?? id;
|
|
60
|
+
paint();
|
|
61
|
+
});
|
|
62
|
+
paint();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
initControlledMenuDemo();
|
|
66
|
+
document.addEventListener("astro:page-load", initControlledMenuDemo);
|
|
67
|
+
</script>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Menu from "../menu.astro";
|
|
3
|
+
import { stack } from "@pindoba/styled-system/patterns";
|
|
4
|
+
import type { MenuNode } from "@pindoba/core-menu";
|
|
5
|
+
|
|
6
|
+
// The trailing ⌫ <Kbd> from the Svelte demo is omitted (see default.astro);
|
|
7
|
+
// the danger tone is driven server-side via the item's `feedback: "danger"`.
|
|
8
|
+
const items: MenuNode[] = [
|
|
9
|
+
{ type: "action", id: "rename", label: "Rename" },
|
|
10
|
+
{ type: "action", id: "duplicate", label: "Duplicate" },
|
|
11
|
+
{ type: "separator", id: "s1" },
|
|
12
|
+
{
|
|
13
|
+
type: "action",
|
|
14
|
+
id: "delete",
|
|
15
|
+
label: "Delete…",
|
|
16
|
+
feedback: "danger",
|
|
17
|
+
shortcut: "⌫",
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
|
|
23
|
+
<Menu id="astro-menu-danger" items={items} aria-label="Item actions">
|
|
24
|
+
Actions
|
|
25
|
+
</Menu>
|
|
26
|
+
<p id="astro-menu-danger-status">Last action: —</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<script>
|
|
30
|
+
const LABELS: Record<string, string> = {
|
|
31
|
+
rename: "Rename",
|
|
32
|
+
duplicate: "Duplicate",
|
|
33
|
+
delete: "Delete…",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function initDangerMenuDemo() {
|
|
37
|
+
const root = document.getElementById("astro-menu-danger");
|
|
38
|
+
const status = document.getElementById("astro-menu-danger-status");
|
|
39
|
+
if (!root || !status || root.dataset.demoWired) return;
|
|
40
|
+
root.dataset.demoWired = "true";
|
|
41
|
+
|
|
42
|
+
root.addEventListener("pindoba:menu:action", (e) => {
|
|
43
|
+
const { id } = (e as CustomEvent<{ id: string }>).detail;
|
|
44
|
+
status.textContent = `Last action: ${LABELS[id] ?? id}`;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
initDangerMenuDemo();
|
|
49
|
+
document.addEventListener("astro:page-load", initDangerMenuDemo);
|
|
50
|
+
</script>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Menu from "../menu.astro";
|
|
3
|
+
import Stamp from "@pindoba/astro-stamp";
|
|
4
|
+
import {
|
|
5
|
+
FilePlus,
|
|
6
|
+
FolderOpen,
|
|
7
|
+
Share2,
|
|
8
|
+
Link,
|
|
9
|
+
Mail,
|
|
10
|
+
Users,
|
|
11
|
+
BookOpen,
|
|
12
|
+
Trash2,
|
|
13
|
+
} from "@lucide/astro";
|
|
14
|
+
import { stack } from "@pindoba/styled-system/patterns";
|
|
15
|
+
import type { MenuNode } from "@pindoba/core-menu";
|
|
16
|
+
|
|
17
|
+
// NOTE (Astro): trailing <Kbd> shortcuts that the Svelte/React/Vue demos
|
|
18
|
+
// render as real components are represented here via the `shortcut` string
|
|
19
|
+
// prop instead — Astro's item data is JSON-serialized for hydration, so
|
|
20
|
+
// components can't ride along in `items`. Leading icons ARE forwarded, the
|
|
21
|
+
// same way rich-items.astro does it: named slots on <Menu> keyed
|
|
22
|
+
// `<id>:leading`, which Menu resolves per level (including nested submenus)
|
|
23
|
+
// via `optionSlotsHtml`. Checkbox/radio items already render a built-in
|
|
24
|
+
// check indicator, so they're left without a leading icon here. Checked/
|
|
25
|
+
// selected state is conveyed via aria-checked (server-rendered) and
|
|
26
|
+
// reflected in the status line below via events.
|
|
27
|
+
const items: MenuNode[] = [
|
|
28
|
+
{ type: "action", id: "new", label: "New file", shortcut: "⌘N" },
|
|
29
|
+
{ type: "action", id: "open", label: "Open…", shortcut: "⌘O" },
|
|
30
|
+
{ type: "separator", id: "s1" },
|
|
31
|
+
{
|
|
32
|
+
type: "group",
|
|
33
|
+
id: "view",
|
|
34
|
+
label: "View",
|
|
35
|
+
items: [
|
|
36
|
+
{ type: "checkbox", id: "wrap", label: "Word wrap", checked: true },
|
|
37
|
+
{ type: "checkbox", id: "minimap", label: "Minimap", checked: false },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{ type: "separator", id: "s2" },
|
|
41
|
+
{
|
|
42
|
+
type: "radiogroup",
|
|
43
|
+
id: "sort",
|
|
44
|
+
label: "Sort by",
|
|
45
|
+
value: "name",
|
|
46
|
+
items: [
|
|
47
|
+
{ type: "radio", id: "sort-name", label: "Name", value: "name" },
|
|
48
|
+
{ type: "radio", id: "sort-date", label: "Date modified", value: "date" },
|
|
49
|
+
{ type: "radio", id: "sort-size", label: "Size", value: "size" },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{ type: "separator", id: "s3" },
|
|
53
|
+
{
|
|
54
|
+
type: "submenu",
|
|
55
|
+
id: "share",
|
|
56
|
+
label: "Share",
|
|
57
|
+
items: [
|
|
58
|
+
{ type: "action", id: "copy", label: "Copy link" },
|
|
59
|
+
{ type: "action", id: "email", label: "Email" },
|
|
60
|
+
{
|
|
61
|
+
type: "submenu",
|
|
62
|
+
id: "social",
|
|
63
|
+
label: "Social",
|
|
64
|
+
items: [
|
|
65
|
+
{ type: "action", id: "x", label: "X / Twitter" },
|
|
66
|
+
{ type: "action", id: "ln", label: "LinkedIn" },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{ type: "separator", id: "s4" },
|
|
72
|
+
{
|
|
73
|
+
type: "link",
|
|
74
|
+
id: "docs",
|
|
75
|
+
label: "Documentation",
|
|
76
|
+
href: "https://pindoba.js.org",
|
|
77
|
+
target: "_blank",
|
|
78
|
+
},
|
|
79
|
+
{ type: "action", id: "del", label: "Delete", feedback: "danger" },
|
|
80
|
+
];
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
|
|
84
|
+
<Menu id="astro-menu-default" items={items} aria-label="File actions">
|
|
85
|
+
Actions
|
|
86
|
+
|
|
87
|
+
<Stamp slot="new:leading" size="sm" emphasis="ghost"><FilePlus /></Stamp>
|
|
88
|
+
<Stamp slot="open:leading" size="sm" emphasis="ghost"><FolderOpen /></Stamp>
|
|
89
|
+
<Stamp slot="share:leading" size="sm" emphasis="ghost"><Share2 /></Stamp>
|
|
90
|
+
<Stamp slot="copy:leading" size="sm" emphasis="ghost"><Link /></Stamp>
|
|
91
|
+
<Stamp slot="email:leading" size="sm" emphasis="ghost"><Mail /></Stamp>
|
|
92
|
+
<Stamp slot="social:leading" size="sm" emphasis="ghost"><Users /></Stamp>
|
|
93
|
+
<Stamp slot="docs:leading" size="sm" emphasis="ghost"><BookOpen /></Stamp>
|
|
94
|
+
<Stamp slot="del:leading" size="sm" emphasis="ghost"><Trash2 /></Stamp>
|
|
95
|
+
</Menu>
|
|
96
|
+
<p id="astro-menu-default-status">
|
|
97
|
+
Word wrap: on · Minimap: off · Sort: name · Last action: —
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<script>
|
|
102
|
+
const LABELS: Record<string, string> = {
|
|
103
|
+
new: "New file",
|
|
104
|
+
open: "Open",
|
|
105
|
+
copy: "Copy link",
|
|
106
|
+
email: "Email",
|
|
107
|
+
x: "X",
|
|
108
|
+
ln: "LinkedIn",
|
|
109
|
+
docs: "Documentation",
|
|
110
|
+
del: "Delete",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function initDefaultMenuDemo() {
|
|
114
|
+
const root = document.getElementById("astro-menu-default");
|
|
115
|
+
const status = document.getElementById("astro-menu-default-status");
|
|
116
|
+
if (!root || !status || root.dataset.demoWired) return;
|
|
117
|
+
root.dataset.demoWired = "true";
|
|
118
|
+
|
|
119
|
+
const state = { wrap: true, minimap: false, sort: "name", last: "—" };
|
|
120
|
+
const paint = () => {
|
|
121
|
+
status.textContent = `Word wrap: ${state.wrap ? "on" : "off"} · Minimap: ${
|
|
122
|
+
state.minimap ? "on" : "off"
|
|
123
|
+
} · Sort: ${state.sort} · Last action: ${state.last}`;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
root.addEventListener("pindoba:menu:action", (e) => {
|
|
127
|
+
const { id } = (e as CustomEvent<{ id: string }>).detail;
|
|
128
|
+
state.last = LABELS[id] ?? id;
|
|
129
|
+
paint();
|
|
130
|
+
});
|
|
131
|
+
root.addEventListener("pindoba:menu:checkedchange", (e) => {
|
|
132
|
+
const { id, checked } = (
|
|
133
|
+
e as CustomEvent<{ id: string; checked: boolean }>
|
|
134
|
+
).detail;
|
|
135
|
+
if (id === "wrap") state.wrap = checked;
|
|
136
|
+
if (id === "minimap") state.minimap = checked;
|
|
137
|
+
paint();
|
|
138
|
+
});
|
|
139
|
+
root.addEventListener("pindoba:menu:valuechange", (e) => {
|
|
140
|
+
const { groupId, value } = (
|
|
141
|
+
e as CustomEvent<{ groupId: string; value: string }>
|
|
142
|
+
).detail;
|
|
143
|
+
if (groupId === "sort") state.sort = value;
|
|
144
|
+
paint();
|
|
145
|
+
});
|
|
146
|
+
paint();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
initDefaultMenuDemo();
|
|
150
|
+
document.addEventListener("astro:page-load", initDefaultMenuDemo);
|
|
151
|
+
</script>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as Default } from "./default.astro";
|
|
2
|
+
export { default as CheckboxRadio } from "./checkbox-radio.astro";
|
|
3
|
+
export { default as Submenu } from "./submenu.astro";
|
|
4
|
+
export { default as Danger } from "./danger.astro";
|
|
5
|
+
export { default as Controlled } from "./controlled.astro";
|
|
6
|
+
export { default as RichItems } from "./rich-items.astro";
|
|
7
|
+
export { default as Account } from "./account.astro";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Menu from "../menu.astro";
|
|
3
|
+
import Stamp from "@pindoba/astro-stamp";
|
|
4
|
+
import Badge from "@pindoba/astro-badge";
|
|
5
|
+
import { Building2, Globe, Plus } from "@lucide/astro";
|
|
6
|
+
import { stack } from "@pindoba/styled-system/patterns";
|
|
7
|
+
import type { MenuNode } from "@pindoba/core-menu";
|
|
8
|
+
|
|
9
|
+
// Each item's content renders through ListBox's Banner, same as Svelte/React/
|
|
10
|
+
// Vue — Astro just authors it via per-item NAMED slots (`<id>:leading` /
|
|
11
|
+
// `<id>:trailing`) instead of passing components through `items` (Astro's
|
|
12
|
+
// item data is JSON-serialized for hydration, so components can't ride
|
|
13
|
+
// along). Menu forwards its own resolved slots down to whichever nested
|
|
14
|
+
// ListBox level actually renders that item — see `menu.astro`'s
|
|
15
|
+
// `optionSlotsHtml` — so this works identically for a submenu's items, not
|
|
16
|
+
// just the root level.
|
|
17
|
+
const items: MenuNode[] = [
|
|
18
|
+
{
|
|
19
|
+
type: "action",
|
|
20
|
+
id: "acme",
|
|
21
|
+
label: "Acme Inc",
|
|
22
|
+
description: "12 members",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: "action",
|
|
26
|
+
id: "globex",
|
|
27
|
+
label: "Globex Corp",
|
|
28
|
+
description: "48 members",
|
|
29
|
+
},
|
|
30
|
+
{ type: "separator", id: "s1" },
|
|
31
|
+
{
|
|
32
|
+
type: "submenu",
|
|
33
|
+
id: "more",
|
|
34
|
+
label: "More workspaces",
|
|
35
|
+
items: [
|
|
36
|
+
{
|
|
37
|
+
type: "action",
|
|
38
|
+
id: "initech",
|
|
39
|
+
label: "Initech",
|
|
40
|
+
description: "8 members",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: "action",
|
|
44
|
+
id: "umbrella",
|
|
45
|
+
label: "Umbrella Corp",
|
|
46
|
+
description: "120 members",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{ type: "separator", id: "s2" },
|
|
51
|
+
{ type: "action", id: "create", label: "Create workspace…" },
|
|
52
|
+
];
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
|
|
56
|
+
<Menu id="astro-menu-rich-items" items={items} aria-label="Switch workspace">
|
|
57
|
+
Switch workspace
|
|
58
|
+
|
|
59
|
+
<Stamp slot="acme:leading" size="md"><Building2 /></Stamp>
|
|
60
|
+
|
|
61
|
+
<Stamp slot="globex:leading" size="md"><Globe /></Stamp>
|
|
62
|
+
<Badge slot="globex:trailing" size="sm" feedback="success">Pro</Badge>
|
|
63
|
+
|
|
64
|
+
<Stamp slot="more:leading" size="md"><Building2 /></Stamp>
|
|
65
|
+
|
|
66
|
+
<Stamp slot="initech:leading" size="md"><Building2 /></Stamp>
|
|
67
|
+
|
|
68
|
+
<Stamp slot="umbrella:leading" size="md"><Globe /></Stamp>
|
|
69
|
+
|
|
70
|
+
<Stamp slot="create:leading" size="md"><Plus /></Stamp>
|
|
71
|
+
</Menu>
|
|
72
|
+
<p id="astro-menu-rich-items-status">Last action: —</p>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<script>
|
|
76
|
+
const LABELS: Record<string, string> = {
|
|
77
|
+
acme: "Acme Inc",
|
|
78
|
+
globex: "Globex Corp",
|
|
79
|
+
initech: "Initech",
|
|
80
|
+
umbrella: "Umbrella Corp",
|
|
81
|
+
create: "Create workspace…",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function initRichItemsMenuDemo() {
|
|
85
|
+
const root = document.getElementById("astro-menu-rich-items");
|
|
86
|
+
const status = document.getElementById("astro-menu-rich-items-status");
|
|
87
|
+
if (!root || !status || root.dataset.demoWired) return;
|
|
88
|
+
root.dataset.demoWired = "true";
|
|
89
|
+
|
|
90
|
+
const state = { last: "—" };
|
|
91
|
+
const paint = () => {
|
|
92
|
+
status.textContent = `Last action: ${state.last}`;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
root.addEventListener("pindoba:menu:action", (e) => {
|
|
96
|
+
const { id } = (e as CustomEvent<{ id: string }>).detail;
|
|
97
|
+
state.last = LABELS[id] ?? id;
|
|
98
|
+
paint();
|
|
99
|
+
});
|
|
100
|
+
paint();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
initRichItemsMenuDemo();
|
|
104
|
+
document.addEventListener("astro:page-load", initRichItemsMenuDemo);
|
|
105
|
+
</script>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Menu from "../menu.astro";
|
|
3
|
+
import { stack } from "@pindoba/styled-system/patterns";
|
|
4
|
+
import type { MenuNode } from "@pindoba/core-menu";
|
|
5
|
+
|
|
6
|
+
const items: MenuNode[] = [
|
|
7
|
+
{ type: "action", id: "duplicate", label: "Duplicate" },
|
|
8
|
+
{ type: "separator", id: "s1" },
|
|
9
|
+
{
|
|
10
|
+
type: "submenu",
|
|
11
|
+
id: "share",
|
|
12
|
+
label: "Share",
|
|
13
|
+
items: [
|
|
14
|
+
{ type: "action", id: "copy", label: "Copy link" },
|
|
15
|
+
{ type: "action", id: "email", label: "Email" },
|
|
16
|
+
{
|
|
17
|
+
type: "submenu",
|
|
18
|
+
id: "social",
|
|
19
|
+
label: "Social",
|
|
20
|
+
items: [
|
|
21
|
+
{ type: "action", id: "x", label: "X / Twitter" },
|
|
22
|
+
{ type: "action", id: "ln", label: "LinkedIn" },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<div class={stack({ gap: "sm", alignItems: "flex-start" })}>
|
|
31
|
+
<Menu id="astro-menu-submenu" items={items} aria-label="Item actions">
|
|
32
|
+
Actions
|
|
33
|
+
</Menu>
|
|
34
|
+
<p id="astro-menu-submenu-status">Last action: —</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<script>
|
|
38
|
+
const LABELS: Record<string, string> = {
|
|
39
|
+
duplicate: "Duplicate",
|
|
40
|
+
copy: "Copy link",
|
|
41
|
+
email: "Email",
|
|
42
|
+
x: "X / Twitter",
|
|
43
|
+
ln: "LinkedIn",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function initSubmenuMenuDemo() {
|
|
47
|
+
const root = document.getElementById("astro-menu-submenu");
|
|
48
|
+
const status = document.getElementById("astro-menu-submenu-status");
|
|
49
|
+
if (!root || !status || root.dataset.demoWired) return;
|
|
50
|
+
root.dataset.demoWired = "true";
|
|
51
|
+
|
|
52
|
+
root.addEventListener("pindoba:menu:action", (e) => {
|
|
53
|
+
const { id } = (e as CustomEvent<{ id: string }>).detail;
|
|
54
|
+
status.textContent = `Last action: ${LABELS[id] ?? id}`;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
initSubmenuMenuDemo();
|
|
59
|
+
document.addEventListener("astro:page-load", initSubmenuMenuDemo);
|
|
60
|
+
</script>
|
package/src/env.d.ts
ADDED