cjm-fasthtml-card-stack 0.0.1__py3-none-any.whl → 0.0.2__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.2.dist-info}/METADATA +17 -9
  26. cjm_fasthtml_card_stack-0.0.2.dist-info/RECORD +36 -0
  27. {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.2.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.2.dist-info}/WHEEL +0 -0
  35. {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.2.dist-info}/entry_points.txt +0 -0
  36. {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.2.dist-info}/licenses/LICENSE +0 -0
@@ -1 +1 @@
1
- __version__ = "0.0.1"
1
+ __version__ = "0.0.2"
File without changes
@@ -0,0 +1,115 @@
1
+ """Width slider, scale slider, and card count selector components."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/components/controls.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['render_width_slider', 'render_scale_slider', 'render_card_count_select']
7
+
8
+ # %% ../../nbs/components/controls.ipynb #ct000003
9
+ from typing import Any
10
+
11
+ from fasthtml.common import Div, Span, Input, Select, Option
12
+
13
+ # DaisyUI components
14
+ from cjm_fasthtml_daisyui.components.data_input.range_slider import range_dui, range_sizes
15
+ from cjm_fasthtml_daisyui.components.data_input.select import select, select_sizes
16
+ from cjm_fasthtml_daisyui.utilities.semantic_colors import text_dui
17
+
18
+ # Tailwind utilities
19
+ from cjm_fasthtml_tailwind.utilities.flexbox_and_grid import (
20
+ flex_display, items, gap, grow
21
+ )
22
+ from cjm_fasthtml_tailwind.utilities.sizing import w
23
+ from cjm_fasthtml_tailwind.utilities.typography import font_size
24
+ from cjm_fasthtml_tailwind.core.base import combine_classes
25
+
26
+ # Local imports
27
+ from ..core.config import CardStackConfig
28
+ from ..core.html_ids import CardStackHtmlIds
29
+
30
+ # %% ../../nbs/components/controls.ipynb #ct000005
31
+ def render_width_slider(
32
+ config: CardStackConfig, # Card stack configuration
33
+ ids: CardStackHtmlIds, # HTML IDs for this instance
34
+ card_width: int = 80, # Current card width in rem
35
+ ) -> Any: # Width slider component
36
+ """Render the card stack width slider control."""
37
+ js_fn = f"window.cardStacks['{config.prefix}'].updateWidth(this.value)"
38
+
39
+ return Div(
40
+ Span(
41
+ "Narrow",
42
+ cls=combine_classes(font_size.xs, text_dui.base_content.opacity(50))
43
+ ),
44
+ Input(
45
+ type="range",
46
+ id=ids.width_slider,
47
+ min=str(config.card_width_min),
48
+ max=str(config.card_width_max),
49
+ step=str(config.card_width_step),
50
+ value=str(card_width),
51
+ cls=combine_classes(range_dui, range_sizes.xs, grow()),
52
+ oninput=js_fn
53
+ ),
54
+ Span(
55
+ "Wide",
56
+ cls=combine_classes(font_size.xs, text_dui.base_content.opacity(50))
57
+ ),
58
+ cls=combine_classes(flex_display, items.center, gap(3), w.full)
59
+ )
60
+
61
+ # %% ../../nbs/components/controls.ipynb #ct000008
62
+ def render_scale_slider(
63
+ config: CardStackConfig, # Card stack configuration
64
+ ids: CardStackHtmlIds, # HTML IDs for this instance
65
+ card_scale: int = 100, # Current scale percentage
66
+ ) -> Any: # Scale slider component
67
+ """Render the card stack scale slider control."""
68
+ js_fn = f"window.cardStacks['{config.prefix}'].updateScale(this.value)"
69
+
70
+ return Div(
71
+ Span(
72
+ "Smaller",
73
+ cls=combine_classes(font_size.xs, text_dui.base_content.opacity(50))
74
+ ),
75
+ Input(
76
+ type="range",
77
+ id=ids.scale_slider,
78
+ min=str(config.card_scale_min),
79
+ max=str(config.card_scale_max),
80
+ step=str(config.card_scale_step),
81
+ value=str(card_scale),
82
+ cls=combine_classes(range_dui, range_sizes.xs, grow()),
83
+ oninput=js_fn
84
+ ),
85
+ Span(
86
+ "Larger",
87
+ cls=combine_classes(font_size.xs, text_dui.base_content.opacity(50))
88
+ ),
89
+ cls=combine_classes(flex_display, items.center, gap(3), w.full)
90
+ )
91
+
92
+ # %% ../../nbs/components/controls.ipynb #ct000011
93
+ def render_card_count_select(
94
+ config: CardStackConfig, # Card stack configuration
95
+ ids: CardStackHtmlIds, # HTML IDs for this instance
96
+ current_count: int = 3, # Currently selected card count
97
+ ) -> Any: # Card count dropdown component
98
+ """Render the card count dropdown selector."""
99
+ js_fn = f"window.cardStacks['{config.prefix}'].updateCardCount(this.value)"
100
+
101
+ options = [
102
+ Option(
103
+ f"{count} card{'s' if count > 1 else ''}",
104
+ value=str(count),
105
+ selected=(count == current_count)
106
+ )
107
+ for count in config.visible_count_options
108
+ ]
109
+
110
+ return Select(
111
+ *options,
112
+ id=ids.card_count_select,
113
+ cls=combine_classes(select, select_sizes.sm),
114
+ onchange=js_fn
115
+ )
@@ -0,0 +1,44 @@
1
+ """Progress indicator showing the current position within the card stack."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/components/progress.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['render_progress_indicator']
7
+
8
+ # %% ../../nbs/components/progress.ipynb #p1000003
9
+ from typing import Any
10
+
11
+ from fasthtml.common import Div, Span
12
+
13
+ # DaisyUI utilities
14
+ from cjm_fasthtml_daisyui.utilities.semantic_colors import text_dui
15
+
16
+ # Tailwind utilities
17
+ from cjm_fasthtml_tailwind.utilities.typography import font_size, font_family
18
+ from cjm_fasthtml_tailwind.core.base import combine_classes
19
+
20
+ # Local imports
21
+ from ..core.html_ids import CardStackHtmlIds
22
+
23
+ # %% ../../nbs/components/progress.ipynb #p1000005
24
+ def render_progress_indicator(
25
+ focused_index: int, # Currently focused item index (0-based)
26
+ total_items: int, # Total number of items
27
+ ids: CardStackHtmlIds, # HTML IDs for this card stack instance
28
+ label: str = "Item", # Label prefix (e.g., "Item", "Segment", "Card")
29
+ oob: bool = False, # Whether to render as OOB swap
30
+ ) -> Any: # Progress indicator component
31
+ """Render position indicator showing current item in the collection."""
32
+ current = focused_index + 1
33
+
34
+ return Div(
35
+ Span(
36
+ f"{label} {current:,} of {total_items:,}",
37
+ cls=combine_classes(
38
+ font_size.sm, font_family.mono,
39
+ text_dui.base_content.opacity(70)
40
+ )
41
+ ),
42
+ id=ids.progress,
43
+ hx_swap_oob="true" if oob else None
44
+ )
@@ -0,0 +1,107 @@
1
+ """Loading, empty, and placeholder card components for the card stack viewport."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/components/states.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['render_placeholder_card', 'render_loading_state', 'render_empty_state']
7
+
8
+ # %% ../../nbs/components/states.ipynb #s1000003
9
+ from typing import Any, Literal
10
+
11
+ from fasthtml.common import Div, P, Span
12
+
13
+ # DaisyUI components
14
+ from cjm_fasthtml_daisyui.components.data_display.card import card, card_body
15
+ from cjm_fasthtml_daisyui.components.feedback.loading import loading, loading_styles, loading_sizes
16
+ from cjm_fasthtml_daisyui.utilities.semantic_colors import bg_dui, text_dui
17
+
18
+ # Tailwind utilities
19
+ from cjm_fasthtml_tailwind.utilities.borders import border, border_color
20
+ from cjm_fasthtml_tailwind.utilities.effects import shadow
21
+ from cjm_fasthtml_tailwind.utilities.flexbox_and_grid import (
22
+ flex_display, flex_direction, items, justify, grow
23
+ )
24
+ from cjm_fasthtml_tailwind.utilities.spacing import p, m
25
+ from cjm_fasthtml_tailwind.utilities.sizing import w
26
+ from cjm_fasthtml_tailwind.utilities.typography import font_size, italic, text_align
27
+ from cjm_fasthtml_tailwind.core.base import combine_classes
28
+
29
+ # Local imports
30
+ from ..core.html_ids import CardStackHtmlIds
31
+
32
+ # %% ../../nbs/components/states.ipynb #s1000005
33
+ def render_placeholder_card(
34
+ placeholder_type: Literal["start", "end"], # Which edge of the list
35
+ ) -> Any: # Placeholder card component
36
+ """Render a placeholder card for viewport edges."""
37
+ text = "Beginning" if placeholder_type == "start" else "End"
38
+
39
+ return Div(
40
+ Div(
41
+ P(
42
+ text,
43
+ cls=combine_classes(
44
+ font_size.lg, italic,
45
+ text_dui.base_content.opacity(30),
46
+ p.y(4)
47
+ )
48
+ ),
49
+ cls=combine_classes(card_body, p(4))
50
+ ),
51
+ cls=combine_classes(
52
+ card, "placeholder-card",
53
+ bg_dui.base_100.opacity(50), shadow.none,
54
+ border(2), border_color.transparent
55
+ ),
56
+ data_placeholder_type=placeholder_type
57
+ )
58
+
59
+ # %% ../../nbs/components/states.ipynb #s1000008
60
+ def render_loading_state(
61
+ ids: CardStackHtmlIds, # HTML IDs for this card stack instance
62
+ message: str = "Loading...", # Loading message text
63
+ ) -> Any: # Loading component
64
+ """Render loading state with spinner and message."""
65
+ return Div(
66
+ Div(
67
+ Span(cls=combine_classes(loading, loading_styles.spinner, loading_sizes.lg)),
68
+ P(
69
+ message,
70
+ cls=combine_classes(m.t(4), text_dui.base_content.opacity(60))
71
+ ),
72
+ cls=combine_classes(
73
+ flex_display, flex_direction.col, items.center, justify.center,
74
+ p(16)
75
+ )
76
+ ),
77
+ id=ids.loading
78
+ )
79
+
80
+ # %% ../../nbs/components/states.ipynb #s1000011
81
+ def render_empty_state(
82
+ ids: CardStackHtmlIds, # HTML IDs for this card stack instance
83
+ title: str = "No items available", # Main empty state message
84
+ subtitle: str = "", # Optional subtitle text
85
+ ) -> Any: # Empty state component
86
+ """Render empty state when no items exist."""
87
+ return Div(
88
+ P(
89
+ title,
90
+ cls=combine_classes(
91
+ text_dui.base_content.opacity(40),
92
+ text_align.center, font_size.lg
93
+ )
94
+ ),
95
+ P(
96
+ subtitle,
97
+ cls=combine_classes(
98
+ text_dui.base_content.opacity(30),
99
+ text_align.center, font_size.sm, m.t(2)
100
+ )
101
+ ) if subtitle else None,
102
+ id=ids.card_stack_empty,
103
+ cls=combine_classes(
104
+ flex_display, flex_direction.col, items.center, justify.center,
105
+ p(16), grow()
106
+ )
107
+ )
@@ -0,0 +1,320 @@
1
+ """Card stack viewport with 3-section CSS Grid layout, slot rendering,"""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/components/viewport.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['render_slot_card', 'render_all_slots_oob', 'render_viewport']
7
+
8
+ # %% ../../nbs/components/viewport.ipynb #v1000003
9
+ from typing import Any, Callable, List, Optional
10
+
11
+ from fasthtml.common import Div, Script, A, Hidden
12
+
13
+ # DaisyUI utilities
14
+ from cjm_fasthtml_daisyui.utilities.semantic_colors import ring_dui
15
+ from cjm_fasthtml_daisyui.utilities.border_radius import border_radius
16
+
17
+ # Tailwind utilities
18
+ from cjm_fasthtml_tailwind.utilities.effects import ring
19
+ from cjm_fasthtml_tailwind.utilities.flexbox_and_grid import (
20
+ flex_display, flex_direction, justify, items, gap, grid_display
21
+ )
22
+ from cjm_fasthtml_tailwind.utilities.layout import overflow, position, inset, z
23
+ from cjm_fasthtml_tailwind.utilities.interactivity import cursor
24
+ from cjm_fasthtml_tailwind.utilities.sizing import w, h
25
+ from cjm_fasthtml_tailwind.utilities.spacing import p, m
26
+ from cjm_fasthtml_tailwind.core.base import combine_classes
27
+
28
+ # Local imports
29
+ from ..core.config import CardStackConfig
30
+ from ..core.html_ids import CardStackHtmlIds
31
+ from ..core.models import CardStackState, CardRenderContext, CardStackUrls
32
+ from ..core.constants import CardRole
33
+ from ..helpers.focus import resolve_focus_slot, calculate_viewport_window
34
+ from .states import render_placeholder_card
35
+
36
+ # %% ../../nbs/components/viewport.ipynb #v1000005
37
+ def _render_mode_sync_script(
38
+ active_mode: Optional[str] = None, # Active keyboard mode name (None = navigation)
39
+ ) -> Any: # Script element that syncs keyboard mode state
40
+ """Generate script to sync keyboard navigation mode with rendered UI state."""
41
+ target_mode = active_mode if active_mode else "navigation"
42
+
43
+ return Script(f"""
44
+ (function() {{
45
+ if (typeof window.kbNav !== 'undefined') {{
46
+ const state = window.kbNav.getState();
47
+ const currentMode = state ? state.currentMode : 'navigation';
48
+ const targetMode = '{target_mode}';
49
+ if (targetMode !== 'navigation' && currentMode !== targetMode) {{
50
+ window.kbNav.enterMode(targetMode);
51
+ }} else if (targetMode === 'navigation' && currentMode !== 'navigation') {{
52
+ window.kbNav.exitMode();
53
+ }}
54
+ }}
55
+ }})();
56
+ """)
57
+
58
+ # %% ../../nbs/components/viewport.ipynb #v1000007
59
+ def _render_click_overlay(
60
+ item_index: int, # Index of the item this slot represents
61
+ urls: CardStackUrls, # URL bundle for navigation
62
+ ) -> Any: # Transparent click overlay element
63
+ """Render transparent click-to-focus overlay for a context card slot."""
64
+ return A(
65
+ cls=combine_classes(
66
+ position.absolute, inset(0), z(10),
67
+ cursor.pointer
68
+ ),
69
+ hx_post=urls.nav_to_index,
70
+ hx_vals=f'{{"target_index": {item_index}}}',
71
+ hx_swap="none"
72
+ )
73
+
74
+ # %% ../../nbs/components/viewport.ipynb #v1000009
75
+ def render_slot_card(
76
+ slot_index: int, # Index of this slot in the viewport (0-based)
77
+ focus_slot: int, # Which slot is the focused position
78
+ card_items: List[Any], # Full items list
79
+ item_index: Optional[int], # Item index for this slot (None for placeholder)
80
+ render_card: Callable, # Callback: (item, CardRenderContext) -> FT
81
+ state: CardStackState, # Current card stack state
82
+ config: CardStackConfig, # Card stack configuration
83
+ ids: CardStackHtmlIds, # HTML IDs for this instance
84
+ urls: CardStackUrls, # URL bundle for navigation
85
+ oob: bool = False, # Whether to render as OOB swap
86
+ ) -> Any: # Slot content wrapper
87
+ """Render a single card for a viewport slot."""
88
+ slot_id = ids.viewport_slot(slot_index)
89
+ is_focused = slot_index == focus_slot
90
+ card_role: CardRole = "focused" if is_focused else "context"
91
+ total_items = len(card_items)
92
+ distance = slot_index - focus_slot
93
+
94
+ # Determine placeholder type based on position relative to focus
95
+ placeholder_type = "start" if slot_index < focus_slot else "end"
96
+
97
+ # Render content
98
+ if item_index is None:
99
+ content = render_placeholder_card(placeholder_type)
100
+ else:
101
+ context = CardRenderContext(
102
+ card_role=card_role,
103
+ index=item_index,
104
+ total_items=total_items,
105
+ is_first=(item_index == 0),
106
+ is_last=(item_index == total_items - 1),
107
+ active_mode=state.active_mode,
108
+ card_scale=state.card_scale,
109
+ distance_from_focus=distance,
110
+ )
111
+ content = render_card(card_items[item_index], context)
112
+
113
+ # Focus ring styling for focused slot
114
+ focus_cls = combine_classes(
115
+ ring(3), ring_dui.primary, border_radius.box
116
+ ) if is_focused else ""
117
+
118
+ # Mode sync script in focused slot OOB updates
119
+ mode_sync = _render_mode_sync_script(state.active_mode) if (oob and is_focused) else None
120
+
121
+ # Click-to-focus overlay for context cards
122
+ click_overlay = None
123
+ if config.click_to_focus and not is_focused and item_index is not None:
124
+ click_overlay = _render_click_overlay(item_index, urls)
125
+
126
+ # Slot container
127
+ slot_cls = combine_classes(
128
+ "viewport-slot",
129
+ p(1) if not is_focused else "",
130
+ w.full,
131
+ focus_cls,
132
+ position.relative if click_overlay else ""
133
+ )
134
+
135
+ return Div(
136
+ content,
137
+ click_overlay,
138
+ mode_sync,
139
+ id=slot_id,
140
+ cls=slot_cls,
141
+ tabindex="0" if is_focused else "-1",
142
+ hx_swap_oob="innerHTML" if oob else None
143
+ )
144
+
145
+ # %% ../../nbs/components/viewport.ipynb #v1000021
146
+ def render_all_slots_oob(
147
+ card_items: List[Any], # All data items
148
+ state: CardStackState, # Current card stack state
149
+ config: CardStackConfig, # Card stack configuration
150
+ ids: CardStackHtmlIds, # HTML IDs for this instance
151
+ urls: CardStackUrls, # URL bundle for navigation
152
+ render_card: Callable, # Card renderer callback
153
+ ) -> List[Any]: # List of OOB elements (3 sections)
154
+ """Render all viewport sections with OOB swap for granular updates."""
155
+ total_items = len(card_items)
156
+ focus_slot = resolve_focus_slot(state.focus_position, state.visible_count)
157
+
158
+ viewport_indices = calculate_viewport_window(
159
+ state.focused_index, total_items, state.visible_count, state.focus_position
160
+ )
161
+
162
+ before_cards = []
163
+ focused_card = None
164
+ after_cards = []
165
+
166
+ for slot_index, item_index in enumerate(viewport_indices):
167
+ card_el = render_slot_card(
168
+ slot_index=slot_index, focus_slot=focus_slot,
169
+ card_items=card_items, item_index=item_index,
170
+ render_card=render_card, state=state,
171
+ config=config, ids=ids, urls=urls, oob=False,
172
+ )
173
+
174
+ if slot_index < focus_slot:
175
+ before_cards.append(card_el)
176
+ elif slot_index == focus_slot:
177
+ focused_card = card_el
178
+ else:
179
+ after_cards.append(card_el)
180
+
181
+ # Section styling
182
+ section_cls = lambda alignment: combine_classes(
183
+ flex_display, flex_direction.col, alignment, items.center,
184
+ w.full, gap(4), overflow.hidden
185
+ )
186
+
187
+ before_section = Div(
188
+ *before_cards,
189
+ id=ids.viewport_section_before,
190
+ cls=section_cls(justify.end),
191
+ hx_swap_oob="innerHTML"
192
+ )
193
+
194
+ mode_sync = _render_mode_sync_script(state.active_mode)
195
+ focused_section = Div(
196
+ focused_card, mode_sync,
197
+ id=ids.viewport_section_focused,
198
+ cls=combine_classes(flex_display, justify.center, items.center, w.full),
199
+ hx_swap_oob="innerHTML"
200
+ )
201
+
202
+ after_section = Div(
203
+ *after_cards,
204
+ id=ids.viewport_section_after,
205
+ cls=section_cls(justify.start),
206
+ hx_swap_oob="innerHTML"
207
+ )
208
+
209
+ return [before_section, focused_section, after_section]
210
+
211
+ # %% ../../nbs/components/viewport.ipynb #v1000031
212
+ def _grid_template_rows(
213
+ focus_slot: int, # Resolved focus slot position
214
+ visible_count: int, # Number of visible slots
215
+ ) -> 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
224
+ else:
225
+ return "1fr auto 1fr" # Standard 3-section
226
+
227
+ # %% ../../nbs/components/viewport.ipynb #v1000041
228
+ def render_viewport(
229
+ card_items: List[Any], # All data items
230
+ state: CardStackState, # Current card stack state
231
+ config: CardStackConfig, # Card stack configuration
232
+ ids: CardStackHtmlIds, # HTML IDs for this instance
233
+ urls: CardStackUrls, # URL bundle for navigation
234
+ render_card: Callable, # Card renderer callback
235
+ form_input_name: str = "focused_index", # Name for the focused index hidden input
236
+ ) -> Any: # Viewport component with 3-section layout
237
+ """Render the card stack viewport with 3-section CSS Grid layout."""
238
+ total_items = len(card_items)
239
+ focus_slot = resolve_focus_slot(state.focus_position, state.visible_count)
240
+
241
+ viewport_indices = calculate_viewport_window(
242
+ state.focused_index, total_items, state.visible_count, state.focus_position
243
+ )
244
+
245
+ before_cards = []
246
+ focused_card = None
247
+ after_cards = []
248
+
249
+ for slot_index, item_index in enumerate(viewport_indices):
250
+ card_el = render_slot_card(
251
+ slot_index=slot_index, focus_slot=focus_slot,
252
+ card_items=card_items, item_index=item_index,
253
+ render_card=render_card, state=state,
254
+ config=config, ids=ids, urls=urls, oob=False,
255
+ )
256
+
257
+ if slot_index < focus_slot:
258
+ before_cards.append(card_el)
259
+ elif slot_index == focus_slot:
260
+ focused_card = card_el
261
+ else:
262
+ after_cards.append(card_el)
263
+
264
+ # Section styling helper
265
+ section_cls = lambda alignment: combine_classes(
266
+ flex_display, flex_direction.col, alignment, items.center,
267
+ w.full, gap(4), overflow.hidden
268
+ )
269
+
270
+ before_section = Div(
271
+ *before_cards,
272
+ id=ids.viewport_section_before,
273
+ cls=section_cls(justify.end)
274
+ )
275
+
276
+ focused_section = Div(
277
+ focused_card,
278
+ id=ids.viewport_section_focused,
279
+ cls=combine_classes(flex_display, justify.center, items.center, w.full)
280
+ )
281
+
282
+ after_section = Div(
283
+ *after_cards,
284
+ id=ids.viewport_section_after,
285
+ cls=section_cls(justify.start)
286
+ )
287
+
288
+ # Grid template adapts to focus position
289
+ grid_rows = _grid_template_rows(focus_slot, state.visible_count)
290
+
291
+ inner_cls = combine_classes(grid_display, w.full, h.full, m.x.auto, gap(4))
292
+ inner_style = f"grid-template-rows: {grid_rows}; max-width: {state.card_width}rem"
293
+
294
+ outer_cls = combine_classes(w.full, p.x(2), p.y(2), overflow.hidden)
295
+
296
+ # Hidden input for focused index (needed for keyboard nav hx-include and OOB updates)
297
+ focused_input = Hidden(
298
+ id=ids.focused_index_input,
299
+ name=form_input_name,
300
+ value=str(state.focused_index),
301
+ )
302
+
303
+ return Div(
304
+ Div(
305
+ before_section,
306
+ focused_section,
307
+ after_section,
308
+ id=ids.card_stack_inner,
309
+ cls=inner_cls,
310
+ style=inner_style
311
+ ),
312
+ _render_mode_sync_script(state.active_mode),
313
+ focused_input,
314
+ id=ids.card_stack,
315
+ cls=outer_cls,
316
+ style="opacity: 0; transition: opacity 150ms ease-in",
317
+ data_focused_index=str(state.focused_index),
318
+ data_total_items=str(total_items),
319
+ data_visible_count=str(state.visible_count)
320
+ )
File without changes
@@ -0,0 +1,69 @@
1
+ """Prefix-based IDs for hidden keyboard action buttons."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/button_ids.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['CardStackButtonIds']
7
+
8
+ # %% ../../nbs/core/button_ids.ipynb #d1000003
9
+ from dataclasses import dataclass
10
+
11
+ # %% ../../nbs/core/button_ids.ipynb #d1000005
12
+ @dataclass
13
+ class CardStackButtonIds:
14
+ """Prefix-based IDs for hidden keyboard action buttons."""
15
+ prefix: str # ID prefix for this card stack instance
16
+
17
+ # --- Navigation buttons ---
18
+
19
+ @property
20
+ def nav_up(self) -> str: # Navigate to previous item
21
+ """Navigate up button."""
22
+ return f"{self.prefix}-btn-nav-up"
23
+
24
+ @property
25
+ def nav_down(self) -> str: # Navigate to next item
26
+ """Navigate down button."""
27
+ return f"{self.prefix}-btn-nav-down"
28
+
29
+ @property
30
+ def nav_first(self) -> str: # Navigate to first item
31
+ """Navigate to first item button."""
32
+ return f"{self.prefix}-btn-nav-first"
33
+
34
+ @property
35
+ def nav_last(self) -> str: # Navigate to last item
36
+ """Navigate to last item button."""
37
+ return f"{self.prefix}-btn-nav-last"
38
+
39
+ @property
40
+ def nav_page_up(self) -> str: # Page jump up
41
+ """Page up button."""
42
+ return f"{self.prefix}-btn-nav-page-up"
43
+
44
+ @property
45
+ def nav_page_down(self) -> str: # Page jump down
46
+ """Page down button."""
47
+ return f"{self.prefix}-btn-nav-page-down"
48
+
49
+ # --- Viewport control buttons ---
50
+
51
+ @property
52
+ def width_narrow(self) -> str: # Decrease viewport width
53
+ """Narrow viewport button."""
54
+ return f"{self.prefix}-btn-width-narrow"
55
+
56
+ @property
57
+ def width_widen(self) -> str: # Increase viewport width
58
+ """Widen viewport button."""
59
+ return f"{self.prefix}-btn-width-widen"
60
+
61
+ @property
62
+ def scale_decrease(self) -> str: # Decrease content scale
63
+ """Decrease scale button."""
64
+ return f"{self.prefix}-btn-scale-decrease"
65
+
66
+ @property
67
+ def scale_increase(self) -> str: # Increase content scale
68
+ """Increase scale button."""
69
+ return f"{self.prefix}-btn-scale-increase"