@sarunyu/system-one 4.1.1 → 4.2.1

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,216 @@ 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
+ {
3434
+ inset: "12.5%",
3435
+ src: "https://www.figma.com/api/mcp/asset/7052a092-a432-4e8c-b559-6b51d28d878f"
3436
+ },
3437
+ {
3438
+ inset: "22.5%",
3439
+ src: "https://www.figma.com/api/mcp/asset/a291a1b2-06c8-455c-8e21-29755aa05c57"
3440
+ },
3441
+ {
3442
+ inset: "28.57% 30.71% 32.86% 30.71%",
3443
+ src: "https://www.figma.com/api/mcp/asset/a22c7520-55fe-4003-ba78-65dab40b9e23"
3444
+ }
3445
+ ]
3446
+ },
3447
+ success: {
3448
+ titleColor: "var(--success)",
3449
+ background: "https://www.figma.com/api/mcp/asset/2a865e6f-8a92-4496-88b5-71ac99e2c385",
3450
+ layers: [
3451
+ {
3452
+ inset: "12.77%",
3453
+ src: "https://www.figma.com/api/mcp/asset/5878ce35-4f9a-4203-97a8-70a2f17b182c"
3454
+ },
3455
+ {
3456
+ inset: "22.55%",
3457
+ src: "https://www.figma.com/api/mcp/asset/cea74180-b261-4db7-8712-6d32c4ccdeaf"
3458
+ }
3459
+ ]
3460
+ },
3461
+ danger: {
3462
+ titleColor: "var(--destructive)",
3463
+ background: "https://www.figma.com/api/mcp/asset/c7a65595-684e-4a04-b7fd-d443951f680a",
3464
+ layers: [
3465
+ {
3466
+ inset: "12.77%",
3467
+ src: "https://www.figma.com/api/mcp/asset/10090345-ae32-4fc4-aff6-cba04ea93700"
3468
+ },
3469
+ {
3470
+ inset: "22.55%",
3471
+ src: "https://www.figma.com/api/mcp/asset/3aa1156e-e48b-411f-ab98-93e1da98ecc1"
3472
+ }
3473
+ ]
3474
+ }
3475
+ };
3476
+ function Modal({
3477
+ variant = "dialog",
3478
+ actionLayout = "none",
3479
+ responsive = "mobile",
3480
+ alertStatus = "warning",
3481
+ showClose = true,
3482
+ title = "Text label",
3483
+ description = "Lorem ipsum dolor sit amet consectetur. Mi id nunc ac tempus turpis. Ipsum consectetur dictum volutpat viverra arcu rhoncus sit arcu.",
3484
+ primaryLabel = "Accept",
3485
+ secondaryLabel = "Cancel",
3486
+ children,
3487
+ className,
3488
+ onClose,
3489
+ onPrimaryClick,
3490
+ onSecondaryClick
3491
+ }) {
3492
+ const isContent = variant === "content";
3493
+ const isAlert = variant === "alert";
3494
+ const isDesktop = responsive === "desktop";
3495
+ const alert = ALERT_CONFIG[alertStatus];
3496
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3497
+ "div",
3498
+ {
3499
+ className: cn(
3500
+ "w-auto overflow-hidden border border-border bg-background",
3501
+ isAlert ? "w-[343px] rounded-2xl" : "rounded-xl",
3502
+ isContent ? "min-w-[343px] max-w-[500px] " : isAlert ? void 0 : "min-w-[375px] max-w-[500px]",
3503
+ className
3504
+ ),
3505
+ children: [
3506
+ /* @__PURE__ */ jsxRuntime.jsxs(
3507
+ "div",
3508
+ {
3509
+ className: cn(
3510
+ "flex items-center px-4 pt-4",
3511
+ isAlert ? "justify-end" : "justify-between gap-4"
3512
+ ),
3513
+ children: [
3514
+ !isAlert ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[18px] leading-7 font-bold text-foreground", children: title }) : null,
3515
+ showClose ? /* @__PURE__ */ jsxRuntime.jsx(
3516
+ Button,
3517
+ {
3518
+ "aria-label": "Close dialog",
3519
+ className: "h-5 w-5 shrink-0 rounded-none text-subtle-text",
3520
+ onClick: onClose,
3521
+ size: "icon-xs",
3522
+ variant: "plain-black",
3523
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.X, { size: 20, weight: "regular" })
3524
+ }
3525
+ ) : null
3526
+ ]
3527
+ }
3528
+ ),
3529
+ /* @__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 }) }),
3530
+ actionLayout !== "none" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 pb-4", children: /* @__PURE__ */ jsxRuntime.jsx(
3531
+ ModalActions,
3532
+ {
3533
+ layout: actionLayout,
3534
+ isContent,
3535
+ isDesktop,
3536
+ primaryLabel,
3537
+ secondaryLabel,
3538
+ onPrimaryClick,
3539
+ onSecondaryClick
3540
+ }
3541
+ ) }) : null
3542
+ ]
3543
+ }
3544
+ );
3545
+ }
3546
+ function AlertBody({
3547
+ config,
3548
+ title,
3549
+ description
3550
+ }) {
3551
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-4 text-center", children: [
3552
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative size-[100px]", children: [
3553
+ /* @__PURE__ */ jsxRuntime.jsx(
3554
+ "img",
3555
+ {
3556
+ alt: "",
3557
+ className: "absolute inset-0 size-full",
3558
+ src: config.background
3559
+ }
3560
+ ),
3561
+ config.layers.map((layer) => /* @__PURE__ */ jsxRuntime.jsx(
3562
+ "div",
3563
+ {
3564
+ className: "absolute",
3565
+ style: { inset: layer.inset },
3566
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3567
+ "img",
3568
+ {
3569
+ alt: "",
3570
+ className: "absolute inset-0 size-full",
3571
+ src: layer.src
3572
+ }
3573
+ )
3574
+ },
3575
+ layer.src
3576
+ ))
3577
+ ] }),
3578
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-2", children: [
3579
+ /* @__PURE__ */ jsxRuntime.jsx(
3580
+ "p",
3581
+ {
3582
+ className: "text-[18px] leading-7 font-bold",
3583
+ style: { color: config.titleColor },
3584
+ children: title
3585
+ }
3586
+ ),
3587
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm leading-5 text-muted-foreground", children: description })
3588
+ ] })
3589
+ ] });
3590
+ }
3591
+ function ModalActions({
3592
+ layout,
3593
+ isContent,
3594
+ isDesktop,
3595
+ primaryLabel,
3596
+ secondaryLabel,
3597
+ onPrimaryClick,
3598
+ onSecondaryClick
3599
+ }) {
3600
+ const desktopInline = isContent && isDesktop;
3601
+ if (layout === "single") {
3602
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(desktopInline ? "flex justify-end" : void 0), children: /* @__PURE__ */ jsxRuntime.jsx(
3603
+ Button,
3604
+ {
3605
+ className: cn(desktopInline ? void 0 : "w-full"),
3606
+ onClick: onPrimaryClick,
3607
+ size: "xl",
3608
+ variant: "primary",
3609
+ children: primaryLabel
3610
+ }
3611
+ ) });
3612
+ }
3613
+ const containerClass = isContent ? cn("flex gap-4", isDesktop ? "justify-end" : "flex-col") : "flex items-center gap-4";
3614
+ const buttonClass = isContent ? isDesktop ? void 0 : "w-full" : "min-w-0 flex-1";
3615
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: containerClass, children: [
3616
+ /* @__PURE__ */ jsxRuntime.jsx(
3617
+ Button,
3618
+ {
3619
+ className: buttonClass,
3620
+ onClick: onSecondaryClick,
3621
+ size: "xl",
3622
+ variant: "outline",
3623
+ children: secondaryLabel
3624
+ }
3625
+ ),
3626
+ /* @__PURE__ */ jsxRuntime.jsx(
3627
+ Button,
3628
+ {
3629
+ className: buttonClass,
3630
+ onClick: onPrimaryClick,
3631
+ size: "xl",
3632
+ variant: "primary",
3633
+ children: primaryLabel
3634
+ }
3635
+ )
3636
+ ] });
3637
+ }
3297
3638
  const OptionList = React.forwardRef(
3298
3639
  function OptionList2({
3299
3640
  options,
@@ -4876,16 +5217,22 @@ const TimeInput = React.forwardRef(
4876
5217
  className
4877
5218
  ),
4878
5219
  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: [
5220
+ isMobile ? /* @__PURE__ */ jsxRuntime.jsx(
5221
+ BottomSheet,
5222
+ {
5223
+ open,
5224
+ onOpenChange: handleOpenChange,
5225
+ trigger: triggerButton,
5226
+ title: "เลือกเวลา",
5227
+ showHeader: false,
5228
+ rightSide: "none",
5229
+ contentClassName: "pt-0",
5230
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "overflow-auto px-4 pt-2 pb-8 w-full", children: [
4884
5231
  pickerContent,
4885
5232
  actionButtons
4886
5233
  ] })
4887
- ] })
4888
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
5234
+ }
5235
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
4889
5236
  Popover__namespace.Root,
4890
5237
  {
4891
5238
  open,
@@ -4920,6 +5267,7 @@ const TimeInput = React.forwardRef(
4920
5267
  }
4921
5268
  );
4922
5269
  TimeInput.displayName = "TimeInput";
5270
+ exports.BottomSheet = BottomSheet;
4923
5271
  exports.Button = Button;
4924
5272
  exports.Card = Card;
4925
5273
  exports.Checkbox = Checkbox;
@@ -4928,6 +5276,7 @@ exports.DateInput = DateInput;
4928
5276
  exports.Dropdown = Dropdown;
4929
5277
  exports.DropdownMultiple = DropdownMultiple;
4930
5278
  exports.Input = Input;
5279
+ exports.Modal = Modal;
4931
5280
  exports.OptionList = OptionList;
4932
5281
  exports.Radio = Radio;
4933
5282
  exports.SearchInput = SearchInput;
@@ -4942,4 +5291,5 @@ exports.Tag = Tag;
4942
5291
  exports.TextArea = TextArea;
4943
5292
  exports.TimeInput = TimeInput;
4944
5293
  exports.cn = cn;
5294
+ exports.useIsMobile = useIsMobile;
4945
5295
  //# sourceMappingURL=index.cjs.map