cjm-transcript-source-select 0.0.28__tar.gz → 0.0.30__tar.gz

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.
Files changed (36) hide show
  1. {cjm_transcript_source_select-0.0.28/cjm_transcript_source_select.egg-info → cjm_transcript_source_select-0.0.30}/PKG-INFO +82 -55
  2. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/README.md +77 -51
  3. cjm_transcript_source_select-0.0.30/cjm_transcript_source_select/__init__.py +1 -0
  4. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/_modidx.py +2 -0
  5. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/components/source_browser.py +54 -18
  6. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/components/step_renderer.py +46 -22
  7. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/models.py +4 -0
  8. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/routes/init.py +9 -1
  9. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/routes/local_files.py +3 -0
  10. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/routes/source_browser.py +28 -1
  11. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30/cjm_transcript_source_select.egg-info}/PKG-INFO +82 -55
  12. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select.egg-info/requires.txt +4 -3
  13. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/pyproject.toml +1 -1
  14. cjm_transcript_source_select-0.0.28/cjm_transcript_source_select/__init__.py +0 -1
  15. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/LICENSE +0 -0
  16. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/MANIFEST.in +0 -0
  17. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/components/__init__.py +0 -0
  18. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/components/helpers.py +0 -0
  19. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/components/local_files.py +0 -0
  20. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/components/preview_panel.py +0 -0
  21. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/components/selection_queue.py +0 -0
  22. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/html_ids.py +0 -0
  23. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/routes/__init__.py +0 -0
  24. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/routes/core.py +0 -0
  25. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/routes/filtering.py +0 -0
  26. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/routes/queue.py +0 -0
  27. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/routes/tabs.py +0 -0
  28. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/services/__init__.py +0 -0
  29. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/services/source.py +0 -0
  30. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/services/source_utils.py +0 -0
  31. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select/utils.py +0 -0
  32. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select.egg-info/SOURCES.txt +0 -0
  33. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select.egg-info/dependency_links.txt +0 -0
  34. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select.egg-info/entry_points.txt +0 -0
  35. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/cjm_transcript_source_select.egg-info/top_level.txt +0 -0
  36. {cjm_transcript_source_select-0.0.28 → cjm_transcript_source_select-0.0.30}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-transcript-source-select
3
- Version: 0.0.28
3
+ Version: 0.0.30
4
4
  Summary: FastHTML source selection component for transcript decomposition workflows, with federated database browsing, drag-drop ordering, and keyboard navigation.
5
5
  Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
6
6
  License: Apache-2.0
@@ -15,13 +15,14 @@ Classifier: Programming Language :: Python :: 3 :: Only
15
15
  Requires-Python: >=3.12
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
+ Requires-Dist: python-fasthtml==0.13.4
18
19
  Requires-Dist: cjm-plugin-system
19
20
  Requires-Dist: cjm-transcription-plugin-system
20
21
  Requires-Dist: cjm-fasthtml-app-core
21
22
  Requires-Dist: cjm-fasthtml-daisyui
22
23
  Requires-Dist: cjm_fasthtml_lucide_icons
23
- Requires-Dist: cjm_fasthtml_file_browser>=0.0.17
24
- Requires-Dist: cjm_fasthtml_keyboard_navigation>=0.0.20
24
+ Requires-Dist: cjm_fasthtml_file_browser>=0.0.19
25
+ Requires-Dist: cjm_fasthtml_keyboard_navigation>=0.0.23
25
26
  Requires-Dist: duckdb
26
27
  Requires-Dist: pandas
27
28
  Requires-Dist: cjm_workflow_state
@@ -29,7 +30,7 @@ Requires-Dist: cjm_source_provider
29
30
  Requires-Dist: cjm_fasthtml_interactions
30
31
  Requires-Dist: cjm_fasthtml_viewport_fit
31
32
  Requires-Dist: cjm_fasthtml_virtual_collection
32
- Requires-Dist: cjm_fasthtml_sortable_queue>=0.0.13
33
+ Requires-Dist: cjm_fasthtml_sortable_queue>=0.0.16
33
34
  Requires-Dist: cjm_fasthtml_design_system>=0.0.9
34
35
  Dynamic: license-file
35
36
 
@@ -95,57 +96,57 @@ graph LR
95
96
  utils[utils<br/>utils]
96
97
 
97
98
  components_helpers --> models
98
- components_local_files --> components_helpers
99
99
  components_local_files --> html_ids
100
+ components_local_files --> components_helpers
100
101
  components_preview_panel --> html_ids
101
102
  components_source_browser --> services_source_utils
102
103
  components_source_browser --> html_ids
103
104
  components_source_browser --> utils
104
- components_step_renderer --> components_preview_panel
105
- components_step_renderer --> utils
106
105
  components_step_renderer --> components_selection_queue
107
- components_step_renderer --> components_source_browser
108
- components_step_renderer --> models
109
106
  components_step_renderer --> html_ids
107
+ components_step_renderer --> models
110
108
  components_step_renderer --> components_local_files
111
- routes_core --> components_step_renderer
109
+ components_step_renderer --> components_source_browser
110
+ components_step_renderer --> components_preview_panel
111
+ components_step_renderer --> utils
112
112
  routes_core --> models
113
- routes_core --> components_selection_queue
114
- routes_core --> services_source
115
113
  routes_core --> html_ids
116
- routes_filtering --> routes_core
114
+ routes_core --> components_step_renderer
115
+ routes_core --> services_source
116
+ routes_core --> components_selection_queue
117
117
  routes_filtering --> services_source_utils
118
118
  routes_filtering --> services_source
119
+ routes_filtering --> routes_core
119
120
  routes_filtering --> models
120
- routes_init --> routes_local_files
121
- routes_init --> routes_source_browser
122
121
  routes_init --> routes_core
122
+ routes_init --> services_source
123
+ routes_init --> routes_local_files
123
124
  routes_init --> routes_queue
124
125
  routes_init --> routes_tabs
125
- routes_init --> services_source
126
- routes_init --> routes_filtering
127
126
  routes_init --> models
128
- routes_local_files --> routes_core
127
+ routes_init --> routes_filtering
128
+ routes_init --> routes_source_browser
129
129
  routes_local_files --> components_local_files
130
130
  routes_local_files --> services_source
131
+ routes_local_files --> routes_core
131
132
  routes_local_files --> models
132
- routes_queue --> components_preview_panel
133
- routes_queue --> routes_core
134
- routes_queue --> services_source_utils
135
133
  routes_queue --> services_source
134
+ routes_queue --> services_source_utils
135
+ routes_queue --> routes_core
136
136
  routes_queue --> models
137
- routes_source_browser --> components_preview_panel
138
- routes_source_browser --> components_source_browser
137
+ routes_queue --> components_preview_panel
139
138
  routes_source_browser --> services_source
140
- routes_source_browser --> routes_core
139
+ routes_source_browser --> components_source_browser
140
+ routes_source_browser --> html_ids
141
141
  routes_source_browser --> models
142
142
  routes_source_browser --> services_source_utils
143
- routes_source_browser --> html_ids
144
- routes_tabs --> services_source
143
+ routes_source_browser --> routes_core
144
+ routes_source_browser --> components_preview_panel
145
+ routes_tabs --> models
145
146
  routes_tabs --> routes_core
146
- routes_tabs --> components_step_renderer
147
+ routes_tabs --> services_source
147
148
  routes_tabs --> services_source_utils
148
- routes_tabs --> models
149
+ routes_tabs --> components_step_renderer
149
150
  ```
150
151
 
151
152
  *52 cross-module dependencies detected*
@@ -524,12 +525,6 @@ def _handle_remove_external_source(
524
525
 
525
526
  ``` python
526
527
  def init_local_files_router(
527
- state_store: WorkflowStateStore, # The workflow state store
528
- workflow_id: str, # The workflow identifier
529
- source_service: SourceService, # The source service for external db ops
530
- prefix: str, # Route prefix (e.g., "/workflow/selection/local_files")
531
- urls: SelectionUrls, # URL bundle for rendering
532
- ) -> LocalFilesResult: # Router result with routers, routes, render, restore, and reset
533
528
  "Initialize local files browser routes with new file browser API."
534
529
  ```
535
530
 
@@ -614,6 +609,7 @@ class LocalFilesResult:
614
609
  render_panel: Callable # (error_message?, session_id?) -> rendered panel
615
610
  restore_state: Callable = field(...) # (session_id) -> None, restore persisted state
616
611
  reset_state: Callable = field(...) # () -> None, reset in-memory caches
612
+ kb_manager: Optional[ZoneManager] # ZoneManager backing the local file browser's keyboard system — hand to render_keyboard_hints_modal(..., child_managers=[...]) for hierarchical hint display
617
613
  ```
618
614
 
619
615
  ``` python
@@ -628,6 +624,7 @@ class SelectionResult:
628
624
  sb_state: Any # SourceBrowserRouterState
629
625
  restore_state: Callable = field(...) # (session_id) -> None, restore persisted state
630
626
  reset_state: Callable = field(...) # () -> None, reset in-memory caches
627
+ fb_kb_manager: Optional[ZoneManager] # ZoneManager from the local file browser; convenience pointer mirroring LocalFilesResult.kb_manager so step renderers can read it directly from SelectionResult without descending into LocalFilesResult
631
628
  ```
632
629
 
633
630
  ### preview_panel (`preview_panel.ipynb`)
@@ -1066,7 +1063,8 @@ from cjm_transcript_source_select.components.source_browser import (
1066
1063
  build_source_items,
1067
1064
  is_source_item_skippable,
1068
1065
  create_source_cell_renderer,
1069
- render_source_empty
1066
+ render_source_empty,
1067
+ build_source_browser_keyboard_system
1070
1068
  )
1071
1069
  ```
1072
1070
 
@@ -1127,19 +1125,45 @@ def render_source_empty() -> Any: # Empty state component
1127
1125
  "Render empty state when no transcription sources are available."
1128
1126
  ```
1129
1127
 
1128
+ ``` python
1129
+ def build_source_browser_keyboard_system(
1130
+ sb_state: Any, # SourceBrowserRouterState (carries vc_ids, vc_btn_ids, vc_urls)
1131
+ manager_label: Optional[str] = None, # Human-readable label for the ZoneManager (consumed by render_keyboard_hints_modal section header when this is a child_managers entry)
1132
+ ) -> KeyboardSystem: # Complete rendered KeyboardSystem (carries .manager for hierarchical hints handoff)
1133
+ """
1134
+ Build the source browser's KeyboardSystem standalone.
1135
+
1136
+ Lifts the manager construction out of `_render_source_browser_vc_content`
1137
+ so the underlying `ZoneManager` is accessible via `KeyboardSystem.manager`
1138
+ for hierarchical hints handoff. Consumers wiring this as a child of the
1139
+ parent selection-step manager pass the manager to
1140
+ `render_keyboard_hints_modal(..., child_managers=[...])` so the modal's
1141
+ section header reads as `manager_label` instead of the technical SB_SYSTEM_ID.
1142
+ """
1143
+ ```
1144
+
1130
1145
  ``` python
1131
1146
  def _render_source_browser_vc_content(
1132
- sb_state: Any, # SourceBrowserRouterState from routes.source_browser
1147
+ sb_state: Any, # SourceBrowserRouterState from routes.source_browser
1148
+ keyboard_system: Optional[KeyboardSystem] = None, # Pre-built keyboard system (when None, builds internally via build_source_browser_keyboard_system)
1133
1149
  ) -> Any: # VC content wrapper (without search/grouping header)
1134
- "Render the VC content portion of the source browser."
1150
+ """
1151
+ Render the VC content portion of the source browser.
1152
+
1153
+ When `keyboard_system` is provided, the caller has built the system upstream
1154
+ (typically `init_source_browser_router`) and owns the underlying ZoneManager.
1155
+ When None, builds the system internally — preserves pre-C1 behavior for any
1156
+ caller that doesn't need the manager handoff.
1157
+ """
1135
1158
  ```
1136
1159
 
1137
1160
  ``` python
1138
1161
  def _render_source_browser_vc(
1139
- sb_state: Any, # SourceBrowserRouterState from routes.source_browser
1140
- filter_url: str = "", # URL for filtering sources
1141
- grouping_mode: str = "media_path", # Current grouping mode
1142
- grouping_change_url: str = "", # URL for changing grouping mode
1162
+ sb_state: Any, # SourceBrowserRouterState from routes.source_browser
1163
+ filter_url: str = "", # URL for filtering sources
1164
+ grouping_mode: str = "media_path", # Current grouping mode
1165
+ grouping_change_url: str = "", # URL for changing grouping mode
1166
+ keyboard_system: Optional[KeyboardSystem] = None, # Pre-built keyboard system, threaded through to VC content
1143
1167
  ) -> Any: # Source browser component with virtual collection
1144
1168
  "Render the full source browser panel (header + VC content)."
1145
1169
  ```
@@ -1186,10 +1210,6 @@ from cjm_transcript_source_select.routes.source_browser import (
1186
1210
 
1187
1211
  ``` python
1188
1212
  def init_source_browser_router(
1189
- source_service: SourceService, # Source service for querying transcriptions
1190
- urls: SelectionUrls, # URL bundle (toggle, select_all, filter, grouping_change)
1191
- prefix: str = "/browser", # Route prefix for VC routes
1192
- ) -> SourceBrowserRouterState: # Router state with all VC objects and helpers
1193
1213
  "Initialize the source browser virtual collection router."
1194
1214
  ```
1195
1215
 
@@ -1214,6 +1234,7 @@ class SourceBrowserRouterState:
1214
1234
  get_visible_checkbox_oobs: Callable # () -> tuple of OOB elements
1215
1235
  get_checkbox_oob_for: Callable # (record_id, provider_id) -> OOB element or None
1216
1236
  get_vc_row_id_for: Callable # (record_id, provider_id) -> str or None
1237
+ kb_manager: Optional[ZoneManager] # ZoneManager backing the source browser's VC keyboard system — hand to render_keyboard_hints_modal(..., child_managers=[...]) for hierarchical hint display
1217
1238
  ```
1218
1239
 
1219
1240
  ### source_utils (`source_utils.ipynb`)
@@ -1380,7 +1401,22 @@ from cjm_transcript_source_select.components.step_renderer import (
1380
1401
 
1381
1402
  ``` python
1382
1403
  def _create_parent_keyboard_manager() -> ZoneManager: # Parent keyboard manager for hierarchy
1383
- "Create the parent keyboard manager with two ghost zones for column switching."
1404
+ """
1405
+ Create the parent keyboard manager with two ghost zones for column switching.
1406
+
1407
+ Each ghost zone declares `activate_child_callback` so the library-baked
1408
+ Enter/Space dispatch (`ZoneManager.activate_keys` defaults to `("Enter", " ")`)
1409
+ invokes the consumer-defined JS function — `window.activateBrowserChild`
1410
+ and `window.activateQueueChild` are wired in `_generate_hierarchy_js`. The
1411
+ callback path is used (vs. the declarative `activate_child_id` path) because
1412
+ the browser ghost zone's active child depends on the currently-selected
1413
+ tab (`db` → source-browser child; `files` → file-browser child); this
1414
+ dynamic resolution can't be a static declarative mapping.
1415
+
1416
+ Each ghost zone's `label` surfaces as the section header for that zone's
1417
+ derived rows in the hints modal (e.g., the parent's "Source Browser" /
1418
+ "Selection Queue" — Activate row appears under those labels).
1419
+ """
1384
1420
  ```
1385
1421
 
1386
1422
  ``` python
@@ -1427,15 +1463,6 @@ def _generate_hierarchy_js(
1427
1463
 
1428
1464
  ``` python
1429
1465
  def render_selection_step(
1430
- sources: List[Dict[str, Any]], # Available source plugins
1431
- transcriptions: List[Dict[str, Any]], # Available transcription records
1432
- selected_sources: List[Dict[str, str]], # Ordered selection
1433
- grouping_mode: str, # Grouping mode: "media_path" or "batch_id"
1434
- active_tab: str, # Active tab: "db" or "files"
1435
- urls: SelectionUrls, # URL bundle for selection routes
1436
- render_local_files_panel: Optional[Callable] = None, # Render fn for Files tab content
1437
- sb_state: Any = None, # SourceBrowserRouterState for DB tab VC rendering
1438
- ) -> Any: # FastHTML component
1439
1466
  "Render Phase 1: Source Selection & Ordering step with two-column layout."
1440
1467
  ```
1441
1468
 
@@ -60,57 +60,57 @@ graph LR
60
60
  utils[utils<br/>utils]
61
61
 
62
62
  components_helpers --> models
63
- components_local_files --> components_helpers
64
63
  components_local_files --> html_ids
64
+ components_local_files --> components_helpers
65
65
  components_preview_panel --> html_ids
66
66
  components_source_browser --> services_source_utils
67
67
  components_source_browser --> html_ids
68
68
  components_source_browser --> utils
69
- components_step_renderer --> components_preview_panel
70
- components_step_renderer --> utils
71
69
  components_step_renderer --> components_selection_queue
72
- components_step_renderer --> components_source_browser
73
- components_step_renderer --> models
74
70
  components_step_renderer --> html_ids
71
+ components_step_renderer --> models
75
72
  components_step_renderer --> components_local_files
76
- routes_core --> components_step_renderer
73
+ components_step_renderer --> components_source_browser
74
+ components_step_renderer --> components_preview_panel
75
+ components_step_renderer --> utils
77
76
  routes_core --> models
78
- routes_core --> components_selection_queue
79
- routes_core --> services_source
80
77
  routes_core --> html_ids
81
- routes_filtering --> routes_core
78
+ routes_core --> components_step_renderer
79
+ routes_core --> services_source
80
+ routes_core --> components_selection_queue
82
81
  routes_filtering --> services_source_utils
83
82
  routes_filtering --> services_source
83
+ routes_filtering --> routes_core
84
84
  routes_filtering --> models
85
- routes_init --> routes_local_files
86
- routes_init --> routes_source_browser
87
85
  routes_init --> routes_core
86
+ routes_init --> services_source
87
+ routes_init --> routes_local_files
88
88
  routes_init --> routes_queue
89
89
  routes_init --> routes_tabs
90
- routes_init --> services_source
91
- routes_init --> routes_filtering
92
90
  routes_init --> models
93
- routes_local_files --> routes_core
91
+ routes_init --> routes_filtering
92
+ routes_init --> routes_source_browser
94
93
  routes_local_files --> components_local_files
95
94
  routes_local_files --> services_source
95
+ routes_local_files --> routes_core
96
96
  routes_local_files --> models
97
- routes_queue --> components_preview_panel
98
- routes_queue --> routes_core
99
- routes_queue --> services_source_utils
100
97
  routes_queue --> services_source
98
+ routes_queue --> services_source_utils
99
+ routes_queue --> routes_core
101
100
  routes_queue --> models
102
- routes_source_browser --> components_preview_panel
103
- routes_source_browser --> components_source_browser
101
+ routes_queue --> components_preview_panel
104
102
  routes_source_browser --> services_source
105
- routes_source_browser --> routes_core
103
+ routes_source_browser --> components_source_browser
104
+ routes_source_browser --> html_ids
106
105
  routes_source_browser --> models
107
106
  routes_source_browser --> services_source_utils
108
- routes_source_browser --> html_ids
109
- routes_tabs --> services_source
107
+ routes_source_browser --> routes_core
108
+ routes_source_browser --> components_preview_panel
109
+ routes_tabs --> models
110
110
  routes_tabs --> routes_core
111
- routes_tabs --> components_step_renderer
111
+ routes_tabs --> services_source
112
112
  routes_tabs --> services_source_utils
113
- routes_tabs --> models
113
+ routes_tabs --> components_step_renderer
114
114
  ```
115
115
 
116
116
  *52 cross-module dependencies detected*
@@ -489,12 +489,6 @@ def _handle_remove_external_source(
489
489
 
490
490
  ``` python
491
491
  def init_local_files_router(
492
- state_store: WorkflowStateStore, # The workflow state store
493
- workflow_id: str, # The workflow identifier
494
- source_service: SourceService, # The source service for external db ops
495
- prefix: str, # Route prefix (e.g., "/workflow/selection/local_files")
496
- urls: SelectionUrls, # URL bundle for rendering
497
- ) -> LocalFilesResult: # Router result with routers, routes, render, restore, and reset
498
492
  "Initialize local files browser routes with new file browser API."
499
493
  ```
500
494
 
@@ -579,6 +573,7 @@ class LocalFilesResult:
579
573
  render_panel: Callable # (error_message?, session_id?) -> rendered panel
580
574
  restore_state: Callable = field(...) # (session_id) -> None, restore persisted state
581
575
  reset_state: Callable = field(...) # () -> None, reset in-memory caches
576
+ kb_manager: Optional[ZoneManager] # ZoneManager backing the local file browser's keyboard system — hand to render_keyboard_hints_modal(..., child_managers=[...]) for hierarchical hint display
582
577
  ```
583
578
 
584
579
  ``` python
@@ -593,6 +588,7 @@ class SelectionResult:
593
588
  sb_state: Any # SourceBrowserRouterState
594
589
  restore_state: Callable = field(...) # (session_id) -> None, restore persisted state
595
590
  reset_state: Callable = field(...) # () -> None, reset in-memory caches
591
+ fb_kb_manager: Optional[ZoneManager] # ZoneManager from the local file browser; convenience pointer mirroring LocalFilesResult.kb_manager so step renderers can read it directly from SelectionResult without descending into LocalFilesResult
596
592
  ```
597
593
 
598
594
  ### preview_panel (`preview_panel.ipynb`)
@@ -1031,7 +1027,8 @@ from cjm_transcript_source_select.components.source_browser import (
1031
1027
  build_source_items,
1032
1028
  is_source_item_skippable,
1033
1029
  create_source_cell_renderer,
1034
- render_source_empty
1030
+ render_source_empty,
1031
+ build_source_browser_keyboard_system
1035
1032
  )
1036
1033
  ```
1037
1034
 
@@ -1092,19 +1089,45 @@ def render_source_empty() -> Any: # Empty state component
1092
1089
  "Render empty state when no transcription sources are available."
1093
1090
  ```
1094
1091
 
1092
+ ``` python
1093
+ def build_source_browser_keyboard_system(
1094
+ sb_state: Any, # SourceBrowserRouterState (carries vc_ids, vc_btn_ids, vc_urls)
1095
+ manager_label: Optional[str] = None, # Human-readable label for the ZoneManager (consumed by render_keyboard_hints_modal section header when this is a child_managers entry)
1096
+ ) -> KeyboardSystem: # Complete rendered KeyboardSystem (carries .manager for hierarchical hints handoff)
1097
+ """
1098
+ Build the source browser's KeyboardSystem standalone.
1099
+
1100
+ Lifts the manager construction out of `_render_source_browser_vc_content`
1101
+ so the underlying `ZoneManager` is accessible via `KeyboardSystem.manager`
1102
+ for hierarchical hints handoff. Consumers wiring this as a child of the
1103
+ parent selection-step manager pass the manager to
1104
+ `render_keyboard_hints_modal(..., child_managers=[...])` so the modal's
1105
+ section header reads as `manager_label` instead of the technical SB_SYSTEM_ID.
1106
+ """
1107
+ ```
1108
+
1095
1109
  ``` python
1096
1110
  def _render_source_browser_vc_content(
1097
- sb_state: Any, # SourceBrowserRouterState from routes.source_browser
1111
+ sb_state: Any, # SourceBrowserRouterState from routes.source_browser
1112
+ keyboard_system: Optional[KeyboardSystem] = None, # Pre-built keyboard system (when None, builds internally via build_source_browser_keyboard_system)
1098
1113
  ) -> Any: # VC content wrapper (without search/grouping header)
1099
- "Render the VC content portion of the source browser."
1114
+ """
1115
+ Render the VC content portion of the source browser.
1116
+
1117
+ When `keyboard_system` is provided, the caller has built the system upstream
1118
+ (typically `init_source_browser_router`) and owns the underlying ZoneManager.
1119
+ When None, builds the system internally — preserves pre-C1 behavior for any
1120
+ caller that doesn't need the manager handoff.
1121
+ """
1100
1122
  ```
1101
1123
 
1102
1124
  ``` python
1103
1125
  def _render_source_browser_vc(
1104
- sb_state: Any, # SourceBrowserRouterState from routes.source_browser
1105
- filter_url: str = "", # URL for filtering sources
1106
- grouping_mode: str = "media_path", # Current grouping mode
1107
- grouping_change_url: str = "", # URL for changing grouping mode
1126
+ sb_state: Any, # SourceBrowserRouterState from routes.source_browser
1127
+ filter_url: str = "", # URL for filtering sources
1128
+ grouping_mode: str = "media_path", # Current grouping mode
1129
+ grouping_change_url: str = "", # URL for changing grouping mode
1130
+ keyboard_system: Optional[KeyboardSystem] = None, # Pre-built keyboard system, threaded through to VC content
1108
1131
  ) -> Any: # Source browser component with virtual collection
1109
1132
  "Render the full source browser panel (header + VC content)."
1110
1133
  ```
@@ -1151,10 +1174,6 @@ from cjm_transcript_source_select.routes.source_browser import (
1151
1174
 
1152
1175
  ``` python
1153
1176
  def init_source_browser_router(
1154
- source_service: SourceService, # Source service for querying transcriptions
1155
- urls: SelectionUrls, # URL bundle (toggle, select_all, filter, grouping_change)
1156
- prefix: str = "/browser", # Route prefix for VC routes
1157
- ) -> SourceBrowserRouterState: # Router state with all VC objects and helpers
1158
1177
  "Initialize the source browser virtual collection router."
1159
1178
  ```
1160
1179
 
@@ -1179,6 +1198,7 @@ class SourceBrowserRouterState:
1179
1198
  get_visible_checkbox_oobs: Callable # () -> tuple of OOB elements
1180
1199
  get_checkbox_oob_for: Callable # (record_id, provider_id) -> OOB element or None
1181
1200
  get_vc_row_id_for: Callable # (record_id, provider_id) -> str or None
1201
+ kb_manager: Optional[ZoneManager] # ZoneManager backing the source browser's VC keyboard system — hand to render_keyboard_hints_modal(..., child_managers=[...]) for hierarchical hint display
1182
1202
  ```
1183
1203
 
1184
1204
  ### source_utils (`source_utils.ipynb`)
@@ -1345,7 +1365,22 @@ from cjm_transcript_source_select.components.step_renderer import (
1345
1365
 
1346
1366
  ``` python
1347
1367
  def _create_parent_keyboard_manager() -> ZoneManager: # Parent keyboard manager for hierarchy
1348
- "Create the parent keyboard manager with two ghost zones for column switching."
1368
+ """
1369
+ Create the parent keyboard manager with two ghost zones for column switching.
1370
+
1371
+ Each ghost zone declares `activate_child_callback` so the library-baked
1372
+ Enter/Space dispatch (`ZoneManager.activate_keys` defaults to `("Enter", " ")`)
1373
+ invokes the consumer-defined JS function — `window.activateBrowserChild`
1374
+ and `window.activateQueueChild` are wired in `_generate_hierarchy_js`. The
1375
+ callback path is used (vs. the declarative `activate_child_id` path) because
1376
+ the browser ghost zone's active child depends on the currently-selected
1377
+ tab (`db` → source-browser child; `files` → file-browser child); this
1378
+ dynamic resolution can't be a static declarative mapping.
1379
+
1380
+ Each ghost zone's `label` surfaces as the section header for that zone's
1381
+ derived rows in the hints modal (e.g., the parent's "Source Browser" /
1382
+ "Selection Queue" — Activate row appears under those labels).
1383
+ """
1349
1384
  ```
1350
1385
 
1351
1386
  ``` python
@@ -1392,15 +1427,6 @@ def _generate_hierarchy_js(
1392
1427
 
1393
1428
  ``` python
1394
1429
  def render_selection_step(
1395
- sources: List[Dict[str, Any]], # Available source plugins
1396
- transcriptions: List[Dict[str, Any]], # Available transcription records
1397
- selected_sources: List[Dict[str, str]], # Ordered selection
1398
- grouping_mode: str, # Grouping mode: "media_path" or "batch_id"
1399
- active_tab: str, # Active tab: "db" or "files"
1400
- urls: SelectionUrls, # URL bundle for selection routes
1401
- render_local_files_panel: Optional[Callable] = None, # Render fn for Files tab content
1402
- sb_state: Any = None, # SourceBrowserRouterState for DB tab VC rendering
1403
- ) -> Any: # FastHTML component
1404
1430
  "Render Phase 1: Source Selection & Ordering step with two-column layout."
1405
1431
  ```
1406
1432
 
@@ -0,0 +1 @@
1
+ __version__ = "0.0.30"
@@ -45,6 +45,8 @@ d = { 'settings': { 'branch': 'main',
45
45
  'cjm_transcript_source_select/components/source_browser.py'),
46
46
  'cjm_transcript_source_select.components.source_browser._render_source_browser_vc_content': ( 'components/source_browser.html#_render_source_browser_vc_content',
47
47
  'cjm_transcript_source_select/components/source_browser.py'),
48
+ 'cjm_transcript_source_select.components.source_browser.build_source_browser_keyboard_system': ( 'components/source_browser.html#build_source_browser_keyboard_system',
49
+ 'cjm_transcript_source_select/components/source_browser.py'),
48
50
  'cjm_transcript_source_select.components.source_browser.build_source_items': ( 'components/source_browser.html#build_source_items',
49
51
  'cjm_transcript_source_select/components/source_browser.py'),
50
52
  'cjm_transcript_source_select.components.source_browser.create_source_cell_renderer': ( 'components/source_browser.html#create_source_cell_renderer',
@@ -4,7 +4,7 @@
4
4
 
5
5
  # %% auto #0
6
6
  __all__ = ['SOURCE_BROWSER_COLUMNS', 'SB_SYSTEM_ID', 'SourceBrowserItem', 'build_source_items', 'is_source_item_skippable',
7
- 'create_source_cell_renderer', 'render_source_empty']
7
+ 'create_source_cell_renderer', 'render_source_empty', 'build_source_browser_keyboard_system']
8
8
 
9
9
  # %% ../../nbs/components/source_browser.ipynb #7b2e89c5
10
10
  import json
@@ -53,7 +53,7 @@ from cjm_fasthtml_virtual_collection.js.scrollbar import generate_scrollbar_js
53
53
  from cjm_fasthtml_virtual_collection.js.auto_fit import generate_auto_fit_js, auto_fit_callback_name
54
54
 
55
55
  from cjm_fasthtml_keyboard_navigation.core.manager import ZoneManager
56
- from cjm_fasthtml_keyboard_navigation.components.system import render_keyboard_system
56
+ from cjm_fasthtml_keyboard_navigation.components.system import render_keyboard_system, KeyboardSystem
57
57
 
58
58
  from cjm_fasthtml_viewport_fit.models import ViewportFitConfig
59
59
  from cjm_fasthtml_viewport_fit.components import render_viewport_fit_script
@@ -306,10 +306,52 @@ _SB_VC_WRAPPER_ID = "sb-vc-wrapper"
306
306
  SB_SYSTEM_ID = "sb-collection"
307
307
 
308
308
 
309
+ def build_source_browser_keyboard_system(
310
+ sb_state: Any, # SourceBrowserRouterState (carries vc_ids, vc_btn_ids, vc_urls)
311
+ manager_label: Optional[str] = None, # Human-readable label for the ZoneManager (consumed by render_keyboard_hints_modal section header when this is a child_managers entry)
312
+ ) -> KeyboardSystem: # Complete rendered KeyboardSystem (carries .manager for hierarchical hints handoff)
313
+ """Build the source browser's KeyboardSystem standalone.
314
+
315
+ Lifts the manager construction out of `_render_source_browser_vc_content`
316
+ so the underlying `ZoneManager` is accessible via `KeyboardSystem.manager`
317
+ for hierarchical hints handoff. Consumers wiring this as a child of the
318
+ parent selection-step manager pass the manager to
319
+ `render_keyboard_hints_modal(..., child_managers=[...])` so the modal's
320
+ section header reads as `manager_label` instead of the technical SB_SYSTEM_ID.
321
+ """
322
+ vc_ids = sb_state.ids
323
+ vc_btn_ids = sb_state.btn_ids
324
+ vc_urls = sb_state.urls
325
+
326
+ zone = create_collection_focus_zone(vc_ids)
327
+ nav_actions = create_collection_nav_actions(zone.id, vc_btn_ids)
328
+ manager = ZoneManager(
329
+ zones=(zone,),
330
+ actions=nav_actions,
331
+ system_id=SB_SYSTEM_ID,
332
+ label=manager_label,
333
+ )
334
+ url_map = build_collection_url_map(vc_btn_ids, vc_urls)
335
+ target_map = {btn_id: f"#{vc_ids.rows}" for btn_id in url_map}
336
+ swap_map = {btn_id: "none" for btn_id in url_map}
337
+ kb_system = render_keyboard_system(
338
+ manager, url_map=url_map, target_map=target_map, swap_map=swap_map,
339
+ )
340
+ apply_nav_sync(kb_system, vc_ids)
341
+ return kb_system
342
+
343
+
309
344
  def _render_source_browser_vc_content(
310
- sb_state: Any, # SourceBrowserRouterState from routes.source_browser
345
+ sb_state: Any, # SourceBrowserRouterState from routes.source_browser
346
+ keyboard_system: Optional[KeyboardSystem] = None, # Pre-built keyboard system (when None, builds internally via build_source_browser_keyboard_system)
311
347
  ) -> Any: # VC content wrapper (without search/grouping header)
312
- """Render the VC content portion of the source browser."""
348
+ """Render the VC content portion of the source browser.
349
+
350
+ When `keyboard_system` is provided, the caller has built the system upstream
351
+ (typically `init_source_browser_router`) and owns the underlying ZoneManager.
352
+ When None, builds the system internally — preserves pre-C1 behavior for any
353
+ caller that doesn't need the manager handoff.
354
+ """
313
355
  vc_ids = sb_state.ids
314
356
  vc_btn_ids = sb_state.btn_ids
315
357
  vc_config = sb_state.config
@@ -323,15 +365,8 @@ def _render_source_browser_vc_content(
323
365
  render_empty=render_source_empty,
324
366
  )
325
367
 
326
- # Keyboard system for VC navigation
327
- zone = create_collection_focus_zone(vc_ids)
328
- nav_actions = create_collection_nav_actions(zone.id, vc_btn_ids)
329
- manager = ZoneManager(zones=(zone,), actions=nav_actions, system_id=SB_SYSTEM_ID)
330
- url_map = build_collection_url_map(vc_btn_ids, vc_urls)
331
- target_map = {btn_id: f"#{vc_ids.rows}" for btn_id in url_map}
332
- swap_map = {btn_id: "none" for btn_id in url_map}
333
- kb_system = render_keyboard_system(manager, url_map=url_map, target_map=target_map, swap_map=swap_map)
334
- apply_nav_sync(kb_system, vc_ids)
368
+ # Keyboard system use caller-supplied or build internally.
369
+ kb_system = keyboard_system if keyboard_system is not None else build_source_browser_keyboard_system(sb_state)
335
370
 
336
371
  # Auto-fit JS
337
372
  auto_fit_js = generate_auto_fit_js(
@@ -372,13 +407,14 @@ def _render_source_browser_vc_content(
372
407
 
373
408
 
374
409
  def _render_source_browser_vc(
375
- sb_state: Any, # SourceBrowserRouterState from routes.source_browser
376
- filter_url: str = "", # URL for filtering sources
377
- grouping_mode: str = "media_path", # Current grouping mode
378
- grouping_change_url: str = "", # URL for changing grouping mode
410
+ sb_state: Any, # SourceBrowserRouterState from routes.source_browser
411
+ filter_url: str = "", # URL for filtering sources
412
+ grouping_mode: str = "media_path", # Current grouping mode
413
+ grouping_change_url: str = "", # URL for changing grouping mode
414
+ keyboard_system: Optional[KeyboardSystem] = None, # Pre-built keyboard system, threaded through to VC content
379
415
  ) -> Any: # Source browser component with virtual collection
380
416
  """Render the full source browser panel (header + VC content)."""
381
- vc_content = _render_source_browser_vc_content(sb_state)
417
+ vc_content = _render_source_browser_vc_content(sb_state, keyboard_system=keyboard_system)
382
418
 
383
419
  return Div(
384
420
  # Header bar with search and grouping selector
@@ -87,43 +87,47 @@ _ZONE_FOCUS_CLASSES = (str(shadow.lg), str(shadow_dui.primary))
87
87
 
88
88
 
89
89
  def _create_parent_keyboard_manager() -> ZoneManager: # Parent keyboard manager for hierarchy
90
- """Create the parent keyboard manager with two ghost zones for column switching."""
90
+ """Create the parent keyboard manager with two ghost zones for column switching.
91
+
92
+ Each ghost zone declares `activate_child_callback` so the library-baked
93
+ Enter/Space dispatch (`ZoneManager.activate_keys` defaults to `("Enter", " ")`)
94
+ invokes the consumer-defined JS function — `window.activateBrowserChild`
95
+ and `window.activateQueueChild` are wired in `_generate_hierarchy_js`. The
96
+ callback path is used (vs. the declarative `activate_child_id` path) because
97
+ the browser ghost zone's active child depends on the currently-selected
98
+ tab (`db` → source-browser child; `files` → file-browser child); this
99
+ dynamic resolution can't be a static declarative mapping.
100
+
101
+ Each ghost zone's `label` surfaces as the section header for that zone's
102
+ derived rows in the hints modal (e.g., the parent's "Source Browser" /
103
+ "Selection Queue" — Activate row appears under those labels).
104
+ """
91
105
  # Ghost zone for browser area (wraps source tabs — child systems live here)
92
106
  ghost_browser_zone = FocusZone(
93
107
  id=SelectionHtmlIds.GHOST_BROWSER,
108
+ label="Source Browser",
94
109
  item_selector=None,
95
110
  navigation=ScrollOnly(),
96
111
  zone_focus_classes=_ZONE_FOCUS_CLASSES,
112
+ activate_child_callback="activateBrowserChild",
97
113
  )
98
114
 
99
115
  # Ghost zone for queue area (wraps queue panel — queue child system lives here)
100
116
  ghost_queue_zone = FocusZone(
101
117
  id=SelectionHtmlIds.GHOST_QUEUE,
118
+ label="Selection Queue",
102
119
  item_selector=None,
103
120
  navigation=ScrollOnly(),
104
121
  zone_focus_classes=_ZONE_FOCUS_CLASSES,
122
+ activate_child_callback="activateQueueChild",
105
123
  )
106
124
 
107
- # Parent-level actions (no queue-scoped actions — those live in queue child system)
125
+ # Parent-level actions (no queue-scoped actions — those live in queue child system).
126
+ # The previous per-zone Enter KeyActions for child activation have been removed —
127
+ # library-baked Enter/Space dispatch via FocusZone.activate_child_callback handles
128
+ # activation. Tab switching stays here because Ctrl+Shift+{/} doesn't fit the
129
+ # activation seam (different keys, different dispatch path).
108
130
  actions = (
109
- # Enter activates the browser child when ghost-browser zone is active
110
- KeyAction(
111
- key="Enter",
112
- js_callback="activateBrowserChild",
113
- zone_ids=(SelectionHtmlIds.GHOST_BROWSER,),
114
- description="Activate browser",
115
- hint_group="Navigation",
116
- ),
117
-
118
- # Enter activates the queue child when ghost-queue zone is active
119
- KeyAction(
120
- key="Enter",
121
- js_callback="activateQueueChild",
122
- zone_ids=(SelectionHtmlIds.GHOST_QUEUE,),
123
- description="Activate queue",
124
- hint_group="Navigation",
125
- ),
126
-
127
131
  # Tab switching — no zone_ids so it fires from any parent zone
128
132
  # and bubbles up from children (children don't handle Ctrl+Shift+{/})
129
133
  KeyAction(
@@ -146,6 +150,7 @@ def _create_parent_keyboard_manager() -> ZoneManager: # Parent keyboard manager
146
150
  zones=(ghost_browser_zone, ghost_queue_zone),
147
151
  actions=actions,
148
152
  system_id=SelectionHtmlIds.PARENT_SYSTEM_ID,
153
+ label="Source Selection",
149
154
  prev_zone_key="ArrowLeft",
150
155
  next_zone_key="ArrowRight",
151
156
  state_hidden_inputs=True,
@@ -377,6 +382,7 @@ def render_selection_step(
377
382
  urls: SelectionUrls, # URL bundle for selection routes
378
383
  render_local_files_panel: Optional[Callable] = None, # Render fn for Files tab content
379
384
  sb_state: Any = None, # SourceBrowserRouterState for DB tab VC rendering
385
+ fb_kb_manager: Optional[Any] = None, # File browser ZoneManager (from LocalFilesResult.kb_manager / SelectionResult.fb_kb_manager) for the hints modal's hierarchical child_managers
380
386
  ) -> Any: # FastHTML component
381
387
  """Render Phase 1: Source Selection & Ordering step with two-column layout."""
382
388
  # Create parent keyboard manager (two ghost zones for column switching)
@@ -407,6 +413,8 @@ def render_selection_step(
407
413
 
408
414
  # Build queue keyboard system (self-contained child system from sortable-queue library)
409
415
  # data_attributes includes "key" for remove route + "record-id"/"provider-id" for preview
416
+ # manager_label surfaces as the section header for the queue's kb_manager in
417
+ # the hints modal's hierarchical child_managers section.
410
418
  queue_kb = create_queue_keyboard_system(
411
419
  config=SD_QUEUE_CONFIG,
412
420
  ids=SD_QUEUE_IDS,
@@ -414,6 +422,7 @@ def render_selection_step(
414
422
  data_attributes=("key", "record-id", "provider-id"),
415
423
  on_focus_change="triggerPreviewUpdate",
416
424
  hidden_input_prefix="sd-focused",
425
+ manager_label="Selection Queue",
417
426
  )
418
427
 
419
428
  # Build include selector for preview hidden inputs
@@ -472,8 +481,23 @@ def render_selection_step(
472
481
  # Hierarchy wiring script (must come after all keyboard systems are rendered)
473
482
  hierarchy_js = _generate_hierarchy_js(active_tab)
474
483
 
475
- # Keyboard hints modal
476
- hints_modal, hints_trigger, hints_script = render_keyboard_hints_modal(kb_manager)
484
+ # Keyboard hints modal — hierarchical mode with all three children.
485
+ # Pass all three regardless of active_tab (modal is a reference, not a
486
+ # "what's active right now" indicator — user benefits from seeing all
487
+ # available actions across DB / Files / Queue). None entries are filtered
488
+ # so the modal handles missing managers gracefully (e.g. before host
489
+ # wiring is complete during tests).
490
+ child_managers = tuple(
491
+ m for m in (
492
+ sb_state.kb_manager if sb_state is not None else None,
493
+ fb_kb_manager,
494
+ queue_kb.manager,
495
+ ) if m is not None
496
+ )
497
+ hints_modal, hints_trigger, hints_script = render_keyboard_hints_modal(
498
+ kb_manager,
499
+ child_managers=child_managers if child_managers else None,
500
+ )
477
501
 
478
502
  return Div(
479
503
  # Header with keyboard hints trigger
@@ -12,6 +12,8 @@ from dataclasses import dataclass, field
12
12
 
13
13
  from fasthtml.common import APIRouter
14
14
 
15
+ from cjm_fasthtml_keyboard_navigation.core.manager import ZoneManager
16
+
15
17
  from cjm_source_provider.models import SelectedSource
16
18
 
17
19
  # %% ../nbs/models.ipynb #selection-step-state
@@ -71,6 +73,7 @@ class LocalFilesResult:
71
73
  render_panel: Callable # (error_message?, session_id?) -> rendered panel
72
74
  restore_state: Callable = field(default=_no_op_restore) # (session_id) -> None, restore persisted state
73
75
  reset_state: Callable = field(default=_no_op_reset) # () -> None, reset in-memory caches
76
+ kb_manager: Optional[ZoneManager] = None # ZoneManager backing the local file browser's keyboard system — hand to render_keyboard_hints_modal(..., child_managers=[...]) for hierarchical hint display
74
77
 
75
78
  @dataclass
76
79
  class SelectionResult:
@@ -82,3 +85,4 @@ class SelectionResult:
82
85
  sb_state: Any = None # SourceBrowserRouterState
83
86
  restore_state: Callable = field(default=_no_op_restore) # (session_id) -> None, restore persisted state
84
87
  reset_state: Callable = field(default=_no_op_reset) # () -> None, reset in-memory caches
88
+ fb_kb_manager: Optional[ZoneManager] = None # ZoneManager from the local file browser; convenience pointer mirroring LocalFilesResult.kb_manager so step renderers can read it directly from SelectionResult without descending into LocalFilesResult
@@ -42,13 +42,20 @@ def init_selection_routers(
42
42
  filtering_router, filtering_routes = init_filtering_router(
43
43
  state_store, workflow_id, source_service, f"{prefix}/filtering", urls
44
44
  )
45
+ # manager_label="Local Files" surfaces as the section header for the file
46
+ # browser's kb_manager in the Phase 1 hints modal (rendered via
47
+ # render_keyboard_hints_modal in step_renderer).
45
48
  local_files = init_local_files_router(
46
- state_store, workflow_id, source_service, f"{prefix}/local_files", urls
49
+ state_store, workflow_id, source_service, f"{prefix}/local_files", urls,
50
+ manager_label="Local Files",
47
51
  )
52
+ # manager_label="Source Browser" surfaces as the section header for the
53
+ # source browser's VC kb_manager in the Phase 1 hints modal.
48
54
  sb_state = init_source_browser_router(
49
55
  source_service=source_service,
50
56
  urls=urls,
51
57
  prefix=f"{prefix}/browser",
58
+ manager_label="Source Browser",
52
59
  )
53
60
 
54
61
  # Wire the module-level refs
@@ -128,4 +135,5 @@ def init_selection_routers(
128
135
  sb_state=sb_state,
129
136
  restore_state=local_files.restore_state,
130
137
  reset_state=local_files.reset_state,
138
+ fb_kb_manager=local_files.kb_manager,
131
139
  )
@@ -103,6 +103,7 @@ def init_local_files_router(
103
103
  source_service: SourceService, # The source service for external db ops
104
104
  prefix: str, # Route prefix (e.g., "/workflow/selection/local_files")
105
105
  urls: SelectionUrls, # URL bundle for rendering
106
+ manager_label: Optional[str] = None, # Human-readable label for the local file browser's ZoneManager — surfaces in the hints modal section header when LocalFilesResult.kb_manager is passed as a child_managers entry
106
107
  ) -> LocalFilesResult: # Router result with routers, routes, render, restore, and reset
107
108
  """Initialize local files browser routes with new file browser API."""
108
109
  provider = _get_local_files_provider()
@@ -253,6 +254,7 @@ def init_local_files_router(
253
254
  route_prefix=f"{prefix}/browser",
254
255
  callbacks=fb_callbacks,
255
256
  home_path=home_path,
257
+ manager_label=manager_label,
256
258
  )
257
259
 
258
260
  # --- Render function for the full local files panel ---
@@ -314,4 +316,5 @@ def init_local_files_router(
314
316
  render_panel=_render_panel,
315
317
  restore_state=_restore_state,
316
318
  reset_state=_reset_state,
319
+ kb_manager=fb_routers.kb_manager,
317
320
  )
@@ -20,6 +20,8 @@ from cjm_fasthtml_virtual_collection.core.windowing import find_nearest_focusabl
20
20
  from cjm_fasthtml_virtual_collection.routes.router import init_virtual_collection_router
21
21
  from cjm_fasthtml_virtual_collection.components.table import render_cell_oob
22
22
 
23
+ from cjm_fasthtml_keyboard_navigation.core.manager import ZoneManager
24
+
23
25
  from ..html_ids import SelectionHtmlIds
24
26
  from ..models import SelectionUrls
25
27
  from ..services.source import SourceService
@@ -29,6 +31,7 @@ from cjm_transcript_source_select.components.source_browser import (
29
31
  SourceBrowserItem, SOURCE_BROWSER_COLUMNS,
30
32
  build_source_items, is_source_item_skippable,
31
33
  render_source_empty, _render_header_cell, _render_record_cell,
34
+ build_source_browser_keyboard_system,
32
35
  )
33
36
  from ..components.preview_panel import _render_preview_panel
34
37
 
@@ -50,12 +53,14 @@ class SourceBrowserRouterState:
50
53
  get_visible_checkbox_oobs: Callable # () -> tuple of OOB elements
51
54
  get_checkbox_oob_for: Callable # (record_id, provider_id) -> OOB element or None
52
55
  get_vc_row_id_for: Callable # (record_id, provider_id) -> str or None
56
+ kb_manager: Optional[ZoneManager] = None # ZoneManager backing the source browser's VC keyboard system — hand to render_keyboard_hints_modal(..., child_managers=[...]) for hierarchical hint display
53
57
 
54
58
  # %% ../../nbs/routes/source_browser.ipynb #sb-init-router
55
59
  def init_source_browser_router(
56
60
  source_service: SourceService, # Source service for querying transcriptions
57
61
  urls: SelectionUrls, # URL bundle (toggle, select_all, filter, grouping_change)
58
62
  prefix: str = "/browser", # Route prefix for VC routes
63
+ manager_label: Optional[str] = None, # Human-readable label for the source browser's ZoneManager (used as the modal section header when wired into a hierarchical hints display via SourceBrowserRouterState.kb_manager)
59
64
  ) -> SourceBrowserRouterState: # Router state with all VC objects and helpers
60
65
  """Initialize the source browser virtual collection router."""
61
66
  # --- VC config and state (closure) ---
@@ -157,6 +162,12 @@ def init_source_browser_router(
157
162
  return None
158
163
 
159
164
  # --- Rebuild and render ---
165
+ # `_kb_system_ref` carries the kb_system after it's built at the bottom of
166
+ # this function (built once, reused across renders). rebuild_and_render
167
+ # threads it down to source_browser components so the same kb_system —
168
+ # and thus the same exposed kb_manager — is reused on every render.
169
+ _kb_system_ref: list = [None]
170
+
160
171
  def rebuild_and_render(
161
172
  transcriptions: List[Dict[str, Any]], # Transcription records
162
173
  selected_sources: List[Dict[str, str]], # Current selections
@@ -167,13 +178,17 @@ def init_source_browser_router(
167
178
  rebuild_items(transcriptions, selected_sources, grouping_mode)
168
179
  if content_only:
169
180
  from cjm_transcript_source_select.components.source_browser import _render_source_browser_vc_content
170
- return _render_source_browser_vc_content(sb_state=_sb_state_ref[0])
181
+ return _render_source_browser_vc_content(
182
+ sb_state=_sb_state_ref[0],
183
+ keyboard_system=_kb_system_ref[0],
184
+ )
171
185
  from cjm_transcript_source_select.components.source_browser import _render_source_browser_vc
172
186
  return _render_source_browser_vc(
173
187
  sb_state=_sb_state_ref[0],
174
188
  filter_url=urls.filter,
175
189
  grouping_mode=grouping_mode,
176
190
  grouping_change_url=urls.grouping_change,
191
+ keyboard_system=_kb_system_ref[0],
177
192
  )
178
193
 
179
194
  # --- VC callbacks ---
@@ -228,4 +243,16 @@ def init_source_browser_router(
228
243
  get_vc_row_id_for=get_vc_row_id_for,
229
244
  )
230
245
  _sb_state_ref[0] = result
246
+
247
+ # --- Build the kb_system once now that sb_state is wired up ---
248
+ # The kb_system's DOM elements (script, hidden_inputs, action_buttons) are
249
+ # static across renders; only the item list rebuilds. Building once at init
250
+ # ensures kb_manager is identity-stable across renders and gives single
251
+ # source of truth for the manager exposed on SourceBrowserRouterState.
252
+ kb_system = build_source_browser_keyboard_system(
253
+ sb_state=result,
254
+ manager_label=manager_label,
255
+ )
256
+ _kb_system_ref[0] = kb_system
257
+ result.kb_manager = kb_system.manager
231
258
  return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-transcript-source-select
3
- Version: 0.0.28
3
+ Version: 0.0.30
4
4
  Summary: FastHTML source selection component for transcript decomposition workflows, with federated database browsing, drag-drop ordering, and keyboard navigation.
5
5
  Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
6
6
  License: Apache-2.0
@@ -15,13 +15,14 @@ Classifier: Programming Language :: Python :: 3 :: Only
15
15
  Requires-Python: >=3.12
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
+ Requires-Dist: python-fasthtml==0.13.4
18
19
  Requires-Dist: cjm-plugin-system
19
20
  Requires-Dist: cjm-transcription-plugin-system
20
21
  Requires-Dist: cjm-fasthtml-app-core
21
22
  Requires-Dist: cjm-fasthtml-daisyui
22
23
  Requires-Dist: cjm_fasthtml_lucide_icons
23
- Requires-Dist: cjm_fasthtml_file_browser>=0.0.17
24
- Requires-Dist: cjm_fasthtml_keyboard_navigation>=0.0.20
24
+ Requires-Dist: cjm_fasthtml_file_browser>=0.0.19
25
+ Requires-Dist: cjm_fasthtml_keyboard_navigation>=0.0.23
25
26
  Requires-Dist: duckdb
26
27
  Requires-Dist: pandas
27
28
  Requires-Dist: cjm_workflow_state
@@ -29,7 +30,7 @@ Requires-Dist: cjm_source_provider
29
30
  Requires-Dist: cjm_fasthtml_interactions
30
31
  Requires-Dist: cjm_fasthtml_viewport_fit
31
32
  Requires-Dist: cjm_fasthtml_virtual_collection
32
- Requires-Dist: cjm_fasthtml_sortable_queue>=0.0.13
33
+ Requires-Dist: cjm_fasthtml_sortable_queue>=0.0.16
33
34
  Requires-Dist: cjm_fasthtml_design_system>=0.0.9
34
35
  Dynamic: license-file
35
36
 
@@ -95,57 +96,57 @@ graph LR
95
96
  utils[utils<br/>utils]
96
97
 
97
98
  components_helpers --> models
98
- components_local_files --> components_helpers
99
99
  components_local_files --> html_ids
100
+ components_local_files --> components_helpers
100
101
  components_preview_panel --> html_ids
101
102
  components_source_browser --> services_source_utils
102
103
  components_source_browser --> html_ids
103
104
  components_source_browser --> utils
104
- components_step_renderer --> components_preview_panel
105
- components_step_renderer --> utils
106
105
  components_step_renderer --> components_selection_queue
107
- components_step_renderer --> components_source_browser
108
- components_step_renderer --> models
109
106
  components_step_renderer --> html_ids
107
+ components_step_renderer --> models
110
108
  components_step_renderer --> components_local_files
111
- routes_core --> components_step_renderer
109
+ components_step_renderer --> components_source_browser
110
+ components_step_renderer --> components_preview_panel
111
+ components_step_renderer --> utils
112
112
  routes_core --> models
113
- routes_core --> components_selection_queue
114
- routes_core --> services_source
115
113
  routes_core --> html_ids
116
- routes_filtering --> routes_core
114
+ routes_core --> components_step_renderer
115
+ routes_core --> services_source
116
+ routes_core --> components_selection_queue
117
117
  routes_filtering --> services_source_utils
118
118
  routes_filtering --> services_source
119
+ routes_filtering --> routes_core
119
120
  routes_filtering --> models
120
- routes_init --> routes_local_files
121
- routes_init --> routes_source_browser
122
121
  routes_init --> routes_core
122
+ routes_init --> services_source
123
+ routes_init --> routes_local_files
123
124
  routes_init --> routes_queue
124
125
  routes_init --> routes_tabs
125
- routes_init --> services_source
126
- routes_init --> routes_filtering
127
126
  routes_init --> models
128
- routes_local_files --> routes_core
127
+ routes_init --> routes_filtering
128
+ routes_init --> routes_source_browser
129
129
  routes_local_files --> components_local_files
130
130
  routes_local_files --> services_source
131
+ routes_local_files --> routes_core
131
132
  routes_local_files --> models
132
- routes_queue --> components_preview_panel
133
- routes_queue --> routes_core
134
- routes_queue --> services_source_utils
135
133
  routes_queue --> services_source
134
+ routes_queue --> services_source_utils
135
+ routes_queue --> routes_core
136
136
  routes_queue --> models
137
- routes_source_browser --> components_preview_panel
138
- routes_source_browser --> components_source_browser
137
+ routes_queue --> components_preview_panel
139
138
  routes_source_browser --> services_source
140
- routes_source_browser --> routes_core
139
+ routes_source_browser --> components_source_browser
140
+ routes_source_browser --> html_ids
141
141
  routes_source_browser --> models
142
142
  routes_source_browser --> services_source_utils
143
- routes_source_browser --> html_ids
144
- routes_tabs --> services_source
143
+ routes_source_browser --> routes_core
144
+ routes_source_browser --> components_preview_panel
145
+ routes_tabs --> models
145
146
  routes_tabs --> routes_core
146
- routes_tabs --> components_step_renderer
147
+ routes_tabs --> services_source
147
148
  routes_tabs --> services_source_utils
148
- routes_tabs --> models
149
+ routes_tabs --> components_step_renderer
149
150
  ```
150
151
 
151
152
  *52 cross-module dependencies detected*
@@ -524,12 +525,6 @@ def _handle_remove_external_source(
524
525
 
525
526
  ``` python
526
527
  def init_local_files_router(
527
- state_store: WorkflowStateStore, # The workflow state store
528
- workflow_id: str, # The workflow identifier
529
- source_service: SourceService, # The source service for external db ops
530
- prefix: str, # Route prefix (e.g., "/workflow/selection/local_files")
531
- urls: SelectionUrls, # URL bundle for rendering
532
- ) -> LocalFilesResult: # Router result with routers, routes, render, restore, and reset
533
528
  "Initialize local files browser routes with new file browser API."
534
529
  ```
535
530
 
@@ -614,6 +609,7 @@ class LocalFilesResult:
614
609
  render_panel: Callable # (error_message?, session_id?) -> rendered panel
615
610
  restore_state: Callable = field(...) # (session_id) -> None, restore persisted state
616
611
  reset_state: Callable = field(...) # () -> None, reset in-memory caches
612
+ kb_manager: Optional[ZoneManager] # ZoneManager backing the local file browser's keyboard system — hand to render_keyboard_hints_modal(..., child_managers=[...]) for hierarchical hint display
617
613
  ```
618
614
 
619
615
  ``` python
@@ -628,6 +624,7 @@ class SelectionResult:
628
624
  sb_state: Any # SourceBrowserRouterState
629
625
  restore_state: Callable = field(...) # (session_id) -> None, restore persisted state
630
626
  reset_state: Callable = field(...) # () -> None, reset in-memory caches
627
+ fb_kb_manager: Optional[ZoneManager] # ZoneManager from the local file browser; convenience pointer mirroring LocalFilesResult.kb_manager so step renderers can read it directly from SelectionResult without descending into LocalFilesResult
631
628
  ```
632
629
 
633
630
  ### preview_panel (`preview_panel.ipynb`)
@@ -1066,7 +1063,8 @@ from cjm_transcript_source_select.components.source_browser import (
1066
1063
  build_source_items,
1067
1064
  is_source_item_skippable,
1068
1065
  create_source_cell_renderer,
1069
- render_source_empty
1066
+ render_source_empty,
1067
+ build_source_browser_keyboard_system
1070
1068
  )
1071
1069
  ```
1072
1070
 
@@ -1127,19 +1125,45 @@ def render_source_empty() -> Any: # Empty state component
1127
1125
  "Render empty state when no transcription sources are available."
1128
1126
  ```
1129
1127
 
1128
+ ``` python
1129
+ def build_source_browser_keyboard_system(
1130
+ sb_state: Any, # SourceBrowserRouterState (carries vc_ids, vc_btn_ids, vc_urls)
1131
+ manager_label: Optional[str] = None, # Human-readable label for the ZoneManager (consumed by render_keyboard_hints_modal section header when this is a child_managers entry)
1132
+ ) -> KeyboardSystem: # Complete rendered KeyboardSystem (carries .manager for hierarchical hints handoff)
1133
+ """
1134
+ Build the source browser's KeyboardSystem standalone.
1135
+
1136
+ Lifts the manager construction out of `_render_source_browser_vc_content`
1137
+ so the underlying `ZoneManager` is accessible via `KeyboardSystem.manager`
1138
+ for hierarchical hints handoff. Consumers wiring this as a child of the
1139
+ parent selection-step manager pass the manager to
1140
+ `render_keyboard_hints_modal(..., child_managers=[...])` so the modal's
1141
+ section header reads as `manager_label` instead of the technical SB_SYSTEM_ID.
1142
+ """
1143
+ ```
1144
+
1130
1145
  ``` python
1131
1146
  def _render_source_browser_vc_content(
1132
- sb_state: Any, # SourceBrowserRouterState from routes.source_browser
1147
+ sb_state: Any, # SourceBrowserRouterState from routes.source_browser
1148
+ keyboard_system: Optional[KeyboardSystem] = None, # Pre-built keyboard system (when None, builds internally via build_source_browser_keyboard_system)
1133
1149
  ) -> Any: # VC content wrapper (without search/grouping header)
1134
- "Render the VC content portion of the source browser."
1150
+ """
1151
+ Render the VC content portion of the source browser.
1152
+
1153
+ When `keyboard_system` is provided, the caller has built the system upstream
1154
+ (typically `init_source_browser_router`) and owns the underlying ZoneManager.
1155
+ When None, builds the system internally — preserves pre-C1 behavior for any
1156
+ caller that doesn't need the manager handoff.
1157
+ """
1135
1158
  ```
1136
1159
 
1137
1160
  ``` python
1138
1161
  def _render_source_browser_vc(
1139
- sb_state: Any, # SourceBrowserRouterState from routes.source_browser
1140
- filter_url: str = "", # URL for filtering sources
1141
- grouping_mode: str = "media_path", # Current grouping mode
1142
- grouping_change_url: str = "", # URL for changing grouping mode
1162
+ sb_state: Any, # SourceBrowserRouterState from routes.source_browser
1163
+ filter_url: str = "", # URL for filtering sources
1164
+ grouping_mode: str = "media_path", # Current grouping mode
1165
+ grouping_change_url: str = "", # URL for changing grouping mode
1166
+ keyboard_system: Optional[KeyboardSystem] = None, # Pre-built keyboard system, threaded through to VC content
1143
1167
  ) -> Any: # Source browser component with virtual collection
1144
1168
  "Render the full source browser panel (header + VC content)."
1145
1169
  ```
@@ -1186,10 +1210,6 @@ from cjm_transcript_source_select.routes.source_browser import (
1186
1210
 
1187
1211
  ``` python
1188
1212
  def init_source_browser_router(
1189
- source_service: SourceService, # Source service for querying transcriptions
1190
- urls: SelectionUrls, # URL bundle (toggle, select_all, filter, grouping_change)
1191
- prefix: str = "/browser", # Route prefix for VC routes
1192
- ) -> SourceBrowserRouterState: # Router state with all VC objects and helpers
1193
1213
  "Initialize the source browser virtual collection router."
1194
1214
  ```
1195
1215
 
@@ -1214,6 +1234,7 @@ class SourceBrowserRouterState:
1214
1234
  get_visible_checkbox_oobs: Callable # () -> tuple of OOB elements
1215
1235
  get_checkbox_oob_for: Callable # (record_id, provider_id) -> OOB element or None
1216
1236
  get_vc_row_id_for: Callable # (record_id, provider_id) -> str or None
1237
+ kb_manager: Optional[ZoneManager] # ZoneManager backing the source browser's VC keyboard system — hand to render_keyboard_hints_modal(..., child_managers=[...]) for hierarchical hint display
1217
1238
  ```
1218
1239
 
1219
1240
  ### source_utils (`source_utils.ipynb`)
@@ -1380,7 +1401,22 @@ from cjm_transcript_source_select.components.step_renderer import (
1380
1401
 
1381
1402
  ``` python
1382
1403
  def _create_parent_keyboard_manager() -> ZoneManager: # Parent keyboard manager for hierarchy
1383
- "Create the parent keyboard manager with two ghost zones for column switching."
1404
+ """
1405
+ Create the parent keyboard manager with two ghost zones for column switching.
1406
+
1407
+ Each ghost zone declares `activate_child_callback` so the library-baked
1408
+ Enter/Space dispatch (`ZoneManager.activate_keys` defaults to `("Enter", " ")`)
1409
+ invokes the consumer-defined JS function — `window.activateBrowserChild`
1410
+ and `window.activateQueueChild` are wired in `_generate_hierarchy_js`. The
1411
+ callback path is used (vs. the declarative `activate_child_id` path) because
1412
+ the browser ghost zone's active child depends on the currently-selected
1413
+ tab (`db` → source-browser child; `files` → file-browser child); this
1414
+ dynamic resolution can't be a static declarative mapping.
1415
+
1416
+ Each ghost zone's `label` surfaces as the section header for that zone's
1417
+ derived rows in the hints modal (e.g., the parent's "Source Browser" /
1418
+ "Selection Queue" — Activate row appears under those labels).
1419
+ """
1384
1420
  ```
1385
1421
 
1386
1422
  ``` python
@@ -1427,15 +1463,6 @@ def _generate_hierarchy_js(
1427
1463
 
1428
1464
  ``` python
1429
1465
  def render_selection_step(
1430
- sources: List[Dict[str, Any]], # Available source plugins
1431
- transcriptions: List[Dict[str, Any]], # Available transcription records
1432
- selected_sources: List[Dict[str, str]], # Ordered selection
1433
- grouping_mode: str, # Grouping mode: "media_path" or "batch_id"
1434
- active_tab: str, # Active tab: "db" or "files"
1435
- urls: SelectionUrls, # URL bundle for selection routes
1436
- render_local_files_panel: Optional[Callable] = None, # Render fn for Files tab content
1437
- sb_state: Any = None, # SourceBrowserRouterState for DB tab VC rendering
1438
- ) -> Any: # FastHTML component
1439
1466
  "Render Phase 1: Source Selection & Ordering step with two-column layout."
1440
1467
  ```
1441
1468
 
@@ -1,10 +1,11 @@
1
+ python-fasthtml==0.13.4
1
2
  cjm-plugin-system
2
3
  cjm-transcription-plugin-system
3
4
  cjm-fasthtml-app-core
4
5
  cjm-fasthtml-daisyui
5
6
  cjm_fasthtml_lucide_icons
6
- cjm_fasthtml_file_browser>=0.0.17
7
- cjm_fasthtml_keyboard_navigation>=0.0.20
7
+ cjm_fasthtml_file_browser>=0.0.19
8
+ cjm_fasthtml_keyboard_navigation>=0.0.23
8
9
  duckdb
9
10
  pandas
10
11
  cjm_workflow_state
@@ -12,5 +13,5 @@ cjm_source_provider
12
13
  cjm_fasthtml_interactions
13
14
  cjm_fasthtml_viewport_fit
14
15
  cjm_fasthtml_virtual_collection
15
- cjm_fasthtml_sortable_queue>=0.0.13
16
+ cjm_fasthtml_sortable_queue>=0.0.16
16
17
  cjm_fasthtml_design_system>=0.0.9
@@ -12,7 +12,7 @@ license = {text = "Apache-2.0"}
12
12
  authors = [{name = "Christian J. Mills", email = "9126128+cj-mills@users.noreply.github.com"}]
13
13
  keywords = ['nbdev', 'jupyter', 'notebook', 'python']
14
14
  classifiers = ["Natural Language :: English", "Intended Audience :: Developers", "Development Status :: 3 - Alpha", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only"]
15
- dependencies = ['cjm-plugin-system', 'cjm-transcription-plugin-system', 'cjm-fasthtml-app-core', 'cjm-fasthtml-daisyui', 'cjm_fasthtml_lucide_icons', 'cjm_fasthtml_file_browser>=0.0.17', 'cjm_fasthtml_keyboard_navigation>=0.0.20', 'duckdb', 'pandas', 'cjm_workflow_state', 'cjm_source_provider', 'cjm_fasthtml_interactions', 'cjm_fasthtml_viewport_fit', 'cjm_fasthtml_virtual_collection', 'cjm_fasthtml_sortable_queue>=0.0.13', 'cjm_fasthtml_design_system>=0.0.9']
15
+ dependencies = ['python-fasthtml==0.13.4', 'cjm-plugin-system', 'cjm-transcription-plugin-system', 'cjm-fasthtml-app-core', 'cjm-fasthtml-daisyui', 'cjm_fasthtml_lucide_icons', 'cjm_fasthtml_file_browser>=0.0.19', 'cjm_fasthtml_keyboard_navigation>=0.0.23', 'duckdb', 'pandas', 'cjm_workflow_state', 'cjm_source_provider', 'cjm_fasthtml_interactions', 'cjm_fasthtml_viewport_fit', 'cjm_fasthtml_virtual_collection', 'cjm_fasthtml_sortable_queue>=0.0.16', 'cjm_fasthtml_design_system>=0.0.9']
16
16
 
17
17
  [project.urls]
18
18
  Repository = "https://github.com/cj-mills/cjm-transcript-source-select"
@@ -1 +0,0 @@
1
- __version__ = "0.0.28"