@dashforge/tw 0.1.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.swcrc +29 -0
- package/CHANGELOG.md +131 -0
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/dist/components/AppShell/AppShell.d.ts +32 -0
- package/dist/components/AppShell/AppShell.d.ts.map +1 -0
- package/dist/components/AppShell/appShell.types.d.ts +62 -0
- package/dist/components/AppShell/appShell.types.d.ts.map +1 -0
- package/dist/components/AppShell/appShell.variants.d.ts +65 -0
- package/dist/components/AppShell/appShell.variants.d.ts.map +1 -0
- package/dist/components/Autocomplete/Autocomplete.d.ts +32 -0
- package/dist/components/Autocomplete/Autocomplete.d.ts.map +1 -0
- package/dist/components/Autocomplete/autocomplete.types.d.ts +214 -0
- package/dist/components/Autocomplete/autocomplete.types.d.ts.map +1 -0
- package/dist/components/Autocomplete/autocomplete.variants.d.ts +214 -0
- package/dist/components/Autocomplete/autocomplete.variants.d.ts.map +1 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +23 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.d.ts.map +1 -0
- package/dist/components/Breadcrumbs/breadcrumbs.types.d.ts +98 -0
- package/dist/components/Breadcrumbs/breadcrumbs.types.d.ts.map +1 -0
- package/dist/components/Breadcrumbs/breadcrumbs.variants.d.ts +85 -0
- package/dist/components/Breadcrumbs/breadcrumbs.variants.d.ts.map +1 -0
- package/dist/components/Button/Button.d.ts +44 -0
- package/dist/components/Button/Button.d.ts.map +1 -0
- package/dist/components/Button/button.types.d.ts +66 -0
- package/dist/components/Button/button.types.d.ts.map +1 -0
- package/dist/components/Button/button.variants.d.ts +104 -0
- package/dist/components/Button/button.variants.d.ts.map +1 -0
- package/dist/components/Checkbox/Checkbox.d.ts +31 -0
- package/dist/components/Checkbox/Checkbox.d.ts.map +1 -0
- package/dist/components/Checkbox/checkbox.types.d.ts +86 -0
- package/dist/components/Checkbox/checkbox.types.d.ts.map +1 -0
- package/dist/components/Checkbox/checkbox.variants.d.ts +109 -0
- package/dist/components/Checkbox/checkbox.variants.d.ts.map +1 -0
- package/dist/components/ConfirmDialog/ConfirmDialog.d.ts +38 -0
- package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -0
- package/dist/components/ConfirmDialog/confirmDialog.types.d.ts +80 -0
- package/dist/components/ConfirmDialog/confirmDialog.types.d.ts.map +1 -0
- package/dist/components/ConfirmDialog/confirmDialog.variants.d.ts +90 -0
- package/dist/components/ConfirmDialog/confirmDialog.variants.d.ts.map +1 -0
- package/dist/components/DateTimePicker/DateTimePicker.d.ts +49 -0
- package/dist/components/DateTimePicker/DateTimePicker.d.ts.map +1 -0
- package/dist/components/DateTimePicker/dateTimePicker.types.d.ts +95 -0
- package/dist/components/DateTimePicker/dateTimePicker.types.d.ts.map +1 -0
- package/dist/components/DateTimePicker/dateTimePicker.variants.d.ts +165 -0
- package/dist/components/DateTimePicker/dateTimePicker.variants.d.ts.map +1 -0
- package/dist/components/LeftNav/LeftNav.d.ts +34 -0
- package/dist/components/LeftNav/LeftNav.d.ts.map +1 -0
- package/dist/components/LeftNav/leftNav.types.d.ts +136 -0
- package/dist/components/LeftNav/leftNav.types.d.ts.map +1 -0
- package/dist/components/LeftNav/leftNav.variants.d.ts +143 -0
- package/dist/components/LeftNav/leftNav.variants.d.ts.map +1 -0
- package/dist/components/NumberField/NumberField.d.ts +18 -0
- package/dist/components/NumberField/NumberField.d.ts.map +1 -0
- package/dist/components/NumberField/numberField.types.d.ts +79 -0
- package/dist/components/NumberField/numberField.types.d.ts.map +1 -0
- package/dist/components/NumberField/numberField.variants.d.ts +169 -0
- package/dist/components/NumberField/numberField.variants.d.ts.map +1 -0
- package/dist/components/OTPField/OTPField.d.ts +25 -0
- package/dist/components/OTPField/OTPField.d.ts.map +1 -0
- package/dist/components/OTPField/otpField.types.d.ts +74 -0
- package/dist/components/OTPField/otpField.types.d.ts.map +1 -0
- package/dist/components/OTPField/otpField.variants.d.ts +105 -0
- package/dist/components/OTPField/otpField.variants.d.ts.map +1 -0
- package/dist/components/RadioGroup/RadioGroup.d.ts +35 -0
- package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -0
- package/dist/components/RadioGroup/radioGroup.types.d.ts +103 -0
- package/dist/components/RadioGroup/radioGroup.types.d.ts.map +1 -0
- package/dist/components/RadioGroup/radioGroup.variants.d.ts +166 -0
- package/dist/components/RadioGroup/radioGroup.variants.d.ts.map +1 -0
- package/dist/components/Snackbar/Snackbar.d.ts +37 -0
- package/dist/components/Snackbar/Snackbar.d.ts.map +1 -0
- package/dist/components/Snackbar/snackbar.types.d.ts +86 -0
- package/dist/components/Snackbar/snackbar.types.d.ts.map +1 -0
- package/dist/components/Snackbar/snackbar.variants.d.ts +153 -0
- package/dist/components/Snackbar/snackbar.variants.d.ts.map +1 -0
- package/dist/components/Switch/Switch.d.ts +15 -0
- package/dist/components/Switch/Switch.d.ts.map +1 -0
- package/dist/components/Switch/switch.types.d.ts +47 -0
- package/dist/components/Switch/switch.types.d.ts.map +1 -0
- package/dist/components/Switch/switch.variants.d.ts +105 -0
- package/dist/components/Switch/switch.variants.d.ts.map +1 -0
- package/dist/components/TextField/TextField.d.ts +25 -0
- package/dist/components/TextField/TextField.d.ts.map +1 -0
- package/dist/components/TextField/textField.types.d.ts +72 -0
- package/dist/components/TextField/textField.types.d.ts.map +1 -0
- package/dist/components/TextField/textField.variants.d.ts +160 -0
- package/dist/components/TextField/textField.variants.d.ts.map +1 -0
- package/dist/components/Textarea/Textarea.d.ts +16 -0
- package/dist/components/Textarea/Textarea.d.ts.map +1 -0
- package/dist/components/Textarea/textarea.types.d.ts +63 -0
- package/dist/components/Textarea/textarea.types.d.ts.map +1 -0
- package/dist/components/Textarea/textarea.variants.d.ts +197 -0
- package/dist/components/Textarea/textarea.variants.d.ts.map +1 -0
- package/dist/components/TopBar/TopBar.d.ts +33 -0
- package/dist/components/TopBar/TopBar.d.ts.map +1 -0
- package/dist/components/TopBar/topBar.types.d.ts +49 -0
- package/dist/components/TopBar/topBar.types.d.ts.map +1 -0
- package/dist/components/TopBar/topBar.variants.d.ts +79 -0
- package/dist/components/TopBar/topBar.variants.d.ts.map +1 -0
- package/dist/components/_shared/resolveValidationState.d.ts +42 -0
- package/dist/components/_shared/resolveValidationState.d.ts.map +1 -0
- package/dist/hooks/useAccessState.d.ts +37 -0
- package/dist/hooks/useAccessState.d.ts.map +1 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +4958 -0
- package/dist/src/components/AppShell/AppShell.d.ts +32 -0
- package/dist/src/components/AppShell/AppShell.d.ts.map +1 -0
- package/dist/src/components/AppShell/appShell.types.d.ts +62 -0
- package/dist/src/components/AppShell/appShell.types.d.ts.map +1 -0
- package/dist/src/components/AppShell/appShell.variants.d.ts +65 -0
- package/dist/src/components/AppShell/appShell.variants.d.ts.map +1 -0
- package/dist/src/components/Autocomplete/Autocomplete.d.ts +32 -0
- package/dist/src/components/Autocomplete/Autocomplete.d.ts.map +1 -0
- package/dist/src/components/Autocomplete/autocomplete.types.d.ts +214 -0
- package/dist/src/components/Autocomplete/autocomplete.types.d.ts.map +1 -0
- package/dist/src/components/Autocomplete/autocomplete.variants.d.ts +214 -0
- package/dist/src/components/Autocomplete/autocomplete.variants.d.ts.map +1 -0
- package/dist/src/components/Breadcrumbs/Breadcrumbs.d.ts +23 -0
- package/dist/src/components/Breadcrumbs/Breadcrumbs.d.ts.map +1 -0
- package/dist/src/components/Breadcrumbs/breadcrumbs.types.d.ts +98 -0
- package/dist/src/components/Breadcrumbs/breadcrumbs.types.d.ts.map +1 -0
- package/dist/src/components/Breadcrumbs/breadcrumbs.variants.d.ts +85 -0
- package/dist/src/components/Breadcrumbs/breadcrumbs.variants.d.ts.map +1 -0
- package/dist/src/components/Button/Button.d.ts +44 -0
- package/dist/src/components/Button/Button.d.ts.map +1 -0
- package/dist/src/components/Button/button.types.d.ts +66 -0
- package/dist/src/components/Button/button.types.d.ts.map +1 -0
- package/dist/src/components/Button/button.variants.d.ts +104 -0
- package/dist/src/components/Button/button.variants.d.ts.map +1 -0
- package/dist/src/components/Checkbox/Checkbox.d.ts +31 -0
- package/dist/src/components/Checkbox/Checkbox.d.ts.map +1 -0
- package/dist/src/components/Checkbox/checkbox.types.d.ts +86 -0
- package/dist/src/components/Checkbox/checkbox.types.d.ts.map +1 -0
- package/dist/src/components/Checkbox/checkbox.variants.d.ts +109 -0
- package/dist/src/components/Checkbox/checkbox.variants.d.ts.map +1 -0
- package/dist/src/components/ConfirmDialog/ConfirmDialog.d.ts +38 -0
- package/dist/src/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -0
- package/dist/src/components/ConfirmDialog/confirmDialog.types.d.ts +80 -0
- package/dist/src/components/ConfirmDialog/confirmDialog.types.d.ts.map +1 -0
- package/dist/src/components/ConfirmDialog/confirmDialog.variants.d.ts +90 -0
- package/dist/src/components/ConfirmDialog/confirmDialog.variants.d.ts.map +1 -0
- package/dist/src/components/DateTimePicker/DateTimePicker.d.ts +49 -0
- package/dist/src/components/DateTimePicker/DateTimePicker.d.ts.map +1 -0
- package/dist/src/components/DateTimePicker/dateTimePicker.types.d.ts +95 -0
- package/dist/src/components/DateTimePicker/dateTimePicker.types.d.ts.map +1 -0
- package/dist/src/components/DateTimePicker/dateTimePicker.variants.d.ts +165 -0
- package/dist/src/components/DateTimePicker/dateTimePicker.variants.d.ts.map +1 -0
- package/dist/src/components/LeftNav/LeftNav.d.ts +34 -0
- package/dist/src/components/LeftNav/LeftNav.d.ts.map +1 -0
- package/dist/src/components/LeftNav/leftNav.types.d.ts +136 -0
- package/dist/src/components/LeftNav/leftNav.types.d.ts.map +1 -0
- package/dist/src/components/LeftNav/leftNav.variants.d.ts +143 -0
- package/dist/src/components/LeftNav/leftNav.variants.d.ts.map +1 -0
- package/dist/src/components/NumberField/NumberField.d.ts +18 -0
- package/dist/src/components/NumberField/NumberField.d.ts.map +1 -0
- package/dist/src/components/NumberField/numberField.types.d.ts +79 -0
- package/dist/src/components/NumberField/numberField.types.d.ts.map +1 -0
- package/dist/src/components/NumberField/numberField.variants.d.ts +169 -0
- package/dist/src/components/NumberField/numberField.variants.d.ts.map +1 -0
- package/dist/src/components/OTPField/OTPField.d.ts +25 -0
- package/dist/src/components/OTPField/OTPField.d.ts.map +1 -0
- package/dist/src/components/OTPField/otpField.types.d.ts +74 -0
- package/dist/src/components/OTPField/otpField.types.d.ts.map +1 -0
- package/dist/src/components/OTPField/otpField.variants.d.ts +105 -0
- package/dist/src/components/OTPField/otpField.variants.d.ts.map +1 -0
- package/dist/src/components/RadioGroup/RadioGroup.d.ts +35 -0
- package/dist/src/components/RadioGroup/RadioGroup.d.ts.map +1 -0
- package/dist/src/components/RadioGroup/radioGroup.types.d.ts +103 -0
- package/dist/src/components/RadioGroup/radioGroup.types.d.ts.map +1 -0
- package/dist/src/components/RadioGroup/radioGroup.variants.d.ts +166 -0
- package/dist/src/components/RadioGroup/radioGroup.variants.d.ts.map +1 -0
- package/dist/src/components/Snackbar/Snackbar.d.ts +37 -0
- package/dist/src/components/Snackbar/Snackbar.d.ts.map +1 -0
- package/dist/src/components/Snackbar/snackbar.types.d.ts +86 -0
- package/dist/src/components/Snackbar/snackbar.types.d.ts.map +1 -0
- package/dist/src/components/Snackbar/snackbar.variants.d.ts +153 -0
- package/dist/src/components/Snackbar/snackbar.variants.d.ts.map +1 -0
- package/dist/src/components/Switch/Switch.d.ts +15 -0
- package/dist/src/components/Switch/Switch.d.ts.map +1 -0
- package/dist/src/components/Switch/switch.types.d.ts +47 -0
- package/dist/src/components/Switch/switch.types.d.ts.map +1 -0
- package/dist/src/components/Switch/switch.variants.d.ts +105 -0
- package/dist/src/components/Switch/switch.variants.d.ts.map +1 -0
- package/dist/src/components/TextField/TextField.d.ts +25 -0
- package/dist/src/components/TextField/TextField.d.ts.map +1 -0
- package/dist/src/components/TextField/textField.types.d.ts +72 -0
- package/dist/src/components/TextField/textField.types.d.ts.map +1 -0
- package/dist/src/components/TextField/textField.variants.d.ts +160 -0
- package/dist/src/components/TextField/textField.variants.d.ts.map +1 -0
- package/dist/src/components/Textarea/Textarea.d.ts +16 -0
- package/dist/src/components/Textarea/Textarea.d.ts.map +1 -0
- package/dist/src/components/Textarea/textarea.types.d.ts +63 -0
- package/dist/src/components/Textarea/textarea.types.d.ts.map +1 -0
- package/dist/src/components/Textarea/textarea.variants.d.ts +197 -0
- package/dist/src/components/Textarea/textarea.variants.d.ts.map +1 -0
- package/dist/src/components/TopBar/TopBar.d.ts +33 -0
- package/dist/src/components/TopBar/TopBar.d.ts.map +1 -0
- package/dist/src/components/TopBar/topBar.types.d.ts +49 -0
- package/dist/src/components/TopBar/topBar.types.d.ts.map +1 -0
- package/dist/src/components/TopBar/topBar.variants.d.ts +79 -0
- package/dist/src/components/TopBar/topBar.variants.d.ts.map +1 -0
- package/dist/src/components/_shared/resolveValidationState.d.ts +42 -0
- package/dist/src/components/_shared/resolveValidationState.d.ts.map +1 -0
- package/dist/src/hooks/useAccessState.d.ts +37 -0
- package/dist/src/hooks/useAccessState.d.ts.map +1 -0
- package/dist/src/index.d.ts +82 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/utils/cn.d.ts +26 -0
- package/dist/src/utils/cn.d.ts.map +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/utils/cn.d.ts +26 -0
- package/dist/utils/cn.d.ts.map +1 -0
- package/eslint.config.mjs +47 -0
- package/package.json +37 -0
- package/project.json +15 -0
- package/rollup.config.cjs +16 -0
- package/src/components/AppShell/AppShell.test.tsx +223 -0
- package/src/components/AppShell/AppShell.tsx +114 -0
- package/src/components/AppShell/appShell.types.ts +49 -0
- package/src/components/AppShell/appShell.variants.ts +48 -0
- package/src/components/Autocomplete/Autocomplete.perf.test.tsx +225 -0
- package/src/components/Autocomplete/Autocomplete.test.tsx +556 -0
- package/src/components/Autocomplete/Autocomplete.tsx +996 -0
- package/src/components/Autocomplete/autocomplete.types.ts +198 -0
- package/src/components/Autocomplete/autocomplete.variants.ts +135 -0
- package/src/components/Breadcrumbs/Breadcrumbs.perf.test.tsx +125 -0
- package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +231 -0
- package/src/components/Breadcrumbs/Breadcrumbs.tsx +203 -0
- package/src/components/Breadcrumbs/breadcrumbs.types.ts +87 -0
- package/src/components/Breadcrumbs/breadcrumbs.variants.ts +49 -0
- package/src/components/Button/Button.test.tsx +183 -0
- package/src/components/Button/Button.tsx +139 -0
- package/src/components/Button/button.types.ts +74 -0
- package/src/components/Button/button.variants.ts +159 -0
- package/src/components/Checkbox/Checkbox.test.tsx +146 -0
- package/src/components/Checkbox/Checkbox.tsx +228 -0
- package/src/components/Checkbox/checkbox.types.ts +88 -0
- package/src/components/Checkbox/checkbox.variants.ts +68 -0
- package/src/components/ConfirmDialog/ConfirmDialog.perf.test.tsx +172 -0
- package/src/components/ConfirmDialog/ConfirmDialog.test.tsx +311 -0
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +252 -0
- package/src/components/ConfirmDialog/confirmDialog.types.ts +70 -0
- package/src/components/ConfirmDialog/confirmDialog.variants.ts +76 -0
- package/src/components/DateTimePicker/DateTimePicker.perf.test.tsx +153 -0
- package/src/components/DateTimePicker/DateTimePicker.test.tsx +204 -0
- package/src/components/DateTimePicker/DateTimePicker.tsx +319 -0
- package/src/components/DateTimePicker/dateTimePicker.types.ts +83 -0
- package/src/components/DateTimePicker/dateTimePicker.variants.ts +84 -0
- package/src/components/LeftNav/LeftNav.perf.test.tsx +172 -0
- package/src/components/LeftNav/LeftNav.test.tsx +302 -0
- package/src/components/LeftNav/LeftNav.tsx +330 -0
- package/src/components/LeftNav/leftNav.types.ts +120 -0
- package/src/components/LeftNav/leftNav.variants.ts +96 -0
- package/src/components/NumberField/NumberField.test.tsx +83 -0
- package/src/components/NumberField/NumberField.tsx +293 -0
- package/src/components/NumberField/numberField.types.ts +72 -0
- package/src/components/NumberField/numberField.variants.ts +94 -0
- package/src/components/OTPField/OTPField.test.tsx +121 -0
- package/src/components/OTPField/OTPField.tsx +250 -0
- package/src/components/OTPField/otpField.types.ts +58 -0
- package/src/components/OTPField/otpField.variants.ts +72 -0
- package/src/components/RadioGroup/RadioGroup.test.tsx +140 -0
- package/src/components/RadioGroup/RadioGroup.tsx +261 -0
- package/src/components/RadioGroup/radioGroup.types.ts +85 -0
- package/src/components/RadioGroup/radioGroup.variants.ts +87 -0
- package/src/components/Snackbar/Snackbar.perf.test.tsx +144 -0
- package/src/components/Snackbar/Snackbar.test.tsx +327 -0
- package/src/components/Snackbar/Snackbar.tsx +298 -0
- package/src/components/Snackbar/snackbar.types.ts +83 -0
- package/src/components/Snackbar/snackbar.variants.ts +72 -0
- package/src/components/Switch/Switch.test.tsx +131 -0
- package/src/components/Switch/Switch.tsx +158 -0
- package/src/components/Switch/switch.types.ts +36 -0
- package/src/components/Switch/switch.variants.ts +65 -0
- package/src/components/TextField/TextField.test.tsx +194 -0
- package/src/components/TextField/TextField.tsx +216 -0
- package/src/components/TextField/textField.types.ts +70 -0
- package/src/components/TextField/textField.variants.ts +87 -0
- package/src/components/Textarea/Textarea.test.tsx +75 -0
- package/src/components/Textarea/Textarea.tsx +190 -0
- package/src/components/Textarea/textarea.types.ts +60 -0
- package/src/components/Textarea/textarea.variants.ts +85 -0
- package/src/components/TopBar/TopBar.test.tsx +129 -0
- package/src/components/TopBar/TopBar.tsx +66 -0
- package/src/components/TopBar/topBar.types.ts +42 -0
- package/src/components/TopBar/topBar.variants.ts +38 -0
- package/src/components/_shared/resolveValidationState.ts +60 -0
- package/src/hooks/useAccessState.spec.tsx +161 -0
- package/src/hooks/useAccessState.ts +81 -0
- package/src/index.ts +191 -0
- package/src/utils/cn.spec.ts +16 -0
- package/src/utils/cn.ts +29 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +55 -0
- package/tsconfig.spec.json +34 -0
- package/vite.config.ts +25 -0
- package/vitest.config.mts +22 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { cn } from '../../utils/cn.js';
|
|
3
|
+
import { appShellVariants } from './appShell.variants.js';
|
|
4
|
+
import type { AppShellProps } from './appShell.types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dashforge TW AppShell — top-level layout orchestrator.
|
|
8
|
+
*
|
|
9
|
+
* ┌──────────────────────────────────────────┐
|
|
10
|
+
* │ header │ ← `header` slot
|
|
11
|
+
* ├────────┬─────────────────────────────────┤
|
|
12
|
+
* │ │ │
|
|
13
|
+
* │ nav │ main │ ← `nav` + `children`
|
|
14
|
+
* │ │ │
|
|
15
|
+
* ├────────┴─────────────────────────────────┤
|
|
16
|
+
* │ footer │ ← `footer` slot
|
|
17
|
+
* └──────────────────────────────────────────┘
|
|
18
|
+
*
|
|
19
|
+
* **Responsive**:
|
|
20
|
+
* - `≥ md`: nav stays in the inline slot at the left.
|
|
21
|
+
* - `< md`: nav becomes a slide-in drawer toggled by `navOpen`.
|
|
22
|
+
* A backdrop closes the drawer on click (mirrors `<Dialog>` UX).
|
|
23
|
+
*
|
|
24
|
+
* **Body scroll lock**: when the mobile drawer is open we add
|
|
25
|
+
* `overflow-hidden` to `<body>` to prevent dual-scroll wobble. The
|
|
26
|
+
* effect cleans up on close + on unmount.
|
|
27
|
+
*
|
|
28
|
+
* **A11y**:
|
|
29
|
+
* - `<main>` landmark wraps the content area.
|
|
30
|
+
* - `<header>` / `<footer>` come for free from their HTML tags.
|
|
31
|
+
* - The mobile drawer + backdrop participate in the standard
|
|
32
|
+
* "click outside to close" pattern — `Escape` closes the drawer
|
|
33
|
+
* too (added via global keydown).
|
|
34
|
+
*/
|
|
35
|
+
export function AppShell(props: AppShellProps) {
|
|
36
|
+
const {
|
|
37
|
+
header,
|
|
38
|
+
nav,
|
|
39
|
+
footer,
|
|
40
|
+
children,
|
|
41
|
+
navOpen = false,
|
|
42
|
+
onNavOpenChange,
|
|
43
|
+
sx,
|
|
44
|
+
slotProps,
|
|
45
|
+
} = props;
|
|
46
|
+
|
|
47
|
+
const v = appShellVariants({ navOpen });
|
|
48
|
+
|
|
49
|
+
// Lock body scroll while the mobile drawer is open.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!navOpen) return;
|
|
52
|
+
const prev = document.body.style.overflow;
|
|
53
|
+
document.body.style.overflow = 'hidden';
|
|
54
|
+
return () => {
|
|
55
|
+
document.body.style.overflow = prev;
|
|
56
|
+
};
|
|
57
|
+
}, [navOpen]);
|
|
58
|
+
|
|
59
|
+
// Esc closes the drawer.
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!navOpen || !onNavOpenChange) return;
|
|
62
|
+
const handler = (event: KeyboardEvent) => {
|
|
63
|
+
if (event.key === 'Escape') onNavOpenChange(false);
|
|
64
|
+
};
|
|
65
|
+
document.addEventListener('keydown', handler);
|
|
66
|
+
return () => document.removeEventListener('keydown', handler);
|
|
67
|
+
}, [navOpen, onNavOpenChange]);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className={cn(v.root(), sx, slotProps?.root?.className)}>
|
|
71
|
+
{header && (
|
|
72
|
+
<div className={cn(v.header(), slotProps?.header?.className)}>
|
|
73
|
+
{header}
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
<div className={v.body()}>
|
|
78
|
+
{nav && (
|
|
79
|
+
<>
|
|
80
|
+
<aside className={cn(v.nav(), slotProps?.nav?.className)}>
|
|
81
|
+
{nav}
|
|
82
|
+
</aside>
|
|
83
|
+
<aside
|
|
84
|
+
className={cn(v.navMobile(), slotProps?.navMobile?.className)}
|
|
85
|
+
aria-hidden={!navOpen}
|
|
86
|
+
// The drawer is the SAME nav content rendered twice — once
|
|
87
|
+
// inline (desktop) and once as a slide-in (mobile). We
|
|
88
|
+
// mark the inactive copy hidden from AT to avoid double
|
|
89
|
+
// announcements.
|
|
90
|
+
>
|
|
91
|
+
{nav}
|
|
92
|
+
</aside>
|
|
93
|
+
<div
|
|
94
|
+
className={cn(v.backdrop(), slotProps?.backdrop?.className)}
|
|
95
|
+
role="presentation"
|
|
96
|
+
onClick={() => onNavOpenChange?.(false)}
|
|
97
|
+
data-testid="appshell-backdrop"
|
|
98
|
+
/>
|
|
99
|
+
</>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
<main className={cn(v.main(), slotProps?.main?.className)}>
|
|
103
|
+
{children}
|
|
104
|
+
</main>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{footer && (
|
|
108
|
+
<div className={cn(v.footer(), slotProps?.footer?.className)}>
|
|
109
|
+
{footer}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { AppShellVariants } from './appShell.variants.js';
|
|
3
|
+
|
|
4
|
+
export interface AppShellSlotProps {
|
|
5
|
+
root?: { className?: string };
|
|
6
|
+
header?: { className?: string };
|
|
7
|
+
nav?: { className?: string };
|
|
8
|
+
navMobile?: { className?: string };
|
|
9
|
+
main?: { className?: string };
|
|
10
|
+
footer?: { className?: string };
|
|
11
|
+
backdrop?: { className?: string };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Props for `<AppShell>`.
|
|
16
|
+
*
|
|
17
|
+
* Top-level layout orchestrator with 4 named slots:
|
|
18
|
+
*
|
|
19
|
+
* - `header` (top, sticky)
|
|
20
|
+
* - `nav` (left side, fixed-width on desktop, drawer on mobile)
|
|
21
|
+
* - `footer` (bottom, non-sticky)
|
|
22
|
+
* - `children` (main content, scrolls independently)
|
|
23
|
+
*
|
|
24
|
+
* Responsive behavior:
|
|
25
|
+
* - **≥ md (768px)**: nav is rendered inline at the left.
|
|
26
|
+
* - **< md**: nav slides in as an overlay drawer when
|
|
27
|
+
* `navOpen={true}` (controlled).
|
|
28
|
+
*
|
|
29
|
+
* The shell is router-agnostic — pass any nav/header content. Typical
|
|
30
|
+
* pairing is `<LeftNav>` + `<TopBar>` (with a hamburger button in
|
|
31
|
+
* `TopBar.start` that flips `navOpen`).
|
|
32
|
+
*/
|
|
33
|
+
export interface AppShellProps extends AppShellVariants {
|
|
34
|
+
header?: ReactNode;
|
|
35
|
+
nav?: ReactNode;
|
|
36
|
+
footer?: ReactNode;
|
|
37
|
+
children?: ReactNode;
|
|
38
|
+
/**
|
|
39
|
+
* Mobile drawer open state. Controlled — provide
|
|
40
|
+
* `onNavOpenChange` to update. When `undefined`, the mobile drawer
|
|
41
|
+
* is always closed (consumer hasn't wired up the toggle yet).
|
|
42
|
+
*/
|
|
43
|
+
navOpen?: boolean;
|
|
44
|
+
onNavOpenChange?: (open: boolean) => void;
|
|
45
|
+
/** Root className shortcut. */
|
|
46
|
+
sx?: string;
|
|
47
|
+
/** Per-slot className overrides. */
|
|
48
|
+
slotProps?: AppShellSlotProps;
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tailwind-variants recipe for `<AppShell>`.
|
|
5
|
+
*
|
|
6
|
+
* Slots:
|
|
7
|
+
* - `root` — outer flex column (full viewport)
|
|
8
|
+
* - `header` — top region
|
|
9
|
+
* - `body` — flex row holding nav + main
|
|
10
|
+
* - `nav` — desktop nav rail (md+)
|
|
11
|
+
* - `navMobile` — mobile drawer (slide-in)
|
|
12
|
+
* - `main` — scrollable content area
|
|
13
|
+
* - `footer` — bottom region
|
|
14
|
+
* - `backdrop` — semi-opaque overlay behind the mobile drawer
|
|
15
|
+
*/
|
|
16
|
+
export const appShellVariants = tv({
|
|
17
|
+
slots: {
|
|
18
|
+
root: 'flex flex-col min-h-screen bg-neutral-100',
|
|
19
|
+
header: 'shrink-0',
|
|
20
|
+
body: 'flex flex-1 min-h-0',
|
|
21
|
+
nav: 'hidden md:flex shrink-0',
|
|
22
|
+
navMobile: [
|
|
23
|
+
'flex md:hidden fixed inset-y-0 left-0 z-40',
|
|
24
|
+
'transition-transform duration-200 ease-out',
|
|
25
|
+
'-translate-x-full',
|
|
26
|
+
],
|
|
27
|
+
main: 'flex-1 min-w-0 overflow-y-auto',
|
|
28
|
+
footer: 'shrink-0',
|
|
29
|
+
backdrop: [
|
|
30
|
+
'md:hidden fixed inset-0 z-30 bg-black/40',
|
|
31
|
+
'transition-opacity duration-200',
|
|
32
|
+
'opacity-0 pointer-events-none',
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
variants: {
|
|
36
|
+
navOpen: {
|
|
37
|
+
true: {
|
|
38
|
+
navMobile: 'translate-x-0',
|
|
39
|
+
backdrop: 'opacity-100 pointer-events-auto',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
defaultVariants: {
|
|
44
|
+
navOpen: false,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export type AppShellVariants = VariantProps<typeof appShellVariants>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* Performance + re-render budget tests for `<Autocomplete>`.
|
|
4
|
+
*
|
|
5
|
+
* These are NOT "is X fast" benchmarks — they're guardrails against
|
|
6
|
+
* regressions in the things that matter for a 1000+ option combobox:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Filter memoization**: `filteredOptions` must be reference-stable
|
|
9
|
+
* when its inputs (options + filter text + selection state) don't
|
|
10
|
+
* change. A stable reference means downstream lists don't re-mount.
|
|
11
|
+
*
|
|
12
|
+
* 2. **Render budget**: typing N characters must cause O(N) renders
|
|
13
|
+
* of the component, not O(N²) or worse. With our internals there
|
|
14
|
+
* are 2 renders per keystroke (one for `inputValue`, one for the
|
|
15
|
+
* popover opening on first keystroke), so the budget is `2N + k`
|
|
16
|
+
* for a small constant `k`.
|
|
17
|
+
*
|
|
18
|
+
* 3. **Initial render time** with a large option list must finish
|
|
19
|
+
* within a generous wall-clock budget. This catches a future
|
|
20
|
+
* refactor that, say, accidentally calls `getOptionLabel` 1000²
|
|
21
|
+
* times during mount.
|
|
22
|
+
*/
|
|
23
|
+
import { describe, it, expect } from 'vitest';
|
|
24
|
+
import { useEffect, useRef } from 'react';
|
|
25
|
+
import { render, fireEvent, screen } from '@testing-library/react';
|
|
26
|
+
import { Autocomplete } from './Autocomplete.js';
|
|
27
|
+
import type { AutocompleteOption } from './autocomplete.types.js';
|
|
28
|
+
|
|
29
|
+
// Generate `n` deterministic options.
|
|
30
|
+
function makeOptions(n: number): AutocompleteOption[] {
|
|
31
|
+
return Array.from({ length: n }, (_, i) => ({
|
|
32
|
+
value: `o-${i}`,
|
|
33
|
+
label: `Option ${i}`,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Wrap a component and expose a render-counter ref so tests can assert
|
|
39
|
+
* how many times React re-rendered the wrapped subtree.
|
|
40
|
+
*/
|
|
41
|
+
function withRenderCounter<P>(
|
|
42
|
+
Component: React.ComponentType<P>,
|
|
43
|
+
counterRef: { count: number }
|
|
44
|
+
): React.FC<P> {
|
|
45
|
+
return function Counted(props: P) {
|
|
46
|
+
counterRef.count++;
|
|
47
|
+
return <Component {...(props as P & object)} />;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('<Autocomplete> performance', () => {
|
|
52
|
+
it('mounts a 1000-option list well under 500ms', () => {
|
|
53
|
+
const options = makeOptions(1000);
|
|
54
|
+
const t0 = performance.now();
|
|
55
|
+
render(<Autocomplete name="x" options={options} label="X" />);
|
|
56
|
+
const t1 = performance.now();
|
|
57
|
+
expect(t1 - t0).toBeLessThan(500);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('filters a 1000-option list well under 100ms per keystroke', () => {
|
|
61
|
+
const options = makeOptions(1000);
|
|
62
|
+
render(<Autocomplete name="x" options={options} label="X" />);
|
|
63
|
+
const input = screen.getByRole('combobox') as HTMLInputElement;
|
|
64
|
+
const t0 = performance.now();
|
|
65
|
+
fireEvent.change(input, { target: { value: '9' } });
|
|
66
|
+
const t1 = performance.now();
|
|
67
|
+
expect(t1 - t0).toBeLessThan(100);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('<Autocomplete> re-render budget', () => {
|
|
72
|
+
it('renders at most 2 times on mount (StrictMode-free path)', () => {
|
|
73
|
+
const counter = { count: 0 };
|
|
74
|
+
const Counted = withRenderCounter(Autocomplete, counter);
|
|
75
|
+
render(<Counted name="x" options={makeOptions(20)} label="X" />);
|
|
76
|
+
// 1 initial render — and we tolerate up to 2 to account for the
|
|
77
|
+
// synchronous useState callbacks that schedule a follow-up commit.
|
|
78
|
+
expect(counter.count).toBeLessThanOrEqual(2);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('keeps renders linear in keystrokes (≤ 3 renders per char)', () => {
|
|
82
|
+
const counter = { count: 0 };
|
|
83
|
+
const Counted = withRenderCounter(Autocomplete, counter);
|
|
84
|
+
const { container } = render(
|
|
85
|
+
<Counted name="x" options={makeOptions(50)} label="X" />
|
|
86
|
+
);
|
|
87
|
+
const baseline = counter.count;
|
|
88
|
+
const input = container.querySelector(
|
|
89
|
+
'input[role="combobox"]'
|
|
90
|
+
) as HTMLInputElement;
|
|
91
|
+
const N = 10;
|
|
92
|
+
for (let i = 0; i < N; i++) {
|
|
93
|
+
fireEvent.change(input, { target: { value: 'a'.repeat(i + 1) } });
|
|
94
|
+
}
|
|
95
|
+
const perChar = (counter.count - baseline) / N;
|
|
96
|
+
// 1-2 renders per keystroke is the design budget; allow some slack
|
|
97
|
+
// for popover-open transitions on the first keystroke and the
|
|
98
|
+
// highlight reset useState call.
|
|
99
|
+
expect(perChar).toBeLessThanOrEqual(3);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('does NOT re-render an unrelated sibling on each keystroke', () => {
|
|
103
|
+
// Critical perf property: typing into one Autocomplete must not
|
|
104
|
+
// cause sibling components in the same tree to re-render. We check
|
|
105
|
+
// this by mounting a sibling whose render count we count directly.
|
|
106
|
+
const sibCount = { count: 0 };
|
|
107
|
+
const Sibling = () => {
|
|
108
|
+
sibCount.count++;
|
|
109
|
+
return <div data-testid="sib" />;
|
|
110
|
+
};
|
|
111
|
+
render(
|
|
112
|
+
<div>
|
|
113
|
+
<Autocomplete name="x" options={makeOptions(50)} label="X" />
|
|
114
|
+
<Sibling />
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
const before = sibCount.count;
|
|
118
|
+
const input = screen.getByRole('combobox') as HTMLInputElement;
|
|
119
|
+
for (let i = 0; i < 5; i++) {
|
|
120
|
+
fireEvent.change(input, { target: { value: 'a'.repeat(i + 1) } });
|
|
121
|
+
}
|
|
122
|
+
// No state in the parent changes ⇒ sibling never re-renders.
|
|
123
|
+
expect(sibCount.count).toBe(before);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('filteredOptions reference is stable across unrelated re-renders', () => {
|
|
127
|
+
// Functional contract: if the deps (options, filter text, selection)
|
|
128
|
+
// don't change, `useMemo` should return the same array reference.
|
|
129
|
+
// We verify by triggering a parent re-render with no relevant input
|
|
130
|
+
// change and asserting the `<li>` DOM elements are reused (same id
|
|
131
|
+
// identities ⇒ React reused the keys).
|
|
132
|
+
const options = makeOptions(20);
|
|
133
|
+
const seenIds = new Set<string>();
|
|
134
|
+
const { rerender } = render(
|
|
135
|
+
<Autocomplete name="x" options={options} label="X" />
|
|
136
|
+
);
|
|
137
|
+
const input = screen.getByRole('combobox') as HTMLInputElement;
|
|
138
|
+
fireEvent.focus(input); // open popover so the list renders
|
|
139
|
+
const lis1 = Array.from(
|
|
140
|
+
document.querySelectorAll('[role="option"]')
|
|
141
|
+
).map((el) => el.id);
|
|
142
|
+
lis1.forEach((id) => seenIds.add(id));
|
|
143
|
+
|
|
144
|
+
// Trigger an unrelated re-render: same options ref, same input
|
|
145
|
+
// value, same selection.
|
|
146
|
+
rerender(<Autocomplete name="x" options={options} label="X" />);
|
|
147
|
+
const lis2 = Array.from(
|
|
148
|
+
document.querySelectorAll('[role="option"]')
|
|
149
|
+
).map((el) => el.id);
|
|
150
|
+
|
|
151
|
+
// Same ids ⇒ React keyed reuse held; list DOM was not torn down.
|
|
152
|
+
expect(lis2).toEqual(lis1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('captures number of options accessor calls during one filter cycle', () => {
|
|
156
|
+
// Implementation detail with strong perf relevance: filtering 1000
|
|
157
|
+
// options must call `getOptionLabel` at most O(N) times per filter
|
|
158
|
+
// (NOT O(N²) or anything weirdly memoized that re-computes per
|
|
159
|
+
// child).
|
|
160
|
+
let labelCalls = 0;
|
|
161
|
+
let valueCalls = 0;
|
|
162
|
+
const N = 200;
|
|
163
|
+
const options: AutocompleteOption[] = makeOptions(N);
|
|
164
|
+
render(
|
|
165
|
+
<Autocomplete
|
|
166
|
+
name="x"
|
|
167
|
+
options={options}
|
|
168
|
+
label="X"
|
|
169
|
+
getOptionLabel={(o) => {
|
|
170
|
+
labelCalls++;
|
|
171
|
+
return o.label;
|
|
172
|
+
}}
|
|
173
|
+
getOptionValue={(o) => {
|
|
174
|
+
valueCalls++;
|
|
175
|
+
return o.value;
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
const input = screen.getByRole('combobox') as HTMLInputElement;
|
|
180
|
+
labelCalls = 0;
|
|
181
|
+
valueCalls = 0;
|
|
182
|
+
fireEvent.change(input, { target: { value: '5' } });
|
|
183
|
+
// Budget: O(N) on both. Allow 4x N to cover the filter pass plus
|
|
184
|
+
// the popover-open render's per-option `getOptionValue` for ids
|
|
185
|
+
// and `getOptionLabel` for the rendered <li> body.
|
|
186
|
+
expect(labelCalls).toBeLessThanOrEqual(4 * N);
|
|
187
|
+
expect(valueCalls).toBeLessThanOrEqual(4 * N);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('<Autocomplete> async + perf interaction', () => {
|
|
192
|
+
it('debounces loadOptions so 5 rapid keystrokes ⇒ 1 fetch', async () => {
|
|
193
|
+
const calls: string[] = [];
|
|
194
|
+
const load = (q: string) => {
|
|
195
|
+
calls.push(q);
|
|
196
|
+
return Promise.resolve<AutocompleteOption[]>([]);
|
|
197
|
+
};
|
|
198
|
+
render(
|
|
199
|
+
<Autocomplete
|
|
200
|
+
name="x"
|
|
201
|
+
options={[]}
|
|
202
|
+
loadOptions={load}
|
|
203
|
+
loadDebounceMs={50}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
const input = screen.getByRole('combobox') as HTMLInputElement;
|
|
207
|
+
// Type 5 chars in quick succession (synchronous).
|
|
208
|
+
for (let i = 0; i < 5; i++) {
|
|
209
|
+
fireEvent.change(input, { target: { value: 'abcde'.slice(0, i + 1) } });
|
|
210
|
+
}
|
|
211
|
+
// Before debounce expires, no call yet.
|
|
212
|
+
expect(calls.length).toBe(0);
|
|
213
|
+
// Wait past debounce window.
|
|
214
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
215
|
+
// Exactly ONE call lands, with the LAST typed value.
|
|
216
|
+
expect(calls).toEqual(['abcde']);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Module-level useEffect import sanity (suppresses "useRef declared but
|
|
222
|
+
* not used" if a test runner tree-shake removes some perf tests in CI).
|
|
223
|
+
*/
|
|
224
|
+
void useRef;
|
|
225
|
+
void useEffect;
|