@sarunyu/system-one 4.1.1 → 4.2.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/AGENTS.md CHANGED
@@ -17,6 +17,8 @@ in this package.** This file is the short version: the rules you must follow.
17
17
  - Custom checkbox/radio → use `<Checkbox>` / `<Radio>`.
18
18
  - Custom date/time pickers → use `<DateInput>` / `<TimeInput>`.
19
19
  - Custom tables → use `<Table>` + `<TableRow>` + `<TableHeaderCell>` + `<TableCell>`.
20
+ - Custom modals/dialogs/alerts → use `<Modal>` (wrap it in your own `fixed inset-0` backdrop).
21
+ - Custom bottom sheets / drawers from the bottom → use `<BottomSheet>` (it ships its own backdrop via Vaul).
20
22
 
21
23
  2. **Use token-backed Tailwind classes for color.** Never emit hard-coded colors:
22
24
  - Hex (`#3b82f6`), arbitrary (`bg-[#...]`), and palette utilities
@@ -38,6 +40,19 @@ in this package.** This file is the short version: the rules you must follow.
38
40
  - Checkbox/Radio take their `label` as a prop. Don't wrap them in `<label>`.
39
41
  - All tabs in one `TabGroup` must share the same `size`.
40
42
  - One `<Button variant="primary">` per context.
43
+ - `Modal` renders the panel only — provide your own `fixed inset-0` backdrop + open/close state. One primary action per modal.
44
+ - `BottomSheet` is mobile-only. On desktop, use `Modal` instead.
45
+
46
+ 6. **Mobile forms and action-heavy modals MUST use `<BottomSheet>`, not `<Modal>`.**
47
+ Login, signup, settings panels, profile editors, any multi-field form,
48
+ multi-step flow, long picker list, or action menu — on mobile (< 768px)
49
+ these render as `<BottomSheet>`. Only simple `variant="alert"` and short
50
+ `variant="dialog"` confirmations (no form) may stay as `<Modal>` on mobile.
51
+ Desktop (≥ 768px) always uses `<Modal>`. Branch with the library's
52
+ `useIsMobile()` hook — do not build a custom "ResponsiveModal" wrapper;
53
+ put the `if (isMobile) return <BottomSheet>… / return <Modal>…` inline and
54
+ share one `const body = …` between the two branches. See the `LoginSheet`
55
+ recipe in `llms.txt`.
41
56
 
42
57
  ## Setup check
43
58
 
package/dist/index.cjs CHANGED
@@ -1192,6 +1192,11 @@ function DrawerPortal({
1192
1192
  }) {
1193
1193
  return /* @__PURE__ */ jsxRuntime.jsx(vaul.Drawer.Portal, { "data-slot": "drawer-portal", ...props });
1194
1194
  }
1195
+ function DrawerClose({
1196
+ ...props
1197
+ }) {
1198
+ return /* @__PURE__ */ jsxRuntime.jsx(vaul.Drawer.Close, { "data-slot": "drawer-close", ...props });
1199
+ }
1195
1200
  function DrawerOverlay({
1196
1201
  className,
1197
1202
  ...props
@@ -1249,6 +1254,126 @@ function DrawerTitle({
1249
1254
  }
1250
1255
  );
1251
1256
  }
1257
+ function BottomSheet({
1258
+ open,
1259
+ onOpenChange,
1260
+ trigger,
1261
+ headerType = "text",
1262
+ showHeader = true,
1263
+ rightSide = "icon",
1264
+ title = "Title",
1265
+ actionLabel = "Action",
1266
+ imageSrc,
1267
+ leftIcon,
1268
+ rightIcon,
1269
+ onActionClick,
1270
+ showHandle = true,
1271
+ children,
1272
+ className,
1273
+ contentClassName
1274
+ }) {
1275
+ return /* @__PURE__ */ jsxRuntime.jsxs(Drawer, { direction: "bottom", open, onOpenChange, children: [
1276
+ trigger ? /* @__PURE__ */ jsxRuntime.jsx(DrawerTrigger, { asChild: true, children: trigger }) : null,
1277
+ /* @__PURE__ */ jsxRuntime.jsxs(
1278
+ DrawerContent,
1279
+ {
1280
+ className: cn(
1281
+ "[&>div:first-child]:hidden rounded-t-[24px] border-t-0 px-4 pb-6 pt-2",
1282
+ className
1283
+ ),
1284
+ children: [
1285
+ /* @__PURE__ */ jsxRuntime.jsx(DrawerTitle, { className: "sr-only", children: title }),
1286
+ showHandle ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2 flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-1 w-10 rounded-full bg-muted" }) }) : null,
1287
+ showHeader ? /* @__PURE__ */ jsxRuntime.jsx(
1288
+ Header,
1289
+ {
1290
+ headerType,
1291
+ rightSide,
1292
+ title,
1293
+ actionLabel,
1294
+ imageSrc,
1295
+ leftIcon,
1296
+ rightIcon,
1297
+ onActionClick
1298
+ }
1299
+ ) : null,
1300
+ children ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("pt-2", contentClassName), children }) : null
1301
+ ]
1302
+ }
1303
+ )
1304
+ ] });
1305
+ }
1306
+ function Header({
1307
+ headerType,
1308
+ rightSide,
1309
+ title,
1310
+ actionLabel,
1311
+ imageSrc,
1312
+ leftIcon,
1313
+ rightIcon,
1314
+ onActionClick
1315
+ }) {
1316
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center gap-3", rightSide === "action" ? "pr-2" : void 0), children: [
1317
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
1318
+ /* @__PURE__ */ jsxRuntime.jsx(Leading, { headerType, imageSrc, leftIcon }),
1319
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-base leading-6 font-bold text-foreground", children: title })
1320
+ ] }),
1321
+ /* @__PURE__ */ jsxRuntime.jsx(
1322
+ Trailing,
1323
+ {
1324
+ rightSide,
1325
+ actionLabel,
1326
+ rightIcon,
1327
+ onActionClick
1328
+ }
1329
+ )
1330
+ ] });
1331
+ }
1332
+ function Leading({
1333
+ headerType,
1334
+ imageSrc,
1335
+ leftIcon
1336
+ }) {
1337
+ if (headerType === "image") {
1338
+ return imageSrc ? /* @__PURE__ */ jsxRuntime.jsx("img", { alt: "", className: "size-8 shrink-0 rounded-md object-cover", src: imageSrc }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "size-8 shrink-0 rounded-md bg-muted" });
1339
+ }
1340
+ if (headerType === "icon") {
1341
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "text-foreground", children: leftIcon ?? /* @__PURE__ */ jsxRuntime.jsx(react.Circle, { size: 22 }) });
1342
+ }
1343
+ return null;
1344
+ }
1345
+ function Trailing({
1346
+ rightSide,
1347
+ actionLabel,
1348
+ rightIcon,
1349
+ onActionClick
1350
+ }) {
1351
+ if (rightSide === "action") {
1352
+ return /* @__PURE__ */ jsxRuntime.jsx(
1353
+ Button,
1354
+ {
1355
+ className: "px-0 py-0 text-base leading-6 font-bold",
1356
+ onClick: onActionClick,
1357
+ size: "md",
1358
+ variant: "plain",
1359
+ children: actionLabel
1360
+ }
1361
+ );
1362
+ }
1363
+ if (rightSide === "icon") {
1364
+ return /* @__PURE__ */ jsxRuntime.jsx(DrawerClose, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
1365
+ Button,
1366
+ {
1367
+ "aria-label": "Close bottom sheet",
1368
+ className: "text-foreground",
1369
+ size: "icon-xs",
1370
+ variant: "plain-black",
1371
+ children: rightIcon ?? /* @__PURE__ */ jsxRuntime.jsx(react.Circle, { size: 22 })
1372
+ }
1373
+ ) });
1374
+ }
1375
+ return null;
1376
+ }
1252
1377
  const THAI_MONTHS_SHORT = [
1253
1378
  "ม.ค.",
1254
1379
  "ก.พ.",
@@ -2179,16 +2304,22 @@ const DateInput = React.forwardRef(
2179
2304
  ref,
2180
2305
  className: cn("flex flex-col gap-[4px] w-full", className),
2181
2306
  children: [
2182
- isMobile ? /* @__PURE__ */ jsxRuntime.jsxs(Drawer, { open, onOpenChange: handleOpenChange, children: [
2183
- /* @__PURE__ */ jsxRuntime.jsx(DrawerTrigger, { asChild: true, children: triggerButton }),
2184
- /* @__PURE__ */ jsxRuntime.jsxs(DrawerContent, { children: [
2185
- /* @__PURE__ */ jsxRuntime.jsx(DrawerTitle, { className: "sr-only", children: "เลือกวันที่" }),
2186
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "overflow-auto px-4 pt-2 pb-8 w-full", children: [
2307
+ isMobile ? /* @__PURE__ */ jsxRuntime.jsx(
2308
+ BottomSheet,
2309
+ {
2310
+ open,
2311
+ onOpenChange: handleOpenChange,
2312
+ trigger: triggerButton,
2313
+ title: "เลือกวันที่",
2314
+ showHeader: false,
2315
+ rightSide: "none",
2316
+ contentClassName: "pt-0",
2317
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "overflow-auto px-4 pt-2 pb-8 w-full", children: [
2187
2318
  /* @__PURE__ */ jsxRuntime.jsx(DrawerRangeCtx.Provider, { value: mode === "range", children: calendarContent }),
2188
2319
  actionButtons
2189
2320
  ] })
2190
- ] })
2191
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
2321
+ }
2322
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
2192
2323
  Popover__namespace.Root,
2193
2324
  {
2194
2325
  open,
@@ -3294,6 +3425,139 @@ const Input = React.forwardRef(function Input2({
3294
3425
  ] });
3295
3426
  });
3296
3427
  Input.displayName = "Input";
3428
+ const ALERT_CONFIG = {
3429
+ warning: {
3430
+ titleColor: "var(--accent-orange)",
3431
+ background: "https://www.figma.com/api/mcp/asset/f4ca68ad-5732-4124-9ff4-cfb69330cc02",
3432
+ layers: [
3433
+ { inset: "12.5%", src: "https://www.figma.com/api/mcp/asset/7052a092-a432-4e8c-b559-6b51d28d878f" },
3434
+ { inset: "22.5%", src: "https://www.figma.com/api/mcp/asset/a291a1b2-06c8-455c-8e21-29755aa05c57" },
3435
+ { inset: "28.57% 30.71% 32.86% 30.71%", src: "https://www.figma.com/api/mcp/asset/a22c7520-55fe-4003-ba78-65dab40b9e23" }
3436
+ ]
3437
+ },
3438
+ success: {
3439
+ titleColor: "var(--success)",
3440
+ background: "https://www.figma.com/api/mcp/asset/2a865e6f-8a92-4496-88b5-71ac99e2c385",
3441
+ layers: [
3442
+ { inset: "12.77%", src: "https://www.figma.com/api/mcp/asset/5878ce35-4f9a-4203-97a8-70a2f17b182c" },
3443
+ { inset: "22.55%", src: "https://www.figma.com/api/mcp/asset/cea74180-b261-4db7-8712-6d32c4ccdeaf" }
3444
+ ]
3445
+ },
3446
+ danger: {
3447
+ titleColor: "var(--destructive)",
3448
+ background: "https://www.figma.com/api/mcp/asset/c7a65595-684e-4a04-b7fd-d443951f680a",
3449
+ layers: [
3450
+ { inset: "12.77%", src: "https://www.figma.com/api/mcp/asset/10090345-ae32-4fc4-aff6-cba04ea93700" },
3451
+ { inset: "22.55%", src: "https://www.figma.com/api/mcp/asset/3aa1156e-e48b-411f-ab98-93e1da98ecc1" }
3452
+ ]
3453
+ }
3454
+ };
3455
+ function Modal({
3456
+ variant = "dialog",
3457
+ actionLayout = "none",
3458
+ responsive = "mobile",
3459
+ alertStatus = "warning",
3460
+ showClose = true,
3461
+ title = "Text label",
3462
+ description = "Lorem ipsum dolor sit amet consectetur. Mi id nunc ac tempus turpis. Ipsum consectetur dictum volutpat viverra arcu rhoncus sit arcu.",
3463
+ primaryLabel = "Accept",
3464
+ secondaryLabel = "Cancel",
3465
+ children,
3466
+ className,
3467
+ onClose,
3468
+ onPrimaryClick,
3469
+ onSecondaryClick
3470
+ }) {
3471
+ const isContent = variant === "content";
3472
+ const isAlert = variant === "alert";
3473
+ const isDesktop = responsive === "desktop";
3474
+ const alert = ALERT_CONFIG[alertStatus];
3475
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3476
+ "div",
3477
+ {
3478
+ className: cn(
3479
+ "max-w-full overflow-hidden border border-border bg-background",
3480
+ isAlert ? "w-[343px] rounded-2xl" : "rounded-xl",
3481
+ isContent ? "w-[343px]" : isAlert ? void 0 : "w-[375px]",
3482
+ className
3483
+ ),
3484
+ children: [
3485
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center px-4 pt-4", isAlert ? "justify-end" : "justify-between gap-4"), children: [
3486
+ !isAlert ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[18px] leading-7 font-bold text-foreground", children: title }) : null,
3487
+ showClose ? /* @__PURE__ */ jsxRuntime.jsx(
3488
+ Button,
3489
+ {
3490
+ "aria-label": "Close dialog",
3491
+ className: "h-5 w-5 shrink-0 rounded-none text-subtle-text",
3492
+ onClick: onClose,
3493
+ size: "icon-xs",
3494
+ variant: "plain-black",
3495
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.X, { size: 20, weight: "regular" })
3496
+ }
3497
+ ) : null
3498
+ ] }),
3499
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("px-4 pb-6", isAlert ? "pt-0" : "pt-4"), children: isAlert ? /* @__PURE__ */ jsxRuntime.jsx(AlertBody, { config: alert, title, description }) : isContent ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full", children: children ?? null }) : children ?? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm leading-5 text-muted-foreground", children: description }) }),
3500
+ actionLayout !== "none" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 pb-4", children: /* @__PURE__ */ jsxRuntime.jsx(
3501
+ ModalActions,
3502
+ {
3503
+ layout: actionLayout,
3504
+ isContent,
3505
+ isDesktop,
3506
+ primaryLabel,
3507
+ secondaryLabel,
3508
+ onPrimaryClick,
3509
+ onSecondaryClick
3510
+ }
3511
+ ) }) : null
3512
+ ]
3513
+ }
3514
+ );
3515
+ }
3516
+ function AlertBody({
3517
+ config,
3518
+ title,
3519
+ description
3520
+ }) {
3521
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-4 text-center", children: [
3522
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative size-[100px]", children: [
3523
+ /* @__PURE__ */ jsxRuntime.jsx("img", { alt: "", className: "absolute inset-0 size-full", src: config.background }),
3524
+ config.layers.map((layer) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute", style: { inset: layer.inset }, children: /* @__PURE__ */ jsxRuntime.jsx("img", { alt: "", className: "absolute inset-0 size-full", src: layer.src }) }, layer.src))
3525
+ ] }),
3526
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-2", children: [
3527
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[18px] leading-7 font-bold", style: { color: config.titleColor }, children: title }),
3528
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm leading-5 text-muted-foreground", children: description })
3529
+ ] })
3530
+ ] });
3531
+ }
3532
+ function ModalActions({
3533
+ layout,
3534
+ isContent,
3535
+ isDesktop,
3536
+ primaryLabel,
3537
+ secondaryLabel,
3538
+ onPrimaryClick,
3539
+ onSecondaryClick
3540
+ }) {
3541
+ const desktopInline = isContent && isDesktop;
3542
+ if (layout === "single") {
3543
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(desktopInline ? "flex justify-end" : void 0), children: /* @__PURE__ */ jsxRuntime.jsx(
3544
+ Button,
3545
+ {
3546
+ className: cn(desktopInline ? void 0 : "w-full"),
3547
+ onClick: onPrimaryClick,
3548
+ size: "xl",
3549
+ variant: "primary",
3550
+ children: primaryLabel
3551
+ }
3552
+ ) });
3553
+ }
3554
+ const containerClass = isContent ? cn("flex gap-4", isDesktop ? "justify-end" : "flex-col") : "flex items-center gap-4";
3555
+ const buttonClass = isContent ? isDesktop ? void 0 : "w-full" : "min-w-0 flex-1";
3556
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: containerClass, children: [
3557
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { className: buttonClass, onClick: onSecondaryClick, size: "xl", variant: "outline", children: secondaryLabel }),
3558
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { className: buttonClass, onClick: onPrimaryClick, size: "xl", variant: "primary", children: primaryLabel })
3559
+ ] });
3560
+ }
3297
3561
  const OptionList = React.forwardRef(
3298
3562
  function OptionList2({
3299
3563
  options,
@@ -4876,16 +5140,22 @@ const TimeInput = React.forwardRef(
4876
5140
  className
4877
5141
  ),
4878
5142
  children: [
4879
- isMobile ? /* @__PURE__ */ jsxRuntime.jsxs(Drawer, { open, onOpenChange: handleOpenChange, children: [
4880
- /* @__PURE__ */ jsxRuntime.jsx(DrawerTrigger, { asChild: true, children: triggerButton }),
4881
- /* @__PURE__ */ jsxRuntime.jsxs(DrawerContent, { children: [
4882
- /* @__PURE__ */ jsxRuntime.jsx(DrawerTitle, { className: "sr-only", children: "เลือกเวลา" }),
4883
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "overflow-auto px-4 pt-2 pb-8 w-full", children: [
5143
+ isMobile ? /* @__PURE__ */ jsxRuntime.jsx(
5144
+ BottomSheet,
5145
+ {
5146
+ open,
5147
+ onOpenChange: handleOpenChange,
5148
+ trigger: triggerButton,
5149
+ title: "เลือกเวลา",
5150
+ showHeader: false,
5151
+ rightSide: "none",
5152
+ contentClassName: "pt-0",
5153
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "overflow-auto px-4 pt-2 pb-8 w-full", children: [
4884
5154
  pickerContent,
4885
5155
  actionButtons
4886
5156
  ] })
4887
- ] })
4888
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
5157
+ }
5158
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
4889
5159
  Popover__namespace.Root,
4890
5160
  {
4891
5161
  open,
@@ -4920,6 +5190,7 @@ const TimeInput = React.forwardRef(
4920
5190
  }
4921
5191
  );
4922
5192
  TimeInput.displayName = "TimeInput";
5193
+ exports.BottomSheet = BottomSheet;
4923
5194
  exports.Button = Button;
4924
5195
  exports.Card = Card;
4925
5196
  exports.Checkbox = Checkbox;
@@ -4928,6 +5199,7 @@ exports.DateInput = DateInput;
4928
5199
  exports.Dropdown = Dropdown;
4929
5200
  exports.DropdownMultiple = DropdownMultiple;
4930
5201
  exports.Input = Input;
5202
+ exports.Modal = Modal;
4931
5203
  exports.OptionList = OptionList;
4932
5204
  exports.Radio = Radio;
4933
5205
  exports.SearchInput = SearchInput;
@@ -4942,4 +5214,5 @@ exports.Tag = Tag;
4942
5214
  exports.TextArea = TextArea;
4943
5215
  exports.TimeInput = TimeInput;
4944
5216
  exports.cn = cn;
5217
+ exports.useIsMobile = useIsMobile;
4945
5218
  //# sourceMappingURL=index.cjs.map