staffa 0.2.1 → 0.3.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/README.md +153 -127
- package/dist/components/autocomplete.js +3 -2
- package/dist/components/box.js +2 -2
- package/dist/components/button.d.ts +11 -2
- package/dist/components/button.js +36 -8
- package/dist/components/buttonChooser.d.ts +4 -5
- package/dist/components/buttonChooser.js +2 -2
- package/dist/components/buttonGroup.js +0 -3
- package/dist/components/field.js +0 -3
- package/dist/components/main.d.ts +34 -9
- package/dist/components/main.js +187 -49
- package/dist/components/menu.d.ts +118 -0
- package/dist/components/menu.js +218 -0
- package/dist/components/tabs.d.ts +0 -2
- package/dist/components/tabs.js +6 -19
- package/dist/components/toast.d.ts +37 -0
- package/dist/components/toast.js +79 -0
- package/dist/components/tooltip.d.ts +32 -0
- package/dist/components/tooltip.js +130 -0
- package/dist/core.d.ts +1 -1
- package/dist/icons-helpers.d.ts +46 -0
- package/dist/icons-helpers.js +44 -0
- package/dist/icons.d.ts +1960 -0
- package/dist/icons.js +1972 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/staffa.esm.js +1 -1
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +114 -13
- package/package.json +10 -4
- package/src/components/autocomplete.ts +3 -2
- package/src/components/box.ts +2 -2
- package/src/components/button.ts +42 -10
- package/src/components/buttonChooser.ts +6 -7
- package/src/components/buttonGroup.ts +0 -3
- package/src/components/field.ts +0 -3
- package/src/components/main.ts +201 -45
- package/src/components/menu.ts +288 -0
- package/src/components/tabs.ts +6 -20
- package/src/components/toast.ts +115 -0
- package/src/components/tooltip.ts +139 -0
- package/src/core.ts +1 -1
- package/src/icons-helpers.ts +90 -0
- package/src/icons.ts +1977 -0
- package/src/index.ts +11 -0
- package/src/theme.ts +128 -18
- package/dist/components/modal.d.ts +0 -2
- package/dist/components/modal.js +0 -2
- package/dist/skye.esm.js +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Content, type Slot, type Attributes } from "../core.js";
|
|
2
|
+
import { type MenuOptions } from "./menu.js";
|
|
2
3
|
/** Options for {@link main}. */
|
|
3
4
|
export interface MainOptions {
|
|
4
5
|
/** Aberdeen attr/style string applied to the outermost shell element. */
|
|
@@ -16,31 +17,55 @@ export interface MainOptions {
|
|
|
16
17
|
/** Footer content, pinned below the scroll area. */
|
|
17
18
|
footer?: Slot;
|
|
18
19
|
/**
|
|
19
|
-
* Max
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* Max width for the page's *content*, e.g. `"60rem"`. The header and footer
|
|
21
|
+
* backgrounds still span the full shell width, but their contents — and the
|
|
22
|
+
* sidebar + separator + content trio (or just the content when there's no
|
|
23
|
+
* sidebar) — cap to this width and centre horizontally. When unset, everything
|
|
24
|
+
* fills the available width. Either way the content shares the page surface —
|
|
25
|
+
* it is not boxed.
|
|
22
26
|
*/
|
|
23
27
|
maxWidth?: string;
|
|
24
|
-
/** Aberdeen attr/style string applied to the content
|
|
28
|
+
/** Aberdeen attr/style string applied to the content area. */
|
|
25
29
|
contentAttrs?: Attributes;
|
|
26
30
|
/** Aberdeen attr/style string applied to the top bar. */
|
|
27
31
|
topbarAttrs?: Attributes;
|
|
32
|
+
/**
|
|
33
|
+
* Navigation menu. When provided, renders a sidebar (in `"left"` / `"right"`
|
|
34
|
+
* mode) or a button+dropdown (in `"button"` mode). The sidebar automatically
|
|
35
|
+
* collapses to button mode when the shell is too narrow.
|
|
36
|
+
*/
|
|
37
|
+
nav?: MenuOptions;
|
|
38
|
+
/**
|
|
39
|
+
* Where to render the nav. Defaults to `"left"`.
|
|
40
|
+
* - `"left"` / `"right"`: sidebar next to the content area; collapses to a
|
|
41
|
+
* button+dropdown in the top bar when the shell width drops below 640 px.
|
|
42
|
+
* - `"button"`: always a button+dropdown, never a sidebar.
|
|
43
|
+
*/
|
|
44
|
+
navPosition?: "left" | "right" | "button";
|
|
45
|
+
/** Aberdeen attr/style string applied to the sidebar nav panel. */
|
|
46
|
+
navAttrs?: Attributes;
|
|
28
47
|
}
|
|
29
48
|
/**
|
|
30
49
|
* An application shell that wires up the things almost every app needs: a sticky
|
|
31
50
|
* top bar (icon, title, subtitle, action menu), a scrollable content area, and a
|
|
32
|
-
* footer. With {@link MainOptions.maxWidth} the content
|
|
33
|
-
*
|
|
34
|
-
*
|
|
51
|
+
* footer. With {@link MainOptions.maxWidth} the content area is centred and its
|
|
52
|
+
* width capped. Add a `nav` to get a responsive sidebar (auto-collapses to a
|
|
53
|
+
* menu button below 640 px, or always a button with `navPosition: "button"`).
|
|
35
54
|
*
|
|
36
55
|
* @example
|
|
37
56
|
* ```ts
|
|
38
57
|
* S.main({
|
|
39
58
|
* icon: "✦",
|
|
40
59
|
* title: "Staffa Demo",
|
|
41
|
-
* subtitle: "Component playground",
|
|
42
60
|
* maxWidth: "56rem",
|
|
43
|
-
*
|
|
61
|
+
* nav: {
|
|
62
|
+
* items: [
|
|
63
|
+
* { label: "Home", icon: () => A("#🏠"), href: "/" },
|
|
64
|
+
* { label: "Settings", href: "/settings" },
|
|
65
|
+
* ],
|
|
66
|
+
* },
|
|
67
|
+
* navPosition: "left",
|
|
68
|
+
* menu: () => S.button({ text: "New", attrs: ".small" }),
|
|
44
69
|
* content: () => drawPage(),
|
|
45
70
|
* footer: "© 2026",
|
|
46
71
|
* });
|
package/dist/components/main.js
CHANGED
|
@@ -1,91 +1,229 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
import { drawSlot } from "../core.js";
|
|
3
|
+
import { menuButton, drawMenu } from "./menu.js";
|
|
3
4
|
A.insertGlobalCss({
|
|
4
5
|
".s-main": {
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"> header
|
|
6
|
+
// container-type so @container queries below can respond to shell width.
|
|
7
|
+
"&": "display:flex flex-direction:column min-height:100vh max-height:100vh container-type:inline-size",
|
|
8
|
+
// Header/footer stretch their background the full shell width; their inner
|
|
9
|
+
// `.s-bar` caps to maxWidth and centres, so chrome aligns with the content.
|
|
10
|
+
"> header": "border-bottom: 1px solid $s-border; position:sticky top:0 z-index:10",
|
|
11
|
+
"> footer": "border-top: 1px solid $s-border; fg:$s-fg-muted",
|
|
12
|
+
"> header > .s-bar, > footer > .s-bar": "display:flex align-items:center width:100% margin-inline:auto gap:$3 padding: $2 $3;",
|
|
13
|
+
"> header .s-header-icon": "display:flex align-items:center font-size:1.4em background: $s-gradient; -webkit-background-clip:text; background-clip:text; color:transparent;",
|
|
10
14
|
"> header .s-titles": "display:flex flex-direction:column min-width:0 flex:1",
|
|
11
|
-
"> header .s-title": "font-weight:
|
|
15
|
+
"> header .s-title": "font-weight:800 font-size:1.1em line-height:1.2 overflow:hidden text-overflow:ellipsis white-space:nowrap letter-spacing:-0.01em background: $s-gradient; -webkit-background-clip:text; background-clip:text; color:transparent; width:fit-content max-width:100%",
|
|
12
16
|
"> header .s-subtitle": "fg:$s-fg-muted font-size:0.85em overflow:hidden text-overflow:ellipsis white-space:nowrap",
|
|
13
17
|
"> header .s-menu": "display:flex align-items:center gap:$2",
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
18
|
+
// Body holds sidebar + separator + <main> side by side (only used in sidebar
|
|
19
|
+
// nav mode). It centres `.s-body-inner`, which caps the trio to maxWidth.
|
|
20
|
+
".s-body": "flex:1 overflow:hidden display:flex flex-direction:row min-height:0 justify-content:center",
|
|
21
|
+
".s-body-inner": "flex:1 display:flex flex-direction:row min-height:0",
|
|
22
|
+
// Put the sidebar on the right (content fills the left) for right-hand navs.
|
|
23
|
+
"&.s-nav-right .s-body-inner": "flex-direction:row-reverse",
|
|
24
|
+
// A vertical hairline between sidebar and content, fading out at both ends —
|
|
25
|
+
// the vertical sibling of the menu's `hr.s-menu-sep`.
|
|
26
|
+
".s-nav-sep": "width:1px flex-shrink:0 align-self:stretch margin: 0.6rem 0; border:0 background: linear-gradient(to bottom, transparent, $s-border-strong 18%, $s-border-strong 82%, transparent);",
|
|
27
|
+
// Without a sidebar, <main> is a direct child; with one it lives in .s-body.
|
|
28
|
+
"> main, .s-body main": "flex:1 overflow-y:auto display:flex flex-direction:column",
|
|
29
|
+
// The content area fills the scroll region with comfortable padding. Without a
|
|
30
|
+
// sidebar it caps its own width to maxWidth and centres (applied inline in
|
|
31
|
+
// drawMainContent); with one, `.s-body-inner` does the capping for the trio.
|
|
32
|
+
// It is deliberately NOT a boxed "sheet" — content brings its own boxes.
|
|
33
|
+
"> main > .s-content, .s-body main > .s-content": "width:100% flex:1 p:$3",
|
|
34
|
+
// When <main> actually shows a vertical scrollbar (the `.s-scroll-y` class is
|
|
35
|
+
// toggled from JS by watchVerticalOverflow), inset it from the shell edge by
|
|
36
|
+
// $3 so the bar's right edge lines up with the header/footer content (which
|
|
37
|
+
// sits $3 inside the edge via `.s-bar` padding). The $3 gap between the content
|
|
38
|
+
// and the bar already comes from `.s-content`'s padding. Without a scrollbar
|
|
39
|
+
// there's no margin, so the content keeps its single $3 edge — not 2×$3.
|
|
40
|
+
"> main.s-scroll-y, .s-body main.s-scroll-y": "margin-right:$3",
|
|
41
|
+
},
|
|
42
|
+
// Sidebar nav panel. Items reuse the shared `.s-menu-item[-link]` /
|
|
43
|
+
// `.s-menu-sep` styles from menu.ts, so the sidebar and the floating
|
|
44
|
+
// dropdown stay visually identical.
|
|
45
|
+
// Borderless and transparent so the page's aurora shows through — an airy,
|
|
46
|
+
// floating sidebar whose only chrome is the active item's gradient pill.
|
|
47
|
+
".s-nav-panel": {
|
|
48
|
+
// Extra horizontal padding leaves room for the active pill's glow, which the
|
|
49
|
+
// vertical scroll (overflow-y:auto, which also clips overflow-x) would
|
|
50
|
+
// otherwise cut off at the panel edges.
|
|
51
|
+
"&": "display:flex flex-direction:column overflow-y:auto flex-shrink:0 max-width:228px padding:$3 gap:$1 background:transparent",
|
|
52
|
+
},
|
|
53
|
+
// In button-only mode (or always-button navPosition), hide the sidebar and
|
|
54
|
+
// show the trigger. In sidebar mode, show the panel and hide the trigger.
|
|
55
|
+
// CSS @container queries handle the responsive collapse automatically.
|
|
56
|
+
".s-main.s-nav-left .s-nav-trigger, .s-main.s-nav-right .s-nav-trigger": "display:none",
|
|
57
|
+
".s-main.s-nav-btn-only .s-nav-panel": "display:none",
|
|
58
|
+
".s-main.s-nav-btn-only .s-nav-trigger": "display:flex",
|
|
59
|
+
// Collapse sidebar → button when shell is narrow.
|
|
60
|
+
"@container (max-width: 640px)": {
|
|
61
|
+
".s-main.s-nav-left .s-nav-panel, .s-main.s-nav-right .s-nav-panel, .s-main .s-nav-sep": "display:none",
|
|
62
|
+
".s-main.s-nav-left .s-nav-trigger, .s-main.s-nav-right .s-nav-trigger": "display:flex",
|
|
63
|
+
// On phones a top-level content box becomes a full-bleed block: pull it out
|
|
64
|
+
// to negate the content padding and drop the rounded corners.
|
|
65
|
+
".s-content > .s-box": "margin-inline: calc(-1 * $3); r:0 border-inline:0",
|
|
19
66
|
},
|
|
20
67
|
});
|
|
21
68
|
/**
|
|
22
69
|
* An application shell that wires up the things almost every app needs: a sticky
|
|
23
70
|
* top bar (icon, title, subtitle, action menu), a scrollable content area, and a
|
|
24
|
-
* footer. With {@link MainOptions.maxWidth} the content
|
|
25
|
-
*
|
|
26
|
-
*
|
|
71
|
+
* footer. With {@link MainOptions.maxWidth} the content area is centred and its
|
|
72
|
+
* width capped. Add a `nav` to get a responsive sidebar (auto-collapses to a
|
|
73
|
+
* menu button below 640 px, or always a button with `navPosition: "button"`).
|
|
27
74
|
*
|
|
28
75
|
* @example
|
|
29
76
|
* ```ts
|
|
30
77
|
* S.main({
|
|
31
78
|
* icon: "✦",
|
|
32
79
|
* title: "Staffa Demo",
|
|
33
|
-
* subtitle: "Component playground",
|
|
34
80
|
* maxWidth: "56rem",
|
|
35
|
-
*
|
|
81
|
+
* nav: {
|
|
82
|
+
* items: [
|
|
83
|
+
* { label: "Home", icon: () => A("#🏠"), href: "/" },
|
|
84
|
+
* { label: "Settings", href: "/settings" },
|
|
85
|
+
* ],
|
|
86
|
+
* },
|
|
87
|
+
* navPosition: "left",
|
|
88
|
+
* menu: () => S.button({ text: "New", attrs: ".small" }),
|
|
36
89
|
* content: () => drawPage(),
|
|
37
90
|
* footer: "© 2026",
|
|
38
91
|
* });
|
|
39
92
|
* ```
|
|
40
93
|
*/
|
|
41
94
|
export function main(opts = {}) {
|
|
42
|
-
|
|
43
|
-
|
|
95
|
+
const nav = opts.nav;
|
|
96
|
+
const navPos = opts.navPosition ?? "left";
|
|
97
|
+
const hasNav = nav != null && nav.items.length > 0;
|
|
98
|
+
const navCls = hasNav ? (navPos === "button" ? ".s-nav-btn-only" : `.s-nav-${navPos}`) : "";
|
|
99
|
+
A(`div.s-main.s-s.base${navCls}`, opts.attrs, () => {
|
|
100
|
+
// Top bar.
|
|
44
101
|
A(() => {
|
|
45
|
-
const hasBar = opts.title != null ||
|
|
102
|
+
const hasBar = opts.title != null ||
|
|
103
|
+
opts.subtitle != null ||
|
|
104
|
+
opts.icon != null ||
|
|
105
|
+
opts.menu != null ||
|
|
106
|
+
hasNav;
|
|
46
107
|
if (!hasBar)
|
|
47
108
|
return;
|
|
48
109
|
A("header.s-s.raised", opts.topbarAttrs, () => {
|
|
49
|
-
A(() => {
|
|
50
|
-
|
|
51
|
-
A("div.s-icon", () => drawSlot(opts.icon));
|
|
52
|
-
});
|
|
53
|
-
A("div.s-titles", () => {
|
|
110
|
+
A("div.s-bar", () => {
|
|
111
|
+
// Cap the bar's content to maxWidth and centre it within the full-width header.
|
|
54
112
|
A(() => {
|
|
55
|
-
if (opts.
|
|
56
|
-
A("
|
|
113
|
+
if (opts.maxWidth != null)
|
|
114
|
+
A("max-width:", opts.maxWidth);
|
|
57
115
|
});
|
|
116
|
+
// Nav trigger button — visible when sidebar is hidden (button mode or narrow viewport).
|
|
58
117
|
A(() => {
|
|
59
|
-
if (
|
|
60
|
-
|
|
118
|
+
if (!hasNav)
|
|
119
|
+
return;
|
|
120
|
+
// .s-nav-trigger: CSS toggles display based on sidebar visibility.
|
|
121
|
+
A("div.s-nav-trigger", () => {
|
|
122
|
+
menuButton({
|
|
123
|
+
...nav,
|
|
124
|
+
button: {
|
|
125
|
+
icon: () => A("span aria-hidden=true #☰"),
|
|
126
|
+
ariaLabel: "Open navigation",
|
|
127
|
+
attrs: ".neutral .outlined .small",
|
|
128
|
+
...nav.button,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
A(() => {
|
|
134
|
+
if (opts.icon != null)
|
|
135
|
+
A("div.s-header-icon", () => drawSlot(opts.icon));
|
|
136
|
+
});
|
|
137
|
+
A("div.s-titles", () => {
|
|
138
|
+
A(() => {
|
|
139
|
+
if (opts.title != null)
|
|
140
|
+
A("div.s-title", () => drawSlot(opts.title));
|
|
141
|
+
});
|
|
142
|
+
A(() => {
|
|
143
|
+
if (opts.subtitle != null)
|
|
144
|
+
A("div.s-subtitle", () => drawSlot(opts.subtitle));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
A(() => {
|
|
148
|
+
if (opts.menu)
|
|
149
|
+
A("div.s-menu", () => opts.menu?.());
|
|
61
150
|
});
|
|
62
|
-
});
|
|
63
|
-
A(() => {
|
|
64
|
-
if (opts.menu)
|
|
65
|
-
A("div.s-menu", () => opts.menu?.());
|
|
66
151
|
});
|
|
67
152
|
});
|
|
68
153
|
});
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
154
|
+
// Body — wraps sidebar + separator + main when nav is in sidebar mode. The
|
|
155
|
+
// trio together caps to maxWidth (via .s-body-inner); main fills the rest.
|
|
156
|
+
if (hasNav && navPos !== "button") {
|
|
157
|
+
A("div.s-body", () => {
|
|
158
|
+
A("div.s-body-inner", () => {
|
|
159
|
+
A(() => {
|
|
160
|
+
if (opts.maxWidth != null)
|
|
161
|
+
A("max-width:", opts.maxWidth);
|
|
162
|
+
});
|
|
163
|
+
A(`nav.s-nav-panel.s-s.raised.s-nav-${navPos}`, opts.navAttrs, () => {
|
|
164
|
+
drawMenu(nav.items);
|
|
165
|
+
});
|
|
166
|
+
A("div.s-nav-sep aria-hidden=true");
|
|
167
|
+
drawMainContent(opts, false);
|
|
80
168
|
});
|
|
81
|
-
if (opts.content)
|
|
82
|
-
opts.content();
|
|
83
169
|
});
|
|
84
|
-
}
|
|
85
|
-
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
drawMainContent(opts, true);
|
|
173
|
+
}
|
|
174
|
+
// Footer — full-width background, content centred to maxWidth via .s-bar.
|
|
86
175
|
A(() => {
|
|
87
|
-
if (opts.footer != null)
|
|
88
|
-
A("footer", () =>
|
|
176
|
+
if (opts.footer != null) {
|
|
177
|
+
A("footer", () => {
|
|
178
|
+
A("div.s-bar", () => {
|
|
179
|
+
A(() => {
|
|
180
|
+
if (opts.maxWidth != null)
|
|
181
|
+
A("max-width:", opts.maxWidth);
|
|
182
|
+
});
|
|
183
|
+
drawSlot(opts.footer);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Draw the scrollable `<main>` + content area. When `capWidth` is true (no
|
|
192
|
+
* sidebar), the content caps its own width to maxWidth and centres; in sidebar
|
|
193
|
+
* mode the surrounding `.s-body-inner` already caps the sidebar+content trio.
|
|
194
|
+
*/
|
|
195
|
+
function drawMainContent(opts, capWidth) {
|
|
196
|
+
const mainEl = A("main", () => {
|
|
197
|
+
A("div.s-content", opts.contentAttrs, () => {
|
|
198
|
+
if (capWidth) {
|
|
199
|
+
A(() => {
|
|
200
|
+
if (opts.maxWidth != null)
|
|
201
|
+
A("margin-inline:auto max-width:", opts.maxWidth);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (opts.content)
|
|
205
|
+
opts.content();
|
|
89
206
|
});
|
|
90
207
|
});
|
|
208
|
+
watchVerticalOverflow(mainEl);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Toggle the `.s-scroll-y` class on `el` whenever a vertical scrollbar is eating
|
|
212
|
+
* into its width, so CSS can inset the bar from the shell edge (see the
|
|
213
|
+
* `.s-scroll-y` rule above). We key on `offsetWidth > clientWidth` — a
|
|
214
|
+
* *space-consuming* scrollbar — rather than on content overflow, so overlay
|
|
215
|
+
* scrollbars (mobile, macOS) that take no layout width don't trigger the margin.
|
|
216
|
+
* A `ResizeObserver` watches both the viewport and its content, so the class
|
|
217
|
+
* tracks live content/layout changes; it's disconnected when the scope tears down.
|
|
218
|
+
*/
|
|
219
|
+
function watchVerticalOverflow(el) {
|
|
220
|
+
if (typeof ResizeObserver === "undefined")
|
|
221
|
+
return; // No-op outside the browser.
|
|
222
|
+
const update = () => el.classList.toggle("s-scroll-y", el.offsetWidth > el.clientWidth);
|
|
223
|
+
const ro = new ResizeObserver(update);
|
|
224
|
+
ro.observe(el);
|
|
225
|
+
if (el.firstElementChild)
|
|
226
|
+
ro.observe(el.firstElementChild);
|
|
227
|
+
update();
|
|
228
|
+
A.clean(() => ro.disconnect());
|
|
91
229
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { type Content, type Slot, type Attributes } from "../core.js";
|
|
2
|
+
import { type ButtonOptions } from "./button.js";
|
|
3
|
+
/**
|
|
4
|
+
* A clickable item in a menu or sidebar nav.
|
|
5
|
+
*
|
|
6
|
+
* **Tip:** set `href` and call Aberdeen's `interceptLinks()` once at app
|
|
7
|
+
* startup for SPA-style navigation. When `href` is set, the item is
|
|
8
|
+
* automatically highlighted as active whenever the current URL matches it
|
|
9
|
+
* (via {@link matchCurrent}).
|
|
10
|
+
*/
|
|
11
|
+
export interface MenuItem {
|
|
12
|
+
/** Label text or draw function. Strings are rendered as rich text. */
|
|
13
|
+
label: Slot;
|
|
14
|
+
/** Leading icon drawn before the label. */
|
|
15
|
+
icon?: Slot;
|
|
16
|
+
/** Click handler. */
|
|
17
|
+
click?: (e: Event) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Render as a link (`<a>`) pointing here. Pairs naturally with
|
|
20
|
+
* `interceptLinks()` — the item is highlighted automatically when the URL
|
|
21
|
+
* matches.
|
|
22
|
+
*/
|
|
23
|
+
href?: string;
|
|
24
|
+
/** `target` for the link (`_blank`, etc.). Only meaningful with `href`. */
|
|
25
|
+
target?: string;
|
|
26
|
+
/** Disables the item. */
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
/** Aberdeen attr/style string on the item element. */
|
|
29
|
+
attrs?: Attributes;
|
|
30
|
+
}
|
|
31
|
+
/** A visual divider between groups of items. */
|
|
32
|
+
export interface MenuSeparator {
|
|
33
|
+
separator: true;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* An entry in a menu or sidebar nav list. Three forms:
|
|
37
|
+
* - `MenuItem` — a clickable/linkable row with label and optional icon.
|
|
38
|
+
* - `MenuSeparator` — a visual divider (`{ separator: true }`).
|
|
39
|
+
* - A draw function `() => void` — renders custom content (section header,
|
|
40
|
+
* avatar, search box, …). Skipped by keyboard navigation.
|
|
41
|
+
*/
|
|
42
|
+
export type MenuEntry = MenuItem | MenuSeparator | Content;
|
|
43
|
+
/** Options for {@link menuButton} and {@link MainOptions.nav}. */
|
|
44
|
+
export interface MenuOptions {
|
|
45
|
+
/** Items shown in the dropdown or sidebar nav. */
|
|
46
|
+
items: MenuEntry[];
|
|
47
|
+
/**
|
|
48
|
+
* Customize the trigger button rendered by {@link menuButton}. Defaults to a
|
|
49
|
+
* `☰` icon button. The `click` handler is managed internally.
|
|
50
|
+
*
|
|
51
|
+
* When used as a `nav` in `S.main()`, this also customizes the hamburger
|
|
52
|
+
* button shown when the sidebar collapses.
|
|
53
|
+
*/
|
|
54
|
+
button?: ButtonOptions;
|
|
55
|
+
/** Aberdeen attr/style string on the floating dropdown panel. */
|
|
56
|
+
dropdownAttrs?: Attributes;
|
|
57
|
+
}
|
|
58
|
+
/** Options for {@link showFloatingMenu}. */
|
|
59
|
+
export interface FloatingMenuOptions {
|
|
60
|
+
/** Items to show. */
|
|
61
|
+
items: MenuEntry[];
|
|
62
|
+
/** Element to anchor the menu to (positioned just below it, flips up if needed). */
|
|
63
|
+
anchor: HTMLElement;
|
|
64
|
+
/** Aberdeen attr/style string on the floating panel. */
|
|
65
|
+
dropdownAttrs?: Attributes;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Draw a list of {@link MenuEntry} items into the *current* element, with
|
|
69
|
+
* arrow-key / Home / End navigation between the focusable items. The single
|
|
70
|
+
* shared primitive behind both the floating dropdown ({@link showFloatingMenu})
|
|
71
|
+
* and the sidebar nav in `S.main()` — call it inside whatever container
|
|
72
|
+
* (`<nav>`, the floating panel, …) you've opened.
|
|
73
|
+
*
|
|
74
|
+
* Items are real `<a>`/`<button>` elements, so Enter/Space activate them and
|
|
75
|
+
* screen readers narrate them natively.
|
|
76
|
+
*
|
|
77
|
+
* @param items The entries to render.
|
|
78
|
+
* @param onActivate Optional — run when any item is activated (used by the
|
|
79
|
+
* floating menu to close itself on selection).
|
|
80
|
+
*/
|
|
81
|
+
export declare function drawMenu(items: MenuEntry[], onActivate?: () => void): void;
|
|
82
|
+
/**
|
|
83
|
+
* Open a floating dropdown menu anchored to an element. Portals to
|
|
84
|
+
* `document.body` (never clipped), positions itself (flipping up when there's
|
|
85
|
+
* no room below), and closes on Escape, Tab, item selection, or any click
|
|
86
|
+
* outside the panel and anchor. Returns a `close()` function.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* // Custom context menu:
|
|
91
|
+
* el.addEventListener("contextmenu", (e) => {
|
|
92
|
+
* e.preventDefault();
|
|
93
|
+
* S.showFloatingMenu({ items, anchor: el });
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export declare function showFloatingMenu(opts: FloatingMenuOptions): () => void;
|
|
98
|
+
/**
|
|
99
|
+
* A button that opens a {@link showFloatingMenu | floating dropdown menu} on
|
|
100
|
+
* click. Keyboard navigation: Arrow Up/Down, Home, End; Escape/Tab to close;
|
|
101
|
+
* Enter/Space activate the focused item natively.
|
|
102
|
+
*
|
|
103
|
+
* **Tip:** set `href` on items and call `interceptLinks()` once at app startup
|
|
104
|
+
* for SPA navigation — active items are highlighted automatically.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* S.menuButton({
|
|
109
|
+
* button: { text: "Actions", attrs: ".neutral .outlined" },
|
|
110
|
+
* items: [
|
|
111
|
+
* { label: "Edit", icon: () => A("#✎"), click: () => edit() },
|
|
112
|
+
* { separator: true },
|
|
113
|
+
* { label: "Delete", attrs: "fg:$s-danger", click: () => del() },
|
|
114
|
+
* ],
|
|
115
|
+
* });
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export declare function menuButton(opts: MenuOptions): void;
|