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.
- 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.2.dist-info}/METADATA +17 -9
- cjm_fasthtml_card_stack-0.0.2.dist-info/RECORD +36 -0
- {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.2.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.2.dist-info}/WHEEL +0 -0
- {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.2.dist-info}/entry_points.txt +0 -0
- {cjm_fasthtml_card_stack-0.0.1.dist-info → cjm_fasthtml_card_stack-0.0.2.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))
|