cjm-fasthtml-card-stack 0.0.1__tar.gz → 0.0.2__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 (48) hide show
  1. {cjm_fasthtml_card_stack-0.0.1/cjm_fasthtml_card_stack.egg-info → cjm_fasthtml_card_stack-0.0.2}/PKG-INFO +17 -9
  2. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/__init__.py +1 -0
  3. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/components/__init__.py +0 -0
  4. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/components/controls.py +115 -0
  5. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/components/progress.py +44 -0
  6. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/components/states.py +107 -0
  7. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/components/viewport.py +320 -0
  8. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/core/__init__.py +0 -0
  9. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/core/button_ids.py +69 -0
  10. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/core/config.py +48 -0
  11. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/core/constants.py +41 -0
  12. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/core/html_ids.py +94 -0
  13. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/core/models.py +53 -0
  14. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/helpers/__init__.py +0 -0
  15. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/helpers/focus.py +72 -0
  16. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/js/__init__.py +0 -0
  17. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/js/core.py +303 -0
  18. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/js/navigation.py +37 -0
  19. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/js/scroll.py +76 -0
  20. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/js/viewport.py +124 -0
  21. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/keyboard/__init__.py +0 -0
  22. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/keyboard/actions.py +200 -0
  23. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/routes/__init__.py +0 -0
  24. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/routes/handlers.py +158 -0
  25. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack/routes/router.py +150 -0
  26. {cjm_fasthtml_card_stack-0.0.1 → cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack.egg-info}/PKG-INFO +17 -9
  27. cjm_fasthtml_card_stack-0.0.2/cjm_fasthtml_card_stack.egg-info/SOURCES.txt +43 -0
  28. {cjm_fasthtml_card_stack-0.0.1 → cjm_fasthtml_card_stack-0.0.2}/cjm_fasthtml_card_stack.egg-info/requires.txt +2 -0
  29. {cjm_fasthtml_card_stack-0.0.1 → cjm_fasthtml_card_stack-0.0.2}/cjm_fasthtml_card_stack.egg-info/top_level.txt +1 -0
  30. cjm_fasthtml_card_stack-0.0.2/demos/__init__.py +0 -0
  31. cjm_fasthtml_card_stack-0.0.2/demos/basic.py +115 -0
  32. cjm_fasthtml_card_stack-0.0.2/demos/bottom.py +104 -0
  33. cjm_fasthtml_card_stack-0.0.2/demos/data.py +29 -0
  34. cjm_fasthtml_card_stack-0.0.2/demos/shared.py +153 -0
  35. cjm_fasthtml_card_stack-0.0.2/pyproject.toml +11 -0
  36. {cjm_fasthtml_card_stack-0.0.1 → cjm_fasthtml_card_stack-0.0.2}/settings.ini +15 -23
  37. cjm_fasthtml_card_stack-0.0.1/cjm_fasthtml_card_stack/__init__.py +0 -1
  38. cjm_fasthtml_card_stack-0.0.1/cjm_fasthtml_card_stack.egg-info/SOURCES.txt +0 -15
  39. cjm_fasthtml_card_stack-0.0.1/pyproject.toml +0 -31
  40. {cjm_fasthtml_card_stack-0.0.1 → cjm_fasthtml_card_stack-0.0.2}/LICENSE +0 -0
  41. {cjm_fasthtml_card_stack-0.0.1 → cjm_fasthtml_card_stack-0.0.2}/MANIFEST.in +0 -0
  42. {cjm_fasthtml_card_stack-0.0.1 → cjm_fasthtml_card_stack-0.0.2}/README.md +0 -0
  43. {cjm_fasthtml_card_stack-0.0.1 → cjm_fasthtml_card_stack-0.0.2}/cjm_fasthtml_card_stack/_modidx.py +0 -0
  44. {cjm_fasthtml_card_stack-0.0.1 → cjm_fasthtml_card_stack-0.0.2}/cjm_fasthtml_card_stack.egg-info/dependency_links.txt +0 -0
  45. {cjm_fasthtml_card_stack-0.0.1 → cjm_fasthtml_card_stack-0.0.2}/cjm_fasthtml_card_stack.egg-info/entry_points.txt +0 -0
  46. {cjm_fasthtml_card_stack-0.0.1 → cjm_fasthtml_card_stack-0.0.2}/cjm_fasthtml_card_stack.egg-info/not-zip-safe +0 -0
  47. {cjm_fasthtml_card_stack-0.0.1 → cjm_fasthtml_card_stack-0.0.2}/setup.cfg +0 -0
  48. {cjm_fasthtml_card_stack-0.0.1 → cjm_fasthtml_card_stack-0.0.2}/setup.py +0 -0
@@ -1,19 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-fasthtml-card-stack
3
- Version: 0.0.1
3
+ Version: 0.0.2
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
7
- Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
7
+ Author-email: 9126128+cj-mills@users.noreply.github.com
8
8
  License: Apache-2.0
9
- Project-URL: Repository, https://github.com/cj-mills/cjm-fasthtml-card-stack
10
- Project-URL: Documentation, https://cj-mills.github.io/cjm-fasthtml-card-stack
11
- Keywords: nbdev,jupyter,notebook,python
12
- Classifier: Natural Language :: English
9
+ Keywords: nbdev jupyter notebook python
10
+ Classifier: Development Status :: 4 - Beta
13
11
  Classifier: Intended Audience :: Developers
14
- Classifier: Development Status :: 3 - Alpha
15
- Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Natural Language :: English
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
17
15
  Requires-Python: >=3.12
18
16
  Description-Content-Type: text/markdown
19
17
  License-File: LICENSE
@@ -21,10 +19,20 @@ Requires-Dist: python-fasthtml
21
19
  Requires-Dist: cjm-fasthtml-tailwind
22
20
  Requires-Dist: cjm-fasthtml-daisyui
23
21
  Requires-Dist: cjm-fasthtml-keyboard-navigation
22
+ Provides-Extra: dev
24
23
  Dynamic: author
24
+ Dynamic: author-email
25
+ Dynamic: classifier
26
+ Dynamic: description
27
+ Dynamic: description-content-type
25
28
  Dynamic: home-page
29
+ Dynamic: keywords
30
+ Dynamic: license
26
31
  Dynamic: license-file
32
+ Dynamic: provides-extra
33
+ Dynamic: requires-dist
27
34
  Dynamic: requires-python
35
+ Dynamic: summary
28
36
 
29
37
  # cjm-fasthtml-card-stack
30
38
 
@@ -0,0 +1 @@
1
+ __version__ = "0.0.2"
@@ -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
+ )