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,76 @@
1
+ """JavaScript generator for scroll-to-nav conversion."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/js/scroll.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['generate_scroll_nav_js']
7
+
8
+ # %% ../../nbs/js/scroll.ipynb #js000003
9
+ from typing import Tuple
10
+
11
+ from ..core.html_ids import CardStackHtmlIds
12
+ from ..core.button_ids import CardStackButtonIds
13
+ from ..core.constants import SCROLL_THRESHOLD, NAVIGATION_COOLDOWN
14
+
15
+ # %% ../../nbs/js/scroll.ipynb #js000005
16
+ def generate_scroll_nav_js(
17
+ ids: CardStackHtmlIds, # HTML IDs for this card stack instance
18
+ button_ids: CardStackButtonIds, # Button IDs for navigation triggers
19
+ disable_in_modes: Tuple[str, ...] = (), # Mode names where scroll nav is suppressed
20
+ ) -> str: # JavaScript code fragment for scroll navigation
21
+ """Generate JS for scroll wheel to navigation conversion."""
22
+ # Build mode check
23
+ if disable_in_modes:
24
+ modes_array = ', '.join(f"'{m}'" for m in disable_in_modes)
25
+ mode_check = f"""
26
+ function isScrollDisabled() {{
27
+ if (typeof window.kbNav !== 'undefined') {{
28
+ const state = window.kbNav.getState();
29
+ const disabledModes = [{modes_array}];
30
+ return state && disabledModes.includes(state.currentMode);
31
+ }}
32
+ return false;
33
+ }}
34
+ """
35
+ mode_guard = "if (isScrollDisabled()) return;"
36
+ else:
37
+ mode_check = ""
38
+ mode_guard = ""
39
+
40
+ return f"""
41
+ // === Scroll Navigation ===
42
+ const _scrollState = {{ accumulatedDelta: 0, lastNavTime: 0 }};
43
+ const _SCROLL_THRESHOLD = {SCROLL_THRESHOLD};
44
+ const _NAV_COOLDOWN = {NAVIGATION_COOLDOWN};
45
+ {mode_check}
46
+ function setupScrollNavigation() {{
47
+ const cardStack = document.getElementById('{ids.card_stack}');
48
+ if (!cardStack || cardStack._scrollNavSetup) return;
49
+ cardStack._scrollNavSetup = true;
50
+
51
+ cardStack.addEventListener('wheel', function(evt) {{
52
+ {mode_guard}
53
+ evt.preventDefault();
54
+
55
+ const now = Date.now();
56
+ if (now - _scrollState.lastNavTime > _NAV_COOLDOWN * 2) {{
57
+ _scrollState.accumulatedDelta = 0;
58
+ }}
59
+ _scrollState.accumulatedDelta += evt.deltaY;
60
+
61
+ if (Math.abs(_scrollState.accumulatedDelta) >= _SCROLL_THRESHOLD) {{
62
+ if (now - _scrollState.lastNavTime >= _NAV_COOLDOWN) {{
63
+ const btnId = _scrollState.accumulatedDelta > 0
64
+ ? '{button_ids.nav_down}' : '{button_ids.nav_up}';
65
+ _scrollState.accumulatedDelta = 0;
66
+ _scrollState.lastNavTime = now;
67
+ const btn = document.getElementById(btnId);
68
+ if (btn) btn.click();
69
+ }}
70
+ }}
71
+ }}, {{ passive: false }});
72
+ }}
73
+
74
+ // Expose for master coordinator to re-setup after swaps
75
+ ns._setupScrollNav = setupScrollNavigation;
76
+ """
@@ -0,0 +1,124 @@
1
+ """JavaScript generator for dynamic viewport height calculation."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/js/viewport.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['generate_viewport_height_js']
7
+
8
+ # %% ../../nbs/js/viewport.ipynb #jv000003
9
+ from ..core.html_ids import CardStackHtmlIds
10
+
11
+ # %% ../../nbs/js/viewport.ipynb #jv000005
12
+ def generate_viewport_height_js(
13
+ ids: CardStackHtmlIds, # HTML IDs for this card stack instance
14
+ container_id: str = "", # Consumer's parent container ID (empty = use card stack parent)
15
+ ) -> str: # JavaScript code fragment for viewport height calculation
16
+ """Generate JS for dynamic viewport height calculation.
17
+
18
+ Uses the browser's layout engine to measure the space consumed by sibling
19
+ elements rather than summing individual heights. This naturally handles
20
+ margin collapsing regardless of the container's display type.
21
+
22
+ Strategy: temporarily collapse the card stack to 0 height, measure how
23
+ much vertical space the remaining content occupies, then set the card
24
+ stack height to fill the remaining viewport space.
25
+ """
26
+ # Build container resolution logic — guarded against null for SPA navigation
27
+ if container_id:
28
+ container_resolution = f"""
29
+ const container = document.getElementById('{container_id}');"""
30
+ else:
31
+ container_resolution = f"""
32
+ const _csEl = document.getElementById('{ids.card_stack}');
33
+ if (!_csEl) return 400;
34
+ const container = _csEl.parentElement;"""
35
+
36
+ return f"""
37
+ // === Viewport Height Calculation ===
38
+
39
+ function calculateSpaceBelowContainer(container) {{
40
+ let spaceUsedBelow = 0;
41
+ let currentElement = container;
42
+ let parent = container.parentElement;
43
+ while (parent && parent !== document.documentElement) {{
44
+ const parentStyles = getComputedStyle(parent);
45
+ spaceUsedBelow += parseFloat(parentStyles.paddingBottom) || 0;
46
+ spaceUsedBelow += parseFloat(parentStyles.borderBottomWidth) || 0;
47
+ let foundCurrent = false;
48
+ for (const sibling of parent.children) {{
49
+ if (sibling === currentElement) {{ foundCurrent = true; continue; }}
50
+ if (!foundCurrent) continue;
51
+ if (sibling.nodeType !== Node.ELEMENT_NODE) continue;
52
+ const tag = sibling.tagName;
53
+ if (tag === 'SCRIPT' || tag === 'STYLE') continue;
54
+ if (tag === 'INPUT' && sibling.type === 'hidden') continue;
55
+ const s = getComputedStyle(sibling);
56
+ if (s.display === 'none') continue;
57
+ const mt = parseFloat(s.marginTop) || 0;
58
+ const mb = parseFloat(s.marginBottom) || 0;
59
+ spaceUsedBelow += sibling.getBoundingClientRect().height + mt + mb;
60
+ }}
61
+ currentElement = parent;
62
+ parent = parent.parentElement;
63
+ }}
64
+ return spaceUsedBelow;
65
+ }}
66
+
67
+ function calculateAndSetViewportHeight() {{
68
+ {container_resolution}
69
+ const cardStack = document.getElementById('{ids.card_stack}');
70
+ const cardStackInner = document.getElementById('{ids.card_stack_inner}');
71
+ if (!container || !cardStack) return 400;
72
+
73
+ // Temporarily collapse the card stack to measure sibling space.
74
+ // Save current values to restore if something goes wrong.
75
+ const prevHeight = cardStack.style.height;
76
+ const prevMinHeight = cardStack.style.minHeight;
77
+ const prevMaxHeight = cardStack.style.maxHeight;
78
+
79
+ cardStack.style.height = '0px';
80
+ cardStack.style.minHeight = '0px';
81
+ cardStack.style.maxHeight = '0px';
82
+
83
+ // Now measure: with the card stack collapsed, the container's
84
+ // height is driven entirely by the sibling content + padding.
85
+ // The browser handles margin collapsing for us.
86
+ const containerRect = container.getBoundingClientRect();
87
+ const siblingSpace = containerRect.height;
88
+
89
+ // Restore immediately (before any repaint)
90
+ cardStack.style.height = prevHeight;
91
+ cardStack.style.minHeight = prevMinHeight;
92
+ cardStack.style.maxHeight = prevMaxHeight;
93
+
94
+ const windowHeight = window.innerHeight;
95
+ const containerTop = containerRect.top;
96
+ const spaceBelow = calculateSpaceBelowContainer(container);
97
+
98
+ const viewportHeight = Math.floor(Math.max(200,
99
+ windowHeight - containerTop - siblingSpace - spaceBelow
100
+ ));
101
+
102
+ cardStack.style.height = viewportHeight + 'px';
103
+ cardStack.style.maxHeight = viewportHeight + 'px';
104
+ cardStack.style.minHeight = viewportHeight + 'px';
105
+ if (cardStackInner) cardStackInner.style.height = viewportHeight + 'px';
106
+
107
+ return viewportHeight;
108
+ }}
109
+
110
+ function _debounce(fn, delay) {{
111
+ let tid;
112
+ return function(...args) {{ clearTimeout(tid); tid = setTimeout(() => fn.apply(this, args), delay); }};
113
+ }}
114
+
115
+ const _debouncedResize = _debounce(calculateAndSetViewportHeight, 100);
116
+
117
+ if (!window._csResizeListener_{ids.prefix.replace('-', '_')}) {{
118
+ window._csResizeListener_{ids.prefix.replace('-', '_')} = true;
119
+ window.addEventListener('resize', _debouncedResize);
120
+ }}
121
+
122
+ // Expose on namespace
123
+ ns.recalculateHeight = calculateAndSetViewportHeight;
124
+ """
File without changes
@@ -0,0 +1,200 @@
1
+ """Keyboard navigation focus zone and action factories for the card stack."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/keyboard/actions.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['create_card_stack_focus_zone', 'create_card_stack_nav_actions', 'build_card_stack_url_map',
7
+ 'render_card_stack_action_buttons']
8
+
9
+ # %% ../../nbs/keyboard/actions.ipynb #ka000003
10
+ from typing import Optional, Tuple, Dict
11
+
12
+ from fasthtml.common import Button, Div, FT
13
+ from cjm_fasthtml_tailwind.utilities.layout import display_tw
14
+
15
+ from cjm_fasthtml_keyboard_navigation.core.focus_zone import FocusZone
16
+ from cjm_fasthtml_keyboard_navigation.core.actions import KeyAction
17
+ from cjm_fasthtml_keyboard_navigation.core.navigation import ScrollOnly
18
+
19
+ from ..core.config import CardStackConfig
20
+ from ..core.html_ids import CardStackHtmlIds
21
+ from ..core.button_ids import CardStackButtonIds
22
+ from ..core.models import CardStackUrls
23
+ from ..js.core import global_callback_name
24
+
25
+ # %% ../../nbs/keyboard/actions.ipynb #ka000005
26
+ def create_card_stack_focus_zone(
27
+ ids: CardStackHtmlIds, # HTML IDs for this card stack instance
28
+ on_focus_change: Optional[str] = None, # JS callback name on focus change
29
+ hidden_input_prefix: Optional[str] = None, # Prefix for keyboard nav hidden inputs
30
+ data_attributes: Tuple[str, ...] = (), # Data attributes to track on focused items
31
+ ) -> FocusZone: # Configured focus zone for the card stack
32
+ """Create a focus zone for a card stack viewport."""
33
+ return FocusZone(
34
+ id=ids.card_stack,
35
+ item_selector="[data-card-role='focused']",
36
+ navigation=ScrollOnly(),
37
+ data_attributes=data_attributes,
38
+ zone_focus_classes=(),
39
+ item_focus_classes=(),
40
+ on_focus_change=on_focus_change or "",
41
+ hidden_input_prefix=hidden_input_prefix or f"{ids.prefix}-focused",
42
+ )
43
+
44
+ # %% ../../nbs/keyboard/actions.ipynb #ka000010
45
+ def create_card_stack_nav_actions(
46
+ zone_id: str, # Focus zone ID to restrict actions to
47
+ button_ids: CardStackButtonIds, # Button IDs for HTMX triggers
48
+ config: CardStackConfig, # Config (for prefix-unique callback names)
49
+ disable_in_modes: Tuple[str, ...] = (), # Mode names that disable navigation
50
+ ) -> Tuple[KeyAction, ...]: # Standard card stack navigation actions
51
+ """Create standard keyboard navigation actions for a card stack."""
52
+ zone_ids = (zone_id,)
53
+ not_modes = disable_in_modes if disable_in_modes else ()
54
+ prefix = config.prefix
55
+
56
+ return (
57
+ # --- Item navigation (HTMX triggers) ---
58
+ KeyAction(
59
+ key="ArrowUp",
60
+ htmx_trigger=button_ids.nav_up,
61
+ zone_ids=zone_ids,
62
+ not_modes=not_modes,
63
+ description="Previous item",
64
+ hint_group="Navigation",
65
+ ),
66
+ KeyAction(
67
+ key="ArrowDown",
68
+ htmx_trigger=button_ids.nav_down,
69
+ zone_ids=zone_ids,
70
+ not_modes=not_modes,
71
+ description="Next item",
72
+ hint_group="Navigation",
73
+ ),
74
+
75
+ # --- Page jump (JS callbacks, prefix-unique) ---
76
+ KeyAction(
77
+ key="ArrowUp",
78
+ modifiers=frozenset({"ctrl"}),
79
+ js_callback=global_callback_name(prefix, "jumpPageUp"),
80
+ zone_ids=zone_ids,
81
+ not_modes=not_modes,
82
+ description="Page up",
83
+ hint_group="Navigation",
84
+ ),
85
+ KeyAction(
86
+ key="ArrowDown",
87
+ modifiers=frozenset({"ctrl"}),
88
+ js_callback=global_callback_name(prefix, "jumpPageDown"),
89
+ zone_ids=zone_ids,
90
+ not_modes=not_modes,
91
+ description="Page down",
92
+ hint_group="Navigation",
93
+ ),
94
+
95
+ # --- First/last item (JS callbacks, prefix-unique) ---
96
+ KeyAction(
97
+ key="ArrowUp",
98
+ modifiers=frozenset({"ctrl", "shift"}),
99
+ js_callback=global_callback_name(prefix, "jumpToFirstItem"),
100
+ zone_ids=zone_ids,
101
+ not_modes=not_modes,
102
+ description="First item",
103
+ hint_group="Navigation",
104
+ ),
105
+ KeyAction(
106
+ key="ArrowDown",
107
+ modifiers=frozenset({"ctrl", "shift"}),
108
+ js_callback=global_callback_name(prefix, "jumpToLastItem"),
109
+ zone_ids=zone_ids,
110
+ not_modes=not_modes,
111
+ description="Last item",
112
+ hint_group="Navigation",
113
+ ),
114
+
115
+ # --- Width adjustment (available in any mode) ---
116
+ KeyAction(
117
+ key="[",
118
+ js_callback=global_callback_name(prefix, "decreaseWidth"),
119
+ zone_ids=zone_ids,
120
+ description="Narrower",
121
+ hint_group="View",
122
+ ),
123
+ KeyAction(
124
+ key="]",
125
+ js_callback=global_callback_name(prefix, "increaseWidth"),
126
+ zone_ids=zone_ids,
127
+ description="Wider",
128
+ hint_group="View",
129
+ ),
130
+
131
+ # --- Scale adjustment (available in any mode) ---
132
+ KeyAction(
133
+ key="-",
134
+ js_callback=global_callback_name(prefix, "decreaseScale"),
135
+ zone_ids=zone_ids,
136
+ description="Smaller",
137
+ hint_group="View",
138
+ ),
139
+ KeyAction(
140
+ key="=",
141
+ js_callback=global_callback_name(prefix, "increaseScale"),
142
+ zone_ids=zone_ids,
143
+ description="Larger",
144
+ hint_group="View",
145
+ ),
146
+ )
147
+
148
+ # %% ../../nbs/keyboard/actions.ipynb #q6nqfsne4vf
149
+ def build_card_stack_url_map(
150
+ button_ids: CardStackButtonIds, # Button IDs for this card stack instance
151
+ urls: CardStackUrls, # URL bundle for routing
152
+ ) -> Dict[str, str]: # Mapping of button ID -> route URL
153
+ """Build url_map for render_keyboard_system with all card stack navigation buttons.
154
+
155
+ Returns a dict mapping button IDs to URLs for all navigation actions:
156
+ nav_up, nav_down, nav_first, nav_last, nav_page_up, nav_page_down.
157
+
158
+ Merge with consumer's own action URLs when building the keyboard system:
159
+ url_map = {**build_card_stack_url_map(btn_ids, urls), **my_action_urls}
160
+ """
161
+ return {
162
+ button_ids.nav_up: urls.nav_up,
163
+ button_ids.nav_down: urls.nav_down,
164
+ button_ids.nav_first: urls.nav_first,
165
+ button_ids.nav_last: urls.nav_last,
166
+ button_ids.nav_page_up: urls.nav_page_up,
167
+ button_ids.nav_page_down: urls.nav_page_down,
168
+ }
169
+
170
+ # %% ../../nbs/keyboard/actions.ipynb #m9cf824czm
171
+ def render_card_stack_action_buttons(
172
+ button_ids: CardStackButtonIds, # Button IDs for this card stack instance
173
+ urls: CardStackUrls, # URL bundle for routing
174
+ ids: CardStackHtmlIds, # HTML IDs (for hx-include of focused_index_input)
175
+ ) -> 'FT': # Div containing hidden action buttons
176
+ """Render hidden HTMX buttons for JS-callback-triggered navigation actions.
177
+
178
+ Creates buttons for: page_up, page_down, first, last.
179
+ These are clicked programmatically by the card stack's JS functions.
180
+ Must be included in the DOM alongside the keyboard system's own buttons.
181
+ """
182
+ include_selector = f"#{ids.focused_index_input}"
183
+ hidden_cls = str(display_tw.hidden)
184
+
185
+ def _btn(btn_id, url):
186
+ return Button(
187
+ id=btn_id,
188
+ hx_post=url,
189
+ hx_swap="none",
190
+ hx_include=include_selector,
191
+ cls=hidden_cls,
192
+ )
193
+
194
+ return Div(
195
+ _btn(button_ids.nav_page_up, urls.nav_page_up),
196
+ _btn(button_ids.nav_page_down, urls.nav_page_down),
197
+ _btn(button_ids.nav_first, urls.nav_first),
198
+ _btn(button_ids.nav_last, urls.nav_last),
199
+ cls=hidden_cls,
200
+ )
File without changes
@@ -0,0 +1,158 @@
1
+ """Response builder functions for card stack operations (Tier 1 API)."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/routes/handlers.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['build_slots_response', 'build_nav_response', 'card_stack_navigate', 'card_stack_navigate_to_index',
7
+ 'card_stack_update_viewport', 'card_stack_save_width', 'card_stack_save_scale']
8
+
9
+ # %% ../../nbs/routes/handlers.ipynb #h1000003
10
+ from typing import Any, Callable, List, Optional, Tuple
11
+
12
+ from ..core.config import CardStackConfig
13
+ from ..core.html_ids import CardStackHtmlIds
14
+ from ..core.models import CardStackState, CardStackUrls
15
+ from ..components.viewport import render_all_slots_oob, render_viewport
16
+ from ..components.progress import render_progress_indicator
17
+ from ..helpers.focus import render_focus_oob
18
+
19
+ # %% ../../nbs/routes/handlers.ipynb #h1000005
20
+ def build_slots_response(
21
+ card_items: List[Any], # All data items
22
+ state: CardStackState, # Current card stack state
23
+ config: CardStackConfig, # Card stack configuration
24
+ ids: CardStackHtmlIds, # HTML IDs for this instance
25
+ urls: CardStackUrls, # URL bundle for navigation
26
+ render_card: Callable, # Card renderer callback
27
+ ) -> List[Any]: # OOB slot elements (3 viewport sections)
28
+ """Build OOB slot updates for the viewport sections only."""
29
+ return render_all_slots_oob(
30
+ card_items=card_items,
31
+ state=state,
32
+ config=config,
33
+ ids=ids,
34
+ urls=urls,
35
+ render_card=render_card,
36
+ )
37
+
38
+ # %% ../../nbs/routes/handlers.ipynb #h1000006
39
+ def build_nav_response(
40
+ card_items: List[Any], # All data items
41
+ state: CardStackState, # Current card stack state
42
+ config: CardStackConfig, # Card stack configuration
43
+ ids: CardStackHtmlIds, # HTML IDs for this instance
44
+ urls: CardStackUrls, # URL bundle for navigation
45
+ render_card: Callable, # Card renderer callback
46
+ progress_label: str = "Item", # Label for progress indicator
47
+ ) -> Tuple: # OOB elements (slots + progress + focus)
48
+ """Build full OOB response for navigation: slots + progress + focus inputs."""
49
+ slots_oob = build_slots_response(
50
+ card_items=card_items, state=state, config=config,
51
+ ids=ids, urls=urls, render_card=render_card,
52
+ )
53
+ progress_oob = render_progress_indicator(
54
+ state.focused_index, len(card_items), ids,
55
+ label=progress_label, oob=True,
56
+ )
57
+ focus_oob = render_focus_oob(state.focused_index, ids)
58
+ return (*slots_oob, progress_oob, *focus_oob)
59
+
60
+ # %% ../../nbs/routes/handlers.ipynb #h1000008
61
+ def card_stack_navigate(
62
+ direction: str, # "up", "down", "first", "last", "page_up", "page_down"
63
+ card_items: List[Any], # All data items
64
+ state: CardStackState, # Current card stack state (mutated in place)
65
+ config: CardStackConfig, # Card stack configuration
66
+ ids: CardStackHtmlIds, # HTML IDs for this instance
67
+ urls: CardStackUrls, # URL bundle for navigation
68
+ render_card: Callable, # Card renderer callback
69
+ progress_label: str = "Item", # Label for progress indicator
70
+ ) -> Tuple: # OOB elements (slots + progress + focus)
71
+ """Navigate to a different item. Mutates state.focused_index in place."""
72
+ total = len(card_items)
73
+ if total == 0:
74
+ return build_slots_response(
75
+ card_items, state, config, ids, urls, render_card
76
+ )
77
+
78
+ # Page jump = visible_count - 1 (one overlap card), minimum 1
79
+ page_jump = max(1, state.visible_count - 1)
80
+
81
+ direction_map = {
82
+ "up": max(0, state.focused_index - 1),
83
+ "down": min(total - 1, state.focused_index + 1),
84
+ "first": 0,
85
+ "last": total - 1,
86
+ "page_up": max(0, state.focused_index - page_jump),
87
+ "page_down": min(total - 1, state.focused_index + page_jump),
88
+ }
89
+ state.focused_index = direction_map.get(direction, state.focused_index)
90
+
91
+ return build_nav_response(
92
+ card_items, state, config, ids, urls, render_card,
93
+ progress_label=progress_label,
94
+ )
95
+
96
+ # %% ../../nbs/routes/handlers.ipynb #h1000009
97
+ def card_stack_navigate_to_index(
98
+ target_index: int, # Target item index to navigate to
99
+ card_items: List[Any], # All data items
100
+ state: CardStackState, # Current card stack state (mutated in place)
101
+ config: CardStackConfig, # Card stack configuration
102
+ ids: CardStackHtmlIds, # HTML IDs for this instance
103
+ urls: CardStackUrls, # URL bundle for navigation
104
+ render_card: Callable, # Card renderer callback
105
+ progress_label: str = "Item", # Label for progress indicator
106
+ ) -> Tuple: # OOB elements (slots + progress + focus)
107
+ """Navigate to a specific item index. Mutates state.focused_index in place."""
108
+ total = len(card_items)
109
+ if total == 0:
110
+ return build_slots_response(
111
+ card_items, state, config, ids, urls, render_card
112
+ )
113
+
114
+ state.focused_index = max(0, min(total - 1, target_index))
115
+
116
+ return build_nav_response(
117
+ card_items, state, config, ids, urls, render_card,
118
+ progress_label=progress_label,
119
+ )
120
+
121
+ # %% ../../nbs/routes/handlers.ipynb #h1000011
122
+ def card_stack_update_viewport(
123
+ visible_count: int, # New number of visible cards
124
+ card_items: List[Any], # All data items
125
+ state: CardStackState, # Current card stack state (mutated in place)
126
+ config: CardStackConfig, # Card stack configuration
127
+ ids: CardStackHtmlIds, # HTML IDs for this instance
128
+ urls: CardStackUrls, # URL bundle for navigation
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."""
132
+ state.visible_count = visible_count
133
+ return render_viewport(
134
+ card_items=card_items,
135
+ state=state,
136
+ config=config,
137
+ ids=ids,
138
+ urls=urls,
139
+ render_card=render_card,
140
+ )
141
+
142
+ # %% ../../nbs/routes/handlers.ipynb #h1000013
143
+ def card_stack_save_width(
144
+ state: CardStackState, # Current card stack state (mutated in place)
145
+ card_width: int, # Card stack width in rem
146
+ config: CardStackConfig, # Card stack configuration (for clamping bounds)
147
+ ) -> None: # No response (swap=none on client)
148
+ """Save card stack width. Mutates state.card_width in place."""
149
+ state.card_width = max(config.card_width_min, min(config.card_width_max, card_width))
150
+
151
+ # %% ../../nbs/routes/handlers.ipynb #h1000014
152
+ def card_stack_save_scale(
153
+ state: CardStackState, # Current card stack state (mutated in place)
154
+ card_scale: int, # Card stack scale percentage
155
+ config: CardStackConfig, # Card stack configuration (for clamping bounds)
156
+ ) -> None: # No response (swap=none on client)
157
+ """Save card stack scale. Mutates state.card_scale in place."""
158
+ state.card_scale = max(config.card_scale_min, min(config.card_scale_max, card_scale))