cjm-fasthtml-card-stack 0.0.3__tar.gz → 0.0.4__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 (47) hide show
  1. {cjm_fasthtml_card_stack-0.0.3/cjm_fasthtml_card_stack.egg-info → cjm_fasthtml_card_stack-0.0.4}/PKG-INFO +17 -17
  2. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/README.md +16 -16
  3. cjm_fasthtml_card_stack-0.0.4/cjm_fasthtml_card_stack/__init__.py +1 -0
  4. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/components/viewport.py +11 -13
  5. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/js/core.py +25 -3
  6. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/routes/handlers.py +4 -4
  7. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/routes/router.py +1 -1
  8. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4/cjm_fasthtml_card_stack.egg-info}/PKG-INFO +17 -17
  9. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack.egg-info/SOURCES.txt +1 -0
  10. cjm_fasthtml_card_stack-0.0.4/demos/custom_position.py +114 -0
  11. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/demos/shared.py +1 -0
  12. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/settings.ini +1 -1
  13. cjm_fasthtml_card_stack-0.0.3/cjm_fasthtml_card_stack/__init__.py +0 -1
  14. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/LICENSE +0 -0
  15. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/MANIFEST.in +0 -0
  16. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/_modidx.py +0 -0
  17. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/components/__init__.py +0 -0
  18. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/components/controls.py +0 -0
  19. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/components/progress.py +0 -0
  20. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/components/states.py +0 -0
  21. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/core/__init__.py +0 -0
  22. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/core/button_ids.py +0 -0
  23. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/core/config.py +0 -0
  24. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/core/constants.py +0 -0
  25. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/core/html_ids.py +0 -0
  26. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/core/models.py +0 -0
  27. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/helpers/__init__.py +0 -0
  28. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/helpers/focus.py +0 -0
  29. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/js/__init__.py +0 -0
  30. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/js/navigation.py +0 -0
  31. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/js/scroll.py +0 -0
  32. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/js/viewport.py +0 -0
  33. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/keyboard/__init__.py +0 -0
  34. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/keyboard/actions.py +0 -0
  35. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/routes/__init__.py +0 -0
  36. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack.egg-info/dependency_links.txt +0 -0
  37. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack.egg-info/entry_points.txt +0 -0
  38. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack.egg-info/not-zip-safe +0 -0
  39. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack.egg-info/requires.txt +0 -0
  40. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack.egg-info/top_level.txt +0 -0
  41. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/demos/__init__.py +0 -0
  42. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/demos/basic.py +0 -0
  43. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/demos/bottom.py +0 -0
  44. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/demos/data.py +0 -0
  45. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/pyproject.toml +0 -0
  46. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/setup.cfg +0 -0
  47. {cjm_fasthtml_card_stack-0.0.3 → cjm_fasthtml_card_stack-0.0.4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-fasthtml-card-stack
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: A fixed-viewport card stack component for FastHTML with keyboard navigation, scroll-to-nav, configurable focus position, and HTMX-driven OOB updates.
5
5
  Home-page: https://github.com/cj-mills/cjm-fasthtml-card-stack
6
6
  Author: Christian J. Mills
@@ -102,38 +102,38 @@ graph LR
102
102
  components_states --> core_html_ids
103
103
  components_viewport --> core_config
104
104
  components_viewport --> core_html_ids
105
- components_viewport --> components_states
106
- components_viewport --> core_constants
107
105
  components_viewport --> core_models
108
106
  components_viewport --> helpers_focus
107
+ components_viewport --> core_constants
108
+ components_viewport --> components_states
109
109
  helpers_focus --> core_html_ids
110
110
  js_core --> core_constants
111
111
  js_core --> core_config
112
112
  js_core --> core_html_ids
113
- js_core --> js_viewport
114
- js_core --> js_navigation
115
- js_core --> js_scroll
116
113
  js_core --> core_models
114
+ js_core --> js_scroll
115
+ js_core --> js_navigation
116
+ js_core --> js_viewport
117
117
  js_core --> core_button_ids
118
118
  js_navigation --> core_button_ids
119
119
  js_scroll --> core_constants
120
- js_scroll --> core_button_ids
121
120
  js_scroll --> core_html_ids
121
+ js_scroll --> core_button_ids
122
122
  js_viewport --> core_html_ids
123
+ keyboard_actions --> js_core
123
124
  keyboard_actions --> core_config
124
125
  keyboard_actions --> core_html_ids
125
- keyboard_actions --> js_core
126
126
  keyboard_actions --> core_models
127
127
  keyboard_actions --> core_button_ids
128
- routes_handlers --> core_models
129
128
  routes_handlers --> core_config
130
129
  routes_handlers --> core_html_ids
131
- routes_handlers --> components_progress
132
- routes_handlers --> helpers_focus
130
+ routes_handlers --> core_models
133
131
  routes_handlers --> components_viewport
132
+ routes_handlers --> helpers_focus
133
+ routes_handlers --> components_progress
134
+ routes_router --> routes_handlers
134
135
  routes_router --> core_config
135
136
  routes_router --> core_html_ids
136
- routes_router --> routes_handlers
137
137
  routes_router --> core_models
138
138
  ```
139
139
 
@@ -514,6 +514,7 @@ def _generate_card_count_mgmt_js(
514
514
  def _generate_coordinator_js(
515
515
  ids: CardStackHtmlIds, # HTML IDs for this instance
516
516
  config: CardStackConfig, # Config for prefix-unique listener guards
517
+ focus_position: Optional[int] = None, # Focus slot offset (None=center, -1=bottom, 0=top)
517
518
  ) -> str: # JS code fragment for master coordinator
518
519
  "Generate JS for the master coordinator and HTMX listener."
519
520
  ```
@@ -670,8 +671,8 @@ def card_stack_update_viewport(
670
671
  ids: CardStackHtmlIds, # HTML IDs for this instance
671
672
  urls: CardStackUrls, # URL bundle for navigation
672
673
  render_card: Callable, # Card renderer callback
673
- ) -> Any: # Full viewport component (outerHTML swap)
674
- "Update viewport with new card count. Mutates state.visible_count in place."
674
+ ) -> Tuple: # OOB section elements (3 viewport sections)
675
+ "Update viewport with new card count via OOB section swaps. Mutates state.visible_count in place."
675
676
  ```
676
677
 
677
678
  ``` python
@@ -1082,10 +1083,9 @@ def render_all_slots_oob(
1082
1083
 
1083
1084
  ``` python
1084
1085
  def _grid_template_rows(
1085
- focus_slot: int, # Resolved focus slot position
1086
- visible_count: int, # Number of visible slots
1086
+ focus_position: Optional[int] = None, # Focus slot offset (None=center, -1=bottom, 0=top)
1087
1087
  ) -> str: # CSS grid-template-rows value
1088
- "Compute CSS grid-template-rows based on focus slot position."
1088
+ "Compute CSS grid-template-rows based on focus position intent."
1089
1089
  ```
1090
1090
 
1091
1091
  ``` python
@@ -66,38 +66,38 @@ graph LR
66
66
  components_states --> core_html_ids
67
67
  components_viewport --> core_config
68
68
  components_viewport --> core_html_ids
69
- components_viewport --> components_states
70
- components_viewport --> core_constants
71
69
  components_viewport --> core_models
72
70
  components_viewport --> helpers_focus
71
+ components_viewport --> core_constants
72
+ components_viewport --> components_states
73
73
  helpers_focus --> core_html_ids
74
74
  js_core --> core_constants
75
75
  js_core --> core_config
76
76
  js_core --> core_html_ids
77
- js_core --> js_viewport
78
- js_core --> js_navigation
79
- js_core --> js_scroll
80
77
  js_core --> core_models
78
+ js_core --> js_scroll
79
+ js_core --> js_navigation
80
+ js_core --> js_viewport
81
81
  js_core --> core_button_ids
82
82
  js_navigation --> core_button_ids
83
83
  js_scroll --> core_constants
84
- js_scroll --> core_button_ids
85
84
  js_scroll --> core_html_ids
85
+ js_scroll --> core_button_ids
86
86
  js_viewport --> core_html_ids
87
+ keyboard_actions --> js_core
87
88
  keyboard_actions --> core_config
88
89
  keyboard_actions --> core_html_ids
89
- keyboard_actions --> js_core
90
90
  keyboard_actions --> core_models
91
91
  keyboard_actions --> core_button_ids
92
- routes_handlers --> core_models
93
92
  routes_handlers --> core_config
94
93
  routes_handlers --> core_html_ids
95
- routes_handlers --> components_progress
96
- routes_handlers --> helpers_focus
94
+ routes_handlers --> core_models
97
95
  routes_handlers --> components_viewport
96
+ routes_handlers --> helpers_focus
97
+ routes_handlers --> components_progress
98
+ routes_router --> routes_handlers
98
99
  routes_router --> core_config
99
100
  routes_router --> core_html_ids
100
- routes_router --> routes_handlers
101
101
  routes_router --> core_models
102
102
  ```
103
103
 
@@ -478,6 +478,7 @@ def _generate_card_count_mgmt_js(
478
478
  def _generate_coordinator_js(
479
479
  ids: CardStackHtmlIds, # HTML IDs for this instance
480
480
  config: CardStackConfig, # Config for prefix-unique listener guards
481
+ focus_position: Optional[int] = None, # Focus slot offset (None=center, -1=bottom, 0=top)
481
482
  ) -> str: # JS code fragment for master coordinator
482
483
  "Generate JS for the master coordinator and HTMX listener."
483
484
  ```
@@ -634,8 +635,8 @@ def card_stack_update_viewport(
634
635
  ids: CardStackHtmlIds, # HTML IDs for this instance
635
636
  urls: CardStackUrls, # URL bundle for navigation
636
637
  render_card: Callable, # Card renderer callback
637
- ) -> Any: # Full viewport component (outerHTML swap)
638
- "Update viewport with new card count. Mutates state.visible_count in place."
638
+ ) -> Tuple: # OOB section elements (3 viewport sections)
639
+ "Update viewport with new card count via OOB section swaps. Mutates state.visible_count in place."
639
640
  ```
640
641
 
641
642
  ``` python
@@ -1046,10 +1047,9 @@ def render_all_slots_oob(
1046
1047
 
1047
1048
  ``` python
1048
1049
  def _grid_template_rows(
1049
- focus_slot: int, # Resolved focus slot position
1050
- visible_count: int, # Number of visible slots
1050
+ focus_position: Optional[int] = None, # Focus slot offset (None=center, -1=bottom, 0=top)
1051
1051
  ) -> str: # CSS grid-template-rows value
1052
- "Compute CSS grid-template-rows based on focus slot position."
1052
+ "Compute CSS grid-template-rows based on focus position intent."
1053
1053
  ```
1054
1054
 
1055
1055
  ``` python
@@ -0,0 +1 @@
1
+ __version__ = "0.0.4"
@@ -210,19 +210,17 @@ def render_all_slots_oob(
210
210
 
211
211
  # %% ../../nbs/components/viewport.ipynb #v1000031
212
212
  def _grid_template_rows(
213
- focus_slot: int, # Resolved focus slot position
214
- visible_count: int, # Number of visible slots
213
+ focus_position: Optional[int] = None, # Focus slot offset (None=center, -1=bottom, 0=top)
215
214
  ) -> str: # CSS grid-template-rows value
216
- """Compute CSS grid-template-rows based on focus slot position."""
217
- slots_before = focus_slot
218
- slots_after = visible_count - focus_slot - 1
219
-
220
- if slots_before == 0:
221
- return "auto 1fr" # No before section
222
- elif slots_after == 0:
223
- return "1fr auto" # No after section
215
+ """Compute CSS grid-template-rows based on focus position intent."""
216
+ if focus_position is None:
217
+ return "1fr auto 1fr" # Center: always 3-section
218
+ elif focus_position == 0:
219
+ return "auto 1fr" # Top: focused first
220
+ elif focus_position < 0:
221
+ return "1fr auto" # Bottom: focused last
224
222
  else:
225
- return "1fr auto 1fr" # Standard 3-section
223
+ return "1fr auto 1fr" # Custom positive: always 3-section
226
224
 
227
225
  # %% ../../nbs/components/viewport.ipynb #v1000041
228
226
  def render_viewport(
@@ -285,8 +283,8 @@ def render_viewport(
285
283
  cls=section_cls(justify.start)
286
284
  )
287
285
 
288
- # Grid template adapts to focus position
289
- grid_rows = _grid_template_rows(focus_slot, state.visible_count)
286
+ # Grid template based on focus position intent (stable across count changes)
287
+ grid_rows = _grid_template_rows(state.focus_position)
290
288
 
291
289
  inner_cls = combine_classes(grid_display, w.full, h.full, m.x.auto, gap(4))
292
290
  inner_style = f"grid-template-rows: {grid_rows}; max-width: {state.card_width}rem"
@@ -6,7 +6,7 @@
6
6
  __all__ = ['global_callback_name', 'generate_card_stack_js']
7
7
 
8
8
  # %% ../../nbs/js/core.ipynb #jc000003
9
- from typing import Any, Tuple
9
+ from typing import Any, Optional, Tuple
10
10
 
11
11
  from fasthtml.common import Script
12
12
 
@@ -166,7 +166,7 @@ def _generate_card_count_mgmt_js(
166
166
  if ('{urls.update_viewport}') {{
167
167
  htmx.ajax('POST', '{urls.update_viewport}', {{
168
168
  target: '#' + '{ids.card_stack}',
169
- swap: 'outerHTML',
169
+ swap: 'none',
170
170
  values: {{ visible_count: count }}
171
171
  }});
172
172
  }}
@@ -184,15 +184,36 @@ def _generate_card_count_mgmt_js(
184
184
  def _generate_coordinator_js(
185
185
  ids: CardStackHtmlIds, # HTML IDs for this instance
186
186
  config: CardStackConfig, # Config for prefix-unique listener guards
187
+ focus_position: Optional[int] = None, # Focus slot offset (None=center, -1=bottom, 0=top)
187
188
  ) -> str: # JS code fragment for master coordinator
188
189
  """Generate JS for the master coordinator and HTMX listener."""
189
190
  guard_var = f"_csMasterListener_{config.prefix.replace('-', '_')}"
191
+ js_focus_pos = "null" if focus_position is None else str(focus_position)
190
192
  return f"""
193
+ // === Grid Template Management ===
194
+ ns.applyGridTemplate = function() {{
195
+ const inner = document.getElementById('{ids.card_stack_inner}');
196
+ if (!inner) return;
197
+ const focusPosRaw = {js_focus_pos};
198
+ let tmpl;
199
+ if (focusPosRaw === null) {{
200
+ tmpl = '1fr auto 1fr';
201
+ }} else if (focusPosRaw === 0) {{
202
+ tmpl = 'auto 1fr';
203
+ }} else if (focusPosRaw < 0) {{
204
+ tmpl = '1fr auto';
205
+ }} else {{
206
+ tmpl = '1fr auto 1fr';
207
+ }}
208
+ inner.style.gridTemplateRows = tmpl;
209
+ }};
210
+
191
211
  // === Master Coordinator ===
192
212
  ns.applyAllViewportSettings = function() {{
193
213
  requestAnimationFrame(function() {{
194
214
  if (ns.applyWidth) ns.applyWidth();
195
215
  if (ns.applyScale) ns.applyScale();
216
+ if (ns.applyGridTemplate) ns.applyGridTemplate();
196
217
  if (ns.recalculateHeight) ns.recalculateHeight();
197
218
 
198
219
  const cs = document.getElementById('{ids.card_stack}');
@@ -272,6 +293,7 @@ def generate_card_stack_js(
272
293
  urls: CardStackUrls, # URL bundle for routing
273
294
  container_id: str = "", # Consumer's parent container ID (for height calc)
274
295
  extra_scripts: Tuple[str, ...] = (), # Additional JS to include in the IIFE
296
+ focus_position: Optional[int] = None, # Focus slot offset (None=center, -1=bottom, 0=top)
275
297
  ) -> Any: # Script element with all card stack JavaScript
276
298
  """Compose all card stack JS into a single namespaced IIFE."""
277
299
  prefix = config.prefix
@@ -285,7 +307,7 @@ def generate_card_stack_js(
285
307
  scale_js = _generate_scale_mgmt_js(ids, config, urls)
286
308
  count_js = _generate_card_count_mgmt_js(ids, config, urls)
287
309
  global_cbs_js = _generate_global_callbacks_js(config)
288
- coordinator_js = _generate_coordinator_js(ids, config)
310
+ coordinator_js = _generate_coordinator_js(ids, config, focus_position)
289
311
 
290
312
  return Script(f"""(function() {{
291
313
  window.cardStacks = window.cardStacks || {{}};
@@ -127,17 +127,17 @@ def card_stack_update_viewport(
127
127
  ids: CardStackHtmlIds, # HTML IDs for this instance
128
128
  urls: CardStackUrls, # URL bundle for navigation
129
129
  render_card: Callable, # Card renderer callback
130
- ) -> Any: # Full viewport component (outerHTML swap)
131
- """Update viewport with new card count. Mutates state.visible_count in place."""
130
+ ) -> Tuple: # OOB section elements (3 viewport sections)
131
+ """Update viewport with new card count via OOB section swaps. Mutates state.visible_count in place."""
132
132
  state.visible_count = visible_count
133
- return render_viewport(
133
+ return tuple(build_slots_response(
134
134
  card_items=card_items,
135
135
  state=state,
136
136
  config=config,
137
137
  ids=ids,
138
138
  urls=urls,
139
139
  render_card=render_card,
140
- )
140
+ ))
141
141
 
142
142
  # %% ../../nbs/routes/handlers.ipynb #h1000013
143
143
  def card_stack_save_width(
@@ -100,7 +100,7 @@ def init_card_stack_router(
100
100
 
101
101
  @router
102
102
  def update_viewport(visible_count: int) -> Any:
103
- """Update viewport with new card count (full outerHTML swap)."""
103
+ """Update viewport with new card count (OOB section swaps)."""
104
104
  state = state_getter()
105
105
  items = get_items()
106
106
  result = card_stack_update_viewport(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-fasthtml-card-stack
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: A fixed-viewport card stack component for FastHTML with keyboard navigation, scroll-to-nav, configurable focus position, and HTMX-driven OOB updates.
5
5
  Home-page: https://github.com/cj-mills/cjm-fasthtml-card-stack
6
6
  Author: Christian J. Mills
@@ -102,38 +102,38 @@ graph LR
102
102
  components_states --> core_html_ids
103
103
  components_viewport --> core_config
104
104
  components_viewport --> core_html_ids
105
- components_viewport --> components_states
106
- components_viewport --> core_constants
107
105
  components_viewport --> core_models
108
106
  components_viewport --> helpers_focus
107
+ components_viewport --> core_constants
108
+ components_viewport --> components_states
109
109
  helpers_focus --> core_html_ids
110
110
  js_core --> core_constants
111
111
  js_core --> core_config
112
112
  js_core --> core_html_ids
113
- js_core --> js_viewport
114
- js_core --> js_navigation
115
- js_core --> js_scroll
116
113
  js_core --> core_models
114
+ js_core --> js_scroll
115
+ js_core --> js_navigation
116
+ js_core --> js_viewport
117
117
  js_core --> core_button_ids
118
118
  js_navigation --> core_button_ids
119
119
  js_scroll --> core_constants
120
- js_scroll --> core_button_ids
121
120
  js_scroll --> core_html_ids
121
+ js_scroll --> core_button_ids
122
122
  js_viewport --> core_html_ids
123
+ keyboard_actions --> js_core
123
124
  keyboard_actions --> core_config
124
125
  keyboard_actions --> core_html_ids
125
- keyboard_actions --> js_core
126
126
  keyboard_actions --> core_models
127
127
  keyboard_actions --> core_button_ids
128
- routes_handlers --> core_models
129
128
  routes_handlers --> core_config
130
129
  routes_handlers --> core_html_ids
131
- routes_handlers --> components_progress
132
- routes_handlers --> helpers_focus
130
+ routes_handlers --> core_models
133
131
  routes_handlers --> components_viewport
132
+ routes_handlers --> helpers_focus
133
+ routes_handlers --> components_progress
134
+ routes_router --> routes_handlers
134
135
  routes_router --> core_config
135
136
  routes_router --> core_html_ids
136
- routes_router --> routes_handlers
137
137
  routes_router --> core_models
138
138
  ```
139
139
 
@@ -514,6 +514,7 @@ def _generate_card_count_mgmt_js(
514
514
  def _generate_coordinator_js(
515
515
  ids: CardStackHtmlIds, # HTML IDs for this instance
516
516
  config: CardStackConfig, # Config for prefix-unique listener guards
517
+ focus_position: Optional[int] = None, # Focus slot offset (None=center, -1=bottom, 0=top)
517
518
  ) -> str: # JS code fragment for master coordinator
518
519
  "Generate JS for the master coordinator and HTMX listener."
519
520
  ```
@@ -670,8 +671,8 @@ def card_stack_update_viewport(
670
671
  ids: CardStackHtmlIds, # HTML IDs for this instance
671
672
  urls: CardStackUrls, # URL bundle for navigation
672
673
  render_card: Callable, # Card renderer callback
673
- ) -> Any: # Full viewport component (outerHTML swap)
674
- "Update viewport with new card count. Mutates state.visible_count in place."
674
+ ) -> Tuple: # OOB section elements (3 viewport sections)
675
+ "Update viewport with new card count via OOB section swaps. Mutates state.visible_count in place."
675
676
  ```
676
677
 
677
678
  ``` python
@@ -1082,10 +1083,9 @@ def render_all_slots_oob(
1082
1083
 
1083
1084
  ``` python
1084
1085
  def _grid_template_rows(
1085
- focus_slot: int, # Resolved focus slot position
1086
- visible_count: int, # Number of visible slots
1086
+ focus_position: Optional[int] = None, # Focus slot offset (None=center, -1=bottom, 0=top)
1087
1087
  ) -> str: # CSS grid-template-rows value
1088
- "Compute CSS grid-template-rows based on focus slot position."
1088
+ "Compute CSS grid-template-rows based on focus position intent."
1089
1089
  ```
1090
1090
 
1091
1091
  ``` python
@@ -39,5 +39,6 @@ cjm_fasthtml_card_stack/routes/router.py
39
39
  demos/__init__.py
40
40
  demos/basic.py
41
41
  demos/bottom.py
42
+ demos/custom_position.py
42
43
  demos/data.py
43
44
  demos/shared.py
@@ -0,0 +1,114 @@
1
+ """Custom focus position demo (focus at slot 1)."""
2
+
3
+ from fasthtml.common import Div, P, Span
4
+
5
+ from cjm_fasthtml_daisyui.components.data_display.card import card, card_body
6
+ from cjm_fasthtml_daisyui.components.data_display.badge import badge, badge_colors, badge_styles
7
+ from cjm_fasthtml_daisyui.utilities.semantic_colors import bg_dui, text_dui
8
+ from cjm_fasthtml_tailwind.utilities.spacing import p, m
9
+ from cjm_fasthtml_tailwind.utilities.sizing import w
10
+ from cjm_fasthtml_tailwind.core.base import combine_classes
11
+
12
+ from cjm_fasthtml_card_stack.core.config import CardStackConfig
13
+ from cjm_fasthtml_card_stack.core.models import CardStackState, CardRenderContext
14
+ from cjm_fasthtml_card_stack.core.html_ids import CardStackHtmlIds
15
+ from cjm_fasthtml_card_stack.core.button_ids import CardStackButtonIds
16
+ from cjm_fasthtml_card_stack.routes.router import init_card_stack_router
17
+
18
+ from demos.data import SAMPLE_ITEMS
19
+
20
+
21
+ def render_card(item, context: CardRenderContext):
22
+ """Render a card with position info for the custom focus demo."""
23
+ is_focused = context.card_role == "focused"
24
+
25
+ index_badge = Span(
26
+ f"#{context.index + 1}",
27
+ cls=combine_classes(
28
+ badge,
29
+ badge_colors.accent if is_focused else badge_colors.neutral,
30
+ )
31
+ )
32
+
33
+ distance_label = Span(
34
+ f"slot offset: {context.distance_from_focus:+d}",
35
+ cls=combine_classes(
36
+ badge, badge_styles.ghost,
37
+ )
38
+ ) if not is_focused else Span(
39
+ "focused",
40
+ cls=combine_classes(badge, badge_colors.accent),
41
+ )
42
+
43
+ return Div(
44
+ Div(
45
+ Div(index_badge, distance_label, cls=combine_classes(m.b(1))),
46
+ P(
47
+ item,
48
+ cls=combine_classes(text_dui.base_content),
49
+ style="font-size: calc(0.875rem * var(--card-stack-scale, 100) / 100)",
50
+ ),
51
+ cls=combine_classes(card_body, p(3)),
52
+ ),
53
+ cls=combine_classes(
54
+ card,
55
+ bg_dui.base_100,
56
+ w.full,
57
+ ),
58
+ )
59
+
60
+
61
+ def setup(route_prefix="/custom"):
62
+ """Set up the custom focus position demo.
63
+
64
+ Returns dict with config, ids, btn_ids, router, urls, state management,
65
+ and page rendering dependencies.
66
+ """
67
+ config = CardStackConfig(prefix="custom", click_to_focus=True)
68
+ ids = CardStackHtmlIds(prefix=config.prefix)
69
+ btn_ids = CardStackButtonIds(prefix=config.prefix)
70
+
71
+ state = CardStackState(
72
+ visible_count=5,
73
+ card_width=60,
74
+ focus_position=1, # Second slot from top
75
+ )
76
+
77
+ def get_state():
78
+ return state
79
+
80
+ def set_state(s):
81
+ nonlocal state
82
+ state.focused_index = s.focused_index
83
+ state.visible_count = s.visible_count
84
+ state.card_width = s.card_width
85
+ state.card_scale = s.card_scale
86
+ state.active_mode = s.active_mode
87
+ state.focus_position = s.focus_position
88
+
89
+ def get_items():
90
+ return SAMPLE_ITEMS
91
+
92
+ router, urls = init_card_stack_router(
93
+ config=config,
94
+ state_getter=get_state,
95
+ state_setter=set_state,
96
+ get_items=get_items,
97
+ render_card=render_card,
98
+ route_prefix=route_prefix,
99
+ )
100
+
101
+ return dict(
102
+ config=config,
103
+ ids=ids,
104
+ btn_ids=btn_ids,
105
+ router=router,
106
+ urls=urls,
107
+ get_state=get_state,
108
+ get_items=get_items,
109
+ render_card=render_card,
110
+ container_id="custom-demo-container",
111
+ title="Custom Focus Position",
112
+ description="Focus at slot 1 (second from top). One context card above, rest below.",
113
+ progress_label="Item",
114
+ )
@@ -79,6 +79,7 @@ def render_demo_page(
79
79
  config=config,
80
80
  urls=urls,
81
81
  container_id=container_id,
82
+ focus_position=state.focus_position,
82
83
  )
83
84
 
84
85
  return Div(
@@ -1,7 +1,7 @@
1
1
  [DEFAULT]
2
2
  repo = cjm-fasthtml-card-stack
3
3
  lib_name = cjm-fasthtml-card-stack
4
- version = 0.0.3
4
+ version = 0.0.4
5
5
  min_python = 3.12
6
6
  license = apache2
7
7
  black_formatting = False
@@ -1 +0,0 @@
1
- __version__ = "0.0.3"