@plastic-js/tsumiki 0.1.22 → 0.1.24

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.
Files changed (199) hide show
  1. package/README.md +838 -65
  2. package/dist/components/Accordion.js +93 -4
  3. package/dist/components/Avatar.js +96 -4
  4. package/dist/components/BottomSheet.js +131 -0
  5. package/dist/components/Button.js +194 -0
  6. package/dist/components/CardNumberInput.js +28 -44
  7. package/dist/components/Carousel.js +71 -5
  8. package/dist/components/Checkbox.js +178 -4
  9. package/dist/components/Clipboard.js +135 -5
  10. package/dist/components/CloseButton.js +97 -0
  11. package/dist/components/Collapsible.js +22 -4
  12. package/dist/components/ColorPicker.js +327 -4
  13. package/dist/components/Combobox.js +86 -13
  14. package/dist/components/ConfirmDialog.js +174 -19
  15. package/dist/components/DateInput.js +437 -5
  16. package/dist/components/{DatePickerMobile/wheel.js → DateWheel.js} +1 -3
  17. package/dist/components/Dialog.js +214 -110
  18. package/dist/components/Drawer.js +169 -4
  19. package/dist/components/Field.js +121 -5
  20. package/dist/components/Fieldset.js +64 -5
  21. package/dist/components/FileUpload.js +181 -4
  22. package/dist/components/FocusTrap.js +0 -2
  23. package/dist/components/Icon.js +32 -17
  24. package/dist/components/Input.js +211 -0
  25. package/dist/components/Listbox.js +47 -4
  26. package/dist/components/Menu.js +99 -4
  27. package/dist/components/MoneyInput.js +33 -85
  28. package/dist/components/NumberInput.js +117 -4
  29. package/dist/components/Pagination.js +91 -5
  30. package/dist/components/Popover.js +123 -4
  31. package/dist/components/Portal.js +0 -2
  32. package/dist/components/Presence.js +0 -2
  33. package/dist/components/Progress.js +107 -4
  34. package/dist/components/RadioGroup.js +104 -4
  35. package/dist/components/RatingGroup.js +75 -4
  36. package/dist/components/SearchInput.js +60 -55
  37. package/dist/components/Select.js +428 -5
  38. package/dist/components/SelectPc.js +239 -0
  39. package/dist/components/SignaturePad.js +77 -4
  40. package/dist/components/Skeleton.js +2 -4
  41. package/dist/components/Slider.js +112 -4
  42. package/dist/components/Spinner.js +41 -0
  43. package/dist/components/Splitter.js +51 -4
  44. package/dist/components/Steps.js +259 -4
  45. package/dist/components/SwipeReveal.js +2 -4
  46. package/dist/components/Switch.js +147 -4
  47. package/dist/components/Tabs.js +80 -4
  48. package/dist/components/TagsInput.js +128 -4
  49. package/dist/components/Toast.js +174 -4
  50. package/dist/components/Toggle.js +39 -4
  51. package/dist/components/ToggleGroup.js +103 -4
  52. package/dist/components/Tour.js +63 -4
  53. package/dist/components/TreeView.js +113 -4
  54. package/dist/index.js +44 -45
  55. package/dist/tsumiki.css +1 -0
  56. package/docs/api.md +670 -0
  57. package/docs/component-api-pattern.md +362 -0
  58. package/package.json +16 -6
  59. package/src/styles/color.css +112 -0
  60. package/src/styles/index.css +1 -0
  61. package/src/styles/radius.css +27 -0
  62. package/src/styles/reset.css +111 -0
  63. package/src/styles/shadow.css +5 -0
  64. package/src/styles/sizing.css +59 -0
  65. package/src/styles/spacing.css +9 -0
  66. package/src/styles/theme.css +38 -0
  67. package/src/styles/tokens.css +22 -0
  68. package/src/styles/transition.css +5 -0
  69. package/src/styles/typography.css +8 -0
  70. package/dist/components/Accordion.js.map +0 -1
  71. package/dist/components/Avatar.js.map +0 -1
  72. package/dist/components/CardNumberInput.js.map +0 -1
  73. package/dist/components/Carousel.js.map +0 -1
  74. package/dist/components/Checkbox.js.map +0 -1
  75. package/dist/components/Clipboard.js.map +0 -1
  76. package/dist/components/Collapsible.js.map +0 -1
  77. package/dist/components/ColorPicker.js.map +0 -1
  78. package/dist/components/Combobox.js.map +0 -1
  79. package/dist/components/ConfirmDialog.js.map +0 -1
  80. package/dist/components/DateInput.js.map +0 -1
  81. package/dist/components/DatePicker.js +0 -7
  82. package/dist/components/DatePicker.js.map +0 -1
  83. package/dist/components/DatePickerMobile/index.js +0 -479
  84. package/dist/components/DatePickerMobile/index.js.map +0 -1
  85. package/dist/components/DatePickerMobile/wheel.js.map +0 -1
  86. package/dist/components/Dialog.js.map +0 -1
  87. package/dist/components/Drawer.js.map +0 -1
  88. package/dist/components/Field.js.map +0 -1
  89. package/dist/components/Fieldset.js.map +0 -1
  90. package/dist/components/FileUpload.js.map +0 -1
  91. package/dist/components/FilterableSelect.js +0 -210
  92. package/dist/components/FilterableSelect.js.map +0 -1
  93. package/dist/components/FilterableSelectMobile.js +0 -167
  94. package/dist/components/FilterableSelectMobile.js.map +0 -1
  95. package/dist/components/FocusTrap.js.map +0 -1
  96. package/dist/components/HoverCard.js +0 -7
  97. package/dist/components/HoverCard.js.map +0 -1
  98. package/dist/components/Icon.js.map +0 -1
  99. package/dist/components/Listbox.js.map +0 -1
  100. package/dist/components/Menu.js.map +0 -1
  101. package/dist/components/MoneyInput.js.map +0 -1
  102. package/dist/components/NumberInput.js.map +0 -1
  103. package/dist/components/Pagination.js.map +0 -1
  104. package/dist/components/Popover.js.map +0 -1
  105. package/dist/components/Portal.js.map +0 -1
  106. package/dist/components/Presence.js.map +0 -1
  107. package/dist/components/Progress.js.map +0 -1
  108. package/dist/components/RadioGroup.js.map +0 -1
  109. package/dist/components/RatingGroup.js.map +0 -1
  110. package/dist/components/SearchInput.js.map +0 -1
  111. package/dist/components/Select.js.map +0 -1
  112. package/dist/components/SelectMobile/anatomy.js +0 -10
  113. package/dist/components/SelectMobile/anatomy.js.map +0 -1
  114. package/dist/components/SelectMobile/index.js +0 -310
  115. package/dist/components/SelectMobile/index.js.map +0 -1
  116. package/dist/components/SignaturePad.js.map +0 -1
  117. package/dist/components/Skeleton.js.map +0 -1
  118. package/dist/components/Slider.js.map +0 -1
  119. package/dist/components/Splitter.js.map +0 -1
  120. package/dist/components/Steps.js.map +0 -1
  121. package/dist/components/SwipeReveal.js.map +0 -1
  122. package/dist/components/Switch.js.map +0 -1
  123. package/dist/components/Tabs.js.map +0 -1
  124. package/dist/components/TagsInput.js.map +0 -1
  125. package/dist/components/Toast.js.map +0 -1
  126. package/dist/components/Toggle.js.map +0 -1
  127. package/dist/components/ToggleGroup.js.map +0 -1
  128. package/dist/components/Tooltip.js +0 -7
  129. package/dist/components/Tooltip.js.map +0 -1
  130. package/dist/components/Tour.js.map +0 -1
  131. package/dist/components/TreeView.js.map +0 -1
  132. package/dist/node_modules/framesync/dist/es/create-render-step.js +0 -54
  133. package/dist/node_modules/framesync/dist/es/create-render-step.js.map +0 -1
  134. package/dist/node_modules/framesync/dist/es/index.js +0 -61
  135. package/dist/node_modules/framesync/dist/es/index.js.map +0 -1
  136. package/dist/node_modules/framesync/dist/es/on-next-frame.js +0 -8
  137. package/dist/node_modules/framesync/dist/es/on-next-frame.js.map +0 -1
  138. package/dist/node_modules/hey-listen/dist/hey-listen.es.js +0 -15
  139. package/dist/node_modules/hey-listen/dist/hey-listen.es.js.map +0 -1
  140. package/dist/node_modules/popmotion/dist/es/animations/generators/decay.js +0 -24
  141. package/dist/node_modules/popmotion/dist/es/animations/generators/decay.js.map +0 -1
  142. package/dist/node_modules/popmotion/dist/es/animations/generators/keyframes.js +0 -40
  143. package/dist/node_modules/popmotion/dist/es/animations/generators/keyframes.js.map +0 -1
  144. package/dist/node_modules/popmotion/dist/es/animations/generators/spring.js +0 -96
  145. package/dist/node_modules/popmotion/dist/es/animations/generators/spring.js.map +0 -1
  146. package/dist/node_modules/popmotion/dist/es/animations/index.js +0 -94
  147. package/dist/node_modules/popmotion/dist/es/animations/index.js.map +0 -1
  148. package/dist/node_modules/popmotion/dist/es/animations/inertia.js +0 -74
  149. package/dist/node_modules/popmotion/dist/es/animations/inertia.js.map +0 -1
  150. package/dist/node_modules/popmotion/dist/es/animations/utils/detect-animation-from-options.js +0 -21
  151. package/dist/node_modules/popmotion/dist/es/animations/utils/detect-animation-from-options.js.map +0 -1
  152. package/dist/node_modules/popmotion/dist/es/animations/utils/elapsed.js +0 -14
  153. package/dist/node_modules/popmotion/dist/es/animations/utils/elapsed.js.map +0 -1
  154. package/dist/node_modules/popmotion/dist/es/animations/utils/find-spring.js +0 -68
  155. package/dist/node_modules/popmotion/dist/es/animations/utils/find-spring.js.map +0 -1
  156. package/dist/node_modules/popmotion/dist/es/easing/index.js +0 -28
  157. package/dist/node_modules/popmotion/dist/es/easing/index.js.map +0 -1
  158. package/dist/node_modules/popmotion/dist/es/easing/utils.js +0 -13
  159. package/dist/node_modules/popmotion/dist/es/easing/utils.js.map +0 -1
  160. package/dist/node_modules/popmotion/dist/es/utils/clamp.js +0 -6
  161. package/dist/node_modules/popmotion/dist/es/utils/clamp.js.map +0 -1
  162. package/dist/node_modules/popmotion/dist/es/utils/hsla-to-rgba.js +0 -35
  163. package/dist/node_modules/popmotion/dist/es/utils/hsla-to-rgba.js.map +0 -1
  164. package/dist/node_modules/popmotion/dist/es/utils/inc.js +0 -6
  165. package/dist/node_modules/popmotion/dist/es/utils/inc.js.map +0 -1
  166. package/dist/node_modules/popmotion/dist/es/utils/interpolate.js +0 -69
  167. package/dist/node_modules/popmotion/dist/es/utils/interpolate.js.map +0 -1
  168. package/dist/node_modules/popmotion/dist/es/utils/mix-color.js +0 -45
  169. package/dist/node_modules/popmotion/dist/es/utils/mix-color.js.map +0 -1
  170. package/dist/node_modules/popmotion/dist/es/utils/mix-complex.js +0 -61
  171. package/dist/node_modules/popmotion/dist/es/utils/mix-complex.js.map +0 -1
  172. package/dist/node_modules/popmotion/dist/es/utils/mix.js +0 -6
  173. package/dist/node_modules/popmotion/dist/es/utils/mix.js.map +0 -1
  174. package/dist/node_modules/popmotion/dist/es/utils/pipe.js +0 -7
  175. package/dist/node_modules/popmotion/dist/es/utils/pipe.js.map +0 -1
  176. package/dist/node_modules/popmotion/dist/es/utils/progress.js +0 -9
  177. package/dist/node_modules/popmotion/dist/es/utils/progress.js.map +0 -1
  178. package/dist/node_modules/popmotion/dist/es/utils/velocity-per-second.js +0 -8
  179. package/dist/node_modules/popmotion/dist/es/utils/velocity-per-second.js.map +0 -1
  180. package/dist/node_modules/popmotion/node_modules/tslib/tslib.es6.js +0 -13
  181. package/dist/node_modules/popmotion/node_modules/tslib/tslib.es6.js.map +0 -1
  182. package/dist/node_modules/style-value-types/dist/es/color/hex.js +0 -39
  183. package/dist/node_modules/style-value-types/dist/es/color/hex.js.map +0 -1
  184. package/dist/node_modules/style-value-types/dist/es/color/hsla.js +0 -16
  185. package/dist/node_modules/style-value-types/dist/es/color/hsla.js.map +0 -1
  186. package/dist/node_modules/style-value-types/dist/es/color/index.js +0 -20
  187. package/dist/node_modules/style-value-types/dist/es/color/index.js.map +0 -1
  188. package/dist/node_modules/style-value-types/dist/es/color/rgba.js +0 -15
  189. package/dist/node_modules/style-value-types/dist/es/color/rgba.js.map +0 -1
  190. package/dist/node_modules/style-value-types/dist/es/color/utils.js +0 -19
  191. package/dist/node_modules/style-value-types/dist/es/color/utils.js.map +0 -1
  192. package/dist/node_modules/style-value-types/dist/es/complex/index.js +0 -58
  193. package/dist/node_modules/style-value-types/dist/es/complex/index.js.map +0 -1
  194. package/dist/node_modules/style-value-types/dist/es/numbers/index.js +0 -13
  195. package/dist/node_modules/style-value-types/dist/es/numbers/index.js.map +0 -1
  196. package/dist/node_modules/style-value-types/dist/es/numbers/units.js +0 -16
  197. package/dist/node_modules/style-value-types/dist/es/numbers/units.js.map +0 -1
  198. package/dist/node_modules/style-value-types/dist/es/utils.js +0 -13
  199. package/dist/node_modules/style-value-types/dist/es/utils.js.map +0 -1
package/README.md CHANGED
@@ -8,21 +8,35 @@ A UI component library for Plastic JS.
8
8
  npm install @plastic-js/tsumiki
9
9
  ```
10
10
 
11
+ ## Styles
12
+
13
+ Import the bundled stylesheet — a single file (`tsumiki.css`) containing the design tokens (`--tsu-*` CSS custom properties) **and** the reset:
14
+
15
+ ```js
16
+ import '@plastic-js/tsumiki/styles'
17
+ ```
18
+
19
+ Every component is styled with `--tsu-*` tokens, so this import is required for components to render correctly. If you bring your own reset, the reset portion of `tsumiki.css` is safe to keep anyway (it's minimal); to skip it entirely, compose your own stylesheet from the raw token files, e.g. `@plastic-js/tsumiki/styles/color.css`.
20
+
21
+ > Raw granular imports (`@plastic-js/tsumiki/styles/*.css`) reference Radix Colors scales via `@import '@radix-ui/colors/…'`, so they require your bundler to resolve that package — install it yourself (`npm i @radix-ui/colors`) if you go that route. The bundled `@plastic-js/tsumiki/styles` entry is fully self-contained and needs nothing extra.
22
+
23
+ > Custom theming: override any `--tsu-*` variable in your own CSS **after** the library styles (see §3.2). Your `:root` overrides apply to both light and dark mode; add a `.dark { … }` block for per-mode colors.
24
+
11
25
  ## Usage
12
26
 
13
27
  ```js
14
- import { Dialog, Combobox, Tooltip, SelectMobile, FilterableSelectMobile } from '@plastic-js/tsumiki'
28
+ import { Dialog, Combobox, Select } from '@plastic-js/tsumiki'
15
29
  ```
16
30
 
17
31
  ## Components
18
32
 
19
- ### SelectMobile
33
+ ### Select
20
34
 
21
- A mobile-optimized bottom-sheet select built with a compound parts pattern. Renders a trigger button and a draggable sheet overlay — ideal for touch interfaces.
35
+ A mobile-optimized bottom-sheet select. Renders a trigger button and a draggable sheet overlay — ideal for touch interfaces.
22
36
 
23
37
  ```jsx
24
38
  import { createSignal } from '@plastic-js/plastic'
25
- import { SelectMobile } from '@plastic-js/tsumiki'
39
+ import { Select, SelectTrigger } from '@plastic-js/tsumiki'
26
40
 
27
41
  function Example(){
28
42
  const value = createSignal(null)
@@ -32,19 +46,18 @@ function Example(){
32
46
  ]
33
47
 
34
48
  return (
35
- <SelectMobile.Root
49
+ <Select
36
50
  value={value}
37
51
  onValueChange={v => value(v)}
38
52
  items={items}
39
53
  >
40
- <SelectMobile.Trigger placeholder='Choose a city' />
41
- <SelectMobile.Content />
42
- </SelectMobile.Root>
54
+ <SelectTrigger placeholder='Choose a city' />
55
+ </Select>
43
56
  )
44
57
  }
45
58
  ```
46
59
 
47
- **SelectMobile.Root props:**
60
+ **Select props:**
48
61
 
49
62
  | Prop | Type | Default | Description |
50
63
  |---|---|---|---|
@@ -53,11 +66,16 @@ function Example(){
53
66
  | `items` | `array \| (() => array)` | `[]` | Data array |
54
67
  | `itemToValue` | `(item) => string` | `item.value` | Extract value from an item |
55
68
  | `itemToLabel` | `(item) => node` | `item.label` | Extract label from an item |
56
- | `open` | `(() => boolean)` | — | Controlled open state (getter) |
57
- | `defaultOpen` | `boolean` | `false` | Initial open state (uncontrolled) |
69
+ | `open` | `(() => boolean)` | — | Controlled open state (getter) — REQUIRED |
58
70
  | `onOpenChange` | `(isOpen: boolean) => void` | — | Called when open state changes |
71
+ | `disabled` | `boolean` | `false` | Disable the select |
72
+ | `clearable` | `boolean` | `false` | Show a "clear selection" row at the top |
73
+ | `clearLabel` | `string` | `'None'` | Label for the clear row |
74
+ | `backdropClassName` | `string` | — | CSS class for the backdrop overlay |
75
+ | `backdropStyle` | `object` | — | Inline style for the backdrop overlay |
76
+ | `filter` | `boolean \| (item, query, itemToLabel) => boolean` | — | Enable filter: `true` for substring match, or a custom filter function |
59
77
 
60
- **SelectMobile.Trigger props:**
78
+ **SelectTrigger props:**
61
79
 
62
80
  | Prop | Type | Default | Description |
63
81
  |---|---|---|---|
@@ -65,79 +83,834 @@ function Example(){
65
83
  | `className` | `string` | — | CSS class for the trigger button |
66
84
  | `children` | `node` | — | Custom trigger content (replaces default label + chevron) |
67
85
 
68
- > **Note on Trigger element:** The trigger renders `<div role="button" tabIndex={0}>` instead of a native `<button>` as a defense-in-depth measure against a **Chrome iOS (WebKit) focus-lock bug**.
69
- >
70
- > The primary fix is in `SelectMobile.Content`: the sheet is rendered inline (no `<Portal>`) so it stays inside the parent Dialog's focus-trap boundary. However, the `<div role="button">` is kept as an additional safeguard — in WebKit, tapping a `<button>` inside a scrollable container with `-webkit-overflow-scrolling: touch` can pin `activeElement` to the button, causing subsequent `focus()` calls on the filter `<input>` to be silently ignored. A `<div role="button">` is semantically equivalent for accessibility but does not trigger this WebKit focus-lock behaviour.
71
- >
72
- > Typically a native `<button>` would be preferred for keyboard tab navigation (`tabIndex` works natively). On mobile, however, keyboard tab navigation is irrelevant — the sheet is operated via touch, and the Escape key for closing is handled by the `Dialog`-like overlay, not by tab order. The `tabIndex={0}` on the `<div>` preserves keyboard discoverability for assistive technology while working around the iOS focus trap.
86
+ > **Note on Trigger element:** The trigger renders `<div role="button" tabIndex={0}>` instead of a native `<button>` as a defense-in-depth measure against a **Chrome iOS (WebKit) focus-lock bug**. The sheet is rendered inline (no `<Portal>`) so it stays inside the parent Dialog's focus-trap boundary.
73
87
 
74
- **SelectMobile.Content props:**
88
+ **Filter usage:**
75
89
 
76
- | Prop | Type | Default | Description |
77
- |---|---|---|---|
78
- | `clearable` | `boolean` | `false` | Show a "clear selection" row at the top |
79
- | `clearLabel` | `string` | `'None'` | Label for the clear row |
80
- | `className` | `string` | — | CSS class for the sheet panel |
81
- | `backdropClassName` | `string` | — | CSS class for the backdrop overlay |
82
- | `backdropStyle` | `object` | — | Inline style for the backdrop overlay |
90
+ ```jsx
91
+ // Enable default substring filter
92
+ <Select filter items={cities} ...>
93
+ <SelectTrigger placeholder='Search city…' />
94
+ </Select>
83
95
 
84
- **SelectMobile.Item props:**
96
+ // Custom filter function
97
+ <Select filter={(item, query, itemToLabel) => itemToLabel(item).toLowerCase().startsWith(query)} ...>
98
+ ```
85
99
 
86
- | Prop | Type | Default | Description |
87
- |---|---|---|---|
88
- | `item` | `any` | — | Item from the `items` array (resolves value/label automatically) |
89
- | `value` | `string` | — | Explicit value (use when children are custom) |
90
- | `className` | `string` | — | CSS class for the item button |
91
- | `children` | `node` | — | Custom item content |
100
+ The search input is auto-focused when the sheet opens and cleared when it closes. "No results" is shown when no items match the query.
101
+
102
+ ---
103
+
104
+ ## 1. Project Nature
105
+
106
+ **Tsumiki** (`@plastic-js/tsumiki`) is a mobile-first UI component library for the [Plastic JS](https://github.com/plastic-js/plastic) reactive framework. It provides 51 production-grade, accessible components built on top of headless UI primitives from Ark UI, styled with a Radix Colors-based design token system.
107
+
108
+ The name "tsumiki" (積み木) means "building blocks" in Japanese — the library is designed to be composable, tree-shakable, and themeable.
109
+
110
+ ### Key characteristics
111
+
112
+ - **Mobile-first** — Components default to touch-friendly sizing, with progressive enhancement for desktop interactions
113
+ - **Accessible by default** — Powered by Zag.js state machines via Ark UI wrappers; all components meet WAI-ARIA authoring practices
114
+ - **Signal-reactive** — All props accept both static values and Plastic reactive getter functions (`() => value`), enabling zero-boilerplate reactive UIs
115
+ - **Design-token driven** — Every visual attribute (size, color, radius, spacing, typography) is exposed via CSS custom properties with the `--tsu-` prefix
116
+ - **Tree-shakable** — ESM build with `preserveModules: true`; consumers only bundle what they import
117
+
118
+ ---
119
+
120
+ ## 2. Technology Stack
121
+
122
+ | Layer | Library | Role |
123
+ |-------|---------|------|
124
+ | **State machines** | [@zag-js](https://zag-js.com/) | Accessible component logic (focus management, keyboard nav, ARIA attributes) |
125
+ | **Headless UI** | [@plastic-js/ark](https://github.com/plastic-js/ark) | Plastic JS wrappers around Zag.js machines, exposes composable `Root`/`Control`/`Label` etc. sub-components |
126
+ | **Reactive runtime** | [@plastic-js/plastic](https://github.com/plastic-js/plastic) | Signals, `createContext`/`useContext`, `mergeProps`/`splitProps`, JSX runtime |
127
+ | **CSS-in-JS** | [@emotion/css](https://emotion.sh/) | Zero-runtime `css()` template literals for static styles; CSS custom properties for dynamic values |
128
+ | **Color system** | [@radix-ui/colors](https://www.radix-ui.com/colors) | 12-step color scales with automatic light/dark mode via `prefers-color-scheme` |
129
+ | **Build tool** | [Vite](https://vitejs.dev/) | Dev server for `showcase/`, library build for `dist/`, Babel plugin for Plastic JSX transform |
130
+ | **JSX transform** | `@plastic-js/babel-preset-plastic` | Compiles JSX to `h()` calls with signal-aware reconciliation |
131
+
132
+ ### Architecture diagram
133
+
134
+ ```
135
+ @zag-js/* (state machines)
136
+
137
+
138
+ @plastic-js/ark (headless primitives, Plastic-wrapped)
139
+
140
+
141
+ tsumiki components (styled, sized, token-connected)
142
+
143
+
144
+ Consumer application
145
+ ```
146
+
147
+ ### Ark component mapping
148
+
149
+ Every tsumiki component wraps a corresponding `@plastic-js/ark` primitive:
150
+
151
+ ```js
152
+ import { Switch as ArkSwitch } from '@plastic-js/ark'
153
+ ```
154
+
155
+ The Ark import is always aliased with an `Ark` prefix to distinguish it from the tsumiki component being defined. All 36 Ark module components have corresponding tsumiki implementations, plus 15 additional custom/project-specific components (Button, Input, Skeleton, ConfirmDialog, mobile variants, etc.).
156
+
157
+ ---
158
+
159
+ ## 3. Design Token System
160
+
161
+ All tokens live in `src/styles/` and cascade from `reset.css` → `tokens.css` → sub-files. The published package ships one bundled stylesheet built from them (`@plastic-js/tsumiki/styles` → `dist/tsumiki.css`), built from `index.css`. Every token uses the `--tsu-` namespace.
162
+
163
+ ### 3.1 File structure
164
+
165
+ ```
166
+ src/styles/
167
+ ├── index.css # Bundle entry: imports reset.css (→ tokens.css)
168
+ ├── reset.css # Modern CSS reset, imports tokens.css
169
+ ├── tokens.css # Aggregator: @import all sub-files + root tokens
170
+ ├── theme.css # Radix Colors bridge (generated by `npm run theme`)
171
+ ├── color.css # Semantic color tokens + legacy aliases
172
+ ├── sizing.css # Component sizing (6 tiers × 2 level sequences)
173
+ ├── radius.css # Border radius (L1/L2/L3 per size + round)
174
+ ├── spacing.css # 8-step spacing scale
175
+ ├── typography.css # Font sizes, family, line-height
176
+ ├── transition.css # Timing presets
177
+ └── shadow.css # Elevation shadows
178
+ ```
179
+
180
+ ### 3.2 Color System
181
+
182
+ Powered by Radix Colors 12-step scales. The active theme (generated by `scripts/theme.js`) maps:
183
+
184
+ - **Accent**: Indigo scale → `--tsu-accent-1` through `--tsu-accent-12`
185
+ - **Neutral**: Olive scale → `--tsu-neutral-1` through `--tsu-neutral-12`
186
+
187
+ Additionally, semantic color categories alias Radix scales:
188
+
189
+ | Category | Radix Source | Token Range | Usage |
190
+ |----------|-------------|-------------|-------|
191
+ | `accent` | Indigo | `--tsu-accent-1..12` | Primary brand color, focus rings |
192
+ | `neutral` | Olive | `--tsu-neutral-1..12` | Text, surfaces, borders |
193
+ | `danger` | Red | `--tsu-danger-1..12` | Error states, destructive actions |
194
+ | `success` | Green | `--tsu-success-1..12` | Success states, positive feedback |
195
+ | `warning` | Amber | `--tsu-warning-1..12` | Warning states |
196
+ | `neutral-alpha` | Mauve Alpha | `--tsu-neutral-alpha-1..12` | Shadows, backdrops, translucent overlays |
197
+
198
+ #### Color scale semantics (standard Radix convention)
199
+
200
+ - Steps **1–2**: App background / subtle surfaces
201
+ - Steps **3–5**: Component backgrounds (hover, active, selected)
202
+ - Steps **6–8**: Borders, separators, subtle interactive states
203
+ - Steps **9–10**: Solid backgrounds (primary action, checked state)
204
+ - Steps **11–12**: High-contrast text, icons
205
+
206
+ #### Global surface/text primitives
207
+
208
+ ```css
209
+ --tsu-bg: var(--tsu-neutral-1); /* Page & component surfaces */
210
+ --tsu-fg: var(--tsu-neutral-12); /* Primary text */
211
+ ```
212
+
213
+ #### State tokens
214
+
215
+ ```css
216
+ /* Disabled */
217
+ --tsu-disabled-bg: var(--tsu-neutral-3);
218
+ --tsu-disabled-fg: var(--tsu-neutral-9);
219
+ --tsu-disabled-border: var(--tsu-neutral-7);
220
+
221
+ /* Focus */
222
+ --tsu-focus-bg: var(--tsu-neutral-2);
223
+ --tsu-focus-border: var(--tsu-accent-8);
224
+ ```
225
+
226
+ #### Legacy aliases (backward compatibility)
227
+
228
+ ```css
229
+ --ink: var(--tsu-fg);
230
+ --muted: var(--tsu-neutral-11);
231
+ --accent: var(--tsu-accent-9);
232
+ --surface: var(--tsu-bg);
233
+ --bg: var(--tsu-neutral-2);
234
+ --border: var(--tsu-neutral-7);
235
+ --danger: var(--tsu-danger-9);
236
+ ```
237
+
238
+ #### Dark mode
239
+
240
+ Radix Colors ships both light and dark variants. Dark values are applied when the `.dark` class is present on the root element (`<html>`):
241
+
242
+ ```html
243
+ <html class="dark">
244
+ ```
245
+
246
+ Toggle it at runtime:
247
+
248
+ ```js
249
+ document.documentElement.classList.toggle('dark')
250
+ ```
251
+
252
+ When `.dark` is active, every token re-resolves to the dark Radix scale automatically (`--tsu-neutral-1..12`, `--tsu-accent-1..12`, `--tsu-danger-*`, etc.). Tsumiki's theme-level dark overrides (e.g. the lighter page background) live under `:root.dark` in `src/styles/color.css`.
253
+
254
+ For native form controls (date pickers, scrollbars, etc.) to render in dark mode, also set `color-scheme: dark`:
255
+
256
+ ```css
257
+ html.dark {
258
+ color-scheme: dark;
259
+ }
260
+ ```
261
+
262
+ ### 3.3 Sizing System
263
+
264
+ Six tiers (`xs` → `xxl`) with two level sequences:
265
+
266
+ | Tier | Primary Height | Secondary Height | Font Size | Line Height | Padding Y | Padding X |
267
+ |------|---------------|-------------------|-----------|-------------|-----------|-----------|
268
+ | **xs** | 32px | 16px | 12px | 16px | 7px | 8px |
269
+ | **sm** | 36px | 18px | 14px | 20px | 7px | 12px |
270
+ | **md** | 40px | 20px | 14px | 20px | 9px | 16px |
271
+ | **lg** | 44px | 22px | 16px | 24px | 9px | 20px |
272
+ | **xl** | 48px | 24px | 18px | 28px | 9px | 24px |
273
+ | **xxl** | 64px | 32px | 20px | 30px | 16px | 32px |
274
+
275
+ - **Primary height** (`--tsu-comp-height-{size}`): Used for buttons, inputs, selects — the full component height
276
+ - **Secondary height** (`--tsu-comp-secondary-height-{size}`): Used for checkboxes, radio indicators, switch thumbs — the inner control size
277
+
278
+ #### Token naming pattern
279
+
280
+ ```
281
+ --tsu-comp-height-{size} # Full component height (e.g. Button)
282
+ --tsu-comp-secondary-height-{size} # Icon/indicator/thumb size (e.g. Checkbox control)
283
+ --tsu-comp-font-size-{size} # Component text size
284
+ --tsu-comp-line-height-{size} # Component text line height
285
+ --tsu-comp-padding-y-{size} # Vertical padding
286
+ --tsu-comp-padding-x-{size} # Horizontal padding
287
+ ```
288
+
289
+ ### 3.4 Border Radius
290
+
291
+ Three nesting levels per size tier, plus a fully-round utility:
292
+
293
+ ```
294
+ --tsu-radius-l1-{size} # Atomic: buttons, inputs, toggles (2px–16px)
295
+ --tsu-radius-l2-{size} # Card: cards, popovers, menus
296
+ --tsu-radius-l3-{size} # Modal: dialogs, drawers, bottom sheets
297
+ --tsu-radius-round # 9999px — pill shapes, avatars
298
+ ```
299
+
300
+ ### 3.5 Spacing
301
+
302
+ 8-step scale for gaps and margins:
303
+
304
+ | xxs | xs | sm | md | lg | xl | xxl |
305
+ |-----|----|----|----|----|----|-----|
306
+ | 2px | 4px | 8px | 12px | 16px | 24px | 32px |
307
+
308
+ Tokens: `--tsu-spacing-{step}`
309
+
310
+ ### 3.6 Typography
311
+
312
+ General text sizes (separate from component font sizes):
313
+
314
+ | xs | sm | md | lg | xl | xxl |
315
+ |----|----|----|----|----|-----|
316
+ | 10px | 12px | 14px | 16px | 20px | 24px |
317
+
318
+ Global defaults:
319
+ ```css
320
+ --tsu-font-family: system-ui, -apple-system, sans-serif;
321
+ --tsu-line-height: 1.5;
322
+ ```
323
+
324
+ ### 3.7 Transitions
325
+
326
+ Three timing presets:
327
+
328
+ ```css
329
+ --tsu-transition-fast: 0.15s ease; /* Quick interactions: hover, focus, toggle */
330
+ --tsu-transition-normal: 0.3s ease; /* Medium transitions: expand, slide */
331
+ --tsu-transition-overlay: 0.3s ease; /* Overlays/backdrops: dialog, drawer, popover */
332
+ ```
333
+
334
+ ### 3.8 Shadows
335
+
336
+ Three elevation levels using alpha overlay tokens for automatic dark mode adaptation:
337
+
338
+ ```css
339
+ --tsu-shadow-sm: 0 1px 2px var(--tsu-neutral-alpha-6);
340
+ --tsu-shadow-md: 0 4px 12px var(--tsu-neutral-alpha-8);
341
+ --tsu-shadow-lg: 0 8px 24px var(--tsu-neutral-alpha-10);
342
+ ```
343
+
344
+ ### 3.9 Z-Index Layers
345
+
346
+ ```css
347
+ --tsu-z-base: 0; /* Default */
348
+ --tsu-z-dropdown: 100; /* Popovers, menus, listboxes */
349
+ --tsu-z-modal: 200; /* Dialogs, drawers */
350
+ --tsu-z-toast: 300; /* Toast notifications */
351
+ ```
352
+
353
+ ---
354
+
355
+ ## 4. Controlled-Only Policy
356
+
357
+ Tsumiki enforces a **controlled-only** pattern for all components. Uncontrolled usage (via `defaultValue`, `defaultOpen`, `defaultChecked`, etc.) is **not supported** at the tsumiki component level, even if the underlying Ark component supports it.
358
+
359
+ ### Rationale
360
+
361
+ - **Predictability** — Controlled state is explicit and deterministic; uncontrolled state creates sources of truth that are hard to reason about
362
+ - **Consistency** — Every component follows the same `value`/`onValueChange` pattern, making the API uniform
363
+ - **Testability** — Controlled components are easier to test because state is always owned by the consumer
364
+
365
+ ### What this means
366
+
367
+ - Components take a `value` (or `checked`, `open`, etc.) signal as a controlled prop
368
+ - Components call an `onValueChange` (or `onCheckedChange`, `onOpenChange`, etc.) callback when the value changes
369
+ - No `defaultValue`, `defaultOpen`, `defaultChecked`, or similar props exist on tsumiki components
370
+ - If a consumer wants an initial value, they set it in their own `createSignal`:
371
+
372
+ ```jsx
373
+ // CORRECT: Controlled
374
+ const isOpen = createSignal(false)
375
+ <Dialog open={isOpen} onOpenChange={v => isOpen(v.open)} />
376
+
377
+ // WRONG: Uncontrolled (not supported)
378
+ <Dialog defaultOpen />
379
+ ```
380
+
381
+ ### Enforcement
382
+
383
+ - The `src/components/Select.jsx` component **does not** accept `defaultOpen` — `open` is always required
384
+ - All Ark-wrapped components pass through their `...rest` props to the underlying Ark primitives. While the Ark primitive may technically accept `default*` props internally, consumers **must not** rely on this behavior as tsumiki reserves the right to break uncontrolled usage in future versions
385
+
386
+ ---
387
+
388
+ ## 5. Component Authoring Patterns
389
+
390
+ Every tsumiki component follows a consistent set of conventions. Understanding these patterns is essential for adding new components or writing showcase pages.
391
+
392
+ ### 5.1 Standard Import Block
393
+
394
+ ```js
395
+ import { css, keyframes } from '@emotion/css'
396
+ import { ComponentName as ArkComponentName } from '@plastic-js/ark'
397
+ import { mergeProps, splitProps } from '@plastic-js/plastic'
398
+ ```
399
+
400
+ - `css` / `keyframes` — from `@emotion/css` for static CSS generation
401
+ - Ark primitive — always aliased with `Ark` prefix to avoid name collision with the tsumiki component
402
+ - `mergeProps` / `splitProps` — from `@plastic-js/plastic` for layered defaults + prop splitting
403
+ - Additional imports (`createContext`, `useContext`) from `@plastic-js/plastic` for compound components
404
+
405
+ ### 5.2 The `read()` Helper
406
+
407
+ Defined locally in every component file (not imported from a shared location):
408
+
409
+ ```js
410
+ const read = (v) => typeof v === 'function' ? v() : v
411
+ ```
412
+
413
+ All props can be either **static values** or **Plastic reactive getter functions**. The `read()` helper unwraps them:
414
+
415
+ ```js
416
+ // Consumers can pass either:
417
+ <Switch size="md" /> // static
418
+ <Switch size={() => mySignal()} /> // reactive
419
+ ```
420
+
421
+ This is used wherever a prop value is consumed outside of JSX attribute position.
422
+
423
+ ### 5.3 Props Merging Pattern
424
+
425
+ ```js
426
+ const [local, rest] = splitProps(
427
+ mergeProps({ size: 'md' /* defaults */ }, props),
428
+ ['size', 'className', 'children' /* consumed props */],
429
+ )
430
+ ```
431
+
432
+ 1. **`mergeProps(defaults, props)`** — layers defaults under user-provided props
433
+ 2. **`splitProps(merged, keys)`** — separates props into `[consumed, rest]`
434
+ 3. **`local`** — the tsumiki component's own props (size, className, children, etc.)
435
+ 4. **`rest`** — everything else, forwarded to the Ark primitive via spread
436
+
437
+ ### 5.4 CSS Architecture: Static Classes + Custom Properties
438
+
439
+ Tsumiki uses a hybrid approach:
440
+
441
+ #### Approach A: Direct token references (simple components)
442
+
443
+ ```js
444
+ const sizeClasses = {
445
+ md: css({
446
+ height: 'var(--tsu-comp-height-md)',
447
+ padding: 'var(--tsu-comp-padding-y-md) var(--tsu-comp-padding-x-md)',
448
+ fontSize: 'var(--tsu-comp-font-size-md)',
449
+ }),
450
+ }
451
+ ```
452
+
453
+ #### Approach B: Scoped custom properties (complex components)
454
+
455
+ Components that need to compute derived dimensions (e.g., track width = 2× secondary height) use private CSS variables:
456
+
457
+ ```js
458
+ // Size class defines scoped variables
459
+ const sizeClasses = {
460
+ md: css({
461
+ '--_sw-track-height': 'var(--tsu-comp-secondary-height-md)',
462
+ '--_sw-track-width': 'calc(2 * var(--tsu-comp-secondary-height-md))',
463
+ '--_sw-thumb-travel': 'var(--tsu-comp-secondary-height-md)',
464
+ }),
465
+ }
466
+
467
+ // Base class references them with fallbacks
468
+ const controlClass = css({
469
+ height: 'var(--_sw-track-height, var(--tsu-comp-secondary-height-md))',
470
+ width: 'var(--_sw-track-width, calc(2 * var(--tsu-comp-secondary-height-md)))',
471
+ })
472
+ ```
473
+
474
+ **Naming convention**: Private variables use `--_{initials}-` prefix:
475
+ - Button: `--_btn-*`
476
+ - Checkbox: `--_cb-*`
477
+ - Switch: `--_sw-*`
478
+ - Toggle: `--_tgl-*`
479
+ - Progress: `--_prog-*`
480
+
481
+ #### Variant / Intent pattern (Button style)
482
+
483
+ ```js
484
+ const intentClasses = {
485
+ default: css({
486
+ '--_btn-solid': 'var(--tsu-accent-9)',
487
+ '--_btn-solid-active': 'var(--tsu-accent-8)',
488
+ '--_btn-border': 'var(--tsu-accent-9)',
489
+ }),
490
+ danger: css({
491
+ '--_btn-solid': 'var(--tsu-danger-9)',
492
+ '--_btn-solid-active': 'var(--tsu-danger-8)',
493
+ '--_btn-border': 'var(--tsu-danger-9)',
494
+ }),
495
+ }
496
+ ```
497
+
498
+ ### 5.5 Class Name Composition (Reactive)
499
+
500
+ ```js
501
+ className={() => [
502
+ baseClass,
503
+ sizeClasses[read(local.size)],
504
+ read(local.rounded) && roundedClass,
505
+ local.className,
506
+ ].filter(Boolean).join(' ')}
507
+ ```
508
+
509
+ This uses a **Plastic reactive getter function** so the className list is re-evaluated whenever signals change. Consumer-provided `className` is appended last for overrides.
510
+
511
+ ### 5.6 Base Style Conventions
512
+
513
+ All interactive components share these base styles:
514
+
515
+ ```js
516
+ const rootClass = css({
517
+ display: 'inline-flex',
518
+ alignItems: 'center',
519
+ cursor: 'pointer',
520
+ fontFamily: 'inherit',
521
+ userSelect: 'none',
522
+ touchAction: 'manipulation',
523
+ WebkitTapHighlightColor: 'transparent',
524
+ '&[data-disabled]': { cursor: 'not-allowed' },
525
+ })
526
+ ```
527
+
528
+ Transitions always reference the design tokens:
529
+ ```css
530
+ transition: 'background-color var(--tsu-transition-fast)'
531
+ ```
532
+
533
+ ### 5.7 Disabled State Handling
534
+
535
+ Disabled is applied via the `disabled` prop on the Ark Root, which sets `data-disabled` attribute internally. Components style it with:
536
+
537
+ ```css
538
+ '&[data-disabled]': {
539
+ opacity: 0.5, /* or use --tsu-disabled-* tokens */
540
+ cursor: 'not-allowed',
541
+ }
542
+ ```
543
+
544
+ ### 5.8 Focus State Styling
545
+
546
+ Focus states use the focus tokens defined in `color.css`:
547
+
548
+ ```css
549
+ '&:focus-visible': {
550
+ backgroundColor: 'var(--tsu-focus-bg)',
551
+ outline: '2px solid var(--tsu-focus-border)',
552
+ outlineOffset: '2px',
553
+ }
554
+ ```
555
+
556
+ ### 5.9 Compound Component Pattern
557
+
558
+ Complex components (Menu, Dialog, Accordion, etc.) expose tsumiki-wrapped sub-components as **named exports**. The default export is always the root render component:
559
+
560
+ ```jsx
561
+ import { Menu as ArkMenu } from '@plastic-js/ark'
562
+
563
+ // Default export: the root wrapper
564
+ const Menu = (props = {}) => (
565
+ <ArkMenu.Root className={rootClass} {...rest}>
566
+ {children}
567
+ </ArkMenu.Root>
568
+ )
569
+
570
+ // Named exports: tsumiki-wrapped sub-components
571
+ export const MenuTrigger = (props = {}) => (
572
+ <ArkMenu.Trigger className={triggerClass} {...rest}>
573
+ {children}
574
+ </ArkMenu.Trigger>
575
+ )
576
+
577
+ export const MenuContent = (props = {}) => (
578
+ <ArkMenu.Content className={contentClass} {...rest}>
579
+ {children}
580
+ </ArkMenu.Content>
581
+ )
582
+
583
+ Menu.origin = {
584
+ Root: ArkMenu.Root,
585
+ Trigger: ArkMenu.Trigger,
586
+ Content: ArkMenu.Content,
587
+ }
588
+
589
+ export default Menu
590
+ export { Menu }
591
+ ```
592
+
593
+ Sub-components are **never** attached as static properties on the default export — no `Menu.Trigger = ...`, no `Object.assign`.
594
+
595
+ Self-contained components like Progress auto-render their internals (Track + Range), exposing only Progress + ProgressLabel:
596
+
597
+ ```jsx
598
+ <Progress size="md" value={75} label="Loading" />
599
+ // or with ProgressLabel as child
600
+ <Progress size="md" value={75}>
601
+ <ProgressLabel>Loading</ProgressLabel>
602
+ </Progress>
603
+ ```
604
+
605
+ Internal state sharing uses `createContext` / `useContext` from `@plastic-js/plastic`:
606
+
607
+ ```js
608
+ const ProgressContext = createContext()
609
+ // In ProgressRoot:
610
+ <ProgressContext.Provider value={{ size: read(size) }}>
611
+ // In child:
612
+ const ctx = useContext(ProgressContext)
613
+ ```
614
+
615
+ ### 5.10 Ark Pass-through Export (the `.origin` escape hatch)
616
+
617
+ Components that wrap Ark primitives (Checkbox, Dialog, Switch, etc.) expose the raw Ark sub-components via the `.origin` object. `.origin` is the **only** static property on the component — it maps tsumiki-friendly names to their raw `@plastic-js/ark` counterparts:
618
+
619
+ ```js
620
+ import { Checkbox as ArkCheckbox } from '@plastic-js/ark'
621
+
622
+ const Checkbox = (props = {}) => (
623
+ <ArkCheckbox.Root className={rootClass} {...rest}>
624
+ {children}
625
+ </ArkCheckbox.Root>
626
+ )
627
+
628
+ Checkbox.origin = {
629
+ Root: ArkCheckbox.Root,
630
+ Control: ArkCheckbox.Control,
631
+ Indicator: ArkCheckbox.Indicator,
632
+ Label: ArkCheckbox.Label,
633
+ HiddenInput: ArkCheckbox.HiddenInput,
634
+ }
635
+
636
+ export default Checkbox
637
+ export { Checkbox }
638
+ ```
639
+
640
+ Key rules:
641
+
642
+ - `.origin` (lowercase) is an **object**, never a component — do not write `Component.origin = Root`
643
+ - Only mount what exists on the Ark component
644
+ - No `Object.assign`; the component carries nothing except `.origin`
645
+ - Raw Ark parts live in `.origin`; tsumiki-wrapped sub-components are named exports
646
+
647
+ #### Consumer usage
648
+
649
+ ```jsx
650
+ {/* Primary usage: tsumiki-styled */}
651
+ <Checkbox>Accept terms</Checkbox>
652
+
653
+ {/* Escape hatch: raw Ark sub-parts */}
654
+ <Checkbox.origin.Root>
655
+ <Checkbox.origin.Control>
656
+ <Checkbox.origin.Indicator />
657
+ </Checkbox.origin.Control>
658
+ </Checkbox.origin.Root>
659
+ ```
660
+
661
+ #### File-level example (Dialog.jsx)
662
+
663
+ ```js
664
+ import { Dialog as ArkDialog } from '@plastic-js/ark'
665
+
666
+ const Dialog = (props = {}) => {
667
+ const [local, rest] = splitProps(props, ['className'])
668
+ return <ArkDialog.Root {...rest} className={[rootClass, local.className].filter(Boolean).join(' ')} />
669
+ }
670
+
671
+ Dialog.origin = {
672
+ Root: ArkDialog.Root,
673
+ Backdrop: ArkDialog.Backdrop,
674
+ Positioner: ArkDialog.Positioner,
675
+ Content: ArkDialog.Content,
676
+ Title: ArkDialog.Title,
677
+ Description: ArkDialog.Description,
678
+ CloseTrigger: ArkDialog.CloseTrigger,
679
+ }
680
+
681
+ export default Dialog
682
+ export { Dialog }
683
+ ```
684
+
685
+ Higher-level presets (like `DrawerContent`) are named exports, never static properties.
686
+
687
+ ### 5.11 Component Structure Summary
688
+
689
+ | Pattern | Example | When to use |
690
+ |---------|---------|-------------|
691
+ | Simple single-component | Switch, Button, Toggle, Tabs | One interactive element |
692
+ | Self-contained compound | Progress, Steps | Auto-renders internals; exposes only the root (+ optional named parts) |
693
+ | Multi-part with named exports | Menu, Accordion, TreeView, Drawer | Tsumiki-wrapped sub-components exported by name |
694
+ | Ark pass-through + `.origin` | Checkbox, Dialog, Switch | Wraps Ark primitives; raw Ark parts exposed via `.origin` object |
695
+ | Pure re-export | Portal, Presence, FocusTrap | No styling, direct passthrough |
696
+
697
+ ### 5.11.1 Forbidden Patterns
698
+
699
+ Do **not** use `Object.assign` to assemble components, and do **not** attach sub-components as static properties:
700
+
701
+ ```js
702
+ // ❌ Forbidden — Object.assign spreading sub-components
703
+ const Component = Object.assign(Root, { Trigger: X, Content: Y })
704
+
705
+ // ❌ Forbidden — static sub-properties
706
+ Component.Root = Component
707
+ Component.Trigger = SomeTrigger
708
+
709
+ // ✅ Approved: only .origin (object mapping to raw Ark parts) + named exports
710
+ Component.origin = { Root: ArkPrimitive.Root, Trigger: ArkPrimitive.Trigger }
711
+ export const ComponentTrigger = ArkPrimitive.Trigger
712
+ export default Root
713
+ ```
714
+
715
+ The component function carries **nothing** except `.origin`. Any tsumiki-wrapped sub-components must be named exports — never mount points.
716
+
717
+ ### 5.12 File Organization
718
+
719
+ ```
720
+ src/components/
721
+ ├── Accordion.jsx
722
+ ├── Avatar.jsx
723
+ ├── Button.jsx
724
+ ├── Checkbox.jsx
725
+ ├── Dialog.jsx
726
+ ├── ...
727
+ ├── Select.jsx
728
+ └── index.js # Barrel export file
729
+ ```
730
+
731
+ All components are single `.jsx` files.
732
+
733
+ ---
734
+
735
+ ## 6. Responsive Design
736
+
737
+ ### Mobile-first approach
738
+
739
+ Components default to mobile-appropriate sizing. Desktop adaptations are layered on top:
740
+
741
+ ```css
742
+ /* Default: bottom sheet on mobile */
743
+ '@media (min-width: 640px)': {
744
+ position: 'fixed',
745
+ top: '50%', left: '50%',
746
+ transform: 'translate(-50%, -50%)',
747
+ /* Centered modal on desktop */
748
+ }
749
+ ```
750
+
751
+ The breakpoint (`640px`) is hard-coded per component, not currently a design token.
92
752
 
93
753
  ---
94
754
 
95
- ### FilterableSelectMobile
755
+ ## 7. Showcase Application
96
756
 
97
- A mobile bottom-sheet select with a filter input at the top of the list. Wraps `SelectMobile` internally and exposes a flat single-component API.
757
+ ### Structure
758
+
759
+ ```
760
+ showcase/
761
+ ├── index.html
762
+ ├── global.css # Showcase-specific styles
763
+ ├── App.jsx # SPA shell with client-side routing
764
+ └── pages/
765
+ ├── ButtonPage.jsx
766
+ ├── SwitchPage.jsx
767
+ └── ...
768
+ ```
769
+
770
+ ### Routing
771
+
772
+ Pure client-side SPA via `window.history.pushState` — no router library. Routes are defined in `allShowcases` array in `App.jsx`:
773
+
774
+ ```js
775
+ { href: '/button', label: 'Button', page: ButtonPage }
776
+ ```
777
+
778
+ ### Page Template Pattern
779
+
780
+ Every showcase page follows this structure:
98
781
 
99
782
  ```jsx
100
783
  import { createSignal } from '@plastic-js/plastic'
101
- import { FilterableSelectMobile } from '@plastic-js/tsumiki'
784
+ import ComponentName from '../../src/components/ComponentName.jsx'
102
785
 
103
- function Example(){
104
- const value = createSignal(null)
105
- const cities = [
106
- { value: 'tpe', label: 'Taipei' },
107
- { value: 'kxg', label: 'Kaohsiung' },
108
- ]
786
+ const SIZES = ['xs', 'sm', 'md', 'lg', 'xl']
787
+
788
+ function ComponentNamePage() {
789
+ const someSignal = createSignal(defaultValue)
109
790
 
110
791
  return (
111
- <FilterableSelectMobile
112
- items={cities}
113
- itemToValue={item => item.value}
114
- itemToLabel={item => item.label}
115
- value={value}
116
- onValueChange={v => value(v)}
117
- placeholder='Search city…'
118
- />
792
+ <div className='container'>
793
+ {/* Hero */}
794
+ <div className='hero'>
795
+ <p className='eyebrow'>Category</p>
796
+ <h1>Component Name</h1>
797
+ <p className='hero-copy'>Description. Uses <span className='tag'>--tsu-*</span> tokens.</p>
798
+ </div>
799
+
800
+ {/* Sizes demo */}
801
+ <div className='feature-card'>
802
+ <p className='demo-label'>Sizes</p>
803
+ <div className='demo-row'>
804
+ {SIZES.map(s => <Component key={s} size={s} />)}
805
+ </div>
806
+ </div>
807
+
808
+ {/* Variants demo */}
809
+ <div className='feature-card'>
810
+ <p className='demo-label'>Variants</p>
811
+ <div className='demo-row'>
812
+ {/* ... */}
813
+ </div>
814
+ </div>
815
+
816
+ {/* Interactive playground */}
817
+ <div className='feature-card'>
818
+ <p className='demo-label'>Interactive</p>
819
+ <div className='demo-check-row'>
820
+ <label>
821
+ <select className='demo-select' onChange={...}>
822
+ {SIZES.map(s => <option value={s}>{s}</option>)}
823
+ </select>
824
+ </label>
825
+ </div>
826
+ <Component size={sizeSignal} ... />
827
+ </div>
828
+ </div>
119
829
  )
120
830
  }
831
+
832
+ export default ComponentNamePage
121
833
  ```
122
834
 
123
- **Props:**
835
+ ### Showcase CSS conventions
124
836
 
125
- | Prop | Type | Default | Description |
126
- |---|---|---|---|
127
- | `items` | `array \| (() => array)` | `[]` | Data array |
128
- | `value` | `string \| (() => string)` | `''` | Current selected value |
129
- | `onValueChange` | `(value: string) => void` | | Called when an item is selected |
130
- | `itemToValue` | `(item) => string` | `item.value ?? item` | Extract value from an item |
131
- | `itemToLabel` | `(item) => string` | `item.label ?? String(item)` | Extract label from an item |
132
- | `placeholder` | `string` | `'Select'` | Placeholder text on the trigger |
133
- | `filter` | `(item, query, itemToLabel) => boolean` | substring match | Custom filter function |
134
- | `clearable` | `boolean` | `false` | Show a "clear" row at the top |
135
- | `clearLabel` | `string` | `'None'` | Label for the clear row |
136
- | `backdropClassName` | `string` | — | CSS class for the backdrop overlay |
137
- | `backdropStyle` | `object` | — | Inline style for the backdrop overlay |
837
+ | Class | Purpose |
838
+ |-------|---------|
839
+ | `.container` | Page padding (`0 16px 16px`) |
840
+ | `.hero` / `.eyebrow` / `.hero-copy` | Page header with category label |
841
+ | `.feature-card` | Demo section card (`border`, `border-radius`, `padding: 24px`) |
842
+ | `.demo-label` | Section label (12px, uppercase, `letter-spacing: 0.06em`, `color: var(--tsu-neutral-11)`) |
843
+ | `.demo-row` | Horizontal flex layout (`display: flex; flex-wrap: wrap; gap: 12px; align-items: center`) |
844
+ | `.demo-check-row` | Vertical flex for playground controls |
845
+ | `.demo-select` | Styled `<select>` for interactive size/variant pickers |
846
+ | `.tag` | Inline code-like label for token references (monospace, accent background) |
847
+
848
+ ---
849
+
850
+ ## 8. Theme Customization
851
+
852
+ ### For consumers
853
+
854
+ Override any `--tsu-*` CSS custom property at any level of specificity:
855
+
856
+ ```css
857
+ :root {
858
+ --tsu-accent-9: #your-color;
859
+ --tsu-comp-height-md: 44px;
860
+ }
861
+
862
+ .my-component {
863
+ --tsu-radius-round: 12px;
864
+ }
865
+ ```
138
866
 
139
- The filter input is auto-focused when the sheet opens and cleared when it closes.
867
+ ### Regenerating the default theme (maintainers only)
868
+
869
+ `npm run theme` runs `scripts/theme.js` — an interactive CLI for picking new accent and neutral colors. It regenerates `src/styles/theme.css`, which is the **default theme shipped with the library**. This tool is internal, for library maintainers or anyone forking the library; it is **not** part of the published package.
870
+
871
+ **Consumers never need this.** To theme the library in your own app, override the `--tsu-*` variables after importing the styles (see "For consumers" above).
872
+
873
+ ### Adding new colors (maintainers only)
874
+
875
+ Add to `src/styles/color.css` following the existing pattern:
876
+
877
+ ```css
878
+ --tsu-info-1: var(--tsu-blue-1);
879
+ --tsu-info-2: var(--tsu-blue-2);
880
+ /* ... */
881
+ ```
882
+
883
+ Then define the Radix scale in `theme.css` by importing the desired color:
884
+
885
+ ```css
886
+ @import '@radix-ui/colors/blue.css';
887
+ @import '@radix-ui/colors/blue-dark.css';
888
+ ```
889
+
890
+ ---
891
+
892
+ ## 9. Adding a New Component — Checklist
893
+
894
+ 1. **Create `src/components/NewComponent.jsx`** following the patterns in §5
895
+ 2. **Export from `src/index.js`** — add to the barrel export
896
+ 3. **Create `showcase/pages/NewComponentPage.jsx`** following the template in §7
897
+ 4. **Register the route** in `showcase/App.jsx` `allShowcases` array
898
+ 5. **Verify**: run `npm run dev` and navigate to `/new-component`
899
+
900
+ ---
901
+
902
+ ## 10. Commands Cheatsheet
903
+
904
+ | Command | Purpose |
905
+ |---------|---------|
906
+ | `npm run dev` | Start showcase dev server on port 3456 |
907
+ | `npm run build` | Build library to `dist/` |
908
+ | `npm run theme` | Regenerate default theme CSS (maintainers only) |
909
+ | `npm publish` | Publish (runs `prepublishOnly` → `npm run build`) |
910
+
911
+
912
+ ---
140
913
 
141
914
  ## License
142
915
 
143
- MIT
916
+ MIT