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.
- cjm_fasthtml_card_stack/__init__.py +1 -1
- cjm_fasthtml_card_stack/components/__init__.py +0 -0
- cjm_fasthtml_card_stack/components/controls.py +115 -0
- cjm_fasthtml_card_stack/components/progress.py +44 -0
- cjm_fasthtml_card_stack/components/states.py +107 -0
- cjm_fasthtml_card_stack/components/viewport.py +320 -0
- cjm_fasthtml_card_stack/core/__init__.py +0 -0
- cjm_fasthtml_card_stack/core/button_ids.py +69 -0
- cjm_fasthtml_card_stack/core/config.py +48 -0
- cjm_fasthtml_card_stack/core/constants.py +41 -0
- cjm_fasthtml_card_stack/core/html_ids.py +94 -0
- cjm_fasthtml_card_stack/core/models.py +53 -0
- cjm_fasthtml_card_stack/helpers/__init__.py +0 -0
- cjm_fasthtml_card_stack/helpers/focus.py +72 -0
- cjm_fasthtml_card_stack/js/__init__.py +0 -0
- cjm_fasthtml_card_stack/js/core.py +303 -0
- cjm_fasthtml_card_stack/js/navigation.py +37 -0
- cjm_fasthtml_card_stack/js/scroll.py +76 -0
- cjm_fasthtml_card_stack/js/viewport.py +124 -0
- cjm_fasthtml_card_stack/keyboard/__init__.py +0 -0
- cjm_fasthtml_card_stack/keyboard/actions.py +200 -0
- cjm_fasthtml_card_stack/routes/__init__.py +0 -0
- cjm_fasthtml_card_stack/routes/handlers.py +158 -0
- cjm_fasthtml_card_stack/routes/router.py +150 -0
- {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.3.dist-info}/METADATA +32 -24
- cjm_fasthtml_card_stack-0.0.3.dist-info/RECORD +36 -0
- {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.3.dist-info}/top_level.txt +1 -0
- demos/__init__.py +0 -0
- demos/basic.py +115 -0
- demos/bottom.py +104 -0
- demos/data.py +29 -0
- demos/shared.py +153 -0
- cjm_fasthtml_card_stack-0.0.1.dist-info/RECORD +0 -8
- {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.3.dist-info}/WHEEL +0 -0
- {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.3.dist-info}/entry_points.txt +0 -0
- {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.0.
|
|
1
|
+
__version__ = "0.0.3"
|
|
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 shadow_dui
|
|
15
|
+
from cjm_fasthtml_daisyui.utilities.border_radius import border_radius
|
|
16
|
+
|
|
17
|
+
# Tailwind utilities
|
|
18
|
+
from cjm_fasthtml_tailwind.utilities.effects import shadow
|
|
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 shadow styling for focused slot
|
|
114
|
+
focus_cls = combine_classes(
|
|
115
|
+
shadow.lg, shadow_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, p.x(2), p.b(4)),
|
|
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, p.x(2), p.b(4))
|
|
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"
|