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.
@@ -0,0 +1,75 @@
1
+ # Critical Paths
2
+
3
+ This document defines the critical business paths that MUST be covered by e2E tests.
4
+
5
+ ---
6
+
7
+ ## How to Identify Critical Paths
8
+
9
+ A path is critical if:
10
+
11
+ • It handles money or payments
12
+
13
+ • It manages user authentication or authorization
14
+
15
+ • It writes to the database in ways that affect business data
16
+
17
+ • It integrates with external systems
18
+
19
+ • If broken, it causes immediate user-facing impact
20
+
21
+ ---
22
+
23
+ ## Paths
24
+
25
+ ### Authentication
26
+
27
+ - [ ] Login with valid credentials → 200 + token
28
+
29
+ - [ ] Login with invalid credentials → 401
30
+
31
+ - [ ] Token refresh → 200 + new token
32
+
33
+ - [ ] Logout → 200 + token invalidated
34
+
35
+ ### Payment
36
+
37
+ - [ ] Create payment → 201 + payment record
38
+
39
+ - [ ] Confirm payment → 200 + status updated
40
+
41
+ - [ ] Handle payment failure → 200 + status updated
42
+
43
+ - [ ] Process refund → 200 + refund record
44
+
45
+ ### Order
46
+
47
+ - [ ] Create order → 201 + order record
48
+
49
+ - [ ] Pay order → 200 + order status updated
50
+
51
+ - [ ] Ship order → 200 + order status updated
52
+
53
+ - [ ] Complete order → 200 + order status updated
54
+
55
+ - [ ] Cancel order → 200 + order status updated
56
+
57
+ ### Data
58
+
59
+ - [ ] Import data → 200 + records created
60
+
61
+ - [ ] Export data → 200 + file returned
62
+
63
+ ---
64
+
65
+ ## Adding New Paths
66
+
67
+ When adding a new critical path:
68
+
69
+ 1. Add the path description above
70
+
71
+ 2. List each scenario with expected outcome
72
+
73
+ 3. Write e2e tests covering each scenario
74
+
75
+ 4. Mark scenarios as tested: `- [x]`
@@ -0,0 +1,213 @@
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