@tailor-platform/app-shell 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-shell-core.css +3 -1
- package/dist/app-shell.d.ts +925 -36
- package/dist/app-shell.js +10662 -14286
- package/dist/appshell-CSQWUk6q.js +1823 -0
- package/dist/fonts/NOTO-SANS-JP-LICENSE.txt +93 -0
- package/dist/fonts/files/noto-sans-jp-0-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-1-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-10-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-100-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-101-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-102-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-103-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-104-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-105-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-106-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-107-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-108-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-109-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-11-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-110-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-111-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-112-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-113-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-114-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-115-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-116-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-117-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-118-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-119-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-12-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-13-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-14-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-15-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-16-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-17-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-18-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-19-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-2-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-20-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-21-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-22-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-23-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-24-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-25-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-26-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-27-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-28-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-29-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-3-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-30-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-31-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-32-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-33-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-34-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-35-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-36-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-37-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-38-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-39-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-4-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-40-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-41-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-42-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-43-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-44-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-45-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-46-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-47-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-48-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-49-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-5-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-50-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-51-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-52-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-53-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-54-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-55-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-6-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-60-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-61-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-62-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-63-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-64-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-65-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-66-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-67-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-68-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-69-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-7-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-70-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-71-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-72-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-73-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-74-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-75-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-76-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-77-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-78-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-79-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-8-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-80-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-81-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-82-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-83-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-84-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-85-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-86-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-87-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-88-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-89-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-9-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-90-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-91-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-92-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-93-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-94-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-95-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-96-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-97-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-98-wght-normal.woff2 +0 -0
- package/dist/fonts/files/noto-sans-jp-99-wght-normal.woff2 +0 -0
- package/dist/fonts/noto-sans-jp.css +2506 -0
- package/dist/style-entry.d.ts +600 -0
- package/dist/style-entry.js +0 -1
- package/dist/testing.d.ts +1140 -0
- package/dist/testing.js +14 -0
- package/dist/theme.bridge.css +29 -1
- package/dist/vite-plugin.d.ts +600 -0
- package/dist/vite-plugin.js +2 -4
- package/package.json +22 -14
- package/skills/app-shell-patterns/SKILL.md +7 -6
- package/skills/app-shell-patterns/references/fundamental/components.md +53 -0
- package/skills/app-shell-patterns/references/fundamental/design-system.md +1 -1
- package/skills/app-shell-patterns/references/patterns/form-composer.md +142 -0
- package/skills/app-shell-patterns/references/patterns/form-single-page.md +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tailor-platform/app-shell",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "An opinionated React application framework for building ERP applications on Tailor Platform",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app-shell",
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
"./vite-plugin": {
|
|
38
38
|
"types": "./dist/vite-plugin.d.ts",
|
|
39
39
|
"default": "./dist/vite-plugin.js"
|
|
40
|
+
},
|
|
41
|
+
"./testing": {
|
|
42
|
+
"types": "./dist/testing.d.ts",
|
|
43
|
+
"default": "./dist/testing.js"
|
|
40
44
|
}
|
|
41
45
|
},
|
|
42
46
|
"publishConfig": {
|
|
@@ -46,7 +50,7 @@
|
|
|
46
50
|
"@base-ui/react": "1.6.0",
|
|
47
51
|
"@internationalized/date": "3.12.2",
|
|
48
52
|
"@standard-schema/spec": "^1.1.0",
|
|
49
|
-
"@tailor-platform/auth-public-client": "^0.
|
|
53
|
+
"@tailor-platform/auth-public-client": "^0.6.1",
|
|
50
54
|
"change-case": "^5.4.4",
|
|
51
55
|
"class-variance-authority": "^0.7.1",
|
|
52
56
|
"clsx": "^2.1.1",
|
|
@@ -60,26 +64,28 @@
|
|
|
60
64
|
"@tailor-platform/app-shell-vite-plugin": "0.2.3"
|
|
61
65
|
},
|
|
62
66
|
"devDependencies": {
|
|
63
|
-
"@
|
|
64
|
-
"@
|
|
65
|
-
"@
|
|
66
|
-
"@
|
|
67
|
+
"@fontsource-variable/noto-sans-jp": "5.3.0",
|
|
68
|
+
"@microsoft/api-extractor": "7.58.12",
|
|
69
|
+
"@tailwindcss/postcss": "^4.3.3",
|
|
70
|
+
"@testing-library/react": "16.3.3",
|
|
71
|
+
"@testing-library/user-event": "14.6.6",
|
|
72
|
+
"@types/encoding-japanese": "2.2.1",
|
|
67
73
|
"@types/node": "^25.9.5",
|
|
68
|
-
"@types/papaparse": "
|
|
74
|
+
"@types/papaparse": "5.5.2",
|
|
69
75
|
"@types/react": "^19",
|
|
70
76
|
"@types/react-dom": "^19",
|
|
71
|
-
"@vitejs/plugin-react": "^
|
|
77
|
+
"@vitejs/plugin-react": "^6.1.0",
|
|
78
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
72
79
|
"happy-dom": "20.9.0",
|
|
73
80
|
"postcss": "8.5.26",
|
|
74
81
|
"react": "^19.2.7",
|
|
75
82
|
"react-dom": "^19.2.7",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
83
|
+
"react-hook-form": "7.71.2",
|
|
84
|
+
"tailwindcss": "^4.3.3",
|
|
85
|
+
"tw-animate-css": "1.4.0",
|
|
78
86
|
"typescript": "5.9.3",
|
|
79
|
-
"vite": "^
|
|
80
|
-
"vite-plugin-dts": "
|
|
81
|
-
"vite-plugin-externalize-deps": "^0.10.0",
|
|
82
|
-
"vite-tsconfig-paths": "^6.1.1",
|
|
87
|
+
"vite": "^8.2.2",
|
|
88
|
+
"vite-plugin-dts": "5.0.3",
|
|
83
89
|
"vitest": "^4.1.10"
|
|
84
90
|
},
|
|
85
91
|
"peerDependencies": {
|
|
@@ -89,6 +95,8 @@
|
|
|
89
95
|
"scripts": {
|
|
90
96
|
"dev": "vite build --watch --mode development",
|
|
91
97
|
"build": "vite build",
|
|
98
|
+
"check-dts": "node scripts/check-dts.mjs",
|
|
99
|
+
"vendor:fonts": "node scripts/sync-noto-fonts.mjs",
|
|
92
100
|
"lint": "oxlint -c .oxlintrc.jsonc",
|
|
93
101
|
"test": "vitest run",
|
|
94
102
|
"type-check": "tsc --incremental"
|
|
@@ -35,12 +35,13 @@ Read it when upgrading `@tailor-platform/app-shell`, and whenever styling, themi
|
|
|
35
35
|
|
|
36
36
|
### form
|
|
37
37
|
|
|
38
|
-
| Slug | Name | Description
|
|
39
|
-
| ------------------------------------------------------------- | ---------------- |
|
|
40
|
-
| [`form/
|
|
41
|
-
| [`form/
|
|
42
|
-
| [`form/
|
|
43
|
-
| [`form/
|
|
38
|
+
| Slug | Name | Description |
|
|
39
|
+
| ------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
40
|
+
| [`form/composer`](references/patterns/form-composer.md) | Composer | Free-text composer — a multi-line input above a trailing action row, for notes, replies, and comments submitted in place |
|
|
41
|
+
| [`form/modal`](references/patterns/form-modal.md) | Modal Form | Default form pattern for Create/Edit — keeps user in context on the parent screen |
|
|
42
|
+
| [`form/sectioned`](references/patterns/form-sectioned.md) | Sectioned Form | Complex form with 15+ fields organized into named fieldset sections |
|
|
43
|
+
| [`form/single-page`](references/patterns/form-single-page.md) | Single Page Form | Routed full-page form for moderate field count (6-15) without natural sectioning |
|
|
44
|
+
| [`form/wizard`](references/patterns/form-wizard.md) | Wizard Form | Multi-stage create flow with 3-7 steps and per-step validation gates |
|
|
44
45
|
|
|
45
46
|
### interaction
|
|
46
47
|
|
|
@@ -194,6 +194,13 @@ import { Button, Link } from '@tailor-platform/app-shell';
|
|
|
194
194
|
**API:** `to`, `replace`, `state`, etc. — same as `react-router`.
|
|
195
195
|
**Used in patterns:** all (navigation).
|
|
196
196
|
|
|
197
|
+
### `NavLink`
|
|
198
|
+
|
|
199
|
+
**Import:** `import { NavLink } from '@tailor-platform/app-shell'`
|
|
200
|
+
**Purpose:** `Link` that knows when it is active — for nav menus and tab bars.
|
|
201
|
+
**API:** `to`, plus `className` / `style` / `children` as render functions receiving `{ isActive, isPending }`.
|
|
202
|
+
**Used in patterns:** navigation shells.
|
|
203
|
+
|
|
197
204
|
### `Dialog`
|
|
198
205
|
|
|
199
206
|
> Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/dialog.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/dialog.md)
|
|
@@ -643,6 +650,26 @@ const table = useDataTable({
|
|
|
643
650
|
**API:** `InputProps` — extends native `<input>` props.
|
|
644
651
|
**Used in patterns:** all `form/*`.
|
|
645
652
|
|
|
653
|
+
### `Textarea`
|
|
654
|
+
|
|
655
|
+
> Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/textarea.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/textarea.md)
|
|
656
|
+
|
|
657
|
+
**Import:** `import { Textarea } from '@tailor-platform/app-shell'`
|
|
658
|
+
**Purpose:** Multi-line text input. Maps to spec field types: `text`, long-form `string`. Use instead of `Input` whenever the value is prose (notes, descriptions, replies, reasons).
|
|
659
|
+
**API:** `TextareaProps` — Base UI's field control (`value`, `defaultValue`, `onChange`, `onValueChange`, `disabled`, `required`, `readOnly`, `name`, aria-\*) rendered as a `<textarea>`, plus `rows`. `cols` / `wrap` are not accepted — the control is `w-full`, so neither can take effect.
|
|
660
|
+
**Sizing:** no fixed height — `rows` sets the visible line count and the user can drag it taller. `min-h-16` is a hard floor, so `rows` only takes effect from 3 up. Width is the parent's job: the control is `w-full`, and `cols` is not accepted because it could never win against that. Do **not** reach for `Input` and a height override; `Input` is locked to `h-9`, and an `astw:h-*` override from a consuming app resolves to nothing unless that exact utility was compiled into the package CSS.
|
|
661
|
+
**Field integration:** drops straight into `Field.Root` for label/description/error wiring exactly like `Input` and `Checkbox` — no bespoke `error` prop.
|
|
662
|
+
**Example:**
|
|
663
|
+
|
|
664
|
+
```tsx
|
|
665
|
+
<Field.Root name="reason">
|
|
666
|
+
<Field.Label>Reason</Field.Label>
|
|
667
|
+
<Textarea rows={4} placeholder="Why is this being rejected?" />
|
|
668
|
+
</Field.Root>
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
**Used in patterns:** `form/composer`, `form/*` (long-text fields).
|
|
672
|
+
|
|
646
673
|
### `Checkbox`
|
|
647
674
|
|
|
648
675
|
**Import:** `import { Checkbox } from '@tailor-platform/app-shell'`
|
|
@@ -730,6 +757,32 @@ Types for authoring guard functions used by `WithGuard` and `appShellPageProps.g
|
|
|
730
757
|
**Purpose:** Re-exported from `react-router`. Use the AppShell barrel — never import from `react-router` directly.
|
|
731
758
|
**Used in patterns:** all (navigation, route params, query state).
|
|
732
759
|
|
|
760
|
+
### `useMatch`, `useResolvedPath`, `useNavigation`
|
|
761
|
+
|
|
762
|
+
**Import:** `import { useMatch, useResolvedPath, useNavigation } from '@tailor-platform/app-shell'`
|
|
763
|
+
**Purpose:** `useMatch` tests a pattern against the current location (active states); `useResolvedPath` resolves a relative path; `useNavigation` exposes the in-flight navigation for pending UI.
|
|
764
|
+
**Used in patterns:** navigation shells, list→detail transitions.
|
|
765
|
+
|
|
766
|
+
### `useBlocker`, `useBeforeUnload`
|
|
767
|
+
|
|
768
|
+
**Import:** `import { useBlocker, useBeforeUnload } from '@tailor-platform/app-shell'`
|
|
769
|
+
**Purpose:** Guard navigation away from unsaved work — `useBlocker` for in-app navigation, `useBeforeUnload` for closing or reloading the tab.
|
|
770
|
+
**Used in patterns:** form-heavy screens.
|
|
771
|
+
|
|
772
|
+
### Types
|
|
773
|
+
|
|
774
|
+
**Import:** `import type { Location, NavigateFunction, NavigateOptions, To, Params, PathMatch, LinkProps, NavLinkProps, Navigation, Blocker, BlockerFunction } from '@tailor-platform/app-shell'`
|
|
775
|
+
**Purpose:** The React Router types that go with the hooks above, so app code never imports types from `react-router` either.
|
|
776
|
+
|
|
777
|
+
### Not exported
|
|
778
|
+
|
|
779
|
+
`createBrowserRouter`, `RouterProvider`, `MemoryRouter`, `Routes`, `Route` — AppShell builds the router; a second one nested inside it breaks navigation. `useLoaderData`, `Form`, `useSubmit`, `useFetcher`, `useActionData` — AppShell does not wire up React Router's data layer. Load data in the page component or the app's data layer.
|
|
780
|
+
|
|
781
|
+
### Testing
|
|
782
|
+
|
|
783
|
+
**Import:** `import { AppShell, TestRouter } from '@tailor-platform/app-shell/testing'`
|
|
784
|
+
**Purpose:** Test routing without a `react-router` dependency. `AppShell` from this entry also accepts `memory` / `initialEntries`, for mounting a page at a fixed URL; `TestRouter` gives a single component router context, and takes `path` when that component reads the route (`useParams` is `{}` without it).
|
|
785
|
+
|
|
733
786
|
### `createTypedPaths`
|
|
734
787
|
|
|
735
788
|
**Import:** `import { createTypedPaths } from '@tailor-platform/app-shell'`
|
|
@@ -184,7 +184,7 @@ Hand-typing `padding: 13px` is a smell. Round to the nearest scale step; if noth
|
|
|
184
184
|
|
|
185
185
|
### Typography
|
|
186
186
|
|
|
187
|
-
**AppShell defines no typography scale tokens.** There is no `text-h1`, `text-body`, or `text-caption`. The only typography token is `--font-sans` (Inter Variable) → `font-sans`, which the base layer already applies to `body`.
|
|
187
|
+
**AppShell defines no typography scale tokens.** There is no `text-h1`, `text-body`, or `text-caption`. The only typography token is `--font-sans` (Inter Variable for Latin, Noto Sans JP Variable for Japanese) → `font-sans`, which the base layer already applies to `body`. Both are variable fonts, so every weight token is real in both scripts. Override the whole stack by setting `--app-shell-font-sans` on `:root`.
|
|
188
188
|
|
|
189
189
|
Compose roles from stock Tailwind utilities. These pairings are what AppShell's own components use — match them so your screens sit consistently alongside the primitives:
|
|
190
190
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: pattern/form/composer
|
|
3
|
+
name: Composer
|
|
4
|
+
category: pattern
|
|
5
|
+
subcategory: form
|
|
6
|
+
description: Free-text composer — a multi-line input above a trailing action row, for notes, replies, and comments submitted in place
|
|
7
|
+
requiredImports: [Card, Textarea, Button, Checkbox]
|
|
8
|
+
tags: [composer, textarea, note, reply, comment, actions, discard, send]
|
|
9
|
+
do:
|
|
10
|
+
- A free-text box the user fills and submits in place — add a note, reply on a thread, leave a comment, give a rejection reason
|
|
11
|
+
- The prose IS the interaction; any other controls only qualify how the text is submitted
|
|
12
|
+
dont:
|
|
13
|
+
- The text is one field among many on a record form — use form/modal, form/single-page or form/sectioned with a Textarea field
|
|
14
|
+
- A chat input where Enter sends and there is no Discard — that is a different layout
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# pattern/form/composer
|
|
18
|
+
|
|
19
|
+
## When to Use
|
|
20
|
+
|
|
21
|
+
- A free-text box the user fills and submits in place — add a note, reply on a thread, leave a comment, give a rejection reason
|
|
22
|
+
- The prose IS the interaction; any other controls only qualify how the text is submitted
|
|
23
|
+
|
|
24
|
+
## Layout
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
+---------------------------------------------------------+
|
|
28
|
+
| Card.Root / Card.Content |
|
|
29
|
+
| +-----------------------------------------------------+ |
|
|
30
|
+
| | Textarea | |
|
|
31
|
+
| | | |
|
|
32
|
+
| +-----------------------------------------------------+ |
|
|
33
|
+
| [ options slot … ] [Discard] [ Send ] |
|
|
34
|
+
+---------------------------------------------------------+
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Two rows inside one `Card`:
|
|
38
|
+
|
|
39
|
+
1. **Body** — a single `Textarea`, sized with `rows`. Tall enough to read as "write here", short enough that the action row stays on screen.
|
|
40
|
+
2. **Action row** — one `flex items-center justify-between` line with a fixed right side and a free left side.
|
|
41
|
+
|
|
42
|
+
## The action row
|
|
43
|
+
|
|
44
|
+
This is the part the pattern actually fixes. Everything sits on **one line**, never stacked.
|
|
45
|
+
|
|
46
|
+
**Right side — fixed.** Reading left to right: an optional `variant="ghost"` **Discard**, then exactly **one** primary submit. Nothing else goes here.
|
|
47
|
+
|
|
48
|
+
- Discard clears the body; it does not close or navigate. Omit it when there is nothing to discard back to.
|
|
49
|
+
- The submit is the view's single primary `Button` — never a second filled button beside it.
|
|
50
|
+
- Both disable while `submitting`; the submit also disables on empty/whitespace-only input.
|
|
51
|
+
- Swap the submit's label for a pending label while in flight ("Send" → "Sending…"). Do not swap in a spinner-only button — the label is what tells the user what is happening.
|
|
52
|
+
|
|
53
|
+
**Left side — open.** Zero or more controls that qualify the submission, laid out `flex items-center gap-2` and allowed to shrink (`min-w-0`) so the buttons never get pushed off. Common occupants:
|
|
54
|
+
|
|
55
|
+
- a `Checkbox` toggle — visibility, "internal note", "notify watchers" (the worked example below)
|
|
56
|
+
- an attachment or template `Button` with `variant="ghost"`
|
|
57
|
+
- a small `Select` — reply-as, channel, canned response
|
|
58
|
+
|
|
59
|
+
Leave it empty and the buttons still sit correctly at the right; `justify-between` needs no placeholder element.
|
|
60
|
+
|
|
61
|
+
## Page Implementation
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
/* pattern: form/composer */
|
|
65
|
+
import { useState } from "react";
|
|
66
|
+
import { Button, Card, Checkbox, Textarea } from "@tailor-platform/app-shell";
|
|
67
|
+
|
|
68
|
+
type Props = {
|
|
69
|
+
onSubmit: (body: string, internal: boolean) => void;
|
|
70
|
+
submitting?: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default function Composer({ onSubmit, submitting = false }: Props) {
|
|
74
|
+
const [body, setBody] = useState("");
|
|
75
|
+
const [internal, setInternal] = useState(false);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Card.Root>
|
|
79
|
+
<Card.Content>
|
|
80
|
+
<div className="space-y-3">
|
|
81
|
+
<Textarea
|
|
82
|
+
aria-label="Reply"
|
|
83
|
+
value={body}
|
|
84
|
+
onChange={(e) => setBody(e.target.value)}
|
|
85
|
+
placeholder={internal ? "Internal context for teammates…" : "Reply to customer…"}
|
|
86
|
+
rows={4}
|
|
87
|
+
/>
|
|
88
|
+
<div className="flex items-center justify-between gap-2">
|
|
89
|
+
{/* Options slot — 0..N controls that qualify the submission. An
|
|
90
|
+
internal-note toggle here; swap for an attachment button, a
|
|
91
|
+
visibility Select, a template picker, or drop it entirely. */}
|
|
92
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
93
|
+
<Checkbox
|
|
94
|
+
label="Internal note (not sent to customer)"
|
|
95
|
+
checked={internal}
|
|
96
|
+
onCheckedChange={setInternal}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
{/* Action slot — fixed shape: optional ghost Discard, then exactly
|
|
100
|
+
one primary submit. Never more than one primary. */}
|
|
101
|
+
<div className="flex shrink-0 gap-2">
|
|
102
|
+
<Button
|
|
103
|
+
variant="ghost"
|
|
104
|
+
onClick={() => setBody("")}
|
|
105
|
+
disabled={body.length === 0 || submitting}
|
|
106
|
+
>
|
|
107
|
+
Discard
|
|
108
|
+
</Button>
|
|
109
|
+
<Button
|
|
110
|
+
onClick={() => onSubmit(body, internal)}
|
|
111
|
+
disabled={body.trim().length === 0 || submitting}
|
|
112
|
+
>
|
|
113
|
+
{submitting ? "Sending…" : "Send"}
|
|
114
|
+
</Button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</Card.Content>
|
|
119
|
+
</Card.Root>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Constraints
|
|
125
|
+
|
|
126
|
+
- The body control is `Textarea`, never `Input` — `Input` is locked to `h-9` and clips prose to one 36px line.
|
|
127
|
+
- Size the body with `rows` (3–5 is the usual range). Don't reach for an `astw:h-*` / `astw:min-h-*` override: `astw:` utilities only resolve if that exact class was compiled into the AppShell package CSS, so an arbitrary one written in a consuming app silently does nothing.
|
|
128
|
+
- The `Textarea` needs an accessible name. There is usually no visible label in a composer, so pass `aria-label`; a placeholder is not a label.
|
|
129
|
+
- One primary `Button` in the action row. Discard is `ghost`, and any left-side action is `ghost` too.
|
|
130
|
+
- The action row is one row at every width — the left side shrinks, the buttons do not wrap under the body.
|
|
131
|
+
- Disable the submit on empty or whitespace-only input, and disable both buttons while `submitting`.
|
|
132
|
+
- Wrap the composer in `Card.Root` when it sits among other cards (a detail page, the end of a thread). A composer that is already inside a `Sheet` or `Dialog` body does not need its own card.
|
|
133
|
+
|
|
134
|
+
## Anti-patterns
|
|
135
|
+
|
|
136
|
+
- `Input` (or `Field.Control` with no override) for the body — a single-line box for multi-line text.
|
|
137
|
+
- Discard rendered as `outline` or filled — it competes with the submit for the eye.
|
|
138
|
+
- Submit and Discard both filled, or a third action added to the right side.
|
|
139
|
+
- The action row stacked above the `Textarea`, or the buttons wrapped onto their own line — the composer stops reading as one unit.
|
|
140
|
+
- A left-side toggle that silently changes what the submit does without changing the submit's own affordance — if "internal note" changes the destination, the placeholder (or the label) should say so.
|
|
141
|
+
- Submit enabled on an empty body, so the user can post nothing.
|
|
142
|
+
- A body so tall (`rows={12}`) that the action row falls below the fold — the composer stops reading as one unit.
|
|
@@ -4,7 +4,7 @@ name: Single Page Form
|
|
|
4
4
|
category: pattern
|
|
5
5
|
subcategory: form
|
|
6
6
|
description: Routed full-page form for moderate field count (6-15) without natural sectioning
|
|
7
|
-
requiredImports: [Layout, Form, Field, Fieldset, Input, Select, Button]
|
|
7
|
+
requiredImports: [Layout, Form, Field, Fieldset, Input, Textarea, Select, Button]
|
|
8
8
|
tags: [form, page, create, edit, routed]
|
|
9
9
|
do:
|
|
10
10
|
- A routed Create or Edit page that the design has explicitly called out (e.g. /orders/create)
|
|
@@ -26,7 +26,7 @@ dont:
|
|
|
26
26
|
|
|
27
27
|
```tsx
|
|
28
28
|
/* pattern: form/single-page */
|
|
29
|
-
import { Button, Layout, Input, Select, Field } from "@tailor-platform/app-shell";
|
|
29
|
+
import { Button, Layout, Input, Textarea, Select, Field } from "@tailor-platform/app-shell";
|
|
30
30
|
|
|
31
31
|
type Props = {
|
|
32
32
|
onSave: (data: Record<string, string>) => void;
|
|
@@ -82,7 +82,7 @@ export default function SinglePageForm({ onSave, onCancel }: Props) {
|
|
|
82
82
|
</Field.Root>
|
|
83
83
|
<Field.Root name="description">
|
|
84
84
|
<Field.Label>Description</Field.Label>
|
|
85
|
-
<
|
|
85
|
+
<Textarea rows={4} />
|
|
86
86
|
</Field.Root>
|
|
87
87
|
</form>
|
|
88
88
|
</Layout.Column>
|