sinwan-ui-core 0.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +31 -12
- package/dist/chunks/chunk-3zrktrd2.js +189 -0
- package/dist/chunks/chunk-6r59k5mb.js +384 -0
- package/dist/chunks/chunk-7r6fwdyt.js +16 -0
- package/dist/chunks/chunk-8zvv4kad.js +116 -0
- package/dist/chunks/chunk-94xhejch.js +511 -0
- package/dist/chunks/chunk-9whagkb6.js +31 -0
- package/dist/chunks/chunk-bw9vp0y9.js +69 -0
- package/dist/chunks/chunk-ejwby1pn.js +107 -0
- package/dist/chunks/chunk-f6hw8ash.js +29 -0
- package/dist/chunks/chunk-te52bwb8.js +448 -0
- package/dist/controls.d.ts +141 -0
- package/dist/controls.js +55 -0
- package/dist/core.d.ts +24 -0
- package/dist/core.js +42 -0
- package/dist/direction.d.ts +15 -0
- package/dist/direction.js +10 -0
- package/dist/dismiss.d.ts +1 -0
- package/dist/dismiss.js +6 -0
- package/dist/floating.d.ts +105 -0
- package/dist/floating.js +24 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +166 -0
- package/dist/menu-sub.d.ts +34 -0
- package/dist/menu-sub.js +14 -0
- package/dist/overlay.d.ts +111 -0
- package/dist/overlay.js +47 -0
- package/dist/slot.d.ts +8 -0
- package/dist/slot.js +7 -0
- package/dist/theme.d.ts +34 -0
- package/dist/theme.js +12 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +8 -0
- package/package.json +108 -5
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +0 -119
- package/bun.lock +0 -66
- package/index.ts +0 -1
- package/tsconfig.json +0 -30
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this package are documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this package uses [Semantic Versioning](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## [1.1.0]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `cazview` palette: `Palette` now accepts `"cazview"` and `ThemeProvider`'s default `palettes` list is `["credit-blue", "jomlaty", "cazview"]`. Pair it with the registry stylesheet, which defines the `html[data-palette="cazview"]` tokens (midnight `#111E2E` text, `#1A73E8` → `#3B52D9` → `#7B2CBF` brand gradient, `#FAFAFA` surface).
|
|
10
|
+
|
|
11
|
+
## [1.0.0]
|
|
12
|
+
|
|
13
|
+
Initial release: the headless primitives extracted from `sinwan-ui` 0.1.0, published separately so registry components copied into apps can depend on them.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Modules `core`, `overlay`, `controls`, `floating`, `dismiss`, `menu-sub`, `slot`, `types`, `theme` and `direction`, each available as a subpath export and through the root barrel. They were previously `sinwan-ui/src/primitives/*`, `src/lib/slot.ts`, `src/lib/types.ts`, `src/theme/theme-provider.tsx` and `src/theme/direction.tsx`.
|
|
18
|
+
- `sinwan` is a peer dependency (`>=2.0.3 <3.0.0`) and stays a bare import in `dist/`; the build fails if a Sinwan runtime or an undeclared package is bundled. Sinwan 2.0.3 is required because older runtimes keep a submenu's portal in `document.body` when the parent menu unmounts during the submenu's exit animation.
|
|
19
|
+
- `createMenuSubApi` accepts `parentOpen`: when the enclosing menu or submenu closes, the submenu closes too (and reports `onOpenChange(false)`), so its Presence exit runs with the parent instead of leaving an open layer behind.
|
|
20
|
+
- `createMenuSubTriggerHandlers(api)`: shared sub-trigger handlers. Hover, click and ArrowRight open the submenu; a click never toggles it closed; leaving starts the hover close delay.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `MenuSubContentLayer` is headless: it renders only the `class` it receives. The former default Tailwind classes now live in the registry's `dropdown-menu`, `context-menu` and `menubar` components.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- `dist/` uses the production JSX runtime (`jsx` from `sinwan/jsx-runtime`). The build sets Bun's `jsx.development: false`; before, every JSX chunk imported `jsxDEV` from `sinwan/jsx-dev-runtime` even though the Sinwan compiler ran with `dev: false`. The dist check now fails on any `sinwan/jsx-dev-runtime` import.
|
package/README.md
CHANGED
|
@@ -1,23 +1,42 @@
|
|
|
1
1
|
# sinwan-ui-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Headless behaviour primitives for [Sinwan](https://sinwanjs.com) UI components — the Radix-style layer under the `sinwan-ui` registry components.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
bun install
|
|
7
|
-
```
|
|
5
|
+
`sinwan-ui-core` owns state, accessibility attributes, focus handling, dismissal, positioning and exit-animation timing. It ships **no styles**: no Tailwind classes, no `clsx`/`tailwind-merge`, no icons. Components copied from the `sinwan-ui` registry (`bunx sinwan-ui add dialog`) import these primitives and add their own classes through `class` props. Use it directly only if you are building your own styled components.
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
## Install
|
|
10
8
|
|
|
11
9
|
```bash
|
|
12
|
-
bun
|
|
10
|
+
bun add sinwan-ui-core
|
|
13
11
|
```
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
Requirements:
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
- `sinwan` 2.x as a peer dependency (your app's runtime; the package never bundles its own copy).
|
|
16
|
+
- `@floating-ui/dom` is installed as a dependency.
|
|
17
|
+
- Compile your app with `bun-plugin-sinwan` or `vite-plugin-sinwan` (JSX with `jsxImportSource: "sinwan"`).
|
|
18
|
+
|
|
19
|
+
## Modules
|
|
20
|
+
|
|
21
|
+
Import from the root barrel or from a subpath (subpaths keep bundles small):
|
|
22
|
+
|
|
23
|
+
| Subpath | Contents |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `sinwan-ui-core/core` | `Presence` (keeps content mounted through CSS exit), `UiPortal`, `useControllableState`, `getFocusableElements`, `trapFocus`; re-exports the positioning helpers from `floating` |
|
|
26
|
+
| `sinwan-ui-core/overlay` | Dialog, Collapsible, Popover and Tooltip primitives (`DialogRoot`, `DialogTrigger`, `DialogOverlay`, `DialogContent`, `PopoverRoot`, …), their injection keys and `OpenApi` |
|
|
27
|
+
| `sinwan-ui-core/controls` | Tabs, Accordion, Checkbox, Switch, RadioGroup, Toggle and ToggleGroup primitives with injection keys |
|
|
28
|
+
| `sinwan-ui-core/floating` | `useAnchorPosition`, `usePointPosition`, `computePosition`, `estimatePosition` and placement helpers (Floating UI based) |
|
|
29
|
+
| `sinwan-ui-core/dismiss` | `isDismissExemptPointerTarget` for outside-press handling across portaled layers |
|
|
30
|
+
| `sinwan-ui-core/menu-sub` | `createMenuSubApi` (closes with its `parentOpen`), `createMenuSubTriggerHandlers`, `isInsideMenuSubContent`, `MenuSubContentLayer` for nested menus |
|
|
31
|
+
| `sinwan-ui-core/slot` | `Slot` (the `asChild` prop merge) |
|
|
32
|
+
| `sinwan-ui-core/types` | Intrinsic prop aliases, `isSinwanElement`, `flattenChildren` |
|
|
33
|
+
| `sinwan-ui-core/theme` | `ThemeProvider`, `useTheme`, `ThemeToggle` (unstyled), `ThemeKey` |
|
|
34
|
+
| `sinwan-ui-core/direction` | `DirectionProvider`, `useDirection`, `DirectionKey` |
|
|
35
|
+
|
|
36
|
+
## Styling contract
|
|
37
|
+
|
|
38
|
+
Every primitive renders a `data-slot` and, where relevant, `data-state` (`open`/`closed`, `checked`/`unchecked`, `on`/`off`, `active`/`inactive`) and `data-side`. Style those attributes from your component layer. `Presence` keeps closing content mounted for `exitMs` (200 ms by default), so exit animations must hold their last keyframe (`animation-fill-mode: forwards`) and finish within that time.
|
|
20
39
|
|
|
21
|
-
|
|
40
|
+
## License
|
|
22
41
|
|
|
23
|
-
|
|
42
|
+
MIT
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// src/theme.tsx
|
|
2
|
+
import { _$createTemplate } from "sinwan/renderer/web";
|
|
3
|
+
import { unwrap as _$unwrap } from "sinwan/reactivity";
|
|
4
|
+
import {
|
|
5
|
+
cc,
|
|
6
|
+
inject,
|
|
7
|
+
onMounted,
|
|
8
|
+
onUnmounted,
|
|
9
|
+
provide
|
|
10
|
+
} from "sinwan/component";
|
|
11
|
+
import { batch, computed, signal } from "sinwan/reactivity";
|
|
12
|
+
import { jsx, Fragment } from "sinwan/jsx-runtime";
|
|
13
|
+
var _$tmpl_0 = {
|
|
14
|
+
html: '<button type="button" data-slot="theme-toggle" aria-label="Toggle color theme"></button>',
|
|
15
|
+
slots: [
|
|
16
|
+
{ path: [], type: "attr", name: "class" },
|
|
17
|
+
{ path: [], type: "event", name: "onclick" },
|
|
18
|
+
{ path: [], type: "child" }
|
|
19
|
+
]
|
|
20
|
+
};
|
|
21
|
+
var DEFAULT_PALETTES = ["credit-blue", "jomlaty", "cazview"];
|
|
22
|
+
var ThemeKey2 = Symbol("sinwan-ui.theme");
|
|
23
|
+
function isTheme(value) {
|
|
24
|
+
return value === "light" || value === "dark" || value === "system";
|
|
25
|
+
}
|
|
26
|
+
function isPalette(value, allowed) {
|
|
27
|
+
return value !== null && allowed.includes(value);
|
|
28
|
+
}
|
|
29
|
+
function readStoredTheme(storageKey, fallback) {
|
|
30
|
+
if (typeof localStorage === "undefined")
|
|
31
|
+
return fallback;
|
|
32
|
+
try {
|
|
33
|
+
const value = localStorage.getItem(storageKey);
|
|
34
|
+
if (isTheme(value))
|
|
35
|
+
return value;
|
|
36
|
+
} catch {}
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
function readStoredPalette(storageKey, fallback, allowed) {
|
|
40
|
+
if (typeof localStorage === "undefined")
|
|
41
|
+
return fallback;
|
|
42
|
+
try {
|
|
43
|
+
const value = localStorage.getItem(storageKey);
|
|
44
|
+
if (isPalette(value, allowed))
|
|
45
|
+
return value;
|
|
46
|
+
} catch {}
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
function getSystemTheme() {
|
|
50
|
+
if (typeof window === "undefined" || !window.matchMedia)
|
|
51
|
+
return "light";
|
|
52
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
53
|
+
}
|
|
54
|
+
function disableTransitionsTemporarily() {
|
|
55
|
+
const style = document.createElement("style");
|
|
56
|
+
style.appendChild(document.createTextNode("*,*::before,*::after{transition:none!important;animation:none!important}"));
|
|
57
|
+
document.head.appendChild(style);
|
|
58
|
+
return () => {
|
|
59
|
+
window.getComputedStyle(document.body);
|
|
60
|
+
requestAnimationFrame(() => {
|
|
61
|
+
style.parentNode?.removeChild(style);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function writeDomTheme(next, attribute) {
|
|
66
|
+
if (typeof document === "undefined")
|
|
67
|
+
return;
|
|
68
|
+
const root = document.documentElement;
|
|
69
|
+
if (attribute === "class") {
|
|
70
|
+
root.classList.remove("light", "dark");
|
|
71
|
+
root.classList.add(next);
|
|
72
|
+
} else {
|
|
73
|
+
root.setAttribute("data-theme", next);
|
|
74
|
+
}
|
|
75
|
+
root.style.colorScheme = next;
|
|
76
|
+
}
|
|
77
|
+
function writeDomPalette(next) {
|
|
78
|
+
if (typeof document === "undefined")
|
|
79
|
+
return;
|
|
80
|
+
document.documentElement.setAttribute("data-palette", next);
|
|
81
|
+
}
|
|
82
|
+
var ThemeProvider2 = cc((props) => {
|
|
83
|
+
const allowedPalettes = (props.palettes === undefined ? DEFAULT_PALETTES : props.palettes).length > 0 ? props.palettes === undefined ? DEFAULT_PALETTES : props.palettes : DEFAULT_PALETTES;
|
|
84
|
+
const initialTheme = props.forcedTheme ?? readStoredTheme(props.storageKey === undefined ? "sinwan-ui-theme" : props.storageKey, props.defaultTheme === undefined ? "system" : props.defaultTheme);
|
|
85
|
+
const initialPalette = props.forcedPalette ?? readStoredPalette(props.paletteStorageKey === undefined ? "sinwan-ui-palette" : props.paletteStorageKey, props.defaultPalette === undefined ? "credit-blue" : props.defaultPalette, allowedPalettes);
|
|
86
|
+
const theme = signal(initialTheme);
|
|
87
|
+
const palette = signal(initialPalette);
|
|
88
|
+
const resolved = signal(initialTheme === "system" ? getSystemTheme() : initialTheme);
|
|
89
|
+
const resolve = (value) => {
|
|
90
|
+
if (props.forcedTheme === "light" || props.forcedTheme === "dark")
|
|
91
|
+
return props.forcedTheme;
|
|
92
|
+
if (props.forcedTheme === "system")
|
|
93
|
+
return getSystemTheme();
|
|
94
|
+
if (value === "system")
|
|
95
|
+
return (props.enableSystem === undefined ? true : props.enableSystem) ? getSystemTheme() : "light";
|
|
96
|
+
return value;
|
|
97
|
+
};
|
|
98
|
+
const withTransitionLock = (apply) => {
|
|
99
|
+
const restore = (props.disableTransitionOnChange === undefined ? true : props.disableTransitionOnChange) && typeof document !== "undefined" ? disableTransitionsTemporarily() : null;
|
|
100
|
+
apply();
|
|
101
|
+
restore?.();
|
|
102
|
+
};
|
|
103
|
+
const commit = (value, nextTheme) => {
|
|
104
|
+
const next = resolve(value);
|
|
105
|
+
withTransitionLock(() => {
|
|
106
|
+
writeDomTheme(next, props.attribute === undefined ? "class" : props.attribute);
|
|
107
|
+
writeDomPalette(palette.value);
|
|
108
|
+
batch(() => {
|
|
109
|
+
if (nextTheme !== undefined) {
|
|
110
|
+
theme.value = nextTheme;
|
|
111
|
+
}
|
|
112
|
+
resolved.value = next;
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
const commitPalette = (next) => {
|
|
117
|
+
withTransitionLock(() => {
|
|
118
|
+
writeDomPalette(next);
|
|
119
|
+
writeDomTheme(resolved.value, props.attribute === undefined ? "class" : props.attribute);
|
|
120
|
+
palette.value = next;
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
const setTheme = (value) => {
|
|
124
|
+
if (props.forcedTheme)
|
|
125
|
+
return;
|
|
126
|
+
try {
|
|
127
|
+
localStorage.setItem(props.storageKey === undefined ? "sinwan-ui-theme" : props.storageKey, value);
|
|
128
|
+
} catch {}
|
|
129
|
+
commit(value, value);
|
|
130
|
+
};
|
|
131
|
+
const setPalette = (value) => {
|
|
132
|
+
if (props.forcedPalette)
|
|
133
|
+
return;
|
|
134
|
+
if (!allowedPalettes.includes(value))
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
localStorage.setItem(props.paletteStorageKey === undefined ? "sinwan-ui-palette" : props.paletteStorageKey, value);
|
|
138
|
+
} catch {}
|
|
139
|
+
commitPalette(value);
|
|
140
|
+
};
|
|
141
|
+
onMounted(() => {
|
|
142
|
+
commit(theme.value);
|
|
143
|
+
writeDomPalette(palette.value);
|
|
144
|
+
if (!(props.enableSystem === undefined ? true : props.enableSystem))
|
|
145
|
+
return;
|
|
146
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
147
|
+
const onChange = () => {
|
|
148
|
+
if (theme.value === "system" || props.forcedTheme === "system") {
|
|
149
|
+
commit("system");
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
mq.addEventListener("change", onChange);
|
|
153
|
+
onUnmounted(() => mq.removeEventListener("change", onChange));
|
|
154
|
+
});
|
|
155
|
+
provide(ThemeKey2, {
|
|
156
|
+
theme,
|
|
157
|
+
resolved,
|
|
158
|
+
setTheme,
|
|
159
|
+
themes: (props.enableSystem === undefined ? true : props.enableSystem) ? ["light", "dark", "system"] : ["light", "dark"],
|
|
160
|
+
palette,
|
|
161
|
+
setPalette,
|
|
162
|
+
palettes: allowedPalettes
|
|
163
|
+
});
|
|
164
|
+
computed(() => resolved.value);
|
|
165
|
+
computed(() => palette.value);
|
|
166
|
+
return /* @__PURE__ */ jsx(Fragment, {
|
|
167
|
+
children: () => _$unwrap(props).children
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
function useTheme2() {
|
|
171
|
+
const api = inject(ThemeKey2);
|
|
172
|
+
if (!api) {
|
|
173
|
+
throw new Error("useTheme() must be used within a ThemeProvider");
|
|
174
|
+
}
|
|
175
|
+
return api;
|
|
176
|
+
}
|
|
177
|
+
var ThemeToggle2 = cc((props) => {
|
|
178
|
+
const { theme, resolved, setTheme } = useTheme2();
|
|
179
|
+
return _$createTemplate(_$tmpl_0, [
|
|
180
|
+
() => _$unwrap(props).class,
|
|
181
|
+
() => {
|
|
182
|
+
const next = theme.value === "system" ? resolved.value === "dark" ? "light" : "dark" : theme.value === "dark" ? "light" : "dark";
|
|
183
|
+
setTheme(next);
|
|
184
|
+
},
|
|
185
|
+
() => _$unwrap(props).children ?? (resolved.value === "dark" ? "Light" : "Dark")
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
export { ThemeKey2, ThemeProvider2, useTheme2, ThemeToggle2 };
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// src/floating.ts
|
|
2
|
+
import {
|
|
3
|
+
autoUpdate,
|
|
4
|
+
computePosition as fuiComputePosition,
|
|
5
|
+
flip,
|
|
6
|
+
limitShift,
|
|
7
|
+
offset,
|
|
8
|
+
platform as domPlatform,
|
|
9
|
+
shift
|
|
10
|
+
} from "@floating-ui/dom";
|
|
11
|
+
import { onMounted, onUnmounted } from "sinwan/component";
|
|
12
|
+
import {
|
|
13
|
+
effect,
|
|
14
|
+
signal,
|
|
15
|
+
untrack
|
|
16
|
+
} from "sinwan/reactivity";
|
|
17
|
+
function toFloatingPlacement2(placement = "bottom", align = "center") {
|
|
18
|
+
if (align === "center")
|
|
19
|
+
return placement;
|
|
20
|
+
return `${placement}-${align}`;
|
|
21
|
+
}
|
|
22
|
+
function basePlacement2(placement) {
|
|
23
|
+
const base = String(placement).split("-")[0] ?? "bottom";
|
|
24
|
+
if (base === "top" || base === "right" || base === "bottom" || base === "left") {
|
|
25
|
+
return base;
|
|
26
|
+
}
|
|
27
|
+
return "bottom";
|
|
28
|
+
}
|
|
29
|
+
function readDocumentRtl2() {
|
|
30
|
+
try {
|
|
31
|
+
const root = document.documentElement;
|
|
32
|
+
if (!(root instanceof Element))
|
|
33
|
+
return false;
|
|
34
|
+
return getComputedStyle(root).direction === "rtl";
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
var safePlatform = {
|
|
40
|
+
...domPlatform,
|
|
41
|
+
isRTL: readDocumentRtl2
|
|
42
|
+
};
|
|
43
|
+
function virtualClientRect(x, y, width, height) {
|
|
44
|
+
return {
|
|
45
|
+
x,
|
|
46
|
+
y,
|
|
47
|
+
width,
|
|
48
|
+
height,
|
|
49
|
+
top: y,
|
|
50
|
+
left: x,
|
|
51
|
+
right: x + width,
|
|
52
|
+
bottom: y + height,
|
|
53
|
+
toJSON() {
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function createRectReference2(rect) {
|
|
59
|
+
return {
|
|
60
|
+
getBoundingClientRect() {
|
|
61
|
+
return virtualClientRect(rect.left, rect.top, rect.width, rect.height);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function createSizeFloating2(width, height) {
|
|
66
|
+
return {
|
|
67
|
+
getBoundingClientRect() {
|
|
68
|
+
return virtualClientRect(0, 0, width, height);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function createPointReference2(x, y) {
|
|
73
|
+
return {
|
|
74
|
+
getBoundingClientRect() {
|
|
75
|
+
return virtualClientRect(x, y, 0, 0);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function defaultMiddleware(gap) {
|
|
80
|
+
return [
|
|
81
|
+
offset(gap),
|
|
82
|
+
flip({ crossAxis: true, fallbackAxisSideDirection: "start" }),
|
|
83
|
+
shift({ padding: 8, limiter: limitShift() })
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
async function runFloatingCompute(reference, floating, placement, gap) {
|
|
87
|
+
const result = await fuiComputePosition(reference, floating, {
|
|
88
|
+
placement,
|
|
89
|
+
strategy: "fixed",
|
|
90
|
+
middleware: defaultMiddleware(gap),
|
|
91
|
+
platform: safePlatform
|
|
92
|
+
});
|
|
93
|
+
return { top: result.y, left: result.x, placement: result.placement };
|
|
94
|
+
}
|
|
95
|
+
async function computePosition2(anchor, content, placement = "bottom", align = "center", gap = 8) {
|
|
96
|
+
try {
|
|
97
|
+
return await runFloatingCompute(createRectReference2(anchor), createSizeFloating2(content.width, content.height), toFloatingPlacement2(placement, align), gap);
|
|
98
|
+
} catch {
|
|
99
|
+
const estimated = estimatePosition2(anchor, content, placement, align, gap);
|
|
100
|
+
return {
|
|
101
|
+
top: estimated.top,
|
|
102
|
+
left: estimated.left,
|
|
103
|
+
placement: toFloatingPlacement2(placement, align)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function estimatePosition2(anchor, content, placement = "bottom", align = "center", gap = 8) {
|
|
108
|
+
let top = 0;
|
|
109
|
+
let left = 0;
|
|
110
|
+
switch (placement) {
|
|
111
|
+
case "top":
|
|
112
|
+
top = anchor.top - content.height - gap;
|
|
113
|
+
break;
|
|
114
|
+
case "bottom":
|
|
115
|
+
top = anchor.bottom + gap;
|
|
116
|
+
break;
|
|
117
|
+
case "left":
|
|
118
|
+
left = anchor.left - content.width - gap;
|
|
119
|
+
top = anchor.top;
|
|
120
|
+
break;
|
|
121
|
+
case "right":
|
|
122
|
+
left = anchor.right + gap;
|
|
123
|
+
top = anchor.top;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
if (placement === "top" || placement === "bottom") {
|
|
127
|
+
if (align === "start")
|
|
128
|
+
left = anchor.left;
|
|
129
|
+
else if (align === "end")
|
|
130
|
+
left = anchor.right - content.width;
|
|
131
|
+
else
|
|
132
|
+
left = anchor.left + (anchor.width - content.width) / 2;
|
|
133
|
+
} else {
|
|
134
|
+
if (align === "start")
|
|
135
|
+
top = anchor.top;
|
|
136
|
+
else if (align === "end")
|
|
137
|
+
top = anchor.bottom - content.height;
|
|
138
|
+
else
|
|
139
|
+
top = anchor.top + (anchor.height - content.height) / 2;
|
|
140
|
+
}
|
|
141
|
+
const maxLeft = window.innerWidth - content.width - 8;
|
|
142
|
+
const maxTop = window.innerHeight - content.height - 8;
|
|
143
|
+
return {
|
|
144
|
+
top: Math.max(8, Math.min(top, maxTop)),
|
|
145
|
+
left: Math.max(8, Math.min(left, maxLeft))
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function applyFixedStyle(style, top, left, extra) {
|
|
149
|
+
untrack(() => {
|
|
150
|
+
style.value = {
|
|
151
|
+
position: "fixed",
|
|
152
|
+
top: `${top}px`,
|
|
153
|
+
left: `${left}px`,
|
|
154
|
+
zIndex: "50",
|
|
155
|
+
...extra
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function bindAutoUpdate(reference, floating, update) {
|
|
160
|
+
if (!floating.isConnected)
|
|
161
|
+
return;
|
|
162
|
+
try {
|
|
163
|
+
return autoUpdate(reference, floating, update, { layoutShift: false });
|
|
164
|
+
} catch {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function readContentRect(content) {
|
|
169
|
+
if (content == null || typeof content.getBoundingClientRect !== "function") {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
return content.getBoundingClientRect();
|
|
174
|
+
} catch {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function useAnchorPosition2(options) {
|
|
179
|
+
const initialSide = options.placement ?? "bottom";
|
|
180
|
+
const style = signal({
|
|
181
|
+
position: "fixed",
|
|
182
|
+
top: "0px",
|
|
183
|
+
left: "0px",
|
|
184
|
+
zIndex: "50"
|
|
185
|
+
});
|
|
186
|
+
const ready = signal(false);
|
|
187
|
+
const side = signal(initialSide);
|
|
188
|
+
let generation = 0;
|
|
189
|
+
let stopAutoUpdate;
|
|
190
|
+
const clearAutoUpdate = () => {
|
|
191
|
+
stopAutoUpdate?.();
|
|
192
|
+
stopAutoUpdate = undefined;
|
|
193
|
+
};
|
|
194
|
+
const readContent = () => options.content?.value;
|
|
195
|
+
const contentSize = () => {
|
|
196
|
+
const measured = readContentRect(readContent());
|
|
197
|
+
return {
|
|
198
|
+
width: measured && measured.width > 0 ? measured.width : options.fallbackSize?.width ?? 0,
|
|
199
|
+
height: measured && measured.height > 0 ? measured.height : options.fallbackSize?.height ?? 0
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
const refineWithFloatingUi = (trigger, floating, gen) => {
|
|
203
|
+
const placement = options.placement ?? "bottom";
|
|
204
|
+
const align = options.align ?? "center";
|
|
205
|
+
const gap = options.gap ?? 8;
|
|
206
|
+
runFloatingCompute(trigger, floating, toFloatingPlacement2(placement, align), gap).then((result) => {
|
|
207
|
+
if (gen !== generation)
|
|
208
|
+
return;
|
|
209
|
+
if (!options.open.value)
|
|
210
|
+
return;
|
|
211
|
+
const anchor = trigger.getBoundingClientRect();
|
|
212
|
+
applyFixedStyle(style, result.top, result.left, options.extra?.(anchor));
|
|
213
|
+
untrack(() => {
|
|
214
|
+
side.value = basePlacement2(result.placement);
|
|
215
|
+
ready.value = true;
|
|
216
|
+
});
|
|
217
|
+
}).catch(() => {});
|
|
218
|
+
};
|
|
219
|
+
const place = () => {
|
|
220
|
+
const trigger = options.trigger.value;
|
|
221
|
+
if (!options.open.value || !trigger) {
|
|
222
|
+
untrack(() => {
|
|
223
|
+
ready.value = false;
|
|
224
|
+
});
|
|
225
|
+
clearAutoUpdate();
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
const placement = options.placement ?? "bottom";
|
|
229
|
+
const align = options.align ?? "center";
|
|
230
|
+
const gap = options.gap ?? 8;
|
|
231
|
+
const size = contentSize();
|
|
232
|
+
const anchor = trigger.getBoundingClientRect();
|
|
233
|
+
const estimated = estimatePosition2(anchor, size, placement, align, gap);
|
|
234
|
+
applyFixedStyle(style, estimated.top, estimated.left, options.extra?.(anchor));
|
|
235
|
+
untrack(() => {
|
|
236
|
+
side.value = placement;
|
|
237
|
+
ready.value = true;
|
|
238
|
+
});
|
|
239
|
+
const gen = ++generation;
|
|
240
|
+
const content = readContent();
|
|
241
|
+
const floating = content ?? createSizeFloating2(size.width, size.height);
|
|
242
|
+
refineWithFloatingUi(trigger, floating, gen);
|
|
243
|
+
clearAutoUpdate();
|
|
244
|
+
if (content) {
|
|
245
|
+
stopAutoUpdate = bindAutoUpdate(trigger, content, () => {
|
|
246
|
+
refineWithFloatingUi(trigger, content, generation);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
};
|
|
251
|
+
const present = () => {
|
|
252
|
+
if (!options.open.value) {
|
|
253
|
+
untrack(() => {
|
|
254
|
+
ready.value = false;
|
|
255
|
+
});
|
|
256
|
+
clearAutoUpdate();
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
return place();
|
|
260
|
+
};
|
|
261
|
+
onMounted(() => {
|
|
262
|
+
const stop = effect(() => {
|
|
263
|
+
options.open.value;
|
|
264
|
+
options.trigger.value;
|
|
265
|
+
readContent();
|
|
266
|
+
untrack(() => {
|
|
267
|
+
place();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
onUnmounted(() => {
|
|
271
|
+
stop();
|
|
272
|
+
clearAutoUpdate();
|
|
273
|
+
generation += 1;
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
return {
|
|
277
|
+
style,
|
|
278
|
+
ready,
|
|
279
|
+
side,
|
|
280
|
+
present: {
|
|
281
|
+
get value() {
|
|
282
|
+
return present();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function usePointPosition2(options) {
|
|
288
|
+
const style = signal({
|
|
289
|
+
position: "fixed",
|
|
290
|
+
top: "0px",
|
|
291
|
+
left: "0px",
|
|
292
|
+
zIndex: "50"
|
|
293
|
+
});
|
|
294
|
+
const ready = signal(false);
|
|
295
|
+
const side = signal("bottom");
|
|
296
|
+
let generation = 0;
|
|
297
|
+
let stopAutoUpdate;
|
|
298
|
+
const clearAutoUpdate = () => {
|
|
299
|
+
stopAutoUpdate?.();
|
|
300
|
+
stopAutoUpdate = undefined;
|
|
301
|
+
};
|
|
302
|
+
const readContent = () => options.content?.value;
|
|
303
|
+
const refinePoint = (reference, floating, gen) => {
|
|
304
|
+
runFloatingCompute(reference, floating, "bottom-start", 0).then((result) => {
|
|
305
|
+
if (gen !== generation)
|
|
306
|
+
return;
|
|
307
|
+
if (!options.open.value)
|
|
308
|
+
return;
|
|
309
|
+
applyFixedStyle(style, result.top, result.left);
|
|
310
|
+
untrack(() => {
|
|
311
|
+
side.value = basePlacement2(result.placement);
|
|
312
|
+
ready.value = true;
|
|
313
|
+
});
|
|
314
|
+
}).catch(() => {});
|
|
315
|
+
};
|
|
316
|
+
const place = () => {
|
|
317
|
+
if (!options.open.value) {
|
|
318
|
+
untrack(() => {
|
|
319
|
+
ready.value = false;
|
|
320
|
+
});
|
|
321
|
+
clearAutoUpdate();
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
const { x, y } = options.point.value;
|
|
325
|
+
applyFixedStyle(style, y, x);
|
|
326
|
+
untrack(() => {
|
|
327
|
+
ready.value = true;
|
|
328
|
+
});
|
|
329
|
+
const content = readContent();
|
|
330
|
+
const measured = readContentRect(content);
|
|
331
|
+
const size = {
|
|
332
|
+
width: measured && measured.width > 0 ? measured.width : options.fallbackSize?.width ?? 0,
|
|
333
|
+
height: measured && measured.height > 0 ? measured.height : options.fallbackSize?.height ?? 0
|
|
334
|
+
};
|
|
335
|
+
const gen = ++generation;
|
|
336
|
+
const reference = createPointReference2(x, y);
|
|
337
|
+
const floating = content ?? createSizeFloating2(size.width, size.height);
|
|
338
|
+
refinePoint(reference, floating, gen);
|
|
339
|
+
clearAutoUpdate();
|
|
340
|
+
if (content) {
|
|
341
|
+
stopAutoUpdate = bindAutoUpdate(reference, content, () => {
|
|
342
|
+
refinePoint(reference, content, generation);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return true;
|
|
346
|
+
};
|
|
347
|
+
const present = () => {
|
|
348
|
+
if (!options.open.value) {
|
|
349
|
+
untrack(() => {
|
|
350
|
+
ready.value = false;
|
|
351
|
+
});
|
|
352
|
+
clearAutoUpdate();
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
return place();
|
|
356
|
+
};
|
|
357
|
+
onMounted(() => {
|
|
358
|
+
const stop = effect(() => {
|
|
359
|
+
options.open.value;
|
|
360
|
+
options.point.value;
|
|
361
|
+
readContent();
|
|
362
|
+
untrack(() => {
|
|
363
|
+
place();
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
onUnmounted(() => {
|
|
367
|
+
stop();
|
|
368
|
+
clearAutoUpdate();
|
|
369
|
+
generation += 1;
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
style,
|
|
374
|
+
ready,
|
|
375
|
+
side,
|
|
376
|
+
present: {
|
|
377
|
+
get value() {
|
|
378
|
+
return present();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export { toFloatingPlacement2, basePlacement2, readDocumentRtl2, createRectReference2, createSizeFloating2, createPointReference2, computePosition2, estimatePosition2, useAnchorPosition2, usePointPosition2 };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
import { unwrap } from "sinwan/reactivity";
|
|
3
|
+
function isSinwanElement2(node) {
|
|
4
|
+
return typeof node === "object" && node !== null && !Array.isArray(node) && "tag" in node && "props" in node;
|
|
5
|
+
}
|
|
6
|
+
function flattenChildren2(children) {
|
|
7
|
+
const resolved = unwrap(children);
|
|
8
|
+
if (resolved == null || resolved === false || resolved === true)
|
|
9
|
+
return [];
|
|
10
|
+
if (Array.isArray(resolved)) {
|
|
11
|
+
return resolved.flatMap((child) => flattenChildren2(child));
|
|
12
|
+
}
|
|
13
|
+
return [resolved];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { isSinwanElement2, flattenChildren2 };
|