@plastic-js/tsumiki 0.1.42 → 0.1.44
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/dist/components/BottomSheet.js +85 -43
- package/dist/components/Dialog.js +11 -3
- package/docs/api.md +6 -2
- package/package.json +5 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Portal_default from "./Portal.js";
|
|
2
2
|
import { insert, jsx, mergeProps, setProp, template } from "@plastic-js/plastic/jsx-runtime";
|
|
3
|
-
import { css } from "@emotion/css";
|
|
4
|
-
import { createEffect, createSignal, mergeProps as mergeProps$1, splitProps } from "@plastic-js/plastic";
|
|
3
|
+
import { css, keyframes } from "@emotion/css";
|
|
4
|
+
import { Either, createEffect, createSignal, mergeProps as mergeProps$1, onCleanup, splitProps } from "@plastic-js/plastic";
|
|
5
5
|
//#region src/components/BottomSheet.jsx
|
|
6
6
|
var _tmpl3 = template("<div data-part=\"grabber\"><div></div></div>");
|
|
7
7
|
var _tmpl2 = template("<div role=\"dialog\" aria-modal=\"true\" aria-label=\"BottomSheet\"><!><div data-part=\"content\"></div></div>");
|
|
@@ -45,7 +45,11 @@ var sheetClass = css({
|
|
|
45
45
|
"&[data-open]": {
|
|
46
46
|
transform: "translateY(0)",
|
|
47
47
|
pointerEvents: "auto",
|
|
48
|
-
transition: "transform var(--tsu-transition-overlay-enter)"
|
|
48
|
+
transition: "transform var(--tsu-transition-overlay-enter)",
|
|
49
|
+
animation: `${keyframes({
|
|
50
|
+
from: { transform: "translateY(100%)" },
|
|
51
|
+
to: { transform: "translateY(0)" }
|
|
52
|
+
})} var(--tsu-duration-overlay-enter) var(--tsu-ease-out)`
|
|
49
53
|
},
|
|
50
54
|
"&::after": {
|
|
51
55
|
content: "\"\"",
|
|
@@ -57,7 +61,10 @@ var sheetClass = css({
|
|
|
57
61
|
background: "var(--tsu-surface)"
|
|
58
62
|
}
|
|
59
63
|
});
|
|
60
|
-
var draggingClass = css({
|
|
64
|
+
var draggingClass = css({
|
|
65
|
+
transition: "none",
|
|
66
|
+
animation: "none"
|
|
67
|
+
});
|
|
61
68
|
var grabberAreaClass = css({
|
|
62
69
|
display: "flex",
|
|
63
70
|
alignItems: "center",
|
|
@@ -82,11 +89,21 @@ var contentClass = css({
|
|
|
82
89
|
minHeight: 0
|
|
83
90
|
});
|
|
84
91
|
var read = (v) => typeof v === "function" ? v() : v;
|
|
92
|
+
var DEFAULT_EXIT_MS = 200;
|
|
93
|
+
var toMs = (s) => {
|
|
94
|
+
const v = String(s).trim().toLowerCase();
|
|
95
|
+
if (!v) return 0;
|
|
96
|
+
if (v.endsWith("ms")) return parseFloat(v) || 0;
|
|
97
|
+
if (v.endsWith("s")) return (parseFloat(v) || 0) * 1e3;
|
|
98
|
+
return parseFloat(v) || 0;
|
|
99
|
+
};
|
|
85
100
|
var BottomSheet = (props) => {
|
|
86
101
|
const [local] = splitProps(mergeProps$1({
|
|
87
102
|
dismissible: true,
|
|
88
103
|
backdropBlur: true,
|
|
89
|
-
draggable: true
|
|
104
|
+
draggable: true,
|
|
105
|
+
lazyMount: true,
|
|
106
|
+
unmountOnExit: true
|
|
90
107
|
}, props), [
|
|
91
108
|
"open",
|
|
92
109
|
"onOpen",
|
|
@@ -98,10 +115,22 @@ var BottomSheet = (props) => {
|
|
|
98
115
|
"backdropClassName",
|
|
99
116
|
"backdropStyle",
|
|
100
117
|
"contentClassName",
|
|
118
|
+
"lazyMount",
|
|
119
|
+
"unmountOnExit",
|
|
101
120
|
"class",
|
|
102
121
|
"children"
|
|
103
122
|
]);
|
|
104
123
|
const dragging = createSignal(false);
|
|
124
|
+
const isOpen = () => read(local.open);
|
|
125
|
+
const render = createSignal(!local.lazyMount || isOpen());
|
|
126
|
+
let exitTimer = null;
|
|
127
|
+
const measureExitDuration = () => {
|
|
128
|
+
const el = sheetEl;
|
|
129
|
+
if (!el) return DEFAULT_EXIT_MS;
|
|
130
|
+
const duration = getComputedStyle(el).transitionDuration;
|
|
131
|
+
const ms = String(duration).split(",").reduce((max, d) => Math.max(max, toMs(d)), 0);
|
|
132
|
+
return ms > 0 ? ms : DEFAULT_EXIT_MS;
|
|
133
|
+
};
|
|
105
134
|
let sheetEl = null;
|
|
106
135
|
let startY = 0;
|
|
107
136
|
let currentDy = 0;
|
|
@@ -112,13 +141,22 @@ var BottomSheet = (props) => {
|
|
|
112
141
|
if (!detail.open) local.onClose?.();
|
|
113
142
|
local.onOpenChange?.(detail);
|
|
114
143
|
};
|
|
115
|
-
const isOpen = () => read(local.open);
|
|
116
144
|
let prevOpen = isOpen();
|
|
117
145
|
createEffect(() => {
|
|
118
146
|
const cur = isOpen();
|
|
119
147
|
if (cur && !prevOpen) local.onOpen?.();
|
|
148
|
+
if (cur) {
|
|
149
|
+
if (exitTimer) {
|
|
150
|
+
clearTimeout(exitTimer);
|
|
151
|
+
exitTimer = null;
|
|
152
|
+
}
|
|
153
|
+
render(true);
|
|
154
|
+
} else if (prevOpen && local.unmountOnExit && render()) exitTimer = setTimeout(() => render(false), measureExitDuration());
|
|
120
155
|
prevOpen = cur;
|
|
121
156
|
});
|
|
157
|
+
onCleanup(() => {
|
|
158
|
+
if (exitTimer) clearTimeout(exitTimer);
|
|
159
|
+
});
|
|
122
160
|
const handleBackdropClick = () => {
|
|
123
161
|
if (read(local.dismissible)) handleOpenChange({ open: false });
|
|
124
162
|
};
|
|
@@ -156,45 +194,49 @@ var BottomSheet = (props) => {
|
|
|
156
194
|
settle(false);
|
|
157
195
|
};
|
|
158
196
|
const showGrabber = read(local.draggable);
|
|
159
|
-
return jsx(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
insert(_el0, () => showGrabber && (() => {
|
|
175
|
-
const _el0 = _tmpl3.cloneNode(true);
|
|
197
|
+
return jsx(Either, mergeProps({
|
|
198
|
+
condition: () => render(),
|
|
199
|
+
trueBranch: () => jsx(Portal_default, mergeProps({ children: [(() => {
|
|
200
|
+
const _el0 = _tmpl.cloneNode(true);
|
|
201
|
+
setProp(_el0, "className", () => [
|
|
202
|
+
backdropClass,
|
|
203
|
+
local.backdropBlur && backdropBlurClass,
|
|
204
|
+
local.backdropClassName
|
|
205
|
+
].filter(Boolean).join(" "));
|
|
206
|
+
setProp(_el0, "style", () => local.backdropStyle);
|
|
207
|
+
setProp(_el0, "data-open", () => read(local.open) ? "" : void 0);
|
|
208
|
+
setProp(_el0, "onClick", () => handleBackdropClick);
|
|
209
|
+
return _el0;
|
|
210
|
+
})(), () => {
|
|
211
|
+
const _el0 = _tmpl2.cloneNode(true);
|
|
176
212
|
const _el1 = _el0.firstChild;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
213
|
+
const _el2 = _el0.firstChild.nextSibling;
|
|
214
|
+
insert(_el0, () => showGrabber && (() => {
|
|
215
|
+
const _el0 = _tmpl3.cloneNode(true);
|
|
216
|
+
const _el1 = _el0.firstChild;
|
|
217
|
+
setProp(_el1, "className", () => grabberClass);
|
|
218
|
+
setProp(_el0, "className", () => grabberAreaClass);
|
|
219
|
+
setProp(_el0, "onPointerDown", () => onGrabberPointerDown);
|
|
220
|
+
setProp(_el0, "onPointerMove", () => onGrabberPointerMove);
|
|
221
|
+
setProp(_el0, "onPointerUp", () => onGrabberPointerUp);
|
|
222
|
+
setProp(_el0, "onPointerCancel", () => onGrabberPointerCancel);
|
|
223
|
+
return _el0;
|
|
224
|
+
})(), _el1);
|
|
225
|
+
insert(_el2, () => local.children);
|
|
226
|
+
setProp(_el2, "className", () => [contentClass, local.contentClassName].filter(Boolean).join(" "));
|
|
227
|
+
setProp(_el0, "ref", (el) => {
|
|
228
|
+
sheetEl = el;
|
|
229
|
+
});
|
|
230
|
+
setProp(_el0, "className", () => [
|
|
231
|
+
sheetClass,
|
|
232
|
+
dragging() && draggingClass,
|
|
233
|
+
local.class
|
|
234
|
+
].filter(Boolean).join(" "));
|
|
235
|
+
setProp(_el0, "data-open", () => read(local.open) ? "" : void 0);
|
|
183
236
|
return _el0;
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
setProp(_el0, "ref", (el) => {
|
|
188
|
-
sheetEl = el;
|
|
189
|
-
});
|
|
190
|
-
setProp(_el0, "className", () => [
|
|
191
|
-
sheetClass,
|
|
192
|
-
dragging() && draggingClass,
|
|
193
|
-
local.class
|
|
194
|
-
].filter(Boolean).join(" "));
|
|
195
|
-
setProp(_el0, "data-open", () => read(local.open) ? "" : void 0);
|
|
196
|
-
return _el0;
|
|
197
|
-
}] }));
|
|
237
|
+
}] })),
|
|
238
|
+
falseBranch: () => null
|
|
239
|
+
}));
|
|
198
240
|
};
|
|
199
241
|
//#endregion
|
|
200
242
|
export { BottomSheet as default };
|
|
@@ -3,8 +3,8 @@ import Button from "./Button.js";
|
|
|
3
3
|
import Icon from "./Icon.js";
|
|
4
4
|
import { insert, jsx, mergeProps, setProp, template } from "@plastic-js/plastic/jsx-runtime";
|
|
5
5
|
import { css, keyframes } from "@emotion/css";
|
|
6
|
-
import { Dialog } from "@plastic-js/ark";
|
|
7
|
-
import { mergeProps as mergeProps$2, splitProps } from "@plastic-js/plastic";
|
|
6
|
+
import { Dialog, useDialogContext } from "@plastic-js/ark";
|
|
7
|
+
import { Either, mergeProps as mergeProps$2, splitProps } from "@plastic-js/plastic";
|
|
8
8
|
//#region src/components/Dialog.jsx
|
|
9
9
|
var _tmpl4 = template("<div data-scope=\"dialog\" data-part=\"footer\"></div>");
|
|
10
10
|
var _tmpl3 = template("<div data-scope=\"dialog\" data-part=\"body\"></div>");
|
|
@@ -156,6 +156,14 @@ var positiveBtnClass = css({
|
|
|
156
156
|
});
|
|
157
157
|
var read = (v) => typeof v === "function" ? v() : v;
|
|
158
158
|
var warnedSheetNoTitle = false;
|
|
159
|
+
var DialogPortal = (props) => {
|
|
160
|
+
const dialog = useDialogContext();
|
|
161
|
+
return jsx(Either, mergeProps({
|
|
162
|
+
condition: () => !dialog()?.unmounted,
|
|
163
|
+
trueBranch: () => jsx(Portal_default, mergeProps({ children: () => props.children })),
|
|
164
|
+
falseBranch: () => null
|
|
165
|
+
}));
|
|
166
|
+
};
|
|
159
167
|
var Dialog$1 = (props) => {
|
|
160
168
|
const [local, rest] = splitProps(mergeProps$2({
|
|
161
169
|
mode: "sheet",
|
|
@@ -273,7 +281,7 @@ var Dialog$1 = (props) => {
|
|
|
273
281
|
return local.closeOnInteractOutside;
|
|
274
282
|
},
|
|
275
283
|
modal: true
|
|
276
|
-
}, rest, { children: jsx(
|
|
284
|
+
}, rest, { children: jsx(DialogPortal, mergeProps({ children: [jsx(Dialog.Backdrop, mergeProps({ className: isSheet ? backdropSheetClass : backdropModalClass })), jsx(Dialog.Positioner, mergeProps({
|
|
277
285
|
className: isSheet ? positionerSheetClass : positionerModalClass,
|
|
278
286
|
children: jsx(Dialog.Content, mergeProps({
|
|
279
287
|
get className() {
|
package/docs/api.md
CHANGED
|
@@ -643,6 +643,8 @@ Passthrough: Ark `Root` props (`value`, `min`, `max`, …).
|
|
|
643
643
|
|
|
644
644
|
> **Note:** Dialog never renders a grabber / drag handle in either mode (`modal` or `sheet`). Drag-to-dismiss and the grabber are exclusive to [`BottomSheet`](#bottomsheet) — this is a deliberate difference between the two components.
|
|
645
645
|
|
|
646
|
+
> **Note:** Dialog has **enter-only** animations — its content unmounts immediately on close (no exit transition). [`BottomSheet`](#bottomsheet) is the opposite: it stays mounted through its exit transition before the nodes are removed. If you need a closing animation, use `BottomSheet`.
|
|
647
|
+
|
|
646
648
|
| Prop | Type | Default | Description |
|
|
647
649
|
|---|---|---|---|
|
|
648
650
|
| `mode` | `string` | `'sheet'` | `'modal'` (centered) or `'sheet'` (bottom sheet). |
|
|
@@ -763,14 +765,16 @@ A free-form presentation layer: a backdrop, an optional drag grabber, and a scro
|
|
|
763
765
|
|
|
764
766
|
**Exports:** `BottomSheet` · no `.origin`
|
|
765
767
|
|
|
766
|
-
**Rendered DOM parts:** exactly three — `data-part="backdrop"`, `data-part="grabber"` (only when `draggable`), and `data-part="content"`. BottomSheet has no focus trap or Escape-to-close — use [`Dialog`](#dialog) when those are required.
|
|
768
|
+
**Rendered DOM parts:** exactly three — `data-part="backdrop"`, `data-part="grabber"` (only when `draggable`), and `data-part="content"`. With the default `lazyMount: true` + `unmountOnExit: true`, a closed sheet that was never opened renders **zero** nodes; after close it stays mounted only long enough for the exit transition, then unmounts. BottomSheet has no focus trap or Escape-to-close — use [`Dialog`](#dialog) when those are required.
|
|
767
769
|
|
|
768
770
|
> **Design decision:** `content` is a pure container — `display: flex; flex-direction: column` with `padding: 0 24px` only. It has **no vertical padding, no margin, and no scrolling**. Spacing and scrolling are entirely consumer-owned: build your own layout inside `children` (the sheet caps height at `70vh`, so a scrollable section can be a `flex: 1; min-height: 0; overflow-y: auto` child).
|
|
769
771
|
|
|
770
772
|
| Prop | Type | Default | Description |
|
|
771
|
-
|
|
773
|
+
|---|---|---|---|---|
|
|
772
774
|
| `open` | `boolean \| (() => boolean)` | — | **Required** controlled open state. |
|
|
773
775
|
| `onOpenChange` | `({ open }) => void` | — | Fired on open/close; sync your state here. |
|
|
776
|
+
| `lazyMount` | `boolean` | `true` | Mount only after the first open. A closed sheet that was never opened renders **no** DOM nodes. |
|
|
777
|
+
| `unmountOnExit` | `boolean` | `true` | Unmount after close, once the exit transition has played (the close delay is measured from the sheet's computed `transition-duration`, `200ms` fallback). When `false`, the sheet stays mounted and is hidden with CSS. |
|
|
774
778
|
| `dismissible` | `boolean` | `true` | Allow dismiss via backdrop / drag. |
|
|
775
779
|
| `draggable` | `boolean` | `true` | Show the grabber and enable drag-to-dismiss. |
|
|
776
780
|
| `backdropBlur` | `boolean` | `true` | Blur the backdrop. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plastic-js/tsumiki",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
4
4
|
"description": "A UI component library for Plastic JS.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "tigre",
|
|
@@ -50,7 +50,10 @@
|
|
|
50
50
|
"prepublishOnly": "npm run build",
|
|
51
51
|
"preview": "vite preview",
|
|
52
52
|
"theme": "node scripts/theme.js",
|
|
53
|
-
"e2e:repro": "node e2e/dlgcbx-repro.mjs"
|
|
53
|
+
"e2e:repro": "node e2e/dlgcbx-repro.mjs",
|
|
54
|
+
"e2e:bottomsheet": "node e2e/bottomsheet-presence-verify.mjs",
|
|
55
|
+
"e2e:dialog-portal": "node e2e/dialog-portal-verify.mjs",
|
|
56
|
+
"e2e:bottomsheet-animation": "node e2e/bottomsheet-animation-verify.mjs"
|
|
54
57
|
},
|
|
55
58
|
"dependencies": {
|
|
56
59
|
"@emotion/css": "^11.13.5",
|