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,48 @@
|
|
|
1
|
+
"""Configuration dataclass for card stack initialization."""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/config.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['CardStackConfig']
|
|
7
|
+
|
|
8
|
+
# %% ../../nbs/core/config.ipynb #b1000003
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Tuple
|
|
11
|
+
|
|
12
|
+
# %% ../../nbs/core/config.ipynb #b1000005
|
|
13
|
+
_prefix_counter: int = 0
|
|
14
|
+
|
|
15
|
+
def _auto_prefix() -> str: # Unique prefix string (e.g., "cs0", "cs1")
|
|
16
|
+
"""Generate an auto-incrementing unique prefix."""
|
|
17
|
+
global _prefix_counter
|
|
18
|
+
p = f"cs{_prefix_counter}"
|
|
19
|
+
_prefix_counter += 1
|
|
20
|
+
return p
|
|
21
|
+
|
|
22
|
+
def _reset_prefix_counter() -> None:
|
|
23
|
+
"""Reset the prefix counter (for testing only)."""
|
|
24
|
+
global _prefix_counter
|
|
25
|
+
_prefix_counter = 0
|
|
26
|
+
|
|
27
|
+
# %% ../../nbs/core/config.ipynb #b1000008
|
|
28
|
+
@dataclass
|
|
29
|
+
class CardStackConfig:
|
|
30
|
+
"""Initialization-time settings for a card stack instance."""
|
|
31
|
+
prefix: str = field(default_factory=_auto_prefix) # HTML ID prefix (auto-generated if omitted)
|
|
32
|
+
|
|
33
|
+
# Card count selector options
|
|
34
|
+
visible_count_options: Tuple[int, ...] = (1, 3, 5, 7, 9) # Choices for card count dropdown
|
|
35
|
+
|
|
36
|
+
# Width slider bounds
|
|
37
|
+
card_width_min: int = 30 # Width slider minimum (rem)
|
|
38
|
+
card_width_max: int = 120 # Width slider maximum (rem)
|
|
39
|
+
card_width_step: int = 5 # Width slider step (rem)
|
|
40
|
+
|
|
41
|
+
# Scale slider bounds
|
|
42
|
+
card_scale_min: int = 50 # Scale slider minimum (%)
|
|
43
|
+
card_scale_max: int = 200 # Scale slider maximum (%)
|
|
44
|
+
card_scale_step: int = 10 # Scale slider step (%)
|
|
45
|
+
|
|
46
|
+
# Interaction
|
|
47
|
+
click_to_focus: bool = False # Whether context cards get transparent click overlay
|
|
48
|
+
disable_scroll_in_modes: Tuple[str, ...] = () # Mode names where scroll-to-nav is suppressed
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""CSS class constants, type aliases, and default values for the card stack library."""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/constants.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['CardRole', 'SCROLL_THRESHOLD', 'NAVIGATION_COOLDOWN', 'DEFAULT_VISIBLE_COUNT', 'DEFAULT_CARD_WIDTH',
|
|
7
|
+
'DEFAULT_CARD_SCALE', 'width_storage_key', 'scale_storage_key', 'card_count_storage_key']
|
|
8
|
+
|
|
9
|
+
# %% ../../nbs/core/constants.ipynb #e1000003
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
# %% ../../nbs/core/constants.ipynb #e1000005
|
|
13
|
+
CardRole = Literal["focused", "context"]
|
|
14
|
+
|
|
15
|
+
# %% ../../nbs/core/constants.ipynb #e1000008
|
|
16
|
+
SCROLL_THRESHOLD: int = 50 # Pixels of wheel delta to trigger navigation
|
|
17
|
+
NAVIGATION_COOLDOWN: int = 100 # Milliseconds between scroll-triggered navigations
|
|
18
|
+
|
|
19
|
+
# %% ../../nbs/core/constants.ipynb #e1000011
|
|
20
|
+
def width_storage_key(
|
|
21
|
+
prefix: str # Card stack instance prefix
|
|
22
|
+
) -> str: # localStorage key for card width
|
|
23
|
+
"""Generate localStorage key for card width."""
|
|
24
|
+
return f"{prefix}-card-width"
|
|
25
|
+
|
|
26
|
+
def scale_storage_key(
|
|
27
|
+
prefix: str # Card stack instance prefix
|
|
28
|
+
) -> str: # localStorage key for card scale
|
|
29
|
+
"""Generate localStorage key for card scale."""
|
|
30
|
+
return f"{prefix}-card-scale"
|
|
31
|
+
|
|
32
|
+
def card_count_storage_key(
|
|
33
|
+
prefix: str # Card stack instance prefix
|
|
34
|
+
) -> str: # localStorage key for card count
|
|
35
|
+
"""Generate localStorage key for card count."""
|
|
36
|
+
return f"{prefix}-card-count"
|
|
37
|
+
|
|
38
|
+
# %% ../../nbs/core/constants.ipynb #e1000014
|
|
39
|
+
DEFAULT_VISIBLE_COUNT: int = 3 # Default number of cards visible in viewport
|
|
40
|
+
DEFAULT_CARD_WIDTH: int = 80 # Default card stack width in rem
|
|
41
|
+
DEFAULT_CARD_SCALE: int = 100 # Default card scale percentage
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Prefix-based HTML ID generator for card stack DOM elements."""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/html_ids.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['CardStackHtmlIds']
|
|
7
|
+
|
|
8
|
+
# %% ../../nbs/core/html_ids.ipynb #c1000003
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
# %% ../../nbs/core/html_ids.ipynb #c1000005
|
|
12
|
+
@dataclass
|
|
13
|
+
class CardStackHtmlIds:
|
|
14
|
+
"""Prefix-based HTML ID generator for card stack DOM elements."""
|
|
15
|
+
prefix: str # ID prefix for this card stack instance
|
|
16
|
+
|
|
17
|
+
# --- Outer containers ---
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def card_stack(self) -> str: # Full-width scroll capture container
|
|
21
|
+
"""Outer card stack container."""
|
|
22
|
+
return f"{self.prefix}-card-stack"
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def card_stack_inner(self) -> str: # Width-constrained CSS Grid container
|
|
26
|
+
"""Inner grid container for 3-section layout."""
|
|
27
|
+
return f"{self.prefix}-card-stack-inner"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def card_stack_empty(self) -> str: # Empty state placeholder
|
|
31
|
+
"""Empty state container."""
|
|
32
|
+
return f"{self.prefix}-card-stack-empty"
|
|
33
|
+
|
|
34
|
+
# --- Viewport sections ---
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def viewport_section_before(self) -> str: # Cards before focused (1fr, justify-end)
|
|
38
|
+
"""Viewport section for context cards before focused card."""
|
|
39
|
+
return f"{self.prefix}-viewport-section-before"
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def viewport_section_focused(self) -> str: # Focused card (auto)
|
|
43
|
+
"""Viewport section for the focused card."""
|
|
44
|
+
return f"{self.prefix}-viewport-section-focused"
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def viewport_section_after(self) -> str: # Cards after focused (1fr, justify-start)
|
|
48
|
+
"""Viewport section for context cards after focused card."""
|
|
49
|
+
return f"{self.prefix}-viewport-section-after"
|
|
50
|
+
|
|
51
|
+
# --- Dynamic slot IDs ---
|
|
52
|
+
|
|
53
|
+
def viewport_slot(
|
|
54
|
+
self,
|
|
55
|
+
index: int # Slot index within the viewport
|
|
56
|
+
) -> str: # Slot element ID
|
|
57
|
+
"""ID for an individual viewport slot container."""
|
|
58
|
+
return f"{self.prefix}-viewport-slot-{index}"
|
|
59
|
+
|
|
60
|
+
# --- Controls ---
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def card_count_select(self) -> str: # Card count dropdown
|
|
64
|
+
"""Card count selector dropdown."""
|
|
65
|
+
return f"{self.prefix}-card-count-select"
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def width_slider(self) -> str: # Width range slider
|
|
69
|
+
"""Card stack width slider."""
|
|
70
|
+
return f"{self.prefix}-width-slider"
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def scale_slider(self) -> str: # Scale range slider
|
|
74
|
+
"""Card stack scale slider."""
|
|
75
|
+
return f"{self.prefix}-scale-slider"
|
|
76
|
+
|
|
77
|
+
# --- Status elements ---
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def progress(self) -> str: # Progress indicator
|
|
81
|
+
"""Progress indicator element."""
|
|
82
|
+
return f"{self.prefix}-progress"
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def loading(self) -> str: # Loading state container
|
|
86
|
+
"""Loading state container."""
|
|
87
|
+
return f"{self.prefix}-loading"
|
|
88
|
+
|
|
89
|
+
# --- Hidden inputs ---
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def focused_index_input(self) -> str: # Hidden input for keyboard nav focus recovery
|
|
93
|
+
"""Hidden input storing the focused index for HTMX submissions."""
|
|
94
|
+
return f"{self.prefix}-focused-index"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Core dataclasses for card stack state, render context, and URL routing."""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/models.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['CardStackState', 'CardRenderContext', 'CardStackUrls']
|
|
7
|
+
|
|
8
|
+
# %% ../../nbs/core/models.ipynb #a1000003
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
# %% ../../nbs/core/models.ipynb #a1000005
|
|
13
|
+
@dataclass
|
|
14
|
+
class CardStackState:
|
|
15
|
+
"""Viewport state for a card stack instance."""
|
|
16
|
+
focused_index: int = 0 # Index of focused item in the items list
|
|
17
|
+
visible_count: int = 3 # Number of card slots visible in viewport
|
|
18
|
+
card_width: int = 80 # Max width of card stack inner container in rem
|
|
19
|
+
card_scale: int = 100 # Content scale percentage (50-200)
|
|
20
|
+
active_mode: Optional[str] = None # Current interaction mode name (consumer-defined)
|
|
21
|
+
focus_position: Optional[int] = None # Slot offset for focused card (None=center, -1=bottom)
|
|
22
|
+
|
|
23
|
+
# %% ../../nbs/core/models.ipynb #a1000010
|
|
24
|
+
@dataclass
|
|
25
|
+
class CardRenderContext:
|
|
26
|
+
"""Context passed to the consumer's render_card callback."""
|
|
27
|
+
card_role: str # "focused" or "context"
|
|
28
|
+
index: int # Item's position in the full items list
|
|
29
|
+
total_items: int # Total item count
|
|
30
|
+
is_first: bool # Whether this is the first item
|
|
31
|
+
is_last: bool # Whether this is the last item
|
|
32
|
+
active_mode: Optional[str] # Current interaction mode
|
|
33
|
+
card_scale: int # Scale percentage (50-200)
|
|
34
|
+
distance_from_focus: int # Signed slot offset from focused card (0=focused)
|
|
35
|
+
|
|
36
|
+
# %% ../../nbs/core/models.ipynb #a1000015
|
|
37
|
+
@dataclass
|
|
38
|
+
class CardStackUrls:
|
|
39
|
+
"""URL bundle for card stack navigation and viewport operations."""
|
|
40
|
+
|
|
41
|
+
# Navigation URLs
|
|
42
|
+
nav_up: str = "" # Navigate to previous item
|
|
43
|
+
nav_down: str = "" # Navigate to next item
|
|
44
|
+
nav_first: str = "" # Navigate to first item
|
|
45
|
+
nav_last: str = "" # Navigate to last item
|
|
46
|
+
nav_page_up: str = "" # Page jump up
|
|
47
|
+
nav_page_down: str = "" # Page jump down
|
|
48
|
+
nav_to_index: str = "" # Navigate to specific index (click-to-focus)
|
|
49
|
+
|
|
50
|
+
# Viewport URLs
|
|
51
|
+
update_viewport: str = "" # Change visible_count (full viewport re-render)
|
|
52
|
+
save_width: str = "" # Persist card_width
|
|
53
|
+
save_scale: str = "" # Persist card_scale
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Focus position resolution, viewport window calculation, and OOB focus sync."""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/helpers/focus.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['resolve_focus_slot', 'calculate_viewport_window', 'render_focus_oob']
|
|
7
|
+
|
|
8
|
+
# %% ../../nbs/helpers/focus.ipynb #f1000003
|
|
9
|
+
from typing import List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from fasthtml.common import Hidden
|
|
12
|
+
|
|
13
|
+
from ..core.html_ids import CardStackHtmlIds
|
|
14
|
+
|
|
15
|
+
# %% ../../nbs/helpers/focus.ipynb #f1000005
|
|
16
|
+
def resolve_focus_slot(
|
|
17
|
+
focus_position: Optional[int], # Slot offset (None=center, -1=bottom, 0=top)
|
|
18
|
+
visible_count: int, # Number of visible card slots
|
|
19
|
+
) -> int: # Resolved 0-indexed slot position
|
|
20
|
+
"""Resolve focus_position to an actual slot index within the viewport."""
|
|
21
|
+
if focus_position is None:
|
|
22
|
+
return visible_count // 2
|
|
23
|
+
if focus_position < 0:
|
|
24
|
+
slot = visible_count + focus_position
|
|
25
|
+
else:
|
|
26
|
+
slot = focus_position
|
|
27
|
+
return max(0, min(slot, visible_count - 1))
|
|
28
|
+
|
|
29
|
+
# %% ../../nbs/helpers/focus.ipynb #f1000011
|
|
30
|
+
def calculate_viewport_window(
|
|
31
|
+
focused_index: int, # Index of the focused item
|
|
32
|
+
total_items: int, # Total number of items
|
|
33
|
+
visible_count: int, # Number of visible card slots
|
|
34
|
+
focus_position: Optional[int] = None, # Focus slot (None=center)
|
|
35
|
+
) -> List[Optional[int]]: # Slot indices (None for placeholder slots)
|
|
36
|
+
"""Calculate which item indices should be visible in each viewport slot."""
|
|
37
|
+
focus_slot = resolve_focus_slot(focus_position, visible_count)
|
|
38
|
+
slots_before = focus_slot
|
|
39
|
+
slots_after = visible_count - focus_slot - 1
|
|
40
|
+
|
|
41
|
+
result = []
|
|
42
|
+
|
|
43
|
+
# Slots before focused
|
|
44
|
+
for offset in range(slots_before, 0, -1):
|
|
45
|
+
idx = focused_index - offset
|
|
46
|
+
result.append(idx if idx >= 0 else None)
|
|
47
|
+
|
|
48
|
+
# Focused slot
|
|
49
|
+
result.append(focused_index if 0 <= focused_index < total_items else None)
|
|
50
|
+
|
|
51
|
+
# Slots after focused
|
|
52
|
+
for offset in range(1, slots_after + 1):
|
|
53
|
+
idx = focused_index + offset
|
|
54
|
+
result.append(idx if idx < total_items else None)
|
|
55
|
+
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
# %% ../../nbs/helpers/focus.ipynb #f1000019
|
|
59
|
+
def render_focus_oob(
|
|
60
|
+
focused_index: int, # The item index to focus
|
|
61
|
+
ids: CardStackHtmlIds, # HTML IDs for this card stack instance
|
|
62
|
+
form_input_name: str = "focused_index", # Field name for the form input
|
|
63
|
+
) -> Tuple[Hidden, ...]: # Hidden inputs with OOB swap
|
|
64
|
+
"""Render OOB hidden inputs to synchronize focus after HTMX swap."""
|
|
65
|
+
return (
|
|
66
|
+
Hidden(
|
|
67
|
+
id=ids.focused_index_input,
|
|
68
|
+
name=form_input_name,
|
|
69
|
+
value=str(focused_index),
|
|
70
|
+
hx_swap_oob="true",
|
|
71
|
+
),
|
|
72
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""Master composer for card stack JavaScript. Combines viewport height,"""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/js/core.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['global_callback_name', 'generate_card_stack_js']
|
|
7
|
+
|
|
8
|
+
# %% ../../nbs/js/core.ipynb #jc000003
|
|
9
|
+
from typing import Any, Tuple
|
|
10
|
+
|
|
11
|
+
from fasthtml.common import Script
|
|
12
|
+
|
|
13
|
+
from ..core.config import CardStackConfig
|
|
14
|
+
from ..core.html_ids import CardStackHtmlIds
|
|
15
|
+
from ..core.button_ids import CardStackButtonIds
|
|
16
|
+
from ..core.models import CardStackUrls, CardStackState
|
|
17
|
+
from cjm_fasthtml_card_stack.core.constants import (
|
|
18
|
+
width_storage_key, scale_storage_key, card_count_storage_key,
|
|
19
|
+
DEFAULT_CARD_WIDTH, DEFAULT_CARD_SCALE, DEFAULT_VISIBLE_COUNT,
|
|
20
|
+
)
|
|
21
|
+
from .viewport import generate_viewport_height_js
|
|
22
|
+
from .scroll import generate_scroll_nav_js
|
|
23
|
+
from .navigation import generate_page_nav_js
|
|
24
|
+
|
|
25
|
+
# %% ../../nbs/js/core.ipynb #jc000005
|
|
26
|
+
def _generate_width_mgmt_js(
|
|
27
|
+
ids: CardStackHtmlIds, # HTML IDs for this instance
|
|
28
|
+
config: CardStackConfig, # Config with slider bounds
|
|
29
|
+
urls: CardStackUrls, # URL bundle (save_width)
|
|
30
|
+
) -> str: # JS code fragment for width management
|
|
31
|
+
"""Generate JS for width slider management."""
|
|
32
|
+
storage_key = width_storage_key(config.prefix)
|
|
33
|
+
return f"""
|
|
34
|
+
// === Width Management ===
|
|
35
|
+
const _WIDTH_KEY = '{storage_key}';
|
|
36
|
+
let _saveWidthTimer = null;
|
|
37
|
+
|
|
38
|
+
function _saveWidthToServer(val) {{
|
|
39
|
+
if (!'{urls.save_width}') return;
|
|
40
|
+
clearTimeout(_saveWidthTimer);
|
|
41
|
+
_saveWidthTimer = setTimeout(function() {{
|
|
42
|
+
htmx.ajax('POST', '{urls.save_width}', {{
|
|
43
|
+
swap: 'none', values: {{ card_width: val }}
|
|
44
|
+
}});
|
|
45
|
+
}}, 500);
|
|
46
|
+
}}
|
|
47
|
+
|
|
48
|
+
ns.updateWidth = function(value) {{
|
|
49
|
+
const inner = document.getElementById('{ids.card_stack_inner}');
|
|
50
|
+
if (!inner) return;
|
|
51
|
+
inner.style.maxWidth = value + 'rem';
|
|
52
|
+
try {{ localStorage.setItem(_WIDTH_KEY, value); }} catch (e) {{}}
|
|
53
|
+
const slider = document.getElementById('{ids.width_slider}');
|
|
54
|
+
if (slider && parseInt(slider.value) !== parseInt(value)) slider.value = value;
|
|
55
|
+
_saveWidthToServer(value);
|
|
56
|
+
}};
|
|
57
|
+
|
|
58
|
+
ns.decreaseWidth = function() {{
|
|
59
|
+
const slider = document.getElementById('{ids.width_slider}');
|
|
60
|
+
const current = slider ? parseInt(slider.value) : {DEFAULT_CARD_WIDTH};
|
|
61
|
+
ns.updateWidth(Math.max({config.card_width_min}, current - {config.card_width_step}));
|
|
62
|
+
}};
|
|
63
|
+
|
|
64
|
+
ns.increaseWidth = function() {{
|
|
65
|
+
const slider = document.getElementById('{ids.width_slider}');
|
|
66
|
+
const current = slider ? parseInt(slider.value) : {DEFAULT_CARD_WIDTH};
|
|
67
|
+
ns.updateWidth(Math.min({config.card_width_max}, current + {config.card_width_step}));
|
|
68
|
+
}};
|
|
69
|
+
|
|
70
|
+
ns.applyWidth = function() {{
|
|
71
|
+
const inner = document.getElementById('{ids.card_stack_inner}');
|
|
72
|
+
if (!inner) return;
|
|
73
|
+
let val = {DEFAULT_CARD_WIDTH};
|
|
74
|
+
try {{ const s = localStorage.getItem(_WIDTH_KEY); if (s) val = parseInt(s); }} catch (e) {{}}
|
|
75
|
+
inner.style.maxWidth = val + 'rem';
|
|
76
|
+
const slider = document.getElementById('{ids.width_slider}');
|
|
77
|
+
if (slider) slider.value = val;
|
|
78
|
+
}};
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
# %% ../../nbs/js/core.ipynb #jc000006
|
|
82
|
+
def _generate_scale_mgmt_js(
|
|
83
|
+
ids: CardStackHtmlIds, # HTML IDs for this instance
|
|
84
|
+
config: CardStackConfig, # Config with slider bounds
|
|
85
|
+
urls: CardStackUrls, # URL bundle (save_scale)
|
|
86
|
+
) -> str: # JS code fragment for scale management
|
|
87
|
+
"""Generate JS for scale slider management."""
|
|
88
|
+
storage_key = scale_storage_key(config.prefix)
|
|
89
|
+
return f"""
|
|
90
|
+
// === Scale Management ===
|
|
91
|
+
const _SCALE_KEY = '{storage_key}';
|
|
92
|
+
let _saveScaleTimer = null;
|
|
93
|
+
|
|
94
|
+
function _saveScaleToServer(val) {{
|
|
95
|
+
if (!'{urls.save_scale}') return;
|
|
96
|
+
clearTimeout(_saveScaleTimer);
|
|
97
|
+
_saveScaleTimer = setTimeout(function() {{
|
|
98
|
+
htmx.ajax('POST', '{urls.save_scale}', {{
|
|
99
|
+
swap: 'none', values: {{ card_scale: val }}
|
|
100
|
+
}});
|
|
101
|
+
}}, 500);
|
|
102
|
+
}}
|
|
103
|
+
|
|
104
|
+
function _applyScaleCssProperty(val) {{
|
|
105
|
+
const cs = document.getElementById('{ids.card_stack}');
|
|
106
|
+
if (cs) cs.style.setProperty('--card-stack-scale', val);
|
|
107
|
+
}}
|
|
108
|
+
|
|
109
|
+
ns.updateScale = function(value) {{
|
|
110
|
+
_applyScaleCssProperty(value);
|
|
111
|
+
try {{ localStorage.setItem(_SCALE_KEY, value); }} catch (e) {{}}
|
|
112
|
+
const slider = document.getElementById('{ids.scale_slider}');
|
|
113
|
+
if (slider && parseInt(slider.value) !== parseInt(value)) slider.value = value;
|
|
114
|
+
_saveScaleToServer(value);
|
|
115
|
+
}};
|
|
116
|
+
|
|
117
|
+
ns.decreaseScale = function() {{
|
|
118
|
+
const slider = document.getElementById('{ids.scale_slider}');
|
|
119
|
+
const current = slider ? parseInt(slider.value) : {DEFAULT_CARD_SCALE};
|
|
120
|
+
ns.updateScale(Math.max({config.card_scale_min}, current - {config.card_scale_step}));
|
|
121
|
+
}};
|
|
122
|
+
|
|
123
|
+
ns.increaseScale = function() {{
|
|
124
|
+
const slider = document.getElementById('{ids.scale_slider}');
|
|
125
|
+
const current = slider ? parseInt(slider.value) : {DEFAULT_CARD_SCALE};
|
|
126
|
+
ns.updateScale(Math.min({config.card_scale_max}, current + {config.card_scale_step}));
|
|
127
|
+
}};
|
|
128
|
+
|
|
129
|
+
ns.applyScale = function() {{
|
|
130
|
+
let val = {DEFAULT_CARD_SCALE};
|
|
131
|
+
try {{ const s = localStorage.getItem(_SCALE_KEY); if (s) val = parseInt(s); }} catch (e) {{}}
|
|
132
|
+
_applyScaleCssProperty(val);
|
|
133
|
+
const slider = document.getElementById('{ids.scale_slider}');
|
|
134
|
+
if (slider) slider.value = val;
|
|
135
|
+
}};
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
# %% ../../nbs/js/core.ipynb #jc000007
|
|
139
|
+
def _generate_card_count_mgmt_js(
|
|
140
|
+
ids: CardStackHtmlIds, # HTML IDs for this instance
|
|
141
|
+
config: CardStackConfig, # Config with count options
|
|
142
|
+
urls: CardStackUrls, # URL bundle (update_viewport)
|
|
143
|
+
) -> str: # JS code fragment for card count management
|
|
144
|
+
"""Generate JS for card count selector management."""
|
|
145
|
+
storage_key = card_count_storage_key(config.prefix)
|
|
146
|
+
valid_counts = ', '.join(str(c) for c in config.visible_count_options)
|
|
147
|
+
return f"""
|
|
148
|
+
// === Card Count Management ===
|
|
149
|
+
const _COUNT_KEY = '{storage_key}';
|
|
150
|
+
const _VALID_COUNTS = [{valid_counts}];
|
|
151
|
+
|
|
152
|
+
function _getStoredCount() {{
|
|
153
|
+
try {{
|
|
154
|
+
const s = localStorage.getItem(_COUNT_KEY);
|
|
155
|
+
if (s) {{ const c = parseInt(s); if (_VALID_COUNTS.includes(c)) return c; }}
|
|
156
|
+
}} catch (e) {{}}
|
|
157
|
+
return {DEFAULT_VISIBLE_COUNT};
|
|
158
|
+
}}
|
|
159
|
+
|
|
160
|
+
ns.updateCardCount = function(value) {{
|
|
161
|
+
const count = parseInt(value);
|
|
162
|
+
if (!_VALID_COUNTS.includes(count)) return;
|
|
163
|
+
try {{ localStorage.setItem(_COUNT_KEY, count); }} catch (e) {{}}
|
|
164
|
+
const cardStack = document.getElementById('{ids.card_stack}');
|
|
165
|
+
if (cardStack) cardStack.dataset.visibleCount = count;
|
|
166
|
+
if ('{urls.update_viewport}') {{
|
|
167
|
+
htmx.ajax('POST', '{urls.update_viewport}', {{
|
|
168
|
+
target: '#' + '{ids.card_stack}',
|
|
169
|
+
swap: 'outerHTML',
|
|
170
|
+
values: {{ visible_count: count }}
|
|
171
|
+
}});
|
|
172
|
+
}}
|
|
173
|
+
}};
|
|
174
|
+
|
|
175
|
+
function _syncCountDropdown() {{
|
|
176
|
+
const sel = document.getElementById('{ids.card_count_select}');
|
|
177
|
+
if (!sel) return;
|
|
178
|
+
const stored = _getStoredCount();
|
|
179
|
+
if (parseInt(sel.value) !== stored) sel.value = stored;
|
|
180
|
+
}}
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
# %% ../../nbs/js/core.ipynb #jc000009
|
|
184
|
+
def _generate_coordinator_js(
|
|
185
|
+
ids: CardStackHtmlIds, # HTML IDs for this instance
|
|
186
|
+
config: CardStackConfig, # Config for prefix-unique listener guards
|
|
187
|
+
) -> str: # JS code fragment for master coordinator
|
|
188
|
+
"""Generate JS for the master coordinator and HTMX listener."""
|
|
189
|
+
guard_var = f"_csMasterListener_{config.prefix.replace('-', '_')}"
|
|
190
|
+
return f"""
|
|
191
|
+
// === Master Coordinator ===
|
|
192
|
+
ns.applyAllViewportSettings = function() {{
|
|
193
|
+
requestAnimationFrame(function() {{
|
|
194
|
+
if (ns.applyWidth) ns.applyWidth();
|
|
195
|
+
if (ns.applyScale) ns.applyScale();
|
|
196
|
+
if (ns.recalculateHeight) ns.recalculateHeight();
|
|
197
|
+
|
|
198
|
+
const cs = document.getElementById('{ids.card_stack}');
|
|
199
|
+
if (cs) {{
|
|
200
|
+
cs._scrollNavSetup = false;
|
|
201
|
+
if (ns._setupScrollNav) ns._setupScrollNav();
|
|
202
|
+
}}
|
|
203
|
+
|
|
204
|
+
requestAnimationFrame(function() {{
|
|
205
|
+
const cs2 = document.getElementById('{ids.card_stack}');
|
|
206
|
+
if (cs2) cs2.style.opacity = '1';
|
|
207
|
+
}});
|
|
208
|
+
}});
|
|
209
|
+
}};
|
|
210
|
+
|
|
211
|
+
// === HTMX Event Listener ===
|
|
212
|
+
if (!window.{guard_var}) {{
|
|
213
|
+
window.{guard_var} = true;
|
|
214
|
+
document.body.addEventListener('htmx:afterSettle', function(evt) {{
|
|
215
|
+
const target = evt.detail.target;
|
|
216
|
+
if (!target) return;
|
|
217
|
+
const cs = document.getElementById('{ids.card_stack}');
|
|
218
|
+
const isCSSwap = (
|
|
219
|
+
target.id === '{ids.card_stack}' ||
|
|
220
|
+
target.id === '{ids.card_stack_inner}' ||
|
|
221
|
+
(cs && cs.contains(target))
|
|
222
|
+
);
|
|
223
|
+
if (isCSSwap) {{
|
|
224
|
+
_syncCountDropdown();
|
|
225
|
+
ns.applyAllViewportSettings();
|
|
226
|
+
}}
|
|
227
|
+
}});
|
|
228
|
+
}}
|
|
229
|
+
|
|
230
|
+
// === Initialize ===
|
|
231
|
+
requestAnimationFrame(function() {{
|
|
232
|
+
_syncCountDropdown();
|
|
233
|
+
setTimeout(function() {{ ns.applyAllViewportSettings(); }}, 50);
|
|
234
|
+
}});
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
# %% ../../nbs/js/core.ipynb #jhgisn5noqk
|
|
238
|
+
# Callback name constants — must match what create_card_stack_nav_actions uses
|
|
239
|
+
_GLOBAL_CALLBACKS = (
|
|
240
|
+
"jumpPageUp",
|
|
241
|
+
"jumpPageDown",
|
|
242
|
+
"jumpToFirstItem",
|
|
243
|
+
"jumpToLastItem",
|
|
244
|
+
"decreaseWidth",
|
|
245
|
+
"increaseWidth",
|
|
246
|
+
"decreaseScale",
|
|
247
|
+
"increaseScale",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def global_callback_name(
|
|
251
|
+
prefix: str, # Card stack instance prefix
|
|
252
|
+
callback: str, # Base callback name (e.g., "jumpPageUp")
|
|
253
|
+
) -> str: # Global function name (e.g., "cs0_jumpPageUp")
|
|
254
|
+
"""Generate a prefix-unique global callback name for keyboard navigation."""
|
|
255
|
+
return f"{prefix}_{callback}"
|
|
256
|
+
|
|
257
|
+
def _generate_global_callbacks_js(
|
|
258
|
+
config: CardStackConfig, # Config with prefix
|
|
259
|
+
) -> str: # JS code fragment registering global wrappers
|
|
260
|
+
"""Register global wrappers for keyboard navigation system."""
|
|
261
|
+
lines = [" // === Global Keyboard Callbacks ==="]
|
|
262
|
+
for cb in _GLOBAL_CALLBACKS:
|
|
263
|
+
global_name = global_callback_name(config.prefix, cb)
|
|
264
|
+
lines.append(f" window['{global_name}'] = function() {{ if (ns.{cb}) ns.{cb}(); }};")
|
|
265
|
+
return "\n".join(lines)
|
|
266
|
+
|
|
267
|
+
# %% ../../nbs/js/core.ipynb #jc000011
|
|
268
|
+
def generate_card_stack_js(
|
|
269
|
+
ids: CardStackHtmlIds, # HTML IDs for this instance
|
|
270
|
+
button_ids: CardStackButtonIds, # Button IDs for keyboard triggers
|
|
271
|
+
config: CardStackConfig, # Card stack configuration
|
|
272
|
+
urls: CardStackUrls, # URL bundle for routing
|
|
273
|
+
container_id: str = "", # Consumer's parent container ID (for height calc)
|
|
274
|
+
extra_scripts: Tuple[str, ...] = (), # Additional JS to include in the IIFE
|
|
275
|
+
) -> Any: # Script element with all card stack JavaScript
|
|
276
|
+
"""Compose all card stack JS into a single namespaced IIFE."""
|
|
277
|
+
prefix = config.prefix
|
|
278
|
+
extra_js = "\n".join(extra_scripts)
|
|
279
|
+
|
|
280
|
+
# Collect all fragments
|
|
281
|
+
viewport_js = generate_viewport_height_js(ids, container_id)
|
|
282
|
+
scroll_js = generate_scroll_nav_js(ids, button_ids, config.disable_scroll_in_modes)
|
|
283
|
+
page_nav_js = generate_page_nav_js(button_ids)
|
|
284
|
+
width_js = _generate_width_mgmt_js(ids, config, urls)
|
|
285
|
+
scale_js = _generate_scale_mgmt_js(ids, config, urls)
|
|
286
|
+
count_js = _generate_card_count_mgmt_js(ids, config, urls)
|
|
287
|
+
global_cbs_js = _generate_global_callbacks_js(config)
|
|
288
|
+
coordinator_js = _generate_coordinator_js(ids, config)
|
|
289
|
+
|
|
290
|
+
return Script(f"""(function() {{
|
|
291
|
+
window.cardStacks = window.cardStacks || {{}};
|
|
292
|
+
const ns = window.cardStacks['{prefix}'] = {{}};
|
|
293
|
+
|
|
294
|
+
{viewport_js}
|
|
295
|
+
{scroll_js}
|
|
296
|
+
{page_nav_js}
|
|
297
|
+
{width_js}
|
|
298
|
+
{scale_js}
|
|
299
|
+
{count_js}
|
|
300
|
+
{global_cbs_js}
|
|
301
|
+
{coordinator_js}
|
|
302
|
+
{extra_js}
|
|
303
|
+
}})();""")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""JavaScript generator for page jump and first/last navigation helpers."""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/js/navigation.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['generate_page_nav_js']
|
|
7
|
+
|
|
8
|
+
# %% ../../nbs/js/navigation.ipynb #jn000003
|
|
9
|
+
from ..core.button_ids import CardStackButtonIds
|
|
10
|
+
|
|
11
|
+
# %% ../../nbs/js/navigation.ipynb #jn000005
|
|
12
|
+
def generate_page_nav_js(
|
|
13
|
+
button_ids: CardStackButtonIds, # Button IDs for navigation triggers
|
|
14
|
+
) -> str: # JavaScript code fragment for page navigation
|
|
15
|
+
"""Generate JS for page-based and first/last navigation functions."""
|
|
16
|
+
return f"""
|
|
17
|
+
// === Page Navigation ===
|
|
18
|
+
ns.jumpPageUp = function() {{
|
|
19
|
+
const btn = document.getElementById('{button_ids.nav_page_up}');
|
|
20
|
+
if (btn) btn.click();
|
|
21
|
+
}};
|
|
22
|
+
|
|
23
|
+
ns.jumpPageDown = function() {{
|
|
24
|
+
const btn = document.getElementById('{button_ids.nav_page_down}');
|
|
25
|
+
if (btn) btn.click();
|
|
26
|
+
}};
|
|
27
|
+
|
|
28
|
+
ns.jumpToFirstItem = function() {{
|
|
29
|
+
const btn = document.getElementById('{button_ids.nav_first}');
|
|
30
|
+
if (btn) btn.click();
|
|
31
|
+
}};
|
|
32
|
+
|
|
33
|
+
ns.jumpToLastItem = function() {{
|
|
34
|
+
const btn = document.getElementById('{button_ids.nav_last}');
|
|
35
|
+
if (btn) btn.click();
|
|
36
|
+
}};
|
|
37
|
+
"""
|