@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,169 @@
|
|
|
1
|
+
# LightboxContainer & ImageLightbox
|
|
2
|
+
|
|
3
|
+
**Category**: Design
|
|
4
|
+
**Applicable To**: Full-screen image galleries, memory detail viewers, crop editors, and any swipeable full-screen overlay
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
LightboxContainer is a reusable full-screen shell (portal, z-55) with slide animation, keyboard/swipe navigation, and a counter badge. ImageLightbox extends it for image galleries with crop data, lazy preloading, and scaled previews. Other lightboxes (MemoryLightbox, ImageCropLightbox) also build on LightboxContainer.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Implementation
|
|
16
|
+
|
|
17
|
+
### LightboxContainer (Shell)
|
|
18
|
+
|
|
19
|
+
**File**: `src/components/LightboxContainer.tsx`
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
interface LightboxContainerProps {
|
|
23
|
+
totalCount: number
|
|
24
|
+
index: number
|
|
25
|
+
onPrev: () => void
|
|
26
|
+
onNext: () => void
|
|
27
|
+
onClose: () => void
|
|
28
|
+
navDisabled?: boolean // Disable keyboard/swipe nav (e.g., during crop edit)
|
|
29
|
+
onEscapeWhileNavDisabled?: () => void // Escape exits sub-mode, not lightbox
|
|
30
|
+
onBackdropClickWhileNavDisabled?: () => void
|
|
31
|
+
children: ReactNode // The slide content
|
|
32
|
+
overlay?: ReactNode // Non-animated toolbar/controls
|
|
33
|
+
animatedClassName?: string // Extra classes on the animated content div
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Features**:
|
|
38
|
+
- Portal to `document.body`, z-55, `bg-black/90 backdrop-blur-sm`
|
|
39
|
+
- Body scroll lock + safe-area-inset-top
|
|
40
|
+
- **Slide animation**: 200ms ease-out, scale(0.92) + translateX(±60px) on exit/enter
|
|
41
|
+
- **Keyboard**: ArrowLeft/Right for prev/next, Escape to close
|
|
42
|
+
- **Touch/swipe**: Horizontal >100px = nav, vertical up >80px from backdrop = dismiss
|
|
43
|
+
- **Navigation UI**: Chevron buttons (hidden on mobile), counter badge at bottom ("3 / 10")
|
|
44
|
+
- Close button (X) top-right with safe-area offset
|
|
45
|
+
|
|
46
|
+
**Gesture Detection**:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Horizontal swipe: navigate
|
|
50
|
+
if (deltaX > 100 && absX > absY) onPrev()
|
|
51
|
+
if (deltaX < -100 && absX > absY) onNext()
|
|
52
|
+
// Vertical swipe up from backdrop: dismiss
|
|
53
|
+
if (onBackdrop && deltaY < -80 && absY > absX) onClose()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Usage** (building a custom lightbox):
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
function MyLightbox({ items, startIndex, onClose }) {
|
|
60
|
+
const [index, setIndex] = useState(startIndex)
|
|
61
|
+
return (
|
|
62
|
+
<LightboxContainer
|
|
63
|
+
totalCount={items.length}
|
|
64
|
+
index={index}
|
|
65
|
+
onPrev={() => setIndex(i => Math.max(0, i - 1))}
|
|
66
|
+
onNext={() => setIndex(i => Math.min(items.length - 1, i + 1))}
|
|
67
|
+
onClose={onClose}
|
|
68
|
+
>
|
|
69
|
+
<MySlideContent item={items[index]} />
|
|
70
|
+
</LightboxContainer>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### ImageLightbox (Gallery Viewer)
|
|
78
|
+
|
|
79
|
+
**File**: `src/components/ImageLightbox.tsx`
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
interface ImageLightboxProps {
|
|
83
|
+
images: Array<{ src: string; alt?: string; crop?: CropData | null }>
|
|
84
|
+
startIndex: number
|
|
85
|
+
onClose: () => void
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Features**:
|
|
90
|
+
- Built on LightboxContainer
|
|
91
|
+
- Lazy-loads crop data via HEAD request to image proxy (module-level cache)
|
|
92
|
+
- Preloads adjacent images (current ± 1) to eliminate flash
|
|
93
|
+
- ScaledCropPreview: uses CSS `background-position` + `background-size` for efficient cropped display
|
|
94
|
+
- Max constraints: 95vw width, 85vh height
|
|
95
|
+
- Click-stop propagation on images (prevents backdrop close)
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### ImageCropLightbox (Crop Editor)
|
|
100
|
+
|
|
101
|
+
**File**: `src/components/ImageCropLightbox.tsx`
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
interface ImageCropLightboxProps {
|
|
105
|
+
images: CropImage[]
|
|
106
|
+
startIndex: number
|
|
107
|
+
onClose: () => void
|
|
108
|
+
onCropChange?: (index: number, crop: CropData) => void
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Features**:
|
|
113
|
+
- Built on LightboxContainer with `navDisabled` during active crop
|
|
114
|
+
- Toggle between view mode and crop mode (Escape exits crop mode, not lightbox)
|
|
115
|
+
- Overlay buttons: crop toggle, reset crop
|
|
116
|
+
- Lazy-loads natural image dimensions for proper crop calculations
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Anti-Patterns
|
|
121
|
+
|
|
122
|
+
### Building Full-Screen Overlays Without LightboxContainer
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Bad: Reimplementing swipe, keyboard, animation, scroll lock
|
|
126
|
+
<div className="fixed inset-0 z-50" onKeyDown={handleKey}>
|
|
127
|
+
{/* Custom implementation */}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
// Good: Use LightboxContainer for the shell
|
|
131
|
+
<LightboxContainer totalCount={n} index={i} onPrev={prev} onNext={next} onClose={close}>
|
|
132
|
+
{/* Just the slide content */}
|
|
133
|
+
</LightboxContainer>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Forgetting navDisabled for Sub-Modes
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Bad: User swipes during crop edit, navigates away and loses changes
|
|
140
|
+
<LightboxContainer onPrev={prev} onNext={next}>
|
|
141
|
+
<CropEditor />
|
|
142
|
+
</LightboxContainer>
|
|
143
|
+
|
|
144
|
+
// Good: Disable nav during sub-mode
|
|
145
|
+
<LightboxContainer
|
|
146
|
+
navDisabled={cropActive}
|
|
147
|
+
onEscapeWhileNavDisabled={() => setCropActive(false)}
|
|
148
|
+
onBackdropClickWhileNavDisabled={() => setCropActive(false)}
|
|
149
|
+
>
|
|
150
|
+
<CropEditor />
|
|
151
|
+
</LightboxContainer>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Checklist
|
|
157
|
+
|
|
158
|
+
- [ ] Use LightboxContainer for any full-screen gallery or detail viewer
|
|
159
|
+
- [ ] Set `navDisabled` when entering a sub-mode (crop, edit, etc.)
|
|
160
|
+
- [ ] Provide `onEscapeWhileNavDisabled` to exit sub-mode on Escape
|
|
161
|
+
- [ ] Preload adjacent slides to eliminate flash on navigation
|
|
162
|
+
- [ ] Apply `e.stopPropagation()` on interactive content to prevent backdrop close
|
|
163
|
+
- [ ] Safe-area-inset-top applied via inline style
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
**Status**: Stable
|
|
168
|
+
**Last Updated**: 2026-03-14
|
|
169
|
+
**Contributors**: Community
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Markdown Content Rendering
|
|
2
|
+
|
|
3
|
+
**Category**: Design
|
|
4
|
+
**Applicable To**: Rendering AI-generated or user-submitted markdown with XSS protection, @mention badges, code blocks, and font size preferences
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`MarkdownContent` wraps ReactMarkdown with rehype-sanitize for XSS protection, custom mention preprocessing (`@agent`, `@uid:userId`), link validation (internal/external routing), syntax-highlighted code blocks, and user font size preferences. Safe for rendering AI-generated content that may contain arbitrary markdown.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Implementation
|
|
16
|
+
|
|
17
|
+
**File**: `src/components/chat/MarkdownContent.tsx`
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
interface MarkdownContentProps {
|
|
21
|
+
content: string
|
|
22
|
+
className?: string
|
|
23
|
+
currentUserId?: string // For mention badge interactivity
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Processing Pipeline
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Raw content
|
|
31
|
+
→ stripTimestampPrefix() // Remove <msg ts="..."/> prefixes
|
|
32
|
+
→ linkifyText() // Convert bare URLs to [url](url) markdown
|
|
33
|
+
→ preprocessMentions() // @agent → **@agent**, @uid:X → **@uid:X**
|
|
34
|
+
→ ReactMarkdown
|
|
35
|
+
+ rehype-sanitize // Strip XSS vectors
|
|
36
|
+
+ custom components // Links, code, strong (mentions)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### XSS Sanitization
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
const sanitizeSchema = {
|
|
43
|
+
...defaultSchema,
|
|
44
|
+
attributes: {
|
|
45
|
+
code: [...(defaultSchema.attributes?.code || []), 'className'],
|
|
46
|
+
a: ['href', 'title'],
|
|
47
|
+
},
|
|
48
|
+
protocols: { href: ['http', 'https', 'mailto'] },
|
|
49
|
+
tagNames: (defaultSchema.tagNames || []).filter(tag =>
|
|
50
|
+
!['script', 'iframe', 'object', 'embed', 'style'].includes(tag)
|
|
51
|
+
),
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Link Routing
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// Internal links (/memory/abc): navigate within app
|
|
59
|
+
// External links (https://...): target="_blank" rel="noopener noreferrer"
|
|
60
|
+
// Invalid URLs: render as red [Invalid Link] span
|
|
61
|
+
|
|
62
|
+
function isInternalUrl(url: string): boolean { return url.startsWith('/') }
|
|
63
|
+
function isValidUrl(url: string): boolean {
|
|
64
|
+
if (url.startsWith('/')) return true
|
|
65
|
+
try { return ['http:', 'https:', 'mailto:'].includes(new URL(url).protocol) }
|
|
66
|
+
catch { return false }
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Code Blocks
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// Multiline → CodeBlock component with syntax highlighting + copy button
|
|
74
|
+
// Inline → gray background with font size preference
|
|
75
|
+
const isCodeBlock = code.includes('\n') || startLine !== endLine
|
|
76
|
+
if (isCodeBlock) return <CodeBlock code={code} language={language} />
|
|
77
|
+
return <code className="bg-gray-700 px-1.5 py-0.5 rounded">{children}</code>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Mention Badges
|
|
81
|
+
|
|
82
|
+
`@agent` and `@uid:userId` are preprocessed to bold markdown, then the `strong` renderer detects them:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
strong({ children }) {
|
|
86
|
+
const text = extractText(children)
|
|
87
|
+
if (text === '@agent') return <span className="bg-blue-500/20 text-blue-400 ...">@agent</span>
|
|
88
|
+
if (text.startsWith('@uid:')) return <MentionBadge userId={text.slice(5)} />
|
|
89
|
+
return <strong>{children}</strong>
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Font Size Integration
|
|
94
|
+
|
|
95
|
+
Uses `useUIPreferencesLocal()` context — heading sizes and prose classes scale with preference.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Anti-Patterns
|
|
100
|
+
|
|
101
|
+
### Rendering Unsanitized HTML
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// Bad: XSS vulnerability
|
|
105
|
+
<div dangerouslySetInnerHTML={{ __html: aiResponse }} />
|
|
106
|
+
|
|
107
|
+
// Good: ReactMarkdown + rehype-sanitize
|
|
108
|
+
<MarkdownContent content={aiResponse} />
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
**Status**: Stable
|
|
114
|
+
**Last Updated**: 2026-03-14
|
|
115
|
+
**Contributors**: Community
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Mention Suggestion System
|
|
2
|
+
|
|
3
|
+
**Category**: Design
|
|
4
|
+
**Applicable To**: @mention autocomplete in chat inputs with instant context + async search tiers
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
A two-tier mention autocomplete: Tier 1 (instant) uses in-memory conversation participant profiles for sub-millisecond results. Tier 2 (async) searches the global people index via API when the context tier has no matches. Stale call detection prevents race conditions. Suggestions sorted by: agent first → prefix matches → substring matches.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Implementation
|
|
16
|
+
|
|
17
|
+
### useMentionSuggestions Hook
|
|
18
|
+
|
|
19
|
+
**File**: `src/hooks/useMentionSuggestions.ts`
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
interface MentionSuggestion {
|
|
23
|
+
id: string // userId or 'agent'
|
|
24
|
+
username: string
|
|
25
|
+
type: 'agent' | 'user'
|
|
26
|
+
avatarUrl?: string
|
|
27
|
+
displayName?: string
|
|
28
|
+
section?: 'context' | 'search'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function useMentionSuggestions(participantIds?: string[]) {
|
|
32
|
+
const contextProfilesRef = useRef<Map<string, UserProfile>>(new Map())
|
|
33
|
+
const callIdRef = useRef(0) // Stale call detection
|
|
34
|
+
|
|
35
|
+
// Tier 1: Load participant profiles on mount
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!participantIds?.length) return
|
|
38
|
+
ProfileService.getProfiles(participantIds).then(profiles => {
|
|
39
|
+
contextProfilesRef.current = new Map(Object.entries(profiles))
|
|
40
|
+
})
|
|
41
|
+
}, [participantIds?.join(',')])
|
|
42
|
+
|
|
43
|
+
const getSuggestions = useCallback(async (query: string) => {
|
|
44
|
+
const thisCallId = ++callIdRef.current
|
|
45
|
+
|
|
46
|
+
// Tier 1: Instant context matches
|
|
47
|
+
const contextResults = matchFromContext(query, contextProfilesRef.current)
|
|
48
|
+
|
|
49
|
+
// Tier 2: Async global search (if context insufficient)
|
|
50
|
+
if (contextResults.length < 3 && query.length >= 1) {
|
|
51
|
+
const searchResults = await PeopleService.search(query, 5)
|
|
52
|
+
if (callIdRef.current !== thisCallId) return [] // Stale
|
|
53
|
+
return [...contextResults, ...searchResults]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return contextResults
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
return { getSuggestions }
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### MentionAutocomplete Component
|
|
64
|
+
|
|
65
|
+
**File**: `src/components/chat/MentionAutocomplete.tsx`
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
interface MentionAutocompleteProps {
|
|
69
|
+
inputValue: string
|
|
70
|
+
textareaRef: RefObject<HTMLTextAreaElement | null>
|
|
71
|
+
onSelect: (suggestion, startIndex, endIndex) => void
|
|
72
|
+
getSuggestions?: (query: string) => MentionSuggestion[] | Promise<MentionSuggestion[]>
|
|
73
|
+
maxResults?: number
|
|
74
|
+
reverse?: boolean // Bottom-to-top rendering (closest to input = most relevant)
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Mention Detection**: Scans backwards from cursor for `@` preceded by whitespace or start-of-input.
|
|
79
|
+
|
|
80
|
+
**Keyboard Navigation**: ArrowUp/Down (flipped in reverse mode), Enter/Tab to select, Escape to close.
|
|
81
|
+
|
|
82
|
+
**Text Insertion**: `onSelect(suggestion, @position, endPosition)` — caller replaces the `@query` range with `@username `.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Checklist
|
|
87
|
+
|
|
88
|
+
- [ ] Tier 1 profiles loaded from conversation participants on mount
|
|
89
|
+
- [ ] Tier 2 search triggered when context has < 3 matches
|
|
90
|
+
- [ ] Stale call detection via incrementing callIdRef
|
|
91
|
+
- [ ] Agent suggestion always appears first if query matches
|
|
92
|
+
- [ ] `reverse` mode used when autocomplete renders above input
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
**Status**: Stable
|
|
97
|
+
**Last Updated**: 2026-03-14
|
|
98
|
+
**Contributors**: Community
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Modal & Confirmation Modal
|
|
2
|
+
|
|
3
|
+
**Category**: Design
|
|
4
|
+
**Applicable To**: All dialog overlays, confirmations, form modals, and persistent consent dialogs
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
The Modal system provides a portal-rendered overlay (z-55) with backdrop blur, escape/click-outside dismissal, body scroll lock, and safe-area-inset-top support. ConfirmationModal extends it with variant-colored icons and a two-button confirm/cancel footer. Use `persistent: true` to disable all dismissal paths for consent flows.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Implementation
|
|
16
|
+
|
|
17
|
+
### Modal (Base)
|
|
18
|
+
|
|
19
|
+
**File**: `src/components/modals/Modal.tsx`
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
interface ModalProps {
|
|
23
|
+
isOpen: boolean
|
|
24
|
+
onClose: () => void
|
|
25
|
+
children: React.ReactNode
|
|
26
|
+
title?: string
|
|
27
|
+
style?: React.CSSProperties
|
|
28
|
+
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
|
29
|
+
persistent?: boolean // Disables Escape, backdrop click, and close button
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Behavior**:
|
|
34
|
+
- `createPortal` to `document.body` at z-index 55
|
|
35
|
+
- Backdrop: `bg-black/50 backdrop-blur-sm`, click-outside closes (unless persistent)
|
|
36
|
+
- Escape key closes (unless persistent)
|
|
37
|
+
- Body scroll lock: `document.body.style.overflow = 'hidden'` on mount, restored on unmount
|
|
38
|
+
- Close button (X) top-right, hidden when persistent
|
|
39
|
+
- Title rendered above children if provided
|
|
40
|
+
- `paddingTop: env(safe-area-inset-top)` on the fixed container
|
|
41
|
+
- Backdrop click uses `e.target === e.currentTarget` to avoid closing on content clicks
|
|
42
|
+
|
|
43
|
+
**Usage**:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
<Modal isOpen={showModal} onClose={() => setShowModal(false)} title="Edit Item" maxWidth="md">
|
|
47
|
+
<form>{/* form content */}</form>
|
|
48
|
+
</Modal>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### ConfirmationModal
|
|
54
|
+
|
|
55
|
+
**File**: `src/components/modals/ConfirmationModal.tsx`
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
interface ConfirmationModalProps {
|
|
59
|
+
isOpen: boolean
|
|
60
|
+
onClose: () => void
|
|
61
|
+
onConfirm: () => void
|
|
62
|
+
title: string
|
|
63
|
+
message: string | React.ReactNode
|
|
64
|
+
confirmText?: string // default: "Confirm"
|
|
65
|
+
cancelText?: string // default: "Cancel"
|
|
66
|
+
variant?: 'danger' | 'warning' | 'info'
|
|
67
|
+
isLoading?: boolean
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Behavior**:
|
|
72
|
+
- Built on Modal (sm width)
|
|
73
|
+
- Variant-colored gradient icon circle at top:
|
|
74
|
+
- `danger`: purple-pink gradient
|
|
75
|
+
- `warning`: yellow-orange gradient
|
|
76
|
+
- `info`: blue-cyan gradient
|
|
77
|
+
- Two-button footer: Cancel (gray) | Confirm (variant gradient)
|
|
78
|
+
- Both buttons disabled during `isLoading`
|
|
79
|
+
- Prevents modal dismiss during loading: passes `() => {}` as `onClose` to Modal
|
|
80
|
+
|
|
81
|
+
**Usage**:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
<ConfirmationModal
|
|
85
|
+
isOpen={showDelete}
|
|
86
|
+
onClose={() => setShowDelete(false)}
|
|
87
|
+
onConfirm={handleDelete}
|
|
88
|
+
title="Delete Memory"
|
|
89
|
+
message="This action cannot be undone."
|
|
90
|
+
confirmText="Delete"
|
|
91
|
+
variant="danger"
|
|
92
|
+
isLoading={deleting}
|
|
93
|
+
/>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### SuccessModal
|
|
99
|
+
|
|
100
|
+
**File**: `src/components/modals/SuccessModal.tsx`
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
interface SuccessModalProps {
|
|
104
|
+
isOpen: boolean
|
|
105
|
+
onClose: () => void
|
|
106
|
+
title: string
|
|
107
|
+
message: React.ReactNode
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Single "Close" button with blue-cyan gradient. Check icon in gradient circle.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Anti-Patterns
|
|
116
|
+
|
|
117
|
+
### Rendering Modal Content Without Portal
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// Bad: Modal renders in component tree, z-index conflicts with parents
|
|
121
|
+
<div className="relative z-10">
|
|
122
|
+
<div className="fixed inset-0 bg-black/50">{content}</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
// Good: Use Modal component (portals to document.body)
|
|
126
|
+
<Modal isOpen={open} onClose={close}>{content}</Modal>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Forgetting isLoading Guard on Dismiss
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// Bad: User can close modal while async confirm is running
|
|
133
|
+
<Modal isOpen={open} onClose={() => setOpen(false)}>
|
|
134
|
+
<button onClick={asyncConfirm}>Confirm</button>
|
|
135
|
+
</Modal>
|
|
136
|
+
|
|
137
|
+
// Good: Disable dismiss during loading
|
|
138
|
+
<ConfirmationModal isLoading={loading} onClose={() => setOpen(false)} ... />
|
|
139
|
+
// ConfirmationModal internally passes () => {} as onClose when loading
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Checklist
|
|
145
|
+
|
|
146
|
+
- [ ] Use `Modal` base for custom dialogs, `ConfirmationModal` for confirm/cancel flows
|
|
147
|
+
- [ ] Set `persistent: true` for consent/TOS dialogs that must not be dismissed
|
|
148
|
+
- [ ] Set `maxWidth` appropriately (sm for confirms, md-lg for forms, xl-2xl for complex content)
|
|
149
|
+
- [ ] Guard dismiss during async operations with `isLoading`
|
|
150
|
+
- [ ] Content uses max-h-[90vh] with overflow-auto for long content
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
**Status**: Stable
|
|
155
|
+
**Last Updated**: 2026-03-14
|
|
156
|
+
**Contributors**: Community
|