@bunnix/components 0.10.2 → 0.11.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/@types/index.d.ts +179 -15
- package/README.md +41 -4
- package/package.json +3 -8
- package/src/core/buttons.css +1 -0
- package/src/core/core.css +86 -4
- package/src/core/dialog.css +3 -1
- package/src/core/dialog.mjs +101 -16
- package/src/core/input.css +202 -0
- package/src/core/inputs.mjs +702 -23
- package/src/core/layout.mjs +1 -2
- package/src/core/media.css +36 -1
- package/src/core/media.mjs +13 -13
- package/src/core/menu.css +10 -29
- package/src/core/menu.mjs +159 -70
- package/src/core/outline.mjs +100 -0
- package/src/core/sidebar.mjs +189 -68
- package/src/core/sliderUtils.mjs +51 -0
- package/src/core/table.css +23 -0
- package/src/core/table.mjs +35 -20
- package/src/core/textareaUtils.mjs +31 -0
- package/src/core/utils.mjs +105 -0
- package/src/font-face/Framework7Icons-Regular.woff2 +0 -0
- package/src/index.mjs +3 -1
- package/src/core/core-mobile.css +0 -69
- package/src/icons/add-circle.svg +0 -1
- package/src/icons/add.svg +0 -1
- package/src/icons/alt.svg +0 -1
- package/src/icons/archive.svg +0 -1
- package/src/icons/arrow-down.svg +0 -1
- package/src/icons/arrow-left.svg +0 -1
- package/src/icons/arrow-right.svg +0 -1
- package/src/icons/arrow-up.svg +0 -1
- package/src/icons/at.svg +0 -1
- package/src/icons/attestation.svg +0 -1
- package/src/icons/battery-25.svg +0 -1
- package/src/icons/bell.svg +0 -3
- package/src/icons/bookmark.svg +0 -1
- package/src/icons/bot.svg +0 -1
- package/src/icons/bubble.svg +0 -1
- package/src/icons/building.svg +0 -3
- package/src/icons/button.svg +0 -1
- package/src/icons/calculate.svg +0 -1
- package/src/icons/calendar.svg +0 -1
- package/src/icons/captions-bubble.svg +0 -1
- package/src/icons/cart.svg +0 -1
- package/src/icons/chart.svg +0 -1
- package/src/icons/check.svg +0 -1
- package/src/icons/chevron-down.svg +0 -1
- package/src/icons/chevron-left.svg +0 -1
- package/src/icons/chevron-right.svg +0 -1
- package/src/icons/clip.svg +0 -1
- package/src/icons/clock.svg +0 -3
- package/src/icons/close-circle.svg +0 -3
- package/src/icons/close.svg +0 -1
- package/src/icons/cloud-download.svg +0 -1
- package/src/icons/cloud-upload.svg +0 -1
- package/src/icons/cloud.svg +0 -1
- package/src/icons/columns-layout.svg +0 -1
- package/src/icons/command.svg +0 -1
- package/src/icons/cube.svg +0 -1
- package/src/icons/delete.svg +0 -3
- package/src/icons/dollar.svg +0 -3
- package/src/icons/download.svg +0 -1
- package/src/icons/draw.svg +0 -1
- package/src/icons/duplicate.svg +0 -3
- package/src/icons/ear.svg +0 -1
- package/src/icons/edit.svg +0 -1
- package/src/icons/exclamation-mark.svg +0 -1
- package/src/icons/eye-open.svg +0 -1
- package/src/icons/eye.svg +0 -1
- package/src/icons/file-html.svg +0 -1
- package/src/icons/file.svg +0 -3
- package/src/icons/finger.svg +0 -1
- package/src/icons/flag.svg +0 -1
- package/src/icons/folder.svg +0 -1
- package/src/icons/function.svg +0 -1
- package/src/icons/gear.svg +0 -1
- package/src/icons/gift.svg +0 -1
- package/src/icons/globe.svg +0 -3
- package/src/icons/grid.svg +0 -1
- package/src/icons/hammer.svg +0 -1
- package/src/icons/hand.svg +0 -1
- package/src/icons/hare.svg +0 -1
- package/src/icons/heart.svg +0 -3
- package/src/icons/home.svg +0 -3
- package/src/icons/image.svg +0 -1
- package/src/icons/inbox.svg +0 -3
- package/src/icons/info.svg +0 -1
- package/src/icons/key.svg +0 -1
- package/src/icons/lamp.svg +0 -1
- package/src/icons/link.svg +0 -1
- package/src/icons/location.svg +0 -1
- package/src/icons/locker.svg +0 -1
- package/src/icons/login.svg +0 -1
- package/src/icons/logout.svg +0 -3
- package/src/icons/mail.svg +0 -3
- package/src/icons/map.svg +0 -3
- package/src/icons/markup.svg +0 -1
- package/src/icons/merge.svg +0 -1
- package/src/icons/more-horizontal.svg +0 -5
- package/src/icons/more-vertical.svg +0 -5
- package/src/icons/mouse.svg +0 -1
- package/src/icons/music-mic.svg +0 -1
- package/src/icons/paintbrush.svg +0 -1
- package/src/icons/palette.svg +0 -1
- package/src/icons/password.svg +0 -1
- package/src/icons/pencil.svg +0 -1
- package/src/icons/people.svg +0 -3
- package/src/icons/percent.svg +0 -1
- package/src/icons/person-add.svg +0 -1
- package/src/icons/person-remove.svg +0 -1
- package/src/icons/person.svg +0 -4
- package/src/icons/phone.svg +0 -1
- package/src/icons/pin.svg +0 -1
- package/src/icons/question-circle.svg +0 -3
- package/src/icons/remove-circle.svg +0 -1
- package/src/icons/return-arrow.svg +0 -1
- package/src/icons/save.svg +0 -1
- package/src/icons/search.svg +0 -1
- package/src/icons/sections.svg +0 -1
- package/src/icons/send.svg +0 -1
- package/src/icons/share.svg +0 -1
- package/src/icons/shine.svg +0 -1
- package/src/icons/sliders.svg +0 -1
- package/src/icons/star.svg +0 -3
- package/src/icons/staroflife.svg +0 -1
- package/src/icons/storage.svg +0 -1
- package/src/icons/success-circle.svg +0 -3
- package/src/icons/swap.svg +0 -1
- package/src/icons/switch.svg +0 -1
- package/src/icons/sync.svg +0 -3
- package/src/icons/table.svg +0 -3
- package/src/icons/tag.svg +0 -3
- package/src/icons/terminal.svg +0 -1
- package/src/icons/text.svg +0 -1
- package/src/icons/thumb-down.svg +0 -1
- package/src/icons/thumb-up.svg +0 -1
- package/src/icons/timer.svg +0 -3
- package/src/icons/toggle.svg +0 -1
- package/src/icons/trash.svg +0 -1
- package/src/icons/tv-music.svg +0 -1
- package/src/icons/update-page.svg +0 -1
- package/src/icons/upload.svg +0 -1
- package/src/icons/video.svg +0 -1
- package/src/icons/wallet.svg +0 -1
- package/src/icons/wand-stars.svg +0 -1
- package/src/icons/waveform.svg +0 -1
- package/src/icons/window.svg +0 -1
- package/src/utils/iconRegistry.generated.mjs +0 -187
- package/src/utils/iconRegistry.mjs +0 -34
package/src/core/layout.mjs
CHANGED
|
@@ -102,13 +102,12 @@ const GridCore = (props, ...children) => {
|
|
|
102
102
|
let layout = props.layout ?? "fixed";
|
|
103
103
|
let columns = props.columns ?? [];
|
|
104
104
|
let gap = props.gridGap;
|
|
105
|
+
let style = { ...(props.style ?? {}) };
|
|
105
106
|
|
|
106
107
|
delete props.layout;
|
|
107
108
|
delete props.columns;
|
|
108
109
|
delete props.gridGap;
|
|
109
110
|
|
|
110
|
-
let style = {};
|
|
111
|
-
|
|
112
111
|
if (gap !== undefined) {
|
|
113
112
|
style["--grid-gap"] = (typeof gap === "number") ? `${gap}px` : gap;
|
|
114
113
|
}
|
package/src/core/media.css
CHANGED
|
@@ -1,8 +1,43 @@
|
|
|
1
|
+
@font-face {
|
|
2
|
+
font-family: "Framework7 Icons";
|
|
3
|
+
font-style: normal;
|
|
4
|
+
font-weight: 400;
|
|
5
|
+
src: url("../font-face/Framework7Icons-Regular.woff2") format("woff2");
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
/* Icon Base Styles */
|
|
2
9
|
.icon {
|
|
3
|
-
display: inline-
|
|
10
|
+
display: inline-flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: center;
|
|
4
13
|
width: 22px;
|
|
5
14
|
height: 22px;
|
|
15
|
+
color: currentColor;
|
|
16
|
+
font-size: 22px;
|
|
17
|
+
line-height: 1;
|
|
18
|
+
flex-shrink: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.f7-icons {
|
|
22
|
+
font-family: "Framework7 Icons";
|
|
23
|
+
font-weight: normal;
|
|
24
|
+
font-style: normal;
|
|
25
|
+
font-size: 28px;
|
|
26
|
+
line-height: 1;
|
|
27
|
+
letter-spacing: normal;
|
|
28
|
+
text-transform: none;
|
|
29
|
+
display: inline-block;
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
word-wrap: normal;
|
|
32
|
+
direction: ltr;
|
|
33
|
+
-webkit-font-smoothing: antialiased;
|
|
34
|
+
text-rendering: optimizeLegibility;
|
|
35
|
+
-moz-osx-font-smoothing: grayscale;
|
|
36
|
+
-webkit-font-feature-settings: "liga";
|
|
37
|
+
-moz-font-feature-settings: "liga=1";
|
|
38
|
+
-moz-font-feature-settings: "liga";
|
|
39
|
+
font-feature-settings: "liga";
|
|
40
|
+
text-align: center;
|
|
6
41
|
}
|
|
7
42
|
|
|
8
43
|
/* Spinner Base Styles */
|
package/src/core/media.mjs
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Components:
|
|
7
7
|
* - Media: Generic media component that renders images or inline SVG
|
|
8
|
-
* - Icon: Icon component using the icon
|
|
8
|
+
* - Icon: Icon component using the Framework7 icon font
|
|
9
9
|
* - Spinner: Animated loading spinner with customizable size
|
|
10
10
|
* - Avatar: User avatar with support for images or letter initials
|
|
11
11
|
*
|
|
12
12
|
* Features:
|
|
13
13
|
* - Automatic style extraction (width, height, size, etc.)
|
|
14
14
|
* - Flexible props normalization (supports both props object and direct children)
|
|
15
|
-
* -
|
|
15
|
+
* - Font-based icon rendering plus inline SVG support for spinners
|
|
16
16
|
* - Avatar size and appearance customization
|
|
17
17
|
*/
|
|
18
18
|
import Bunnix, { Show, useState } from "@bunnix/core";
|
|
@@ -21,9 +21,8 @@ import {
|
|
|
21
21
|
withExtractedStyles,
|
|
22
22
|
isStateLike,
|
|
23
23
|
} from "./utils.mjs";
|
|
24
|
-
import { iconRegistry } from "../utils/iconRegistry.generated.mjs";
|
|
25
24
|
|
|
26
|
-
const { span, img } = Bunnix;
|
|
25
|
+
const { span, img, i } = Bunnix;
|
|
27
26
|
|
|
28
27
|
const MediaCore = (props, ...children) => {
|
|
29
28
|
if ("svg" in props) {
|
|
@@ -38,15 +37,16 @@ const MediaCore = (props, ...children) => {
|
|
|
38
37
|
const IconCore = (props, ...children) => {
|
|
39
38
|
const { name, ...restProps } = props;
|
|
40
39
|
if (!name) return null;
|
|
40
|
+
const style = { ...(restProps.style || {}) };
|
|
41
|
+
if (!style.fontSize && (style.width || style.height)) {
|
|
42
|
+
style.fontSize = style.width || style.height;
|
|
43
|
+
}
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
if (!svgContent) return null;
|
|
44
|
-
|
|
45
|
-
return span({
|
|
45
|
+
return i({
|
|
46
46
|
...restProps,
|
|
47
|
-
|
|
48
|
-
class: `icon ${restProps.class || ""}
|
|
49
|
-
});
|
|
47
|
+
style,
|
|
48
|
+
class: `icon f7-icons ${restProps.class || ""}`.trim(),
|
|
49
|
+
}, name);
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
const spinnerSvg = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
|
|
@@ -113,10 +113,10 @@ export const Media = withNormalizedArgs((props, ...children) =>
|
|
|
113
113
|
);
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
* Icon component using the icon
|
|
116
|
+
* Icon component using the Framework7 icon font ligatures.
|
|
117
117
|
*
|
|
118
118
|
* @param {Object} props - Component props
|
|
119
|
-
* @param {string} props.name -
|
|
119
|
+
* @param {string} props.name - Official Framework7 icon name
|
|
120
120
|
* @param {number} [props.size=22] - Icon size in pixels
|
|
121
121
|
* @param {string} [props.color] - Icon color (CSS value)
|
|
122
122
|
* @param {string} [props.class] - Additional CSS classes
|
package/src/core/menu.css
CHANGED
|
@@ -5,40 +5,21 @@
|
|
|
5
5
|
display: inline-block;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/* Popover reset + positioning
|
|
9
|
+
* Uses HTML Popover API — element lives in the CSS Top Layer,
|
|
10
|
+
* fully escaping any overflow: hidden ancestor.
|
|
11
|
+
* Browser UA stylesheet sets inset: 0 and margin: auto on [popover],
|
|
12
|
+
* so both must be reset for our fixed coordinates to take effect. */
|
|
8
13
|
.menu-items {
|
|
9
|
-
position:
|
|
10
|
-
|
|
14
|
+
position: fixed;
|
|
15
|
+
inset: unset;
|
|
16
|
+
margin: 0;
|
|
11
17
|
border: 1px solid var(--color-border-primary);
|
|
12
18
|
border-radius: var(--radius-md);
|
|
19
|
+
background-color: var(--color-bg-primary);
|
|
13
20
|
min-width: 160px;
|
|
14
|
-
z-index: 1000;
|
|
15
|
-
overflow: hidden;
|
|
16
21
|
padding: 4px;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
/* Anchor positions */
|
|
20
|
-
.menu-items--bottomLeft {
|
|
21
|
-
top: 100%;
|
|
22
|
-
left: 0;
|
|
23
|
-
margin-top: 4px;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.menu-items--bottomRight {
|
|
27
|
-
top: 100%;
|
|
28
|
-
right: 0;
|
|
29
|
-
margin-top: 4px;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
.menu-items--topLeft {
|
|
33
|
-
bottom: 100%;
|
|
34
|
-
left: 0;
|
|
35
|
-
margin-bottom: 4px;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.menu-items--topRight {
|
|
39
|
-
bottom: 100%;
|
|
40
|
-
right: 0;
|
|
41
|
-
margin-bottom: 4px;
|
|
22
|
+
overflow: hidden;
|
|
42
23
|
}
|
|
43
24
|
|
|
44
25
|
.menu-divider {
|
package/src/core/menu.mjs
CHANGED
|
@@ -8,103 +8,187 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Features:
|
|
10
10
|
* - Automatic open/close state management
|
|
11
|
-
* -
|
|
11
|
+
* - Popover API for overflow-safe rendering (CSS Top Layer)
|
|
12
|
+
* - Browser-native dismiss (click outside, Escape key, page scroll)
|
|
12
13
|
* - Action items with optional icons
|
|
13
14
|
* - Divider support for grouping items
|
|
14
15
|
* - Custom trigger support
|
|
15
16
|
*/
|
|
16
|
-
import Bunnix, { useState, useEffect, useRef,
|
|
17
|
-
import { withNormalizedArgs, withExtractedStyles } from "./utils.mjs";
|
|
17
|
+
import Bunnix, { useState, useEffect, useRef, ForEach, Compute } from "@bunnix/core";
|
|
18
|
+
import { withNormalizedArgs, withExtractedStyles, resolveCollectionState } from "./utils.mjs";
|
|
18
19
|
import { Column, Row } from "./layout.mjs";
|
|
19
20
|
import { Button } from "./buttons.mjs";
|
|
20
21
|
import { Icon } from "./media.mjs";
|
|
22
|
+
import { Text } from "./typography.mjs";
|
|
21
23
|
import "./menu.css";
|
|
22
24
|
|
|
23
|
-
const { div
|
|
25
|
+
const { div } = Bunnix;
|
|
24
26
|
|
|
25
27
|
const MenuCore = (props, ...children) => {
|
|
26
28
|
const isOpen = useState(false);
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
+
const triggerRef = useRef(null);
|
|
30
|
+
const popoverRef = useRef(null);
|
|
31
|
+
|
|
29
32
|
// Resolve items (state or raw value)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const itemsValue = resolveCollectionState(props.items, []);
|
|
34
|
+
const keyedItemsValue = Compute(itemsValue, (resolvedItems) =>
|
|
35
|
+
(resolvedItems ?? []).map((item, index) =>
|
|
36
|
+
item?.divider && (item.key === undefined || item.key === null)
|
|
37
|
+
? { ...item, key: `divider-${index}` }
|
|
38
|
+
: item,
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
|
|
34
42
|
// Resolve trigger
|
|
35
43
|
let trigger = props.trigger || "Menu";
|
|
36
|
-
|
|
37
|
-
// Resolve anchor position
|
|
38
|
-
let
|
|
39
|
-
|
|
44
|
+
|
|
45
|
+
// Resolve anchor position (support both kebab-case and camelCase for backward compatibility)
|
|
46
|
+
let anchorInput = props.anchor || "bottom-left";
|
|
47
|
+
const anchorMap = {
|
|
48
|
+
"bottom-left": "bottomLeft",
|
|
49
|
+
"bottom-right": "bottomRight",
|
|
50
|
+
"top-left": "topLeft",
|
|
51
|
+
"top-right": "topRight",
|
|
52
|
+
"bottomLeft": "bottomLeft",
|
|
53
|
+
"bottomRight": "bottomRight",
|
|
54
|
+
"topLeft": "topLeft",
|
|
55
|
+
"topRight": "topRight",
|
|
56
|
+
};
|
|
57
|
+
let anchor = anchorMap[anchorInput] || "bottomLeft";
|
|
58
|
+
|
|
40
59
|
delete props.items;
|
|
41
60
|
delete props.trigger;
|
|
42
61
|
delete props.anchor;
|
|
43
|
-
|
|
44
|
-
//
|
|
62
|
+
|
|
63
|
+
// Sync isOpen state from popover toggle events & close on scroll
|
|
64
|
+
// Handles browser auto-dismiss (click outside, Escape key) + scroll-away behavior
|
|
65
|
+
// Uses queueMicrotask to defer listener attachment until after popoverRef is assigned by bunnixToDOM
|
|
45
66
|
useEffect(() => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
queueMicrotask(() => {
|
|
68
|
+
const el = popoverRef.current;
|
|
69
|
+
if (!el) return;
|
|
70
|
+
|
|
71
|
+
// Close menu when page scrolls (capture phase for early detection)
|
|
72
|
+
const handleScroll = () => {
|
|
73
|
+
el.hidePopover();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Sync state when popover opens/closes, manage scroll listener lifecycle
|
|
77
|
+
const handleToggle = (e) => {
|
|
78
|
+
const open = e.newState === "open";
|
|
79
|
+
isOpen.set(open);
|
|
80
|
+
if (open) {
|
|
81
|
+
// Attach scroll listener when menu opens
|
|
82
|
+
window.addEventListener("scroll", handleScroll, true);
|
|
83
|
+
} else {
|
|
84
|
+
// Remove scroll listener when menu closes (click outside, Escape, item click, etc.)
|
|
85
|
+
window.removeEventListener("scroll", handleScroll, true);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
el.addEventListener("toggle", handleToggle);
|
|
90
|
+
// Toggle listener lives with the element and is GC'd when element is removed
|
|
91
|
+
// Scroll listener is self-managed: attached on open, removed on close
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const computeMenuPos = (rect, anchor) => {
|
|
96
|
+
const GAP = 4;
|
|
97
|
+
// clientWidth/clientHeight exclude scrollbar width — matches getBoundingClientRect() coordinate space.
|
|
98
|
+
// window.innerWidth/innerHeight include scrollbar and would produce a consistent gap on the right/bottom.
|
|
99
|
+
const vw = document.documentElement.clientWidth;
|
|
100
|
+
const vh = document.documentElement.clientHeight;
|
|
101
|
+
switch (anchor) {
|
|
102
|
+
case "bottomRight":
|
|
103
|
+
return {
|
|
104
|
+
top: `${rect.bottom + GAP}px`,
|
|
105
|
+
left: "auto",
|
|
106
|
+
bottom: "auto",
|
|
107
|
+
right: `${vw - rect.right}px`,
|
|
108
|
+
};
|
|
109
|
+
case "topLeft":
|
|
110
|
+
return {
|
|
111
|
+
top: "auto",
|
|
112
|
+
left: `${rect.left}px`,
|
|
113
|
+
bottom: `${vh - rect.top + GAP}px`,
|
|
114
|
+
right: "auto",
|
|
115
|
+
};
|
|
116
|
+
case "topRight":
|
|
117
|
+
return {
|
|
118
|
+
top: "auto",
|
|
119
|
+
left: "auto",
|
|
120
|
+
bottom: `${vh - rect.top + GAP}px`,
|
|
121
|
+
right: `${vw - rect.right}px`,
|
|
122
|
+
};
|
|
123
|
+
case "bottomLeft":
|
|
124
|
+
default:
|
|
125
|
+
return {
|
|
126
|
+
top: `${rect.bottom + GAP}px`,
|
|
127
|
+
left: `${rect.left}px`,
|
|
128
|
+
bottom: "auto",
|
|
129
|
+
right: "auto",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
58
134
|
const toggleDropdown = () => {
|
|
59
|
-
|
|
135
|
+
if (!isOpen.get()) {
|
|
136
|
+
// Measure the trigger element (first child), not the wrapper, for accurate right-edge alignment
|
|
137
|
+
const triggerEl = triggerRef.current.firstElementChild ?? triggerRef.current;
|
|
138
|
+
const rect = triggerEl.getBoundingClientRect();
|
|
139
|
+
const pos = computeMenuPos(rect, anchor);
|
|
140
|
+
if (popoverRef.current) {
|
|
141
|
+
Object.assign(popoverRef.current.style, pos);
|
|
142
|
+
popoverRef.current.showPopover();
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
popoverRef.current?.hidePopover();
|
|
146
|
+
}
|
|
60
147
|
};
|
|
61
|
-
|
|
148
|
+
|
|
62
149
|
const handleItemClick = (item) => {
|
|
63
150
|
if (item.divider) return;
|
|
64
|
-
|
|
65
|
-
isOpen.set(false);
|
|
66
|
-
|
|
151
|
+
popoverRef.current?.hidePopover();
|
|
67
152
|
if (item.action) {
|
|
68
153
|
item.action();
|
|
69
154
|
}
|
|
70
155
|
};
|
|
71
|
-
|
|
156
|
+
|
|
72
157
|
return div(
|
|
73
|
-
{ ...props, class: `menu ${props.class || ""}`, ref:
|
|
158
|
+
{ ...props, class: `menu ${props.class || ""}`, ref: triggerRef },
|
|
74
159
|
// Trigger
|
|
75
|
-
typeof trigger === "function"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
160
|
+
typeof trigger === "function"
|
|
161
|
+
? trigger({ isOpen: isOpen.get(), toggle: toggleDropdown })
|
|
162
|
+
: Button(
|
|
163
|
+
{
|
|
164
|
+
variant: "secondary",
|
|
165
|
+
click: toggleDropdown,
|
|
166
|
+
},
|
|
167
|
+
trigger
|
|
168
|
+
),
|
|
169
|
+
// Popover dropdown — always in DOM, shown/hidden via Popover API (CSS Top Layer)
|
|
170
|
+
div(
|
|
171
|
+
{ class: "menu-items", popover: "auto", ref: popoverRef },
|
|
172
|
+
Column(
|
|
173
|
+
{ gap: 0 },
|
|
174
|
+
ForEach(keyedItemsValue, "key", (item) => {
|
|
175
|
+
if (item.divider) {
|
|
176
|
+
return div({ class: "menu-divider" });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return Button(
|
|
180
|
+
{
|
|
181
|
+
variant: "tertiary",
|
|
182
|
+
click: () => handleItemClick(item),
|
|
183
|
+
padding: false,
|
|
184
|
+
},
|
|
185
|
+
Row(
|
|
186
|
+
{ fillWidth: true, alignItems: "center", gap: "small" },
|
|
187
|
+
item.icon && Icon({ name: item.icon, size: 16 }),
|
|
188
|
+
Text({ weight: "heavy" }, item.text || item.key),
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
})
|
|
108
192
|
)
|
|
109
193
|
),
|
|
110
194
|
);
|
|
@@ -113,15 +197,20 @@ const MenuCore = (props, ...children) => {
|
|
|
113
197
|
/**
|
|
114
198
|
* Menu component with trigger and action items.
|
|
115
199
|
*
|
|
200
|
+
* Uses the HTML Popover API (`popover="auto"`) to render the dropdown in
|
|
201
|
+
* the CSS Top Layer — fully escaping any `overflow: hidden` ancestor.
|
|
202
|
+
* Browser provides native dismiss on click-outside and Escape key.
|
|
203
|
+
* Menu also closes automatically when the page scrolls.
|
|
204
|
+
*
|
|
116
205
|
* @param {Object} props - Component props
|
|
117
206
|
* @param {Array<Object>} props.items - Menu items array
|
|
118
207
|
* @param {string} props.items[].key - Unique identifier for the item
|
|
119
208
|
* @param {string} props.items[].text - Display text for the item
|
|
120
|
-
* @param {string} [props.items[].icon] - Optional icon name
|
|
209
|
+
* @param {string} [props.items[].icon] - Optional official Framework7 icon name
|
|
121
210
|
* @param {Function} [props.items[].action] - Optional action to run on click
|
|
122
211
|
* @param {boolean} [props.items[].divider] - If true, renders a divider
|
|
123
212
|
* @param {*} [props.trigger] - Trigger element or function that receives {isOpen, toggle}
|
|
124
|
-
* @param {string} [props.anchor="
|
|
213
|
+
* @param {string} [props.anchor="bottom-left"] - Menu anchor position: "bottom-left" | "bottom-right" | "top-left" | "top-right" (or camelCase variants for backward compatibility)
|
|
125
214
|
* @param {string} [props.class] - Additional CSS classes
|
|
126
215
|
* @param {...*} children - Children elements
|
|
127
216
|
* @returns {*} Menu component
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outline Component (Disclosure Widget)
|
|
3
|
+
*
|
|
4
|
+
* A collapsible section component with a clickable anchor header and expandable details region.
|
|
5
|
+
*
|
|
6
|
+
* Components:
|
|
7
|
+
* - Outline: Disclosure widget with togglable details
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Always-visible anchor header (any Bunnix node)
|
|
11
|
+
* - Collapsible details region (any Bunnix node)
|
|
12
|
+
* - Automatic chevron icon (up/down)
|
|
13
|
+
* - Full layout prop support (gap, padding, margin, width, etc.)
|
|
14
|
+
*/
|
|
15
|
+
import Bunnix, { useState, Show } from "@bunnix/core";
|
|
16
|
+
import { withNormalizedArgs, withExtractedStyles } from "./utils.mjs";
|
|
17
|
+
import { Column, Row } from "./layout.mjs";
|
|
18
|
+
import { Icon } from "./media.mjs";
|
|
19
|
+
|
|
20
|
+
const OutlineCore = (props) => {
|
|
21
|
+
let { anchor, details, showChevron = true } = props;
|
|
22
|
+
delete props.anchor;
|
|
23
|
+
delete props.details;
|
|
24
|
+
delete props.showChevron;
|
|
25
|
+
|
|
26
|
+
// Bindable state: accept external StateLike<boolean> or fallback to internal useState
|
|
27
|
+
let showingDetails =
|
|
28
|
+
props.open?.get && props.open?.set
|
|
29
|
+
? props.open
|
|
30
|
+
: useState(props.open ?? false);
|
|
31
|
+
delete props.open;
|
|
32
|
+
|
|
33
|
+
const handleToggleDetails = () => {
|
|
34
|
+
showingDetails.set(!showingDetails.get());
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return Column(
|
|
38
|
+
props,
|
|
39
|
+
Row(
|
|
40
|
+
{ cursor: "pointer", click: handleToggleDetails, fillWidth: true },
|
|
41
|
+
anchor,
|
|
42
|
+
// Only render chevron if showChevron is true
|
|
43
|
+
showChevron && Show(showingDetails.map((v) => !v), () =>
|
|
44
|
+
Icon({ name: "chevron_down", size: 16, flexShrink: 0 }),
|
|
45
|
+
),
|
|
46
|
+
showChevron && Show(showingDetails, () =>
|
|
47
|
+
Icon({ name: "chevron_up", size: 16, flexShrink: 0 }),
|
|
48
|
+
),
|
|
49
|
+
),
|
|
50
|
+
Show(showingDetails, () => details),
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Outline disclosure component with a togglable details region.
|
|
56
|
+
*
|
|
57
|
+
* Renders a clickable anchor row that expands/collapses a details section.
|
|
58
|
+
* A chevron icon in the anchor row reflects the current open/closed state.
|
|
59
|
+
*
|
|
60
|
+
* @param {Object} props - Component props (also accepts all LayoutProps: gap, padding, margin, width, etc.)
|
|
61
|
+
* @param {*} props.anchor - Always-visible trigger content (any Bunnix node)
|
|
62
|
+
* @param {*} props.details - Collapsible content shown when expanded (any Bunnix node)
|
|
63
|
+
* @param {boolean} [props.showChevron=true] - Whether to render the chevron toggle icon
|
|
64
|
+
* @param {boolean|StateLike<boolean>} [props.open] - Controlled open/closed state; pass a Bunnix State for two-way binding
|
|
65
|
+
* @returns {*} Outline component
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Basic usage
|
|
69
|
+
* Outline({
|
|
70
|
+
* anchor: Row({ alignItems: "center", gap: "small" },
|
|
71
|
+
* Icon({ name: "doc_text", size: 16 }),
|
|
72
|
+
* Text({ weight: "heavy" }, "Section Title"),
|
|
73
|
+
* ),
|
|
74
|
+
* details: Column({ gap: "small", paddingTop: "small" },
|
|
75
|
+
* Text("Expandable content goes here."),
|
|
76
|
+
* ),
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* // With external state control
|
|
81
|
+
* const outlineState = useState(false);
|
|
82
|
+
* Outline({
|
|
83
|
+
* open: outlineState,
|
|
84
|
+
* anchor: Text({ weight: "heavy" }, "Click to expand"),
|
|
85
|
+
* details: Text("Controlled from outside"),
|
|
86
|
+
* });
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* // Without chevron
|
|
90
|
+
* Outline({
|
|
91
|
+
* showChevron: false,
|
|
92
|
+
* anchor: Text({ weight: "heavy" }, "Custom anchor"),
|
|
93
|
+
* details: Text("No automatic chevron"),
|
|
94
|
+
* });
|
|
95
|
+
*/
|
|
96
|
+
export const Outline = withNormalizedArgs((props, ...children) =>
|
|
97
|
+
withExtractedStyles((finalProps, ...children) =>
|
|
98
|
+
OutlineCore(finalProps, ...children),
|
|
99
|
+
)(props, ...children),
|
|
100
|
+
);
|