@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.
Files changed (64) hide show
  1. package/dist/api/index.js +4 -4
  2. package/dist/charts/index.js +5 -5
  3. package/dist/devtools/index.js +7 -7
  4. package/dist/dom/index.js +3 -3
  5. package/dist/form/index.js +2 -2
  6. package/dist/{index-grhdtcb5.js → index-0qat98zs.js} +3 -3
  7. package/dist/{index-h6tjqk6e.js → index-1jcd8gjf.js} +28 -17
  8. package/dist/{index-6kfe1nr4.js → index-1yhbg34h.js} +41 -9
  9. package/dist/{index-9fvarkrx.js → index-3y73zgbk.js} +1 -1
  10. package/dist/{index-y9bq7h9g.js → index-5jhzwtdz.js} +2 -2
  11. package/dist/{index-a4xns6cc.js → index-65j7a9et.js} +1 -1
  12. package/dist/{index-dyvgtp43.js → index-9bez1tmf.js} +2 -2
  13. package/dist/{index-b5wc752a.js → index-aagpn02x.js} +346 -5
  14. package/dist/{index-df5e614s.js → index-awcrz19g.js} +1 -1
  15. package/dist/{index-8x8xz80v.js → index-b7emqmej.js} +3 -3
  16. package/dist/{index-061jf06a.js → index-cgepvvd4.js} +1 -1
  17. package/dist/{index-1xtrtz1t.js → index-dmhmde2z.js} +2 -2
  18. package/dist/{index-0madxn60.js → index-enc8cmea.js} +1 -1
  19. package/dist/{index-js199er4.js → index-g1drdx7k.js} +12 -1
  20. package/dist/{index-zr6cvset.js → index-ptj8rt6b.js} +5 -5
  21. package/dist/{index-8czhpea8.js → index-x6whpv54.js} +30 -19
  22. package/dist/{index-5s255b3w.js → index-xby428n5.js} +23 -22
  23. package/dist/{index-x2rs3vab.js → index-y3nprgn7.js} +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +32 -18
  26. package/dist/jsx-runtime/index.js +4 -4
  27. package/dist/query/index.js +3 -3
  28. package/dist/router/index.js +5 -5
  29. package/dist/rpc/index.js +4 -4
  30. package/dist/server/index.js +2 -2
  31. package/dist/signals/index.js +1 -1
  32. package/dist/signals/system.d.ts +2 -0
  33. package/dist/style/index.js +6 -6
  34. package/dist/template/index.js +2 -2
  35. package/dist/ui/bottom-bar/action.d.ts +3 -0
  36. package/dist/ui/bottom-bar/badge.d.ts +3 -0
  37. package/dist/ui/bottom-bar/context.d.ts +3 -0
  38. package/dist/ui/bottom-bar/index.d.ts +19 -0
  39. package/dist/ui/bottom-bar/item.d.ts +3 -0
  40. package/dist/ui/bottom-bar/root.d.ts +3 -0
  41. package/dist/ui/bottom-bar/separator.d.ts +3 -0
  42. package/dist/ui/bottom-bar/types.d.ts +72 -0
  43. package/dist/ui/index.d.ts +1 -0
  44. package/dist/ui/index.js +21 -7
  45. package/dist/virtual/index.js +5 -5
  46. package/package.json +1 -1
  47. package/src/dom/control-flow.ts +31 -21
  48. package/src/form/form.ts +29 -17
  49. package/src/index.ts +14 -0
  50. package/src/signals/effect.ts +2 -2
  51. package/src/signals/scope.ts +10 -8
  52. package/src/signals/store.ts +32 -1
  53. package/src/signals/system.ts +8 -0
  54. package/src/template/template.ts +12 -1
  55. package/src/ui/bottom-bar/action.tsx +60 -0
  56. package/src/ui/bottom-bar/badge.tsx +51 -0
  57. package/src/ui/bottom-bar/context.ts +8 -0
  58. package/src/ui/bottom-bar/index.ts +29 -0
  59. package/src/ui/bottom-bar/item.tsx +119 -0
  60. package/src/ui/bottom-bar/root.tsx +148 -0
  61. package/src/ui/bottom-bar/separator.tsx +26 -0
  62. package/src/ui/bottom-bar/types.ts +79 -0
  63. package/src/ui/index.ts +16 -0
  64. package/src/virtual/virtualizer.ts +31 -18
@@ -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
+ }
package/src/ui/index.ts CHANGED
@@ -10,6 +10,22 @@ export {
10
10
  useAccordionContext,
11
11
  useAccordionItemContext
12
12
  } from "./accordion";
13
+ export {
14
+ BottomBar,
15
+ BottomBarAction,
16
+ type BottomBarActionProps,
17
+ BottomBarBadge,
18
+ type BottomBarBadgeProps,
19
+ type BottomBarContextValue,
20
+ BottomBarItem,
21
+ type BottomBarItemConfig,
22
+ type BottomBarItemProps,
23
+ type BottomBarProps,
24
+ BottomBarRoot,
25
+ BottomBarSeparator,
26
+ type BottomBarSeparatorProps,
27
+ useBottomBarContext
28
+ } from "./bottom-bar";
13
29
  export {
14
30
  Command,
15
31
  CommandDialog,
@@ -56,30 +56,41 @@ export function createVirtualizer(options: VirtualizerOptions): Virtualizer {
56
56
  return getItemHeight(index);
57
57
  };
58
58
 
59
- // Compute prefix offsets for fast range calculation
60
- const totalSize = computed(() => {
61
- const count = getCount();
62
- if (typeof options.itemHeight === "number" && dynamicSizeCache.size === 0) {
63
- return count * options.itemHeight;
64
- }
65
- let total = 0;
66
- for (let i = 0; i < count; i++) {
67
- total += getItemSize(i);
68
- }
69
- return total;
70
- });
59
+ // Cached prefix offsets for O(1) offset lookups and true O(log N) binary search
60
+ const offsetsCache: number[] = [0];
61
+ let offsetsValidUpTo = 0;
62
+
63
+ const invalidateOffsetsFrom = (index: number) => {
64
+ offsetsValidUpTo = Math.min(offsetsValidUpTo, index);
65
+ };
71
66
 
72
67
  const getOffsetForIndex = (targetIndex: number): number => {
68
+ if (targetIndex <= 0) return 0;
73
69
  if (typeof options.itemHeight === "number" && dynamicSizeCache.size === 0) {
74
70
  return targetIndex * options.itemHeight;
75
71
  }
76
- let offset = 0;
77
- for (let i = 0; i < targetIndex; i++) {
78
- offset += getItemSize(i);
72
+ if (targetIndex <= offsetsValidUpTo) {
73
+ return offsetsCache[targetIndex]!;
79
74
  }
80
- return offset;
75
+ for (let i = offsetsValidUpTo; i < targetIndex; i++) {
76
+ offsetsCache[i + 1] = (offsetsCache[i] ?? 0) + getItemSize(i);
77
+ }
78
+ offsetsValidUpTo = targetIndex;
79
+ return offsetsCache[targetIndex]!;
81
80
  };
82
81
 
82
+ const measurementVersion = signal(0, { internal: true });
83
+
84
+ // Compute prefix offsets for fast range calculation
85
+ const totalSize = computed(() => {
86
+ measurementVersion();
87
+ const count = getCount();
88
+ if (typeof options.itemHeight === "number" && dynamicSizeCache.size === 0) {
89
+ return count * options.itemHeight;
90
+ }
91
+ return getOffsetForIndex(count);
92
+ });
93
+
83
94
  const findIndexAtOffset = (offset: number): number => {
84
95
  const count = getCount();
85
96
  if (count === 0) return 0;
@@ -110,12 +121,14 @@ export function createVirtualizer(options: VirtualizerOptions): Virtualizer {
110
121
  };
111
122
 
112
123
  const startIndex = computed(() => {
124
+ measurementVersion();
113
125
  const offset = scrollOffset();
114
126
  const rawStart = findIndexAtOffset(offset);
115
127
  return Math.max(0, rawStart - overscan);
116
128
  });
117
129
 
118
130
  const endIndex = computed(() => {
131
+ measurementVersion();
119
132
  const count = getCount();
120
133
  if (count === 0) return 0;
121
134
  const endOffset = scrollOffset() + (containerSize() || 500);
@@ -255,8 +268,8 @@ export function createVirtualizer(options: VirtualizerOptions): Virtualizer {
255
268
  : element.getBoundingClientRect().height;
256
269
  if (measuredSize > 0 && dynamicSizeCache.get(index) !== measuredSize) {
257
270
  dynamicSizeCache.set(index, measuredSize);
258
- // Force recalculation by touching scrollOffset
259
- scrollOffset.update((o) => o);
271
+ invalidateOffsetsFrom(index);
272
+ measurementVersion.update((v) => v + 1);
260
273
  }
261
274
  };
262
275