@sarunyu/system-one 4.1.0 → 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,
@@ -3869,7 +4133,15 @@ const Table = React.forwardRef(function Table2({ className, responsive = true, .
3869
4133
  return /* @__PURE__ */ jsxRuntime.jsx(TableScrollShadowContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: scrollRef, className: "w-full overflow-x-auto", children: table }) });
3870
4134
  });
3871
4135
  const TableRow = React.forwardRef(
3872
- function TableRow2({ className, selected = false, hoverable = true, onMouseEnter, onMouseLeave, ...props }, ref) {
4136
+ function TableRow2({
4137
+ className,
4138
+ selected = false,
4139
+ onSelectedChange,
4140
+ hoverable = true,
4141
+ onMouseEnter,
4142
+ onMouseLeave,
4143
+ ...props
4144
+ }, ref) {
3873
4145
  const [hovered, setHovered] = React.useState(false);
3874
4146
  const handleMouseEnter = (e) => {
3875
4147
  if (hoverable) setHovered(true);
@@ -3880,8 +4152,8 @@ const TableRow = React.forwardRef(
3880
4152
  onMouseLeave == null ? void 0 : onMouseLeave(e);
3881
4153
  };
3882
4154
  const rowState = React.useMemo(
3883
- () => ({ selected, hovered: hoverable ? hovered : false }),
3884
- [hoverable, hovered, selected]
4155
+ () => ({ selected, hovered: hoverable ? hovered : false, onSelectedChange }),
4156
+ [hoverable, hovered, selected, onSelectedChange]
3885
4157
  );
3886
4158
  return /* @__PURE__ */ jsxRuntime.jsx(TableRowStateContext.Provider, { value: rowState, children: /* @__PURE__ */ jsxRuntime.jsx(
3887
4159
  "tr",
@@ -4078,7 +4350,14 @@ const TableCell = React.forwardRef(function TableCell2({
4078
4350
  "flex items-center",
4079
4351
  contentAlign === "start" ? "justify-start" : "justify-center"
4080
4352
  ),
4081
- children: /* @__PURE__ */ jsxRuntime.jsx(Checkbox, { checked: cellSelected })
4353
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4354
+ Checkbox,
4355
+ {
4356
+ checked: cellSelected,
4357
+ onChange: rowState.onSelectedChange,
4358
+ ariaLabel: "Select row"
4359
+ }
4360
+ )
4082
4361
  }
4083
4362
  )
4084
4363
  ]
@@ -4861,16 +5140,22 @@ const TimeInput = React.forwardRef(
4861
5140
  className
4862
5141
  ),
4863
5142
  children: [
4864
- isMobile ? /* @__PURE__ */ jsxRuntime.jsxs(Drawer, { open, onOpenChange: handleOpenChange, children: [
4865
- /* @__PURE__ */ jsxRuntime.jsx(DrawerTrigger, { asChild: true, children: triggerButton }),
4866
- /* @__PURE__ */ jsxRuntime.jsxs(DrawerContent, { children: [
4867
- /* @__PURE__ */ jsxRuntime.jsx(DrawerTitle, { className: "sr-only", children: "เลือกเวลา" }),
4868
- /* @__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: [
4869
5154
  pickerContent,
4870
5155
  actionButtons
4871
5156
  ] })
4872
- ] })
4873
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
5157
+ }
5158
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
4874
5159
  Popover__namespace.Root,
4875
5160
  {
4876
5161
  open,
@@ -4905,6 +5190,7 @@ const TimeInput = React.forwardRef(
4905
5190
  }
4906
5191
  );
4907
5192
  TimeInput.displayName = "TimeInput";
5193
+ exports.BottomSheet = BottomSheet;
4908
5194
  exports.Button = Button;
4909
5195
  exports.Card = Card;
4910
5196
  exports.Checkbox = Checkbox;
@@ -4913,6 +5199,7 @@ exports.DateInput = DateInput;
4913
5199
  exports.Dropdown = Dropdown;
4914
5200
  exports.DropdownMultiple = DropdownMultiple;
4915
5201
  exports.Input = Input;
5202
+ exports.Modal = Modal;
4916
5203
  exports.OptionList = OptionList;
4917
5204
  exports.Radio = Radio;
4918
5205
  exports.SearchInput = SearchInput;
@@ -4927,4 +5214,5 @@ exports.Tag = Tag;
4927
5214
  exports.TextArea = TextArea;
4928
5215
  exports.TimeInput = TimeInput;
4929
5216
  exports.cn = cn;
5217
+ exports.useIsMobile = useIsMobile;
4930
5218
  //# sourceMappingURL=index.cjs.map