jattac.libs.web.zest-button 1.2.9 → 1.4.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/ZestButton.d.ts +19 -2
- package/dist/ZestDropdownMenu.d.ts +16 -0
- package/dist/ZestDropdownMenuItem.d.ts +9 -0
- package/dist/index.cjs.js +163 -8
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +19 -2
- package/dist/index.esm.js +143 -9
- package/dist/index.esm.js.map +1 -1
- package/docs/e2e/critical-paths.md +75 -0
- package/docs/features/zest-button-dropdown-options/BRS.md +213 -0
- package/docs/guidelines/AI_ARCHITECTURE.md +455 -0
- package/docs/guidelines/AI_BRS.md +435 -0
- package/docs/guidelines/AI_CODE_REVIEW.md +198 -0
- package/docs/guidelines/AI_E2E_TESTING.md +227 -0
- package/docs/guidelines/AI_GIT_WORKFLOW.md +595 -0
- package/docs/guidelines/AI_KNOWLEDGE.md +213 -0
- package/docs/guidelines/AI_PITFALLS.md +245 -0
- package/docs/guidelines/AI_STYLE_GUIDE.md +162 -0
- package/docs/guidelines/AI_TESTING.md +275 -0
- package/docs/guidelines/AI_TEST_CONFIGURATION.md +529 -0
- package/docs/guidelines/AI_WORKFLOW.md +442 -0
- package/docs/guidelines/AI_WORKFLOW_TRIGGERS.md +222 -0
- package/docs/guidelines/ai-knowledge.md +154 -0
- package/docs/guidelines/decision-log.md +149 -0
- package/package.json +78 -69
- package/dist/ZestContext.d.ts +0 -9
- package/dist/ZestProvider.d.ts +0 -8
- package/dist/semanticTypeDefaults.d.ts +0 -4
- package/docs/api.md +0 -140
- package/docs/breaking-changes.md +0 -143
- package/docs/configuration.md +0 -214
- package/docs/development.md +0 -93
- package/docs/examples.md +0 -227
- package/docs/features.md +0 -126
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# Architectural Rules
|
|
2
|
+
|
|
3
|
+
Business behaviour is more important than implementation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Composition
|
|
8
|
+
|
|
9
|
+
Composition SHALL be preferred over modification of existing code.
|
|
10
|
+
|
|
11
|
+
Decision process:
|
|
12
|
+
|
|
13
|
+
Can composition reasonably solve the problem?
|
|
14
|
+
|
|
15
|
+
YES
|
|
16
|
+
|
|
17
|
+
Use composition.
|
|
18
|
+
|
|
19
|
+
NO
|
|
20
|
+
|
|
21
|
+
Continue.
|
|
22
|
+
|
|
23
|
+
Does existing code violate business behaviour?
|
|
24
|
+
|
|
25
|
+
YES
|
|
26
|
+
|
|
27
|
+
Modify existing code.
|
|
28
|
+
|
|
29
|
+
NO
|
|
30
|
+
|
|
31
|
+
Continue.
|
|
32
|
+
|
|
33
|
+
Would composition become unreasonable?
|
|
34
|
+
|
|
35
|
+
YES
|
|
36
|
+
|
|
37
|
+
Modify existing code.
|
|
38
|
+
|
|
39
|
+
NO
|
|
40
|
+
|
|
41
|
+
Use composition.
|
|
42
|
+
|
|
43
|
+
When composition is chosen, the new code MUST NOT alter the behaviour of existing code.
|
|
44
|
+
|
|
45
|
+
Composition means addition, not modification.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
# Existing Code
|
|
50
|
+
|
|
51
|
+
Existing code SHALL NOT be rewritten merely because another implementation is preferred.
|
|
52
|
+
|
|
53
|
+
Do not replace architecture.
|
|
54
|
+
|
|
55
|
+
Do not modernise code.
|
|
56
|
+
|
|
57
|
+
Do not rename symbols.
|
|
58
|
+
|
|
59
|
+
Do not move files.
|
|
60
|
+
|
|
61
|
+
Do not reformat unrelated code.
|
|
62
|
+
|
|
63
|
+
Do not optimise speculative bottlenecks.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
# Behaviour
|
|
68
|
+
|
|
69
|
+
Existing behaviour is assumed correct unless:
|
|
70
|
+
|
|
71
|
+
• failing tests prove otherwise
|
|
72
|
+
|
|
73
|
+
OR
|
|
74
|
+
|
|
75
|
+
• user explicitly requests behavioural change
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
# Scope
|
|
80
|
+
|
|
81
|
+
Only modify code necessary to complete the requested task.
|
|
82
|
+
|
|
83
|
+
Never perform opportunistic refactors.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
# Simplicity
|
|
88
|
+
|
|
89
|
+
Choose the smallest correct change.
|
|
90
|
+
|
|
91
|
+
Smaller changes reduce regression risk.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
# Unknowns
|
|
96
|
+
|
|
97
|
+
When uncertain:
|
|
98
|
+
|
|
99
|
+
Stop.
|
|
100
|
+
|
|
101
|
+
Explain uncertainty.
|
|
102
|
+
|
|
103
|
+
Never guess business rules.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
# Absolute Prohibitions
|
|
108
|
+
|
|
109
|
+
The following MUST NOT be done unless explicitly instructed by the user.
|
|
110
|
+
|
|
111
|
+
## Dependencies
|
|
112
|
+
|
|
113
|
+
Do NOT change package versions, SDK versions, or tool versions.
|
|
114
|
+
|
|
115
|
+
Do NOT add NuGet packages, npm packages, pip packages, Cargo crates, Go modules, or any other dependencies.
|
|
116
|
+
|
|
117
|
+
Do NOT remove dependencies.
|
|
118
|
+
|
|
119
|
+
## Configuration
|
|
120
|
+
|
|
121
|
+
Do NOT modify configuration files (appsettings.json, .env, docker-compose.yml, app.config, web.config, or any other config).
|
|
122
|
+
|
|
123
|
+
Do NOT modify environment variables.
|
|
124
|
+
|
|
125
|
+
Do NOT modify secrets or secret references.
|
|
126
|
+
|
|
127
|
+
## CI/CD and Infrastructure
|
|
128
|
+
|
|
129
|
+
Do NOT modify CI/CD pipelines, build scripts, or deployment configurations.
|
|
130
|
+
|
|
131
|
+
Do NOT modify GitHub Actions, Azure DevOps pipelines, or any other CI configuration.
|
|
132
|
+
|
|
133
|
+
Do NOT modify Docker files or container configurations.
|
|
134
|
+
|
|
135
|
+
Do NOT modify infrastructure-as-code files.
|
|
136
|
+
|
|
137
|
+
## Database
|
|
138
|
+
|
|
139
|
+
Do NOT modify database migrations.
|
|
140
|
+
|
|
141
|
+
Do NOT modify database schema.
|
|
142
|
+
|
|
143
|
+
Do NOT modify stored procedures.
|
|
144
|
+
|
|
145
|
+
Do NOT modify database scripts.
|
|
146
|
+
|
|
147
|
+
Do NOT create new database tables or columns.
|
|
148
|
+
|
|
149
|
+
## Code Changes
|
|
150
|
+
|
|
151
|
+
Do NOT add comments unless explicitly instructed.
|
|
152
|
+
|
|
153
|
+
Do NOT add logging unless explicitly instructed.
|
|
154
|
+
|
|
155
|
+
Do NOT add error handling (try-catch, null checks, defensive code) unless explicitly instructed.
|
|
156
|
+
|
|
157
|
+
Do NOT add TODO comments.
|
|
158
|
+
|
|
159
|
+
Do NOT add comments about code that needs improvement.
|
|
160
|
+
|
|
161
|
+
Do NOT change method signatures, return types, or public API surface.
|
|
162
|
+
|
|
163
|
+
Do NOT change access modifiers (private to public, etc.).
|
|
164
|
+
|
|
165
|
+
Do NOT change synchronous code to asynchronous.
|
|
166
|
+
|
|
167
|
+
Do NOT add async/await where it was not present.
|
|
168
|
+
|
|
169
|
+
Do NOT add ConfigureAwait(false).
|
|
170
|
+
|
|
171
|
+
Do NOT add null-forgiving operators (!) to silence warnings.
|
|
172
|
+
|
|
173
|
+
Do NOT add #pragma disable to silence warnings.
|
|
174
|
+
|
|
175
|
+
Do NOT add SuppressMessage attributes.
|
|
176
|
+
|
|
177
|
+
Do NOT add Obsolete attributes.
|
|
178
|
+
|
|
179
|
+
Do NOT add EditorBrowsable(Never).
|
|
180
|
+
|
|
181
|
+
Do NOT add InternalsVisibleTo for test projects.
|
|
182
|
+
|
|
183
|
+
Do NOT add blanket try-catch blocks.
|
|
184
|
+
|
|
185
|
+
Do NOT swallow exceptions.
|
|
186
|
+
|
|
187
|
+
Do NOT change exception types.
|
|
188
|
+
|
|
189
|
+
Do NOT change return values.
|
|
190
|
+
|
|
191
|
+
## Formatting and Naming
|
|
192
|
+
|
|
193
|
+
Do NOT rename variables, methods, classes, or any other symbols.
|
|
194
|
+
|
|
195
|
+
Do NOT reorder methods, properties, fields, or any other members.
|
|
196
|
+
|
|
197
|
+
Do NOT reformat code that was not part of the request.
|
|
198
|
+
|
|
199
|
+
Do NOT change whitespace or line breaks in unrelated code.
|
|
200
|
+
|
|
201
|
+
Do NOT change using directives or import statements.
|
|
202
|
+
|
|
203
|
+
Do NOT change namespace declarations.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
# Change Justification
|
|
208
|
+
|
|
209
|
+
Every change you make MUST be directly traceable to a specific requirement in the user's request.
|
|
210
|
+
|
|
211
|
+
If you cannot draw a direct line from the user's words to the change, do not make the change.
|
|
212
|
+
|
|
213
|
+
If you are unsure whether a change is required, ask the user.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
# Rollback
|
|
218
|
+
|
|
219
|
+
Every change MUST be independently revertible.
|
|
220
|
+
|
|
221
|
+
Do not create changes that depend on each other unless they are part of the same logical unit.
|
|
222
|
+
|
|
223
|
+
If a change cannot be independently reverted, document this in the change manifest.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
# Project-Specific Architecture — Lattice Admin Web
|
|
228
|
+
|
|
229
|
+
These are the concrete conventions of this repository. They are the "pattern discovery" reference required by AI_WORKFLOW.md Step 8 — match new code to these before inventing anything new.
|
|
230
|
+
|
|
231
|
+
## Stack
|
|
232
|
+
|
|
233
|
+
Next.js (App Router), TypeScript, React class components for stateful pages.
|
|
234
|
+
|
|
235
|
+
## Project layout
|
|
236
|
+
|
|
237
|
+
Features live under `app/admin/` (admin-only) or `app/` (shared). Each feature contains:
|
|
238
|
+
- `page.tsx` — Next.js route entry point
|
|
239
|
+
- `Data/IModel.ts` — TypeScript interface mirroring the backend model
|
|
240
|
+
- `Data/ModelApiAccessor.ts` — HTTP calls to the backend
|
|
241
|
+
- `State/ModelRepository.ts` — extends `RepositoryBase`, holds reactive state
|
|
242
|
+
- `State/ModelLogic.ts` — extends `LogicBase`, contains all business logic
|
|
243
|
+
- `UI/ModelManager.tsx` — the page component
|
|
244
|
+
|
|
245
|
+
## Page entry pattern
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
"use client";
|
|
249
|
+
import ManagerAccount from "@/app/account/UI/ManagerAccount";
|
|
250
|
+
import FooManager from "./UI/FooManager";
|
|
251
|
+
|
|
252
|
+
export default function FooPage() {
|
|
253
|
+
return (
|
|
254
|
+
<ManagerAccount>
|
|
255
|
+
<FooManager />
|
|
256
|
+
</ManagerAccount>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## State management
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
// Repository — holds only state, no logic
|
|
265
|
+
export default class FooRepository extends RepositoryBase {
|
|
266
|
+
items: IFoo[] = [];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Logic — all async operations go here
|
|
270
|
+
const logic = new FooLogic(); // instantiated at module level, outside the component
|
|
271
|
+
|
|
272
|
+
export default class FooLogic extends LogicBase<FooRepository, IFoo> {
|
|
273
|
+
repository = new FooRepository();
|
|
274
|
+
model = {} as IFoo;
|
|
275
|
+
|
|
276
|
+
async initializeAsync() {
|
|
277
|
+
await this.runner(async () => {
|
|
278
|
+
this.repository.items = await new FooApiAccessor().getAllAsync();
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
- `runner()` handles the `busy` flag automatically — always use it for async operations
|
|
284
|
+
- Logic is instantiated at module level (outside the component class), not inside it
|
|
285
|
+
|
|
286
|
+
## Components
|
|
287
|
+
|
|
288
|
+
- Stateful pages use `PureComponent` class components with `logic.setRerender(() => this.forceUpdate())` in `componentDidMount`
|
|
289
|
+
- `componentDidMount` must call `logic.initializeAsync()`
|
|
290
|
+
- Use `FrostedGlassOverlay show={logic.repository.busy}` to block interaction during loads
|
|
291
|
+
- Use `PageContainer title="..." subtitle="..."` as the top-level wrapper
|
|
292
|
+
|
|
293
|
+
## API accessors
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
export default class FooApiAccessor extends ApiAccessor {
|
|
297
|
+
constructor() {
|
|
298
|
+
super({ controller: "foo", backendService: "fua" }); // or "lattice"
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async getAllAsync(): Promise<IFoo[]> {
|
|
302
|
+
return await this.getUnwrappedAsync<IFoo[]>({ url: "items" });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async createAsync(payload: Omit<IFoo, "id">): Promise<void> {
|
|
306
|
+
await this.postWithUnwrappedResponseAsync({ url: "items", body: payload });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
- `fetchData` supports only `"GET"` and `"POST"` — there is no DELETE method
|
|
311
|
+
- Delete operations must POST to a delete endpoint: `postWithUnwrappedResponseAsync({ url: "items/delete/${id}", body: {} })`
|
|
312
|
+
- URL segments must match the backend controller route exactly (controller class name minus "Controller", lowercase)
|
|
313
|
+
|
|
314
|
+
## UI components
|
|
315
|
+
|
|
316
|
+
| Need | Component |
|
|
317
|
+
|---|---|
|
|
318
|
+
| Page layout | `PageContainer` with `title` and `subtitle` |
|
|
319
|
+
| Sidepane layout | `ZestResponsiveLayout` with `sidePane` prop |
|
|
320
|
+
| Buttons | `ZestButton` with `visualOptions={{ variant: "success" \| "danger" \| "info" }}` |
|
|
321
|
+
| Text inputs | `ZestTextbox` — use `isMultiline` + `rows` for textareas, `stretch` for full width |
|
|
322
|
+
| Dropdowns | `SelectWrapper` — always use this, never a raw `<select>` |
|
|
323
|
+
| Forms | See form system section below |
|
|
324
|
+
| Tables | `ResponsiveTable` with `data` and `columnDefinitions` (each with `cellRenderer` + `displayLabel`) |
|
|
325
|
+
| Action rows | `ActionBar` |
|
|
326
|
+
| Loading overlay | `FrostedGlassOverlay show={logic.repository.busy}` |
|
|
327
|
+
|
|
328
|
+
## Form system
|
|
329
|
+
|
|
330
|
+
All forms use the component system from `@/app/Forms/Form/UI/Form`. Never style inputs or form layouts with raw inline styles.
|
|
331
|
+
|
|
332
|
+
### Component hierarchy
|
|
333
|
+
|
|
334
|
+
```
|
|
335
|
+
Form
|
|
336
|
+
FormSection? ← optional named section (title + subtle background, good for long standalone forms)
|
|
337
|
+
FormControlGroup? ← optional titled group (dashed border header, good for sidepane sections)
|
|
338
|
+
FormRow ← horizontal flex row, wraps on small screens
|
|
339
|
+
FormControlGroup ← one input+label unit; min-width 250px so it auto-wraps when space runs out
|
|
340
|
+
FormLabel ← shows label + required asterisk by default
|
|
341
|
+
{input} ← ZestTextbox, SelectWrapper, checkbox etc.
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Rules
|
|
345
|
+
|
|
346
|
+
**Always use `FormRow` to place related fields side by side.** Never stack items that logically belong together by leaving them as sequential block elements — use `FormRow` so they share a row when space allows and wrap gracefully on narrow screens.
|
|
347
|
+
|
|
348
|
+
**Every input must be wrapped in `FormControlGroup > FormLabel + input`.** No bare inputs, no bare labels.
|
|
349
|
+
|
|
350
|
+
**Mark optional fields.** `FormLabel` adds `*` by default (required). Add `optional` prop for non-required fields:
|
|
351
|
+
```tsx
|
|
352
|
+
<FormLabel optional>Description</FormLabel>
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Use `FormControlGroup title="..."` to group related fields in a sidepane.** This renders a dashed section header — use it instead of `FormSection` inside sidepanes where vertical space is at a premium:
|
|
356
|
+
```tsx
|
|
357
|
+
<FormControlGroup title="Trigger Settings">
|
|
358
|
+
<FormRow>
|
|
359
|
+
<FormControlGroup>...</FormControlGroup>
|
|
360
|
+
<FormControlGroup>...</FormControlGroup>
|
|
361
|
+
</FormRow>
|
|
362
|
+
</FormControlGroup>
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Use `FormSection` for standalone full-page forms** where sections need more visual weight (title + `#f9fafb` background panel).
|
|
366
|
+
|
|
367
|
+
**Use `SelectWrapper` for all dropdowns**, including static option lists:
|
|
368
|
+
```tsx
|
|
369
|
+
const RISK_LEVELS = [
|
|
370
|
+
{ value: "High", label: "High" },
|
|
371
|
+
{ value: "Critical", label: "Critical" },
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
<SelectWrapper
|
|
375
|
+
data={RISK_LEVELS}
|
|
376
|
+
selectedResolver={(r) => r.value === item.riskLevel}
|
|
377
|
+
valueResolver={(r) => r.value}
|
|
378
|
+
labelResolver={(r) => r.label}
|
|
379
|
+
onChange={(selected) => update({ riskLevel: selected[0]?.value ?? "High" })}
|
|
380
|
+
isClearable={false}
|
|
381
|
+
isSearchable={false}
|
|
382
|
+
/>
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Full-width fields** (long text, description, multiline) go in a standalone `FormControlGroup` outside a `FormRow` — they naturally fill 100% width. Use `stretch` on `ZestTextbox`:
|
|
386
|
+
```tsx
|
|
387
|
+
<FormControlGroup>
|
|
388
|
+
<FormLabel optional>Description</FormLabel>
|
|
389
|
+
<ZestTextbox type="text" stretch value={...} onChange={...} />
|
|
390
|
+
</FormControlGroup>
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Number inputs** go in `FormRow` with their peers — never alone as a block element:
|
|
394
|
+
```tsx
|
|
395
|
+
<FormRow>
|
|
396
|
+
<FormControlGroup>
|
|
397
|
+
<FormLabel>Threshold (events)</FormLabel>
|
|
398
|
+
<ZestTextbox type="number" value={...} onChange={...} />
|
|
399
|
+
</FormControlGroup>
|
|
400
|
+
<FormControlGroup>
|
|
401
|
+
<FormLabel>Window (days)</FormLabel>
|
|
402
|
+
<ZestTextbox type="number" value={...} onChange={...} />
|
|
403
|
+
</FormControlGroup>
|
|
404
|
+
</FormRow>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Checkboxes** — no styled component exists; use native `<input type="checkbox">` inside `FormControlGroup`:
|
|
408
|
+
```tsx
|
|
409
|
+
<FormControlGroup>
|
|
410
|
+
<FormLabel>Enabled</FormLabel>
|
|
411
|
+
<input
|
|
412
|
+
type="checkbox"
|
|
413
|
+
checked={item.isEnabled}
|
|
414
|
+
onChange={(e) => update({ isEnabled: e.target.checked })}
|
|
415
|
+
style={{ width: 16, height: 16, marginTop: 6 }}
|
|
416
|
+
/>
|
|
417
|
+
</FormControlGroup>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Sidepane form reference layout
|
|
421
|
+
|
|
422
|
+
For a sidepane create/edit form, structure fields in this order:
|
|
423
|
+
1. Identity fields (name, code, key identifier) — `FormRow` with type/category/risk selector
|
|
424
|
+
2. Optional descriptive fields — standalone full-width `FormControlGroup`
|
|
425
|
+
3. Related configuration fields — `FormControlGroup title="..."` wrapping a `FormRow`
|
|
426
|
+
4. Long text fields (messages, notes) — `FormControlGroup title="..."` wrapping individual full-width fields
|
|
427
|
+
5. Toggle/boolean fields — `FormRow` of checkboxes
|
|
428
|
+
6. Help/instruction panel — `<div>` with `background: #f9fafb`, 11px text, business-language explanations
|
|
429
|
+
|
|
430
|
+
## Sidepane pattern
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
<ZestResponsiveLayout
|
|
434
|
+
sidePane={{
|
|
435
|
+
visible: !!this.state.itemForSidePane,
|
|
436
|
+
title: this.#isEditing ? `Edit — ${item.name}` : "Add Item",
|
|
437
|
+
pane: this.#sidePaneContent ?? <></>,
|
|
438
|
+
onClose: () => this.setState({ itemForSidePane: undefined }),
|
|
439
|
+
}}
|
|
440
|
+
desktopSidePaneWidth="500px"
|
|
441
|
+
enableBounceAnimation={false}
|
|
442
|
+
detailPane={<>...table and action bar...</>}
|
|
443
|
+
/>
|
|
444
|
+
```
|
|
445
|
+
- Never use modals for create/edit forms — always use the sidepane
|
|
446
|
+
- Sidepane title adapts: "Add X" for new, "Edit — {name}" for existing
|
|
447
|
+
|
|
448
|
+
## Navigation
|
|
449
|
+
|
|
450
|
+
New admin pages must be registered in `app/account/UI/MenuItems/AdminMenuItemsProvider.tsx` under the correct parent group.
|
|
451
|
+
|
|
452
|
+
## Routing
|
|
453
|
+
|
|
454
|
+
- Admin pages: `app/admin/[feature]/page.tsx`
|
|
455
|
+
- Route segments must be kebab-case
|