jattac.libs.web.zest-button 1.4.0 → 1.5.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.
@@ -1,213 +0,0 @@
1
- # BRS — ZestButton dropdown options (split button)
2
-
3
- ## Metadata
4
-
5
- | Field | Value |
6
- |-------|-------|
7
- | Status | Complete |
8
- | Task Type | Feature |
9
- | Author | Claude (collaborating with repo owner) |
10
- | Created | 2026-07-28 |
11
- | Last Updated | 2026-07-28 |
12
- | Approved | Yes — user approved full scope as drafted, 2026-07-28 |
13
-
14
- ### Implementation notes
15
-
16
- See `test-reports/CHANGE_MANIFEST_dropdown-options.md` for the full file list, four implementation deviations from this document's original Technical Design (each empirically discovered, not guessed), and the coverage-regression fix. All 9 acceptance criteria satisfied; 81/81 tests pass; coverage 97.21% (up from the prior 95.38% baseline).
17
-
18
- ---
19
-
20
- ## Problem Statement
21
-
22
- **As a** developer using ZestButton, **I want** a way to attach a set of optional secondary actions to a button that already has one obvious default action, **so that** the button visually communicates "there's more here" and gives users a dedicated, discoverable way to reach those alternatives without cluttering the UI with several separate buttons.
23
-
24
- ---
25
-
26
- ## Business Value / Impact
27
-
28
- Several places in Jattac products need "primary action + a few situational alternatives" (e.g. Save / Save & Close / Save As, Export / Export as CSV / Export as PDF). Today that either forces multiple separate buttons or a bespoke one-off menu per screen. A first-class ZestButton capability makes this consistent, accessible, and reusable everywhere ZestButton already is.
29
-
30
- ---
31
-
32
- ## Acceptance Criteria
33
-
34
- ### 1: Split-button rendering
35
-
36
- - **Given** `zest.dropdownOptions` is a non-empty array
37
- - **When** `ZestButton` renders
38
- - **Then** it renders as two visually-fused segments sharing one pill shape: the existing button content/behavior on the left, and a chevron ("more options") segment on the right, separated by a subtle divider
39
- - **And** when `dropdownOptions` is absent or empty, `ZestButton` renders exactly as it does today — zero visual or behavioral change (this is the core regression constraint for every other AC)
40
-
41
- ### 2: Main segment always fires the default action directly
42
-
43
- - **Given** the split-button variant is rendered
44
- - **When** the user clicks/taps/keyboard-activates the main (left) segment
45
- - **Then** it behaves exactly like today's `ZestButton` `onClick` — no menu involvement, no extra click required. The dropdown never intercepts or delays the primary action.
46
-
47
- ### 3: Chevron segment opens/closes the menu
48
-
49
- - **Given** the split-button variant is rendered
50
- - **When** the user clicks/taps/keyboard-activates (Enter/Space) the chevron segment
51
- - **Then** a menu listing `dropdownOptions` opens, positioned relative to the button with automatic collision/flip handling (opens upward instead of downward, or shifts horizontally, when there's insufficient viewport space) — provided by `@radix-ui/react-dropdown-menu`, not hand-rolled
52
- - **And** clicking the chevron again, pressing Escape, or clicking/tapping outside closes it
53
- - **And** the chevron rotates/reflects open vs. closed state visually (CSS-only transition, no animation library)
54
-
55
- ### 4: Each menu item is independent but DRY
56
-
57
- - **Given** a `dropdownOptions` entry with its own `busyOptions`/`successOptions`/`confirmOptions`
58
- - **When** that item is activated
59
- - **Then** it goes through the *same* `useBusyState`/`useConfirmation` hooks the main button already uses — each item is its own hook instance (via a new internal `ZestDropdownMenuItem` sub-component, since React's rules of hooks forbid calling hooks in a loop at the parent level), not a re-implementation of busy/confirm logic
60
- - **And** an item with no `busyOptions`/`confirmOptions` behaves like a plain synchronous menu action: selecting it calls `onClick` and closes the menu immediately (today's Radix default)
61
- - **And** an item with `busyOptions.handleInternally` (default `true`, matching the main button's existing default) keeps the menu open on that row, shows the same spinner/checkmark/shake feedback the main button uses, then closes automatically once the success/fail state settles
62
- - **And** an item with `confirmOptions` keeps the menu open through the full "Confirm X (5s)" countdown flow (identical semantics to the main button's confirm flow today), only closing once the confirmed action actually runs or the countdown expires
63
-
64
- ### 5: Full accessibility
65
-
66
- - **Given** the split-button variant
67
- - **Then** the chevron trigger has `aria-haspopup="menu"`, `aria-expanded`, and an accessible name (`zest.dropdownAriaLabel`, default `"More options"`, always overridable since it has no visible text)
68
- - **And** the menu uses proper `role="menu"`/`role="menuitem"` semantics (via Radix, not authored by hand)
69
- - **And** keyboard interaction works fully: Tab reaches the chevron as a separate stop after the main button, Enter/Space opens it, Arrow Up/Down moves between items, Escape closes and returns focus to the chevron
70
- - **And** while the menu is open, the existing `isDefault` document-level Enter-key listener (`UI/ZestButton.tsx`'s `useEffect` that clicks `buttonRef` on Enter) is suppressed — Enter while the menu is open must operate on the highlighted menu item (Radix's own behavior), not re-trigger the main action underneath it
71
-
72
- ### 6: Mobile-first, desktop-aware
73
-
74
- - **Given** the split-button variant on a touch viewport
75
- - **Then** the chevron segment meets a minimum 44×44px touch target (WCAG 2.5.5 / matches the intent of the prior "improve ZestButton mobile hit area" work), even at `size="sm"`
76
- - **And** on desktop/pointer input, the chevron segment is visually proportionate to the button's `size` (`sm`/`md`/`lg`) rather than always occupying the mobile-minimum footprint
77
-
78
- ### 7: Visual integration with the existing variant system
79
-
80
- - **Given** any combination of `visualOptions.variant` (`standard`/`success`/`danger`), `size` (`sm`/`md`/`lg`), `buttonStyle` (`solid`/`outline`/`text`/`dashed`), and `theme` (`light`/`dark`/`system`)
81
- - **Then** the chevron segment and the menu itself render consistently themed — no hardcoded colors independent of the variant/theme system (this is the concrete gap identified in `jattac.Libs.Web.OverflowMenu`, which hardcodes teal via Framer Motion inline styles that can't be overridden; this feature must not repeat that mistake)
82
-
83
- ### 8: Disabled/busy propagation across the whole control
84
-
85
- - **Given** the split-button variant
86
- - **When** the main segment is busy (internally-handled async click in flight) OR any menu item is busy
87
- - **Then** the *entire* control (main segment + chevron) is disabled — no firing a second action from either segment while one is already in flight
88
- - **And** the existing `disabled` prop, when explicitly passed, disables both segments as it does today for the single button
89
-
90
- ### 9: New dependency
91
-
92
- - **Given** this feature requires menu positioning/dismiss/a11y mechanics
93
- - **Then** `@radix-ui/react-dropdown-menu` is added as a `peerDependency` + `devDependency`, and added to both `external` arrays in `rollup.config.mjs` — following the exact precedent already established for `react-icons` in this repo, so it's never bundled into `dist/` and consumers control their own version
94
- - **And** no other new dependency is introduced (no Framer Motion, no `@radix-ui/react-popover` — this repo's existing CSS-module + hooks approach covers everything else)
95
-
96
- ---
97
-
98
- ## Out of Scope
99
-
100
- - Nested/submenus (dropdown items are a flat list; `jattac.Libs.Web.OverflowMenu` supports nesting, this feature does not need to and will not)
101
- - Any change to `jattac.Libs.Web.OverflowMenu` itself, or reusing it as a dependency (evaluated and rejected — see decision log)
102
- - A "menu-only" button mode where the main segment doesn't have its own default action — every split button has a real default action per the user's stated requirement
103
- - Reordering/drag-and-drop of menu items
104
- - Icons-within-menu-items styling beyond what `semanticType`/`icon` already provide on the main button today
105
- - Any change to `ZestButton`'s existing (non-dropdown) behavior — AC 1's "zero change when `dropdownOptions` is absent" is the hard boundary
106
-
107
- ---
108
-
109
- ## Technical Design
110
-
111
- ### New dependency
112
-
113
- | Package | Type | Purpose |
114
- |---|---|---|
115
- | `@radix-ui/react-dropdown-menu` | `peerDependency` + `devDependency`, externalized in `rollup.config.mjs` | Menu positioning (collision/flip), dismiss-on-outside-click/Escape, WAI-ARIA menu semantics, keyboard nav — used directly (not via `jattac.Libs.Web.OverflowMenu`) |
116
-
117
- ### New types (`UI/ZestButton.tsx`, additive)
118
-
119
- ```ts
120
- export interface ZestDropdownOption {
121
- key?: string;
122
- label: React.ReactNode;
123
- icon?: React.ReactNode;
124
- disabled?: boolean;
125
- semanticType?: SemanticType;
126
- onClick?: (e: Event) => void | Promise<void>;
127
- busyOptions?: BusyOptions;
128
- successOptions?: SuccessOptions;
129
- confirmOptions?: ConfirmOptions;
130
- }
131
- ```
132
-
133
- `ZestCustomProps` gains two new optional fields: `dropdownOptions?: ZestDropdownOption[]` and `dropdownAriaLabel?: string` (default `"More options"`).
134
-
135
- ### New files
136
-
137
- ```
138
- UI/ZestDropdownTrigger.tsx — the chevron segment; wraps Radix's DropdownMenu.Trigger (asChild) around ZestButton's own themed button markup
139
- UI/ZestDropdownMenuItem.tsx — one menu row; internally calls useBusyState/useConfirmation exactly like ZestButton.tsx does today (AC 4's DRY-via-shared-hooks requirement)
140
- UI/hooks/useDropdownDisclosure.ts — thin wrapper around Radix's open/onOpenChange state, exposing the open flag ZestButton.tsx needs to suppress the isDefault Enter-key effect (AC 5)
141
- Styles/ZestButton.module.css — extended (not replaced) with chevron/menu/divider classes, matching this repo's existing CSS-module + identity-obj-proxy-testable approach
142
- ```
143
-
144
- ### Modified files
145
-
146
- - `UI/ZestButton.tsx` — additive only: renders the chevron trigger + menu when `dropdownOptions` is present; the `isDefault` `useEffect` gains the open-menu guard from AC 5. No existing branch of the current render/click logic changes when `dropdownOptions` is absent (AC 1).
147
- - `package.json` — new peer/dev dependency.
148
- - `rollup.config.mjs` — `@radix-ui/react-dropdown-menu` added to both `external` arrays (JS bundle config and the `.d.ts` bundle config), matching the existing `react`/`react-dom`/`react-icons` entries exactly.
149
-
150
- ### Pattern precedent
151
-
152
- `jattac.Libs.Web.OverflowMenu` (sibling repo, `D:\work\nyingi\code\systems\jattac-web-libs\jattac.Libs.Web.OverflowMenu`) already depends on `@radix-ui/react-dropdown-menu` in production — this is a proven, already-trusted-in-this-org package, just consumed directly instead of through that repo's opinionated (non-themeable) wrapper.
153
-
154
- ---
155
-
156
- ## Edge Cases
157
-
158
- ### Menu item busy while main button is also mid-confirmation
159
- **Scenario:** User opens the menu while the main button's own `confirmOptions` countdown is active.
160
- **Expected:** AC 8 disables the whole control while *busy*, but a pending confirm countdown on the main segment is not "busy" (mirrors today's single-button semantics, where `wasSuccessful`/`wasFailed`/`internalBusy` — not `awaitingConfirm` — drive `isDisabled`). The menu remains openable during a pending confirm; opening it does not cancel the main segment's countdown.
161
-
162
- ### Zero dropdownOptions after filtering
163
- **Scenario:** Caller passes `dropdownOptions: []` (explicit empty array, not `undefined`).
164
- **Expected:** Same as AC 1's "absent" case — no chevron segment rendered. An empty array is not treated differently from `undefined`.
165
-
166
- ### Rapid open/close while a menu item is busy
167
- **Scenario:** A menu item's async `onClick` is in flight (menu held open by AC 4); user presses Escape or clicks outside.
168
- **Expected:** The menu closes on dismissal regardless of the in-flight promise — the promise still runs to completion and still updates that item's busy/success/fail state internally, it's just not visible until/unless the menu is reopened. No dangling state or memory leak (existing `useBusyState`/`useConfirmation` cleanup-on-unmount behavior, already tested in `__tests__/hooks/`, covers this as long as the item component doesn't literally unmount when the menu closes — confirm during implementation whether Radix unmounts closed menu content by default, and force-mount if so, so item state survives close/reopen within one page session).
169
-
170
- ---
171
-
172
- ## Dependencies
173
-
174
- ### Internal
175
- - Builds on the existing `useBusyState`/`useConfirmation`/`useZestConfig` hooks and `Styles/ZestButton.module.css` conventions — no changes required to any of them.
176
-
177
- ### External
178
- - `@radix-ui/react-dropdown-menu` (new, see Technical Design).
179
-
180
- ---
181
-
182
- ## Risks and Open Questions
183
-
184
- ### Risks
185
- - Radix's `DropdownMenu.Content` renders into a portal by default — needs a sensible default portal target (`document.body`) with an optional override prop for consumers using shadow DOM/iframes, otherwise CSS custom properties (theme forcing) that rely on ancestor selectors may not reach the portaled menu. Needs verifying during implementation, not just assumed.
186
- - `@radix-ui/react-dropdown-menu`'s peer range for React needs checking against this repo's `react: ">=16.8.0"` peer floor before finalizing the `package.json` entry — if Radix's floor is higher (e.g. requires React 16.8+ hooks, which is fine, but some Radix packages have crept their stated minimums up in recent majors), the two peer ranges must be reconciled or documented as a floor bump.
187
-
188
- ### Open Questions
189
- - None blocking — flagged risks above will be resolved empirically during implementation (installed, checked, and reported), not guessed.
190
-
191
- ---
192
-
193
- ## Regression Prevention
194
-
195
- ### For Features
196
-
197
- **New Tests Required:**
198
- - Every AC above (1–9) gets at least one behavioral test in `__tests__/ZestButton.test.tsx` or a new `__tests__/ZestDropdownMenuItem.test.tsx` / `__tests__/hooks/useDropdownDisclosure.test.ts`, following this repo's existing test conventions (real assertions, fake timers for confirm/busy timing, `identity-obj-proxy` for CSS modules).
199
- - A dedicated regression test asserting AC 1's "zero behavior change when `dropdownOptions` is absent" — re-running (not duplicating) a representative slice of the existing 52-test suite against a build with the new prop wired in but unused.
200
-
201
- ---
202
-
203
- ## Definition of Done
204
-
205
- - [ ] BRS approved by user
206
- - [ ] All acceptance criteria pass (new tests green, full existing 52-test suite still green)
207
- - [ ] `npm run build` clean (no new TS diagnostics, no new bundle-size surprises from an accidentally-inlined Radix dependency — verify `external` actually excluded it)
208
- - [ ] No coverage regression from the current baseline (`test-reports/coverage-baseline.json`)
209
- - [ ] No unsolicited changes outside this BRS's scope
210
- - [ ] Code matches existing patterns (hooks-per-concern, CSS modules, `identity-obj-proxy`-testable styling)
211
- - [ ] Change manifest produced
212
- - [ ] Self review passed with all PASS items
213
- - [ ] `docs/guidelines/ai-knowledge.md` and `docs/guidelines/decision-log.md` updated
@@ -1,455 +0,0 @@
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