@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,375 @@
|
|
|
1
|
+
# Searchable Settings Page
|
|
2
|
+
|
|
3
|
+
**Category**: Design
|
|
4
|
+
**Applicable To**: Settings pages with grouped sections, search-to-scroll, hash-based navigation, and a registry of searchable items
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
The Settings page uses a registry-based architecture: all settings are defined in a central `SettingsItem[]` array with name, description, category, hidden keywords, and a hash-fragment path. A search input filters the registry using AND-logic word matching, and selecting a result navigates to the route + scrolls to the section via its `id` attribute. This makes settings discoverable without browsing every section.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## When to Use This Pattern
|
|
16
|
+
|
|
17
|
+
**Use this pattern when:**
|
|
18
|
+
- Building a settings page with many sections that benefit from search
|
|
19
|
+
- Any page with hash-based scroll-to-section navigation
|
|
20
|
+
- Settings that span multiple sub-pages (main settings, ghost settings, delete account)
|
|
21
|
+
|
|
22
|
+
**Don't use this pattern when:**
|
|
23
|
+
- The page has fewer than 5 settings (just list them, no search needed)
|
|
24
|
+
- Settings are entirely form-based with no sections (use a simple form)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Core Principles
|
|
29
|
+
|
|
30
|
+
1. **Registry as Source of Truth**: All searchable settings defined in one `SettingsItem[]` array
|
|
31
|
+
2. **Hidden Keywords**: Extra search terms (`keywords[]`) improve discoverability without cluttering the UI
|
|
32
|
+
3. **AND-Logic Search**: All query words must match in the concatenated haystack
|
|
33
|
+
4. **Hash-Fragment Navigation**: Search results link to `path#section-id`, scrolling to the section
|
|
34
|
+
5. **Optimistic Updates**: Toggle/slider changes apply immediately, revert on API failure
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Implementation
|
|
39
|
+
|
|
40
|
+
### Settings Registry
|
|
41
|
+
|
|
42
|
+
**File**: `src/constant/settings-registry.ts`
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
export interface SettingsItem {
|
|
46
|
+
/** Unique identifier, also used as Algolia objectID */
|
|
47
|
+
id: string
|
|
48
|
+
/** Display name shown in search results */
|
|
49
|
+
name: string
|
|
50
|
+
/** Short description shown under the name */
|
|
51
|
+
description: string
|
|
52
|
+
/** Category grouping (e.g. "Ghost Mode", "Privacy", "Display") */
|
|
53
|
+
category: string
|
|
54
|
+
/** Extra search terms not visible in UI (e.g. "telemetry" for analytics) */
|
|
55
|
+
keywords: string[]
|
|
56
|
+
/** Route path + optional hash fragment (e.g. "/settings#privacy") */
|
|
57
|
+
path: string
|
|
58
|
+
/** Sub-items listed for context in search results */
|
|
59
|
+
sub_items: string[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const SETTINGS_REGISTRY: SettingsItem[] = [
|
|
63
|
+
{
|
|
64
|
+
id: 'ghost-mode',
|
|
65
|
+
name: 'Ghost Mode',
|
|
66
|
+
description: 'Configure your ghost persona and conversation behavior',
|
|
67
|
+
category: 'Ghost Mode',
|
|
68
|
+
keywords: ['persona', 'alter ego', 'anonymous', 'identity'],
|
|
69
|
+
path: '/settings/ghost#ghost-mode',
|
|
70
|
+
sub_items: ['Enable ghost mode', 'Ghost name', 'Ghost persona'],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'privacy-analytics',
|
|
74
|
+
name: 'Analytics',
|
|
75
|
+
description: 'Control whether usage data is collected',
|
|
76
|
+
category: 'Privacy',
|
|
77
|
+
keywords: ['tracking', 'telemetry', 'data collection', 'opt out'],
|
|
78
|
+
path: '/settings#privacy',
|
|
79
|
+
sub_items: ['Toggle analytics on/off'],
|
|
80
|
+
},
|
|
81
|
+
// ... 25 total items
|
|
82
|
+
]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Search Algorithm
|
|
86
|
+
|
|
87
|
+
**File**: `src/routes/settings/index.tsx`
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
function searchSettings(query: string): SettingsItem[] {
|
|
91
|
+
if (!query.trim()) return []
|
|
92
|
+
const q = query.toLowerCase()
|
|
93
|
+
return SETTINGS_REGISTRY.filter((item) => {
|
|
94
|
+
const haystack = [
|
|
95
|
+
item.name,
|
|
96
|
+
item.description,
|
|
97
|
+
item.category,
|
|
98
|
+
...item.sub_items,
|
|
99
|
+
...item.keywords,
|
|
100
|
+
].join(' ').toLowerCase()
|
|
101
|
+
return q.split(/\s+/).every((word) => haystack.includes(word))
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**How it works**:
|
|
107
|
+
- Concatenates all searchable fields into one string ("haystack")
|
|
108
|
+
- Splits query into words
|
|
109
|
+
- Every word must appear in the haystack (AND logic)
|
|
110
|
+
- Example: `"toggle privacy"` matches items containing both "toggle" AND "privacy"
|
|
111
|
+
|
|
112
|
+
### Search UI
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
116
|
+
const [searchFocused, setSearchFocused] = useState(false)
|
|
117
|
+
const searchRef = useRef<HTMLDivElement>(null)
|
|
118
|
+
|
|
119
|
+
const searchResults = useMemo(() => searchSettings(searchQuery), [searchQuery])
|
|
120
|
+
const showResults = searchFocused && searchQuery.trim().length > 0
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Rendered structure**:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
┌─ Search Input ────────────────────────────────┐
|
|
127
|
+
│ 🔍 Search settings... ✕ │
|
|
128
|
+
├───────────────────────────────────────────────┤
|
|
129
|
+
│ Ghost Mode [Ghost Mode] │ ← category badge
|
|
130
|
+
│ Configure your ghost persona... │
|
|
131
|
+
│ • Enable ghost mode • Ghost name │ ← sub_items
|
|
132
|
+
├───────────────────────────────────────────────┤
|
|
133
|
+
│ Analytics [Privacy] │
|
|
134
|
+
│ Control whether usage data is collected │
|
|
135
|
+
│ • Toggle analytics on/off │
|
|
136
|
+
└───────────────────────────────────────────────┘
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Result click handler**:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
onClick={() => {
|
|
143
|
+
setSearchQuery('')
|
|
144
|
+
setSearchFocused(false)
|
|
145
|
+
const [to, hash] = item.path.split('#')
|
|
146
|
+
navigate({ to, hash })
|
|
147
|
+
if (hash) {
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
document.getElementById(hash)?.scrollIntoView({
|
|
150
|
+
behavior: 'smooth',
|
|
151
|
+
block: 'start',
|
|
152
|
+
})
|
|
153
|
+
}, 300) // Delay allows DOM to settle after navigation
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Section Anchors
|
|
159
|
+
|
|
160
|
+
Each settings section uses an `id` attribute matching the hash fragment:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
<div id="privacy" className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-6">
|
|
164
|
+
<div className="flex items-center gap-3 mb-4">
|
|
165
|
+
<Shield className="w-5 h-5 text-blue-400" />
|
|
166
|
+
<h2 className="text-lg font-semibold text-white">Privacy</h2>
|
|
167
|
+
</div>
|
|
168
|
+
{/* Setting controls */}
|
|
169
|
+
</div>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Hash Scroll on Page Load
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const hash = window.location.hash.slice(1)
|
|
177
|
+
if (!hash) return
|
|
178
|
+
const timer = setTimeout(() => {
|
|
179
|
+
document.getElementById(hash)?.scrollIntoView({
|
|
180
|
+
behavior: 'smooth',
|
|
181
|
+
block: 'start',
|
|
182
|
+
})
|
|
183
|
+
}, 300)
|
|
184
|
+
return () => clearTimeout(timer)
|
|
185
|
+
}, [])
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
This handles both direct links (`/settings#privacy`) and search-driven navigation.
|
|
189
|
+
|
|
190
|
+
### Settings Controls Within Sections
|
|
191
|
+
|
|
192
|
+
**Toggle setting**:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
<div className="flex items-center justify-between py-3">
|
|
196
|
+
<div>
|
|
197
|
+
<p className="text-sm font-medium text-white">Analytics</p>
|
|
198
|
+
<p className="text-xs text-gray-500">Help improve the app by sharing usage data</p>
|
|
199
|
+
</div>
|
|
200
|
+
<ToggleSwitch
|
|
201
|
+
checked={analyticsEnabled}
|
|
202
|
+
onChange={handleAnalyticsToggle}
|
|
203
|
+
disabled={toggling}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Slider setting**:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
<div className="py-3">
|
|
212
|
+
<p className="text-sm font-medium text-white mb-2">Search Relevance</p>
|
|
213
|
+
<p className="text-xs text-gray-500 mb-3">Higher values return fewer but more relevant results</p>
|
|
214
|
+
<Slider min={0} max={0.8} step={0.05} value={threshold} onChange={handleChange} />
|
|
215
|
+
</div>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Link to sub-page**:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
<Link to="/settings/ghost"
|
|
222
|
+
className="flex items-center justify-between p-4 bg-gray-900/50 border border-gray-800 rounded-xl hover:border-purple-500/50 transition-colors">
|
|
223
|
+
<div className="flex items-center gap-3">
|
|
224
|
+
<Ghost className="w-5 h-5 text-purple-400" />
|
|
225
|
+
<div>
|
|
226
|
+
<p className="text-sm font-medium text-white">Ghost Mode</p>
|
|
227
|
+
<p className="text-xs text-gray-500">Configure persona and trust levels</p>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
<ChevronRight className="w-4 h-4 text-gray-500" />
|
|
231
|
+
</Link>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### State Management
|
|
235
|
+
|
|
236
|
+
**Server-synced preferences** (UIPreferencesContext):
|
|
237
|
+
```typescript
|
|
238
|
+
const { preferences, updatePreference } = useUIPreferences()
|
|
239
|
+
|
|
240
|
+
// Optimistic update + API call
|
|
241
|
+
const handleToggle = async (value: boolean) => {
|
|
242
|
+
const success = await updatePreference({ memory_card_overflow: value ? 'scroll' : 'clip' })
|
|
243
|
+
if (!success) toast.error({ title: 'Failed to save' })
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Device-local preferences** (UIPreferencesLocalContext):
|
|
248
|
+
```typescript
|
|
249
|
+
const { contentFontSize, setContentFontSize } = useUIPreferencesLocal()
|
|
250
|
+
// Stored in localStorage, varies per device (mobile vs desktop)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Examples
|
|
256
|
+
|
|
257
|
+
### Adding a New Setting
|
|
258
|
+
|
|
259
|
+
1. **Add to registry** (`src/constant/settings-registry.ts`):
|
|
260
|
+
```typescript
|
|
261
|
+
{
|
|
262
|
+
id: 'theme-mode',
|
|
263
|
+
name: 'Theme',
|
|
264
|
+
description: 'Choose light or dark color scheme',
|
|
265
|
+
category: 'Display',
|
|
266
|
+
keywords: ['dark mode', 'light mode', 'appearance', 'color'],
|
|
267
|
+
path: '/settings#ui-preferences',
|
|
268
|
+
sub_items: ['Light', 'Dark', 'System'],
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
2. **Add section anchor** (in settings page):
|
|
273
|
+
```typescript
|
|
274
|
+
<div id="ui-preferences" className="bg-gray-900/50 ...">
|
|
275
|
+
{/* existing controls + new theme toggle */}
|
|
276
|
+
</div>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
3. **Done** — the new setting is immediately searchable.
|
|
280
|
+
|
|
281
|
+
### Adding a New Sub-Page
|
|
282
|
+
|
|
283
|
+
1. Create route: `src/routes/settings/new-page.tsx`
|
|
284
|
+
2. Add registry entries with `path: '/settings/new-page#section-id'`
|
|
285
|
+
3. Add link in main settings page with ChevronRight arrow
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Anti-Patterns
|
|
290
|
+
|
|
291
|
+
### Hardcoding Settings Without Registry
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
// Bad: Settings not searchable, no central definition
|
|
295
|
+
<div>
|
|
296
|
+
<h2>Privacy</h2>
|
|
297
|
+
<ToggleSwitch checked={analytics} onChange={toggleAnalytics} />
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
// Good: Define in registry, render from section id, searchable automatically
|
|
301
|
+
// settings-registry.ts:
|
|
302
|
+
{ id: 'privacy-analytics', name: 'Analytics', path: '/settings#privacy', ... }
|
|
303
|
+
// settings page:
|
|
304
|
+
<div id="privacy">...</div>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### OR-Logic Search
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// Bad: Returns too many results (any word matches)
|
|
311
|
+
return q.split(/\s+/).some((word) => haystack.includes(word))
|
|
312
|
+
|
|
313
|
+
// Good: AND logic — all words must match
|
|
314
|
+
return q.split(/\s+/).every((word) => haystack.includes(word))
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Scrolling Without Delay
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// Bad: Element might not be in DOM yet after navigation
|
|
321
|
+
document.getElementById(hash)?.scrollIntoView({ behavior: 'smooth' })
|
|
322
|
+
|
|
323
|
+
// Good: Wait for DOM to settle
|
|
324
|
+
setTimeout(() => {
|
|
325
|
+
document.getElementById(hash)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
326
|
+
}, 300)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Key Design Decisions
|
|
332
|
+
|
|
333
|
+
### Search Architecture
|
|
334
|
+
|
|
335
|
+
| Decision | Choice | Rationale |
|
|
336
|
+
|---|---|---|
|
|
337
|
+
| Search data source | Static registry array | Fast, no API call, works offline |
|
|
338
|
+
| Search algorithm | AND-logic word matching | More precise than OR; reduces noise |
|
|
339
|
+
| Hidden keywords | `keywords[]` field | Improves discoverability without UI clutter |
|
|
340
|
+
| Result navigation | Route + hash scroll | Deep-links to exact section |
|
|
341
|
+
| Scroll delay | 300ms setTimeout | Allows DOM render after route navigation |
|
|
342
|
+
|
|
343
|
+
### State Management
|
|
344
|
+
|
|
345
|
+
| Decision | Choice | Rationale |
|
|
346
|
+
|---|---|---|
|
|
347
|
+
| Server preferences | UIPreferencesContext with optimistic update | Syncs across devices; instant UI response |
|
|
348
|
+
| Device preferences | localStorage via UIPreferencesLocalContext | Font size varies per device (mobile vs desktop) |
|
|
349
|
+
| Update pattern | Optimistic + rollback on failure | Feels instant; reverts if API fails |
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Checklist
|
|
354
|
+
|
|
355
|
+
- [ ] New settings added to `SETTINGS_REGISTRY` with id, name, description, category, keywords, path, sub_items
|
|
356
|
+
- [ ] Section div has `id` attribute matching the hash fragment in the registry path
|
|
357
|
+
- [ ] Keywords include synonyms users might search for (e.g., "telemetry" for analytics)
|
|
358
|
+
- [ ] Sub-items list the specific controls within the section
|
|
359
|
+
- [ ] Path uses hash fragment for same-page sections, full route for sub-pages
|
|
360
|
+
- [ ] Settings controls use ToggleSwitch, Slider, or link-with-chevron patterns
|
|
361
|
+
- [ ] Server-synced settings use optimistic update via UIPreferencesContext
|
|
362
|
+
- [ ] Device-local settings use localStorage via UIPreferencesLocalContext
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Related Patterns
|
|
367
|
+
|
|
368
|
+
- **[Form Controls](./tanstack-cloudflare.form-controls.md)**: ToggleSwitch and Slider used within settings sections
|
|
369
|
+
- **[Unified Header](./tanstack-cloudflare.unified-header.md)**: Settings page uses UnifiedHeader with back navigation
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
**Status**: Stable
|
|
374
|
+
**Last Updated**: 2026-03-14
|
|
375
|
+
**Contributors**: Community
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# SlideOverPanel & MessageSearchSlideover
|
|
2
|
+
|
|
3
|
+
**Category**: Design
|
|
4
|
+
**Applicable To**: Right-side slide panels, search slideouts, and configuration drawers
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
SlideOverPanel is a lightweight right-side drawer (w-72) that slides in with a translate-x animation and backdrop. MessageSearchSlideover is a specialized full-width search panel with debounced API search and result navigation. Both render inline (not portaled) with fixed positioning.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Implementation
|
|
16
|
+
|
|
17
|
+
### SlideOverPanel (Generic Drawer)
|
|
18
|
+
|
|
19
|
+
**File**: `src/components/SlideOverPanel.tsx`
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
interface SlideOverPanelProps {
|
|
23
|
+
open: boolean
|
|
24
|
+
onClose: () => void
|
|
25
|
+
children: ReactNode
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Behavior**:
|
|
30
|
+
- Fixed positioning: `top-14` (below header), `right-0`, `bottom-0`
|
|
31
|
+
- Width: `w-72` (288px)
|
|
32
|
+
- **Not** a portal — renders inline from parent component
|
|
33
|
+
- **Animation** (200ms):
|
|
34
|
+
- Backdrop: `opacity-0` → `opacity-100`
|
|
35
|
+
- Panel: `translate-x-full` → `translate-x-0`
|
|
36
|
+
- Mounted/visible state tracking for exit animation before unmount
|
|
37
|
+
- Backdrop click closes
|
|
38
|
+
- z-index: backdrop 20, panel 30
|
|
39
|
+
- Dark theme: `bg-gray-900` with `border-l border-gray-800`
|
|
40
|
+
|
|
41
|
+
**Usage**:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
<SlideOverPanel open={panelOpen} onClose={() => setPanelOpen(false)}>
|
|
45
|
+
<div className="p-4">
|
|
46
|
+
<h3>Panel Content</h3>
|
|
47
|
+
{/* configuration, details, etc. */}
|
|
48
|
+
</div>
|
|
49
|
+
</SlideOverPanel>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### MessageSearchSlideover (Search Panel)
|
|
55
|
+
|
|
56
|
+
**File**: `src/components/chat/MessageSearchSlideover.tsx`
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
interface MessageSearchSlideoverProps {
|
|
60
|
+
conversationId: string
|
|
61
|
+
isOpen: boolean
|
|
62
|
+
onClose: () => void
|
|
63
|
+
onSelectMessage: (messageId: string) => void
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Behavior**:
|
|
68
|
+
- Full-screen fixed panel (not constrained to right side)
|
|
69
|
+
- Max-width: `md` on desktop
|
|
70
|
+
- **Header**: Search icon + auto-focused input + X close button
|
|
71
|
+
- **Debounced search**: 300ms delay, calls `/api/search/messages`
|
|
72
|
+
- **Results**: Message snippet with fade mask, role label, relative time
|
|
73
|
+
- Click result → calls `onSelectMessage(messageId)` and closes panel
|
|
74
|
+
- Escape key closes
|
|
75
|
+
- Backdrop click closes
|
|
76
|
+
- Safe-area-inset-top handling
|
|
77
|
+
|
|
78
|
+
**Usage**:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
<MessageSearchSlideover
|
|
82
|
+
conversationId={conversationId}
|
|
83
|
+
isOpen={searchOpen}
|
|
84
|
+
onClose={() => setSearchOpen(false)}
|
|
85
|
+
onSelectMessage={(id) => scrollToMessage(id)}
|
|
86
|
+
/>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Anti-Patterns
|
|
92
|
+
|
|
93
|
+
### Using Portal for Simple Slide Panels
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// Bad: Unnecessary portal when parent layout supports fixed children
|
|
97
|
+
{createPortal(<div className="fixed right-0">...</div>, document.body)}
|
|
98
|
+
|
|
99
|
+
// Good: Render inline — simpler and avoids portal context issues
|
|
100
|
+
<SlideOverPanel open={open} onClose={close}>{content}</SlideOverPanel>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Not Auto-Focusing Search Input
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Bad: User must click into input after opening
|
|
107
|
+
<input type="text" />
|
|
108
|
+
|
|
109
|
+
// Good: Auto-focus on open
|
|
110
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
111
|
+
useEffect(() => { if (isOpen) inputRef.current?.focus() }, [isOpen])
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Checklist
|
|
117
|
+
|
|
118
|
+
- [ ] Use `SlideOverPanel` for generic right-side drawers
|
|
119
|
+
- [ ] Use `MessageSearchSlideover` for search-within-conversation
|
|
120
|
+
- [ ] Auto-focus input when panel opens
|
|
121
|
+
- [ ] Debounce search input (300ms)
|
|
122
|
+
- [ ] Panel renders below header (`top-14`) to avoid overlapping
|
|
123
|
+
- [ ] Exit animation completes before unmount (mounted/visible state tracking)
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
**Status**: Stable
|
|
128
|
+
**Last Updated**: 2026-03-14
|
|
129
|
+
**Contributors**: Community
|