cjm-fasthtml-card-stack 0.0.2__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.2/cjm_fasthtml_card_stack.egg-info → cjm_fasthtml_card_stack-0.0.4}/PKG-INFO +19 -19
  2. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/README.md +18 -18
  3. cjm_fasthtml_card_stack-0.0.4/cjm_fasthtml_card_stack/__init__.py +1 -0
  4. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/components/viewport.py +17 -19
  5. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/js/core.py +25 -3
  6. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/routes/handlers.py +4 -4
  7. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/routes/router.py +1 -1
  8. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4/cjm_fasthtml_card_stack.egg-info}/PKG-INFO +19 -19
  9. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack.egg-info/SOURCES.txt +1 -0
  10. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/demos/basic.py +1 -1
  11. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/demos/bottom.py +1 -1
  12. cjm_fasthtml_card_stack-0.0.4/demos/custom_position.py +114 -0
  13. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/demos/shared.py +1 -0
  14. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/settings.ini +1 -1
  15. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/__init__.py +0 -1
  16. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/LICENSE +0 -0
  17. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/MANIFEST.in +0 -0
  18. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/_modidx.py +0 -0
  19. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/components/__init__.py +0 -0
  20. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/components/controls.py +0 -0
  21. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/components/progress.py +0 -0
  22. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/components/states.py +0 -0
  23. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/core/__init__.py +0 -0
  24. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/core/button_ids.py +0 -0
  25. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/core/config.py +0 -0
  26. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/core/constants.py +0 -0
  27. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/core/html_ids.py +0 -0
  28. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/core/models.py +0 -0
  29. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/helpers/__init__.py +0 -0
  30. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/helpers/focus.py +0 -0
  31. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/js/__init__.py +0 -0
  32. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/js/navigation.py +0 -0
  33. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/js/scroll.py +0 -0
  34. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/js/viewport.py +0 -0
  35. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/keyboard/__init__.py +0 -0
  36. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/keyboard/actions.py +0 -0
  37. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack/routes/__init__.py +0 -0
  38. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack.egg-info/dependency_links.txt +0 -0
  39. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack.egg-info/entry_points.txt +0 -0
  40. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack.egg-info/not-zip-safe +0 -0
  41. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack.egg-info/requires.txt +0 -0
  42. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/cjm_fasthtml_card_stack.egg-info/top_level.txt +0 -0
  43. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/demos/__init__.py +0 -0
  44. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/demos/data.py +0 -0
  45. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/pyproject.toml +0 -0
  46. {cjm_fasthtml_card_stack-0.0.2 → cjm_fasthtml_card_stack-0.0.4}/setup.cfg +0 -0
  47. {cjm_fasthtml_card_stack-0.0.2 → 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.2
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
@@ -100,41 +100,41 @@ graph LR
100
100
  components_controls --> core_html_ids
101
101
  components_progress --> core_html_ids
102
102
  components_states --> core_html_ids
103
- components_viewport --> core_models
104
103
  components_viewport --> core_config
105
- components_viewport --> core_constants
106
- components_viewport --> helpers_focus
107
104
  components_viewport --> core_html_ids
105
+ components_viewport --> core_models
106
+ components_viewport --> helpers_focus
107
+ components_viewport --> core_constants
108
108
  components_viewport --> components_states
109
109
  helpers_focus --> core_html_ids
110
110
  js_core --> core_constants
111
- js_core --> core_button_ids
112
- js_core --> core_models
113
111
  js_core --> core_config
114
- js_core --> js_scroll
115
112
  js_core --> core_html_ids
113
+ js_core --> core_models
114
+ js_core --> js_scroll
116
115
  js_core --> js_navigation
117
116
  js_core --> js_viewport
117
+ js_core --> core_button_ids
118
118
  js_navigation --> core_button_ids
119
- js_scroll --> core_button_ids
120
119
  js_scroll --> core_constants
121
120
  js_scroll --> core_html_ids
121
+ js_scroll --> core_button_ids
122
122
  js_viewport --> core_html_ids
123
- keyboard_actions --> core_button_ids
124
- keyboard_actions --> core_models
123
+ keyboard_actions --> js_core
125
124
  keyboard_actions --> core_config
126
125
  keyboard_actions --> core_html_ids
127
- keyboard_actions --> js_core
126
+ keyboard_actions --> core_models
127
+ keyboard_actions --> core_button_ids
128
+ routes_handlers --> core_config
129
+ routes_handlers --> core_html_ids
128
130
  routes_handlers --> core_models
129
131
  routes_handlers --> components_viewport
130
132
  routes_handlers --> helpers_focus
131
- routes_handlers --> core_config
132
133
  routes_handlers --> components_progress
133
- routes_handlers --> core_html_ids
134
134
  routes_router --> routes_handlers
135
- routes_router --> core_models
136
135
  routes_router --> core_config
137
136
  routes_router --> core_html_ids
137
+ routes_router --> core_models
138
138
  ```
139
139
 
140
140
  *39 cross-module dependencies detected*
@@ -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
@@ -64,41 +64,41 @@ graph LR
64
64
  components_controls --> core_html_ids
65
65
  components_progress --> core_html_ids
66
66
  components_states --> core_html_ids
67
- components_viewport --> core_models
68
67
  components_viewport --> core_config
69
- components_viewport --> core_constants
70
- components_viewport --> helpers_focus
71
68
  components_viewport --> core_html_ids
69
+ components_viewport --> core_models
70
+ components_viewport --> helpers_focus
71
+ components_viewport --> core_constants
72
72
  components_viewport --> components_states
73
73
  helpers_focus --> core_html_ids
74
74
  js_core --> core_constants
75
- js_core --> core_button_ids
76
- js_core --> core_models
77
75
  js_core --> core_config
78
- js_core --> js_scroll
79
76
  js_core --> core_html_ids
77
+ js_core --> core_models
78
+ js_core --> js_scroll
80
79
  js_core --> js_navigation
81
80
  js_core --> js_viewport
81
+ js_core --> core_button_ids
82
82
  js_navigation --> core_button_ids
83
- js_scroll --> core_button_ids
84
83
  js_scroll --> core_constants
85
84
  js_scroll --> core_html_ids
85
+ js_scroll --> core_button_ids
86
86
  js_viewport --> core_html_ids
87
- keyboard_actions --> core_button_ids
88
- keyboard_actions --> core_models
87
+ keyboard_actions --> js_core
89
88
  keyboard_actions --> core_config
90
89
  keyboard_actions --> core_html_ids
91
- keyboard_actions --> js_core
90
+ keyboard_actions --> core_models
91
+ keyboard_actions --> core_button_ids
92
+ routes_handlers --> core_config
93
+ routes_handlers --> core_html_ids
92
94
  routes_handlers --> core_models
93
95
  routes_handlers --> components_viewport
94
96
  routes_handlers --> helpers_focus
95
- routes_handlers --> core_config
96
97
  routes_handlers --> components_progress
97
- routes_handlers --> core_html_ids
98
98
  routes_router --> routes_handlers
99
- routes_router --> core_models
100
99
  routes_router --> core_config
101
100
  routes_router --> core_html_ids
101
+ routes_router --> core_models
102
102
  ```
103
103
 
104
104
  *39 cross-module dependencies detected*
@@ -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"
@@ -11,11 +11,11 @@ from typing import Any, Callable, List, Optional
11
11
  from fasthtml.common import Div, Script, A, Hidden
12
12
 
13
13
  # DaisyUI utilities
14
- from cjm_fasthtml_daisyui.utilities.semantic_colors import ring_dui
14
+ from cjm_fasthtml_daisyui.utilities.semantic_colors import shadow_dui
15
15
  from cjm_fasthtml_daisyui.utilities.border_radius import border_radius
16
16
 
17
17
  # Tailwind utilities
18
- from cjm_fasthtml_tailwind.utilities.effects import ring
18
+ from cjm_fasthtml_tailwind.utilities.effects import shadow
19
19
  from cjm_fasthtml_tailwind.utilities.flexbox_and_grid import (
20
20
  flex_display, flex_direction, justify, items, gap, grid_display
21
21
  )
@@ -110,9 +110,9 @@ def render_slot_card(
110
110
  )
111
111
  content = render_card(card_items[item_index], context)
112
112
 
113
- # Focus ring styling for focused slot
113
+ # Focus shadow styling for focused slot
114
114
  focus_cls = combine_classes(
115
- ring(3), ring_dui.primary, border_radius.box
115
+ shadow.lg, shadow_dui.primary, border_radius.box
116
116
  ) if is_focused else ""
117
117
 
118
118
  # Mode sync script in focused slot OOB updates
@@ -195,7 +195,7 @@ def render_all_slots_oob(
195
195
  focused_section = Div(
196
196
  focused_card, mode_sync,
197
197
  id=ids.viewport_section_focused,
198
- cls=combine_classes(flex_display, justify.center, items.center, w.full),
198
+ cls=combine_classes(flex_display, justify.center, items.center, w.full, p.x(2), p.b(4)),
199
199
  hx_swap_oob="innerHTML"
200
200
  )
201
201
 
@@ -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(
@@ -276,7 +274,7 @@ def render_viewport(
276
274
  focused_section = Div(
277
275
  focused_card,
278
276
  id=ids.viewport_section_focused,
279
- cls=combine_classes(flex_display, justify.center, items.center, w.full)
277
+ cls=combine_classes(flex_display, justify.center, items.center, w.full, p.x(2), p.b(4))
280
278
  )
281
279
 
282
280
  after_section = Div(
@@ -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.2
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
@@ -100,41 +100,41 @@ graph LR
100
100
  components_controls --> core_html_ids
101
101
  components_progress --> core_html_ids
102
102
  components_states --> core_html_ids
103
- components_viewport --> core_models
104
103
  components_viewport --> core_config
105
- components_viewport --> core_constants
106
- components_viewport --> helpers_focus
107
104
  components_viewport --> core_html_ids
105
+ components_viewport --> core_models
106
+ components_viewport --> helpers_focus
107
+ components_viewport --> core_constants
108
108
  components_viewport --> components_states
109
109
  helpers_focus --> core_html_ids
110
110
  js_core --> core_constants
111
- js_core --> core_button_ids
112
- js_core --> core_models
113
111
  js_core --> core_config
114
- js_core --> js_scroll
115
112
  js_core --> core_html_ids
113
+ js_core --> core_models
114
+ js_core --> js_scroll
116
115
  js_core --> js_navigation
117
116
  js_core --> js_viewport
117
+ js_core --> core_button_ids
118
118
  js_navigation --> core_button_ids
119
- js_scroll --> core_button_ids
120
119
  js_scroll --> core_constants
121
120
  js_scroll --> core_html_ids
121
+ js_scroll --> core_button_ids
122
122
  js_viewport --> core_html_ids
123
- keyboard_actions --> core_button_ids
124
- keyboard_actions --> core_models
123
+ keyboard_actions --> js_core
125
124
  keyboard_actions --> core_config
126
125
  keyboard_actions --> core_html_ids
127
- keyboard_actions --> js_core
126
+ keyboard_actions --> core_models
127
+ keyboard_actions --> core_button_ids
128
+ routes_handlers --> core_config
129
+ routes_handlers --> core_html_ids
128
130
  routes_handlers --> core_models
129
131
  routes_handlers --> components_viewport
130
132
  routes_handlers --> helpers_focus
131
- routes_handlers --> core_config
132
133
  routes_handlers --> components_progress
133
- routes_handlers --> core_html_ids
134
134
  routes_router --> routes_handlers
135
- routes_router --> core_models
136
135
  routes_router --> core_config
137
136
  routes_router --> core_html_ids
137
+ routes_router --> core_models
138
138
  ```
139
139
 
140
140
  *39 cross-module dependencies detected*
@@ -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
@@ -57,7 +57,7 @@ def render_card(item, context: CardRenderContext):
57
57
  ),
58
58
  cls=combine_classes(
59
59
  card,
60
- bg_dui.base_100 if is_focused else bg_dui.base_200,
60
+ bg_dui.base_100,# if is_focused else bg_dui.base_200,
61
61
  w.full,
62
62
  ),
63
63
  )
@@ -42,7 +42,7 @@ def render_card(item, context: CardRenderContext):
42
42
  ),
43
43
  cls=combine_classes(
44
44
  card,
45
- bg_dui.base_100 if is_focused else bg_dui.base_200,
45
+ bg_dui.base_100,# if is_focused else bg_dui.base_200,
46
46
  w.full,
47
47
  ),
48
48
  )
@@ -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.2
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.2"