sinwan-ui-core 0.0.0-stage → 1.0.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 +22 -0
- package/README.md +41 -2
- 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/chunks/chunk-xaegn57w.js +189 -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 +116 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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.0.0]
|
|
6
|
+
|
|
7
|
+
Initial release: the headless primitives extracted from `sinwan-ui` 0.1.0, published separately so registry components copied into apps can depend on them.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 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`.
|
|
12
|
+
- `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.
|
|
13
|
+
- `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.
|
|
14
|
+
- `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.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `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.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `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,3 +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
|
+
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add sinwan-ui-core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requirements:
|
|
14
|
+
|
|
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.
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT
|
|
@@ -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 };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useAnchorPosition2
|
|
3
|
+
} from "./chunk-6r59k5mb.js";
|
|
4
|
+
import {
|
|
5
|
+
Presence2,
|
|
6
|
+
UiPortal2
|
|
7
|
+
} from "./chunk-ejwby1pn.js";
|
|
8
|
+
|
|
9
|
+
// src/menu-sub.tsx
|
|
10
|
+
import { unwrap as _$unwrap } from "sinwan/reactivity";
|
|
11
|
+
import { cc } from "sinwan/component";
|
|
12
|
+
import { onUnmounted } from "sinwan/component";
|
|
13
|
+
import {
|
|
14
|
+
computed,
|
|
15
|
+
effect,
|
|
16
|
+
signal,
|
|
17
|
+
untrack
|
|
18
|
+
} from "sinwan/reactivity";
|
|
19
|
+
import { jsx } from "sinwan/jsx-runtime";
|
|
20
|
+
function createMenuSubApi2(options) {
|
|
21
|
+
const openLocal = signal(options.defaultOpen ?? false);
|
|
22
|
+
const open = options.controlled ? computed(options.readOpen) : openLocal;
|
|
23
|
+
const set = (next) => {
|
|
24
|
+
if (!options.controlled)
|
|
25
|
+
openLocal.value = next;
|
|
26
|
+
};
|
|
27
|
+
const triggerEl = signal(null);
|
|
28
|
+
let closeTimer;
|
|
29
|
+
function cancelClose() {
|
|
30
|
+
if (closeTimer !== undefined) {
|
|
31
|
+
window.clearTimeout(closeTimer);
|
|
32
|
+
closeTimer = undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function setOpen(value) {
|
|
36
|
+
cancelClose();
|
|
37
|
+
set(value);
|
|
38
|
+
options.onOpenChange?.(value);
|
|
39
|
+
}
|
|
40
|
+
function requestClose(delayMs = 100) {
|
|
41
|
+
cancelClose();
|
|
42
|
+
closeTimer = window.setTimeout(() => {
|
|
43
|
+
closeTimer = undefined;
|
|
44
|
+
setOpen(false);
|
|
45
|
+
}, delayMs);
|
|
46
|
+
}
|
|
47
|
+
const parentOpen = options.parentOpen;
|
|
48
|
+
if (parentOpen) {
|
|
49
|
+
effect(() => {
|
|
50
|
+
if (parentOpen())
|
|
51
|
+
return;
|
|
52
|
+
untrack(() => {
|
|
53
|
+
if (open.value)
|
|
54
|
+
setOpen(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
onUnmounted(cancelClose);
|
|
59
|
+
return { open, setOpen, triggerEl, cancelClose, requestClose };
|
|
60
|
+
}
|
|
61
|
+
function createMenuSubTriggerHandlers2(api) {
|
|
62
|
+
function openSub() {
|
|
63
|
+
api.cancelClose();
|
|
64
|
+
if (!api.open.value)
|
|
65
|
+
api.setOpen(true);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
onMouseEnter: openSub,
|
|
69
|
+
onMouseLeave: () => api.requestClose(),
|
|
70
|
+
onClick: openSub,
|
|
71
|
+
onKeyDown: (event) => {
|
|
72
|
+
if (event.key !== "ArrowRight")
|
|
73
|
+
return;
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
openSub();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function isInsideMenuSubContent2(target) {
|
|
80
|
+
return target instanceof Element && target.closest("[data-slot$='-sub-content']") != null;
|
|
81
|
+
}
|
|
82
|
+
var MenuSubContentLayer2 = cc(function MenuSubContentLayer(props) {
|
|
83
|
+
const contentEl = signal(null);
|
|
84
|
+
const { style, side } = useAnchorPosition2({
|
|
85
|
+
open: props.api.open,
|
|
86
|
+
trigger: props.api.triggerEl,
|
|
87
|
+
content: contentEl,
|
|
88
|
+
placement: "right",
|
|
89
|
+
align: "start",
|
|
90
|
+
gap: 4,
|
|
91
|
+
fallbackSize: { width: 128, height: 120 }
|
|
92
|
+
});
|
|
93
|
+
function subState() {
|
|
94
|
+
return props.api.open.value ? "open" : "closed";
|
|
95
|
+
}
|
|
96
|
+
return /* @__PURE__ */ jsx(Presence2, {
|
|
97
|
+
present: () => _$unwrap(props).api.open.value,
|
|
98
|
+
children: /* @__PURE__ */ jsx(UiPortal2, {
|
|
99
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
100
|
+
"data-slot": () => _$unwrap(props).slot,
|
|
101
|
+
"data-state": subState,
|
|
102
|
+
"data-side": () => side.value,
|
|
103
|
+
class: () => _$unwrap(props).class,
|
|
104
|
+
style: () => style.value,
|
|
105
|
+
onmouseenter: () => props.api.cancelClose(),
|
|
106
|
+
onmouseleave: () => props.api.requestClose(),
|
|
107
|
+
ref: (el) => {
|
|
108
|
+
contentEl.value = el;
|
|
109
|
+
},
|
|
110
|
+
children: () => _$unwrap(props).children
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export { createMenuSubApi2, createMenuSubTriggerHandlers2, isInsideMenuSubContent2, MenuSubContentLayer2 };
|