@epoch32/visage 1.1.5 → 1.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/dist/{index-b5wc752a.js → index-dvq5f3s8.js} +342 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +15 -1
- package/dist/ui/bottom-bar/action.d.ts +3 -0
- package/dist/ui/bottom-bar/badge.d.ts +3 -0
- package/dist/ui/bottom-bar/context.d.ts +3 -0
- package/dist/ui/bottom-bar/index.d.ts +19 -0
- package/dist/ui/bottom-bar/item.d.ts +3 -0
- package/dist/ui/bottom-bar/root.d.ts +3 -0
- package/dist/ui/bottom-bar/separator.d.ts +3 -0
- package/dist/ui/bottom-bar/types.d.ts +72 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +15 -1
- package/dist/vite/index.js +34 -15
- package/dist/vite/lucide.d.ts +0 -17
- package/package.json +1 -1
- package/src/index.ts +14 -0
- package/src/ui/bottom-bar/action.tsx +60 -0
- package/src/ui/bottom-bar/badge.tsx +51 -0
- package/src/ui/bottom-bar/context.ts +8 -0
- package/src/ui/bottom-bar/index.ts +29 -0
- package/src/ui/bottom-bar/item.tsx +119 -0
- package/src/ui/bottom-bar/root.tsx +148 -0
- package/src/ui/bottom-bar/separator.tsx +26 -0
- package/src/ui/bottom-bar/types.ts +79 -0
- package/src/ui/index.ts +16 -0
- package/src/vite/lucide.ts +58 -18
package/dist/vite/index.js
CHANGED
|
@@ -15,6 +15,8 @@ function parseNamedImports(importClause) {
|
|
|
15
15
|
};
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
+
var VIRTUAL_PREFIX = "virtual:visage-icons";
|
|
19
|
+
var RESOLVED_PREFIX = `\x00${VIRTUAL_PREFIX}`;
|
|
18
20
|
function visageLucide(options = {}) {
|
|
19
21
|
const aliases = options.aliases ?? ["lucide-react"];
|
|
20
22
|
return {
|
|
@@ -23,17 +25,33 @@ function visageLucide(options = {}) {
|
|
|
23
25
|
config() {
|
|
24
26
|
return {
|
|
25
27
|
optimizeDeps: {
|
|
26
|
-
exclude: aliases
|
|
28
|
+
exclude: [...aliases, VIRTUAL_PREFIX]
|
|
27
29
|
}
|
|
28
30
|
};
|
|
29
31
|
},
|
|
30
32
|
resolveId(id) {
|
|
33
|
+
if (id.startsWith(VIRTUAL_PREFIX)) {
|
|
34
|
+
return `\x00${id}`;
|
|
35
|
+
}
|
|
31
36
|
if (aliases.includes(id)) {
|
|
32
37
|
return `\x00visage-lucide:${id}`;
|
|
33
38
|
}
|
|
34
39
|
return null;
|
|
35
40
|
},
|
|
36
41
|
load(id) {
|
|
42
|
+
if (id.startsWith(RESOLVED_PREFIX)) {
|
|
43
|
+
const query = id.slice(RESOLVED_PREFIX.length);
|
|
44
|
+
const params = new URLSearchParams(query.startsWith("?") ? query.slice(1) : "");
|
|
45
|
+
const names = (params.get("icons") || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
46
|
+
const imports = names.map((n) => `${n} as __icon_node_${n}`).join(", ");
|
|
47
|
+
const exports = names.map((n) => `export const ${n} = /* @__PURE__ */ createIcon("${n}", __icon_node_${n});`).join(`
|
|
48
|
+
`);
|
|
49
|
+
return `import { createIcon } from "visage/ui";
|
|
50
|
+
import { ${imports} } from "lucide";
|
|
51
|
+
|
|
52
|
+
${exports}
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
37
55
|
if (id.startsWith("\x00visage-lucide:")) {
|
|
38
56
|
return `
|
|
39
57
|
import * as lucideIcons from "lucide";
|
|
@@ -51,10 +69,6 @@ export const __getIcon = (name) => {
|
|
|
51
69
|
export default new Proxy({}, {
|
|
52
70
|
get: (_, prop) => typeof prop === "string" ? __getIcon(prop) : undefined
|
|
53
71
|
});
|
|
54
|
-
|
|
55
|
-
// Dynamic proxy export for all lucide icon names
|
|
56
|
-
${Object.keys({})};
|
|
57
|
-
export * from "lucide";
|
|
58
72
|
`;
|
|
59
73
|
}
|
|
60
74
|
return null;
|
|
@@ -66,20 +80,25 @@ export * from "lucide";
|
|
|
66
80
|
let transformed = code;
|
|
67
81
|
let hasMatches = false;
|
|
68
82
|
for (const alias of aliases) {
|
|
69
|
-
const
|
|
70
|
-
transformed = transformed.replace(
|
|
83
|
+
const importRegex = new RegExp(`import\\s*\\{([^}]+)\\}\\s*from\\s*['"]${alias}['"];?`, "g");
|
|
84
|
+
transformed = transformed.replace(importRegex, (_match, importClause) => {
|
|
71
85
|
const imports = parseNamedImports(importClause);
|
|
72
86
|
if (imports.length === 0)
|
|
73
87
|
return "";
|
|
74
88
|
hasMatches = true;
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
`);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
const queryIcons = Array.from(new Set(imports.map((i) => i.importedName))).join(",");
|
|
90
|
+
const specifiers = imports.map((i) => i.importedName === i.localName ? i.importedName : `${i.importedName} as ${i.localName}`).join(", ");
|
|
91
|
+
return `import { ${specifiers} } from "${VIRTUAL_PREFIX}?icons=${queryIcons}";`;
|
|
92
|
+
});
|
|
93
|
+
const exportRegex = new RegExp(`export\\s*\\{([^}]+)\\}\\s*from\\s*['"]${alias}['"];?`, "g");
|
|
94
|
+
transformed = transformed.replace(exportRegex, (_match, exportClause) => {
|
|
95
|
+
const exports = parseNamedImports(exportClause);
|
|
96
|
+
if (exports.length === 0)
|
|
97
|
+
return "";
|
|
98
|
+
hasMatches = true;
|
|
99
|
+
const queryIcons = Array.from(new Set(exports.map((i) => i.importedName))).join(",");
|
|
100
|
+
const specifiers = exports.map((i) => i.importedName === i.localName ? i.importedName : `${i.importedName} as ${i.localName}`).join(", ");
|
|
101
|
+
return `export { ${specifiers} } from "${VIRTUAL_PREFIX}?icons=${queryIcons}";`;
|
|
83
102
|
});
|
|
84
103
|
}
|
|
85
104
|
if (!hasMatches) {
|
package/dist/vite/lucide.d.ts
CHANGED
|
@@ -6,22 +6,5 @@ export interface VisageLucideOptions {
|
|
|
6
6
|
*/
|
|
7
7
|
aliases?: string[];
|
|
8
8
|
}
|
|
9
|
-
/**
|
|
10
|
-
* Official Visage Lucide Vite Plugin
|
|
11
|
-
*
|
|
12
|
-
* Statically transforms `import { Icon } from "lucide-react"` at build time into
|
|
13
|
-
* native, tree-shaken Visage SVG component instances using `createIcon` and `lucide`.
|
|
14
|
-
*
|
|
15
|
-
* Example:
|
|
16
|
-
* ```ts
|
|
17
|
-
* // vite.config.ts
|
|
18
|
-
* import { defineConfig } from "vite";
|
|
19
|
-
* import visage, { visageLucide } from "visage/vite";
|
|
20
|
-
*
|
|
21
|
-
* export default defineConfig({
|
|
22
|
-
* plugins: [visage(), visageLucide()]
|
|
23
|
-
* });
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
9
|
export declare function visageLucide(options?: VisageLucideOptions): VisageVitePlugin;
|
|
27
10
|
export default visageLucide;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -308,6 +308,19 @@ export {
|
|
|
308
308
|
type AccordionProps,
|
|
309
309
|
type AccordionSingleProps,
|
|
310
310
|
AccordionTrigger,
|
|
311
|
+
BottomBar,
|
|
312
|
+
BottomBarAction,
|
|
313
|
+
type BottomBarActionProps,
|
|
314
|
+
BottomBarBadge,
|
|
315
|
+
type BottomBarBadgeProps,
|
|
316
|
+
type BottomBarContextValue,
|
|
317
|
+
BottomBarItem,
|
|
318
|
+
type BottomBarItemConfig,
|
|
319
|
+
type BottomBarItemProps,
|
|
320
|
+
type BottomBarProps,
|
|
321
|
+
BottomBarRoot,
|
|
322
|
+
BottomBarSeparator,
|
|
323
|
+
type BottomBarSeparatorProps,
|
|
311
324
|
type ColumnDef,
|
|
312
325
|
Command,
|
|
313
326
|
CommandDialog,
|
|
@@ -394,6 +407,7 @@ export {
|
|
|
394
407
|
trapFocus,
|
|
395
408
|
useAccordionContext,
|
|
396
409
|
useAccordionItemContext,
|
|
410
|
+
useBottomBarContext,
|
|
397
411
|
useCommandContext,
|
|
398
412
|
useDialogContext,
|
|
399
413
|
useDropdownMenuContext,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Child } from "../../dom/render";
|
|
2
|
+
import type { BottomBarActionProps } from "./types";
|
|
3
|
+
|
|
4
|
+
export function BottomBarAction(props: BottomBarActionProps): Child {
|
|
5
|
+
const {
|
|
6
|
+
as: Tag = "button",
|
|
7
|
+
icon,
|
|
8
|
+
label,
|
|
9
|
+
disabled,
|
|
10
|
+
onClick,
|
|
11
|
+
children,
|
|
12
|
+
class: className,
|
|
13
|
+
style,
|
|
14
|
+
...rest
|
|
15
|
+
} = props;
|
|
16
|
+
|
|
17
|
+
const defaultActionStyle = {
|
|
18
|
+
display: "inline-flex",
|
|
19
|
+
alignItems: "center",
|
|
20
|
+
justifyContent: "center",
|
|
21
|
+
width: "44px",
|
|
22
|
+
height: "44px",
|
|
23
|
+
minWidth: "44px",
|
|
24
|
+
minHeight: "44px",
|
|
25
|
+
borderRadius: "50%",
|
|
26
|
+
background: "linear-gradient(135deg, #ec4899 0%, #a855f7 100%)",
|
|
27
|
+
color: "#ffffff",
|
|
28
|
+
border: "none",
|
|
29
|
+
boxShadow: "0 4px 14px rgba(236, 72, 153, 0.45)",
|
|
30
|
+
cursor: "pointer",
|
|
31
|
+
outline: "none",
|
|
32
|
+
userSelect: "none",
|
|
33
|
+
WebkitTapHighlightColor: "transparent",
|
|
34
|
+
transition: "transform 0.15s ease, box-shadow 0.15s ease",
|
|
35
|
+
boxSizing: "border-box"
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const combinedStyle = style ? { ...defaultActionStyle, ...style } : defaultActionStyle;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Tag
|
|
42
|
+
type={Tag === "button" ? "button" : undefined}
|
|
43
|
+
role="button"
|
|
44
|
+
aria-label={label ?? "Primary action"}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
class={`visage-bottom-bar-action ${className ?? ""}`.trim()}
|
|
47
|
+
style={combinedStyle}
|
|
48
|
+
onClick={onClick}
|
|
49
|
+
{...rest}
|
|
50
|
+
>
|
|
51
|
+
{children !== undefined
|
|
52
|
+
? typeof children === "function"
|
|
53
|
+
? children()
|
|
54
|
+
: children
|
|
55
|
+
: typeof icon === "function"
|
|
56
|
+
? icon()
|
|
57
|
+
: icon}
|
|
58
|
+
</Tag>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Child } from "../../dom/render";
|
|
2
|
+
import { computed } from "../../signals";
|
|
3
|
+
import type { BottomBarBadgeProps } from "./types";
|
|
4
|
+
|
|
5
|
+
export function BottomBarBadge(props: BottomBarBadgeProps): Child {
|
|
6
|
+
const { as: Tag = "span", count, children, class: className, style, ...rest } = props;
|
|
7
|
+
|
|
8
|
+
const displayContent = computed(() => {
|
|
9
|
+
if (children !== undefined) {
|
|
10
|
+
return typeof children === "function" ? children() : children;
|
|
11
|
+
}
|
|
12
|
+
if (count !== undefined) {
|
|
13
|
+
return typeof count === "function" ? count() : count;
|
|
14
|
+
}
|
|
15
|
+
return "";
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const defaultBadgeStyle = {
|
|
19
|
+
position: "absolute",
|
|
20
|
+
top: "3px",
|
|
21
|
+
right: "8px",
|
|
22
|
+
minWidth: "16px",
|
|
23
|
+
height: "16px",
|
|
24
|
+
padding: "0 4px",
|
|
25
|
+
borderRadius: "9999px",
|
|
26
|
+
backgroundColor: "#ef4444",
|
|
27
|
+
color: "#ffffff",
|
|
28
|
+
fontSize: "9px",
|
|
29
|
+
fontWeight: 700,
|
|
30
|
+
lineHeight: "16px",
|
|
31
|
+
textAlign: "center",
|
|
32
|
+
display: "inline-flex",
|
|
33
|
+
alignItems: "center",
|
|
34
|
+
justifyContent: "center",
|
|
35
|
+
boxShadow: "0 0 0 1.5px rgba(18, 18, 24, 0.9)",
|
|
36
|
+
pointerEvents: "none",
|
|
37
|
+
boxSizing: "border-box"
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const combinedStyle = style ? { ...defaultBadgeStyle, ...style } : defaultBadgeStyle;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Tag
|
|
44
|
+
class={`visage-bottom-bar-badge ${className ?? ""}`.trim()}
|
|
45
|
+
style={combinedStyle}
|
|
46
|
+
{...rest}
|
|
47
|
+
>
|
|
48
|
+
{displayContent}
|
|
49
|
+
</Tag>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createContext, useContext } from "../../signals/context";
|
|
2
|
+
import type { BottomBarContextValue } from "./types";
|
|
3
|
+
|
|
4
|
+
export const BottomBarContext = createContext<BottomBarContextValue | null>(null);
|
|
5
|
+
|
|
6
|
+
export function useBottomBarContext(): BottomBarContextValue | null {
|
|
7
|
+
return useContext(BottomBarContext);
|
|
8
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BottomBarAction } from "./action";
|
|
2
|
+
import { BottomBarBadge } from "./badge";
|
|
3
|
+
import { BottomBarItem } from "./item";
|
|
4
|
+
import { BottomBarRoot } from "./root";
|
|
5
|
+
import { BottomBarSeparator } from "./separator";
|
|
6
|
+
|
|
7
|
+
export { BottomBarAction } from "./action";
|
|
8
|
+
export { BottomBarBadge } from "./badge";
|
|
9
|
+
export { BottomBarContext, useBottomBarContext } from "./context";
|
|
10
|
+
export { BottomBarItem } from "./item";
|
|
11
|
+
export { BottomBarRoot } from "./root";
|
|
12
|
+
export { BottomBarSeparator } from "./separator";
|
|
13
|
+
export type {
|
|
14
|
+
BottomBarActionProps,
|
|
15
|
+
BottomBarBadgeProps,
|
|
16
|
+
BottomBarContextValue,
|
|
17
|
+
BottomBarItemConfig,
|
|
18
|
+
BottomBarItemProps,
|
|
19
|
+
BottomBarProps,
|
|
20
|
+
BottomBarSeparatorProps
|
|
21
|
+
} from "./types";
|
|
22
|
+
|
|
23
|
+
export const BottomBar = Object.assign(BottomBarRoot, {
|
|
24
|
+
Root: BottomBarRoot,
|
|
25
|
+
Item: BottomBarItem,
|
|
26
|
+
Action: BottomBarAction,
|
|
27
|
+
Badge: BottomBarBadge,
|
|
28
|
+
Separator: BottomBarSeparator
|
|
29
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Child } from "../../dom/render";
|
|
2
|
+
import { computed } from "../../signals";
|
|
3
|
+
import { BottomBarBadge } from "./badge";
|
|
4
|
+
import { useBottomBarContext } from "./context";
|
|
5
|
+
import type { BottomBarItemProps } from "./types";
|
|
6
|
+
|
|
7
|
+
export function BottomBarItem(props: BottomBarItemProps): Child {
|
|
8
|
+
const ctx = useBottomBarContext();
|
|
9
|
+
const isSelected = computed(() => (ctx ? ctx.value() === props.value : false));
|
|
10
|
+
|
|
11
|
+
const handleClick = (e: MouseEvent) => {
|
|
12
|
+
const isDisabled =
|
|
13
|
+
typeof props.disabled === "function"
|
|
14
|
+
? (props.disabled as () => boolean)()
|
|
15
|
+
: Boolean(props.disabled);
|
|
16
|
+
if (isDisabled) {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (ctx) {
|
|
22
|
+
ctx.setValue(props.value);
|
|
23
|
+
}
|
|
24
|
+
if (props.onClick) {
|
|
25
|
+
props.onClick(e);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
as: Tag = "button",
|
|
31
|
+
value,
|
|
32
|
+
icon,
|
|
33
|
+
label,
|
|
34
|
+
badge,
|
|
35
|
+
disabled,
|
|
36
|
+
onClick: _oc,
|
|
37
|
+
children,
|
|
38
|
+
class: className,
|
|
39
|
+
style,
|
|
40
|
+
...rest
|
|
41
|
+
} = props;
|
|
42
|
+
|
|
43
|
+
const defaultItemStyle = {
|
|
44
|
+
position: "relative",
|
|
45
|
+
display: "flex",
|
|
46
|
+
flexDirection: "column",
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
justifyContent: "center",
|
|
49
|
+
minWidth: "52px",
|
|
50
|
+
minHeight: "44px",
|
|
51
|
+
padding: "6px 8px",
|
|
52
|
+
borderRadius: "16px",
|
|
53
|
+
border: "none",
|
|
54
|
+
background: () => (isSelected() ? "rgba(255, 255, 255, 0.08)" : "transparent"),
|
|
55
|
+
color: () => (isSelected() ? "#ec4899" : "rgba(255, 255, 255, 0.65)"),
|
|
56
|
+
cursor: "pointer",
|
|
57
|
+
outline: "none",
|
|
58
|
+
userSelect: "none",
|
|
59
|
+
WebkitTapHighlightColor: "transparent",
|
|
60
|
+
transition: "color 0.15s ease, background-color 0.15s ease, transform 0.1s ease",
|
|
61
|
+
boxSizing: "border-box"
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const combinedStyle = style ? { ...defaultItemStyle, ...style } : defaultItemStyle;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Tag
|
|
68
|
+
type={Tag === "button" ? "button" : undefined}
|
|
69
|
+
role="tab"
|
|
70
|
+
id={ctx ? `${ctx.baseId}-item-${value}` : undefined}
|
|
71
|
+
aria-selected={() => isSelected()}
|
|
72
|
+
data-state={() => (isSelected() ? "active" : "inactive")}
|
|
73
|
+
data-value={value}
|
|
74
|
+
disabled={disabled}
|
|
75
|
+
class={`visage-bottom-bar-item ${className ?? ""}`.trim()}
|
|
76
|
+
style={combinedStyle}
|
|
77
|
+
onClick={handleClick}
|
|
78
|
+
{...rest}
|
|
79
|
+
>
|
|
80
|
+
{badge !== undefined ? (
|
|
81
|
+
typeof badge === "function" || typeof badge === "string" || typeof badge === "number" ? (
|
|
82
|
+
<BottomBarBadge count={badge as any} />
|
|
83
|
+
) : (
|
|
84
|
+
badge
|
|
85
|
+
)
|
|
86
|
+
) : null}
|
|
87
|
+
|
|
88
|
+
{children !== undefined ? (typeof children === "function" ? children() : children) : null}
|
|
89
|
+
|
|
90
|
+
{icon ? (
|
|
91
|
+
<span
|
|
92
|
+
class="visage-bottom-bar-icon"
|
|
93
|
+
style={{
|
|
94
|
+
display: "inline-flex",
|
|
95
|
+
alignItems: "center",
|
|
96
|
+
justifyContent: "center",
|
|
97
|
+
fontSize: "18px"
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
{typeof icon === "function" ? icon() : icon}
|
|
101
|
+
</span>
|
|
102
|
+
) : null}
|
|
103
|
+
{label ? (
|
|
104
|
+
<span
|
|
105
|
+
class="visage-bottom-bar-label"
|
|
106
|
+
style={{
|
|
107
|
+
fontSize: "10px",
|
|
108
|
+
fontWeight: 500,
|
|
109
|
+
lineHeight: 1.2,
|
|
110
|
+
marginTop: "3px",
|
|
111
|
+
letterSpacing: "0.01em"
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
{typeof label === "function" ? label() : label}
|
|
115
|
+
</span>
|
|
116
|
+
) : null}
|
|
117
|
+
</Tag>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Child } from "../../dom/render";
|
|
2
|
+
import { signal } from "../../signals";
|
|
3
|
+
import { createControllableSignal, createId } from "../utils";
|
|
4
|
+
import { BottomBarContext } from "./context";
|
|
5
|
+
import { BottomBarItem } from "./item";
|
|
6
|
+
import type { BottomBarContextValue, BottomBarProps } from "./types";
|
|
7
|
+
|
|
8
|
+
export function BottomBarRoot(props: BottomBarProps): Child {
|
|
9
|
+
const [value, setValue] = createControllableSignal<string>({
|
|
10
|
+
value: props.value,
|
|
11
|
+
defaultValue:
|
|
12
|
+
props.defaultValue ?? (props.items && props.items.length > 0 ? props.items[0]?.value : ""),
|
|
13
|
+
onChange: props.onValueChange
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const baseId = createId("bottom-bar");
|
|
17
|
+
const variant = props.variant ?? "floating";
|
|
18
|
+
const hideOnScroll = props.hideOnScroll ?? false;
|
|
19
|
+
const threshold = props.scrollThreshold ?? 20;
|
|
20
|
+
|
|
21
|
+
const isVisible = signal(true);
|
|
22
|
+
|
|
23
|
+
if (hideOnScroll && typeof window !== "undefined") {
|
|
24
|
+
let lastScrollY = window.scrollY || 0;
|
|
25
|
+
const onScroll = () => {
|
|
26
|
+
const currentScrollY = window.scrollY || 0;
|
|
27
|
+
const diff = currentScrollY - lastScrollY;
|
|
28
|
+
if (Math.abs(diff) < 5) return;
|
|
29
|
+
|
|
30
|
+
if (currentScrollY > threshold && diff > 0) {
|
|
31
|
+
if (isVisible()) isVisible.set(false);
|
|
32
|
+
} else if (diff < 0) {
|
|
33
|
+
if (!isVisible()) isVisible.set(true);
|
|
34
|
+
}
|
|
35
|
+
lastScrollY = Math.max(0, currentScrollY);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ctx: BottomBarContextValue = {
|
|
42
|
+
value,
|
|
43
|
+
setValue,
|
|
44
|
+
baseId,
|
|
45
|
+
variant
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
as: Tag = "nav",
|
|
50
|
+
value: _v,
|
|
51
|
+
defaultValue: _dv,
|
|
52
|
+
onValueChange: _ovc,
|
|
53
|
+
variant: _var,
|
|
54
|
+
hideOnScroll: _hos,
|
|
55
|
+
scrollThreshold: _st,
|
|
56
|
+
items,
|
|
57
|
+
itemClass,
|
|
58
|
+
itemStyle,
|
|
59
|
+
children,
|
|
60
|
+
class: className,
|
|
61
|
+
style,
|
|
62
|
+
...rest
|
|
63
|
+
} = props;
|
|
64
|
+
|
|
65
|
+
const defaultFloatingStyle = {
|
|
66
|
+
position: "fixed",
|
|
67
|
+
bottom: "max(16px, env(safe-area-inset-bottom, 16px))",
|
|
68
|
+
left: "50%",
|
|
69
|
+
zIndex: 50,
|
|
70
|
+
display: "inline-flex",
|
|
71
|
+
alignItems: "center",
|
|
72
|
+
gap: "6px",
|
|
73
|
+
padding: "6px 10px",
|
|
74
|
+
borderRadius: "9999px",
|
|
75
|
+
backgroundColor: "rgba(18, 18, 24, 0.88)",
|
|
76
|
+
backdropFilter: "blur(20px)",
|
|
77
|
+
WebkitBackdropFilter: "blur(20px)",
|
|
78
|
+
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
79
|
+
boxShadow: "0 10px 30px -5px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05)",
|
|
80
|
+
transform: () =>
|
|
81
|
+
isVisible() ? "translateX(-50%) translateY(0)" : "translateX(-50%) translateY(160%)",
|
|
82
|
+
opacity: () => (isVisible() ? 1 : 0),
|
|
83
|
+
pointerEvents: () => (isVisible() ? "auto" : "none"),
|
|
84
|
+
transition: "transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.25s ease",
|
|
85
|
+
maxWidth: "calc(100vw - 32px)",
|
|
86
|
+
boxSizing: "border-box"
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const defaultDockedStyle = {
|
|
90
|
+
position: "fixed",
|
|
91
|
+
bottom: 0,
|
|
92
|
+
left: 0,
|
|
93
|
+
right: 0,
|
|
94
|
+
zIndex: 50,
|
|
95
|
+
display: "flex",
|
|
96
|
+
justifyContent: "space-around",
|
|
97
|
+
alignItems: "center",
|
|
98
|
+
paddingTop: "8px",
|
|
99
|
+
paddingBottom: "max(10px, env(safe-area-inset-bottom, 10px))",
|
|
100
|
+
paddingLeft: "12px",
|
|
101
|
+
paddingRight: "12px",
|
|
102
|
+
backgroundColor: "rgba(18, 18, 24, 0.94)",
|
|
103
|
+
backdropFilter: "blur(20px)",
|
|
104
|
+
WebkitBackdropFilter: "blur(20px)",
|
|
105
|
+
borderTop: "1px solid rgba(255, 255, 255, 0.1)",
|
|
106
|
+
boxShadow: "0 -4px 20px rgba(0, 0, 0, 0.35)",
|
|
107
|
+
transform: () => (isVisible() ? "translateY(0)" : "translateY(110%)"),
|
|
108
|
+
opacity: () => (isVisible() ? 1 : 0),
|
|
109
|
+
pointerEvents: () => (isVisible() ? "auto" : "none"),
|
|
110
|
+
transition: "transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.25s ease",
|
|
111
|
+
boxSizing: "border-box"
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const computedDefaultStyle = variant === "floating" ? defaultFloatingStyle : defaultDockedStyle;
|
|
115
|
+
const combinedStyle = style ? { ...computedDefaultStyle, ...style } : computedDefaultStyle;
|
|
116
|
+
|
|
117
|
+
return BottomBarContext.Provider({
|
|
118
|
+
value: ctx,
|
|
119
|
+
children: () => (
|
|
120
|
+
<Tag
|
|
121
|
+
role="toolbar"
|
|
122
|
+
aria-label="Mobile toolbar"
|
|
123
|
+
data-variant={variant}
|
|
124
|
+
data-state={() => (isVisible() ? "visible" : "hidden")}
|
|
125
|
+
class={`visage-bottom-bar visage-bottom-bar-${variant} ${className ?? ""}`.trim()}
|
|
126
|
+
style={combinedStyle}
|
|
127
|
+
{...rest}
|
|
128
|
+
>
|
|
129
|
+
{items
|
|
130
|
+
? items.map((item) => (
|
|
131
|
+
<BottomBarItem
|
|
132
|
+
value={item.value}
|
|
133
|
+
icon={item.icon}
|
|
134
|
+
label={item.label}
|
|
135
|
+
badge={item.badge}
|
|
136
|
+
disabled={item.disabled}
|
|
137
|
+
onClick={item.onClick}
|
|
138
|
+
class={item.class ?? itemClass}
|
|
139
|
+
style={item.style ?? itemStyle}
|
|
140
|
+
/>
|
|
141
|
+
))
|
|
142
|
+
: typeof children === "function"
|
|
143
|
+
? children()
|
|
144
|
+
: children}
|
|
145
|
+
</Tag>
|
|
146
|
+
)
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Child } from "../../dom/render";
|
|
2
|
+
import type { BottomBarSeparatorProps } from "./types";
|
|
3
|
+
|
|
4
|
+
export function BottomBarSeparator(props: BottomBarSeparatorProps): Child {
|
|
5
|
+
const { as: Tag = "div", class: className, style, ...rest } = props;
|
|
6
|
+
|
|
7
|
+
const defaultSeparatorStyle = {
|
|
8
|
+
width: "1px",
|
|
9
|
+
height: "22px",
|
|
10
|
+
backgroundColor: "rgba(255, 255, 255, 0.12)",
|
|
11
|
+
margin: "0 2px",
|
|
12
|
+
flexShrink: 0
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const combinedStyle = style ? { ...defaultSeparatorStyle, ...style } : defaultSeparatorStyle;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Tag
|
|
19
|
+
role="separator"
|
|
20
|
+
aria-orientation="vertical"
|
|
21
|
+
class={`visage-bottom-bar-separator ${className ?? ""}`.trim()}
|
|
22
|
+
style={combinedStyle}
|
|
23
|
+
{...rest}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Child } from "../../dom/render";
|
|
2
|
+
import type { ReadonlySignal } from "../../signals";
|
|
3
|
+
|
|
4
|
+
export interface BottomBarContextValue {
|
|
5
|
+
value: ReadonlySignal<string>;
|
|
6
|
+
setValue: (value: string) => void;
|
|
7
|
+
baseId: string;
|
|
8
|
+
variant: "floating" | "docked";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BottomBarItemConfig {
|
|
12
|
+
value: string;
|
|
13
|
+
label?: Child | (() => Child);
|
|
14
|
+
icon?: Child | (() => Child);
|
|
15
|
+
badge?: Child | number | string | (() => Child | number | string | undefined);
|
|
16
|
+
disabled?: boolean | (() => boolean);
|
|
17
|
+
onClick?: (e: MouseEvent) => void;
|
|
18
|
+
class?: any;
|
|
19
|
+
style?: any;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface BottomBarProps {
|
|
23
|
+
value?: () => string | undefined;
|
|
24
|
+
defaultValue?: string;
|
|
25
|
+
onValueChange?: (value: string) => void;
|
|
26
|
+
variant?: "floating" | "docked";
|
|
27
|
+
hideOnScroll?: boolean;
|
|
28
|
+
scrollThreshold?: number;
|
|
29
|
+
items?: BottomBarItemConfig[];
|
|
30
|
+
itemClass?: any;
|
|
31
|
+
itemStyle?: any;
|
|
32
|
+
as?: string;
|
|
33
|
+
class?: any;
|
|
34
|
+
style?: any;
|
|
35
|
+
children?: Child | (() => Child);
|
|
36
|
+
[key: string]: any;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BottomBarItemProps {
|
|
40
|
+
value: string;
|
|
41
|
+
icon?: Child | (() => Child);
|
|
42
|
+
label?: Child | (() => Child);
|
|
43
|
+
badge?: Child | number | string | (() => Child | number | string | undefined);
|
|
44
|
+
disabled?: boolean | (() => boolean);
|
|
45
|
+
onClick?: (e: MouseEvent) => void;
|
|
46
|
+
as?: string;
|
|
47
|
+
class?: any;
|
|
48
|
+
style?: any;
|
|
49
|
+
children?: Child | (() => Child);
|
|
50
|
+
[key: string]: any;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface BottomBarActionProps {
|
|
54
|
+
icon?: Child | (() => Child);
|
|
55
|
+
label?: string;
|
|
56
|
+
disabled?: boolean | (() => boolean);
|
|
57
|
+
onClick?: (e: MouseEvent) => void;
|
|
58
|
+
as?: string;
|
|
59
|
+
class?: any;
|
|
60
|
+
style?: any;
|
|
61
|
+
children?: Child | (() => Child);
|
|
62
|
+
[key: string]: any;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface BottomBarBadgeProps {
|
|
66
|
+
count?: Child | number | string | (() => Child | number | string | undefined);
|
|
67
|
+
as?: string;
|
|
68
|
+
class?: any;
|
|
69
|
+
style?: any;
|
|
70
|
+
children?: Child | (() => Child);
|
|
71
|
+
[key: string]: any;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface BottomBarSeparatorProps {
|
|
75
|
+
as?: string;
|
|
76
|
+
class?: any;
|
|
77
|
+
style?: any;
|
|
78
|
+
[key: string]: any;
|
|
79
|
+
}
|