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.
- package/LICENSE +21 -0
- package/README.md +10 -9
- package/dist/ZestButton.d.ts +4 -0
- package/dist/ZestDropdownMenu.d.ts +4 -0
- package/dist/index.cjs.js +39 -7
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.esm.js +40 -8
- package/dist/index.esm.js.map +1 -1
- package/documentation/api.md +165 -0
- package/documentation/breaking-changes.md +143 -0
- package/documentation/configuration.md +214 -0
- package/documentation/development.md +93 -0
- package/documentation/examples.md +289 -0
- package/documentation/features.md +150 -0
- package/package.json +3 -2
- package/docs/e2e/critical-paths.md +0 -75
- package/docs/features/zest-button-dropdown-options/BRS.md +0 -213
- package/docs/guidelines/AI_ARCHITECTURE.md +0 -455
- package/docs/guidelines/AI_BRS.md +0 -435
- package/docs/guidelines/AI_CODE_REVIEW.md +0 -198
- package/docs/guidelines/AI_E2E_TESTING.md +0 -227
- package/docs/guidelines/AI_GIT_WORKFLOW.md +0 -595
- package/docs/guidelines/AI_KNOWLEDGE.md +0 -213
- package/docs/guidelines/AI_PITFALLS.md +0 -245
- package/docs/guidelines/AI_STYLE_GUIDE.md +0 -162
- package/docs/guidelines/AI_TESTING.md +0 -275
- package/docs/guidelines/AI_TEST_CONFIGURATION.md +0 -529
- package/docs/guidelines/AI_WORKFLOW.md +0 -442
- package/docs/guidelines/AI_WORKFLOW_TRIGGERS.md +0 -222
- package/docs/guidelines/ai-knowledge.md +0 -154
- package/docs/guidelines/decision-log.md +0 -149
|
@@ -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
|