Skip to content

Commit b5842fb

Browse files
abelpzcursoragent
andcommitted
feat: support modal-only resources without panel viewers
Enable resources to be registered without panel tab viewers, making them accessible only via modal popups (Entry Viewer Registry). This allows for auxiliary resources like glossaries and reference materials that don't need full panel views. Changes: - Make viewer optional in ResourceTypeDefinition type - Update ResourceTypeRegistry to conditionally register viewers - Skip panel assignment for resources without viewers - Register Translation Words and Translation Academy as modal-only (testing) - Add logging to differentiate modal-only from full resources Benefits: - Reduces UI clutter by hiding auxiliary resources from panel tabs - Maintains full background download and caching functionality - Resources remain accessible via links and cross-references in modals - Provides flexibility for different resource interaction patterns Technical Details: - ViewerRegistry.hasViewer() checks determine panel assignment - Modal-only resources still load metadata and download content - Entry Viewer Registry handles modal rendering independently Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 357746f commit b5842fb

File tree

5 files changed

+114
-87
lines changed

5 files changed

+114
-87
lines changed

apps/tc-study/src/components/ResourceTypeInitializer.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,27 @@ export function ResourceTypeInitializer() {
2626
} = await import('../resourceTypes')
2727

2828
registry.register(scriptureResourceType)
29-
registry.register(translationWordsResourceType)
3029
registry.register(translationWordsLinksResourceType)
31-
registry.register(translationAcademyResourceType)
3230
registry.register(translationNotesResourceType)
3331

32+
// TESTING: Register TW and TA without viewers (modal-only resources)
33+
// We create modified versions that only have the loader, no viewer
34+
console.log('📦 [Initializer] Registering modal-only resources (TW, TA)...')
35+
36+
// Translation Words (modal-only)
37+
const twModalOnly = {
38+
...translationWordsResourceType,
39+
viewer: undefined // Remove viewer so it won't appear as a tab
40+
}
41+
registry.register(twModalOnly)
42+
43+
// Translation Academy (modal-only)
44+
const taModalOnly = {
45+
...translationAcademyResourceType,
46+
viewer: undefined // Remove viewer so it won't appear as a tab
47+
}
48+
registry.register(taModalOnly)
49+
3450
console.log('📦 [Initializer] ✅ Resource types registered')
3551
setInitialized(true)
3652

apps/tc-study/src/components/read/SimplifiedReadView.tsx

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,43 @@
88
*/
99

1010
import {
11-
DndContext,
12-
DragOverlay,
13-
PointerSensor,
14-
TouchSensor,
15-
pointerWithin,
16-
useSensor,
17-
useSensors,
18-
type DragEndEvent,
19-
type DragOverEvent,
20-
type DragStartEvent,
11+
DndContext,
12+
DragOverlay,
13+
PointerSensor,
14+
TouchSensor,
15+
pointerWithin,
16+
useSensor,
17+
useSensors,
18+
type DragEndEvent,
19+
type DragOverEvent,
20+
type DragStartEvent,
2121
} from '@dnd-kit/core'
2222
import {
23-
LinkedPanel,
24-
LinkedPanelsContainer,
25-
createDefaultPluginRegistry,
26-
type LinkedPanelsConfig,
23+
LinkedPanel,
24+
LinkedPanelsContainer,
25+
createDefaultPluginRegistry,
26+
type LinkedPanelsConfig,
2727
} from 'linked-panels'
28-
import { Loader2, Package, CheckCircle2, XCircle } from 'lucide-react'
28+
import { CheckCircle2, Loader2, Package, XCircle } from 'lucide-react'
2929
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3030
import { useNavigate } from 'react-router-dom'
31-
import { useCatalogManager, useCacheAdapter, useCompletenessChecker, useResourceTypeRegistry, useViewerRegistry } from '../../contexts'
31+
import { useCacheAdapter, useCatalogManager, useCompletenessChecker, useResourceTypeRegistry, useViewerRegistry } from '../../contexts'
3232
import { useAppStore } from '../../contexts/AppContext'
3333
import type { ResourceInfo } from '../../contexts/types'
3434
import { useBackgroundDownload, useCatalogBackgroundDownload, useResourceManagement, useStudioResources, useSwipeGesture } from '../../hooks'
3535
import { createResourceMetadata } from '../../lib/services/ResourceMetadataFactory'
36-
import { useWorkspaceStore } from '../../lib/stores/workspaceStore'
3736
import { usePackageStore } from '../../lib/stores/packageStore'
38-
import type { ExportWorkerMessage, ExportWorkerResponse } from '../../workers/collectionExport.worker'
37+
import { useWorkspaceStore } from '../../lib/stores/workspaceStore'
3938
import {
40-
entryLinkClickPlugin,
41-
linkClickPlugin,
42-
scriptureContentRequestPlugin,
43-
scriptureContentResponsePlugin,
44-
scriptureTokensBroadcastPlugin,
45-
tokenClickPlugin
39+
entryLinkClickPlugin,
40+
linkClickPlugin,
41+
scriptureContentRequestPlugin,
42+
scriptureContentResponsePlugin,
43+
scriptureTokensBroadcastPlugin,
44+
tokenClickPlugin
4645
} from '../../plugins/messageTypePlugins'
4746
import { useStudyStore } from '../../store/studyStore'
47+
import type { ExportWorkerMessage, ExportWorkerResponse } from '../../workers/collectionExport.worker'
4848
import { CollectionImportDialog } from '../collections/CollectionImportDialog'
4949
import { EntryResourceModal } from '../common/EntryResourceModal'
5050
import { FallbackViewer } from '../resources'
@@ -533,17 +533,21 @@ export function SimplifiedReadView({ initialLanguage }: SimplifiedReadViewProps
533533
addResource(basicResourceInfo)
534534
loadedResourceKeys.push(resourceKey)
535535

536-
// Assign to panels immediately
537-
const isScripture = type === 'scripture'
538-
const panelId = isScripture ? 'panel-1' : 'panel-2'
539-
const currentPanel = getPanel(panelId)
540-
const currentIndex = currentPanel?.resourceKeys.length || 0
541-
assignResourceToPanel(resourceKey, panelId, currentIndex)
542-
if (currentIndex === 0) {
543-
setActiveResourceInPanel(panelId, 0)
536+
// Only assign to panels if resource has a viewer (modal-only resources won't appear as tabs)
537+
const hasViewer = viewerRegistry.hasViewer(type)
538+
if (hasViewer) {
539+
const isScripture = type === 'scripture'
540+
const panelId = isScripture ? 'panel-1' : 'panel-2'
541+
const currentPanel = getPanel(panelId)
542+
const currentIndex = currentPanel?.resourceKeys.length || 0
543+
assignResourceToPanel(resourceKey, panelId, currentIndex)
544+
if (currentIndex === 0) {
545+
setActiveResourceInPanel(panelId, 0)
546+
}
547+
console.log(`✅ Immediately added to panel: ${resourceKey} (metadata will load in background)`)
548+
} else {
549+
console.log(`✅ Loaded resource (modal-only): ${resourceKey} (no panel viewer)`)
544550
}
545-
546-
console.log(`✅ Immediately added: ${resourceKey} (metadata will load in background)`)
547551
}
548552

549553
console.log(`⚡ Phase 1 complete: ${loadedResourceKeys.length} resources in UI`)

apps/tc-study/src/components/studio/LinkedPanelsStudio.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,28 +19,28 @@ import { usePackageStore } from '../../lib/stores'
1919
import { useWorkspaceStore } from '../../lib/stores/workspaceStore'
2020
import { entryLinkClickPlugin, linkClickPlugin, scriptureContentRequestPlugin, scriptureContentResponsePlugin, scriptureTokensBroadcastPlugin, tokenClickPlugin } from '../../plugins/messageTypePlugins'
2121
import { useStudyStore } from '../../store/studyStore'
22-
import { FallbackViewer } from '../resources'
2322
import { EntryResourceModal } from '../common/EntryResourceModal'
23+
import { FallbackViewer } from '../resources'
2424
// import { AddResourceWizard } from '../wizard' // Temporarily inlined
2525
// import { AnchorSelector } from './AnchorSelector'
26+
import {
27+
DndContext,
28+
DragOverlay,
29+
PointerSensor,
30+
TouchSensor,
31+
pointerWithin,
32+
useSensor,
33+
useSensors,
34+
type DragEndEvent,
35+
type DragOverEvent,
36+
type DragStartEvent,
37+
} from '@dnd-kit/core'
2638
import { getBaseResourceKey, useResourceManagement, useStudioResources, useSwipeGesture } from '../../hooks'
39+
import { DroppablePanel } from './DroppablePanel'
2740
import { EmptyPanelState } from './EmptyPanelState'
2841
import { NavigationBar } from './NavigationBar'
2942
import { PanelHeader } from './PanelHeader'
30-
import { DroppablePanel } from './DroppablePanel'
3143
import { ResourceLibrarySidebar, ResourceWizardPanel } from './ResourceLibrarySidebar'
32-
import {
33-
DndContext,
34-
DragOverlay,
35-
PointerSensor,
36-
TouchSensor,
37-
useSensor,
38-
useSensors,
39-
pointerWithin,
40-
type DragEndEvent,
41-
type DragStartEvent,
42-
type DragOverEvent,
43-
} from '@dnd-kit/core'
4444

4545
export function LinkedPanelsStudio() {
4646
// Wizard state

packages/resource-types/src/ResourceTypeRegistry.ts

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,8 @@ export class ResourceTypeRegistry {
5454
throw new Error(`Resource type '${id}' must have a loader`)
5555
}
5656

57-
if (!viewer) {
58-
throw new Error(`Resource type '${id}' must have a viewer`)
59-
}
57+
// Viewer is optional - resources without viewers are modal-only
58+
// (accessible via Entry Viewer Registry, not as panel tabs)
6059

6160
// 2. Store definition
6261
this.types.set(id, definition)
@@ -96,44 +95,50 @@ export class ResourceTypeRegistry {
9695
throw error
9796
}
9897

99-
// 5. Register viewer (UI layer)
100-
try {
101-
this.viewerRegistry.registerViewer({
102-
resourceType: id,
103-
displayName: displayName,
104-
component: viewer,
105-
canHandle: (metadata: any) => {
106-
// Check type
107-
if (metadata.type === id) return true
108-
109-
// Check subjects
110-
if (metadata.subject && subjects.includes(metadata.subject)) {
111-
return true
112-
}
113-
114-
// Check aliases
115-
if (aliases) {
116-
for (const alias of aliases) {
117-
if (
118-
metadata.type === alias ||
119-
metadata.resourceId === alias ||
120-
metadata.subject === alias
121-
) {
122-
return true
98+
// 5. Register viewer (UI layer) - only if viewer is provided
99+
if (viewer) {
100+
try {
101+
this.viewerRegistry.registerViewer({
102+
resourceType: id,
103+
displayName: displayName,
104+
component: viewer,
105+
canHandle: (metadata: any) => {
106+
// Check type
107+
if (metadata.type === id) return true
108+
109+
// Check subjects
110+
if (metadata.subject && subjects.includes(metadata.subject)) {
111+
return true
112+
}
113+
114+
// Check aliases
115+
if (aliases) {
116+
for (const alias of aliases) {
117+
if (
118+
metadata.type === alias ||
119+
metadata.resourceId === alias ||
120+
metadata.subject === alias
121+
) {
122+
return true
123+
}
123124
}
124125
}
126+
127+
return false
125128
}
126-
127-
return false
128-
}
129-
})
130-
} catch (error) {
131-
console.error(`Failed to register viewer for '${id}':`, error)
132-
throw error
129+
})
130+
} catch (error) {
131+
console.error(`Failed to register viewer for '${id}':`, error)
132+
throw error
133+
}
134+
} else {
135+
if (this.debug) {
136+
console.log(`ℹ️ No viewer registered for '${id}' (modal-only resource)`)
137+
}
133138
}
134139

135140
// 6. Log success (simplified)
136-
console.log(`✅ Registered: ${id} (${subjects.length} subjects)`)
141+
console.log(`✅ Registered: ${id} (${subjects.length} subjects)${viewer ? '' : ' [modal-only]'}`)
137142
}
138143

139144
/**

packages/resource-types/src/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
* Defines the structure for resource type plugins
55
*/
66

7-
import type { ResourceLoader, ResourceViewerProps } from './base-types'
87
import type { BaseSignal, ResourceMetadata } from '@bt-synergy/resource-panels'
98
import type { ComponentType } from 'react'
9+
import type { ResourceLoader, ResourceViewerProps } from './base-types'
1010

1111
/**
1212
* API filters for Door43 catalog requests
@@ -261,6 +261,8 @@ export interface ResourceTypeDefinition {
261261
/**
262262
* Viewer component - either a single component or platform-specific components
263263
*
264+
* Optional: If omitted, resource will be modal-only (accessible via Entry Viewer Registry)
265+
*
264266
* Single viewer (works on both platforms):
265267
* ```typescript
266268
* viewer: MyViewer
@@ -274,7 +276,7 @@ export interface ResourceTypeDefinition {
274276
* }
275277
* ```
276278
*/
277-
viewer: ComponentType<ResourceViewerProps> | PlatformViewers
279+
viewer?: ComponentType<ResourceViewerProps> | PlatformViewers
278280

279281
// ===== COMMUNICATION =====
280282
/**

0 commit comments

Comments
 (0)