@plastic-js/tsumiki 0.1.39 → 0.1.41

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.
@@ -3,10 +3,9 @@ import { insert, jsx, mergeProps, setProp, template } from "@plastic-js/plastic/
3
3
  import { css } from "@emotion/css";
4
4
  import { createEffect, createSignal, mergeProps as mergeProps$1, splitProps } from "@plastic-js/plastic";
5
5
  //#region src/components/BottomSheet.jsx
6
- var _tmpl4 = template("<div><h2></h2></div>");
7
- var _tmpl3 = template("<div><div></div></div>");
8
- var _tmpl2 = template("<div role=\"dialog\" aria-modal=\"true\"><!><!><div></div><!></div>");
9
- var _tmpl = template("<div></div>");
6
+ var _tmpl3 = template("<div data-part=\"grabber\"><div></div></div>");
7
+ var _tmpl2 = template("<div role=\"dialog\" aria-modal=\"true\" aria-label=\"BottomSheet\"><!><div data-part=\"content\"></div></div>");
8
+ var _tmpl = template("<div data-part=\"backdrop\"></div>");
10
9
  var CLOSE_DISTANCE_RATIO = .3;
11
10
  var CLOSE_VELOCITY = .5;
12
11
  var RUBBER_BAND_FACTOR = 3;
@@ -37,6 +36,7 @@ var sheetClass = css({
37
36
  borderRadius: "var(--tsu-radius-l3-md) var(--tsu-radius-l3-md) 0 0",
38
37
  boxShadow: "var(--tsu-shadow-lg)",
39
38
  maxHeight: "70vh",
39
+ "@supports (height: 100dvh)": { maxHeight: "min(70dvh, 70vh)" },
40
40
  display: "flex",
41
41
  flexDirection: "column",
42
42
  transform: "translateY(100%)",
@@ -62,7 +62,7 @@ var grabberAreaClass = css({
62
62
  display: "flex",
63
63
  alignItems: "center",
64
64
  justifyContent: "center",
65
- padding: "6px 0",
65
+ height: "24px",
66
66
  flexShrink: 0,
67
67
  cursor: "grab",
68
68
  touchAction: "none",
@@ -74,35 +74,12 @@ var grabberClass = css({
74
74
  borderRadius: "2.5px",
75
75
  background: "var(--tsu-neutral-alpha-6)"
76
76
  });
77
- var headerClass = css({
77
+ var contentClass = css({
78
78
  display: "flex",
79
- alignItems: "center",
80
- justifyContent: "center",
81
- padding: "var(--tsu-container-padding-lg)",
82
- borderBottom: "1px solid var(--tsu-neutral-7)",
83
- flexShrink: 0
84
- });
85
- var titleClass = css({
86
- margin: 0,
87
- fontSize: "var(--tsu-text-title-size)",
88
- fontWeight: "var(--tsu-text-title-weight)",
89
- lineHeight: "var(--tsu-text-title-line-height)",
90
- color: "var(--tsu-fg)",
91
- textAlign: "center"
92
- });
93
- var bodyClass = css({
94
- overflowY: "auto",
95
- padding: "var(--tsu-container-padding-lg)",
79
+ flexDirection: "column",
80
+ padding: "0 var(--tsu-container-padding-lg)",
96
81
  flex: 1,
97
- WebkitOverflowScrolling: "touch"
98
- });
99
- var bodyNoHeaderClass = css({ paddingTop: "calc(var(--tsu-container-padding-lg) * 2)" });
100
- var footerClass = css({
101
- display: "flex",
102
- gap: "var(--tsu-spacing-sm)",
103
- padding: "0 var(--tsu-container-padding-lg) var(--tsu-container-padding-lg)",
104
- flexShrink: 0,
105
- paddingTop: "var(--tsu-spacing-sm)"
82
+ minHeight: 0
106
83
  });
107
84
  var read = (v) => typeof v === "function" ? v() : v;
108
85
  var BottomSheet = (props) => {
@@ -115,15 +92,12 @@ var BottomSheet = (props) => {
115
92
  "onOpen",
116
93
  "onClose",
117
94
  "onOpenChange",
118
- "title",
119
95
  "dismissible",
120
96
  "draggable",
121
97
  "backdropBlur",
122
98
  "backdropClassName",
123
99
  "backdropStyle",
124
- "bodyClassName",
125
- "footer",
126
- "footerClass",
100
+ "contentClassName",
127
101
  "class",
128
102
  "children"
129
103
  ]);
@@ -197,8 +171,6 @@ var BottomSheet = (props) => {
197
171
  const _el0 = _tmpl2.cloneNode(true);
198
172
  const _el1 = _el0.firstChild;
199
173
  const _el2 = _el0.firstChild.nextSibling;
200
- const _el3 = _el0.firstChild.nextSibling.nextSibling;
201
- const _el4 = _el0.firstChild.nextSibling.nextSibling.nextSibling;
202
174
  insert(_el0, () => showGrabber && (() => {
203
175
  const _el0 = _tmpl3.cloneNode(true);
204
176
  const _el1 = _el0.firstChild;
@@ -210,22 +182,8 @@ var BottomSheet = (props) => {
210
182
  setProp(_el0, "onPointerCancel", () => onGrabberPointerCancel);
211
183
  return _el0;
212
184
  })(), _el1);
213
- insert(_el0, () => local.title && (() => {
214
- const _el0 = _tmpl4.cloneNode(true);
215
- const _el1 = _el0.firstChild;
216
- insert(_el1, () => local.title);
217
- setProp(_el1, "className", () => titleClass);
218
- setProp(_el0, "className", () => headerClass);
219
- return _el0;
220
- }), _el2);
221
- insert(_el3, () => local.children);
222
- setProp(_el3, "className", () => [!local.title && !showGrabber ? `${bodyClass} ${bodyNoHeaderClass}` : bodyClass, local.bodyClassName].filter(Boolean).join(" "));
223
- insert(_el0, () => local.footer && (() => {
224
- const _el0 = _tmpl.cloneNode(true);
225
- insert(_el0, () => local.footer);
226
- setProp(_el0, "className", () => local.footerClass ? `${footerClass} ${local.footerClass}` : footerClass);
227
- return _el0;
228
- }), _el4);
185
+ insert(_el2, () => local.children);
186
+ setProp(_el2, "className", () => [contentClass, local.contentClassName].filter(Boolean).join(" "));
229
187
  setProp(_el0, "ref", (el) => {
230
188
  sheetEl = el;
231
189
  });
@@ -235,7 +193,6 @@ var BottomSheet = (props) => {
235
193
  local.class
236
194
  ].filter(Boolean).join(" "));
237
195
  setProp(_el0, "data-open", () => read(local.open) ? "" : void 0);
238
- setProp(_el0, "aria-label", () => local.title || "BottomSheet");
239
196
  return _el0;
240
197
  }] }));
241
198
  };
@@ -1,13 +1,15 @@
1
1
  import Portal_default from "./Portal.js";
2
+ import Spinner from "./Spinner.js";
2
3
  import Icon from "./Icon.js";
3
4
  import { insert, jsx, mergeProps, setProp, template } from "@plastic-js/plastic/jsx-runtime";
4
5
  import { css, keyframes } from "@emotion/css";
5
6
  import { Dialog } from "@plastic-js/ark";
6
7
  import { mergeProps as mergeProps$2, splitProps } from "@plastic-js/plastic";
7
8
  //#region src/components/Dialog.jsx
8
- var _tmpl6 = template("<div data-scope=\"dialog\" data-part=\"footer\"></div>");
9
- var _tmpl5 = template("<div data-scope=\"dialog\" data-part=\"body\"></div>");
10
- var _tmpl4 = template("<div data-scope=\"dialog\" data-part=\"header\"><div><!><!></div><!></div>");
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>");
11
13
  var _tmpl3 = template("<button data-part=\"positive-trigger\" type=\"button\"></button>");
12
14
  var _tmpl2 = template("<button data-part=\"negative-trigger\" type=\"button\"></button>");
13
15
  var _tmpl = template("<div><!><!></div>");
@@ -60,7 +62,8 @@ var contentModalClass = css({
60
62
  borderRadius: "var(--tsu-radius-l3-md)",
61
63
  width: "100%",
62
64
  maxWidth: "440px",
63
- maxHeight: "calc(100vh - var(--tsu-spacing-lg) * 2)",
65
+ maxHeight: "min(calc(100vh - var(--tsu-spacing-lg) * 2), 560px)",
66
+ "@supports (height: 100dvh)": { maxHeight: "min(calc(100dvh - var(--tsu-spacing-lg) * 2), 560px)" },
64
67
  display: "flex",
65
68
  flexDirection: "column",
66
69
  color: "var(--tsu-fg)",
@@ -82,7 +85,8 @@ var headerClass = css({
82
85
  display: "flex",
83
86
  alignItems: "center",
84
87
  justifyContent: "space-between",
85
- padding: "18px var(--tsu-container-padding-lg)",
88
+ padding: "18px var(--tsu-container-padding-lg) var(--tsu-spacing-lg)",
89
+ marginBottom: "var(--tsu-spacing-sm)",
86
90
  borderBottom: "1px solid var(--tsu-neutral-7)",
87
91
  flexShrink: 0
88
92
  });
@@ -117,16 +121,16 @@ var closeBtnClass = css({
117
121
  flexShrink: 0
118
122
  });
119
123
  var bodyClass = css({
120
- padding: "var(--tsu-container-padding-lg)",
124
+ padding: "0 var(--tsu-container-padding-lg)",
121
125
  flex: 1,
126
+ minHeight: 0,
122
127
  overflowY: "auto",
123
128
  WebkitOverflowScrolling: "touch"
124
129
  });
125
- var bodySheetNoHeaderClass = css({ paddingTop: "calc(var(--tsu-container-padding-lg) * 2)" });
126
130
  var footerClass = css({
127
131
  display: "flex",
128
132
  gap: "var(--tsu-spacing-md)",
129
- padding: "var(--tsu-spacing-lg) var(--tsu-container-padding-lg) var(--tsu-container-padding-lg)",
133
+ padding: "var(--tsu-container-padding-lg)",
130
134
  flexShrink: 0
131
135
  });
132
136
  var footerBtnClass = css({
@@ -166,7 +170,14 @@ var btnDangerClass = css({
166
170
  cursor: "not-allowed"
167
171
  }
168
172
  });
173
+ var positiveLoadingClass = css({
174
+ display: "inline-flex",
175
+ alignItems: "center",
176
+ justifyContent: "center",
177
+ gap: "8px"
178
+ });
169
179
  var read = (v) => typeof v === "function" ? v() : v;
180
+ var warnedSheetNoTitle = false;
170
181
  var Dialog$1 = (props) => {
171
182
  const [local, rest] = splitProps(mergeProps$2({
172
183
  mode: "sheet",
@@ -218,7 +229,6 @@ var Dialog$1 = (props) => {
218
229
  local.onOpenChange?.(open);
219
230
  };
220
231
  const handleCancel = () => {
221
- if (read(local.loading)) return;
222
232
  cancelRequested.current = true;
223
233
  handleOpenChange(false);
224
234
  };
@@ -241,7 +251,19 @@ var Dialog$1 = (props) => {
241
251
  }), _el1);
242
252
  insert(_el0, () => local.showConfirm && (() => {
243
253
  const _el0 = _tmpl3.cloneNode(true);
244
- insert(_el0, () => read(local.loading) ? "Loading…" : local.confirmText);
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);
245
267
  setProp(_el0, "className", () => `${btnClass} ${local.danger ? btnDangerClass : btnPrimaryClass}`);
246
268
  setProp(_el0, "disabled", () => read(local.loading));
247
269
  setProp(_el0, "onClick", () => handleConfirm);
@@ -250,8 +272,10 @@ var Dialog$1 = (props) => {
250
272
  setProp(_el0, "className", () => onlyOneBtn ? `${footerBtnClass} tsu-dialog-footer--center` : footerBtnClass);
251
273
  return _el0;
252
274
  } : void 0;
253
- const hasFooter = !!footerContent;
254
- const hasHeader = local.title || !isSheet && (local.closeable ?? true);
275
+ if (isSheet && !local.title && !warnedSheetNoTitle) {
276
+ warnedSheetNoTitle = true;
277
+ console.warn("[Dialog] sheet mode requires a title; use BottomSheet for header-less content.");
278
+ }
255
279
  return jsx(Dialog.Root, mergeProps({
256
280
  get open() {
257
281
  return read(local.open);
@@ -280,8 +304,8 @@ var Dialog$1 = (props) => {
280
304
  return local.class ? `${isSheet ? contentSheetClass : contentModalClass} ${local.class}` : isSheet ? contentSheetClass : contentModalClass;
281
305
  },
282
306
  children: [
283
- () => hasHeader && (() => {
284
- const _el0 = _tmpl4.cloneNode(true);
307
+ () => {
308
+ const _el0 = _tmpl5.cloneNode(true);
285
309
  const _el1 = _el0.firstChild;
286
310
  const _el2 = _el1.firstChild;
287
311
  const _el3 = _el1.firstChild.nextSibling;
@@ -308,19 +332,19 @@ var Dialog$1 = (props) => {
308
332
  })), _el4);
309
333
  setProp(_el0, "className", () => headerClass);
310
334
  return _el0;
311
- }),
335
+ },
312
336
  () => {
313
- const _el0 = _tmpl5.cloneNode(true);
337
+ const _el0 = _tmpl6.cloneNode(true);
314
338
  insert(_el0, () => local.children);
315
- setProp(_el0, "className", () => isSheet && !local.title && !(local.closeable ?? true) ? `${bodyClass} ${bodySheetNoHeaderClass}` : bodyClass);
339
+ setProp(_el0, "className", () => bodyClass);
316
340
  return _el0;
317
341
  },
318
- () => hasFooter && (() => {
319
- const _el0 = _tmpl6.cloneNode(true);
342
+ () => {
343
+ const _el0 = _tmpl7.cloneNode(true);
320
344
  insert(_el0, () => footerContent);
321
345
  setProp(_el0, "className", () => footerClass);
322
346
  return _el0;
323
- })
347
+ }
324
348
  ]
325
349
  }))
326
350
  }))] })) }));
@@ -59,7 +59,13 @@ var triggerIndicatorClass = css({
59
59
  }
60
60
  });
61
61
  var listBodyClass = css({ padding: 0 });
62
- var listClass = css({ padding: "0 8px calc(8px + env(safe-area-inset-bottom, 0px))" });
62
+ var listClass = css({
63
+ flex: 1,
64
+ minHeight: 0,
65
+ overflowY: "auto",
66
+ WebkitOverflowScrolling: "touch",
67
+ padding: "0 8px calc(8px + env(safe-area-inset-bottom, 0px))"
68
+ });
63
69
  var itemClass = css({
64
70
  width: "100%",
65
71
  display: "flex",
@@ -308,7 +314,7 @@ var Content = () => {
308
314
  get backdropStyle() {
309
315
  return ctx.backdropStyle ?? { background: "rgba(0,0,0,0.5)" };
310
316
  },
311
- bodyClassName: listBodyClass,
317
+ contentClassName: listBodyClass,
312
318
  children: jsx("div", mergeProps(() => part("list"), {
313
319
  className: listClass,
314
320
  children: [() => ctx.filter && (() => {
package/docs/api.md CHANGED
@@ -641,6 +641,8 @@ Passthrough: Ark `Root` props (`value`, `min`, `max`, …).
641
641
 
642
642
  **Rendered DOM parts:** content is `[data-scope="dialog"][data-part="content"]`; tsumiki adds `data-part="header"`, `data-part="body"`, `data-part="footer"`, and the action buttons `data-part="negative-trigger"` / `data-part="positive-trigger"`. `title`, `description`, and `closeTrigger` carry `data-part` from Ark.
643
643
 
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
+
644
646
  | Prop | Type | Default | Description |
645
647
  |---|---|---|---|
646
648
  | `mode` | `string` | `'sheet'` | `'modal'` (centered) or `'sheet'` (bottom sheet). |
@@ -666,6 +668,16 @@ Passthrough: Ark `Root` props (`value`, `min`, `max`, …).
666
668
  | `class` | `string` | — | Content class. |
667
669
  | `children` | `node` | — | Custom body content. |
668
670
 
671
+ **Callback hierarchy:**
672
+
673
+ `onOpenChange` → `onClose` → `onCancel` are **increasingly high-level**. Each is a refinement of the previous:
674
+
675
+ - `onOpenChange` — raw state sync, fired on every open/close.
676
+ - `onClose` — close cleanup, fired whenever the dialog closes for any reason.
677
+ - `onCancel` — cancel intent, fired only on an explicit cancel (Cancel button, ✕, Escape, outside pointer).
678
+
679
+ > **General rule: always use the highest-level callback you need.** Reach for `onCancel` (and `onConfirm`) first — they express intent. Only fall back to `onClose` when you need close cleanup for every path, and to `onOpenChange` only when you need raw open/close control. Passing a lower-level callback just because a higher-level one exists usually means you are bypassing the intent layer.
680
+
669
681
  **Known issues:**
670
682
  - Pressing <kbd>Escape</kbd> closes both the Dialog and any nested overlay (e.g. `DateInput`, `Select`) that is open inside it. There is currently no event coordination between the two — both react to the same key press.
671
683
 
@@ -747,16 +759,26 @@ Passthrough: Ark `Root` props (`open`, `onOpenChange`, `positioning`, …).
747
759
 
748
760
  ### BottomSheet
749
761
 
750
- Mobile bottom sheet (drag-to-dismiss).
762
+ A free-form presentation layer: a backdrop, an optional drag grabber, and a scrollable content area (`children`). The content is fully consumer-owned — headers, titles, and footers are just regular content.
751
763
 
752
764
  **Exports:** `BottomSheet` · no `.origin`
753
765
 
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.
767
+
768
+ > **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
+
754
770
  | Prop | Type | Default | Description |
755
771
  |---|---|---|---|
756
- | `dismissible` | `boolean` | `true` | Allow dismiss. |
772
+ | `open` | `boolean \| (() => boolean)` | | **Required** controlled open state. |
773
+ | `onOpenChange` | `({ open }) => void` | — | Fired on open/close; sync your state here. |
774
+ | `dismissible` | `boolean` | `true` | Allow dismiss via backdrop / drag. |
775
+ | `draggable` | `boolean` | `true` | Show the grabber and enable drag-to-dismiss. |
757
776
  | `backdropBlur` | `boolean` | `true` | Blur the backdrop. |
758
-
759
- Passthrough: DOM props (`open`, `onOpenChange`, `title`, `children`, …).
777
+ | `backdropClassName` | `string` | — | Extra class for the backdrop. |
778
+ | `backdropStyle` | `object` | | Extra style for the backdrop. |
779
+ | `contentClassName` | `string` | — | Extra class for the content area. |
780
+ | `class` | `string` | — | Extra class for the sheet. |
781
+ | `children` | `node` | — | **Required** body content. |
760
782
 
761
783
  ---
762
784
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plastic-js/tsumiki",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "A UI component library for Plastic JS.",
5
5
  "license": "MIT",
6
6
  "author": "tigre",