cjm-fasthtml-card-stack 0.0.1__py3-none-any.whl → 0.0.3__py3-none-any.whl

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_fasthtml_card_stack/__init__.py +1 -1
  2. cjm_fasthtml_card_stack/components/__init__.py +0 -0
  3. cjm_fasthtml_card_stack/components/controls.py +115 -0
  4. cjm_fasthtml_card_stack/components/progress.py +44 -0
  5. cjm_fasthtml_card_stack/components/states.py +107 -0
  6. cjm_fasthtml_card_stack/components/viewport.py +320 -0
  7. cjm_fasthtml_card_stack/core/__init__.py +0 -0
  8. cjm_fasthtml_card_stack/core/button_ids.py +69 -0
  9. cjm_fasthtml_card_stack/core/config.py +48 -0
  10. cjm_fasthtml_card_stack/core/constants.py +41 -0
  11. cjm_fasthtml_card_stack/core/html_ids.py +94 -0
  12. cjm_fasthtml_card_stack/core/models.py +53 -0
  13. cjm_fasthtml_card_stack/helpers/__init__.py +0 -0
  14. cjm_fasthtml_card_stack/helpers/focus.py +72 -0
  15. cjm_fasthtml_card_stack/js/__init__.py +0 -0
  16. cjm_fasthtml_card_stack/js/core.py +303 -0
  17. cjm_fasthtml_card_stack/js/navigation.py +37 -0
  18. cjm_fasthtml_card_stack/js/scroll.py +76 -0
  19. cjm_fasthtml_card_stack/js/viewport.py +124 -0
  20. cjm_fasthtml_card_stack/keyboard/__init__.py +0 -0
  21. cjm_fasthtml_card_stack/keyboard/actions.py +200 -0
  22. cjm_fasthtml_card_stack/routes/__init__.py +0 -0
  23. cjm_fasthtml_card_stack/routes/handlers.py +158 -0
  24. cjm_fasthtml_card_stack/routes/router.py +150 -0
  25. {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.3.dist-info}/METADATA +32 -24
  26. cjm_fasthtml_card_stack-0.0.3.dist-info/RECORD +36 -0
  27. {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.3.dist-info}/top_level.txt +1 -0
  28. demos/__init__.py +0 -0
  29. demos/basic.py +115 -0
  30. demos/bottom.py +104 -0
  31. demos/data.py +29 -0
  32. demos/shared.py +153 -0
  33. cjm_fasthtml_card_stack-0.0.1.dist-info/RECORD +0 -8
  34. {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.3.dist-info}/WHEEL +0 -0
  35. {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.3.dist-info}/entry_points.txt +0 -0
  36. {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,48 @@
1
+ """Configuration dataclass for card stack initialization."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/config.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['CardStackConfig']
7
+
8
+ # %% ../../nbs/core/config.ipynb #b1000003
9
+ from dataclasses import dataclass, field
10
+ from typing import Tuple
11
+
12
+ # %% ../../nbs/core/config.ipynb #b1000005
13
+ _prefix_counter: int = 0
14
+
15
+ def _auto_prefix() -> str: # Unique prefix string (e.g., "cs0", "cs1")
16
+ """Generate an auto-incrementing unique prefix."""
17
+ global _prefix_counter
18
+ p = f"cs{_prefix_counter}"
19
+ _prefix_counter += 1
20
+ return p
21
+
22
+ def _reset_prefix_counter() -> None:
23
+ """Reset the prefix counter (for testing only)."""
24
+ global _prefix_counter
25
+ _prefix_counter = 0
26
+
27
+ # %% ../../nbs/core/config.ipynb #b1000008
28
+ @dataclass
29
+ class CardStackConfig:
30
+ """Initialization-time settings for a card stack instance."""
31
+ prefix: str = field(default_factory=_auto_prefix) # HTML ID prefix (auto-generated if omitted)
32
+
33
+ # Card count selector options
34
+ visible_count_options: Tuple[int, ...] = (1, 3, 5, 7, 9) # Choices for card count dropdown
35
+
36
+ # Width slider bounds
37
+ card_width_min: int = 30 # Width slider minimum (rem)
38
+ card_width_max: int = 120 # Width slider maximum (rem)
39
+ card_width_step: int = 5 # Width slider step (rem)
40
+
41
+ # Scale slider bounds
42
+ card_scale_min: int = 50 # Scale slider minimum (%)
43
+ card_scale_max: int = 200 # Scale slider maximum (%)
44
+ card_scale_step: int = 10 # Scale slider step (%)
45
+
46
+ # Interaction
47
+ click_to_focus: bool = False # Whether context cards get transparent click overlay
48
+ disable_scroll_in_modes: Tuple[str, ...] = () # Mode names where scroll-to-nav is suppressed
@@ -0,0 +1,41 @@
1
+ """CSS class constants, type aliases, and default values for the card stack library."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/constants.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['CardRole', 'SCROLL_THRESHOLD', 'NAVIGATION_COOLDOWN', 'DEFAULT_VISIBLE_COUNT', 'DEFAULT_CARD_WIDTH',
7
+ 'DEFAULT_CARD_SCALE', 'width_storage_key', 'scale_storage_key', 'card_count_storage_key']
8
+
9
+ # %% ../../nbs/core/constants.ipynb #e1000003
10
+ from typing import Literal
11
+
12
+ # %% ../../nbs/core/constants.ipynb #e1000005
13
+ CardRole = Literal["focused", "context"]
14
+
15
+ # %% ../../nbs/core/constants.ipynb #e1000008
16
+ SCROLL_THRESHOLD: int = 50 # Pixels of wheel delta to trigger navigation
17
+ NAVIGATION_COOLDOWN: int = 100 # Milliseconds between scroll-triggered navigations
18
+
19
+ # %% ../../nbs/core/constants.ipynb #e1000011
20
+ def width_storage_key(
21
+ prefix: str # Card stack instance prefix
22
+ ) -> str: # localStorage key for card width
23
+ """Generate localStorage key for card width."""
24
+ return f"{prefix}-card-width"
25
+
26
+ def scale_storage_key(
27
+ prefix: str # Card stack instance prefix
28
+ ) -> str: # localStorage key for card scale
29
+ """Generate localStorage key for card scale."""
30
+ return f"{prefix}-card-scale"
31
+
32
+ def card_count_storage_key(
33
+ prefix: str # Card stack instance prefix
34
+ ) -> str: # localStorage key for card count
35
+ """Generate localStorage key for card count."""
36
+ return f"{prefix}-card-count"
37
+
38
+ # %% ../../nbs/core/constants.ipynb #e1000014
39
+ DEFAULT_VISIBLE_COUNT: int = 3 # Default number of cards visible in viewport
40
+ DEFAULT_CARD_WIDTH: int = 80 # Default card stack width in rem
41
+ DEFAULT_CARD_SCALE: int = 100 # Default card scale percentage
@@ -0,0 +1,94 @@
1
+ """Prefix-based HTML ID generator for card stack DOM elements."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/html_ids.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['CardStackHtmlIds']
7
+
8
+ # %% ../../nbs/core/html_ids.ipynb #c1000003
9
+ from dataclasses import dataclass
10
+
11
+ # %% ../../nbs/core/html_ids.ipynb #c1000005
12
+ @dataclass
13
+ class CardStackHtmlIds:
14
+ """Prefix-based HTML ID generator for card stack DOM elements."""
15
+ prefix: str # ID prefix for this card stack instance
16
+
17
+ # --- Outer containers ---
18
+
19
+ @property
20
+ def card_stack(self) -> str: # Full-width scroll capture container
21
+ """Outer card stack container."""
22
+ return f"{self.prefix}-card-stack"
23
+
24
+ @property
25
+ def card_stack_inner(self) -> str: # Width-constrained CSS Grid container
26
+ """Inner grid container for 3-section layout."""
27
+ return f"{self.prefix}-card-stack-inner"
28
+
29
+ @property
30
+ def card_stack_empty(self) -> str: # Empty state placeholder
31
+ """Empty state container."""
32
+ return f"{self.prefix}-card-stack-empty"
33
+
34
+ # --- Viewport sections ---
35
+
36
+ @property
37
+ def viewport_section_before(self) -> str: # Cards before focused (1fr, justify-end)
38
+ """Viewport section for context cards before focused card."""
39
+ return f"{self.prefix}-viewport-section-before"
40
+
41
+ @property
42
+ def viewport_section_focused(self) -> str: # Focused card (auto)
43
+ """Viewport section for the focused card."""
44
+ return f"{self.prefix}-viewport-section-focused"
45
+
46
+ @property
47
+ def viewport_section_after(self) -> str: # Cards after focused (1fr, justify-start)
48
+ """Viewport section for context cards after focused card."""
49
+ return f"{self.prefix}-viewport-section-after"
50
+
51
+ # --- Dynamic slot IDs ---
52
+
53
+ def viewport_slot(
54
+ self,
55
+ index: int # Slot index within the viewport
56
+ ) -> str: # Slot element ID
57
+ """ID for an individual viewport slot container."""
58
+ return f"{self.prefix}-viewport-slot-{index}"
59
+
60
+ # --- Controls ---
61
+
62
+ @property
63
+ def card_count_select(self) -> str: # Card count dropdown
64
+ """Card count selector dropdown."""
65
+ return f"{self.prefix}-card-count-select"
66
+
67
+ @property
68
+ def width_slider(self) -> str: # Width range slider
69
+ """Card stack width slider."""
70
+ return f"{self.prefix}-width-slider"
71
+
72
+ @property
73
+ def scale_slider(self) -> str: # Scale range slider
74
+ """Card stack scale slider."""
75
+ return f"{self.prefix}-scale-slider"
76
+
77
+ # --- Status elements ---
78
+
79
+ @property
80
+ def progress(self) -> str: # Progress indicator
81
+ """Progress indicator element."""
82
+ return f"{self.prefix}-progress"
83
+
84
+ @property
85
+ def loading(self) -> str: # Loading state container
86
+ """Loading state container."""
87
+ return f"{self.prefix}-loading"
88
+
89
+ # --- Hidden inputs ---
90
+
91
+ @property
92
+ def focused_index_input(self) -> str: # Hidden input for keyboard nav focus recovery
93
+ """Hidden input storing the focused index for HTMX submissions."""
94
+ return f"{self.prefix}-focused-index"
@@ -0,0 +1,53 @@
1
+ """Core dataclasses for card stack state, render context, and URL routing."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/models.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['CardStackState', 'CardRenderContext', 'CardStackUrls']
7
+
8
+ # %% ../../nbs/core/models.ipynb #a1000003
9
+ from dataclasses import dataclass
10
+ from typing import Optional
11
+
12
+ # %% ../../nbs/core/models.ipynb #a1000005
13
+ @dataclass
14
+ class CardStackState:
15
+ """Viewport state for a card stack instance."""
16
+ focused_index: int = 0 # Index of focused item in the items list
17
+ visible_count: int = 3 # Number of card slots visible in viewport
18
+ card_width: int = 80 # Max width of card stack inner container in rem
19
+ card_scale: int = 100 # Content scale percentage (50-200)
20
+ active_mode: Optional[str] = None # Current interaction mode name (consumer-defined)
21
+ focus_position: Optional[int] = None # Slot offset for focused card (None=center, -1=bottom)
22
+
23
+ # %% ../../nbs/core/models.ipynb #a1000010
24
+ @dataclass
25
+ class CardRenderContext:
26
+ """Context passed to the consumer's render_card callback."""
27
+ card_role: str # "focused" or "context"
28
+ index: int # Item's position in the full items list
29
+ total_items: int # Total item count
30
+ is_first: bool # Whether this is the first item
31
+ is_last: bool # Whether this is the last item
32
+ active_mode: Optional[str] # Current interaction mode
33
+ card_scale: int # Scale percentage (50-200)
34
+ distance_from_focus: int # Signed slot offset from focused card (0=focused)
35
+
36
+ # %% ../../nbs/core/models.ipynb #a1000015
37
+ @dataclass
38
+ class CardStackUrls:
39
+ """URL bundle for card stack navigation and viewport operations."""
40
+
41
+ # Navigation URLs
42
+ nav_up: str = "" # Navigate to previous item
43
+ nav_down: str = "" # Navigate to next item
44
+ nav_first: str = "" # Navigate to first item
45
+ nav_last: str = "" # Navigate to last item
46
+ nav_page_up: str = "" # Page jump up
47
+ nav_page_down: str = "" # Page jump down
48
+ nav_to_index: str = "" # Navigate to specific index (click-to-focus)
49
+
50
+ # Viewport URLs
51
+ update_viewport: str = "" # Change visible_count (full viewport re-render)
52
+ save_width: str = "" # Persist card_width
53
+ save_scale: str = "" # Persist card_scale
File without changes
@@ -0,0 +1,72 @@
1
+ """Focus position resolution, viewport window calculation, and OOB focus sync."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/helpers/focus.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['resolve_focus_slot', 'calculate_viewport_window', 'render_focus_oob']
7
+
8
+ # %% ../../nbs/helpers/focus.ipynb #f1000003
9
+ from typing import List, Optional, Tuple
10
+
11
+ from fasthtml.common import Hidden
12
+
13
+ from ..core.html_ids import CardStackHtmlIds
14
+
15
+ # %% ../../nbs/helpers/focus.ipynb #f1000005
16
+ def resolve_focus_slot(
17
+ focus_position: Optional[int], # Slot offset (None=center, -1=bottom, 0=top)
18
+ visible_count: int, # Number of visible card slots
19
+ ) -> int: # Resolved 0-indexed slot position
20
+ """Resolve focus_position to an actual slot index within the viewport."""
21
+ if focus_position is None:
22
+ return visible_count // 2
23
+ if focus_position < 0:
24
+ slot = visible_count + focus_position
25
+ else:
26
+ slot = focus_position
27
+ return max(0, min(slot, visible_count - 1))
28
+
29
+ # %% ../../nbs/helpers/focus.ipynb #f1000011
30
+ def calculate_viewport_window(
31
+ focused_index: int, # Index of the focused item
32
+ total_items: int, # Total number of items
33
+ visible_count: int, # Number of visible card slots
34
+ focus_position: Optional[int] = None, # Focus slot (None=center)
35
+ ) -> List[Optional[int]]: # Slot indices (None for placeholder slots)
36
+ """Calculate which item indices should be visible in each viewport slot."""
37
+ focus_slot = resolve_focus_slot(focus_position, visible_count)
38
+ slots_before = focus_slot
39
+ slots_after = visible_count - focus_slot - 1
40
+
41
+ result = []
42
+
43
+ # Slots before focused
44
+ for offset in range(slots_before, 0, -1):
45
+ idx = focused_index - offset
46
+ result.append(idx if idx >= 0 else None)
47
+
48
+ # Focused slot
49
+ result.append(focused_index if 0 <= focused_index < total_items else None)
50
+
51
+ # Slots after focused
52
+ for offset in range(1, slots_after + 1):
53
+ idx = focused_index + offset
54
+ result.append(idx if idx < total_items else None)
55
+
56
+ return result
57
+
58
+ # %% ../../nbs/helpers/focus.ipynb #f1000019
59
+ def render_focus_oob(
60
+ focused_index: int, # The item index to focus
61
+ ids: CardStackHtmlIds, # HTML IDs for this card stack instance
62
+ form_input_name: str = "focused_index", # Field name for the form input
63
+ ) -> Tuple[Hidden, ...]: # Hidden inputs with OOB swap
64
+ """Render OOB hidden inputs to synchronize focus after HTMX swap."""
65
+ return (
66
+ Hidden(
67
+ id=ids.focused_index_input,
68
+ name=form_input_name,
69
+ value=str(focused_index),
70
+ hx_swap_oob="true",
71
+ ),
72
+ )
File without changes
@@ -0,0 +1,303 @@
1
+ """Master composer for card stack JavaScript. Combines viewport height,"""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/js/core.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['global_callback_name', 'generate_card_stack_js']
7
+
8
+ # %% ../../nbs/js/core.ipynb #jc000003
9
+ from typing import Any, Tuple
10
+
11
+ from fasthtml.common import Script
12
+
13
+ from ..core.config import CardStackConfig
14
+ from ..core.html_ids import CardStackHtmlIds
15
+ from ..core.button_ids import CardStackButtonIds
16
+ from ..core.models import CardStackUrls, CardStackState
17
+ from cjm_fasthtml_card_stack.core.constants import (
18
+ width_storage_key, scale_storage_key, card_count_storage_key,
19
+ DEFAULT_CARD_WIDTH, DEFAULT_CARD_SCALE, DEFAULT_VISIBLE_COUNT,
20
+ )
21
+ from .viewport import generate_viewport_height_js
22
+ from .scroll import generate_scroll_nav_js
23
+ from .navigation import generate_page_nav_js
24
+
25
+ # %% ../../nbs/js/core.ipynb #jc000005
26
+ def _generate_width_mgmt_js(
27
+ ids: CardStackHtmlIds, # HTML IDs for this instance
28
+ config: CardStackConfig, # Config with slider bounds
29
+ urls: CardStackUrls, # URL bundle (save_width)
30
+ ) -> str: # JS code fragment for width management
31
+ """Generate JS for width slider management."""
32
+ storage_key = width_storage_key(config.prefix)
33
+ return f"""
34
+ // === Width Management ===
35
+ const _WIDTH_KEY = '{storage_key}';
36
+ let _saveWidthTimer = null;
37
+
38
+ function _saveWidthToServer(val) {{
39
+ if (!'{urls.save_width}') return;
40
+ clearTimeout(_saveWidthTimer);
41
+ _saveWidthTimer = setTimeout(function() {{
42
+ htmx.ajax('POST', '{urls.save_width}', {{
43
+ swap: 'none', values: {{ card_width: val }}
44
+ }});
45
+ }}, 500);
46
+ }}
47
+
48
+ ns.updateWidth = function(value) {{
49
+ const inner = document.getElementById('{ids.card_stack_inner}');
50
+ if (!inner) return;
51
+ inner.style.maxWidth = value + 'rem';
52
+ try {{ localStorage.setItem(_WIDTH_KEY, value); }} catch (e) {{}}
53
+ const slider = document.getElementById('{ids.width_slider}');
54
+ if (slider && parseInt(slider.value) !== parseInt(value)) slider.value = value;
55
+ _saveWidthToServer(value);
56
+ }};
57
+
58
+ ns.decreaseWidth = function() {{
59
+ const slider = document.getElementById('{ids.width_slider}');
60
+ const current = slider ? parseInt(slider.value) : {DEFAULT_CARD_WIDTH};
61
+ ns.updateWidth(Math.max({config.card_width_min}, current - {config.card_width_step}));
62
+ }};
63
+
64
+ ns.increaseWidth = function() {{
65
+ const slider = document.getElementById('{ids.width_slider}');
66
+ const current = slider ? parseInt(slider.value) : {DEFAULT_CARD_WIDTH};
67
+ ns.updateWidth(Math.min({config.card_width_max}, current + {config.card_width_step}));
68
+ }};
69
+
70
+ ns.applyWidth = function() {{
71
+ const inner = document.getElementById('{ids.card_stack_inner}');
72
+ if (!inner) return;
73
+ let val = {DEFAULT_CARD_WIDTH};
74
+ try {{ const s = localStorage.getItem(_WIDTH_KEY); if (s) val = parseInt(s); }} catch (e) {{}}
75
+ inner.style.maxWidth = val + 'rem';
76
+ const slider = document.getElementById('{ids.width_slider}');
77
+ if (slider) slider.value = val;
78
+ }};
79
+ """
80
+
81
+ # %% ../../nbs/js/core.ipynb #jc000006
82
+ def _generate_scale_mgmt_js(
83
+ ids: CardStackHtmlIds, # HTML IDs for this instance
84
+ config: CardStackConfig, # Config with slider bounds
85
+ urls: CardStackUrls, # URL bundle (save_scale)
86
+ ) -> str: # JS code fragment for scale management
87
+ """Generate JS for scale slider management."""
88
+ storage_key = scale_storage_key(config.prefix)
89
+ return f"""
90
+ // === Scale Management ===
91
+ const _SCALE_KEY = '{storage_key}';
92
+ let _saveScaleTimer = null;
93
+
94
+ function _saveScaleToServer(val) {{
95
+ if (!'{urls.save_scale}') return;
96
+ clearTimeout(_saveScaleTimer);
97
+ _saveScaleTimer = setTimeout(function() {{
98
+ htmx.ajax('POST', '{urls.save_scale}', {{
99
+ swap: 'none', values: {{ card_scale: val }}
100
+ }});
101
+ }}, 500);
102
+ }}
103
+
104
+ function _applyScaleCssProperty(val) {{
105
+ const cs = document.getElementById('{ids.card_stack}');
106
+ if (cs) cs.style.setProperty('--card-stack-scale', val);
107
+ }}
108
+
109
+ ns.updateScale = function(value) {{
110
+ _applyScaleCssProperty(value);
111
+ try {{ localStorage.setItem(_SCALE_KEY, value); }} catch (e) {{}}
112
+ const slider = document.getElementById('{ids.scale_slider}');
113
+ if (slider && parseInt(slider.value) !== parseInt(value)) slider.value = value;
114
+ _saveScaleToServer(value);
115
+ }};
116
+
117
+ ns.decreaseScale = function() {{
118
+ const slider = document.getElementById('{ids.scale_slider}');
119
+ const current = slider ? parseInt(slider.value) : {DEFAULT_CARD_SCALE};
120
+ ns.updateScale(Math.max({config.card_scale_min}, current - {config.card_scale_step}));
121
+ }};
122
+
123
+ ns.increaseScale = function() {{
124
+ const slider = document.getElementById('{ids.scale_slider}');
125
+ const current = slider ? parseInt(slider.value) : {DEFAULT_CARD_SCALE};
126
+ ns.updateScale(Math.min({config.card_scale_max}, current + {config.card_scale_step}));
127
+ }};
128
+
129
+ ns.applyScale = function() {{
130
+ let val = {DEFAULT_CARD_SCALE};
131
+ try {{ const s = localStorage.getItem(_SCALE_KEY); if (s) val = parseInt(s); }} catch (e) {{}}
132
+ _applyScaleCssProperty(val);
133
+ const slider = document.getElementById('{ids.scale_slider}');
134
+ if (slider) slider.value = val;
135
+ }};
136
+ """
137
+
138
+ # %% ../../nbs/js/core.ipynb #jc000007
139
+ def _generate_card_count_mgmt_js(
140
+ ids: CardStackHtmlIds, # HTML IDs for this instance
141
+ config: CardStackConfig, # Config with count options
142
+ urls: CardStackUrls, # URL bundle (update_viewport)
143
+ ) -> str: # JS code fragment for card count management
144
+ """Generate JS for card count selector management."""
145
+ storage_key = card_count_storage_key(config.prefix)
146
+ valid_counts = ', '.join(str(c) for c in config.visible_count_options)
147
+ return f"""
148
+ // === Card Count Management ===
149
+ const _COUNT_KEY = '{storage_key}';
150
+ const _VALID_COUNTS = [{valid_counts}];
151
+
152
+ function _getStoredCount() {{
153
+ try {{
154
+ const s = localStorage.getItem(_COUNT_KEY);
155
+ if (s) {{ const c = parseInt(s); if (_VALID_COUNTS.includes(c)) return c; }}
156
+ }} catch (e) {{}}
157
+ return {DEFAULT_VISIBLE_COUNT};
158
+ }}
159
+
160
+ ns.updateCardCount = function(value) {{
161
+ const count = parseInt(value);
162
+ if (!_VALID_COUNTS.includes(count)) return;
163
+ try {{ localStorage.setItem(_COUNT_KEY, count); }} catch (e) {{}}
164
+ const cardStack = document.getElementById('{ids.card_stack}');
165
+ if (cardStack) cardStack.dataset.visibleCount = count;
166
+ if ('{urls.update_viewport}') {{
167
+ htmx.ajax('POST', '{urls.update_viewport}', {{
168
+ target: '#' + '{ids.card_stack}',
169
+ swap: 'outerHTML',
170
+ values: {{ visible_count: count }}
171
+ }});
172
+ }}
173
+ }};
174
+
175
+ function _syncCountDropdown() {{
176
+ const sel = document.getElementById('{ids.card_count_select}');
177
+ if (!sel) return;
178
+ const stored = _getStoredCount();
179
+ if (parseInt(sel.value) !== stored) sel.value = stored;
180
+ }}
181
+ """
182
+
183
+ # %% ../../nbs/js/core.ipynb #jc000009
184
+ def _generate_coordinator_js(
185
+ ids: CardStackHtmlIds, # HTML IDs for this instance
186
+ config: CardStackConfig, # Config for prefix-unique listener guards
187
+ ) -> str: # JS code fragment for master coordinator
188
+ """Generate JS for the master coordinator and HTMX listener."""
189
+ guard_var = f"_csMasterListener_{config.prefix.replace('-', '_')}"
190
+ return f"""
191
+ // === Master Coordinator ===
192
+ ns.applyAllViewportSettings = function() {{
193
+ requestAnimationFrame(function() {{
194
+ if (ns.applyWidth) ns.applyWidth();
195
+ if (ns.applyScale) ns.applyScale();
196
+ if (ns.recalculateHeight) ns.recalculateHeight();
197
+
198
+ const cs = document.getElementById('{ids.card_stack}');
199
+ if (cs) {{
200
+ cs._scrollNavSetup = false;
201
+ if (ns._setupScrollNav) ns._setupScrollNav();
202
+ }}
203
+
204
+ requestAnimationFrame(function() {{
205
+ const cs2 = document.getElementById('{ids.card_stack}');
206
+ if (cs2) cs2.style.opacity = '1';
207
+ }});
208
+ }});
209
+ }};
210
+
211
+ // === HTMX Event Listener ===
212
+ if (!window.{guard_var}) {{
213
+ window.{guard_var} = true;
214
+ document.body.addEventListener('htmx:afterSettle', function(evt) {{
215
+ const target = evt.detail.target;
216
+ if (!target) return;
217
+ const cs = document.getElementById('{ids.card_stack}');
218
+ const isCSSwap = (
219
+ target.id === '{ids.card_stack}' ||
220
+ target.id === '{ids.card_stack_inner}' ||
221
+ (cs && cs.contains(target))
222
+ );
223
+ if (isCSSwap) {{
224
+ _syncCountDropdown();
225
+ ns.applyAllViewportSettings();
226
+ }}
227
+ }});
228
+ }}
229
+
230
+ // === Initialize ===
231
+ requestAnimationFrame(function() {{
232
+ _syncCountDropdown();
233
+ setTimeout(function() {{ ns.applyAllViewportSettings(); }}, 50);
234
+ }});
235
+ """
236
+
237
+ # %% ../../nbs/js/core.ipynb #jhgisn5noqk
238
+ # Callback name constants — must match what create_card_stack_nav_actions uses
239
+ _GLOBAL_CALLBACKS = (
240
+ "jumpPageUp",
241
+ "jumpPageDown",
242
+ "jumpToFirstItem",
243
+ "jumpToLastItem",
244
+ "decreaseWidth",
245
+ "increaseWidth",
246
+ "decreaseScale",
247
+ "increaseScale",
248
+ )
249
+
250
+ def global_callback_name(
251
+ prefix: str, # Card stack instance prefix
252
+ callback: str, # Base callback name (e.g., "jumpPageUp")
253
+ ) -> str: # Global function name (e.g., "cs0_jumpPageUp")
254
+ """Generate a prefix-unique global callback name for keyboard navigation."""
255
+ return f"{prefix}_{callback}"
256
+
257
+ def _generate_global_callbacks_js(
258
+ config: CardStackConfig, # Config with prefix
259
+ ) -> str: # JS code fragment registering global wrappers
260
+ """Register global wrappers for keyboard navigation system."""
261
+ lines = [" // === Global Keyboard Callbacks ==="]
262
+ for cb in _GLOBAL_CALLBACKS:
263
+ global_name = global_callback_name(config.prefix, cb)
264
+ lines.append(f" window['{global_name}'] = function() {{ if (ns.{cb}) ns.{cb}(); }};")
265
+ return "\n".join(lines)
266
+
267
+ # %% ../../nbs/js/core.ipynb #jc000011
268
+ def generate_card_stack_js(
269
+ ids: CardStackHtmlIds, # HTML IDs for this instance
270
+ button_ids: CardStackButtonIds, # Button IDs for keyboard triggers
271
+ config: CardStackConfig, # Card stack configuration
272
+ urls: CardStackUrls, # URL bundle for routing
273
+ container_id: str = "", # Consumer's parent container ID (for height calc)
274
+ extra_scripts: Tuple[str, ...] = (), # Additional JS to include in the IIFE
275
+ ) -> Any: # Script element with all card stack JavaScript
276
+ """Compose all card stack JS into a single namespaced IIFE."""
277
+ prefix = config.prefix
278
+ extra_js = "\n".join(extra_scripts)
279
+
280
+ # Collect all fragments
281
+ viewport_js = generate_viewport_height_js(ids, container_id)
282
+ scroll_js = generate_scroll_nav_js(ids, button_ids, config.disable_scroll_in_modes)
283
+ page_nav_js = generate_page_nav_js(button_ids)
284
+ width_js = _generate_width_mgmt_js(ids, config, urls)
285
+ scale_js = _generate_scale_mgmt_js(ids, config, urls)
286
+ count_js = _generate_card_count_mgmt_js(ids, config, urls)
287
+ global_cbs_js = _generate_global_callbacks_js(config)
288
+ coordinator_js = _generate_coordinator_js(ids, config)
289
+
290
+ return Script(f"""(function() {{
291
+ window.cardStacks = window.cardStacks || {{}};
292
+ const ns = window.cardStacks['{prefix}'] = {{}};
293
+
294
+ {viewport_js}
295
+ {scroll_js}
296
+ {page_nav_js}
297
+ {width_js}
298
+ {scale_js}
299
+ {count_js}
300
+ {global_cbs_js}
301
+ {coordinator_js}
302
+ {extra_js}
303
+ }})();""")
@@ -0,0 +1,37 @@
1
+ """JavaScript generator for page jump and first/last navigation helpers."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/js/navigation.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['generate_page_nav_js']
7
+
8
+ # %% ../../nbs/js/navigation.ipynb #jn000003
9
+ from ..core.button_ids import CardStackButtonIds
10
+
11
+ # %% ../../nbs/js/navigation.ipynb #jn000005
12
+ def generate_page_nav_js(
13
+ button_ids: CardStackButtonIds, # Button IDs for navigation triggers
14
+ ) -> str: # JavaScript code fragment for page navigation
15
+ """Generate JS for page-based and first/last navigation functions."""
16
+ return f"""
17
+ // === Page Navigation ===
18
+ ns.jumpPageUp = function() {{
19
+ const btn = document.getElementById('{button_ids.nav_page_up}');
20
+ if (btn) btn.click();
21
+ }};
22
+
23
+ ns.jumpPageDown = function() {{
24
+ const btn = document.getElementById('{button_ids.nav_page_down}');
25
+ if (btn) btn.click();
26
+ }};
27
+
28
+ ns.jumpToFirstItem = function() {{
29
+ const btn = document.getElementById('{button_ids.nav_first}');
30
+ if (btn) btn.click();
31
+ }};
32
+
33
+ ns.jumpToLastItem = function() {{
34
+ const btn = document.getElementById('{button_ids.nav_last}');
35
+ if (btn) btn.click();
36
+ }};
37
+ """