@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/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # @sarunyu/system-one — AI usage guide
2
2
 
3
- React component library. Tailwind CSS v4 + CSS custom properties. 17 components.
3
+ React component library. Tailwind CSS v4 + CSS custom properties. 19 components.
4
4
  Built for AI-powered UI generation (v0, Lovable, Figma Make, Cursor).
5
5
 
6
6
  **This file is the contract.** Read it top-to-bottom before generating any screen
@@ -12,7 +12,7 @@ that uses this library. The rules are non-negotiable.
12
12
 
13
13
  1. **Use library components for every element it provides.** Never recreate
14
14
  Button, Input, Tag, Dropdown, Card, Tab, Checkbox, Radio, DateInput, TimeInput,
15
- Table, SearchInput, TextArea, Chip as raw HTML.
15
+ Table, SearchInput, TextArea, Chip, Modal, BottomSheet as raw HTML.
16
16
  2. **Use design-token classes for color and typography.** Never `text-blue-600`,
17
17
  `bg-gray-100`, `text-[#3b82f6]`. The token table below is exhaustive — if a
18
18
  color you need is not in it, use `text-foreground` / `bg-card`.
@@ -68,8 +68,10 @@ import {
68
68
  Tab, TabGroup,
69
69
  Card,
70
70
  Table, TableRow, TableHeaderCell, TableCell,
71
+ // Overlay
72
+ Modal, BottomSheet,
71
73
  // Utility
72
- cn,
74
+ cn, useIsMobile,
73
75
  } from "@sarunyu/system-one";
74
76
  ```
75
77
 
@@ -500,11 +502,55 @@ Key rules for sort:
500
502
  - Only one column sorts at a time — when a column's `sortKey` changes, previous columns naturally read `"none"` via the `dirFor` helper.
501
503
  - Columns that shouldn't sort (status, actions) → `sortable={false}`.
502
504
 
505
+ **Selectable rows.** To make rows selectable with checkboxes, pair `<TableRow selected onSelectedChange>` with a `<TableCell type="checkbox" />`. The cell renders its own checkbox wired to the row — **do not put a `<Checkbox>` inside the cell yourself.** The row background turns brand-tinted automatically when `selected` is true.
506
+
507
+ ```tsx
508
+ import { useMemo, useState } from "react";
509
+ import { Table, TableRow, TableHeaderCell, TableCell } from "@sarunyu/system-one";
510
+
511
+ export function SelectableTable({ rows }: { rows: Row[] }) {
512
+ const [selected, setSelected] = useState<Set<string>>(new Set());
513
+
514
+ const toggle = (id: string) => (next: boolean) =>
515
+ setSelected(prev => {
516
+ const copy = new Set(prev);
517
+ next ? copy.add(id) : copy.delete(id);
518
+ return copy;
519
+ });
520
+
521
+ const headerState =
522
+ selected.size === 0 ? false : selected.size === rows.length ? true : "indeterminate";
523
+ const toggleAll = (next: boolean) =>
524
+ setSelected(next ? new Set(rows.map(r => r.id)) : new Set());
525
+
526
+ return (
527
+ <Table>
528
+ <TableRow header>
529
+ <TableHeaderCell type="check" checkState={headerState} onCheckChange={toggleAll} />
530
+ <TableHeaderCell>Symbol</TableHeaderCell>
531
+ <TableHeaderCell>Name</TableHeaderCell>
532
+ </TableRow>
533
+ {rows.map(r => (
534
+ <TableRow
535
+ key={r.id}
536
+ selected={selected.has(r.id)}
537
+ onSelectedChange={toggle(r.id)}
538
+ >
539
+ <TableCell type="checkbox" />
540
+ <TableCell>{r.symbol}</TableCell>
541
+ <TableCell>{r.name}</TableCell>
542
+ </TableRow>
543
+ ))}
544
+ </Table>
545
+ );
546
+ }
547
+ ```
548
+
503
549
  Props:
504
550
  - `Table` — `className`, native `<table>` props.
505
- - `TableRow` — `header?`, `selected?`, `hoverable?` (default `true`), `onClick`, `className`.
506
- - `TableHeaderCell` — `children` (label), `sortable?` (default `true`, set `false` to hide the arrow), `sortDirection?: "none" | "asc" | "desc"`, `onSortChange?(next)`, `contentAlign?: "start" | "center" | "end"`, `className`.
507
- - `TableCell` — `children` (content), `contentAlign?`, `fixed?`, `className`.
551
+ - `TableRow` — `header?`, `selected?`, `onSelectedChange?(next: boolean)` (fires when a `type="checkbox"` cell in this row is toggled), `hoverable?` (default `true`), `onClick`, `className`.
552
+ - `TableHeaderCell` — `children` (label), `type?: "text" | "icon" | "check"` (use `"check"` for select-all column), `checkState?: boolean | "indeterminate"`, `onCheckChange?(next)`, `sortable?` (default `true`, set `false` to hide the arrow), `sortDirection?: "none" | "asc" | "desc"`, `onSortChange?(next)`, `contentAlign?: "start" | "center" | "end"`, `className`.
553
+ - `TableCell` — `children` (content), `type?: "default" | "text-icon" | "text-image" | "tag" | "icon" | "button" | "checkbox"`, `contentAlign?`, `fixed?`, `className`. For `type="checkbox"` the cell renders its own checkbox — bind selection via the parent `TableRow`, not by nesting a `<Checkbox>`.
508
554
 
509
555
  ---
510
556
 
@@ -535,6 +581,213 @@ Raw option rows. Use when you need a custom dropdown or a sidebar menu — other
535
581
 
536
582
  ---
537
583
 
584
+ ### Modal
585
+
586
+ Centered overlay surface for confirmations, richer content, or status alerts.
587
+ Caller owns open/close state and supplies the backdrop — `<Modal>` only renders
588
+ the dialog panel.
589
+
590
+ ```tsx
591
+ type ModalVariant = "dialog" | "content" | "alert";
592
+ type ModalActionLayout = "none" | "single" | "double";
593
+ type ModalResponsive = "mobile" | "desktop";
594
+ type ModalAlertStatus = "warning" | "success" | "danger";
595
+
596
+ // Dialog — short confirmation (title + text + up to 2 buttons)
597
+ <Modal
598
+ variant="dialog"
599
+ actionLayout="double"
600
+ title="Delete item?"
601
+ description="This can't be undone."
602
+ primaryLabel="Delete"
603
+ secondaryLabel="Cancel"
604
+ onPrimaryClick={confirm}
605
+ onSecondaryClick={close}
606
+ onClose={close}
607
+ />
608
+
609
+ // Content — custom body (pass children)
610
+ <Modal variant="content" actionLayout="single" responsive="desktop" title="Edit profile" onClose={close}>
611
+ <Input placeholder="Name" value={name} onChange={setName} />
612
+ </Modal>
613
+
614
+ // Alert — status moment (warning / success / danger)
615
+ <Modal
616
+ variant="alert"
617
+ alertStatus="success"
618
+ actionLayout="single"
619
+ title="Saved"
620
+ description="Your changes are live."
621
+ primaryLabel="Done"
622
+ onPrimaryClick={close}
623
+ />
624
+ ```
625
+
626
+ **Rendering it onscreen.** Wrap `<Modal>` in your own backdrop/portal:
627
+
628
+ ```tsx
629
+ {open ? (
630
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30 p-4">
631
+ <Modal variant="dialog" actionLayout="double" onClose={close} {...rest} />
632
+ </div>
633
+ ) : null}
634
+ ```
635
+
636
+ **Widths are fixed by variant** — `dialog` 375px, `content`/`alert` 343px.
637
+ Don't override `className` to enlarge them. Mobile by default; set
638
+ `responsive="desktop"` on `variant="content"` to right-align actions.
639
+
640
+ **Action layout** — `none` (no buttons), `single` (primary only, full width on
641
+ mobile), `double` (secondary + primary). Only one primary per modal.
642
+
643
+ Props: `variant`, `actionLayout`, `responsive`, `alertStatus`, `showClose`,
644
+ `title`, `description`, `primaryLabel`, `secondaryLabel`, `children`,
645
+ `onClose`, `onPrimaryClick`, `onSecondaryClick`, `className`.
646
+
647
+ ---
648
+
649
+ ### BottomSheet
650
+
651
+ Mobile-first bottom-anchored sheet. Built on Vaul — it ships its own backdrop,
652
+ drag-to-dismiss, and portal. Pass a `trigger` or control it via `open` /
653
+ `onOpenChange`.
654
+
655
+ ```tsx
656
+ type BottomSheetHeaderType = "text" | "icon" | "image";
657
+ type BottomSheetRightSide = "icon" | "action" | "none";
658
+
659
+ // Controlled — opens from the bottom with title + close icon
660
+ <BottomSheet
661
+ open={open}
662
+ onOpenChange={setOpen}
663
+ title="Filters"
664
+ rightSide="icon"
665
+ >
666
+ <div className="flex flex-col gap-3">{/* filter controls */}</div>
667
+ </BottomSheet>
668
+
669
+ // Uncontrolled with a trigger
670
+ <BottomSheet
671
+ trigger={<Button variant="outline" size="md">Show options</Button>}
672
+ title="Playlist"
673
+ headerType="image"
674
+ imageSrc="/cover.jpg"
675
+ rightSide="action"
676
+ actionLabel="Save"
677
+ onActionClick={save}
678
+ >
679
+ <OptionList options={tracks} selectedValue={pick} onSelect={setPick} />
680
+ </BottomSheet>
681
+
682
+ // Content-only sheet (no header)
683
+ <BottomSheet open={open} onOpenChange={setOpen} showHeader={false}>
684
+ <p className="text-sm text-muted-foreground">Quick tip body</p>
685
+ </BottomSheet>
686
+ ```
687
+
688
+ **Header layout**
689
+ - `headerType="text"` — title only.
690
+ - `headerType="icon"` — leading icon + title (pass `leftIcon`).
691
+ - `headerType="image"` — leading thumbnail + title (pass `imageSrc`).
692
+
693
+ **Right side**
694
+ - `rightSide="icon"` — close button, auto-wired to `DrawerClose`. Custom glyph via `rightIcon`.
695
+ - `rightSide="action"` — inline text button (e.g., "Save"), fires `onActionClick`.
696
+ - `rightSide="none"` — nothing on the right.
697
+
698
+ `showHandle` (default `true`) shows the grab handle at the top. Hide it only
699
+ when you want a fully formal surface. Do not render a `<BottomSheet>` on desktop
700
+ layouts — use `<Modal>` instead.
701
+
702
+ Props: `open`, `onOpenChange`, `trigger`, `headerType`, `showHeader`,
703
+ `rightSide`, `title`, `actionLabel`, `imageSrc`, `leftIcon`, `rightIcon`,
704
+ `onActionClick`, `showHandle`, `children`, `className`, `contentClassName`.
705
+
706
+ ---
707
+
708
+ ### Modal vs BottomSheet — responsive rule
709
+
710
+ **On mobile (< 768px), content-heavy / action-heavy modals MUST render as
711
+ `<BottomSheet>`, not `<Modal>`.** This is not optional — it is the system
712
+ default for every flow that wraps a form or an option list.
713
+
714
+ Mobile → `<BottomSheet>`:
715
+ - Login / signup / account forms
716
+ - Settings panels and profile editors
717
+ - Any multi-field form or multi-step flow
718
+ - Long option / picker lists (e.g. country picker, category filter)
719
+ - Action menus ("Share to…", "Move to…")
720
+
721
+ Mobile → `<Modal>` stays OK:
722
+ - `variant="alert"` (success / warning / danger notifications)
723
+ - Short `variant="dialog"` confirmations (title + one line of text + 1–2 buttons, no input)
724
+
725
+ Desktop (≥ 768px) → always `<Modal>`.
726
+
727
+ Use the library's `useIsMobile()` hook to branch. It ships with the same
728
+ 768px breakpoint the library uses internally (DateInput/TimeInput already
729
+ swap to `<BottomSheet>` on mobile via this hook).
730
+
731
+ ```tsx
732
+ import { useState } from "react";
733
+ import {
734
+ useIsMobile,
735
+ Modal, BottomSheet,
736
+ Input, Button, Checkbox,
737
+ } from "@sarunyu/system-one";
738
+
739
+ export function LoginSheet({
740
+ open,
741
+ onOpenChange,
742
+ }: { open: boolean; onOpenChange: (open: boolean) => void }) {
743
+ const isMobile = useIsMobile();
744
+ const [email, setEmail] = useState("");
745
+ const [pw, setPw] = useState("");
746
+ const [remember, setRmb] = useState(false);
747
+
748
+ const body = (
749
+ <div className="flex flex-col gap-4">
750
+ <Input placeholder="Email" value={email} onChange={setEmail} required />
751
+ <Input placeholder="Password" value={pw} onChange={setPw} type="password" required />
752
+ <Checkbox checked={remember} onCheckedChange={setRmb} label="Remember me" />
753
+ <Button variant="primary" size="xl" className="w-full" onClick={() => onOpenChange(false)}>
754
+ Sign in
755
+ </Button>
756
+ </div>
757
+ );
758
+
759
+ if (isMobile) {
760
+ return (
761
+ <BottomSheet open={open} onOpenChange={onOpenChange} title="Sign in" rightSide="icon">
762
+ {body}
763
+ </BottomSheet>
764
+ );
765
+ }
766
+
767
+ if (!open) return null;
768
+ return (
769
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30 p-4">
770
+ <Modal
771
+ variant="content"
772
+ responsive="desktop"
773
+ title="Sign in"
774
+ onClose={() => onOpenChange(false)}
775
+ >
776
+ {body}
777
+ </Modal>
778
+ </div>
779
+ );
780
+ }
781
+ ```
782
+
783
+ **Key points in the pattern**
784
+ - Extract the body into a single `const body = (...)` so both branches share it. Do not duplicate the form.
785
+ - `useIsMobile()` returns `false` on the server / first paint — the initial render shows the `<Modal>` branch. That is correct; the component is portalled and non-interactive until hydrated.
786
+ - `onOpenChange(false)` closes both branches. Do not hand-roll separate close handlers.
787
+ - Do NOT build a custom `<ResponsiveModal>` wrapper component. Branching inline is the sanctioned pattern — wrappers hide the decision and lead to mis-use.
788
+
789
+ ---
790
+
538
791
  ## Layout — you design it
539
792
 
540
793
  The library ships zero layout components. Compose page structure with plain
@@ -588,6 +841,8 @@ Tailwind. Example scaffolds:
588
841
  | `bg-primary-action` | `bg-blue-600` / `bg-[#3b82f6]` |
589
842
  | `<h1>Title</h1>` | `<h1 className="text-3xl font-bold">Title</h1>` |
590
843
  | `<div className="flex flex-col gap-6">` | `import { Stack } from "@sarunyu/system-one"` |
844
+ | `<Modal variant="dialog" …>` inside your own `fixed inset-0` backdrop | `<div className="fixed inset-0 bg-white rounded p-6">…` |
845
+ | `<BottomSheet open={open} onOpenChange={setOpen}>` | Hand-rolled sheet with `translate-y` + overlay divs |
591
846
  | One `variant="primary"` per context | Two primary buttons side-by-side |
592
847
  | `onChange={setValue}` (value, not event) | `onChange={e => setValue(e.target.value)}` |
593
848
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarunyu/system-one",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "type": "module",
5
5
  "description": "A production-ready React design system built for AI-powered web generation tools (Figma Make, Lovable, V0). Tailwind CSS v4 + CSS custom properties for full theming support.",
6
6
  "keywords": [