@hadoku/task-ui-components 0.0.0-stage → 4.20.3

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 (169) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +917 -2
  3. package/dist/app-header.css +288 -0
  4. package/dist/bento.css +143 -0
  5. package/dist/button.css +227 -0
  6. package/dist/components/AppHeader.d.ts +61 -0
  7. package/dist/components/AppHeader.d.ts.map +1 -0
  8. package/dist/components/AppHeader.js +23 -0
  9. package/dist/components/AppHeader.js.map +1 -0
  10. package/dist/components/BentoCard.d.ts +23 -0
  11. package/dist/components/BentoCard.d.ts.map +1 -0
  12. package/dist/components/BentoCard.js +49 -0
  13. package/dist/components/BentoCard.js.map +1 -0
  14. package/dist/components/BentoGrid.d.ts +24 -0
  15. package/dist/components/BentoGrid.d.ts.map +1 -0
  16. package/dist/components/BentoGrid.js +22 -0
  17. package/dist/components/BentoGrid.js.map +1 -0
  18. package/dist/components/Button.d.ts +28 -0
  19. package/dist/components/Button.d.ts.map +1 -0
  20. package/dist/components/Button.js +51 -0
  21. package/dist/components/Button.js.map +1 -0
  22. package/dist/components/ConnectedSettings.d.ts +53 -0
  23. package/dist/components/ConnectedSettings.d.ts.map +1 -0
  24. package/dist/components/ConnectedSettings.js +302 -0
  25. package/dist/components/ConnectedSettings.js.map +1 -0
  26. package/dist/components/ConnectedThemePicker.d.ts +52 -0
  27. package/dist/components/ConnectedThemePicker.d.ts.map +1 -0
  28. package/dist/components/ConnectedThemePicker.js +32 -0
  29. package/dist/components/ConnectedThemePicker.js.map +1 -0
  30. package/dist/components/ContextMenu.d.ts +22 -0
  31. package/dist/components/ContextMenu.d.ts.map +1 -0
  32. package/dist/components/ContextMenu.js +23 -0
  33. package/dist/components/ContextMenu.js.map +1 -0
  34. package/dist/components/HadokuThemePicker.d.ts +27 -0
  35. package/dist/components/HadokuThemePicker.d.ts.map +1 -0
  36. package/dist/components/HadokuThemePicker.js +19 -0
  37. package/dist/components/HadokuThemePicker.js.map +1 -0
  38. package/dist/components/LoadingSkeleton.d.ts +14 -0
  39. package/dist/components/LoadingSkeleton.d.ts.map +1 -0
  40. package/dist/components/LoadingSkeleton.js +62 -0
  41. package/dist/components/LoadingSkeleton.js.map +1 -0
  42. package/dist/components/Modal.d.ts +39 -0
  43. package/dist/components/Modal.d.ts.map +1 -0
  44. package/dist/components/Modal.js +106 -0
  45. package/dist/components/Modal.js.map +1 -0
  46. package/dist/components/NumberInput.d.ts +26 -0
  47. package/dist/components/NumberInput.d.ts.map +1 -0
  48. package/dist/components/NumberInput.js +131 -0
  49. package/dist/components/NumberInput.js.map +1 -0
  50. package/dist/components/Select.d.ts +52 -0
  51. package/dist/components/Select.d.ts.map +1 -0
  52. package/dist/components/Select.js +281 -0
  53. package/dist/components/Select.js.map +1 -0
  54. package/dist/components/SettingsModal.d.ts +69 -0
  55. package/dist/components/SettingsModal.d.ts.map +1 -0
  56. package/dist/components/SettingsModal.js +141 -0
  57. package/dist/components/SettingsModal.js.map +1 -0
  58. package/dist/components/ThemeIcons.d.ts +26 -0
  59. package/dist/components/ThemeIcons.d.ts.map +1 -0
  60. package/dist/components/ThemeIcons.js +74 -0
  61. package/dist/components/ThemeIcons.js.map +1 -0
  62. package/dist/components/ThemePicker.d.ts +28 -0
  63. package/dist/components/ThemePicker.d.ts.map +1 -0
  64. package/dist/components/ThemePicker.js +101 -0
  65. package/dist/components/ThemePicker.js.map +1 -0
  66. package/dist/components/Toast.d.ts +13 -0
  67. package/dist/components/Toast.d.ts.map +1 -0
  68. package/dist/components/Toast.js +70 -0
  69. package/dist/components/Toast.js.map +1 -0
  70. package/dist/components/Toaster.d.ts +19 -0
  71. package/dist/components/Toaster.d.ts.map +1 -0
  72. package/dist/components/Toaster.js +19 -0
  73. package/dist/components/Toaster.js.map +1 -0
  74. package/dist/components/Tooltip.d.ts +49 -0
  75. package/dist/components/Tooltip.d.ts.map +1 -0
  76. package/dist/components/Tooltip.js +143 -0
  77. package/dist/components/Tooltip.js.map +1 -0
  78. package/dist/context-menu.css +11 -0
  79. package/dist/controls.css +137 -0
  80. package/dist/hooks/useReducedMotion.d.ts +7 -0
  81. package/dist/hooks/useReducedMotion.d.ts.map +1 -0
  82. package/dist/hooks/useReducedMotion.js +25 -0
  83. package/dist/hooks/useReducedMotion.js.map +1 -0
  84. package/dist/hooks/useToast.d.ts +18 -0
  85. package/dist/hooks/useToast.d.ts.map +1 -0
  86. package/dist/hooks/useToast.js +28 -0
  87. package/dist/hooks/useToast.js.map +1 -0
  88. package/dist/icon-btn.css +64 -0
  89. package/dist/index.d.ts +49 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +72 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/lib/menuPlacement.d.ts +16 -0
  94. package/dist/lib/menuPlacement.d.ts.map +1 -0
  95. package/dist/lib/menuPlacement.js +19 -0
  96. package/dist/lib/menuPlacement.js.map +1 -0
  97. package/dist/lib/selectMenus.d.ts +30 -0
  98. package/dist/lib/selectMenus.d.ts.map +1 -0
  99. package/dist/lib/selectMenus.js +236 -0
  100. package/dist/lib/selectMenus.js.map +1 -0
  101. package/dist/lib/settingsClient.d.ts +120 -0
  102. package/dist/lib/settingsClient.d.ts.map +1 -0
  103. package/dist/lib/settingsClient.js +140 -0
  104. package/dist/lib/settingsClient.js.map +1 -0
  105. package/dist/lib/themeScope.d.ts +19 -0
  106. package/dist/lib/themeScope.d.ts.map +1 -0
  107. package/dist/lib/themeScope.js +10 -0
  108. package/dist/lib/themeScope.js.map +1 -0
  109. package/dist/lib/titleTooltips.d.ts +38 -0
  110. package/dist/lib/titleTooltips.d.ts.map +1 -0
  111. package/dist/lib/titleTooltips.js +166 -0
  112. package/dist/lib/titleTooltips.js.map +1 -0
  113. package/dist/lib/tooltipPlacement.d.ts +14 -0
  114. package/dist/lib/tooltipPlacement.d.ts.map +1 -0
  115. package/dist/lib/tooltipPlacement.js +34 -0
  116. package/dist/lib/tooltipPlacement.js.map +1 -0
  117. package/dist/modal.css +129 -0
  118. package/dist/number-input.css +131 -0
  119. package/dist/scrollbar.css +165 -0
  120. package/dist/select.css +251 -0
  121. package/dist/styles.css +1866 -0
  122. package/dist/task-ui-components.css +1866 -0
  123. package/dist/theme-picker.css +219 -0
  124. package/dist/toaster.css +159 -0
  125. package/dist/tooltip.css +103 -0
  126. package/dist/types/index.d.ts +37 -0
  127. package/dist/types/index.d.ts.map +1 -0
  128. package/package.json +60 -4
  129. package/src/app-header.css +288 -0
  130. package/src/bento.css +143 -0
  131. package/src/button.css +227 -0
  132. package/src/components/AppHeader.tsx +87 -0
  133. package/src/components/BentoCard.tsx +72 -0
  134. package/src/components/BentoGrid.tsx +48 -0
  135. package/src/components/Button.tsx +81 -0
  136. package/src/components/ConnectedSettings.tsx +465 -0
  137. package/src/components/ConnectedThemePicker.tsx +79 -0
  138. package/src/components/ContextMenu.tsx +48 -0
  139. package/src/components/HadokuThemePicker.tsx +39 -0
  140. package/src/components/LoadingSkeleton.tsx +84 -0
  141. package/src/components/Modal.tsx +171 -0
  142. package/src/components/NumberInput.tsx +161 -0
  143. package/src/components/Select.tsx +359 -0
  144. package/src/components/SettingsModal.tsx +235 -0
  145. package/src/components/ThemeIcons.tsx +127 -0
  146. package/src/components/ThemePicker.tsx +149 -0
  147. package/src/components/Toast.tsx +92 -0
  148. package/src/components/Toaster.tsx +43 -0
  149. package/src/components/Tooltip.tsx +212 -0
  150. package/src/context-menu.css +11 -0
  151. package/src/controls.css +137 -0
  152. package/src/hooks/useReducedMotion.ts +36 -0
  153. package/src/hooks/useToast.ts +57 -0
  154. package/src/icon-btn.css +64 -0
  155. package/src/index.ts +140 -0
  156. package/src/lib/menuPlacement.ts +31 -0
  157. package/src/lib/selectMenus.ts +303 -0
  158. package/src/lib/settingsClient.ts +377 -0
  159. package/src/lib/themeScope.ts +23 -0
  160. package/src/lib/titleTooltips.ts +236 -0
  161. package/src/lib/tooltipPlacement.ts +59 -0
  162. package/src/modal.css +129 -0
  163. package/src/number-input.css +131 -0
  164. package/src/scrollbar.css +165 -0
  165. package/src/select.css +251 -0
  166. package/src/theme-picker.css +219 -0
  167. package/src/toaster.css +159 -0
  168. package/src/tooltip.css +103 -0
  169. package/src/types/index.ts +43 -0
package/README.md CHANGED
@@ -1,3 +1,918 @@
1
- # Temporary Holding Version
1
+ # @wolffm/task-ui-components
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Reusable UI components for task management applications. Production-ready and fully typed.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @wolffm/task-ui-components
9
+ ```
10
+
11
+ ## SSR Configuration (Astro/Next.js)
12
+
13
+ If using this package in an SSR environment (Astro, Next.js, etc.), add this one-time configuration:
14
+
15
+ ```js
16
+ // astro.config.mjs
17
+ export default defineConfig({
18
+ vite: {
19
+ ssr: {
20
+ noExternal: ['@wolffm/task-ui-components']
21
+ }
22
+ }
23
+ })
24
+ ```
25
+
26
+ ```js
27
+ // next.config.js
28
+ module.exports = {
29
+ transpilePackages: ['@wolffm/task-ui-components']
30
+ }
31
+ ```
32
+
33
+ This allows the package to bundle its CSS automatically. **This is a one-time setup** - all future component updates will work automatically without additional configuration.
34
+
35
+ ## Components
36
+
37
+ ### AppHeader (Overview)
38
+
39
+ The ecosystem-standard top bar for every hadoku.me app: title on the left, an
40
+ action bar on the right holding the theme picker and the unified settings gear.
41
+ Enforces the shared header contract so child apps stop drifting. See
42
+ [Unified app header](#unified-app-header-recommended-for-hadokume-apps) below.
43
+
44
+ ### ConnectedSettings (Overview)
45
+
46
+ The unified per-user settings gear + popout (access tier, display name, global
47
+ content-visibility level, and a "Switch key" link to the site's `/auth` page —
48
+ only that page creates sessions). "Connected": it self-wires to the
49
+ hadoku.me edge-router via same-origin fetches, so it needs zero props. Injected
50
+ automatically by `AppHeader`; can also be used standalone.
51
+
52
+ ### ThemePicker (Overview)
53
+
54
+ A gorgeous theme picker with light/dark pill design. Fully customizable to work with any theme system.
55
+
56
+ ### Modal (Overview)
57
+
58
+ Generic modal dialog with keyboard shortcuts, customizable actions, and optional input field.
59
+
60
+ ### ContextMenu (Overview)
61
+
62
+ Position-based context menu with customizable items and danger states.
63
+
64
+ ### LoadingSkeleton (Overview)
65
+
66
+ Flexible loading skeleton with multiple layouts (default, minimal, custom) and dark theme support.
67
+
68
+ ### SettingsModal (Overview)
69
+
70
+ Generic settings dialog with section-based organization and multiple field types (toggle, text, password, select, button, custom).
71
+
72
+ ### Toast & Toaster
73
+
74
+ Toast notification system with success, error, and info variants.
75
+
76
+ ### Native-control replacements
77
+
78
+ Themed stand-ins for the browser's default chrome. Each ships its own stylesheet; import them
79
+ **unlayered**, next to `@wolffm/themes/style.css`. Live samples across all 18 themes:
80
+ `pnpm run gallery:controls`.
81
+
82
+ ```tsx
83
+ import '@wolffm/task-ui-components/button.css'
84
+ import '@wolffm/task-ui-components/select.css'
85
+ import '@wolffm/task-ui-components/tooltip.css'
86
+ import '@wolffm/task-ui-components/scrollbar.css'
87
+ import { Button, Select, NativeSelect, Tooltip } from '@wolffm/task-ui-components'
88
+ ;<div className="task-app-container hdk-scroll">
89
+ {' '}
90
+ {/* themes every scroller inside */}
91
+ <Tooltip content="Delete task">
92
+ <Button variant="soft" family="danger" icon="trash" aria-label="Delete task" />
93
+ </Tooltip>
94
+ <Select
95
+ aria-label="Status"
96
+ value={status}
97
+ onChange={setStatus}
98
+ options={[
99
+ { value: 'todo', label: 'To do', icon: 'circle' },
100
+ { value: 'done', label: 'Done', icon: 'check' }
101
+ ]}
102
+ />
103
+ </div>
104
+ ```
105
+
106
+ - **Scrollbars** — CSS only. `hdk-scroll` styles its element and every scroller inside it, thumb in the theme's primary
107
+ colour (resolved per element, so nested `data-theme` scopes get their own);
108
+ modifiers `--subtle`, `--thin`, `--rail`, `--autohide`. Zero specificity (`:where()`) inside
109
+ `@layer hdk-fallbacks`, so any scrollbar rule an app writes itself wins — see below.
110
+ - **Button** — `variant` soft (default) | solid | gradient | outline | ghost × `family` primary | success |
111
+ warning | danger | neutral; `size`, `shape` (rounded | pill | square), `icon`, `iconEnd`,
112
+ `loading`, `block`. An icon with no children is a square icon button — give it `aria-label`.
113
+ - **Select** — ARIA listbox with arrows, Home/End, Enter, Escape and typeahead; `variant`
114
+ outline | filled | ghost | underline. **NativeSelect** is a real `<select>` with the same
115
+ trigger; under the hadoku.me shell its list is the themed one too (see `installSelectMenus`).
116
+ - **NumberInput** — a real `<input type="number">` (all input props pass through, controlled or
117
+ not) with a themed up/down stepper in place of the OS spinner; hold to repeat; `size`.
118
+ - **Tooltip** — replaces `title=`. Hover and keyboard focus, Escape dismisses, hoverable bubble;
119
+ `variant` card (default) | inverse | primary, `placement` with viewport flip.
120
+
121
+ Select and Tooltip portal their popups into the anchor's nearest `[data-theme]` scope (the app
122
+ container, or a nested theme preview inside it), so a popup always paints in its anchor's theme
123
+ and escapes `overflow: hidden` and transformed ancestors.
124
+
125
+ ### Page-wide fallbacks (hadoku_site installs these once, for every app)
126
+
127
+ These restyle what an app left to the browser, without touching its markup. An app does not
128
+ install them itself; they are listed here so it knows what it gets and how to opt out.
129
+
130
+ - `controls.css` + `<html class="hdk-controls">` — focus ring, link colour, accent colour on
131
+ checkboxes/radios/ranges/progress, selection, placeholder, `<summary>` marker, the file-picker
132
+ button, no number spinners, and field/button chrome.
133
+ - `scrollbar.css` + `<html class="hdk-scroll">` — every scroller.
134
+ - `installTitleTooltips()` (`/title-tooltips`) — every `title=` as the card tooltip.
135
+ Opt out: `data-native-title`.
136
+ - `installSelectMenus()` (`/select-menus`) — the themed list for every native `<select>`;
137
+ touch keeps the phone's picker. Opt out: `data-native-select`.
138
+
139
+ Both sheets are zero-specificity **and** sit in `@layer hdk-fallbacks`, so ANY rule an app writes
140
+ wins, including Tailwind utilities, which live in layers and would otherwise lose to an unlayered
141
+ rule of any specificity. That only holds if the page declares the layer first:
142
+ `@layer hdk-fallbacks, base, theme, utilities;` — hadoku_site's `global.css` does.
143
+
144
+ ## Usage
145
+
146
+ ### Unified app header (recommended for hadoku.me apps)
147
+
148
+ Every app under `hadoku.me` should render the same top bar. `AppHeader` gives
149
+ you the enforced layout — title left, `[ status? · ThemePicker · Settings ]`
150
+ right — and injects the hadoku-wired `ConnectedSettings` gear for you. Pass your
151
+ own `ConnectedThemePicker` (theme state stays app-side):
152
+
153
+ ```tsx
154
+ import { AppHeader, ConnectedThemePicker } from '@wolffm/task-ui-components'
155
+ import { useTheme, THEME_FAMILIES, THEME_ICON_MAP } from '@wolffm/themes'
156
+ import '@wolffm/themes/style.css'
157
+ import '@wolffm/task-ui-components/theme-picker.css'
158
+ import '@wolffm/task-ui-components/app-header.css'
159
+
160
+ function App() {
161
+ const { theme, setTheme } = useTheme()
162
+ return (
163
+ <>
164
+ <AppHeader
165
+ title="Print Tool"
166
+ themePicker={
167
+ <ConnectedThemePicker
168
+ themeFamilies={THEME_FAMILIES}
169
+ currentTheme={theme}
170
+ onThemeChange={setTheme}
171
+ getThemeIcon={t => {
172
+ const Icon = THEME_ICON_MAP[t as keyof typeof THEME_ICON_MAP]
173
+ return Icon ? <Icon /> : null
174
+ }}
175
+ />
176
+ }
177
+ />
178
+ {/* app body — the app's own concern */}
179
+ </>
180
+ )
181
+ }
182
+ ```
183
+
184
+ `ConnectedSettings` self-resolves the caller's tier/name via `/session/whoami`
185
+ and hits the edge-router's `/session/*` + `/prefs/api/v1/content-level` routes.
186
+ Off `hadoku.me` (e.g. Storybook) every call resolves to a safe default and the
187
+ gear renders a read-only shell — it never throws. Pass `settingsProps={{ userType,
188
+ name }}` when the app already knows the identity to skip the extra fetch, or
189
+ `showSettings={false}` to omit the gear.
190
+
191
+ The theme picker trigger and the gear are both `.app-header__icon-btn` (32px round,
192
+ one `:focus-visible` ring), defined once in `icon-btn.css`. A link in the actions row
193
+ uses the same class and nothing else — it stays `--color-text` visited or not, with no
194
+ underline, even under a host page's `a:visited` rule:
195
+
196
+ ```tsx
197
+ <a className="app-header__icon-btn" href="https://discord.gg/…" aria-label="Discord">
198
+ <Icon name="chat" size={16} />
199
+ </a>
200
+ ```
201
+
202
+ The title truncates with an ellipsis and the actions never shrink, so the bar holds a
203
+ status and four icons at 320px (`e2e/header-icon-btn.spec.ts`).
204
+
205
+ ### Quick Start with @wolffm/themes (Recommended)
206
+
207
+ The easiest way to use the ThemePicker is with the `@wolffm/themes` package:
208
+
209
+ ```tsx
210
+ import { ConnectedThemePicker } from '@wolffm/task-ui-components'
211
+ import { useTheme, THEME_FAMILIES, THEME_ICON_MAP } from '@wolffm/themes'
212
+ import '@wolffm/themes/style.css'
213
+
214
+ function App() {
215
+ const { theme, setTheme } = useTheme()
216
+
217
+ return (
218
+ <ConnectedThemePicker
219
+ themeFamilies={THEME_FAMILIES}
220
+ currentTheme={theme}
221
+ onThemeChange={setTheme}
222
+ getThemeIcon={theme => {
223
+ const Icon = THEME_ICON_MAP[theme]
224
+ return Icon ? <Icon /> : null
225
+ }}
226
+ />
227
+ )
228
+ }
229
+ ```
230
+
231
+ **Note:** CSS is automatically imported by components - no manual CSS imports needed!
232
+
233
+ ### Using ConnectedThemePicker (Stateful)
234
+
235
+ `ConnectedThemePicker` manages its own open/close state:
236
+
237
+ ```tsx
238
+ import { ConnectedThemePicker } from '@wolffm/task-ui-components'
239
+
240
+ function App() {
241
+ const [theme, setTheme] = useState('light')
242
+
243
+ return (
244
+ <ConnectedThemePicker
245
+ themeFamilies={THEME_FAMILIES}
246
+ currentTheme={theme}
247
+ onThemeChange={setTheme}
248
+ />
249
+ )
250
+ }
251
+ ```
252
+
253
+ ### Using ThemePicker (Manual State)
254
+
255
+ For full control over the picker's state:
256
+
257
+ ```tsx
258
+ import { ThemePicker, SunIcon, MoonIcon } from '@wolffm/task-ui-components'
259
+
260
+ const THEME_FAMILIES = [
261
+ {
262
+ lightIcon: <SunIcon />,
263
+ darkIcon: <MoonIcon />,
264
+ lightTheme: 'light',
265
+ darkTheme: 'dark',
266
+ lightLabel: 'Light',
267
+ darkLabel: 'Dark'
268
+ }
269
+ // ... more themes
270
+ ]
271
+
272
+ function App() {
273
+ const [theme, setTheme] = useState('light')
274
+ const [showPicker, setShowPicker] = useState(false)
275
+
276
+ return (
277
+ <ThemePicker
278
+ currentTheme={theme}
279
+ isOpen={showPicker}
280
+ themeFamilies={THEME_FAMILIES}
281
+ onThemeChange={setTheme}
282
+ onToggle={() => setShowPicker(!showPicker)}
283
+ onSettingsClick={() => console.log('Settings clicked')}
284
+ />
285
+ )
286
+ }
287
+ ```
288
+
289
+ ### With Automatic Fallback Icons (No Icons Needed!)
290
+
291
+ Perfect when using your own theme system:
292
+
293
+ ```tsx
294
+ import { ThemePicker } from '@wolffm/task-ui-components'
295
+ import './my-custom-style.css' // Your own theme CSS
296
+
297
+ const THEME_FAMILIES = [
298
+ {
299
+ // Icons will be automatically assigned (CircleIcon for first theme)
300
+ lightTheme: 'ocean-light',
301
+ darkTheme: 'ocean-dark',
302
+ lightLabel: 'Ocean Light',
303
+ darkLabel: 'Ocean Dark'
304
+ },
305
+ {
306
+ // SquareIcon for second theme
307
+ lightTheme: 'forest-light',
308
+ darkTheme: 'forest-dark',
309
+ lightLabel: 'Forest Light',
310
+ darkLabel: 'Forest Dark'
311
+ },
312
+ {
313
+ // TriangleIcon for third theme
314
+ lightTheme: 'sunset-light',
315
+ darkTheme: 'sunset-dark',
316
+ lightLabel: 'Sunset Light',
317
+ darkLabel: 'Sunset Dark'
318
+ }
319
+ ]
320
+
321
+ function App() {
322
+ const [theme, setTheme] = useState('ocean-light')
323
+ const [showPicker, setShowPicker] = useState(false)
324
+
325
+ return (
326
+ <ThemePicker
327
+ currentTheme={theme}
328
+ isOpen={showPicker}
329
+ themeFamilies={THEME_FAMILIES}
330
+ onThemeChange={setTheme}
331
+ onToggle={() => setShowPicker(!showPicker)}
332
+ />
333
+ )
334
+ }
335
+ ```
336
+
337
+ ## CSS Variables
338
+
339
+ The ThemePicker requires these CSS variables to be defined by your theme system:
340
+
341
+ ```css
342
+ :root {
343
+ --color-text: #000000;
344
+ --color-primary: #3b82f6;
345
+ --color-primary-hover: rgba(59, 130, 246, 0.1);
346
+ --color-primary-bg: rgba(59, 130, 246, 0.05);
347
+ --color-bg-card: #ffffff;
348
+ --color-text-secondary: #64748b;
349
+ --shadow-focus-sm: 0 0 0 2px rgba(59, 130, 246, 0.25);
350
+ --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.1);
351
+ --shadow-modal: 0 8px 24px rgba(0, 0, 0, 0.15);
352
+ --transition-fast: 0.15s ease;
353
+ --spacing-sm: 8px;
354
+ --spacing-md: 12px;
355
+ --spacing-lg: 16px;
356
+ --font-size-lg: 18px;
357
+ --font-weight-semibold: 600;
358
+ }
359
+ ```
360
+
361
+ ## Icons
362
+
363
+ ### Theme-Specific Icons
364
+
365
+ Available themed icons for specific aesthetics:
366
+
367
+ - `SunIcon` - Sun/light mode icon
368
+ - `MoonIcon` - Moon/dark mode icon
369
+ - `StrawberryIcon` - Strawberry fruit icon
370
+ - `WaveIcon` - Ocean wave icon
371
+ - `ZapIcon` - Lightning bolt icon (cyberpunk)
372
+ - `CoffeeIcon` - Coffee cup icon
373
+ - `FlowerIcon` - Flower/lavender icon
374
+ - `HeartIcon` - Heart icon (pink theme)
375
+ - `LeafIcon` - Leaf icon (nature theme)
376
+ - `SettingsIcon` - Gear/settings icon
377
+ - `TagIcon` - Tag/label icon
378
+ - `SpaIcon` - Hot spring/spa icon (izakaya theme)
379
+
380
+ ### Generic/Fallback Icons
381
+
382
+ When you don't have specific icons for your themes, use generic shape icons for visual differentiation:
383
+
384
+ - `CircleIcon` - Simple circle
385
+ - `SquareIcon` - Rounded square
386
+ - `TriangleIcon` - Triangle
387
+ - `DiamondIcon` - Diamond shape
388
+ - `StarIcon` - Star shape
389
+ - `HexagonIcon` - Hexagon
390
+ - `PentagonIcon` - Pentagon
391
+ - `OctagonIcon` - Octagon
392
+
393
+ **Automatic Fallback:**
394
+
395
+ The `ThemePicker` component automatically uses fallback icons if you don't provide any:
396
+
397
+ ```tsx
398
+ import { ThemePicker } from '@wolffm/task-ui-components'
399
+
400
+ const THEME_FAMILIES = [
401
+ {
402
+ // No icons provided - will use CircleIcon automatically
403
+ lightTheme: 'custom-light',
404
+ darkTheme: 'custom-dark',
405
+ lightLabel: 'Custom Light',
406
+ darkLabel: 'Custom Dark'
407
+ },
408
+ {
409
+ // No icons provided - will use SquareIcon automatically
410
+ lightTheme: 'another-light',
411
+ darkTheme: 'another-dark',
412
+ lightLabel: 'Another Light',
413
+ darkLabel: 'Another Dark'
414
+ }
415
+ ]
416
+ ```
417
+
418
+ **Manual Fallback Usage:**
419
+
420
+ You can also manually use fallback icons:
421
+
422
+ ```tsx
423
+ import { getFallbackIcon, FALLBACK_ICONS } from '@wolffm/task-ui-components'
424
+
425
+ // Get icon by index (0-7, repeats after 8)
426
+ const icon = getFallbackIcon(0) // Returns CircleIcon
427
+ const icon2 = getFallbackIcon(5) // Returns HexagonIcon
428
+
429
+ // Or use the array directly
430
+ const CircleIconComponent = FALLBACK_ICONS[0]
431
+ const iconElement = <CircleIconComponent />
432
+ ```
433
+
434
+ **Fallback Icon Sequence:**
435
+
436
+ When icons are omitted from `ThemeFamily`, they're assigned in this order:
437
+
438
+ 1. `CircleIcon` (index 0)
439
+ 2. `SquareIcon` (index 1)
440
+ 3. `TriangleIcon` (index 2)
441
+ 4. `DiamondIcon` (index 3)
442
+ 5. `StarIcon` (index 4)
443
+ 6. `HexagonIcon` (index 5)
444
+ 7. `PentagonIcon` (index 6)
445
+ 8. `OctagonIcon` (index 7)
446
+ 9. Pattern repeats from `CircleIcon` for 9+ themes
447
+
448
+ This ensures each theme gets a visually distinct icon while maintaining consistency across app reloads.
449
+
450
+ ## Logger
451
+
452
+ Production-safe logging utility that respects development mode and admin status.
453
+
454
+ ### Basic Usage
455
+
456
+ ```typescript
457
+ import { logger } from '@wolffm/task-ui-components'
458
+
459
+ // Informational logs (dev/admin only)
460
+ logger.info('[Component] Loading data', { userId: '123' })
461
+
462
+ // Debug logs (dev/admin only)
463
+ logger.debug('[Component] State updated', { isOpen: true, count: 5 })
464
+
465
+ // Warnings (always shown)
466
+ logger.warn('[Component] Deprecated API used', { feature: 'old-api' })
467
+
468
+ // Errors (always shown)
469
+ logger.error('[Component] Failed to save', {
470
+ error: err instanceof Error ? err.message : String(err),
471
+ taskId: 'task-123'
472
+ })
473
+ ```
474
+
475
+ ### Advanced Features
476
+
477
+ **Enable admin mode** for production debugging:
478
+
479
+ ```typescript
480
+ // After user authentication
481
+ if (userIsAdmin) {
482
+ logger.setAdminStatus(true)
483
+ }
484
+ ```
485
+
486
+ **Specialized log methods**:
487
+
488
+ ```typescript
489
+ // Component lifecycle logging
490
+ logger.component('mount', 'ThemePicker', { props })
491
+ logger.component('update', 'TaskList', { taskCount: 42 })
492
+
493
+ // Theme changes
494
+ logger.theme('strawberry-dark', { source: 'user-preference' })
495
+ logger.theme('ocean-light', { source: 'system' })
496
+ ```
497
+
498
+ ### Best Practices
499
+
500
+ **Always use structured logging** with a message and context object:
501
+
502
+ ```typescript
503
+ // ✅ Good - Structured with context
504
+ logger.info('[TaskList] Loading tasks', { boardId, filter })
505
+
506
+ // ❌ Bad - String concatenation
507
+ logger.info(`Loading tasks for board ${boardId}`)
508
+
509
+ // ✅ Good - Proper error extraction
510
+ logger.error('[API] Request failed', {
511
+ error: err instanceof Error ? err.message : String(err),
512
+ endpoint: '/api/tasks'
513
+ })
514
+
515
+ // ❌ Bad - Logging error object directly
516
+ logger.error('Request failed:', err)
517
+ ```
518
+
519
+ **Use component prefixes** for easy filtering:
520
+
521
+ ```typescript
522
+ // Component-based prefixes
523
+ logger.info('[TaskList] Mounted')
524
+ logger.info('[ThemePicker] Theme changed')
525
+
526
+ // Feature-based prefixes
527
+ logger.info('[API] Request completed')
528
+ logger.info('[Auth] User logged in')
529
+ ```
530
+
531
+ ### Behavior
532
+
533
+ | Mode | Development | Production (Admin) | Production (User) |
534
+ | -------------------- | ----------- | ------------------ | ----------------- |
535
+ | `logger.info()` | ✅ | ✅ | ❌ |
536
+ | `logger.debug()` | ✅ | ✅ | ❌ |
537
+ | `logger.component()` | ✅ | ✅ | ❌ |
538
+ | `logger.theme()` | ✅ | ✅ | ❌ |
539
+ | `logger.warn()` | ✅ | ✅ | ✅ |
540
+ | `logger.error()` | ✅ | ✅ | ✅ |
541
+
542
+ **Development Detection:**
543
+
544
+ - Automatically enabled on `localhost` or `127.0.0.1`
545
+ - Or when `import.meta.env.DEV` is true
546
+
547
+ **Admin Mode:**
548
+
549
+ - Call `logger.setAdminStatus(true)` after authentication
550
+ - Enables all logs in production for debugging
551
+ - Useful for troubleshooting production issues
552
+
553
+ ## Props
554
+
555
+ ### ThemePicker
556
+
557
+ | Prop | Type | Required | Description |
558
+ | ----------------- | ------------------------------ | -------- | ----------------------------------------------- |
559
+ | `currentTheme` | `string` | ✅ | Currently active theme name |
560
+ | `isOpen` | `boolean` | ✅ | Whether the picker dropdown is visible |
561
+ | `themeFamilies` | `ThemeFamily[]` | ✅ | Array of theme families to display |
562
+ | `onThemeChange` | `(theme: string) => void` | ✅ | Callback when theme is selected |
563
+ | `onToggle` | `() => void` | ✅ | Callback to toggle picker visibility |
564
+ | `onSettingsClick` | `() => void` | ❌ | Optional callback for settings button |
565
+ | `getThemeIcon` | `(theme: string) => ReactNode` | ❌ | Optional function to get icon for current theme |
566
+ | `className` | `string` | ❌ | Optional CSS class for container |
567
+
568
+ ### ThemeFamily
569
+
570
+ | Prop | Type | Required | Description |
571
+ | ------------ | ----------- | -------- | ------------------------------------------------- |
572
+ | `lightIcon` | `ReactNode` | ❌ | Icon for light variant (auto-fallback if omitted) |
573
+ | `darkIcon` | `ReactNode` | ❌ | Icon for dark variant (auto-fallback if omitted) |
574
+ | `lightTheme` | `string` | ✅ | Theme name for light variant |
575
+ | `darkTheme` | `string` | ✅ | Theme name for dark variant |
576
+ | `lightLabel` | `string` | ✅ | Label for light variant (tooltip) |
577
+ | `darkLabel` | `string` | ✅ | Label for dark variant (tooltip) |
578
+
579
+ ## License
580
+
581
+ MIT
582
+
583
+ ---
584
+
585
+ ## Additional Components Documentation
586
+
587
+ ### Modal (API Reference)
588
+
589
+ Generic modal dialog with keyboard shortcuts and customizable actions.
590
+
591
+ **Features:**
592
+
593
+ - `role="dialog"` + `aria-modal` + `aria-labelledby` wired to the title, and
594
+ focus moved into the dialog on open
595
+ - Escape closes from anywhere in the dialog, with or without an input
596
+ - Enter confirms from the optional input field
597
+ - Configurable buttons (show/hide cancel/confirm), or replace the whole action
598
+ row via `actions` for flows the fixed pair can't express
599
+ - Danger variant for destructive actions
600
+ - Custom class names for styling
601
+
602
+ **Styles ship with the package** — import them once:
603
+
604
+ ```ts
605
+ import '@wolffm/task-ui-components/modal.css'
606
+ ```
607
+
608
+ **Usage:**
609
+
610
+ ```tsx
611
+ import { Modal } from '@wolffm/task-ui-components'
612
+
613
+ function MyComponent() {
614
+ const [isOpen, setIsOpen] = useState(false)
615
+ const [value, setValue] = useState('')
616
+
617
+ return (
618
+ <Modal
619
+ isOpen={isOpen}
620
+ title="Create Item"
621
+ onClose={() => setIsOpen(false)}
622
+ onConfirm={async () => {
623
+ await createItem(value)
624
+ setIsOpen(false)
625
+ }}
626
+ inputValue={value}
627
+ onInputChange={setValue}
628
+ inputPlaceholder="Enter name..."
629
+ confirmLabel="Create"
630
+ confirmDisabled={!value.trim()}
631
+ >
632
+ <p>Enter a name for your new item.</p>
633
+ </Modal>
634
+ )
635
+ }
636
+ ```
637
+
638
+ **Props:**
639
+
640
+ | Prop | Type | Required | Description |
641
+ | -------------------- | ------------------------------- | -------- | -------------------------------------- |
642
+ | \`isOpen\` | \`boolean\` | ✅ | Whether modal is visible |
643
+ | \`title\` | \`string\` | ✅ | Modal title |
644
+ | \`onClose\` | \`() => void\` | ✅ | Close callback |
645
+ | \`onConfirm\` | \`() => void \| Promise<void>\` | ❌ | Confirm callback |
646
+ | \`children\` | \`ReactNode\` | ❌ | Modal content |
647
+ | \`inputValue\` | \`string\` | ❌ | Input field value |
648
+ | \`onInputChange\` | \`(value: string) => void\` | ❌ | Input change callback |
649
+ | \`inputPlaceholder\` | \`string\` | ❌ | Input placeholder |
650
+ | \`confirmLabel\` | \`string\` | ❌ | Confirm button text (default: Confirm) |
651
+ | \`cancelLabel\` | \`string\` | ❌ | Cancel button text (default: Cancel) |
652
+ | \`confirmDisabled\` | \`boolean\` | ❌ | Disable confirm button |
653
+ | \`confirmDanger\` | \`boolean\` | ❌ | Danger styling for confirm |
654
+ | \`showCancel\` | \`boolean\` | ❌ | Show cancel button (default: true) |
655
+ | \`showConfirm\` | \`boolean\` | ❌ | Show confirm button (default: true) |
656
+ | \`className\` | \`string\` | ❌ | Custom CSS class |
657
+ | \`overlayClassName\` | \`string\` | ❌ | Custom overlay CSS class |
658
+ | \`actions\` | \`ReactNode\` | ❌ | Replaces the whole action row |
659
+ | \`lead\` | \`ReactNode\` | ❌ | Rendered left of the title |
660
+
661
+ `actions` is checked against `undefined`, not with `??` — passing `null`
662
+ renders **no** action row (a progress step, a viewer), while omitting it keeps
663
+ the default Cancel/Confirm pair.
664
+
665
+ ---
666
+
667
+ ### ContextMenu (API Reference)
668
+
669
+ Position-based context menu for right-click or contextual actions.
670
+
671
+ **Features:**
672
+
673
+ - Position-based rendering (x, y coordinates)
674
+ - Configurable menu items
675
+ - Danger state for destructive items
676
+ - Disabled state support
677
+ - Fixed positioning with z-index
678
+
679
+ **Usage:**
680
+
681
+ ```tsx
682
+ import { ContextMenu, type ContextMenuItem } from '@wolffm/task-ui-components'
683
+
684
+ function MyComponent() {
685
+ const [menu, setMenu] = useState<{ x: number; y: number } | null>(null)
686
+
687
+ const items: ContextMenuItem[] = [
688
+ {
689
+ label: 'Edit',
690
+ onClick: () => handleEdit()
691
+ },
692
+ {
693
+ label: 'Delete',
694
+ onClick: () => handleDelete(),
695
+ isDanger: true
696
+ }
697
+ ]
698
+
699
+ return (
700
+ <div
701
+ onContextMenu={e => {
702
+ e.preventDefault()
703
+ setMenu({ x: e.clientX, y: e.clientY })
704
+ }}
705
+ >
706
+ <ContextMenu
707
+ isOpen={!!menu}
708
+ x={menu?.x || 0}
709
+ y={menu?.y || 0}
710
+ items={items}
711
+ className="my-context-menu"
712
+ />
713
+ </div>
714
+ )
715
+ }
716
+ ```
717
+
718
+ **Props:**
719
+
720
+ | Prop | Type | Required | Description |
721
+ | ------------- | --------------------- | -------- | ----------------------- |
722
+ | \`isOpen\` | \`boolean\` | ✅ | Whether menu is visible |
723
+ | \`x\` | \`number\` | ✅ | X position (pixels) |
724
+ | \`y\` | \`number\` | ✅ | Y position (pixels) |
725
+ | \`items\` | \`ContextMenuItem[]\` | ✅ | Menu items |
726
+ | \`className\` | \`string\` | ❌ | Custom CSS class |
727
+
728
+ **ContextMenuItem:**
729
+
730
+ | Prop | Type | Required | Description |
731
+ | ------------ | --------------- | --------- | -------------- | ------------- |
732
+ | \`label\` | \`string\` | ✅ | Item label |
733
+ | \`onClick\` | \`() => void \\ | Promise\` | ✅ | Click handler |
734
+ | \`isDanger\` | \`boolean\` | ❌ | Danger styling |
735
+ | \`disabled\` | \`boolean\` | ❌ | Disable item |
736
+
737
+ ---
738
+
739
+ ### LoadingSkeleton (API Reference)
740
+
741
+ Flexible loading skeleton with multiple layout options.
742
+
743
+ **Features:**
744
+
745
+ - Multiple layout options (default, minimal, custom)
746
+ - Dark theme support
747
+ - Customizable content via \`customContent\` prop
748
+
749
+ **Usage:**
750
+
751
+ ```tsx
752
+ import { LoadingSkeleton } from '@wolffm/task-ui-components'
753
+
754
+ function MyComponent() {
755
+ const [isLoading, setIsLoading] = useState(true)
756
+ const isDark = useTheme() === 'dark'
757
+
758
+ if (isLoading) {
759
+ return <LoadingSkeleton isDarkTheme={isDark} layout="minimal" />
760
+ }
761
+
762
+ return <div>Content loaded!</div>
763
+ }
764
+ ```
765
+
766
+ **Props:**
767
+
768
+ | Prop | Type | Required | Description |
769
+ | ----------------- | -------------- | ------------ | ----------------------- | --- | -------------------------------- |
770
+ | \`isDarkTheme\` | \`boolean\` | ❌ | Dark theme mode |
771
+ | \`className\` | \`string\` | ❌ | Custom CSS class |
772
+ | \`layout\` | \`'default' \\ | 'minimal' \\ | 'custom'\` | ❌ | Layout type (default: 'default') |
773
+ | \`customContent\` | \`ReactNode\` | ❌ | Custom skeleton content |
774
+
775
+ ---
776
+
777
+ ### SettingsModal (API Reference)
778
+
779
+ Generic settings dialog with section-based organization.
780
+
781
+ **Features:**
782
+
783
+ - Section-based organization
784
+ - Multiple field types: toggle, text, password, select, button, custom
785
+ - Validation support
786
+ - Description text for fields
787
+
788
+ **Usage:**
789
+
790
+ ```tsx
791
+ import { SettingsModal, type SettingsSection } from '@wolffm/task-ui-components'
792
+
793
+ function MyApp() {
794
+ const [isOpen, setIsOpen] = useState(false)
795
+ const [darkMode, setDarkMode] = useState(false)
796
+ const [language, setLanguage] = useState('en')
797
+
798
+ const sections: SettingsSection[] = [
799
+ {
800
+ id: 'display',
801
+ title: 'Display Settings',
802
+ fields: [
803
+ {
804
+ type: 'toggle',
805
+ id: 'darkMode',
806
+ label: 'Dark Mode',
807
+ description: 'Use dark color scheme',
808
+ value: darkMode,
809
+ onChange: setDarkMode
810
+ },
811
+ {
812
+ type: 'select',
813
+ id: 'language',
814
+ label: 'Language',
815
+ value: language,
816
+ onChange: setLanguage,
817
+ options: [
818
+ { value: 'en', label: 'English' },
819
+ { value: 'es', label: 'Spanish' }
820
+ ]
821
+ }
822
+ ]
823
+ }
824
+ ]
825
+
826
+ return <SettingsModal isOpen={isOpen} sections={sections} onClose={() => setIsOpen(false)} />
827
+ }
828
+ ```
829
+
830
+ **Props:**
831
+
832
+ | Prop | Type | Required | Description |
833
+ | ------------------- | --------------------- | -------- | --------------------------------- |
834
+ | \`isOpen\` | \`boolean\` | ✅ | Whether modal is visible |
835
+ | \`sections\` | \`SettingsSection[]\` | ✅ | Settings sections |
836
+ | \`onClose\` | \`() => void\` | ✅ | Close callback |
837
+ | \`title\` | \`string\` | ❌ | Modal title (default: 'Settings') |
838
+ | \`className\` | \`string\` | ❌ | Custom CSS class |
839
+ | \`showCloseButton\` | \`boolean\` | ❌ | Show close button (default: true) |
840
+
841
+ **Field Types:**
842
+
843
+ ```typescript
844
+ // Toggle field
845
+ {
846
+ type: 'toggle',
847
+ id: string,
848
+ label: string,
849
+ description?: string,
850
+ value: boolean,
851
+ onChange: (value: boolean) => void,
852
+ disabled?: boolean
853
+ }
854
+
855
+ // Text input field
856
+ {
857
+ type: 'text',
858
+ id: string,
859
+ label: string,
860
+ description?: string,
861
+ value: string,
862
+ onChange: (value: string) => void,
863
+ placeholder?: string,
864
+ disabled?: boolean
865
+ }
866
+
867
+ // Password field
868
+ {
869
+ type: 'password',
870
+ id: string,
871
+ label: string,
872
+ description?: string,
873
+ value: string,
874
+ onChange: (value: string) => void,
875
+ placeholder?: string,
876
+ showButton?: boolean,
877
+ buttonLabel?: string,
878
+ onButtonClick?: () => void | Promise<void>,
879
+ buttonDisabled?: boolean,
880
+ error?: string | null,
881
+ autoComplete?: string,
882
+ disabled?: boolean
883
+ }
884
+
885
+ // Select dropdown
886
+ {
887
+ type: 'select',
888
+ id: string,
889
+ label: string,
890
+ description?: string,
891
+ value: string,
892
+ onChange: (value: string) => void,
893
+ options: Array<{ value: string; label: string }>,
894
+ disabled?: boolean
895
+ }
896
+
897
+ // Action button
898
+ {
899
+ type: 'button',
900
+ id: string,
901
+ label?: string,
902
+ description?: string,
903
+ buttonLabel: string,
904
+ onClick: () => void | Promise<void>,
905
+ variant?: 'primary' | 'danger' | 'default',
906
+ disabled?: boolean
907
+ }
908
+
909
+ // Custom component
910
+ {
911
+ type: 'custom',
912
+ id: string,
913
+ label?: string,
914
+ description?: string,
915
+ render: () => ReactNode,
916
+ disabled?: boolean
917
+ }
918
+ ```