@passui/react 0.1.45 → 0.1.46

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 (140) hide show
  1. package/README.md +57 -46
  2. package/dist/types/src/components/accordion/accordion.d.ts +1 -1
  3. package/dist/types/src/components/breadcrumb/breadcrumb.d.ts +1 -1
  4. package/dist/types/src/components/inline-disclosure/inline-disclosure.d.ts +1 -1
  5. package/dist/types/src/components/popover/anchored-surface.d.ts +5 -1
  6. package/dist/types/src/components/popover/index.d.ts +41 -0
  7. package/dist/types/src/components/popover/popover-content.d.ts +32 -0
  8. package/dist/types/src/components/popover/popover-root.d.ts +22 -0
  9. package/dist/types/src/components/popover/popover-trigger.d.ts +23 -0
  10. package/dist/types/src/components/stepper/rolling-digit.d.ts +1 -1
  11. package/dist/types/src/components/table/table.d.ts +1 -1
  12. package/dist/types/src/components/view-controller/view-controller.d.ts +2 -2
  13. package/dist/types/src/index.d.ts +1 -1
  14. package/dist/types/src/machines/popover.machine.d.ts +30 -0
  15. package/package.json +307 -307
  16. package/src/appearance/index.ts +2 -2
  17. package/src/appearance/platform.tsx +76 -60
  18. package/src/appearance/theme.tsx +409 -377
  19. package/src/components/accordion/accordion.tsx +58 -49
  20. package/src/components/ambient/ambient.tsx +25 -25
  21. package/src/components/ambient/index.ts +1 -1
  22. package/src/components/aspect-ratio/aspect-ratio.tsx +21 -16
  23. package/src/components/breadcrumb/breadcrumb.tsx +63 -57
  24. package/src/components/button/button-styles.ts +39 -32
  25. package/src/components/button/button.tsx +91 -84
  26. package/src/components/button/icon-button.tsx +27 -25
  27. package/src/components/button/pending-spinner.tsx +8 -8
  28. package/src/components/button/use-pending-indicator.ts +11 -11
  29. package/src/components/chip/chip.tsx +35 -30
  30. package/src/components/code-block/code-block.tsx +40 -39
  31. package/src/components/combo-box/combo-box.tsx +5 -5
  32. package/src/components/command/command.tsx +124 -94
  33. package/src/components/compact-date-picker/calendar-month.ts +75 -53
  34. package/src/components/compact-date-picker/compact-date-picker.tsx +308 -289
  35. package/src/components/compact-time-picker/compact-time-picker.tsx +288 -281
  36. package/src/components/compact-time-picker/time-value.ts +32 -29
  37. package/src/components/content-unavailable-view/content-unavailable-view.tsx +64 -48
  38. package/src/components/content-view/content-view.tsx +168 -159
  39. package/src/components/content-view/index.ts +1 -1
  40. package/src/components/context-menu/conceal-menu-source.ts +25 -25
  41. package/src/components/context-menu/context-menu-host.tsx +439 -402
  42. package/src/components/context-menu/context-menu-store.ts +125 -123
  43. package/src/components/context-menu/menu-navigation.ts +25 -18
  44. package/src/components/context-menu/menu-position.ts +33 -29
  45. package/src/components/context-menu/menu-reveal.ts +32 -32
  46. package/src/components/context-menu/use-context-menu.ts +63 -56
  47. package/src/components/conversation/conversation.tsx +265 -263
  48. package/src/components/conversation/index.ts +1 -1
  49. package/src/components/date-picker/date-picker.tsx +95 -94
  50. package/src/components/date-picker/date-value.ts +20 -10
  51. package/src/components/dialog/dialog-host.tsx +105 -103
  52. package/src/components/dialog/dialog-store.ts +37 -36
  53. package/src/components/dialog/dialog.tsx +192 -179
  54. package/src/components/dialog/index.ts +1 -1
  55. package/src/components/disclosure-group/disclosure-group.tsx +93 -86
  56. package/src/components/divider/divider.tsx +24 -22
  57. package/src/components/inline-disclosure/inline-disclosure.tsx +40 -36
  58. package/src/components/keyboard-shortcut/keyboard-shortcut.tsx +23 -19
  59. package/src/components/labeled-content/labeled-content.tsx +68 -68
  60. package/src/components/locale-switcher/index.ts +1 -1
  61. package/src/components/locale-switcher/locale-switcher.tsx +279 -251
  62. package/src/components/menu/menu.tsx +75 -58
  63. package/src/components/morph/index.ts +2 -2
  64. package/src/components/morph/morph.tsx +227 -201
  65. package/src/components/morph/use-morph-disclosure.ts +19 -19
  66. package/src/components/one-time-code/one-time-code.tsx +216 -200
  67. package/src/components/picker/picker-trigger.ts +16 -13
  68. package/src/components/picker/picker.tsx +371 -345
  69. package/src/components/popover/anchored-surface.tsx +253 -229
  70. package/src/components/popover/index.ts +50 -0
  71. package/src/components/popover/popover-content.tsx +334 -0
  72. package/src/components/popover/popover-fit.ts +58 -53
  73. package/src/components/popover/popover-root.tsx +97 -0
  74. package/src/components/popover/popover-trigger.tsx +71 -0
  75. package/src/components/profile-image/profile-image.tsx +80 -64
  76. package/src/components/progress-view/progress-view.tsx +61 -59
  77. package/src/components/progress-view/use-liquid-progress.ts +38 -38
  78. package/src/components/radio-group/radio-group.tsx +71 -68
  79. package/src/components/search-field/expanding-search-field.tsx +191 -177
  80. package/src/components/search-field/search-field.tsx +59 -50
  81. package/src/components/search-list/search-list.tsx +246 -236
  82. package/src/components/section/section.tsx +58 -58
  83. package/src/components/secure-field/secure-field.tsx +3 -3
  84. package/src/components/segmented-control/segmented-control.tsx +136 -136
  85. package/src/components/sheet/modal-environment.ts +71 -64
  86. package/src/components/sheet/sheet-portal.tsx +58 -53
  87. package/src/components/sheet/sheet.tsx +146 -141
  88. package/src/components/sheet/use-sheet-focus.ts +97 -82
  89. package/src/components/sidebar/sidebar.tsx +78 -78
  90. package/src/components/skeleton/skeleton.tsx +30 -21
  91. package/src/components/slider/slider.tsx +113 -104
  92. package/src/components/slosh-slider/liquid-step.ts +32 -31
  93. package/src/components/slosh-slider/slosh-slider.tsx +265 -228
  94. package/src/components/stat-tile/index.ts +1 -1
  95. package/src/components/stat-tile/stat-tile.tsx +79 -68
  96. package/src/components/status-indicator/status-indicator.tsx +40 -37
  97. package/src/components/stepper/rolling-digit.tsx +82 -68
  98. package/src/components/stepper/stepper.tsx +122 -113
  99. package/src/components/swatch-picker/swatch-picker.tsx +120 -120
  100. package/src/components/tab-bar/tab-bar.tsx +149 -148
  101. package/src/components/tab-view/tab-view.tsx +140 -137
  102. package/src/components/table/table.tsx +117 -98
  103. package/src/components/text-editor/text-editor.tsx +34 -29
  104. package/src/components/text-field/text-field.tsx +182 -180
  105. package/src/components/toast/toast-store.ts +127 -115
  106. package/src/components/toast/toaster.tsx +361 -347
  107. package/src/components/toggle/toggle.tsx +297 -282
  108. package/src/components/tooltip/tooltip-host.tsx +157 -150
  109. package/src/components/view-controller/index.ts +1 -1
  110. package/src/components/view-controller/view-controller.tsx +64 -53
  111. package/src/components/wake-slider/wake-slider.tsx +220 -205
  112. package/src/hooks/scroll/minimize-decision.ts +11 -7
  113. package/src/hooks/use-anchored-disclosure.ts +19 -19
  114. package/src/hooks/use-anchored-placement.ts +29 -25
  115. package/src/hooks/use-dismiss.ts +33 -29
  116. package/src/hooks/use-floating-dismiss.ts +24 -22
  117. package/src/hooks/use-media-query.ts +67 -60
  118. package/src/hooks/use-minimize-on-scroll.ts +69 -60
  119. package/src/hooks/use-visual-viewport.ts +63 -63
  120. package/src/hooks/viewport/types.ts +16 -16
  121. package/src/hooks/viewport/viewport-metrics.ts +16 -10
  122. package/src/icons/icons.tsx +440 -328
  123. package/src/index.ts +91 -91
  124. package/src/lib/anchored-placement.ts +70 -65
  125. package/src/lib/class-names.ts +14 -13
  126. package/src/lib/clipboard.ts +8 -7
  127. package/src/lib/date.ts +61 -60
  128. package/src/lib/floating-layers.ts +39 -24
  129. package/src/lib/haptics.ts +10 -10
  130. package/src/lib/language.ts +9 -9
  131. package/src/lib/range-value.ts +27 -21
  132. package/src/lib/screen-edges.ts +33 -30
  133. package/src/lib/shapes.ts +38 -34
  134. package/src/machines/popover.machine.ts +51 -0
  135. package/src/motion/transitions.ts +46 -43
  136. package/src/styles/code-block.css +69 -43
  137. package/src/styles/motion-controls.css +393 -191
  138. package/src/tokens.ts +1 -1
  139. package/dist/types/src/components/popover/popover.d.ts +0 -21
  140. package/src/components/popover/popover.tsx +0 -334
@@ -1,8 +1,8 @@
1
- import { TextField, type TextFieldProps } from "../text-field/text-field"
1
+ import { TextField, type TextFieldProps } from "../text-field/text-field";
2
2
 
3
3
  /** TextField's native input props, with the password type managed by SecureField. */
4
- export type SecureFieldProps = Omit<TextFieldProps, "type">
4
+ export type SecureFieldProps = Omit<TextFieldProps, "type">;
5
5
 
6
6
  export function SecureField(props: SecureFieldProps) {
7
- return <TextField {...props} type="password" />
7
+ return <TextField {...props} type="password" />;
8
8
  }
@@ -1,46 +1,46 @@
1
- import { motion, useReducedMotion } from "motion/react"
2
- import { useId, useLayoutEffect, useRef, useState } from "react"
3
- import { cn } from "../../lib/class-names"
4
- import { haptic } from "../../lib/haptics"
5
- import { spring } from "../../motion/transitions"
6
- import { Picker } from "../picker/picker"
1
+ import { motion, useReducedMotion } from "motion/react";
2
+ import { useId, useLayoutEffect, useRef, useState } from "react";
3
+ import { cn } from "../../lib/class-names";
4
+ import { haptic } from "../../lib/haptics";
5
+ import { spring } from "../../motion/transitions";
6
+ import { Picker } from "../picker/picker";
7
7
 
8
- export type SegmentedControlSize = "sm" | "md" | "lg"
8
+ export type SegmentedControlSize = "sm" | "md" | "lg";
9
9
 
10
10
  const segmentPaddings: Record<SegmentedControlSize, string> = {
11
- sm: "p-[2px]",
12
- md: "p-[3px]",
13
- lg: "p-[4px]",
14
- }
11
+ sm: "p-[2px]",
12
+ md: "p-[3px]",
13
+ lg: "p-[4px]",
14
+ };
15
15
  const segmentLabelClasses: Record<SegmentedControlSize, string> = {
16
- sm: "min-w-[52px] px-2 py-0.5 text-caption2 whitespace-nowrap",
17
- md: "min-w-[64px] px-3 py-1 text-footnote whitespace-nowrap",
18
- lg: "min-w-[80px] px-4 py-1.5 text-subheadline whitespace-nowrap",
19
- }
16
+ sm: "min-w-[52px] px-2 py-0.5 text-caption2 whitespace-nowrap",
17
+ md: "min-w-[64px] px-3 py-1 text-footnote whitespace-nowrap",
18
+ lg: "min-w-[80px] px-4 py-1.5 text-subheadline whitespace-nowrap",
19
+ };
20
20
  const segmentRadius: Record<SegmentedControlSize, string> = {
21
- sm: "rounded-sm",
22
- md: "rounded-md",
23
- lg: "rounded-lg",
24
- }
21
+ sm: "rounded-sm",
22
+ md: "rounded-md",
23
+ lg: "rounded-lg",
24
+ };
25
25
  const fallbackPickerSizes: Record<SegmentedControlSize, string> = {
26
- sm: "rounded-md py-1 text-caption2 font-semibold",
27
- md: "rounded-lg py-1.5 text-footnote font-semibold",
28
- lg: "rounded-lg py-2 text-subheadline font-semibold",
29
- }
26
+ sm: "rounded-md py-1 text-caption2 font-semibold",
27
+ md: "rounded-lg py-1.5 text-footnote font-semibold",
28
+ lg: "rounded-lg py-2 text-subheadline font-semibold",
29
+ };
30
30
 
31
31
  export interface SegmentedControlProps<T extends string> {
32
- /** Value of the selected segment. */
33
- value: T
34
- /** The choices, in display order. */
35
- options: ReadonlyArray<{ value: T; label: string }>
36
- /** Called with the selected segment's value. */
37
- onChange: (value: NoInfer<T>) => void
38
- /** Accessible name for the group and its compact picker. */
39
- label: string
40
- /** Custom classes for the control container. */
41
- className?: string
42
- /** Segment padding, label text scale and min-width. @default "md" */
43
- size?: SegmentedControlSize
32
+ /** Value of the selected segment. */
33
+ value: T;
34
+ /** The choices, in display order. */
35
+ options: ReadonlyArray<{ value: T; label: string }>;
36
+ /** Called with the selected segment's value. */
37
+ onChange: (value: NoInfer<T>) => void;
38
+ /** Accessible name for the group and its compact picker. */
39
+ label: string;
40
+ /** Custom classes for the control container. */
41
+ className?: string;
42
+ /** Segment padding, label text scale and min-width. @default "md" */
43
+ size?: SegmentedControlSize;
44
44
  }
45
45
 
46
46
  /**
@@ -50,108 +50,108 @@ export interface SegmentedControlProps<T extends string> {
50
50
  * squeezing or spilling its text.
51
51
  */
52
52
  export function SegmentedControl<T extends string>({
53
- value,
54
- options,
55
- onChange,
56
- label,
57
- className,
58
- size = "md",
53
+ value,
54
+ options,
55
+ onChange,
56
+ label,
57
+ className,
58
+ size = "md",
59
59
  }: SegmentedControlProps<T>) {
60
- const id = useId()
61
- const reducedMotion = useReducedMotion()
62
- const frame = useRef<HTMLDivElement>(null)
63
- const measure = useRef<HTMLDivElement>(null)
64
- const [fits, setFits] = useState(true)
60
+ const id = useId();
61
+ const reducedMotion = useReducedMotion();
62
+ const frame = useRef<HTMLDivElement>(null);
63
+ const measure = useRef<HTMLDivElement>(null);
64
+ const [fits, setFits] = useState(true);
65
65
 
66
- useLayoutEffect(() => {
67
- const box = frame.current
68
- const natural = measure.current
69
- if (!box || !natural) return
70
- const check = () => setFits(natural.offsetWidth <= box.clientWidth + 0.5)
71
- check()
72
- const observer = new ResizeObserver(check)
73
- observer.observe(box)
74
- observer.observe(natural)
75
- return () => observer.disconnect()
76
- }, [])
66
+ useLayoutEffect(() => {
67
+ const box = frame.current;
68
+ const natural = measure.current;
69
+ if (!box || !natural) return;
70
+ const check = () => setFits(natural.offsetWidth <= box.clientWidth + 0.5);
71
+ check();
72
+ const observer = new ResizeObserver(check);
73
+ observer.observe(box);
74
+ observer.observe(natural);
75
+ return () => observer.disconnect();
76
+ }, []);
77
77
 
78
- return (
79
- <div ref={frame} className={cn("relative w-full min-w-0", className)}>
80
- {/* The control at its natural size, to know whether it fits. */}
81
- <div
82
- ref={measure}
83
- aria-hidden="true"
84
- className={cn(
85
- "pointer-events-none invisible absolute top-0 flex w-max font-semibold",
86
- segmentPaddings[size],
87
- segmentLabelClasses[size].replace(/min-w-\[[^\]]+\]\s*/, "")
88
- )}
89
- >
90
- {options.map((option) => (
91
- <span key={option.value} className={segmentLabelClasses[size]}>
92
- {option.label}
93
- </span>
94
- ))}
95
- </div>
96
- {fits ? (
97
- <fieldset
98
- className={cn(
99
- "flex w-full rounded-lg bg-fill shadow-[inset_0_1px_2px_rgb(40_30_0/0.06)]",
100
- segmentPaddings[size]
101
- )}
102
- >
103
- <legend className="sr-only">{label}</legend>
104
- {options.map((option) => {
105
- const selected = option.value === value
106
- return (
107
- <label
108
- key={option.value}
109
- className={cn(
110
- "relative isolate flex-1 cursor-pointer text-center whitespace-nowrap transition-colors has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-accent/60",
111
- segmentLabelClasses[size].replace("whitespace-nowrap", ""),
112
- segmentRadius[size],
113
- selected
114
- ? "font-semibold text-label"
115
- : "font-medium text-label-secondary hover:text-label"
116
- )}
117
- >
118
- <input
119
- type="radio"
120
- name={id}
121
- value={option.value}
122
- checked={selected}
123
- onChange={() => {
124
- haptic("selection")
125
- onChange(option.value)
126
- }}
127
- className="sr-only"
128
- />
129
- {selected && (
130
- <motion.span
131
- layoutId={`segment-${id}`}
132
- className={cn(
133
- "absolute inset-0 -z-10 bg-elevated shadow-[0_0_0_0.5px_rgb(0_0_0/0.06),0_1px_2px_rgb(0_0_0/0.08),0_3px_8px_-2px_rgb(0_0_0/0.10)]",
134
- segmentRadius[size]
135
- )}
136
- transition={reducedMotion ? { duration: 0 } : spring.snappy}
137
- />
138
- )}
139
- {option.label}
140
- </label>
141
- )
142
- })}
143
- </fieldset>
144
- ) : (
145
- <Picker<T>
146
- label={label}
147
- value={value}
148
- options={options}
149
- onChange={onChange}
150
- searchable={false}
151
- className={fallbackPickerSizes[size]}
152
- size={size === "sm" ? "sm" : size === "lg" ? "lg" : "md"}
153
- />
154
- )}
155
- </div>
156
- )
78
+ return (
79
+ <div ref={frame} className={cn("relative w-full min-w-0", className)}>
80
+ {/* The control at its natural size, to know whether it fits. */}
81
+ <div
82
+ ref={measure}
83
+ aria-hidden="true"
84
+ className={cn(
85
+ "pointer-events-none invisible absolute top-0 flex w-max font-semibold",
86
+ segmentPaddings[size],
87
+ segmentLabelClasses[size].replace(/min-w-\[[^\]]+\]\s*/, ""),
88
+ )}
89
+ >
90
+ {options.map((option) => (
91
+ <span key={option.value} className={segmentLabelClasses[size]}>
92
+ {option.label}
93
+ </span>
94
+ ))}
95
+ </div>
96
+ {fits ? (
97
+ <fieldset
98
+ className={cn(
99
+ "bg-fill flex w-full rounded-lg shadow-[inset_0_1px_2px_rgb(40_30_0/0.06)]",
100
+ segmentPaddings[size],
101
+ )}
102
+ >
103
+ <legend className="sr-only">{label}</legend>
104
+ {options.map((option) => {
105
+ const selected = option.value === value;
106
+ return (
107
+ <label
108
+ key={option.value}
109
+ className={cn(
110
+ "has-[:focus-visible]:ring-accent/60 relative isolate flex-1 cursor-pointer text-center whitespace-nowrap transition-colors has-[:focus-visible]:ring-2",
111
+ segmentLabelClasses[size].replace("whitespace-nowrap", ""),
112
+ segmentRadius[size],
113
+ selected
114
+ ? "text-label font-semibold"
115
+ : "text-label-secondary hover:text-label font-medium",
116
+ )}
117
+ >
118
+ <input
119
+ type="radio"
120
+ name={id}
121
+ value={option.value}
122
+ checked={selected}
123
+ onChange={() => {
124
+ haptic("selection");
125
+ onChange(option.value);
126
+ }}
127
+ className="sr-only"
128
+ />
129
+ {selected && (
130
+ <motion.span
131
+ layoutId={`segment-${id}`}
132
+ className={cn(
133
+ "bg-elevated absolute inset-0 -z-10 shadow-[0_0_0_0.5px_rgb(0_0_0/0.06),0_1px_2px_rgb(0_0_0/0.08),0_3px_8px_-2px_rgb(0_0_0/0.10)]",
134
+ segmentRadius[size],
135
+ )}
136
+ transition={reducedMotion ? { duration: 0 } : spring.snappy}
137
+ />
138
+ )}
139
+ {option.label}
140
+ </label>
141
+ );
142
+ })}
143
+ </fieldset>
144
+ ) : (
145
+ <Picker<T>
146
+ label={label}
147
+ value={value}
148
+ options={options}
149
+ onChange={onChange}
150
+ searchable={false}
151
+ className={fallbackPickerSizes[size]}
152
+ size={size === "sm" ? "sm" : size === "lg" ? "lg" : "md"}
153
+ />
154
+ )}
155
+ </div>
156
+ );
157
157
  }
@@ -1,75 +1,82 @@
1
- import { FLOATING_LAYER, ownsFloatingLayer } from "../../lib/floating-layers"
1
+ import { FLOATING_LAYER, ownsFloatingLayer } from "../../lib/floating-layers";
2
2
 
3
- const surfaces: HTMLElement[] = []
4
- const isolated = new Map<HTMLElement, boolean>()
5
- let overflow = ""
6
- let overflowPriority = ""
3
+ const surfaces: HTMLElement[] = [];
4
+ const isolated = new Map<HTMLElement, boolean>();
5
+ let overflow = "";
6
+ let overflowPriority = "";
7
7
 
8
8
  function restoreBackground() {
9
- for (const [element, inert] of isolated) element.inert = inert
10
- isolated.clear()
9
+ for (const [element, inert] of isolated) element.inert = inert;
10
+ isolated.clear();
11
11
  }
12
12
 
13
13
  function isolateBackground() {
14
- restoreBackground()
15
- const surface = surfaces.at(-1)
16
- if (!surface) return
17
- const roots = [surface.parentElement ?? surface]
18
- for (const layer of document.querySelectorAll<HTMLElement>(`[${FLOATING_LAYER}]`)) {
19
- if (ownsFloatingLayer(surface, layer)) roots.push(layer)
20
- }
21
- const visit = (parent: HTMLElement) => {
22
- for (const child of parent.children) {
23
- if (!(child instanceof HTMLElement)) continue
24
- if (roots.includes(child)) continue
25
- if (roots.some((root) => child.contains(root))) {
26
- visit(child)
27
- } else if (!child.matches("script, style, link")) {
28
- isolated.set(child, child.inert)
29
- child.inert = true
30
- }
31
- }
32
- }
33
- visit(document.body)
14
+ restoreBackground();
15
+ const surface = surfaces.at(-1);
16
+ if (!surface) return;
17
+ const roots = [surface.parentElement ?? surface];
18
+ for (const layer of document.querySelectorAll<HTMLElement>(
19
+ `[${FLOATING_LAYER}]`,
20
+ )) {
21
+ if (ownsFloatingLayer(surface, layer)) roots.push(layer);
22
+ }
23
+ const visit = (parent: HTMLElement) => {
24
+ for (const child of parent.children) {
25
+ if (!(child instanceof HTMLElement)) continue;
26
+ if (roots.includes(child)) continue;
27
+ if (roots.some((root) => child.contains(root))) {
28
+ visit(child);
29
+ } else if (!child.matches("script, style, link")) {
30
+ isolated.set(child, child.inert);
31
+ child.inert = true;
32
+ }
33
+ }
34
+ };
35
+ visit(document.body);
34
36
  }
35
37
 
36
38
  /** Keeps nested sheets and their portaled controls above an inactive background. */
37
39
  export function enterModalEnvironment(surface: HTMLElement): () => void {
38
- if (!surfaces.length) {
39
- overflow = document.body.style.getPropertyValue("overflow")
40
- overflowPriority = document.body.style.getPropertyPriority("overflow")
41
- document.body.style.setProperty("overflow", "hidden")
42
- }
43
- surfaces.push(surface)
44
- isolateBackground()
45
- const preventBackgroundScroll = (event: WheelEvent | TouchEvent) => {
46
- if (surfaces.at(-1) !== surface) return
47
- const target = event.target
48
- if (
49
- target instanceof Element &&
50
- (surface.contains(target) || ownsFloatingLayer(surface, target))
51
- )
52
- return
53
- event.preventDefault()
54
- }
55
- document.addEventListener("wheel", preventBackgroundScroll, { passive: false })
56
- document.addEventListener("touchmove", preventBackgroundScroll, { passive: false })
57
- const observer = new MutationObserver(() => {
58
- if (surfaces.at(-1) === surface) isolateBackground()
59
- })
60
- observer.observe(document.body, {
61
- childList: true,
62
- subtree: true,
63
- })
64
- return () => {
65
- observer.disconnect()
66
- document.removeEventListener("wheel", preventBackgroundScroll)
67
- document.removeEventListener("touchmove", preventBackgroundScroll)
68
- surfaces.splice(surfaces.indexOf(surface), 1)
69
- isolateBackground()
70
- if (!surfaces.length) {
71
- if (overflow) document.body.style.setProperty("overflow", overflow, overflowPriority)
72
- else document.body.style.removeProperty("overflow")
73
- }
74
- }
40
+ if (!surfaces.length) {
41
+ overflow = document.body.style.getPropertyValue("overflow");
42
+ overflowPriority = document.body.style.getPropertyPriority("overflow");
43
+ document.body.style.setProperty("overflow", "hidden");
44
+ }
45
+ surfaces.push(surface);
46
+ isolateBackground();
47
+ const preventBackgroundScroll = (event: WheelEvent | TouchEvent) => {
48
+ if (surfaces.at(-1) !== surface) return;
49
+ const target = event.target;
50
+ if (
51
+ target instanceof Element &&
52
+ (surface.contains(target) || ownsFloatingLayer(surface, target))
53
+ )
54
+ return;
55
+ event.preventDefault();
56
+ };
57
+ document.addEventListener("wheel", preventBackgroundScroll, {
58
+ passive: false,
59
+ });
60
+ document.addEventListener("touchmove", preventBackgroundScroll, {
61
+ passive: false,
62
+ });
63
+ const observer = new MutationObserver(() => {
64
+ if (surfaces.at(-1) === surface) isolateBackground();
65
+ });
66
+ observer.observe(document.body, {
67
+ childList: true,
68
+ subtree: true,
69
+ });
70
+ return () => {
71
+ observer.disconnect();
72
+ document.removeEventListener("wheel", preventBackgroundScroll);
73
+ document.removeEventListener("touchmove", preventBackgroundScroll);
74
+ surfaces.splice(surfaces.indexOf(surface), 1);
75
+ isolateBackground();
76
+ if (!surfaces.length) {
77
+ if (overflow)
78
+ document.body.style.setProperty("overflow", overflow, overflowPriority);
79
+ else document.body.style.removeProperty("overflow");
80
+ }
81
+ };
75
82
  }
@@ -1,57 +1,62 @@
1
- import { type ReactNode, useLayoutEffect, useRef, useState } from "react"
2
- import { createPortal } from "react-dom"
1
+ import { type ReactNode, useLayoutEffect, useRef, useState } from "react";
2
+ import { createPortal } from "react-dom";
3
3
 
4
4
  /** Escape transformed page layers while retaining the source's scoped appearance. */
5
5
  export function SheetPortal({ children }: { children: ReactNode }) {
6
- const source = useRef<HTMLSpanElement>(null)
7
- const [host, setHost] = useState<HTMLDivElement | null>(null)
8
- useLayoutEffect(() => {
9
- const marker = source.current
10
- if (!marker) return
11
- const document = marker.ownerDocument
12
- const layer = document.createElement("div")
13
- layer.dataset.passuiOverlay = ""
14
- layer.style.display = "contents"
15
- const attributes = ["data-theme", "data-accent", "data-os", "dir"]
16
- let properties = new Set<string>()
17
- const inherit = () => {
18
- for (const name of attributes) {
19
- const value = marker.closest(`[${name}]`)?.getAttribute(name)
20
- if (value === null || value === undefined) layer.removeAttribute(name)
21
- else layer.setAttribute(name, value)
22
- }
23
- const computed = getComputedStyle(marker)
24
- const next = new Set<string>()
25
- for (const name of computed) {
26
- if (!name.startsWith("--")) continue
27
- next.add(name)
28
- layer.style.setProperty(name, computed.getPropertyValue(name))
29
- }
30
- for (const name of properties) if (!next.has(name)) layer.style.removeProperty(name)
31
- properties = next
32
- layer.style.colorScheme = computed.colorScheme
33
- layer.style.fontFamily = computed.fontFamily
34
- layer.style.direction = computed.direction
35
- }
36
- inherit()
37
- document.body.appendChild(layer)
38
- setHost(layer)
39
- const observer = new MutationObserver(inherit)
40
- for (let ancestor = marker.parentElement; ancestor; ancestor = ancestor.parentElement) {
41
- observer.observe(ancestor, {
42
- attributes: true,
43
- attributeFilter: [...attributes, "style", "class"],
44
- })
45
- }
46
- return () => {
47
- observer.disconnect()
48
- layer.remove()
49
- }
50
- }, [])
51
- return (
52
- <>
53
- <span ref={source} hidden aria-hidden="true" />
54
- {host && createPortal(children, host)}
55
- </>
56
- )
6
+ const source = useRef<HTMLSpanElement>(null);
7
+ const [host, setHost] = useState<HTMLDivElement | null>(null);
8
+ useLayoutEffect(() => {
9
+ const marker = source.current;
10
+ if (!marker) return;
11
+ const document = marker.ownerDocument;
12
+ const layer = document.createElement("div");
13
+ layer.dataset.passuiOverlay = "";
14
+ layer.style.display = "contents";
15
+ const attributes = ["data-theme", "data-accent", "data-os", "dir"];
16
+ let properties = new Set<string>();
17
+ const inherit = () => {
18
+ for (const name of attributes) {
19
+ const value = marker.closest(`[${name}]`)?.getAttribute(name);
20
+ if (value === null || value === undefined) layer.removeAttribute(name);
21
+ else layer.setAttribute(name, value);
22
+ }
23
+ const computed = getComputedStyle(marker);
24
+ const next = new Set<string>();
25
+ for (const name of computed) {
26
+ if (!name.startsWith("--")) continue;
27
+ next.add(name);
28
+ layer.style.setProperty(name, computed.getPropertyValue(name));
29
+ }
30
+ for (const name of properties)
31
+ if (!next.has(name)) layer.style.removeProperty(name);
32
+ properties = next;
33
+ layer.style.colorScheme = computed.colorScheme;
34
+ layer.style.fontFamily = computed.fontFamily;
35
+ layer.style.direction = computed.direction;
36
+ };
37
+ inherit();
38
+ document.body.appendChild(layer);
39
+ setHost(layer);
40
+ const observer = new MutationObserver(inherit);
41
+ for (
42
+ let ancestor = marker.parentElement;
43
+ ancestor;
44
+ ancestor = ancestor.parentElement
45
+ ) {
46
+ observer.observe(ancestor, {
47
+ attributes: true,
48
+ attributeFilter: [...attributes, "style", "class"],
49
+ });
50
+ }
51
+ return () => {
52
+ observer.disconnect();
53
+ layer.remove();
54
+ };
55
+ }, []);
56
+ return (
57
+ <>
58
+ <span ref={source} hidden aria-hidden="true" />
59
+ {host && createPortal(children, host)}
60
+ </>
61
+ );
57
62
  }