@epoch32/visage 1.3.1 → 1.3.2

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.
@@ -4,8 +4,8 @@ import {
4
4
  highlightElement2,
5
5
  VisageDevTools2,
6
6
  connectDevTools2
7
- } from "../index-ptj8rt6b.js";
8
- import"../index-b7emqmej.js";
7
+ } from "../index-dz6n88aq.js";
8
+ import"../index-kykzcs7p.js";
9
9
  import"../index-enc8cmea.js";
10
10
  import"../index-3y73zgbk.js";
11
11
  import"../index-xby428n5.js";
@@ -16,7 +16,7 @@ import {
16
16
  Tabs2,
17
17
  Tooltip3,
18
18
  Switch2
19
- } from "./index-b7emqmej.js";
19
+ } from "./index-kykzcs7p.js";
20
20
  import {
21
21
  Style2
22
22
  } from "./index-enc8cmea.js";
@@ -167,7 +167,16 @@ function Routes2(props) {
167
167
  }
168
168
  function Link2(props) {
169
169
  const router = useContext2(RouterContext);
170
- const { href, class: className, activeClass = "active", replace, children, ...rest } = props;
170
+ const {
171
+ href,
172
+ class: classProp,
173
+ className: classNameProp,
174
+ activeClass = "active",
175
+ replace,
176
+ children,
177
+ ...rest
178
+ } = props;
179
+ const rawClass = classProp ?? classNameProp;
171
180
  const handleClick = (e) => {
172
181
  if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.button !== 0) {
173
182
  return;
@@ -186,7 +195,7 @@ function Link2(props) {
186
195
  return current === href || href !== "/" && current.startsWith(href);
187
196
  });
188
197
  const computedClass = computed2(() => {
189
- const baseCls = typeof className === "function" ? className() : className ?? "";
198
+ const baseCls = typeof rawClass === "function" ? rawClass() : rawClass ?? "";
190
199
  if (activeClass && isActive()) {
191
200
  return baseCls ? `${baseCls} ${activeClass}` : activeClass;
192
201
  }
@@ -69,7 +69,8 @@ function onClickOutside2(elements, handler) {
69
69
  const target = event.target;
70
70
  if (!target)
71
71
  return;
72
- const list = Array.isArray(elements) ? elements : [elements];
72
+ const raw = typeof elements === "function" ? elements() : elements;
73
+ const list = Array.isArray(raw) ? raw : [raw];
73
74
  const isInside = list.some((el) => el?.contains(target));
74
75
  if (!isInside) {
75
76
  handler(event);
@@ -21,7 +21,7 @@ import {
21
21
  createControllableSignal2,
22
22
  trapFocus2,
23
23
  onClickOutside2
24
- } from "./index-b7emqmej.js";
24
+ } from "./index-kykzcs7p.js";
25
25
 
26
26
  // src/ui/accordion.tsx
27
27
  var AccordionContext = createContext2(null);
@@ -539,6 +539,67 @@ var BottomBar2 = Object.assign(BottomBarRoot2, {
539
539
  Badge: BottomBarBadge2,
540
540
  Separator: BottomBarSeparator2
541
541
  });
542
+ // src/ui/overlay.ts
543
+ function useDismiss2(element, options) {
544
+ const isEnabled = () => {
545
+ if (typeof options.enabled === "function")
546
+ return options.enabled();
547
+ return options.enabled ?? true;
548
+ };
549
+ const handleKeyDown = (e) => {
550
+ if (!isEnabled() || e.defaultPrevented)
551
+ return;
552
+ if (e.key === "Escape" && (options.escapeKey ?? true)) {
553
+ if (options.onEscapeKeyDown) {
554
+ options.onEscapeKeyDown(e);
555
+ }
556
+ if (!e.defaultPrevented) {
557
+ options.onDismiss();
558
+ e.stopPropagation();
559
+ }
560
+ } else if (e.key === "Tab" && options.trapFocus) {
561
+ const el = element();
562
+ if (el) {
563
+ trapFocus2(el, e);
564
+ }
565
+ }
566
+ };
567
+ const handleClick = (e) => {
568
+ if (!isEnabled() || e.defaultPrevented)
569
+ return;
570
+ if (options.closeSelector) {
571
+ const target = e.target;
572
+ if (target?.closest(options.closeSelector)) {
573
+ options.onDismiss();
574
+ }
575
+ }
576
+ };
577
+ if (typeof document !== "undefined") {
578
+ const unbind = onClickOutside2(() => {
579
+ const el = element();
580
+ const rawIgnores = typeof options.outsideIgnore === "function" ? options.outsideIgnore() : options.outsideIgnore;
581
+ const ignores = Array.isArray(rawIgnores) ? rawIgnores : rawIgnores ? [rawIgnores] : [];
582
+ return [el, ...ignores];
583
+ }, (event) => {
584
+ if (!isEnabled())
585
+ return;
586
+ if (options.outsidePointer === false)
587
+ return;
588
+ if (options.onOutsidePointer) {
589
+ options.onOutsidePointer(event);
590
+ }
591
+ if (!event.defaultPrevented) {
592
+ options.onDismiss();
593
+ }
594
+ });
595
+ onCleanup2(unbind);
596
+ }
597
+ return {
598
+ handleKeyDown,
599
+ handleClick
600
+ };
601
+ }
602
+
542
603
  // src/ui/command-dialog.tsx
543
604
  function CommandDialog2(props) {
544
605
  const [open, setOpen] = createControllableSignal2({
@@ -546,6 +607,13 @@ function CommandDialog2(props) {
546
607
  defaultValue: props.defaultOpen ?? false,
547
608
  onChange: props.onOpenChange
548
609
  });
610
+ const contentRef = { current: null };
611
+ const dismiss = useDismiss2(() => contentRef.current, {
612
+ enabled: () => open(),
613
+ onDismiss: () => setOpen(false),
614
+ escapeKey: true,
615
+ outsidePointer: false
616
+ });
549
617
  const { overlayClass, contentClass, style, ...rest } = props;
550
618
  return /* @__PURE__ */ jsx2(Portal2, {
551
619
  children: /* @__PURE__ */ jsx2("div", {
@@ -565,7 +633,11 @@ function CommandDialog2(props) {
565
633
  if (e.target === e.currentTarget)
566
634
  setOpen(false);
567
635
  },
636
+ onKeyDown: dismiss.handleKeyDown,
568
637
  children: /* @__PURE__ */ jsx2("div", {
638
+ ref: (el) => {
639
+ contentRef.current = el;
640
+ },
569
641
  style: { width: "100%", maxWidth: "560px" },
570
642
  class: contentClass,
571
643
  children: /* @__PURE__ */ jsx2(CommandRoot, {
@@ -1251,27 +1323,27 @@ function DialogOverlay2(props) {
1251
1323
  }
1252
1324
  function DialogContent2(props) {
1253
1325
  const ctx = useContext2(DialogContext);
1326
+ const dismiss = useDismiss2(() => ctx?.contentRef.current, {
1327
+ enabled: () => ctx?.open() ?? false,
1328
+ onDismiss: () => ctx?.setOpen(false),
1329
+ escapeKey: true,
1330
+ trapFocus: true,
1331
+ outsidePointer: false,
1332
+ closeSelector: "[data-dialog-close]",
1333
+ onEscapeKeyDown: (e) => {
1334
+ if (props.onKeyDown)
1335
+ props.onKeyDown(e);
1336
+ }
1337
+ });
1254
1338
  const handleKeyDown = (e) => {
1255
1339
  if (props.onKeyDown)
1256
1340
  props.onKeyDown(e);
1257
- if (e.defaultPrevented)
1258
- return;
1259
- if (e.key === "Escape") {
1260
- ctx?.setOpen(false);
1261
- e.stopPropagation();
1262
- } else if (e.key === "Tab" && ctx?.contentRef.current) {
1263
- trapFocus2(ctx.contentRef.current, e);
1264
- }
1341
+ dismiss.handleKeyDown(e);
1265
1342
  };
1266
1343
  const handleClick = (e) => {
1267
1344
  if (props.onClick)
1268
1345
  props.onClick(e);
1269
- if (e.defaultPrevented)
1270
- return;
1271
- const target = e.target;
1272
- if (target?.closest("[data-dialog-close]")) {
1273
- ctx?.setOpen(false);
1274
- }
1346
+ dismiss.handleClick(e);
1275
1347
  };
1276
1348
  const {
1277
1349
  as: Tag = "div",
@@ -1502,6 +1574,18 @@ function DropdownMenuPortal2(props) {
1502
1574
  }
1503
1575
  function DropdownMenuContent2(props) {
1504
1576
  const ctx = useContext2(DropdownMenuContext);
1577
+ const dismiss = useDismiss2(() => ctx?.contentRef.current, {
1578
+ enabled: () => ctx?.open() ?? false,
1579
+ onDismiss: () => {
1580
+ if (ctx) {
1581
+ ctx.setOpen(false);
1582
+ ctx.triggerRef.current?.focus();
1583
+ }
1584
+ },
1585
+ escapeKey: true,
1586
+ outsidePointer: true,
1587
+ outsideIgnore: () => [ctx?.triggerRef.current]
1588
+ });
1505
1589
  const handleKeyDown = (e) => {
1506
1590
  if (props.onKeyDown)
1507
1591
  props.onKeyDown(e);
@@ -1528,10 +1612,7 @@ function DropdownMenuContent2(props) {
1528
1612
  items[items.length - 1]?.focus();
1529
1613
  e.preventDefault();
1530
1614
  } else if (e.key === "Escape") {
1531
- if (ctx) {
1532
- ctx.setOpen(false);
1533
- ctx.triggerRef.current?.focus();
1534
- }
1615
+ dismiss.handleKeyDown(e);
1535
1616
  e.preventDefault();
1536
1617
  } else if (e.key === "Tab") {
1537
1618
  if (ctx)
@@ -1552,14 +1633,6 @@ function DropdownMenuContent2(props) {
1552
1633
  ref: (el) => {
1553
1634
  if (ctx) {
1554
1635
  ctx.contentRef.current = el;
1555
- if (el) {
1556
- const cleanup = onClickOutside2([ctx.triggerRef.current, el], () => {
1557
- if (ctx.open()) {
1558
- ctx.setOpen(false);
1559
- }
1560
- });
1561
- onCleanup2(cleanup);
1562
- }
1563
1636
  }
1564
1637
  },
1565
1638
  id: ctx?.menuId,
@@ -1776,26 +1849,30 @@ function PopoverPortal2(props) {
1776
1849
  }
1777
1850
  function PopoverContent2(props) {
1778
1851
  const ctx = useContext2(PopoverContext);
1852
+ const dismiss = useDismiss2(() => ctx?.contentRef.current, {
1853
+ enabled: () => ctx?.open() ?? false,
1854
+ onDismiss: () => ctx?.setOpen(false),
1855
+ escapeKey: true,
1856
+ outsidePointer: true,
1857
+ outsideIgnore: () => [ctx?.triggerRef.current],
1858
+ closeSelector: "[data-popover-close]",
1859
+ onEscapeKeyDown: (e) => {
1860
+ if (props.onKeyDown)
1861
+ props.onKeyDown(e);
1862
+ if (!e.defaultPrevented) {
1863
+ ctx?.triggerRef.current?.focus();
1864
+ }
1865
+ }
1866
+ });
1779
1867
  const handleKeyDown = (e) => {
1780
1868
  if (props.onKeyDown)
1781
1869
  props.onKeyDown(e);
1782
- if (e.defaultPrevented)
1783
- return;
1784
- if (e.key === "Escape" && ctx) {
1785
- ctx.setOpen(false);
1786
- ctx.triggerRef.current?.focus();
1787
- e.stopPropagation();
1788
- }
1870
+ dismiss.handleKeyDown(e);
1789
1871
  };
1790
1872
  const handleClick = (e) => {
1791
1873
  if (props.onClick)
1792
1874
  props.onClick(e);
1793
- if (e.defaultPrevented)
1794
- return;
1795
- const target = e.target;
1796
- if (target?.closest("[data-popover-close]")) {
1797
- ctx?.setOpen(false);
1798
- }
1875
+ dismiss.handleClick(e);
1799
1876
  };
1800
1877
  const {
1801
1878
  as: Tag = "div",
@@ -1812,14 +1889,6 @@ function PopoverContent2(props) {
1812
1889
  ref: (el) => {
1813
1890
  if (ctx) {
1814
1891
  ctx.contentRef.current = el;
1815
- if (el) {
1816
- const cleanup = onClickOutside2([ctx.triggerRef.current, el], () => {
1817
- if (ctx.open()) {
1818
- ctx.setOpen(false);
1819
- }
1820
- });
1821
- onCleanup2(cleanup);
1822
- }
1823
1892
  }
1824
1893
  },
1825
1894
  id: ctx?.contentId,
@@ -2034,4 +2103,4 @@ var Toast2 = Object.assign(ToastRoot, {
2034
2103
  Description: ToastDescription2,
2035
2104
  Close: ToastClose2
2036
2105
  });
2037
- export { useAccordionContext2, useAccordionItemContext2, AccordionItem2, AccordionTrigger2, AccordionContent2, Accordion2, BottomBarAction2, BottomBarBadge2, useBottomBarContext2, BottomBarItem2, BottomBarRoot2, BottomBarSeparator2, BottomBar2, CommandDialog2, useCommandContext2, CommandInput2, CommandList2, CommandEmpty2, CommandGroup2, CommandItem2, Command2, DataTable2, useDialogContext2, DialogTrigger2, DialogPortal2, DialogOverlay2, DialogContent2, DialogTitle2, DialogDescription2, DialogClose2, Dialog2, useDropdownMenuContext2, DropdownMenuTrigger2, DropdownMenuPortal2, DropdownMenuContent2, DropdownMenuItem2, DropdownMenuSeparator2, DropdownMenu2, createIcon2, usePopoverContext2, PopoverTrigger2, PopoverPortal2, PopoverContent2, PopoverClose2, Popover2, createToastManager2, defaultToastManager2, toast2, useToast2, ToastProvider2, ToastViewport2, ToastTitle2, ToastDescription2, ToastClose2, Toast2 };
2106
+ export { useAccordionContext2, useAccordionItemContext2, AccordionItem2, AccordionTrigger2, AccordionContent2, Accordion2, BottomBarAction2, BottomBarBadge2, useBottomBarContext2, BottomBarItem2, BottomBarRoot2, BottomBarSeparator2, BottomBar2, useDismiss2, CommandDialog2, useCommandContext2, CommandInput2, CommandList2, CommandEmpty2, CommandGroup2, CommandItem2, Command2, DataTable2, useDialogContext2, DialogTrigger2, DialogPortal2, DialogOverlay2, DialogContent2, DialogTitle2, DialogDescription2, DialogClose2, Dialog2, useDropdownMenuContext2, DropdownMenuTrigger2, DropdownMenuPortal2, DropdownMenuContent2, DropdownMenuItem2, DropdownMenuSeparator2, DropdownMenu2, createIcon2, usePopoverContext2, PopoverTrigger2, PopoverPortal2, PopoverContent2, PopoverClose2, Popover2, createToastManager2, defaultToastManager2, toast2, useToast2, ToastProvider2, ToastViewport2, ToastTitle2, ToastDescription2, ToastClose2, Toast2 };
package/dist/index.d.ts CHANGED
@@ -11,5 +11,5 @@ export { hydrate, renderChildToString, renderToStream, renderToString, type Stre
11
11
  export { batch, type CleanupFn, Computed, type Context, computed, createContext, createEffect, createMemo, createRoot, createSignal, createStore, type Dependency, Effect, effect, getActiveScope, isSignal, mutateStore, onCleanup, type ReadonlySignal, runInScope, Scope, Signal, type Store, type Subscriber, signal, untrack, unwrapStore, useContext, type WritableSignal } from "./signals";
12
12
  export { type BaseLayoutProps, Center, type CenterProps, type ClassValue, createStyle, css, cx, DIMENSION_KEYWORDS, em, extractStyleTags, fluid, formatStyleProp, type GlassOptions, Grid, type GridProps, getScopeId, glass, hashString, hasReactiveClass, normalizeClass, px, type RadiusVariant, Row, type RowProps, radius, rem, type ScopeOptions, type ShadowVariant, Spacer, type SpacerProps, Stack, type StackProps, Style, type StyleClasses, StyleManager, type StyleProps, scopeCSS, scopeSelector, shadow, space, styleManager, UNITLESS_PROPERTIES, useStyle } from "./style";
13
13
  export { html, template } from "./template";
14
- export { Accordion, AccordionContent, AccordionItem, type AccordionItemProps, type AccordionMultipleProps, type AccordionProps, type AccordionSingleProps, AccordionTrigger, BottomBar, BottomBarAction, type BottomBarActionProps, BottomBarBadge, type BottomBarBadgeProps, type BottomBarContextValue, BottomBarItem, type BottomBarItemConfig, type BottomBarItemProps, type BottomBarProps, BottomBarRoot, BottomBarSeparator, type BottomBarSeparatorProps, type ColumnDef, Command, CommandDialog, type CommandDialogProps, CommandEmpty, CommandGroup, type CommandGroupData, CommandInput, type CommandInputProps, CommandItem, type CommandItemData, type CommandItemProps, CommandList, type CommandProps, type CreateTabsOptions, createControllableSignal, createIcon, createId, createTabs, createToastManager, DataTable, type DataTableProps, Dialog, DialogClose, DialogContent, type DialogContentProps, DialogDescription, DialogOverlay, DialogPortal, type DialogProps, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuItem, type DropdownMenuItemProps, DropdownMenuPortal, type DropdownMenuProps, DropdownMenuSeparator, DropdownMenuTrigger, defaultToastManager, type IconNode, type IconProps, onClickOutside, Popover, PopoverClose, PopoverContent, type PopoverContentProps, PopoverPortal, type PopoverProps, PopoverTrigger, type SortDirection, type SortState, Switch, type SwitchProps, SwitchThumb, type SwitchThumbProps, Tabs, TabsContent, type TabsContentProps, TabsList, type TabsProps, TabsTrigger, Toast, ToastClose, type ToastData, ToastDescription, type ToastManager, type ToastOptions, type ToastProps, ToastProvider, type ToastProviderProps, ToastTitle, type ToastVariant, ToastViewport, type ToastViewportProps, Tooltip, TooltipContent, type TooltipContentProps, TooltipPortal, type TooltipProps, TooltipTrigger, toast, trapFocus, useAccordionContext, useAccordionItemContext, useBottomBarContext, useCommandContext, useDialogContext, useDropdownMenuContext, usePopoverContext, useTabsContext, useToast, useTooltipContext } from "./ui";
14
+ export { Accordion, AccordionContent, AccordionItem, type AccordionItemProps, type AccordionMultipleProps, type AccordionProps, type AccordionSingleProps, AccordionTrigger, BottomBar, BottomBarAction, type BottomBarActionProps, BottomBarBadge, type BottomBarBadgeProps, type BottomBarContextValue, BottomBarItem, type BottomBarItemConfig, type BottomBarItemProps, type BottomBarProps, BottomBarRoot, BottomBarSeparator, type BottomBarSeparatorProps, type ColumnDef, Command, CommandDialog, type CommandDialogProps, CommandEmpty, CommandGroup, type CommandGroupData, CommandInput, type CommandInputProps, CommandItem, type CommandItemData, type CommandItemProps, CommandList, type CommandProps, type CreateTabsOptions, createControllableSignal, createIcon, createId, createTabs, createToastManager, DataTable, type DataTableProps, Dialog, DialogClose, DialogContent, type DialogContentProps, DialogDescription, DialogOverlay, DialogPortal, type DialogProps, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuItem, type DropdownMenuItemProps, DropdownMenuPortal, type DropdownMenuProps, DropdownMenuSeparator, DropdownMenuTrigger, defaultToastManager, type IconNode, type IconProps, onClickOutside, Popover, PopoverClose, PopoverContent, type PopoverContentProps, PopoverPortal, type PopoverProps, PopoverTrigger, type SortDirection, type SortState, Switch, type SwitchProps, SwitchThumb, type SwitchThumbProps, Tabs, TabsContent, type TabsContentProps, TabsList, type TabsProps, TabsTrigger, Toast, ToastClose, type ToastData, ToastDescription, type ToastManager, type ToastOptions, type ToastProps, ToastProvider, type ToastProviderProps, ToastTitle, type ToastVariant, ToastViewport, type ToastViewportProps, Tooltip, TooltipContent, type TooltipContentProps, TooltipPortal, type TooltipProps, TooltipTrigger, toast, trapFocus, type UseDismissOptions, useAccordionContext, useAccordionItemContext, useBottomBarContext, useCommandContext, useDialogContext, useDismiss, useDropdownMenuContext, usePopoverContext, useTabsContext, useToast, useTooltipContext } from "./ui";
15
15
  export { createVirtualizer, VirtualGrid, type VirtualGridProps, type VirtualItem, type Virtualizer, type VirtualizerOptions, VirtualList, type VirtualListProps } from "./virtual";
package/dist/index.js CHANGED
@@ -63,7 +63,7 @@ import {
63
63
  highlightElement2,
64
64
  VisageDevTools2,
65
65
  connectDevTools2
66
- } from "./index-ptj8rt6b.js";
66
+ } from "./index-dz6n88aq.js";
67
67
  import {
68
68
  createForm2
69
69
  } from "./index-1jcd8gjf.js";
@@ -77,7 +77,7 @@ import {
77
77
  useNavigate2,
78
78
  useParams2,
79
79
  useSearchParams2
80
- } from "./index-0qat98zs.js";
80
+ } from "./index-k7zd3awa.js";
81
81
  import {
82
82
  createClient2,
83
83
  RouterImpl2,
@@ -110,6 +110,7 @@ import {
110
110
  BottomBarRoot2,
111
111
  BottomBarSeparator2,
112
112
  BottomBar2,
113
+ useDismiss2,
113
114
  CommandDialog2,
114
115
  useCommandContext2,
115
116
  CommandInput2,
@@ -152,7 +153,7 @@ import {
152
153
  ToastDescription2,
153
154
  ToastClose2,
154
155
  Toast2
155
- } from "./index-aagpn02x.js";
156
+ } from "./index-vbsc8z8h.js";
156
157
  import {
157
158
  createVirtualizer2,
158
159
  VirtualList2,
@@ -198,7 +199,7 @@ import {
198
199
  Tooltip3,
199
200
  SwitchThumb2,
200
201
  Switch2
201
- } from "./index-b7emqmej.js";
202
+ } from "./index-kykzcs7p.js";
202
203
  import {
203
204
  ERROR_CODE_TO_STATUS2,
204
205
  STATUS_TO_ERROR_CODE2,
@@ -459,6 +460,7 @@ export {
459
460
  useCommandContext2 as useCommandContext,
460
461
  useContext2 as useContext,
461
462
  useDialogContext2 as useDialogContext,
463
+ useDismiss2 as useDismiss,
462
464
  useDropdownMenuContext2 as useDropdownMenuContext,
463
465
  useLocation2 as useLocation,
464
466
  useNavigate2 as useNavigate,
@@ -7,7 +7,7 @@ import {
7
7
  useNavigate2,
8
8
  useParams2,
9
9
  useSearchParams2
10
- } from "../index-0qat98zs.js";
10
+ } from "../index-k7zd3awa.js";
11
11
  import"../index-3y73zgbk.js";
12
12
  import {
13
13
  lazy2
@@ -58,6 +58,7 @@ export declare function Routes(props: RoutesProps): Child;
58
58
  export interface LinkProps {
59
59
  href: string;
60
60
  class?: string | (() => string);
61
+ className?: string | (() => string);
61
62
  activeClass?: string;
62
63
  replace?: boolean;
63
64
  children: Child;
@@ -1,5 +1,5 @@
1
1
  import type { Child } from "../dom/render";
2
- import { type ReadonlySignal } from "../signals";
2
+ import type { ReadonlySignal } from "../signals";
3
3
  export interface DropdownMenuContextValue {
4
4
  open: ReadonlySignal<boolean>;
5
5
  setOpen: (open: boolean) => void;
@@ -6,6 +6,7 @@ export { Dialog, DialogClose, DialogContent, type DialogContentProps, DialogDesc
6
6
  export { DropdownMenu, DropdownMenuContent, DropdownMenuItem, type DropdownMenuItemProps, DropdownMenuPortal, type DropdownMenuProps, DropdownMenuSeparator, DropdownMenuTrigger, useDropdownMenuContext } from "./dropdown-menu";
7
7
  export { createIcon, type IconNode, type IconProps } from "./icon";
8
8
  export { type BaseLayoutProps, Center, type CenterProps, Grid, type GridProps, Row, type RowProps, Spacer, type SpacerProps, Stack, type StackProps } from "./layout";
9
+ export { type UseDismissOptions, useDismiss } from "./overlay";
9
10
  export { Popover, PopoverClose, PopoverContent, type PopoverContentProps, PopoverPortal, type PopoverProps, PopoverTrigger, usePopoverContext } from "./popover";
10
11
  export { Switch, type SwitchProps, SwitchThumb, type SwitchThumbProps } from "./switch";
11
12
  export { type CreateTabsOptions, createTabs, Tabs, TabsContent, type TabsContentProps, TabsList, type TabsProps, TabsTrigger, useTabsContext } from "./tabs";
package/dist/ui/index.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  BottomBarRoot2,
13
13
  BottomBarSeparator2,
14
14
  BottomBar2,
15
+ useDismiss2,
15
16
  CommandDialog2,
16
17
  useCommandContext2,
17
18
  CommandInput2,
@@ -54,7 +55,7 @@ import {
54
55
  ToastDescription2,
55
56
  ToastClose2,
56
57
  Toast2
57
- } from "../index-aagpn02x.js";
58
+ } from "../index-vbsc8z8h.js";
58
59
  import {
59
60
  createId2,
60
61
  createControllableSignal2,
@@ -73,7 +74,7 @@ import {
73
74
  Tooltip3,
74
75
  SwitchThumb2,
75
76
  Switch2
76
- } from "../index-b7emqmej.js";
77
+ } from "../index-kykzcs7p.js";
77
78
  import {
78
79
  Stack2,
79
80
  Row2,
@@ -158,6 +159,7 @@ export {
158
159
  useBottomBarContext2 as useBottomBarContext,
159
160
  useCommandContext2 as useCommandContext,
160
161
  useDialogContext2 as useDialogContext,
162
+ useDismiss2 as useDismiss,
161
163
  useDropdownMenuContext2 as useDropdownMenuContext,
162
164
  usePopoverContext2 as usePopoverContext,
163
165
  useTabsContext2 as useTabsContext,
@@ -0,0 +1,53 @@
1
+ export interface UseDismissOptions {
2
+ /**
3
+ * Whether dismiss listeners are active.
4
+ * Defaults to true.
5
+ */
6
+ enabled?: boolean | (() => boolean);
7
+ /**
8
+ * Callback fired when dismissal is requested (Escape, click outside, or close button).
9
+ */
10
+ onDismiss: () => void;
11
+ /**
12
+ * Close on 'Escape' key press.
13
+ * Defaults to true.
14
+ */
15
+ escapeKey?: boolean;
16
+ /**
17
+ * Close on pointer interaction outside the target element(s).
18
+ * Defaults to true.
19
+ */
20
+ outsidePointer?: boolean;
21
+ /**
22
+ * Additional element(s) to exclude from outside click detection (e.g. trigger button).
23
+ */
24
+ outsideIgnore?: Array<HTMLElement | null | undefined> | HTMLElement | null | undefined | (() => Array<HTMLElement | null | undefined>);
25
+ /**
26
+ * Trap keyboard focus inside the container when Tab / Shift+Tab is pressed.
27
+ * Defaults to false.
28
+ */
29
+ trapFocus?: boolean;
30
+ /**
31
+ * Close selector for click events bubbling inside the container (e.g. '[data-dialog-close]').
32
+ */
33
+ closeSelector?: string;
34
+ /**
35
+ * Callback fired when an outside pointer interaction occurs.
36
+ */
37
+ onOutsidePointer?: (event: MouseEvent | TouchEvent) => void;
38
+ /**
39
+ * Callback fired when an Escape keydown occurs.
40
+ */
41
+ onEscapeKeyDown?: (event: KeyboardEvent) => void;
42
+ }
43
+ /**
44
+ * Headless hook to manage overlay dismissal:
45
+ * - Outside pointer dismissal (click outside)
46
+ * - Escape key dismissal
47
+ * - Keyboard focus trapping (Tab cycle)
48
+ * - Close button selector dismissal delegation
49
+ */
50
+ export declare function useDismiss(element: () => HTMLElement | null | undefined, options: UseDismissOptions): {
51
+ handleKeyDown: (e: KeyboardEvent) => void;
52
+ handleClick: (e: MouseEvent) => void;
53
+ };
@@ -1,5 +1,5 @@
1
1
  import type { Child } from "../dom/render";
2
- import { type ReadonlySignal } from "../signals";
2
+ import type { ReadonlySignal } from "../signals";
3
3
  export interface PopoverContextValue {
4
4
  open: ReadonlySignal<boolean>;
5
5
  setOpen: (open: boolean) => void;
@@ -18,4 +18,4 @@ export declare function trapFocus(container: HTMLElement, event: KeyboardEvent):
18
18
  /**
19
19
  * Listen for click outside a target element
20
20
  */
21
- export declare function onClickOutside(elements: Array<HTMLElement | null | undefined> | HTMLElement | null | undefined, handler: (event: MouseEvent | TouchEvent) => void): () => void;
21
+ export declare function onClickOutside(elements: Array<HTMLElement | null | undefined> | HTMLElement | null | undefined | (() => Array<HTMLElement | null | undefined>), handler: (event: MouseEvent | TouchEvent) => void): () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epoch32/visage",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Lightning-fast, zero-VDOM, fine-grained reactive UI library with full TypeScript support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -405,11 +405,13 @@ export {
405
405
  TooltipTrigger,
406
406
  toast,
407
407
  trapFocus,
408
+ type UseDismissOptions,
408
409
  useAccordionContext,
409
410
  useAccordionItemContext,
410
411
  useBottomBarContext,
411
412
  useCommandContext,
412
413
  useDialogContext,
414
+ useDismiss,
413
415
  useDropdownMenuContext,
414
416
  usePopoverContext,
415
417
  useTabsContext,
@@ -253,6 +253,7 @@ export function Routes(props: RoutesProps): Child {
253
253
  export interface LinkProps {
254
254
  href: string;
255
255
  class?: string | (() => string);
256
+ className?: string | (() => string);
256
257
  activeClass?: string;
257
258
  replace?: boolean;
258
259
  children: Child;
@@ -264,7 +265,16 @@ export interface LinkProps {
264
265
  */
265
266
  export function Link(props: LinkProps): Child {
266
267
  const router = useContext(RouterContext);
267
- const { href, class: className, activeClass = "active", replace, children, ...rest } = props;
268
+ const {
269
+ href,
270
+ class: classProp,
271
+ className: classNameProp,
272
+ activeClass = "active",
273
+ replace,
274
+ children,
275
+ ...rest
276
+ } = props;
277
+ const rawClass = classProp ?? classNameProp;
268
278
 
269
279
  const handleClick = (e: MouseEvent) => {
270
280
  if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.button !== 0) {
@@ -286,7 +296,7 @@ export function Link(props: LinkProps): Child {
286
296
  });
287
297
 
288
298
  const computedClass = computed(() => {
289
- const baseCls = typeof className === "function" ? className() : (className ?? "");
299
+ const baseCls = typeof rawClass === "function" ? rawClass() : (rawClass ?? "");
290
300
  if (activeClass && isActive()) {
291
301
  return baseCls ? `${baseCls} ${activeClass}` : activeClass;
292
302
  }
@@ -2,6 +2,7 @@ import { Portal } from "../dom/control-flow";
2
2
  import type { Child } from "../dom/render";
3
3
  import type { CommandProps } from "./command";
4
4
  import { CommandRoot } from "./command";
5
+ import { useDismiss } from "./overlay";
5
6
  import { createControllableSignal } from "./utils";
6
7
 
7
8
  export interface CommandDialogProps extends CommandProps {
@@ -19,6 +20,15 @@ export function CommandDialog(props: CommandDialogProps): Child {
19
20
  onChange: props.onOpenChange
20
21
  });
21
22
 
23
+ const contentRef: { current: HTMLElement | null } = { current: null };
24
+
25
+ const dismiss = useDismiss(() => contentRef.current, {
26
+ enabled: () => open(),
27
+ onDismiss: () => setOpen(false),
28
+ escapeKey: true,
29
+ outsidePointer: false
30
+ });
31
+
22
32
  const { overlayClass, contentClass, style, ...rest } = props;
23
33
 
24
34
  return (
@@ -39,8 +49,15 @@ export function CommandDialog(props: CommandDialogProps): Child {
39
49
  onClick={(e: MouseEvent) => {
40
50
  if (e.target === e.currentTarget) setOpen(false);
41
51
  }}
52
+ onKeyDown={dismiss.handleKeyDown}
42
53
  >
43
- <div style={{ width: "100%", maxWidth: "560px" }} class={contentClass}>
54
+ <div
55
+ ref={(el: HTMLElement | null) => {
56
+ contentRef.current = el;
57
+ }}
58
+ style={{ width: "100%", maxWidth: "560px" }}
59
+ class={contentClass}
60
+ >
44
61
  <CommandRoot {...rest} />
45
62
  </div>
46
63
  </div>
package/src/ui/dialog.tsx CHANGED
@@ -2,7 +2,8 @@ import { Portal } from "../dom/control-flow";
2
2
  import type { Child } from "../dom/render";
3
3
  import type { ReadonlySignal } from "../signals";
4
4
  import { createContext, useContext } from "../signals/context";
5
- import { createControllableSignal, createId, trapFocus } from "./utils";
5
+ import { useDismiss } from "./overlay";
6
+ import { createControllableSignal, createId } from "./utils";
6
7
 
7
8
  export interface DialogContextValue {
8
9
  open: ReadonlySignal<boolean>;
@@ -207,25 +208,26 @@ export interface DialogContentProps {
207
208
  export function DialogContent(props: DialogContentProps): Child {
208
209
  const ctx = useContext(DialogContext);
209
210
 
211
+ const dismiss = useDismiss(() => ctx?.contentRef.current, {
212
+ enabled: () => ctx?.open() ?? false,
213
+ onDismiss: () => ctx?.setOpen(false),
214
+ escapeKey: true,
215
+ trapFocus: true,
216
+ outsidePointer: false, // backdrop clicks are handled by DialogOverlay
217
+ closeSelector: "[data-dialog-close]",
218
+ onEscapeKeyDown: (e) => {
219
+ if (props.onKeyDown) props.onKeyDown(e);
220
+ }
221
+ });
222
+
210
223
  const handleKeyDown = (e: KeyboardEvent) => {
211
224
  if (props.onKeyDown) props.onKeyDown(e);
212
- if (e.defaultPrevented) return;
213
-
214
- if (e.key === "Escape") {
215
- ctx?.setOpen(false);
216
- e.stopPropagation();
217
- } else if (e.key === "Tab" && ctx?.contentRef.current) {
218
- trapFocus(ctx.contentRef.current, e);
219
- }
225
+ dismiss.handleKeyDown(e);
220
226
  };
221
227
 
222
228
  const handleClick = (e: MouseEvent) => {
223
229
  if (props.onClick) props.onClick(e);
224
- if (e.defaultPrevented) return;
225
- const target = e.target as HTMLElement | null;
226
- if (target?.closest("[data-dialog-close]")) {
227
- ctx?.setOpen(false);
228
- }
230
+ dismiss.handleClick(e);
229
231
  };
230
232
 
231
233
  const {
@@ -1,8 +1,9 @@
1
1
  import { Portal } from "../dom/control-flow";
2
2
  import type { Child } from "../dom/render";
3
- import { onCleanup, type ReadonlySignal } from "../signals";
3
+ import type { ReadonlySignal } from "../signals";
4
4
  import { createContext, useContext } from "../signals/context";
5
- import { createControllableSignal, createId, onClickOutside } from "./utils";
5
+ import { useDismiss } from "./overlay";
6
+ import { createControllableSignal, createId } from "./utils";
6
7
 
7
8
  export interface DropdownMenuContextValue {
8
9
  open: ReadonlySignal<boolean>;
@@ -207,6 +208,19 @@ export interface DropdownMenuContentProps {
207
208
  export function DropdownMenuContent(props: DropdownMenuContentProps): Child {
208
209
  const ctx = useContext(DropdownMenuContext);
209
210
 
211
+ const dismiss = useDismiss(() => ctx?.contentRef.current, {
212
+ enabled: () => ctx?.open() ?? false,
213
+ onDismiss: () => {
214
+ if (ctx) {
215
+ ctx.setOpen(false);
216
+ ctx.triggerRef.current?.focus();
217
+ }
218
+ },
219
+ escapeKey: true,
220
+ outsidePointer: true,
221
+ outsideIgnore: () => [ctx?.triggerRef.current]
222
+ });
223
+
210
224
  const handleKeyDown = (e: KeyboardEvent) => {
211
225
  if (props.onKeyDown) props.onKeyDown(e);
212
226
  if (e.defaultPrevented) return;
@@ -233,10 +247,7 @@ export function DropdownMenuContent(props: DropdownMenuContentProps): Child {
233
247
  items[items.length - 1]?.focus();
234
248
  e.preventDefault();
235
249
  } else if (e.key === "Escape") {
236
- if (ctx) {
237
- ctx.setOpen(false);
238
- ctx.triggerRef.current?.focus();
239
- }
250
+ dismiss.handleKeyDown(e);
240
251
  e.preventDefault();
241
252
  } else if (e.key === "Tab") {
242
253
  if (ctx) ctx.setOpen(false);
@@ -259,14 +270,6 @@ export function DropdownMenuContent(props: DropdownMenuContentProps): Child {
259
270
  ref={(el: HTMLElement | null) => {
260
271
  if (ctx) {
261
272
  ctx.contentRef.current = el;
262
- if (el) {
263
- const cleanup = onClickOutside([ctx.triggerRef.current, el], () => {
264
- if (ctx.open()) {
265
- ctx.setOpen(false);
266
- }
267
- });
268
- onCleanup(cleanup);
269
- }
270
273
  }
271
274
  }}
272
275
  id={ctx?.menuId}
package/src/ui/index.ts CHANGED
@@ -91,6 +91,10 @@ export {
91
91
  Stack,
92
92
  type StackProps
93
93
  } from "./layout";
94
+ export {
95
+ type UseDismissOptions,
96
+ useDismiss
97
+ } from "./overlay";
94
98
  export {
95
99
  Popover,
96
100
  PopoverClose,
@@ -0,0 +1,130 @@
1
+ import { onCleanup } from "../signals";
2
+ import { onClickOutside, trapFocus } from "./utils";
3
+
4
+ export interface UseDismissOptions {
5
+ /**
6
+ * Whether dismiss listeners are active.
7
+ * Defaults to true.
8
+ */
9
+ enabled?: boolean | (() => boolean);
10
+ /**
11
+ * Callback fired when dismissal is requested (Escape, click outside, or close button).
12
+ */
13
+ onDismiss: () => void;
14
+ /**
15
+ * Close on 'Escape' key press.
16
+ * Defaults to true.
17
+ */
18
+ escapeKey?: boolean;
19
+ /**
20
+ * Close on pointer interaction outside the target element(s).
21
+ * Defaults to true.
22
+ */
23
+ outsidePointer?: boolean;
24
+ /**
25
+ * Additional element(s) to exclude from outside click detection (e.g. trigger button).
26
+ */
27
+ outsideIgnore?:
28
+ | Array<HTMLElement | null | undefined>
29
+ | HTMLElement
30
+ | null
31
+ | undefined
32
+ | (() => Array<HTMLElement | null | undefined>);
33
+ /**
34
+ * Trap keyboard focus inside the container when Tab / Shift+Tab is pressed.
35
+ * Defaults to false.
36
+ */
37
+ trapFocus?: boolean;
38
+ /**
39
+ * Close selector for click events bubbling inside the container (e.g. '[data-dialog-close]').
40
+ */
41
+ closeSelector?: string;
42
+ /**
43
+ * Callback fired when an outside pointer interaction occurs.
44
+ */
45
+ onOutsidePointer?: (event: MouseEvent | TouchEvent) => void;
46
+ /**
47
+ * Callback fired when an Escape keydown occurs.
48
+ */
49
+ onEscapeKeyDown?: (event: KeyboardEvent) => void;
50
+ }
51
+
52
+ /**
53
+ * Headless hook to manage overlay dismissal:
54
+ * - Outside pointer dismissal (click outside)
55
+ * - Escape key dismissal
56
+ * - Keyboard focus trapping (Tab cycle)
57
+ * - Close button selector dismissal delegation
58
+ */
59
+ export function useDismiss(
60
+ element: () => HTMLElement | null | undefined,
61
+ options: UseDismissOptions
62
+ ): {
63
+ handleKeyDown: (e: KeyboardEvent) => void;
64
+ handleClick: (e: MouseEvent) => void;
65
+ } {
66
+ const isEnabled = () => {
67
+ if (typeof options.enabled === "function") return options.enabled();
68
+ return options.enabled ?? true;
69
+ };
70
+
71
+ const handleKeyDown = (e: KeyboardEvent) => {
72
+ if (!isEnabled() || e.defaultPrevented) return;
73
+
74
+ if (e.key === "Escape" && (options.escapeKey ?? true)) {
75
+ if (options.onEscapeKeyDown) {
76
+ options.onEscapeKeyDown(e);
77
+ }
78
+ if (!e.defaultPrevented) {
79
+ options.onDismiss();
80
+ e.stopPropagation();
81
+ }
82
+ } else if (e.key === "Tab" && options.trapFocus) {
83
+ const el = element();
84
+ if (el) {
85
+ trapFocus(el, e);
86
+ }
87
+ }
88
+ };
89
+
90
+ const handleClick = (e: MouseEvent) => {
91
+ if (!isEnabled() || e.defaultPrevented) return;
92
+ if (options.closeSelector) {
93
+ const target = e.target as HTMLElement | null;
94
+ if (target?.closest(options.closeSelector)) {
95
+ options.onDismiss();
96
+ }
97
+ }
98
+ };
99
+
100
+ // Setup outside pointer listener
101
+ if (typeof document !== "undefined") {
102
+ const unbind = onClickOutside(
103
+ () => {
104
+ const el = element();
105
+ const rawIgnores =
106
+ typeof options.outsideIgnore === "function"
107
+ ? options.outsideIgnore()
108
+ : options.outsideIgnore;
109
+ const ignores = Array.isArray(rawIgnores) ? rawIgnores : rawIgnores ? [rawIgnores] : [];
110
+ return [el, ...ignores];
111
+ },
112
+ (event) => {
113
+ if (!isEnabled()) return;
114
+ if (options.outsidePointer === false) return;
115
+ if (options.onOutsidePointer) {
116
+ options.onOutsidePointer(event);
117
+ }
118
+ if (!event.defaultPrevented) {
119
+ options.onDismiss();
120
+ }
121
+ }
122
+ );
123
+ onCleanup(unbind);
124
+ }
125
+
126
+ return {
127
+ handleKeyDown,
128
+ handleClick
129
+ };
130
+ }
@@ -1,8 +1,9 @@
1
1
  import { Portal } from "../dom/control-flow";
2
2
  import type { Child } from "../dom/render";
3
- import { onCleanup, type ReadonlySignal } from "../signals";
3
+ import type { ReadonlySignal } from "../signals";
4
4
  import { createContext, useContext } from "../signals/context";
5
- import { createControllableSignal, createId, onClickOutside } from "./utils";
5
+ import { useDismiss } from "./overlay";
6
+ import { createControllableSignal, createId } from "./utils";
6
7
 
7
8
  export interface PopoverContextValue {
8
9
  open: ReadonlySignal<boolean>;
@@ -138,24 +139,29 @@ export interface PopoverContentProps {
138
139
  export function PopoverContent(props: PopoverContentProps): Child {
139
140
  const ctx = useContext(PopoverContext);
140
141
 
142
+ const dismiss = useDismiss(() => ctx?.contentRef.current, {
143
+ enabled: () => ctx?.open() ?? false,
144
+ onDismiss: () => ctx?.setOpen(false),
145
+ escapeKey: true,
146
+ outsidePointer: true,
147
+ outsideIgnore: () => [ctx?.triggerRef.current],
148
+ closeSelector: "[data-popover-close]",
149
+ onEscapeKeyDown: (e) => {
150
+ if (props.onKeyDown) props.onKeyDown(e);
151
+ if (!e.defaultPrevented) {
152
+ ctx?.triggerRef.current?.focus();
153
+ }
154
+ }
155
+ });
156
+
141
157
  const handleKeyDown = (e: KeyboardEvent) => {
142
158
  if (props.onKeyDown) props.onKeyDown(e);
143
- if (e.defaultPrevented) return;
144
-
145
- if (e.key === "Escape" && ctx) {
146
- ctx.setOpen(false);
147
- ctx.triggerRef.current?.focus();
148
- e.stopPropagation();
149
- }
159
+ dismiss.handleKeyDown(e);
150
160
  };
151
161
 
152
162
  const handleClick = (e: MouseEvent) => {
153
163
  if (props.onClick) props.onClick(e);
154
- if (e.defaultPrevented) return;
155
- const target = e.target as HTMLElement | null;
156
- if (target?.closest("[data-popover-close]")) {
157
- ctx?.setOpen(false);
158
- }
164
+ dismiss.handleClick(e);
159
165
  };
160
166
 
161
167
  const {
@@ -175,14 +181,6 @@ export function PopoverContent(props: PopoverContentProps): Child {
175
181
  ref={(el: HTMLElement | null) => {
176
182
  if (ctx) {
177
183
  ctx.contentRef.current = el;
178
- if (el) {
179
- const cleanup = onClickOutside([ctx.triggerRef.current, el], () => {
180
- if (ctx.open()) {
181
- ctx.setOpen(false);
182
- }
183
- });
184
- onCleanup(cleanup);
185
- }
186
184
  }
187
185
  }}
188
186
  id={ctx?.contentId}
package/src/ui/utils.ts CHANGED
@@ -79,14 +79,20 @@ export function trapFocus(container: HTMLElement, event: KeyboardEvent) {
79
79
  * Listen for click outside a target element
80
80
  */
81
81
  export function onClickOutside(
82
- elements: Array<HTMLElement | null | undefined> | HTMLElement | null | undefined,
82
+ elements:
83
+ | Array<HTMLElement | null | undefined>
84
+ | HTMLElement
85
+ | null
86
+ | undefined
87
+ | (() => Array<HTMLElement | null | undefined>),
83
88
  handler: (event: MouseEvent | TouchEvent) => void
84
89
  ): () => void {
85
90
  const listener = (event: MouseEvent | TouchEvent) => {
86
91
  const target = event.target as Node | null;
87
92
  if (!target) return;
88
93
 
89
- const list = Array.isArray(elements) ? elements : [elements];
94
+ const raw = typeof elements === "function" ? elements() : elements;
95
+ const list = Array.isArray(raw) ? raw : [raw];
90
96
  const isInside = list.some((el) => el?.contains(target));
91
97
 
92
98
  if (!isInside) {