@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.
- package/dist/components/BottomSheet.js +12 -55
- package/dist/components/Dialog.js +44 -20
- package/dist/components/Select.js +8 -2
- package/docs/api.md +26 -4
- package/package.json +1 -1
|
@@ -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
|
|
7
|
-
var
|
|
8
|
-
var
|
|
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
|
-
|
|
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
|
|
77
|
+
var contentClass = css({
|
|
78
78
|
display: "flex",
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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(
|
|
214
|
-
|
|
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
|
|
9
|
-
var
|
|
10
|
-
var
|
|
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-
|
|
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) ?
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
() =>
|
|
284
|
-
const _el0 =
|
|
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 =
|
|
337
|
+
const _el0 = _tmpl6.cloneNode(true);
|
|
314
338
|
insert(_el0, () => local.children);
|
|
315
|
-
setProp(_el0, "className", () =>
|
|
339
|
+
setProp(_el0, "className", () => bodyClass);
|
|
316
340
|
return _el0;
|
|
317
341
|
},
|
|
318
|
-
() =>
|
|
319
|
-
const _el0 =
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
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
|
-
|
|
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
|
|