@epoch32/visage 1.2.0 → 1.3.1
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/api/index.js +4 -4
- package/dist/charts/index.js +5 -5
- package/dist/devtools/index.js +7 -7
- package/dist/dom/index.js +3 -3
- package/dist/form/index.js +2 -2
- package/dist/{index-grhdtcb5.js → index-0qat98zs.js} +3 -3
- package/dist/{index-h6tjqk6e.js → index-1jcd8gjf.js} +28 -17
- package/dist/{index-6kfe1nr4.js → index-1yhbg34h.js} +41 -9
- package/dist/{index-9fvarkrx.js → index-3y73zgbk.js} +1 -1
- package/dist/{index-y9bq7h9g.js → index-5jhzwtdz.js} +2 -2
- package/dist/{index-a4xns6cc.js → index-65j7a9et.js} +1 -1
- package/dist/{index-dyvgtp43.js → index-9bez1tmf.js} +2 -2
- package/dist/{index-b5wc752a.js → index-aagpn02x.js} +346 -5
- package/dist/{index-df5e614s.js → index-awcrz19g.js} +1 -1
- package/dist/{index-8x8xz80v.js → index-b7emqmej.js} +3 -3
- package/dist/{index-061jf06a.js → index-cgepvvd4.js} +1 -1
- package/dist/{index-1xtrtz1t.js → index-dmhmde2z.js} +2 -2
- package/dist/{index-0madxn60.js → index-enc8cmea.js} +1 -1
- package/dist/{index-js199er4.js → index-g1drdx7k.js} +12 -1
- package/dist/{index-zr6cvset.js → index-ptj8rt6b.js} +5 -5
- package/dist/{index-8czhpea8.js → index-x6whpv54.js} +30 -19
- package/dist/{index-5s255b3w.js → index-xby428n5.js} +23 -22
- package/dist/{index-x2rs3vab.js → index-y3nprgn7.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +32 -18
- package/dist/jsx-runtime/index.js +4 -4
- package/dist/query/index.js +3 -3
- package/dist/router/index.js +5 -5
- package/dist/rpc/index.js +4 -4
- package/dist/server/index.js +2 -2
- package/dist/signals/index.js +1 -1
- package/dist/signals/system.d.ts +2 -0
- package/dist/style/index.js +6 -6
- package/dist/template/index.js +2 -2
- 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 +21 -7
- package/dist/virtual/index.js +5 -5
- package/package.json +1 -1
- package/src/dom/control-flow.ts +31 -21
- package/src/form/form.ts +29 -17
- package/src/index.ts +14 -0
- package/src/signals/effect.ts +2 -2
- package/src/signals/scope.ts +10 -8
- package/src/signals/store.ts +32 -1
- package/src/signals/system.ts +8 -0
- package/src/template/template.ts +12 -1
- 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/virtual/virtualizer.ts +31 -18
package/src/form/form.ts
CHANGED
|
@@ -54,6 +54,32 @@ export function createForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
54
54
|
const submitCount = signal(0);
|
|
55
55
|
const customErrors = signal<Partial<Record<keyof T, string>>>({});
|
|
56
56
|
|
|
57
|
+
const getValues = (): T => {
|
|
58
|
+
const res: Record<string, any> = {};
|
|
59
|
+
for (const key in fields) {
|
|
60
|
+
res[key] = (fields as any)[key].value();
|
|
61
|
+
}
|
|
62
|
+
return res as T;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const schemaValidation = computed(() => {
|
|
66
|
+
if (!options.schema) {
|
|
67
|
+
return { success: true, issuesMap: new Map<string, string>() };
|
|
68
|
+
}
|
|
69
|
+
const currentValues = getValues();
|
|
70
|
+
const res = validateWithSchema(options.schema, currentValues);
|
|
71
|
+
const issuesMap = new Map<string, string>();
|
|
72
|
+
if (!res.success && res.issues) {
|
|
73
|
+
for (const issue of res.issues) {
|
|
74
|
+
const pathKey = String(issue.path);
|
|
75
|
+
if (!issuesMap.has(pathKey)) {
|
|
76
|
+
issuesMap.set(pathKey, issue.message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { success: res.success, issuesMap };
|
|
81
|
+
});
|
|
82
|
+
|
|
57
83
|
for (const key in initialValues) {
|
|
58
84
|
const initVal = initialValues[key];
|
|
59
85
|
const valSignal = signal(initVal);
|
|
@@ -74,25 +100,12 @@ export function createForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
74
100
|
|
|
75
101
|
if (!options.schema) return null;
|
|
76
102
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
if (!res.success && res.issues) {
|
|
80
|
-
const match = res.issues.find((i) => i.path === key || i.path === `${key}`);
|
|
81
|
-
return match ? match.message : null;
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
103
|
+
const validation = schemaValidation();
|
|
104
|
+
return validation.issuesMap.get(key) ?? validation.issuesMap.get(String(key)) ?? null;
|
|
84
105
|
})
|
|
85
106
|
};
|
|
86
107
|
}
|
|
87
108
|
|
|
88
|
-
const getValues = (): T => {
|
|
89
|
-
const res: Record<string, any> = {};
|
|
90
|
-
for (const key in fields) {
|
|
91
|
-
res[key] = (fields as any)[key].value();
|
|
92
|
-
}
|
|
93
|
-
return res as T;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
109
|
const errors = computed<Partial<Record<keyof T, string>>>(() => {
|
|
97
110
|
const errMap: Partial<Record<keyof T, string>> = {};
|
|
98
111
|
for (const key in fields) {
|
|
@@ -118,8 +131,7 @@ export function createForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
118
131
|
|
|
119
132
|
const validate = (): boolean => {
|
|
120
133
|
if (!options.schema) return true;
|
|
121
|
-
|
|
122
|
-
return res.success;
|
|
134
|
+
return schemaValidation().success;
|
|
123
135
|
};
|
|
124
136
|
|
|
125
137
|
const handleSubmit = async (e?: Event): Promise<boolean> => {
|
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,
|
package/src/signals/effect.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { Computed } from "./computed";
|
|
|
5
5
|
import type { Scope } from "./scope";
|
|
6
6
|
import {
|
|
7
7
|
addPendingEffect,
|
|
8
|
-
batchDepth,
|
|
9
8
|
flushEffects,
|
|
9
|
+
getBatchDepth,
|
|
10
10
|
getCurrentScope,
|
|
11
11
|
getCurrentSubscriber,
|
|
12
12
|
removePendingEffect,
|
|
@@ -57,7 +57,7 @@ export class Effect implements ISubscriber {
|
|
|
57
57
|
this.state = targetState;
|
|
58
58
|
addPendingEffect(this);
|
|
59
59
|
|
|
60
|
-
if (
|
|
60
|
+
if (getBatchDepth() === 0) {
|
|
61
61
|
flushEffects();
|
|
62
62
|
}
|
|
63
63
|
}
|
package/src/signals/scope.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Visage Reactive Scope & Lifecycle Management
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { getCurrentScope, getCurrentSubscriber, setCurrentScope } from "./system";
|
|
5
5
|
import type { CleanupFn, ISubscriber } from "./types";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -14,7 +14,7 @@ export class Scope {
|
|
|
14
14
|
isDisposed = false;
|
|
15
15
|
context?: Map<symbol, any>;
|
|
16
16
|
|
|
17
|
-
constructor(parent: Scope | null =
|
|
17
|
+
constructor(parent: Scope | null = getCurrentScope()) {
|
|
18
18
|
this.parent = parent;
|
|
19
19
|
if (parent && !parent.isDisposed) {
|
|
20
20
|
parent.cleanups.push(() => this.dispose());
|
|
@@ -72,10 +72,12 @@ export class Scope {
|
|
|
72
72
|
* ```
|
|
73
73
|
*/
|
|
74
74
|
export function onCleanup(fn: CleanupFn): void {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
const curSub = getCurrentSubscriber();
|
|
76
|
+
const curScope = getCurrentScope();
|
|
77
|
+
if (curSub && "addCleanup" in curSub) {
|
|
78
|
+
(curSub as any).addCleanup(fn);
|
|
79
|
+
} else if (curScope) {
|
|
80
|
+
curScope.addCleanup(fn);
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
|
|
@@ -101,7 +103,7 @@ export function onCleanup(fn: CleanupFn): void {
|
|
|
101
103
|
* @returns The return value of `fn`
|
|
102
104
|
*/
|
|
103
105
|
export function createRoot<T>(fn: (dispose: () => void) => T): T {
|
|
104
|
-
const prevScope =
|
|
106
|
+
const prevScope = getCurrentScope();
|
|
105
107
|
const scope = new Scope(null);
|
|
106
108
|
setCurrentScope(scope);
|
|
107
109
|
try {
|
|
@@ -115,7 +117,7 @@ export function createRoot<T>(fn: (dispose: () => void) => T): T {
|
|
|
115
117
|
* Run a function with a specified reactive scope active.
|
|
116
118
|
*/
|
|
117
119
|
export function runInScope<T>(scope: Scope | null, fn: () => T): T {
|
|
118
|
-
const prevScope =
|
|
120
|
+
const prevScope = getCurrentScope();
|
|
119
121
|
setCurrentScope(scope);
|
|
120
122
|
try {
|
|
121
123
|
return fn();
|
package/src/signals/store.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { isSignal } from "./types";
|
|
|
5
5
|
const STORE_RAW = Symbol("visage.store.raw");
|
|
6
6
|
const STORE_SIGNALS = Symbol("visage.store.signals");
|
|
7
7
|
const STORE_PROXY = Symbol("visage.store.proxy");
|
|
8
|
+
const STORE_ITERATE = Symbol("visage.store.iterate");
|
|
8
9
|
|
|
9
10
|
export type Store<T> = T;
|
|
10
11
|
|
|
@@ -61,9 +62,29 @@ function createStoreProxy<T extends object>(target: T): T {
|
|
|
61
62
|
return value;
|
|
62
63
|
},
|
|
63
64
|
|
|
65
|
+
has(target, prop) {
|
|
66
|
+
if (prop === STORE_RAW || prop === STORE_PROXY) return true;
|
|
67
|
+
if (typeof prop === "string" || typeof prop === "symbol") {
|
|
68
|
+
const sig = getOrCreatePropertySignal(target, prop);
|
|
69
|
+
sig.get();
|
|
70
|
+
}
|
|
71
|
+
return Reflect.has(target, prop);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
ownKeys(target) {
|
|
75
|
+
const iterSig = getOrCreatePropertySignal(target, STORE_ITERATE);
|
|
76
|
+
iterSig.get();
|
|
77
|
+
return Reflect.ownKeys(target);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
81
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
82
|
+
},
|
|
83
|
+
|
|
64
84
|
set(target, prop, value, receiver) {
|
|
85
|
+
const hadProp = Reflect.has(target, prop);
|
|
65
86
|
const prevValue = Reflect.get(target, prop, receiver);
|
|
66
|
-
if (Object.is(prevValue, value)) {
|
|
87
|
+
if (hadProp && Object.is(prevValue, value)) {
|
|
67
88
|
return true;
|
|
68
89
|
}
|
|
69
90
|
|
|
@@ -72,10 +93,18 @@ function createStoreProxy<T extends object>(target: T): T {
|
|
|
72
93
|
const sig = getOrCreatePropertySignal(target, prop);
|
|
73
94
|
sig.set(value);
|
|
74
95
|
|
|
96
|
+
// If a new property was added, notify key iteration
|
|
97
|
+
if (!hadProp) {
|
|
98
|
+
const iterSig = getOrCreatePropertySignal(target, STORE_ITERATE);
|
|
99
|
+
iterSig.update((prev: number = 0) => prev + 1);
|
|
100
|
+
}
|
|
101
|
+
|
|
75
102
|
// If array length or property keys changed, notify length/keys
|
|
76
103
|
if (Array.isArray(target) && prop !== "length") {
|
|
77
104
|
const lenSig = getOrCreatePropertySignal(target, "length");
|
|
78
105
|
lenSig.set(target.length);
|
|
106
|
+
const iterSig = getOrCreatePropertySignal(target, STORE_ITERATE);
|
|
107
|
+
iterSig.update((prev: number = 0) => prev + 1);
|
|
79
108
|
}
|
|
80
109
|
}
|
|
81
110
|
return success;
|
|
@@ -87,6 +116,8 @@ function createStoreProxy<T extends object>(target: T): T {
|
|
|
87
116
|
if (hadProp && success) {
|
|
88
117
|
const sig = getOrCreatePropertySignal(target, prop);
|
|
89
118
|
sig.set(undefined);
|
|
119
|
+
const iterSig = getOrCreatePropertySignal(target, STORE_ITERATE);
|
|
120
|
+
iterSig.update((prev: number = 0) => prev + 1);
|
|
90
121
|
}
|
|
91
122
|
return success;
|
|
92
123
|
}
|
package/src/signals/system.ts
CHANGED
|
@@ -59,6 +59,14 @@ export function removePendingEffect(eff: any): void {
|
|
|
59
59
|
ctx.pendingEffects.delete(eff);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export function getBatchDepth(): number {
|
|
63
|
+
return ctx.batchDepth;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getIsFlushing(): boolean {
|
|
67
|
+
return ctx.isFlushing;
|
|
68
|
+
}
|
|
69
|
+
|
|
62
70
|
const MAX_FLUSH_COUNT = 100;
|
|
63
71
|
|
|
64
72
|
export function flushEffects(): void {
|
package/src/template/template.ts
CHANGED
|
@@ -66,11 +66,22 @@ export function html(
|
|
|
66
66
|
|
|
67
67
|
for (const bind of bindings) {
|
|
68
68
|
if (bind.isAttr && bind.attrName) {
|
|
69
|
-
const targetEl = findElementWithAttributeValue(root, bind.attrName, bind.id);
|
|
69
|
+
const targetEl = findElementWithAttributeValue(root as HTMLElement, bind.attrName, bind.id);
|
|
70
70
|
if (targetEl) {
|
|
71
71
|
targetEl.removeAttribute(bind.attrName);
|
|
72
72
|
setAttribute(targetEl, bind.attrName, bind.value);
|
|
73
73
|
}
|
|
74
|
+
} else if (bind.isAttr && !bind.attrName) {
|
|
75
|
+
const propAttr = `data-visage-prop-${bind.id.replace(MARKER_PREFIX, "")}`;
|
|
76
|
+
const targetEl = findElementWithAttributeValue(root as HTMLElement, propAttr, bind.id);
|
|
77
|
+
if (targetEl) {
|
|
78
|
+
targetEl.removeAttribute(propAttr);
|
|
79
|
+
if (typeof bind.value === "object" && bind.value !== null) {
|
|
80
|
+
for (const key of Object.keys(bind.value)) {
|
|
81
|
+
setAttribute(targetEl, key, bind.value[key]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
74
85
|
} else if (!bind.isAttr) {
|
|
75
86
|
const commentNode = findCommentNode(root, bind.id);
|
|
76
87
|
if (commentNode?.parentNode) {
|
|
@@ -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
|
+
}
|