@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.
@@ -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
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line @typescript-eslint/triple-slash-reference
2
+ /// <reference path="../.astro/types.d.ts" />