@prmichaelsen/acp-visualizer 0.1.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/README.md +68 -0
- package/agent/commands/acp.clarification-address.md +417 -0
- package/agent/commands/acp.clarification-capture.md +386 -0
- package/agent/commands/acp.clarification-create.md +437 -0
- package/agent/commands/acp.clarifications-research.md +326 -0
- package/agent/commands/acp.command-create.md +432 -0
- package/agent/commands/acp.design-create.md +286 -0
- package/agent/commands/acp.design-reference.md +355 -0
- package/agent/commands/acp.handoff.md +270 -0
- package/agent/commands/acp.index.md +423 -0
- package/agent/commands/acp.init.md +546 -0
- package/agent/commands/acp.package-create.md +895 -0
- package/agent/commands/acp.package-info.md +212 -0
- package/agent/commands/acp.package-install.md +539 -0
- package/agent/commands/acp.package-list.md +280 -0
- package/agent/commands/acp.package-publish.md +541 -0
- package/agent/commands/acp.package-remove.md +293 -0
- package/agent/commands/acp.package-search.md +307 -0
- package/agent/commands/acp.package-update.md +361 -0
- package/agent/commands/acp.package-validate.md +540 -0
- package/agent/commands/acp.pattern-create.md +386 -0
- package/agent/commands/acp.plan.md +587 -0
- package/agent/commands/acp.proceed.md +882 -0
- package/agent/commands/acp.project-create.md +675 -0
- package/agent/commands/acp.project-info.md +312 -0
- package/agent/commands/acp.project-list.md +226 -0
- package/agent/commands/acp.project-remove.md +379 -0
- package/agent/commands/acp.project-set.md +227 -0
- package/agent/commands/acp.project-update.md +307 -0
- package/agent/commands/acp.projects-restore.md +228 -0
- package/agent/commands/acp.projects-sync.md +347 -0
- package/agent/commands/acp.report.md +407 -0
- package/agent/commands/acp.resume.md +239 -0
- package/agent/commands/acp.sessions.md +301 -0
- package/agent/commands/acp.status.md +293 -0
- package/agent/commands/acp.sync.md +364 -0
- package/agent/commands/acp.task-create.md +500 -0
- package/agent/commands/acp.update.md +302 -0
- package/agent/commands/acp.validate.md +466 -0
- package/agent/commands/acp.version-check-for-updates.md +276 -0
- package/agent/commands/acp.version-check.md +191 -0
- package/agent/commands/acp.version-update.md +289 -0
- package/agent/commands/command.template.md +339 -0
- package/agent/commands/git.commit.md +526 -0
- package/agent/commands/git.init.md +514 -0
- package/agent/commands/tanstack-cloudflare.deploy.md +272 -0
- package/agent/commands/tanstack-cloudflare.tail.md +275 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/design.template.md +154 -0
- package/agent/design/local.dashboard-layout-routing.md +288 -0
- package/agent/design/local.data-model-yaml-parsing.md +310 -0
- package/agent/design/local.search-filtering.md +331 -0
- package/agent/design/local.server-api-auto-refresh.md +235 -0
- package/agent/design/local.table-tree-views.md +299 -0
- package/agent/design/local.visualizer-requirements.md +349 -0
- package/agent/design/requirements.template.md +387 -0
- package/agent/index/.gitkeep +0 -0
- package/agent/index/acp.core.yaml +137 -0
- package/agent/index/local.main.template.yaml +37 -0
- package/agent/manifest.template.yaml +13 -0
- package/agent/manifest.yaml +302 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +67 -0
- package/agent/milestones/milestone-1-{title}.template.md +206 -0
- package/agent/milestones/milestone-2-dashboard-views-interaction.md +79 -0
- package/agent/package.template.yaml +86 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.template.md +1237 -0
- package/agent/patterns/pattern.template.md +382 -0
- package/agent/patterns/tanstack-cloudflare.acl-permissions.md +332 -0
- package/agent/patterns/tanstack-cloudflare.action-bar-item.md +416 -0
- package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +401 -0
- package/agent/patterns/tanstack-cloudflare.auth-session-management.md +387 -0
- package/agent/patterns/tanstack-cloudflare.card-and-list.md +271 -0
- package/agent/patterns/tanstack-cloudflare.chat-engine.md +353 -0
- package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +346 -0
- package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +516 -0
- package/agent/patterns/tanstack-cloudflare.email-service.md +431 -0
- package/agent/patterns/tanstack-cloudflare.expander.md +98 -0
- package/agent/patterns/tanstack-cloudflare.fcm-push.md +115 -0
- package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +441 -0
- package/agent/patterns/tanstack-cloudflare.firebase-auth.md +348 -0
- package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +550 -0
- package/agent/patterns/tanstack-cloudflare.firebase-storage.md +369 -0
- package/agent/patterns/tanstack-cloudflare.form-controls.md +145 -0
- package/agent/patterns/tanstack-cloudflare.global-search-context.md +93 -0
- package/agent/patterns/tanstack-cloudflare.image-carousel.md +126 -0
- package/agent/patterns/tanstack-cloudflare.library-services.md +553 -0
- package/agent/patterns/tanstack-cloudflare.lightbox.md +169 -0
- package/agent/patterns/tanstack-cloudflare.markdown-content.md +115 -0
- package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +98 -0
- package/agent/patterns/tanstack-cloudflare.modal.md +156 -0
- package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +461 -0
- package/agent/patterns/tanstack-cloudflare.notifications-engine.md +151 -0
- package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +90 -0
- package/agent/patterns/tanstack-cloudflare.og-metadata.md +296 -0
- package/agent/patterns/tanstack-cloudflare.pagination.md +442 -0
- package/agent/patterns/tanstack-cloudflare.pill-input.md +220 -0
- package/agent/patterns/tanstack-cloudflare.provider-adapter.md +401 -0
- package/agent/patterns/tanstack-cloudflare.rate-limiting.md +323 -0
- package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +338 -0
- package/agent/patterns/tanstack-cloudflare.searchable-settings.md +375 -0
- package/agent/patterns/tanstack-cloudflare.slide-over.md +129 -0
- package/agent/patterns/tanstack-cloudflare.ssr-preload.md +571 -0
- package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +508 -0
- package/agent/patterns/tanstack-cloudflare.toast-system.md +142 -0
- package/agent/patterns/tanstack-cloudflare.unified-header.md +280 -0
- package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +628 -0
- package/agent/patterns/tanstack-cloudflare.websocket-manager.md +237 -0
- package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +358 -0
- package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +336 -0
- package/agent/progress.template.yaml +161 -0
- package/agent/progress.yaml +145 -0
- package/agent/schemas/package.schema.yaml +276 -0
- package/agent/scripts/acp.common.sh +1781 -0
- package/agent/scripts/acp.install.sh +333 -0
- package/agent/scripts/acp.package-create.sh +924 -0
- package/agent/scripts/acp.package-info.sh +288 -0
- package/agent/scripts/acp.package-install.sh +893 -0
- package/agent/scripts/acp.package-list.sh +311 -0
- package/agent/scripts/acp.package-publish.sh +420 -0
- package/agent/scripts/acp.package-remove.sh +348 -0
- package/agent/scripts/acp.package-search.sh +156 -0
- package/agent/scripts/acp.package-update.sh +517 -0
- package/agent/scripts/acp.package-validate.sh +1018 -0
- package/agent/scripts/acp.uninstall.sh +85 -0
- package/agent/scripts/acp.version-check-for-updates.sh +98 -0
- package/agent/scripts/acp.version-check.sh +47 -0
- package/agent/scripts/acp.version-update.sh +176 -0
- package/agent/scripts/acp.yaml-parser.sh +985 -0
- package/agent/scripts/acp.yaml-validate.sh +205 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +210 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +294 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +193 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +262 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +156 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +178 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +141 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +153 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +174 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +233 -0
- package/agent/tasks/task-1-{title}.template.md +244 -0
- package/bin/visualize.mjs +84 -0
- package/package.json +48 -0
- package/src/components/ExtraFieldsBadge.tsx +15 -0
- package/src/components/FilterBar.tsx +33 -0
- package/src/components/Header.tsx +23 -0
- package/src/components/MilestoneTable.tsx +167 -0
- package/src/components/MilestoneTree.tsx +84 -0
- package/src/components/ProgressBar.tsx +20 -0
- package/src/components/SearchInput.tsx +22 -0
- package/src/components/Sidebar.tsx +54 -0
- package/src/components/StatusBadge.tsx +23 -0
- package/src/components/StatusDot.tsx +12 -0
- package/src/components/TaskList.tsx +36 -0
- package/src/components/ViewToggle.tsx +31 -0
- package/src/lib/config.ts +8 -0
- package/src/lib/file-watcher.ts +43 -0
- package/src/lib/search.ts +48 -0
- package/src/lib/types.ts +73 -0
- package/src/lib/useAutoRefresh.ts +31 -0
- package/src/lib/useCollapse.ts +31 -0
- package/src/lib/useFilteredData.ts +55 -0
- package/src/lib/yaml-loader-real.spec.ts +47 -0
- package/src/lib/yaml-loader.spec.ts +201 -0
- package/src/lib/yaml-loader.ts +265 -0
- package/src/routeTree.gen.ts +140 -0
- package/src/router.tsx +10 -0
- package/src/routes/__root.tsx +75 -0
- package/src/routes/api/watch.ts +29 -0
- package/src/routes/index.tsx +115 -0
- package/src/routes/milestones.tsx +50 -0
- package/src/routes/search.tsx +84 -0
- package/src/routes/tasks.tsx +63 -0
- package/src/services/progress-database.service.ts +46 -0
- package/src/styles.css +25 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +16 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# ActionBarItem System
|
|
2
|
+
|
|
3
|
+
**Category**: Design
|
|
4
|
+
**Applicable To**: All action bars, action menus, card action strips, and any surface rendering a set of user actions with popovers/modals
|
|
5
|
+
**Status**: Specification (not yet implemented)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
The ActionBarItem system is a config-driven abstraction for composable, portable action items. Each action (rate, delete, publish, share, etc.) is a self-contained object returned by a hook, bundling its icon, state, handler, popover content, and modals. A generic `ActionBar` container iterates items and handles layout + popover orchestration. The same items render identically in horizontal bars, vertical menus, compact card strips, and slide-over panels — no rewiring needed.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## When to Use This Pattern
|
|
16
|
+
|
|
17
|
+
**Use this pattern when:**
|
|
18
|
+
- Building an action bar with multiple actions that need popovers or modals
|
|
19
|
+
- Adding the same action (delete, publish, rate) to multiple surfaces
|
|
20
|
+
- Rendering actions in different layouts (horizontal icons, vertical menu, compact strip)
|
|
21
|
+
- Composing action sets from per-concern hooks
|
|
22
|
+
|
|
23
|
+
**Don't use this pattern when:**
|
|
24
|
+
- A single standalone button with no popover (just use a button)
|
|
25
|
+
- Actions with no shared logic across surfaces (one-off inline handler)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Core Principles
|
|
30
|
+
|
|
31
|
+
1. **Hook Per Action**: Each action is a hook (`useDeleteActionBarItem`, `usePublishActionBarItem`) returning an `ActionBarItem`
|
|
32
|
+
2. **Implicit Content Detection**: `renderContent !== undefined` means the item has a popover — no separate boolean flag
|
|
33
|
+
3. **One Popover at a Time**: The `ActionBar` container manages `openKey: string | null`, closing any open popover when another opens
|
|
34
|
+
4. **Hook Owns Ref**: Each hook creates its own `triggerRef` — the container applies it to the rendered trigger element
|
|
35
|
+
5. **Reusable Renderers**: Shared renderer components (ConfirmRenderer, StarRatingRenderer, PublishMenuRenderer) serve as default `renderContent` implementations, overridable per consumer
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Implementation
|
|
40
|
+
|
|
41
|
+
### ActionBarItem Interface
|
|
42
|
+
|
|
43
|
+
**File**: `src/types/action-bar.ts`
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
interface ActionBarItem {
|
|
47
|
+
key: string // Unique ID for popover orchestration
|
|
48
|
+
icon: LucideIcon
|
|
49
|
+
label: string
|
|
50
|
+
onTrigger?: () => void // Direct action (no popover)
|
|
51
|
+
renderContent?: (ctx: ActionBarContentContext) => ReactNode // Popover content
|
|
52
|
+
renderModals?: () => ReactNode // Always-mounted portaled modals
|
|
53
|
+
onContentClose?: () => void // Cleanup when popover closes
|
|
54
|
+
triggerRef?: RefObject<HTMLButtonElement | null> // Hook owns the ref
|
|
55
|
+
loading?: boolean
|
|
56
|
+
disabled?: boolean
|
|
57
|
+
hidden?: boolean
|
|
58
|
+
danger?: boolean
|
|
59
|
+
active?: boolean
|
|
60
|
+
iconClassName?: string // e.g. 'fill-indigo-400 text-indigo-400'
|
|
61
|
+
to?: string // Link variant (renders as <Link>)
|
|
62
|
+
linkParams?: Record<string, string>
|
|
63
|
+
suffix?: ReactNode // For MenuItem rendering (e.g. "Coming soon")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ActionBarContentContext {
|
|
67
|
+
close: () => void
|
|
68
|
+
anchorRef: RefObject<HTMLElement | null>
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Key decisions**:
|
|
73
|
+
- Items with `renderContent` get a Popover; items without get a direct click handler (`onTrigger`)
|
|
74
|
+
- Items with `to` render as `<Link>` instead of `<button>`
|
|
75
|
+
- Extended return types expose state: hooks return `ActionBarItem & { isDeleted: boolean }` etc.
|
|
76
|
+
- `renderModals` is always-mounted (outside popover lifecycle) for modals that need portal rendering
|
|
77
|
+
|
|
78
|
+
### ActionBar Container
|
|
79
|
+
|
|
80
|
+
**File**: `src/components/action-bar/ActionBar.tsx`
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
interface ActionBarProps {
|
|
84
|
+
items: ActionBarItem[]
|
|
85
|
+
layout?: 'horizontal' | 'vertical' | 'compact'
|
|
86
|
+
className?: string
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Behavior**:
|
|
91
|
+
1. Filters out `hidden` items
|
|
92
|
+
2. Manages `openKey: string | null` — one popover at a time
|
|
93
|
+
3. Applies `item.triggerRef` to each trigger element via ref callback
|
|
94
|
+
4. Renders triggers as:
|
|
95
|
+
- **horizontal/compact**: Icon buttons (`p-2 text-gray-400 hover:text-white`)
|
|
96
|
+
- **vertical**: MenuItem components (icon + label + suffix)
|
|
97
|
+
5. Renders Popover for the active `openKey` item, anchored to its trigger
|
|
98
|
+
6. Always renders `item.renderModals?.()` for all items (modals live outside popover)
|
|
99
|
+
7. Calls `item.onContentClose?.()` when popover closes
|
|
100
|
+
8. Spacing: `gap-1` (horizontal/compact), `space-y-1` (vertical)
|
|
101
|
+
|
|
102
|
+
### Reusable Renderers
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
src/components/action-bar/renderers/
|
|
106
|
+
ConfirmRenderer.tsx — Confirm/cancel with variant color
|
|
107
|
+
StarRatingRenderer.tsx — Wraps StarRating component
|
|
108
|
+
PublishMenuRenderer.tsx — Multi-step menu (main → submenu → confirm)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// ConfirmRenderer
|
|
113
|
+
interface ConfirmRendererProps {
|
|
114
|
+
text: string
|
|
115
|
+
confirmLabel: string
|
|
116
|
+
onConfirm: () => void | Promise<void>
|
|
117
|
+
loading?: boolean
|
|
118
|
+
variant?: 'danger' | 'restore' // Red vs purple button
|
|
119
|
+
close: () => void
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// StarRatingRenderer
|
|
123
|
+
interface StarRatingRendererProps {
|
|
124
|
+
currentRating: number | null
|
|
125
|
+
onRate: (rating: number | null) => void
|
|
126
|
+
close: () => void
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Hooks provide a default `renderContent` using these renderers. Consumers override the whole `renderContent` via hook options if they need different presentation.
|
|
131
|
+
|
|
132
|
+
### Per-Item Hooks
|
|
133
|
+
|
|
134
|
+
All in `src/hooks/action-bar/`:
|
|
135
|
+
|
|
136
|
+
| Hook | Key | Content | Extended State |
|
|
137
|
+
|---|---|---|---|
|
|
138
|
+
| `useRatingActionBarItem(rating, onRate)` | `rate` | StarRatingRenderer | — |
|
|
139
|
+
| `useDeleteActionBarItem(memoryId, isDeleted, opts?)` | `delete` | ConfirmRenderer | `{ isDeleted }` |
|
|
140
|
+
| `usePublishActionBarItem(memoryId, opts)` | `publish` | PublishMenuRenderer + modals | — |
|
|
141
|
+
| `useShareActionBarItem(entityId, type)` | `share` | None (direct action) | — |
|
|
142
|
+
| `useUnlinkActionBarItem(memoryId, relId, onUnlink?)` | `unlink` | ConfirmRenderer | `{ isUnlinked }` |
|
|
143
|
+
| `useViewActionBarItem(memories, index)` | `view` | None (direct — lightbox) | — |
|
|
144
|
+
| `useCommentActionBarItem(onToggle)` | `comment` | None (direct action) | — |
|
|
145
|
+
| `useChatActionBarItem(memoryId, opts)` | `chat` | None (useNavigate internal) | — |
|
|
146
|
+
| `useAssignActionBarItem(memoryId)` | `assign` | Modal via renderModals | — |
|
|
147
|
+
| `useReportActionBarItem(memoryId, opts)` | `report` | Modal via renderModals | — |
|
|
148
|
+
|
|
149
|
+
Each hook:
|
|
150
|
+
- Creates `const triggerRef = useRef<HTMLButtonElement>(null)`
|
|
151
|
+
- Wraps existing shared hooks (`useMemoryDelete`, `useMemoryPublish`) or services
|
|
152
|
+
- Provides default `renderContent` using reusable renderers
|
|
153
|
+
- Accepts optional `renderContent` override in options for customization
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Examples
|
|
158
|
+
|
|
159
|
+
### Example 1: useDeleteActionBarItem
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
function useDeleteActionBarItem(
|
|
163
|
+
memoryId: string,
|
|
164
|
+
initialDeleted: boolean,
|
|
165
|
+
opts?: {
|
|
166
|
+
onDeleteChange?: (deleted: boolean) => void
|
|
167
|
+
renderContent?: (ctx: ActionBarContentContext) => ReactNode
|
|
168
|
+
},
|
|
169
|
+
): ActionBarItem & { isDeleted: boolean } {
|
|
170
|
+
const del = useMemoryDelete(memoryId, initialDeleted, opts?.onDeleteChange)
|
|
171
|
+
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
172
|
+
|
|
173
|
+
const defaultRenderContent = useCallback(({ close }: ActionBarContentContext) => (
|
|
174
|
+
<ConfirmRenderer
|
|
175
|
+
text={del.confirmText}
|
|
176
|
+
confirmLabel={del.actionLabel}
|
|
177
|
+
onConfirm={async () => { await del.handleDeleteRestore(); close() }}
|
|
178
|
+
loading={del.deleteLoading}
|
|
179
|
+
variant={del.isDeleted ? 'restore' : 'danger'}
|
|
180
|
+
close={close}
|
|
181
|
+
/>
|
|
182
|
+
), [del])
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
key: 'delete',
|
|
186
|
+
icon: del.isDeleted ? RotateCcw : Trash2,
|
|
187
|
+
label: del.actionLabel,
|
|
188
|
+
renderContent: opts?.renderContent ?? defaultRenderContent,
|
|
189
|
+
triggerRef,
|
|
190
|
+
loading: del.deleteLoading,
|
|
191
|
+
danger: !del.isDeleted,
|
|
192
|
+
isDeleted: del.isDeleted,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Example 2: Horizontal Action Bar (MemoryDetailActionBar)
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
function MemoryDetailActionBar(props) {
|
|
201
|
+
const rateItem = useRatingActionBarItem(props.userRating, props.onRate)
|
|
202
|
+
const commentItem = useCommentActionBarItem(props.onCommentToggle)
|
|
203
|
+
const assignItem = useAssignActionBarItem(props.memoryId)
|
|
204
|
+
const shareItem = useShareActionBarItem(props.memoryId, 'memory')
|
|
205
|
+
const publishItem = usePublishActionBarItem(props.memoryId, { ... })
|
|
206
|
+
const deleteItem = useDeleteActionBarItem(props.memoryId, props.isDeleted, { ... })
|
|
207
|
+
const chatItem = useChatActionBarItem(props.memoryId, { ... })
|
|
208
|
+
const reportItem = useReportActionBarItem(props.memoryId, { ... })
|
|
209
|
+
const editItem: ActionBarItem = {
|
|
210
|
+
key: 'edit', icon: Pencil, label: 'Edit', disabled: true,
|
|
211
|
+
suffix: <span className="text-xs text-gray-600">Coming soon</span>,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<ActionBar items={[
|
|
216
|
+
rateItem, commentItem, assignItem, shareItem,
|
|
217
|
+
publishItem, editItem, deleteItem, chatItem, reportItem,
|
|
218
|
+
]} />
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Example 3: Compact Card Strip (SortableMemoryCard)
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const viewItem = useViewActionBarItem(allItems, index)
|
|
227
|
+
const rateItem = useRatingActionBarItem(userRating, (r) => onRate(memoryId, r))
|
|
228
|
+
const unlinkItem = useUnlinkActionBarItem(memoryId, relationshipId, onUnlink)
|
|
229
|
+
const deleteItem = useDeleteActionBarItem(memoryId, !!item.deleted_at, {
|
|
230
|
+
renderContent: ({ close }) => (
|
|
231
|
+
<ConfirmRenderer
|
|
232
|
+
text="Delete this memory everywhere?"
|
|
233
|
+
confirmLabel={deleteItem.isDeleted ? 'Restore' : 'Delete'}
|
|
234
|
+
onConfirm={async () => { await del.handleDeleteRestore(); close() }}
|
|
235
|
+
variant={deleteItem.isDeleted ? 'restore' : 'danger'}
|
|
236
|
+
close={close}
|
|
237
|
+
/>
|
|
238
|
+
),
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const faded = deleteItem.isDeleted || unlinkItem.isUnlinked
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<div className={faded ? 'opacity-20' : ''}>
|
|
245
|
+
{/* card content */}
|
|
246
|
+
<ActionBar items={[viewItem, rateItem, unlinkItem, deleteItem]} layout="compact" />
|
|
247
|
+
</div>
|
|
248
|
+
)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Example 4: Vertical Menu (MemoryActions)
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
return (
|
|
255
|
+
<div className="p-4">
|
|
256
|
+
<h3>Settings</h3>
|
|
257
|
+
<ActionBar
|
|
258
|
+
items={[publishItem, shareItem, editItem, deleteItem, chatItem, reportItem, rateItem]}
|
|
259
|
+
layout="vertical"
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Example 5: Link Items (RelationshipDetailActionBar)
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
const reorderItem: ActionBarItem = {
|
|
269
|
+
key: 'reorder',
|
|
270
|
+
icon: LayoutDashboard,
|
|
271
|
+
label: 'Reorder',
|
|
272
|
+
to: '/relationships/$relationshipId/reorder',
|
|
273
|
+
linkParams: { relationshipId },
|
|
274
|
+
}
|
|
275
|
+
const shareItem = useShareActionBarItem(relationshipId, 'relationship')
|
|
276
|
+
|
|
277
|
+
return <ActionBar items={[reorderItem, shareItem]} />
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Example 6: Inline Static Items
|
|
281
|
+
|
|
282
|
+
For one-off actions that don't need a hook:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
const copyItem: ActionBarItem = {
|
|
286
|
+
key: 'copy',
|
|
287
|
+
icon: Copy,
|
|
288
|
+
label: 'Copy link',
|
|
289
|
+
onTrigger: () => {
|
|
290
|
+
navigator.clipboard.writeText(url)
|
|
291
|
+
toast.success({ title: 'Copied!' })
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Anti-Patterns
|
|
299
|
+
|
|
300
|
+
### Duplicating Action Logic Across Consumers
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// Bad: Every consumer reimplements delete state + handler + confirmation
|
|
304
|
+
function MemoryDetailActionBar() {
|
|
305
|
+
const [isDeleted, setIsDeleted] = useState(false)
|
|
306
|
+
const [deleteLoading, setDeleteLoading] = useState(false)
|
|
307
|
+
const handleDelete = async () => { /* 20 lines */ }
|
|
308
|
+
// ... popover state, anchor ref, confirmation UI
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function MemoryActions() {
|
|
312
|
+
const [isDeleted, setIsDeleted] = useState(false) // Same logic again
|
|
313
|
+
// ...
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Good: Hook encapsulates everything, container handles popover
|
|
317
|
+
const deleteItem = useDeleteActionBarItem(memoryId, isDeleted)
|
|
318
|
+
return <ActionBar items={[deleteItem]} />
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Managing Popover State Per Consumer
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
// Bad: Every consumer manages openKey + one-at-a-time logic
|
|
325
|
+
const [openPopover, setOpenPopover] = useState<string | null>(null)
|
|
326
|
+
const rateRef = useRef(null)
|
|
327
|
+
const deleteRef = useRef(null)
|
|
328
|
+
// ... 30 lines of popover orchestration per consumer
|
|
329
|
+
|
|
330
|
+
// Good: ActionBar container handles all orchestration
|
|
331
|
+
<ActionBar items={[rateItem, deleteItem, publishItem]} />
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Per-Prop Customization
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// Bad: Adding confirmText, confirmLabel, variant props to the hook
|
|
338
|
+
useDeleteActionBarItem(id, false, { confirmText: 'Custom text', variant: 'warning' })
|
|
339
|
+
|
|
340
|
+
// Good: Override the entire renderContent with a reusable renderer
|
|
341
|
+
useDeleteActionBarItem(id, false, {
|
|
342
|
+
renderContent: ({ close }) => (
|
|
343
|
+
<ConfirmRenderer text="Custom text" confirmLabel="OK" variant="danger"
|
|
344
|
+
onConfirm={handleDelete} close={close} />
|
|
345
|
+
),
|
|
346
|
+
})
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Key Design Decisions
|
|
352
|
+
|
|
353
|
+
### Interface
|
|
354
|
+
|
|
355
|
+
| Decision | Choice | Rationale |
|
|
356
|
+
|---|---|---|
|
|
357
|
+
| Content detection | Implicit (`renderContent !== undefined`) | Simpler, less API surface |
|
|
358
|
+
| Content context | `{ close, anchorRef }` | No containerType needed — always Popover |
|
|
359
|
+
| State exposure | Extended return types per hook | Type-safe, no casting |
|
|
360
|
+
| Trigger ref ownership | Hook creates ref, container applies it | Simpler lifecycle |
|
|
361
|
+
| Customization | `renderContent` override + reusable renderers | No per-prop customization; renderers compose |
|
|
362
|
+
|
|
363
|
+
### Container
|
|
364
|
+
|
|
365
|
+
| Decision | Choice | Rationale |
|
|
366
|
+
|---|---|---|
|
|
367
|
+
| Content rendering | Always Popover, all layouts | One rendering strategy |
|
|
368
|
+
| Sub-menu navigation | Inside `renderContent` only | Nested item lists too complex for container |
|
|
369
|
+
| Popover position | Delegate to Popover auto-flip | Container doesn't need layout-aware positioning |
|
|
370
|
+
|
|
371
|
+
### Hooks
|
|
372
|
+
|
|
373
|
+
| Decision | Choice | Rationale |
|
|
374
|
+
|---|---|---|
|
|
375
|
+
| Location | `src/hooks/action-bar/` | Grouped by concern |
|
|
376
|
+
| Naming | `use{Action}ActionBarItem` | Explicit, discoverable |
|
|
377
|
+
| Publish modals | Inside hook via `renderModals` | Encapsulates modal lifecycle |
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Dependencies
|
|
382
|
+
|
|
383
|
+
- `useMemoryDelete` hook (`src/hooks/useMemoryDelete.ts`)
|
|
384
|
+
- `useMemoryPublish` hook (`src/hooks/useMemoryPublish.ts`)
|
|
385
|
+
- `Popover` component (`src/components/Popover.tsx`)
|
|
386
|
+
- `MenuItem` component (`src/components/MenuItem.tsx`)
|
|
387
|
+
- `StarRating` component (`src/components/memories/StarRating.tsx`)
|
|
388
|
+
- `PublishModal`, `RetractModal`, `ReportModal`, `RelationshipAssignModal` (all exist)
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Checklist
|
|
393
|
+
|
|
394
|
+
- [ ] Each action is a hook returning `ActionBarItem` — no inline action logic in consumers
|
|
395
|
+
- [ ] Hook creates its own `triggerRef` — container applies it
|
|
396
|
+
- [ ] `renderContent` uses reusable renderers as building blocks
|
|
397
|
+
- [ ] `renderModals` is always-mounted for portal-based modals
|
|
398
|
+
- [ ] Extended state (isDeleted, isUnlinked) exposed via typed return
|
|
399
|
+
- [ ] ActionBar container manages `openKey` for one-popover-at-a-time
|
|
400
|
+
- [ ] Static/one-off items defined as plain objects (no hook needed)
|
|
401
|
+
- [ ] Items with `to` render as `<Link>`, not `<button>`
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Related Patterns
|
|
406
|
+
|
|
407
|
+
- **[Modal](./tanstack-cloudflare.modal.md)**: ConfirmationModal used by ConfirmRenderer
|
|
408
|
+
- **[Toast System](./tanstack-cloudflare.toast-system.md)**: useActionToast used inside action hooks
|
|
409
|
+
- **[Card & List](./tanstack-cloudflare.card-and-list.md)**: Cards consume ActionBar in compact layout
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
**Status**: Specification (not yet implemented — planned for M84)
|
|
414
|
+
**Recommendation**: Implement per-item hooks first, then ActionBar container, then migrate consumers one at a time
|
|
415
|
+
**Last Updated**: 2026-03-14
|
|
416
|
+
**Contributors**: Community
|