@jfdevelops/react-layout 0.16.0 → 0.17.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/package.json +3 -2
- package/skills/build-react-layouts/SKILL.md +132 -0
- package/skills/build-react-layouts/agents/openai.yaml +4 -0
- package/skills/build-react-layouts/references/architecture.md +184 -0
- package/skills/build-react-layouts/references/factory-patterns.md +156 -0
- package/skills/build-react-layouts/references/routing-and-navigation.md +174 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jfdevelops/react-layout",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "A React library for creating layout components.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"files": [
|
|
11
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"skills"
|
|
12
13
|
],
|
|
13
14
|
"sideEffects": false,
|
|
14
15
|
"main": "./dist/index.cjs",
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-react-layouts
|
|
3
|
+
description: Build, extend, refactor, and review typed resource-based React application layouts using @jfdevelops/react-layout. Use when defining shared application shells with defineResourceLayout, generating resource pages with createResourceLayout, creating semantic composables, configuring grouped resource navigation, dispatching route components with createResourceConfig, building create or detail panes, or sharing implementations across multiple resources.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build React Layouts
|
|
7
|
+
|
|
8
|
+
Use `@jfdevelops/react-layout` as a typed application-structure layer.
|
|
9
|
+
|
|
10
|
+
Model the application around a central resource definition, a shared layout
|
|
11
|
+
contract, small resource-specific page configurations, and router adapters that
|
|
12
|
+
dispatch through the resource configuration. Create additional layout
|
|
13
|
+
definitions for structurally different surfaces such as side panes.
|
|
14
|
+
|
|
15
|
+
Do not treat the library as a replacement for routing, data fetching, form
|
|
16
|
+
state, or application business logic.
|
|
17
|
+
|
|
18
|
+
## Follow the workflow
|
|
19
|
+
|
|
20
|
+
1. Inspect the existing layout definition and resource list.
|
|
21
|
+
2. Identify whether the requested surface belongs to an existing layout family.
|
|
22
|
+
3. Add or reuse the resource in the central definition.
|
|
23
|
+
4. Define shared configuration values with `createProp`.
|
|
24
|
+
5. Expose semantic layout regions through composables.
|
|
25
|
+
6. Generate the narrowest suitable page factory.
|
|
26
|
+
7. Connect navigation and route-state dispatch through the helpers returned by
|
|
27
|
+
`defineResourceLayout`.
|
|
28
|
+
8. Run the application's typecheck and relevant tests.
|
|
29
|
+
|
|
30
|
+
Read [references/architecture.md](references/architecture.md) before creating
|
|
31
|
+
or substantially restructuring a layout definition.
|
|
32
|
+
|
|
33
|
+
Read [references/factory-patterns.md](references/factory-patterns.md) when
|
|
34
|
+
choosing between direct creation, `forResource`, `forResources`,
|
|
35
|
+
`makeComposable`, or `createComponent(...).asHOF()`.
|
|
36
|
+
|
|
37
|
+
Read
|
|
38
|
+
[references/routing-and-navigation.md](references/routing-and-navigation.md)
|
|
39
|
+
when integrating resource links, route components, pending states, errors, or
|
|
40
|
+
not-found states.
|
|
41
|
+
|
|
42
|
+
## Apply the core conventions
|
|
43
|
+
|
|
44
|
+
- Import `defineResourceLayout` and `createProp` from
|
|
45
|
+
`@jfdevelops/react-layout`.
|
|
46
|
+
- Import optional preset composables from their dedicated packages.
|
|
47
|
+
- Export the helpers returned by the central `defineResourceLayout` call.
|
|
48
|
+
- Keep the authoritative resource list in the central layout definition.
|
|
49
|
+
- Use resource names that match the application's route vocabulary.
|
|
50
|
+
- Define reusable configuration in `options`.
|
|
51
|
+
- Use `layout.props.include` for definition-time values consumed by the shared
|
|
52
|
+
renderer.
|
|
53
|
+
- Use `layout.props.custom` for props passed when rendering the generated React
|
|
54
|
+
component.
|
|
55
|
+
- Use `createProp.component({ type: 'ReactNode' })` for React content.
|
|
56
|
+
- Mark genuinely optional values with `.optional()`.
|
|
57
|
+
- Give optional custom values defaults in the shared renderer when appropriate.
|
|
58
|
+
- Name composables by semantic role rather than visual implementation.
|
|
59
|
+
- Connect semantic composables to design-system components with `wrapWith`.
|
|
60
|
+
- Use resource-aware composable names so generated components remain
|
|
61
|
+
identifiable in React DevTools.
|
|
62
|
+
- Keep page configuration close to the corresponding resource feature.
|
|
63
|
+
- Keep resource routing generic instead of building a separate route component
|
|
64
|
+
for every resource.
|
|
65
|
+
- Confirm helper signatures against the installed package version and let the
|
|
66
|
+
current TypeScript types determine which resource-configuration branches are
|
|
67
|
+
supported.
|
|
68
|
+
- Reuse the consuming application's router and runtime-validation tools. Do not
|
|
69
|
+
add a schema dependency solely to connect a route parameter to a resource.
|
|
70
|
+
- Create a separate `defineResourceLayout` definition for a surface with a
|
|
71
|
+
meaningfully different structural contract, such as a create or detail pane.
|
|
72
|
+
- Preserve inferred types. Do not use `any`, broad casts, or duplicated
|
|
73
|
+
hand-written resource types to work around a layout mismatch.
|
|
74
|
+
|
|
75
|
+
## Define semantic composables
|
|
76
|
+
|
|
77
|
+
Prefer names that describe application structure, such as `Layout`, `Header`,
|
|
78
|
+
`Content`, `ResourceLayout`, `ResourceHeader`, `ResourceHeaderActions`, and
|
|
79
|
+
`Breadcrumb`.
|
|
80
|
+
|
|
81
|
+
Do not expose every wrapper element as a composable. Expose a region when
|
|
82
|
+
consumers may need to replace it, compose it, style it, or address it through a
|
|
83
|
+
generated component.
|
|
84
|
+
|
|
85
|
+
## Separate definition-time and render-time props
|
|
86
|
+
|
|
87
|
+
Use `options` for values supplied while creating a resource layout:
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
options: {
|
|
91
|
+
title: createProp.component({ type: 'ReactNode' }).optional(),
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Make an option available to the shared renderer through `include`:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
layout: {
|
|
99
|
+
props: {
|
|
100
|
+
include: {
|
|
101
|
+
title: true,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use `custom` for values supplied when rendering the resulting component:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
layout: {
|
|
111
|
+
props: {
|
|
112
|
+
custom: {
|
|
113
|
+
children: createProp.component({ type: 'ReactNode' }),
|
|
114
|
+
actions: createProp.component({ type: 'ReactNode' }).optional(),
|
|
115
|
+
showHeader: createProp.boolean().optional(),
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Keep these categories distinct. Do not turn a definition-time page contract
|
|
122
|
+
into a custom render prop merely because both eventually reach the renderer.
|
|
123
|
+
|
|
124
|
+
## Verify the result
|
|
125
|
+
|
|
126
|
+
1. Typecheck the consuming application.
|
|
127
|
+
2. Run relevant component and route tests.
|
|
128
|
+
3. Exercise at least one resource page.
|
|
129
|
+
4. Exercise pending, error, and not-found dispatch if route configuration
|
|
130
|
+
changed.
|
|
131
|
+
5. Exercise create and detail panes if pane configuration changed.
|
|
132
|
+
6. Confirm grouped navigation generates the intended resource and route.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Application Architecture
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- Central layout definition
|
|
6
|
+
- Shared layout contract
|
|
7
|
+
- Semantic composables
|
|
8
|
+
- Preset composables
|
|
9
|
+
- Secondary layout families
|
|
10
|
+
- Feature organization
|
|
11
|
+
|
|
12
|
+
## Create a central layout definition
|
|
13
|
+
|
|
14
|
+
Create one central definition for resources that share the same application
|
|
15
|
+
shell. Export the generated helpers from that module:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import {
|
|
19
|
+
createProp,
|
|
20
|
+
defineResourceLayout,
|
|
21
|
+
} from '@jfdevelops/react-layout';
|
|
22
|
+
|
|
23
|
+
export const {
|
|
24
|
+
createResourceConfig,
|
|
25
|
+
createResourceLayout,
|
|
26
|
+
createResourceLinks,
|
|
27
|
+
} = defineResourceLayout({
|
|
28
|
+
resources: ['appointments', 'locations', 'services', 'settings'],
|
|
29
|
+
options: {
|
|
30
|
+
title: createProp.component({ type: 'ReactNode' }).optional(),
|
|
31
|
+
},
|
|
32
|
+
layout: {
|
|
33
|
+
// Define the shared contract here.
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Import these generated helpers in feature modules instead of defining parallel
|
|
39
|
+
resource lists or rebuilding the application shell.
|
|
40
|
+
|
|
41
|
+
## Define the shared layout contract
|
|
42
|
+
|
|
43
|
+
Separate definition-time configuration from render-time component props:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
layout: {
|
|
47
|
+
props: {
|
|
48
|
+
include: {
|
|
49
|
+
title: true,
|
|
50
|
+
segments: true,
|
|
51
|
+
},
|
|
52
|
+
custom: {
|
|
53
|
+
children: createProp.component({ type: 'ReactNode' }),
|
|
54
|
+
actions: createProp.component({ type: 'ReactNode' }).optional(),
|
|
55
|
+
showHeader: createProp.boolean().optional(),
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use included props for values that describe the generated page. Use custom
|
|
62
|
+
props for content supplied when the generated page component is rendered.
|
|
63
|
+
|
|
64
|
+
Default optional behavioral props in the renderer:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
render: (
|
|
68
|
+
{ children, actions, showHeader = true, segments },
|
|
69
|
+
{ composables },
|
|
70
|
+
) => {
|
|
71
|
+
// Render the shared shell.
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Create semantic composables
|
|
76
|
+
|
|
77
|
+
Define named layout regions through the scoped `create` function:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
composables: (create) => ({
|
|
81
|
+
Layout: create({
|
|
82
|
+
name: ({ capitalize, resource }) =>
|
|
83
|
+
`${capitalize(resource)}Layout`,
|
|
84
|
+
wrapWith: AppLayout,
|
|
85
|
+
}),
|
|
86
|
+
Header: create({
|
|
87
|
+
name: ({ capitalize, resource }) =>
|
|
88
|
+
`${capitalize(resource)}Header`,
|
|
89
|
+
wrapWith: AppHeader,
|
|
90
|
+
}),
|
|
91
|
+
Content: create({
|
|
92
|
+
name: ({ capitalize, resource }) =>
|
|
93
|
+
`${capitalize(resource)}Content`,
|
|
94
|
+
wrapWith: AppContent,
|
|
95
|
+
}),
|
|
96
|
+
ResourceHeaderActions: create({
|
|
97
|
+
name: ({ capitalize, resource }) =>
|
|
98
|
+
`${capitalize(resource)}ResourceHeaderActions`,
|
|
99
|
+
wrapWith: AppResourceHeaderActions,
|
|
100
|
+
}),
|
|
101
|
+
}),
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Make the composable name communicate the region's role. Use `wrapWith` to
|
|
105
|
+
connect that role to the application's design-system implementation.
|
|
106
|
+
|
|
107
|
+
## Merge preset composables
|
|
108
|
+
|
|
109
|
+
Merge optional preset composables into the same semantic map:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import {
|
|
113
|
+
createBreadcrumbComposable,
|
|
114
|
+
} from '@jfdevelops/react-layout-composable-breadcrumb';
|
|
115
|
+
|
|
116
|
+
composables: (create) => ({
|
|
117
|
+
Layout: create({ name: 'Layout', wrapWith: AppLayout }),
|
|
118
|
+
...createBreadcrumbComposable(({ segments }) => (
|
|
119
|
+
<AppBreadcrumb segments={segments} />
|
|
120
|
+
)),
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Include the preset's required values in `layout.props.include`. Keep
|
|
125
|
+
presentation in the preset renderer and segment definitions in resource page
|
|
126
|
+
configuration.
|
|
127
|
+
|
|
128
|
+
## Create secondary layout families
|
|
129
|
+
|
|
130
|
+
Create another `defineResourceLayout` when a surface has a distinct structural
|
|
131
|
+
contract. A side pane is not merely a variation of the main page shell:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
export const resourcePaneLayout = defineResourceLayout({
|
|
135
|
+
resources: ['create', 'detail'],
|
|
136
|
+
options: {
|
|
137
|
+
title: createProp.component({ type: 'ReactNode' }),
|
|
138
|
+
},
|
|
139
|
+
layout: {
|
|
140
|
+
props: {
|
|
141
|
+
include: {
|
|
142
|
+
title: true,
|
|
143
|
+
},
|
|
144
|
+
custom: {
|
|
145
|
+
children: createProp.component({ type: 'ReactNode' }),
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
render: function Render({ children, title }) {
|
|
149
|
+
return (
|
|
150
|
+
<Panel>
|
|
151
|
+
<PanelHeader>
|
|
152
|
+
<PanelTitle>{title}</PanelTitle>
|
|
153
|
+
</PanelHeader>
|
|
154
|
+
<PanelContent>{children}</PanelContent>
|
|
155
|
+
</Panel>
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Do not overload the main page layout with pane-specific flags and conditional
|
|
163
|
+
branches.
|
|
164
|
+
|
|
165
|
+
## Organize by feature
|
|
166
|
+
|
|
167
|
+
Keep the general layout definition centralized and concrete page definitions
|
|
168
|
+
beside their feature:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
admin/
|
|
172
|
+
├── -utils/
|
|
173
|
+
│ └── layout.tsx
|
|
174
|
+
├── $resource/
|
|
175
|
+
│ ├── -page-config.tsx
|
|
176
|
+
│ ├── -pane/
|
|
177
|
+
│ │ └── layout.tsx
|
|
178
|
+
│ ├── -appointments/
|
|
179
|
+
│ │ └── pages-config.ts
|
|
180
|
+
│ ├── -locations/
|
|
181
|
+
│ │ └── pages-config.ts
|
|
182
|
+
│ └── index.tsx
|
|
183
|
+
└── route.tsx
|
|
184
|
+
```
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Factory Patterns
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- Direct creation
|
|
6
|
+
- Resource-scoped factories
|
|
7
|
+
- Composable detail pages
|
|
8
|
+
- Multi-resource subsets
|
|
9
|
+
- Higher-order multi-resource components
|
|
10
|
+
- Selection guide
|
|
11
|
+
|
|
12
|
+
## Create one page directly
|
|
13
|
+
|
|
14
|
+
Use direct creation for one isolated page:
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
export const WaitlistPage = createResourceLayout({
|
|
18
|
+
resource: 'waitlist',
|
|
19
|
+
name: 'WaitlistPage',
|
|
20
|
+
segments: {
|
|
21
|
+
title: 'Waitlist',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Provide an explicit `name` when the surrounding factory does not establish one.
|
|
27
|
+
|
|
28
|
+
## Scope a factory to one resource
|
|
29
|
+
|
|
30
|
+
Use `forResource` when multiple page definitions share one resource:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
const createLocationsPage = createResourceLayout.forResource({
|
|
34
|
+
resource: 'locations',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const LocationsPage = createLocationsPage({
|
|
38
|
+
segments: {
|
|
39
|
+
title: 'Locations',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Keep the scoped factory private unless several modules genuinely need it.
|
|
45
|
+
|
|
46
|
+
## Make a generated page composable
|
|
47
|
+
|
|
48
|
+
Use `makeComposable` when a page needs to retain the resource layout contract
|
|
49
|
+
while allowing consumers to address or rearrange semantic regions:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
export const LocationsDetailPage = LocationsPage.makeComposable({
|
|
53
|
+
name: 'LocationsDetailPage',
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Prefer this over copying the parent layout or manually reproducing its header
|
|
58
|
+
and content structure. Do not call `makeComposable` when consumers only need to
|
|
59
|
+
pass `children` or other declared custom props.
|
|
60
|
+
|
|
61
|
+
## Narrow a factory to several resources
|
|
62
|
+
|
|
63
|
+
Use `forResources` when a shared implementation applies to a known subset:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
const createCalendarPane =
|
|
67
|
+
resourcePaneLayout.createResourceLayout.forResources('create', 'detail');
|
|
68
|
+
|
|
69
|
+
export const createCalendarDetailPane = createCalendarPane.forResource({
|
|
70
|
+
resource: 'detail',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const createCalendarCreatePane = createCalendarPane.forResource({
|
|
74
|
+
resource: 'create',
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Narrow the resource set before creating the final resource-specific factory.
|
|
79
|
+
|
|
80
|
+
## Create a higher-order multi-resource component
|
|
81
|
+
|
|
82
|
+
Use the advanced component factory when several resources share behavior but
|
|
83
|
+
need distinct render implementations:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
const createCalendarPage = createResourceLayout
|
|
87
|
+
.forResources('appointments', 'availability')
|
|
88
|
+
.createComponent({
|
|
89
|
+
resources: {
|
|
90
|
+
appointments: {
|
|
91
|
+
segments: {
|
|
92
|
+
title: 'Appointments',
|
|
93
|
+
},
|
|
94
|
+
render: function AppointmentsRender() {
|
|
95
|
+
return <AppointmentsView />;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
availability: {
|
|
99
|
+
segments: {
|
|
100
|
+
title: 'Availability',
|
|
101
|
+
},
|
|
102
|
+
render: function AvailabilityRender() {
|
|
103
|
+
return <AvailabilityView />;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
render: function Render({ resource, children }, context) {
|
|
108
|
+
return (
|
|
109
|
+
<context.Root>
|
|
110
|
+
{children ??
|
|
111
|
+
(resource === 'appointments' ? (
|
|
112
|
+
<context.Appointments />
|
|
113
|
+
) : (
|
|
114
|
+
<context.Availability />
|
|
115
|
+
))}
|
|
116
|
+
</context.Root>
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
.asHOF();
|
|
121
|
+
|
|
122
|
+
export const AppointmentsPage = createCalendarPage('appointments');
|
|
123
|
+
export const AvailabilityPage = createCalendarPage('availability');
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Use this pattern only when the resources share meaningful behavior, controls,
|
|
127
|
+
or state transitions. The outer renderer should own behavior shared by the
|
|
128
|
+
resource subset. Each resource renderer should own resource-specific data and
|
|
129
|
+
content.
|
|
130
|
+
|
|
131
|
+
Attach closely related states when that creates a useful page namespace:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
export const AppointmentsPage = Object.assign(
|
|
135
|
+
createCalendarPage('appointments'),
|
|
136
|
+
{
|
|
137
|
+
Loading: AppointmentsLoading,
|
|
138
|
+
Error: AppointmentsError,
|
|
139
|
+
NotFound: AppointmentsNotFound,
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Use this for components that are semantically part of the generated page
|
|
145
|
+
surface. Avoid turning the page component into an unrelated utility namespace.
|
|
146
|
+
|
|
147
|
+
## Choose the narrowest factory
|
|
148
|
+
|
|
149
|
+
- For one page, call `createResourceLayout`.
|
|
150
|
+
- For several pages belonging to one resource, use `forResource`.
|
|
151
|
+
- For several resources with the same configuration shape, use `forResources`.
|
|
152
|
+
- When consumers need semantic subcomponents, use `makeComposable`.
|
|
153
|
+
- When several resources share an outer behavior but need distinct
|
|
154
|
+
implementations, use `createComponent(...).asHOF()`.
|
|
155
|
+
- When the surface has a different structural contract, create another
|
|
156
|
+
`defineResourceLayout`.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Routing and Navigation
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- Grouped resource links
|
|
6
|
+
- Resource configuration
|
|
7
|
+
- Route dispatch
|
|
8
|
+
- Runtime route validation
|
|
9
|
+
- Async route states
|
|
10
|
+
- Separation of concerns
|
|
11
|
+
|
|
12
|
+
## Generate grouped resource links
|
|
13
|
+
|
|
14
|
+
Generate navigation from the layout's resource vocabulary:
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
const navigation = createResourceLinks.withGroups([
|
|
18
|
+
{
|
|
19
|
+
links: {
|
|
20
|
+
dashboard: {
|
|
21
|
+
label: 'Dashboard',
|
|
22
|
+
icon: <DashboardIcon />,
|
|
23
|
+
href: '/admin',
|
|
24
|
+
},
|
|
25
|
+
appointments: {
|
|
26
|
+
label: 'Appointments',
|
|
27
|
+
icon: <AppointmentsIcon />,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: 'Configuration',
|
|
33
|
+
links: {
|
|
34
|
+
settings: {
|
|
35
|
+
label: 'Settings',
|
|
36
|
+
icon: <SettingsIcon />,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Use groups to express information architecture. Let normal resources use the
|
|
44
|
+
shared resource route. Provide `href` only for special destinations whose route
|
|
45
|
+
does not follow the general pattern.
|
|
46
|
+
|
|
47
|
+
Render links using the generated `resource` value instead of reconstructing it
|
|
48
|
+
from labels.
|
|
49
|
+
|
|
50
|
+
## Create the resource configuration
|
|
51
|
+
|
|
52
|
+
Create a typed configuration that maps each routeable resource to its normal
|
|
53
|
+
and asynchronous states:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
export const resourcePageConfig = createResourceConfig({
|
|
57
|
+
appointments: {
|
|
58
|
+
component: <AppointmentsPage />,
|
|
59
|
+
pendingComponent: (
|
|
60
|
+
<AppointmentsPage>
|
|
61
|
+
<AppointmentsPage.Loading />
|
|
62
|
+
</AppointmentsPage>
|
|
63
|
+
),
|
|
64
|
+
errorComponent: (
|
|
65
|
+
<AppointmentsPage>
|
|
66
|
+
<AppointmentsPage.Error />
|
|
67
|
+
</AppointmentsPage>
|
|
68
|
+
),
|
|
69
|
+
notFoundComponent: (
|
|
70
|
+
<AppointmentsPage>
|
|
71
|
+
<AppointmentsPage.NotFound />
|
|
72
|
+
</AppointmentsPage>
|
|
73
|
+
),
|
|
74
|
+
},
|
|
75
|
+
locations: {
|
|
76
|
+
component: <Locations />,
|
|
77
|
+
pendingComponent: <LocationsLoading />,
|
|
78
|
+
errorComponent: <LocationsError />,
|
|
79
|
+
notFoundComponent: <LocationsNotFound />,
|
|
80
|
+
detail: {
|
|
81
|
+
component: <LocationDetail />,
|
|
82
|
+
pendingComponent: <LocationDetailLoading />,
|
|
83
|
+
errorComponent: <LocationDetailError />,
|
|
84
|
+
notFoundComponent: <LocationDetailNotFound />,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Keep this mapping declarative. Keep data fetching and mutation logic inside the
|
|
91
|
+
corresponding feature components or router loaders.
|
|
92
|
+
|
|
93
|
+
## Dispatch route components generically
|
|
94
|
+
|
|
95
|
+
Use one generic route adapter to select the configured component:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
type ResourceComponentKey =
|
|
99
|
+
| 'component'
|
|
100
|
+
| 'pendingComponent'
|
|
101
|
+
| 'errorComponent'
|
|
102
|
+
| 'notFoundComponent';
|
|
103
|
+
|
|
104
|
+
function getResourceComponent(component: ResourceComponentKey) {
|
|
105
|
+
const resource = Route.useParams({
|
|
106
|
+
select: ({ resource }) => resource,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return resourcePageConfig.getComponent.forResource({
|
|
110
|
+
resource,
|
|
111
|
+
})({
|
|
112
|
+
component,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function RouteComponent() {
|
|
117
|
+
return getResourceComponent('component');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function PendingComponent() {
|
|
121
|
+
return getResourceComponent('pendingComponent');
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Use the same mechanism for error and not-found states. Do not create resource
|
|
126
|
+
condition chains in the generic route component when the information belongs
|
|
127
|
+
in `createResourceConfig`.
|
|
128
|
+
|
|
129
|
+
## Validate route parameters at runtime
|
|
130
|
+
|
|
131
|
+
The layout provides compile-time resource constraints. The router must still
|
|
132
|
+
validate untrusted URL parameters at runtime.
|
|
133
|
+
|
|
134
|
+
Parse or validate the route parameter before passing it to generated resource
|
|
135
|
+
helpers. Reject invalid resources through the router's normal not-found
|
|
136
|
+
mechanism. Keep routing validation in the router layer instead of embedding it
|
|
137
|
+
in the shared layout renderer.
|
|
138
|
+
|
|
139
|
+
## Preserve the layout during async states
|
|
140
|
+
|
|
141
|
+
Wrap pending, error, and not-found content in the same generated page shell
|
|
142
|
+
when the shell should remain visible during those states.
|
|
143
|
+
|
|
144
|
+
Use a separate route-state surface when a detail view cannot render its normal
|
|
145
|
+
page structure without loaded data. Keep the mapping explicit so one resource's
|
|
146
|
+
state cannot appear inside another resource's layout.
|
|
147
|
+
|
|
148
|
+
## Keep ownership clear
|
|
149
|
+
|
|
150
|
+
Let the layout layer own:
|
|
151
|
+
|
|
152
|
+
- Valid resource vocabulary
|
|
153
|
+
- Shared application structure
|
|
154
|
+
- Page configuration contracts
|
|
155
|
+
- Semantic composition points
|
|
156
|
+
- Navigation metadata
|
|
157
|
+
- Resource-to-component dispatch
|
|
158
|
+
|
|
159
|
+
Let the router own:
|
|
160
|
+
|
|
161
|
+
- URL parsing
|
|
162
|
+
- Search parameter validation
|
|
163
|
+
- Redirects
|
|
164
|
+
- Loaders
|
|
165
|
+
- Route lifecycle selection
|
|
166
|
+
- Runtime not-found behavior
|
|
167
|
+
|
|
168
|
+
Let feature components own:
|
|
169
|
+
|
|
170
|
+
- Queries and mutations
|
|
171
|
+
- Feature-level loading details
|
|
172
|
+
- Tables and calendars
|
|
173
|
+
- Forms
|
|
174
|
+
- Resource-specific interactions
|