@plastic-js/tsumiki 0.1.41 → 0.1.43

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.
@@ -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
3
  import { css } from "@emotion/css";
4
- import { createEffect, createSignal, mergeProps as mergeProps$1, splitProps } from "@plastic-js/plastic";
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>");
@@ -82,11 +82,21 @@ var contentClass = css({
82
82
  minHeight: 0
83
83
  });
84
84
  var read = (v) => typeof v === "function" ? v() : v;
85
+ var DEFAULT_EXIT_MS = 200;
86
+ var toMs = (s) => {
87
+ const v = String(s).trim().toLowerCase();
88
+ if (!v) return 0;
89
+ if (v.endsWith("ms")) return parseFloat(v) || 0;
90
+ if (v.endsWith("s")) return (parseFloat(v) || 0) * 1e3;
91
+ return parseFloat(v) || 0;
92
+ };
85
93
  var BottomSheet = (props) => {
86
94
  const [local] = splitProps(mergeProps$1({
87
95
  dismissible: true,
88
96
  backdropBlur: true,
89
- draggable: true
97
+ draggable: true,
98
+ lazyMount: true,
99
+ unmountOnExit: true
90
100
  }, props), [
91
101
  "open",
92
102
  "onOpen",
@@ -98,10 +108,22 @@ var BottomSheet = (props) => {
98
108
  "backdropClassName",
99
109
  "backdropStyle",
100
110
  "contentClassName",
111
+ "lazyMount",
112
+ "unmountOnExit",
101
113
  "class",
102
114
  "children"
103
115
  ]);
104
116
  const dragging = createSignal(false);
117
+ const isOpen = () => read(local.open);
118
+ const render = createSignal(!local.lazyMount || isOpen());
119
+ let exitTimer = null;
120
+ const measureExitDuration = () => {
121
+ const el = sheetEl;
122
+ if (!el) return DEFAULT_EXIT_MS;
123
+ const duration = getComputedStyle(el).transitionDuration;
124
+ const ms = String(duration).split(",").reduce((max, d) => Math.max(max, toMs(d)), 0);
125
+ return ms > 0 ? ms : DEFAULT_EXIT_MS;
126
+ };
105
127
  let sheetEl = null;
106
128
  let startY = 0;
107
129
  let currentDy = 0;
@@ -112,13 +134,22 @@ var BottomSheet = (props) => {
112
134
  if (!detail.open) local.onClose?.();
113
135
  local.onOpenChange?.(detail);
114
136
  };
115
- const isOpen = () => read(local.open);
116
137
  let prevOpen = isOpen();
117
138
  createEffect(() => {
118
139
  const cur = isOpen();
119
140
  if (cur && !prevOpen) local.onOpen?.();
141
+ if (cur) {
142
+ if (exitTimer) {
143
+ clearTimeout(exitTimer);
144
+ exitTimer = null;
145
+ }
146
+ render(true);
147
+ } else if (prevOpen && local.unmountOnExit && render()) exitTimer = setTimeout(() => render(false), measureExitDuration());
120
148
  prevOpen = cur;
121
149
  });
150
+ onCleanup(() => {
151
+ if (exitTimer) clearTimeout(exitTimer);
152
+ });
122
153
  const handleBackdropClick = () => {
123
154
  if (read(local.dismissible)) handleOpenChange({ open: false });
124
155
  };
@@ -156,45 +187,49 @@ var BottomSheet = (props) => {
156
187
  settle(false);
157
188
  };
158
189
  const showGrabber = read(local.draggable);
159
- return jsx(Portal_default, mergeProps({ children: [(() => {
160
- const _el0 = _tmpl.cloneNode(true);
161
- setProp(_el0, "className", () => [
162
- backdropClass,
163
- local.backdropBlur && backdropBlurClass,
164
- local.backdropClassName
165
- ].filter(Boolean).join(" "));
166
- setProp(_el0, "style", () => local.backdropStyle);
167
- setProp(_el0, "data-open", () => read(local.open) ? "" : void 0);
168
- setProp(_el0, "onClick", () => handleBackdropClick);
169
- return _el0;
170
- })(), () => {
171
- const _el0 = _tmpl2.cloneNode(true);
172
- const _el1 = _el0.firstChild;
173
- const _el2 = _el0.firstChild.nextSibling;
174
- insert(_el0, () => showGrabber && (() => {
175
- const _el0 = _tmpl3.cloneNode(true);
190
+ return jsx(Either, mergeProps({
191
+ condition: () => render(),
192
+ trueBranch: () => jsx(Portal_default, mergeProps({ children: [(() => {
193
+ const _el0 = _tmpl.cloneNode(true);
194
+ setProp(_el0, "className", () => [
195
+ backdropClass,
196
+ local.backdropBlur && backdropBlurClass,
197
+ local.backdropClassName
198
+ ].filter(Boolean).join(" "));
199
+ setProp(_el0, "style", () => local.backdropStyle);
200
+ setProp(_el0, "data-open", () => read(local.open) ? "" : void 0);
201
+ setProp(_el0, "onClick", () => handleBackdropClick);
202
+ return _el0;
203
+ })(), () => {
204
+ const _el0 = _tmpl2.cloneNode(true);
176
205
  const _el1 = _el0.firstChild;
177
- setProp(_el1, "className", () => grabberClass);
178
- setProp(_el0, "className", () => grabberAreaClass);
179
- setProp(_el0, "onPointerDown", () => onGrabberPointerDown);
180
- setProp(_el0, "onPointerMove", () => onGrabberPointerMove);
181
- setProp(_el0, "onPointerUp", () => onGrabberPointerUp);
182
- setProp(_el0, "onPointerCancel", () => onGrabberPointerCancel);
206
+ const _el2 = _el0.firstChild.nextSibling;
207
+ insert(_el0, () => showGrabber && (() => {
208
+ const _el0 = _tmpl3.cloneNode(true);
209
+ const _el1 = _el0.firstChild;
210
+ setProp(_el1, "className", () => grabberClass);
211
+ setProp(_el0, "className", () => grabberAreaClass);
212
+ setProp(_el0, "onPointerDown", () => onGrabberPointerDown);
213
+ setProp(_el0, "onPointerMove", () => onGrabberPointerMove);
214
+ setProp(_el0, "onPointerUp", () => onGrabberPointerUp);
215
+ setProp(_el0, "onPointerCancel", () => onGrabberPointerCancel);
216
+ return _el0;
217
+ })(), _el1);
218
+ insert(_el2, () => local.children);
219
+ setProp(_el2, "className", () => [contentClass, local.contentClassName].filter(Boolean).join(" "));
220
+ setProp(_el0, "ref", (el) => {
221
+ sheetEl = el;
222
+ });
223
+ setProp(_el0, "className", () => [
224
+ sheetClass,
225
+ dragging() && draggingClass,
226
+ local.class
227
+ ].filter(Boolean).join(" "));
228
+ setProp(_el0, "data-open", () => read(local.open) ? "" : void 0);
183
229
  return _el0;
184
- })(), _el1);
185
- insert(_el2, () => local.children);
186
- setProp(_el2, "className", () => [contentClass, local.contentClassName].filter(Boolean).join(" "));
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
- }] }));
230
+ }] })),
231
+ falseBranch: () => null
232
+ }));
198
233
  };
199
234
  //#endregion
200
235
  export { BottomSheet as default };
@@ -1,17 +1,14 @@
1
1
  import Portal_default from "./Portal.js";
2
- import Spinner from "./Spinner.js";
2
+ 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
6
  import { Dialog } from "@plastic-js/ark";
7
7
  import { mergeProps as mergeProps$2, splitProps } from "@plastic-js/plastic";
8
8
  //#region src/components/Dialog.jsx
9
- var _tmpl7 = template("<div data-scope=\"dialog\" data-part=\"footer\"></div>");
10
- var _tmpl6 = template("<div data-scope=\"dialog\" data-part=\"body\"></div>");
11
- var _tmpl5 = template("<div data-scope=\"dialog\" data-part=\"header\"><div><!><!></div><!></div>");
12
- var _tmpl4 = template("<span><!><!></span>");
13
- var _tmpl3 = template("<button data-part=\"positive-trigger\" type=\"button\"></button>");
14
- var _tmpl2 = template("<button data-part=\"negative-trigger\" type=\"button\"></button>");
9
+ var _tmpl4 = template("<div data-scope=\"dialog\" data-part=\"footer\"></div>");
10
+ var _tmpl3 = template("<div data-scope=\"dialog\" data-part=\"body\"></div>");
11
+ var _tmpl2 = template("<div data-scope=\"dialog\" data-part=\"header\"><div><!><!></div><!></div>");
15
12
  var _tmpl = template("<div><!><!></div>");
16
13
  var backdropModalClass = css({
17
14
  position: "fixed",
@@ -140,41 +137,22 @@ var footerBtnClass = css({
140
137
  justifyContent: "flex-end",
141
138
  "&.tsu-dialog-footer--center": { justifyContent: "center" }
142
139
  });
143
- var btnClass = css({
140
+ var negativeBtnClass = css({
144
141
  flex: 1,
145
- height: "var(--tsu-comp-height-xl)",
146
142
  borderRadius: "var(--tsu-radius-l1-lg)",
147
143
  border: "1px solid var(--tsu-neutral-7)",
148
144
  background: "transparent",
149
145
  color: "var(--tsu-fg)",
150
146
  fontSize: "var(--tsu-comp-font-size-lg)",
151
147
  fontWeight: "var(--tsu-font-weight-semibold)",
152
- fontFamily: "inherit",
153
- cursor: "pointer"
148
+ fontFamily: "inherit"
154
149
  });
155
- var btnPrimaryClass = css({
156
- background: "var(--tsu-accent-9)",
157
- borderColor: "var(--tsu-accent-9)",
158
- color: "var(--tsu-bg)",
159
- "&:disabled": {
160
- opacity: .5,
161
- cursor: "not-allowed"
162
- }
163
- });
164
- var btnDangerClass = css({
165
- background: "var(--tsu-danger-9)",
166
- borderColor: "var(--tsu-danger-9)",
167
- color: "var(--tsu-bg)",
168
- "&:disabled": {
169
- opacity: .5,
170
- cursor: "not-allowed"
171
- }
172
- });
173
- var positiveLoadingClass = css({
174
- display: "inline-flex",
175
- alignItems: "center",
176
- justifyContent: "center",
177
- gap: "8px"
150
+ var positiveBtnClass = css({
151
+ flex: 1,
152
+ borderRadius: "var(--tsu-radius-l1-lg)",
153
+ fontSize: "var(--tsu-comp-font-size-lg)",
154
+ fontWeight: "var(--tsu-font-weight-semibold)",
155
+ fontFamily: "inherit"
178
156
  });
179
157
  var read = (v) => typeof v === "function" ? v() : v;
180
158
  var warnedSheetNoTitle = false;
@@ -241,34 +219,32 @@ var Dialog$1 = (props) => {
241
219
  const _el0 = _tmpl.cloneNode(true);
242
220
  const _el1 = _el0.firstChild;
243
221
  const _el2 = _el0.firstChild.nextSibling;
244
- insert(_el0, () => local.showCancel && (() => {
245
- const _el0 = _tmpl2.cloneNode(true);
246
- insert(_el0, () => local.cancelText);
247
- setProp(_el0, "className", () => btnClass);
248
- setProp(_el0, "disabled", () => read(local.loading));
249
- setProp(_el0, "onClick", () => handleCancel);
250
- return _el0;
251
- }), _el1);
252
- insert(_el0, () => local.showConfirm && (() => {
253
- const _el0 = _tmpl3.cloneNode(true);
254
- insert(_el0, () => read(local.loading) ? () => {
255
- const _el0 = _tmpl4.cloneNode(true);
256
- const _el1 = _el0.firstChild;
257
- const _el2 = _el0.firstChild.nextSibling;
258
- insert(_el0, () => jsx(Spinner, mergeProps({
259
- size: 16,
260
- strokeWidth: 3,
261
- color: "currentColor"
262
- })), _el1);
263
- insert(_el0, () => local.confirmText, _el2);
264
- setProp(_el0, "className", () => positiveLoadingClass);
265
- return _el0;
266
- } : local.confirmText);
267
- setProp(_el0, "className", () => `${btnClass} ${local.danger ? btnDangerClass : btnPrimaryClass}`);
268
- setProp(_el0, "disabled", () => read(local.loading));
269
- setProp(_el0, "onClick", () => handleConfirm);
270
- return _el0;
271
- }), _el2);
222
+ insert(_el0, () => local.showCancel && jsx(Button, mergeProps({
223
+ className: negativeBtnClass,
224
+ "data-part": "negative-trigger",
225
+ get disabled() {
226
+ return read(local.loading);
227
+ },
228
+ onClick: handleCancel,
229
+ size: "xl",
230
+ type: "button",
231
+ children: () => local.cancelText
232
+ })), _el1);
233
+ insert(_el0, () => local.showConfirm && jsx(Button, mergeProps({
234
+ className: positiveBtnClass,
235
+ "data-part": "positive-trigger",
236
+ get danger() {
237
+ return local.danger;
238
+ },
239
+ get loading() {
240
+ return read(local.loading);
241
+ },
242
+ onClick: handleConfirm,
243
+ size: "xl",
244
+ solid: true,
245
+ type: "button",
246
+ children: () => local.confirmText
247
+ })), _el2);
272
248
  setProp(_el0, "className", () => onlyOneBtn ? `${footerBtnClass} tsu-dialog-footer--center` : footerBtnClass);
273
249
  return _el0;
274
250
  } : void 0;
@@ -305,7 +281,7 @@ var Dialog$1 = (props) => {
305
281
  },
306
282
  children: [
307
283
  () => {
308
- const _el0 = _tmpl5.cloneNode(true);
284
+ const _el0 = _tmpl2.cloneNode(true);
309
285
  const _el1 = _el0.firstChild;
310
286
  const _el2 = _el1.firstChild;
311
287
  const _el3 = _el1.firstChild.nextSibling;
@@ -334,13 +310,13 @@ var Dialog$1 = (props) => {
334
310
  return _el0;
335
311
  },
336
312
  () => {
337
- const _el0 = _tmpl6.cloneNode(true);
313
+ const _el0 = _tmpl3.cloneNode(true);
338
314
  insert(_el0, () => local.children);
339
315
  setProp(_el0, "className", () => bodyClass);
340
316
  return _el0;
341
317
  },
342
318
  () => {
343
- const _el0 = _tmpl7.cloneNode(true);
319
+ const _el0 = _tmpl4.cloneNode(true);
344
320
  insert(_el0, () => footerContent);
345
321
  setProp(_el0, "className", () => footerClass);
346
322
  return _el0;
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.41",
3
+ "version": "0.1.43",
4
4
  "description": "A UI component library for Plastic JS.",
5
5
  "license": "MIT",
6
6
  "author": "tigre",
@@ -50,7 +50,8 @@
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"
54
55
  },
55
56
  "dependencies": {
56
57
  "@emotion/css": "^11.13.5",