@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 +15 -0
- package/dist/index.cjs +306 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +308 -20
- package/dist/index.js.map +1 -1
- package/dist/src/components/bottom-sheet.d.ts +23 -0
- package/dist/src/components/bottom-sheet.d.ts.map +1 -0
- package/dist/src/components/date-input.d.ts.map +1 -1
- package/dist/src/components/modal.d.ts +23 -0
- package/dist/src/components/modal.d.ts.map +1 -0
- package/dist/src/components/table.d.ts +2 -0
- package/dist/src/components/table.d.ts.map +1 -1
- package/dist/src/components/time-input.d.ts.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/style.css +1 -1
- package/llms.txt +261 -6
- package/package.json +1 -1
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.
|
|
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.
|
|
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": [
|