cjm-fasthtml-token-selector 0.0.1__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_token_selector/__init__.py +1 -0
- cjm_fasthtml_token_selector/_modidx.py +104 -0
- cjm_fasthtml_token_selector/components/__init__.py +0 -0
- cjm_fasthtml_token_selector/components/inputs.py +46 -0
- cjm_fasthtml_token_selector/components/tokens.py +217 -0
- cjm_fasthtml_token_selector/core/__init__.py +0 -0
- cjm_fasthtml_token_selector/core/config.py +57 -0
- cjm_fasthtml_token_selector/core/constants.py +50 -0
- cjm_fasthtml_token_selector/core/html_ids.py +51 -0
- cjm_fasthtml_token_selector/core/models.py +37 -0
- cjm_fasthtml_token_selector/helpers/__init__.py +0 -0
- cjm_fasthtml_token_selector/helpers/tokenizer.py +76 -0
- cjm_fasthtml_token_selector/js/__init__.py +0 -0
- cjm_fasthtml_token_selector/js/core.py +181 -0
- cjm_fasthtml_token_selector/js/display.py +146 -0
- cjm_fasthtml_token_selector/js/navigation.py +228 -0
- cjm_fasthtml_token_selector/js/repeat.py +119 -0
- cjm_fasthtml_token_selector/keyboard/__init__.py +0 -0
- cjm_fasthtml_token_selector/keyboard/actions.py +157 -0
- cjm_fasthtml_token_selector-0.0.1.dist-info/METADATA +760 -0
- cjm_fasthtml_token_selector-0.0.1.dist-info/RECORD +31 -0
- cjm_fasthtml_token_selector-0.0.1.dist-info/WHEEL +5 -0
- cjm_fasthtml_token_selector-0.0.1.dist-info/entry_points.txt +2 -0
- cjm_fasthtml_token_selector-0.0.1.dist-info/licenses/LICENSE +201 -0
- cjm_fasthtml_token_selector-0.0.1.dist-info/top_level.txt +2 -0
- demos/__init__.py +0 -0
- demos/data.py +17 -0
- demos/gap.py +93 -0
- demos/shared.py +148 -0
- demos/span.py +92 -0
- demos/word.py +88 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Autogenerated by nbdev
|
|
2
|
+
|
|
3
|
+
d = { 'settings': { 'branch': 'main',
|
|
4
|
+
'doc_baseurl': '/cjm-fasthtml-token-selector',
|
|
5
|
+
'doc_host': 'https://cj-mills.github.io',
|
|
6
|
+
'git_url': 'https://github.com/cj-mills/cjm-fasthtml-token-selector',
|
|
7
|
+
'lib_path': 'cjm_fasthtml_token_selector'},
|
|
8
|
+
'syms': { 'cjm_fasthtml_token_selector.components.inputs': { 'cjm_fasthtml_token_selector.components.inputs.build_include_selector': ( 'components/inputs.html#build_include_selector',
|
|
9
|
+
'cjm_fasthtml_token_selector/components/inputs.py'),
|
|
10
|
+
'cjm_fasthtml_token_selector.components.inputs.render_hidden_inputs': ( 'components/inputs.html#render_hidden_inputs',
|
|
11
|
+
'cjm_fasthtml_token_selector/components/inputs.py')},
|
|
12
|
+
'cjm_fasthtml_token_selector.components.tokens': { 'cjm_fasthtml_token_selector.components.tokens._build_token_render_context': ( 'components/tokens.html#_build_token_render_context',
|
|
13
|
+
'cjm_fasthtml_token_selector/components/tokens.py'),
|
|
14
|
+
'cjm_fasthtml_token_selector.components.tokens._is_token_selected': ( 'components/tokens.html#_is_token_selected',
|
|
15
|
+
'cjm_fasthtml_token_selector/components/tokens.py'),
|
|
16
|
+
'cjm_fasthtml_token_selector.components.tokens._render_caret_indicator': ( 'components/tokens.html#_render_caret_indicator',
|
|
17
|
+
'cjm_fasthtml_token_selector/components/tokens.py'),
|
|
18
|
+
'cjm_fasthtml_token_selector.components.tokens.render_end_token': ( 'components/tokens.html#render_end_token',
|
|
19
|
+
'cjm_fasthtml_token_selector/components/tokens.py'),
|
|
20
|
+
'cjm_fasthtml_token_selector.components.tokens.render_token': ( 'components/tokens.html#render_token',
|
|
21
|
+
'cjm_fasthtml_token_selector/components/tokens.py'),
|
|
22
|
+
'cjm_fasthtml_token_selector.components.tokens.render_token_grid': ( 'components/tokens.html#render_token_grid',
|
|
23
|
+
'cjm_fasthtml_token_selector/components/tokens.py')},
|
|
24
|
+
'cjm_fasthtml_token_selector.core.config': { 'cjm_fasthtml_token_selector.core.config.TokenSelectorConfig': ( 'core/config.html#tokenselectorconfig',
|
|
25
|
+
'cjm_fasthtml_token_selector/core/config.py'),
|
|
26
|
+
'cjm_fasthtml_token_selector.core.config._auto_prefix': ( 'core/config.html#_auto_prefix',
|
|
27
|
+
'cjm_fasthtml_token_selector/core/config.py'),
|
|
28
|
+
'cjm_fasthtml_token_selector.core.config._reset_prefix_counter': ( 'core/config.html#_reset_prefix_counter',
|
|
29
|
+
'cjm_fasthtml_token_selector/core/config.py')},
|
|
30
|
+
'cjm_fasthtml_token_selector.core.constants': {},
|
|
31
|
+
'cjm_fasthtml_token_selector.core.html_ids': { 'cjm_fasthtml_token_selector.core.html_ids.TokenSelectorHtmlIds': ( 'core/html_ids.html#tokenselectorhtmlids',
|
|
32
|
+
'cjm_fasthtml_token_selector/core/html_ids.py'),
|
|
33
|
+
'cjm_fasthtml_token_selector.core.html_ids.TokenSelectorHtmlIds.anchor_input': ( 'core/html_ids.html#tokenselectorhtmlids.anchor_input',
|
|
34
|
+
'cjm_fasthtml_token_selector/core/html_ids.py'),
|
|
35
|
+
'cjm_fasthtml_token_selector.core.html_ids.TokenSelectorHtmlIds.anchor_name': ( 'core/html_ids.html#tokenselectorhtmlids.anchor_name',
|
|
36
|
+
'cjm_fasthtml_token_selector/core/html_ids.py'),
|
|
37
|
+
'cjm_fasthtml_token_selector.core.html_ids.TokenSelectorHtmlIds.container': ( 'core/html_ids.html#tokenselectorhtmlids.container',
|
|
38
|
+
'cjm_fasthtml_token_selector/core/html_ids.py'),
|
|
39
|
+
'cjm_fasthtml_token_selector.core.html_ids.TokenSelectorHtmlIds.focus_input': ( 'core/html_ids.html#tokenselectorhtmlids.focus_input',
|
|
40
|
+
'cjm_fasthtml_token_selector/core/html_ids.py'),
|
|
41
|
+
'cjm_fasthtml_token_selector.core.html_ids.TokenSelectorHtmlIds.focus_name': ( 'core/html_ids.html#tokenselectorhtmlids.focus_name',
|
|
42
|
+
'cjm_fasthtml_token_selector/core/html_ids.py'),
|
|
43
|
+
'cjm_fasthtml_token_selector.core.html_ids.TokenSelectorHtmlIds.token': ( 'core/html_ids.html#tokenselectorhtmlids.token',
|
|
44
|
+
'cjm_fasthtml_token_selector/core/html_ids.py'),
|
|
45
|
+
'cjm_fasthtml_token_selector.core.html_ids.TokenSelectorHtmlIds.token_grid': ( 'core/html_ids.html#tokenselectorhtmlids.token_grid',
|
|
46
|
+
'cjm_fasthtml_token_selector/core/html_ids.py')},
|
|
47
|
+
'cjm_fasthtml_token_selector.core.models': { 'cjm_fasthtml_token_selector.core.models.Token': ( 'core/models.html#token',
|
|
48
|
+
'cjm_fasthtml_token_selector/core/models.py'),
|
|
49
|
+
'cjm_fasthtml_token_selector.core.models.TokenRenderContext': ( 'core/models.html#tokenrendercontext',
|
|
50
|
+
'cjm_fasthtml_token_selector/core/models.py'),
|
|
51
|
+
'cjm_fasthtml_token_selector.core.models.TokenSelectorState': ( 'core/models.html#tokenselectorstate',
|
|
52
|
+
'cjm_fasthtml_token_selector/core/models.py')},
|
|
53
|
+
'cjm_fasthtml_token_selector.helpers.tokenizer': { 'cjm_fasthtml_token_selector.helpers.tokenizer.count_tokens': ( 'helpers/tokenizer.html#count_tokens',
|
|
54
|
+
'cjm_fasthtml_token_selector/helpers/tokenizer.py'),
|
|
55
|
+
'cjm_fasthtml_token_selector.helpers.tokenizer.get_token_list': ( 'helpers/tokenizer.html#get_token_list',
|
|
56
|
+
'cjm_fasthtml_token_selector/helpers/tokenizer.py'),
|
|
57
|
+
'cjm_fasthtml_token_selector.helpers.tokenizer.token_index_to_char_position': ( 'helpers/tokenizer.html#token_index_to_char_position',
|
|
58
|
+
'cjm_fasthtml_token_selector/helpers/tokenizer.py'),
|
|
59
|
+
'cjm_fasthtml_token_selector.helpers.tokenizer.tokenize': ( 'helpers/tokenizer.html#tokenize',
|
|
60
|
+
'cjm_fasthtml_token_selector/helpers/tokenizer.py')},
|
|
61
|
+
'cjm_fasthtml_token_selector.js.core': { 'cjm_fasthtml_token_selector.js.core._generate_activation_js': ( 'js/core.html#_generate_activation_js',
|
|
62
|
+
'cjm_fasthtml_token_selector/js/core.py'),
|
|
63
|
+
'cjm_fasthtml_token_selector.js.core._generate_global_callbacks_js': ( 'js/core.html#_generate_global_callbacks_js',
|
|
64
|
+
'cjm_fasthtml_token_selector/js/core.py'),
|
|
65
|
+
'cjm_fasthtml_token_selector.js.core._generate_on_change_js': ( 'js/core.html#_generate_on_change_js',
|
|
66
|
+
'cjm_fasthtml_token_selector/js/core.py'),
|
|
67
|
+
'cjm_fasthtml_token_selector.js.core._generate_settle_handler_js': ( 'js/core.html#_generate_settle_handler_js',
|
|
68
|
+
'cjm_fasthtml_token_selector/js/core.py'),
|
|
69
|
+
'cjm_fasthtml_token_selector.js.core._generate_state_init_js': ( 'js/core.html#_generate_state_init_js',
|
|
70
|
+
'cjm_fasthtml_token_selector/js/core.py'),
|
|
71
|
+
'cjm_fasthtml_token_selector.js.core.generate_token_selector_js': ( 'js/core.html#generate_token_selector_js',
|
|
72
|
+
'cjm_fasthtml_token_selector/js/core.py'),
|
|
73
|
+
'cjm_fasthtml_token_selector.js.core.global_callback_name': ( 'js/core.html#global_callback_name',
|
|
74
|
+
'cjm_fasthtml_token_selector/js/core.py')},
|
|
75
|
+
'cjm_fasthtml_token_selector.js.display': { 'cjm_fasthtml_token_selector.js.display._generate_gap_display_js': ( 'js/display.html#_generate_gap_display_js',
|
|
76
|
+
'cjm_fasthtml_token_selector/js/display.py'),
|
|
77
|
+
'cjm_fasthtml_token_selector.js.display._generate_span_display_js': ( 'js/display.html#_generate_span_display_js',
|
|
78
|
+
'cjm_fasthtml_token_selector/js/display.py'),
|
|
79
|
+
'cjm_fasthtml_token_selector.js.display._generate_update_inputs_js': ( 'js/display.html#_generate_update_inputs_js',
|
|
80
|
+
'cjm_fasthtml_token_selector/js/display.py'),
|
|
81
|
+
'cjm_fasthtml_token_selector.js.display._generate_word_display_js': ( 'js/display.html#_generate_word_display_js',
|
|
82
|
+
'cjm_fasthtml_token_selector/js/display.py'),
|
|
83
|
+
'cjm_fasthtml_token_selector.js.display.generate_display_js': ( 'js/display.html#generate_display_js',
|
|
84
|
+
'cjm_fasthtml_token_selector/js/display.py')},
|
|
85
|
+
'cjm_fasthtml_token_selector.js.navigation': { 'cjm_fasthtml_token_selector.js.navigation._generate_gap_nav_js': ( 'js/navigation.html#_generate_gap_nav_js',
|
|
86
|
+
'cjm_fasthtml_token_selector/js/navigation.py'),
|
|
87
|
+
'cjm_fasthtml_token_selector.js.navigation._generate_span_nav_js': ( 'js/navigation.html#_generate_span_nav_js',
|
|
88
|
+
'cjm_fasthtml_token_selector/js/navigation.py'),
|
|
89
|
+
'cjm_fasthtml_token_selector.js.navigation._generate_word_nav_js': ( 'js/navigation.html#_generate_word_nav_js',
|
|
90
|
+
'cjm_fasthtml_token_selector/js/navigation.py'),
|
|
91
|
+
'cjm_fasthtml_token_selector.js.navigation.generate_navigation_js': ( 'js/navigation.html#generate_navigation_js',
|
|
92
|
+
'cjm_fasthtml_token_selector/js/navigation.py')},
|
|
93
|
+
'cjm_fasthtml_token_selector.js.repeat': { 'cjm_fasthtml_token_selector.js.repeat._generate_movement_dispatch_js': ( 'js/repeat.html#_generate_movement_dispatch_js',
|
|
94
|
+
'cjm_fasthtml_token_selector/js/repeat.py'),
|
|
95
|
+
'cjm_fasthtml_token_selector.js.repeat.generate_key_repeat_js': ( 'js/repeat.html#generate_key_repeat_js',
|
|
96
|
+
'cjm_fasthtml_token_selector/js/repeat.py')},
|
|
97
|
+
'cjm_fasthtml_token_selector.keyboard.actions': { 'cjm_fasthtml_token_selector.keyboard.actions.build_token_selector_url_map': ( 'keyboard/actions.html#build_token_selector_url_map',
|
|
98
|
+
'cjm_fasthtml_token_selector/keyboard/actions.py'),
|
|
99
|
+
'cjm_fasthtml_token_selector.keyboard.actions.create_token_nav_actions': ( 'keyboard/actions.html#create_token_nav_actions',
|
|
100
|
+
'cjm_fasthtml_token_selector/keyboard/actions.py'),
|
|
101
|
+
'cjm_fasthtml_token_selector.keyboard.actions.create_token_selector_mode': ( 'keyboard/actions.html#create_token_selector_mode',
|
|
102
|
+
'cjm_fasthtml_token_selector/keyboard/actions.py'),
|
|
103
|
+
'cjm_fasthtml_token_selector.keyboard.actions.render_token_action_buttons': ( 'keyboard/actions.html#render_token_action_buttons',
|
|
104
|
+
'cjm_fasthtml_token_selector/keyboard/actions.py')}}}
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Hidden input rendering for HTMX state sync."""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/components/inputs.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['render_hidden_inputs', 'build_include_selector']
|
|
7
|
+
|
|
8
|
+
# %% ../../nbs/components/inputs.ipynb #330fdfc6
|
|
9
|
+
from typing import Any, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from fasthtml.common import Div, Hidden
|
|
12
|
+
|
|
13
|
+
from ..core.models import TokenSelectorState
|
|
14
|
+
from ..core.html_ids import TokenSelectorHtmlIds
|
|
15
|
+
|
|
16
|
+
# %% ../../nbs/components/inputs.ipynb #2ebc5b73
|
|
17
|
+
def render_hidden_inputs(
|
|
18
|
+
ids:TokenSelectorHtmlIds, # HTML IDs for this instance
|
|
19
|
+
state:Optional[TokenSelectorState] = None, # current state
|
|
20
|
+
oob:bool = False, # render with hx-swap-oob
|
|
21
|
+
) -> Any: # tuple of anchor and focus hidden inputs
|
|
22
|
+
"""Render hidden inputs for HTMX form submission."""
|
|
23
|
+
anchor = state.anchor if state else 0
|
|
24
|
+
focus = state.focus if state else 0
|
|
25
|
+
|
|
26
|
+
anchor_input = Hidden(
|
|
27
|
+
value=str(anchor),
|
|
28
|
+
id=ids.anchor_input,
|
|
29
|
+
name=ids.anchor_name,
|
|
30
|
+
**(dict(hx_swap_oob="true") if oob else {}),
|
|
31
|
+
)
|
|
32
|
+
focus_input = Hidden(
|
|
33
|
+
value=str(focus),
|
|
34
|
+
id=ids.focus_input,
|
|
35
|
+
name=ids.focus_name,
|
|
36
|
+
**(dict(hx_swap_oob="true") if oob else {}),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return (anchor_input, focus_input)
|
|
40
|
+
|
|
41
|
+
# %% ../../nbs/components/inputs.ipynb #bd8bb783
|
|
42
|
+
def build_include_selector(
|
|
43
|
+
ids:TokenSelectorHtmlIds, # HTML IDs for this instance
|
|
44
|
+
) -> str: # CSS selector string for hx_include
|
|
45
|
+
"""Build a CSS selector for including anchor and focus inputs in HTMX requests."""
|
|
46
|
+
return f"#{ids.anchor_input}, #{ids.focus_input}"
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Token grid rendering for all three selection modes (gap, word, span)."""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/components/tokens.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['render_token', 'render_end_token', 'render_token_grid']
|
|
7
|
+
|
|
8
|
+
# %% ../../nbs/components/tokens.ipynb #99d42647
|
|
9
|
+
from typing import Any, Callable, List, Optional
|
|
10
|
+
|
|
11
|
+
from fasthtml.common import Div, Span
|
|
12
|
+
|
|
13
|
+
from cjm_fasthtml_daisyui.utilities.semantic_colors import bg_dui
|
|
14
|
+
from cjm_fasthtml_daisyui.utilities.border_radius import border_radius
|
|
15
|
+
|
|
16
|
+
from cjm_fasthtml_tailwind.utilities.spacing import p
|
|
17
|
+
from cjm_fasthtml_tailwind.utilities.sizing import w, h
|
|
18
|
+
from cjm_fasthtml_tailwind.utilities.typography import font_size, italic
|
|
19
|
+
from cjm_fasthtml_tailwind.utilities.layout import position, top, left
|
|
20
|
+
from cjm_fasthtml_tailwind.utilities.effects import opacity
|
|
21
|
+
from cjm_fasthtml_tailwind.utilities.interactivity import cursor, select
|
|
22
|
+
from cjm_fasthtml_tailwind.utilities.transitions_and_animation import animate
|
|
23
|
+
from cjm_fasthtml_tailwind.utilities.transforms import translate
|
|
24
|
+
from cjm_fasthtml_tailwind.utilities.flexbox_and_grid import flex_display, flex_wrap, gap
|
|
25
|
+
from cjm_fasthtml_tailwind.core.base import combine_classes
|
|
26
|
+
|
|
27
|
+
from cjm_fasthtml_token_selector.core.constants import (
|
|
28
|
+
CARET_INDICATOR_CLS, OPACITY_50_CLS, HIGHLIGHT_CLS,
|
|
29
|
+
)
|
|
30
|
+
from ..core.config import TokenSelectorConfig
|
|
31
|
+
from ..core.models import Token, TokenRenderContext, TokenSelectorState
|
|
32
|
+
from ..core.html_ids import TokenSelectorHtmlIds
|
|
33
|
+
|
|
34
|
+
# %% ../../nbs/components/tokens.ipynb #0df5a9a9
|
|
35
|
+
def _is_token_selected(
|
|
36
|
+
index:int, # token index
|
|
37
|
+
mode:str, # selection mode
|
|
38
|
+
anchor:int, # anchor position
|
|
39
|
+
focus:int, # focus position
|
|
40
|
+
) -> bool: # whether the token is in the selection
|
|
41
|
+
"""Check if a token at the given index is within the current selection."""
|
|
42
|
+
if mode == "gap":
|
|
43
|
+
return False # gap mode selects positions between tokens, not tokens themselves
|
|
44
|
+
elif mode == "word":
|
|
45
|
+
return index == anchor
|
|
46
|
+
elif mode == "span":
|
|
47
|
+
lo, hi = min(anchor, focus), max(anchor, focus)
|
|
48
|
+
return lo <= index <= hi
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
# %% ../../nbs/components/tokens.ipynb #2b37bdad
|
|
52
|
+
def _build_token_render_context(
|
|
53
|
+
token:Token, # token being rendered
|
|
54
|
+
mode:str, # selection mode
|
|
55
|
+
anchor:int, # anchor position
|
|
56
|
+
focus:int, # focus position
|
|
57
|
+
) -> TokenRenderContext: # render context for styling callback
|
|
58
|
+
"""Build a render context for the per-token styling callback."""
|
|
59
|
+
return TokenRenderContext(
|
|
60
|
+
token=token,
|
|
61
|
+
is_selected=_is_token_selected(token.index, mode, anchor, focus),
|
|
62
|
+
is_anchor=(token.index == anchor),
|
|
63
|
+
is_focus=(token.index == focus),
|
|
64
|
+
selection_mode=mode,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# %% ../../nbs/components/tokens.ipynb #cd35fb5a
|
|
68
|
+
def _render_caret_indicator() -> Any: # caret indicator Div element
|
|
69
|
+
"""Render the pulsing caret indicator bar."""
|
|
70
|
+
return Div(cls=CARET_INDICATOR_CLS)
|
|
71
|
+
|
|
72
|
+
# %% ../../nbs/components/tokens.ipynb #a7c9db4c
|
|
73
|
+
def render_token(
|
|
74
|
+
token:Token, # token to render
|
|
75
|
+
config:TokenSelectorConfig, # config for this instance
|
|
76
|
+
ids:TokenSelectorHtmlIds, # HTML IDs
|
|
77
|
+
state:Optional[TokenSelectorState] = None, # current state for highlighting
|
|
78
|
+
style_callback:Optional[Callable] = None, # (TokenRenderContext) -> str for extra CSS
|
|
79
|
+
read_only:bool = False, # disable interaction
|
|
80
|
+
) -> Any: # Span element for this token
|
|
81
|
+
"""Render a single interactive word token."""
|
|
82
|
+
anchor = state.anchor if state else 0
|
|
83
|
+
focus = state.focus if state else 0
|
|
84
|
+
mode = config.selection_mode
|
|
85
|
+
is_read_only = read_only or config.read_only
|
|
86
|
+
idx = token.index
|
|
87
|
+
|
|
88
|
+
# Determine visual state
|
|
89
|
+
selected = _is_token_selected(idx, mode, anchor, focus)
|
|
90
|
+
show_caret = (mode == "gap" and idx == anchor and state is not None)
|
|
91
|
+
dim = (mode == "gap" and idx < anchor and state is not None)
|
|
92
|
+
|
|
93
|
+
# Build token content
|
|
94
|
+
children = []
|
|
95
|
+
if show_caret:
|
|
96
|
+
children.append(_render_caret_indicator())
|
|
97
|
+
children.append(Span(token.text))
|
|
98
|
+
|
|
99
|
+
# Build CSS classes
|
|
100
|
+
cls_parts = [
|
|
101
|
+
"token", position.relative,
|
|
102
|
+
p.x(1), p.y(0.5),
|
|
103
|
+
border_radius.selector, cursor.pointer, select.none,
|
|
104
|
+
]
|
|
105
|
+
if dim:
|
|
106
|
+
cls_parts.append(OPACITY_50_CLS)
|
|
107
|
+
if selected:
|
|
108
|
+
cls_parts.append(HIGHLIGHT_CLS)
|
|
109
|
+
if not is_read_only:
|
|
110
|
+
cls_parts.append(bg_dui.base_200.hover)
|
|
111
|
+
|
|
112
|
+
# Per-token styling callback
|
|
113
|
+
extra_cls = ""
|
|
114
|
+
if style_callback:
|
|
115
|
+
ctx = _build_token_render_context(token, mode, anchor, focus)
|
|
116
|
+
extra_cls = style_callback(ctx) or ""
|
|
117
|
+
if extra_cls:
|
|
118
|
+
cls_parts.append(extra_cls)
|
|
119
|
+
|
|
120
|
+
# Click handler
|
|
121
|
+
onclick = None
|
|
122
|
+
if not is_read_only:
|
|
123
|
+
ns = f"window.tokenSelectors['{config.prefix}']"
|
|
124
|
+
if mode == "gap":
|
|
125
|
+
onclick = f"{ns}.selectGap({idx})"
|
|
126
|
+
elif mode == "word":
|
|
127
|
+
onclick = f"{ns}.selectWord({idx})"
|
|
128
|
+
elif mode == "span":
|
|
129
|
+
onclick = f"{ns}.selectToken({idx})"
|
|
130
|
+
|
|
131
|
+
return Span(
|
|
132
|
+
*children,
|
|
133
|
+
id=ids.token(idx),
|
|
134
|
+
data_token_index=str(idx),
|
|
135
|
+
cls=combine_classes(*cls_parts),
|
|
136
|
+
onclick=onclick,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# %% ../../nbs/components/tokens.ipynb #91bd40f4
|
|
140
|
+
from cjm_fasthtml_daisyui.utilities.semantic_colors import text_dui
|
|
141
|
+
|
|
142
|
+
# %% ../../nbs/components/tokens.ipynb #efb4515f
|
|
143
|
+
def render_end_token(
|
|
144
|
+
config:TokenSelectorConfig, # config for this instance
|
|
145
|
+
ids:TokenSelectorHtmlIds, # HTML IDs
|
|
146
|
+
state:Optional[TokenSelectorState] = None, # current state
|
|
147
|
+
read_only:bool = False, # disable interaction
|
|
148
|
+
) -> Any: # end sentinel Span element
|
|
149
|
+
"""Render the end-of-text sentinel token."""
|
|
150
|
+
word_count = state.word_count if state else 0
|
|
151
|
+
anchor = state.anchor if state else 0
|
|
152
|
+
mode = config.selection_mode
|
|
153
|
+
is_read_only = read_only or config.read_only
|
|
154
|
+
|
|
155
|
+
# Caret at end position (gap mode only)
|
|
156
|
+
show_caret = (mode == "gap" and anchor == word_count and state is not None)
|
|
157
|
+
|
|
158
|
+
children = []
|
|
159
|
+
if show_caret:
|
|
160
|
+
children.append(_render_caret_indicator())
|
|
161
|
+
children.append(Span(config.end_token_text, cls=combine_classes(font_size.sm, italic)))
|
|
162
|
+
|
|
163
|
+
cls_parts = [
|
|
164
|
+
"token", position.relative,
|
|
165
|
+
p.x(1), p.y(0.5),
|
|
166
|
+
text_dui.base_content.opacity(30),
|
|
167
|
+
cursor.pointer, select.none,
|
|
168
|
+
]
|
|
169
|
+
if not is_read_only:
|
|
170
|
+
cls_parts.append(text_dui.base_content.hover)
|
|
171
|
+
|
|
172
|
+
onclick = None
|
|
173
|
+
if not is_read_only and mode == "gap":
|
|
174
|
+
ns = f"window.tokenSelectors['{config.prefix}']"
|
|
175
|
+
onclick = f"{ns}.selectGap({word_count})"
|
|
176
|
+
|
|
177
|
+
return Span(
|
|
178
|
+
*children,
|
|
179
|
+
data_token_index=str(word_count),
|
|
180
|
+
cls=combine_classes(*cls_parts),
|
|
181
|
+
onclick=onclick,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# %% ../../nbs/components/tokens.ipynb #8b429b4b
|
|
185
|
+
def render_token_grid(
|
|
186
|
+
tokens:List[Token], # token list to render
|
|
187
|
+
config:TokenSelectorConfig, # config for this instance
|
|
188
|
+
ids:TokenSelectorHtmlIds, # HTML IDs
|
|
189
|
+
state:Optional[TokenSelectorState] = None, # current state for highlighting
|
|
190
|
+
style_callback:Optional[Callable] = None, # (TokenRenderContext) -> str for extra CSS
|
|
191
|
+
read_only:bool = False, # disable all interaction
|
|
192
|
+
) -> Any: # Div containing the complete token grid
|
|
193
|
+
"""Render the full interactive token grid."""
|
|
194
|
+
is_read_only = read_only or config.read_only
|
|
195
|
+
|
|
196
|
+
# Render individual tokens
|
|
197
|
+
token_elements = [
|
|
198
|
+
render_token(t, config, ids, state, style_callback, is_read_only)
|
|
199
|
+
for t in tokens
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
# End token (gap mode only, if enabled)
|
|
203
|
+
if config.show_end_token and config.selection_mode == "gap":
|
|
204
|
+
token_elements.append(render_end_token(config, ids, state, is_read_only))
|
|
205
|
+
|
|
206
|
+
word_count = len(tokens)
|
|
207
|
+
|
|
208
|
+
return Div(
|
|
209
|
+
*token_elements,
|
|
210
|
+
id=ids.token_grid,
|
|
211
|
+
data_word_count=str(word_count),
|
|
212
|
+
cls=combine_classes(
|
|
213
|
+
"token-container",
|
|
214
|
+
flex_display, flex_wrap.wrap,
|
|
215
|
+
gap(1), p(2),
|
|
216
|
+
),
|
|
217
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Configuration dataclass for token selector initialization."""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/config.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['TokenSelectorConfig']
|
|
7
|
+
|
|
8
|
+
# %% ../../nbs/core/config.ipynb #4904f597
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
from cjm_fasthtml_token_selector.core.constants import (
|
|
12
|
+
SelectionMode,
|
|
13
|
+
DEFAULT_INITIAL_DELAY, DEFAULT_REPEAT_INTERVAL, DEFAULT_THROTTLE_FLOOR,
|
|
14
|
+
DEFAULT_LEFT_KEY, DEFAULT_RIGHT_KEY, DEFAULT_END_TOKEN_TEXT,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# %% ../../nbs/core/config.ipynb #4f3321a9
|
|
18
|
+
_prefix_counter:int = 0
|
|
19
|
+
|
|
20
|
+
def _auto_prefix() -> str: # unique prefix string
|
|
21
|
+
"""Generate an auto-incrementing unique prefix."""
|
|
22
|
+
global _prefix_counter
|
|
23
|
+
p = f"ts{_prefix_counter}"
|
|
24
|
+
_prefix_counter += 1
|
|
25
|
+
return p
|
|
26
|
+
|
|
27
|
+
def _reset_prefix_counter() -> None:
|
|
28
|
+
"""Reset the prefix counter (for testing only)."""
|
|
29
|
+
global _prefix_counter
|
|
30
|
+
_prefix_counter = 0
|
|
31
|
+
|
|
32
|
+
# %% ../../nbs/core/config.ipynb #2ac6669d
|
|
33
|
+
@dataclass
|
|
34
|
+
class TokenSelectorConfig:
|
|
35
|
+
"""Initialization-time settings for a token selector instance."""
|
|
36
|
+
prefix:str = field(default_factory=_auto_prefix) # unique instance prefix
|
|
37
|
+
selection_mode:SelectionMode = "gap" # selection behavior: "gap", "word", or "span"
|
|
38
|
+
|
|
39
|
+
# Key repeat engine timing
|
|
40
|
+
initial_delay:int = DEFAULT_INITIAL_DELAY # ms before first repeat
|
|
41
|
+
repeat_interval:int = DEFAULT_REPEAT_INTERVAL # ms between repeats
|
|
42
|
+
throttle_floor:int = DEFAULT_THROTTLE_FLOOR # minimum ms between movements
|
|
43
|
+
|
|
44
|
+
# Key mappings (for the internal key repeat engine)
|
|
45
|
+
left_key:str = DEFAULT_LEFT_KEY # key for leftward navigation
|
|
46
|
+
right_key:str = DEFAULT_RIGHT_KEY # key for rightward navigation
|
|
47
|
+
|
|
48
|
+
# Display
|
|
49
|
+
end_token_text:str = DEFAULT_END_TOKEN_TEXT # text for the sentinel end token
|
|
50
|
+
show_end_token:bool = True # whether to show the end token
|
|
51
|
+
|
|
52
|
+
# Behavior
|
|
53
|
+
read_only:bool = False # disable click/keyboard interaction
|
|
54
|
+
wrap_navigation:bool = False # wrap around at boundaries
|
|
55
|
+
|
|
56
|
+
# Callbacks
|
|
57
|
+
on_change_callback:str = "" # JS function name called on selection change
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""CSS class constants, selection mode type, timing defaults, and key defaults for the token selector."""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/constants.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['SelectionMode', 'OPACITY_50_CLS', 'CARET_INDICATOR_CLS', 'HIGHLIGHT_CLS', 'DEFAULT_INITIAL_DELAY',
|
|
7
|
+
'DEFAULT_REPEAT_INTERVAL', 'DEFAULT_THROTTLE_FLOOR', 'DEFAULT_LEFT_KEY', 'DEFAULT_RIGHT_KEY',
|
|
8
|
+
'DEFAULT_END_TOKEN_TEXT']
|
|
9
|
+
|
|
10
|
+
# %% ../../nbs/core/constants.ipynb #2309118c
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
# %% ../../nbs/core/constants.ipynb #a026c6f9
|
|
14
|
+
from cjm_fasthtml_daisyui.utilities.semantic_colors import bg_dui
|
|
15
|
+
|
|
16
|
+
from cjm_fasthtml_tailwind.utilities.sizing import w, h
|
|
17
|
+
from cjm_fasthtml_tailwind.utilities.layout import position, top, left
|
|
18
|
+
from cjm_fasthtml_tailwind.utilities.effects import opacity
|
|
19
|
+
from cjm_fasthtml_tailwind.utilities.transitions_and_animation import animate
|
|
20
|
+
from cjm_fasthtml_tailwind.utilities.transforms import translate
|
|
21
|
+
from cjm_fasthtml_tailwind.core.base import combine_classes
|
|
22
|
+
|
|
23
|
+
# %% ../../nbs/core/constants.ipynb #07b9f913
|
|
24
|
+
SelectionMode = Literal["gap", "word", "span"]
|
|
25
|
+
|
|
26
|
+
# %% ../../nbs/core/constants.ipynb #717f0741
|
|
27
|
+
OPACITY_50_CLS = str(opacity(50))
|
|
28
|
+
|
|
29
|
+
# %% ../../nbs/core/constants.ipynb #75b65c24
|
|
30
|
+
CARET_INDICATOR_CLS = combine_classes(
|
|
31
|
+
"caret-indicator",
|
|
32
|
+
w(0.5), h(5), bg_dui.error,
|
|
33
|
+
position.absolute, left.negative(1), top("1/2"), translate.y.negative("1/2"),
|
|
34
|
+
animate.pulse
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# %% ../../nbs/core/constants.ipynb #c2f02ac7
|
|
38
|
+
HIGHLIGHT_CLS = str(bg_dui.primary.opacity(20))
|
|
39
|
+
|
|
40
|
+
# %% ../../nbs/core/constants.ipynb #ba9f3ddf
|
|
41
|
+
DEFAULT_INITIAL_DELAY:int = 400 # ms before first repeat
|
|
42
|
+
DEFAULT_REPEAT_INTERVAL:int = 80 # ms between repeats
|
|
43
|
+
DEFAULT_THROTTLE_FLOOR:int = 50 # minimum ms between movements
|
|
44
|
+
|
|
45
|
+
# %% ../../nbs/core/constants.ipynb #472a38eb
|
|
46
|
+
DEFAULT_LEFT_KEY:str = "ArrowLeft"
|
|
47
|
+
DEFAULT_RIGHT_KEY:str = "ArrowRight"
|
|
48
|
+
|
|
49
|
+
# %% ../../nbs/core/constants.ipynb #9fee5792
|
|
50
|
+
DEFAULT_END_TOKEN_TEXT:str = "(End)"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Prefix-based HTML ID generator for token selector DOM elements."""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/html_ids.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['TokenSelectorHtmlIds']
|
|
7
|
+
|
|
8
|
+
# %% ../../nbs/core/html_ids.ipynb #00fedd76
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
# %% ../../nbs/core/html_ids.ipynb #24a81661
|
|
12
|
+
@dataclass
|
|
13
|
+
class TokenSelectorHtmlIds:
|
|
14
|
+
"""Prefix-based HTML ID generator for token selector DOM elements."""
|
|
15
|
+
prefix:str # unique instance prefix
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def container(self) -> str: # outer wrapper ID
|
|
19
|
+
"""Outer token selector wrapper."""
|
|
20
|
+
return f"{self.prefix}-token-selector"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def token_grid(self) -> str: # flex-wrap token container ID
|
|
24
|
+
"""Flex-wrap token container."""
|
|
25
|
+
return f"{self.prefix}-token-grid"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def anchor_input(self) -> str: # hidden input ID for anchor position
|
|
29
|
+
"""Hidden input ID for anchor position (hyphenated, for CSS selectors)."""
|
|
30
|
+
return f"{self.prefix}-anchor"
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def focus_input(self) -> str: # hidden input ID for focus position
|
|
34
|
+
"""Hidden input ID for focus position (hyphenated, for CSS selectors)."""
|
|
35
|
+
return f"{self.prefix}-focus"
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def anchor_name(self) -> str: # form field name for anchor position
|
|
39
|
+
"""Form field name for anchor position (underscored, for Python kwargs)."""
|
|
40
|
+
return f"{self.prefix}_anchor"
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def focus_name(self) -> str: # form field name for focus position
|
|
44
|
+
"""Form field name for focus position (underscored, for Python kwargs)."""
|
|
45
|
+
return f"{self.prefix}_focus"
|
|
46
|
+
|
|
47
|
+
def token(self,
|
|
48
|
+
index:int, # token position index
|
|
49
|
+
) -> str: # individual token span ID
|
|
50
|
+
"""Individual token span ID."""
|
|
51
|
+
return f"{self.prefix}-token-{index}"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Data models for tokens, render context, and mutable runtime state."""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/models.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['Token', 'TokenRenderContext', 'TokenSelectorState']
|
|
7
|
+
|
|
8
|
+
# %% ../../nbs/core/models.ipynb #cd8f9e39
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
# %% ../../nbs/core/models.ipynb #1a22b1bf
|
|
13
|
+
@dataclass
|
|
14
|
+
class Token:
|
|
15
|
+
"""A single token in the token grid."""
|
|
16
|
+
text:str # the token text content
|
|
17
|
+
index:int # 0-based position in the token list
|
|
18
|
+
metadata:Any = None # optional consumer metadata per token
|
|
19
|
+
|
|
20
|
+
# %% ../../nbs/core/models.ipynb #a8270e15
|
|
21
|
+
@dataclass
|
|
22
|
+
class TokenRenderContext:
|
|
23
|
+
"""Context passed to per-token styling callbacks."""
|
|
24
|
+
token:Token # the token being rendered
|
|
25
|
+
is_selected:bool # whether this token is in the current selection
|
|
26
|
+
is_anchor:bool # whether this token is at the anchor position
|
|
27
|
+
is_focus:bool # whether this token is at the focus position
|
|
28
|
+
selection_mode:str # current selection mode
|
|
29
|
+
|
|
30
|
+
# %% ../../nbs/core/models.ipynb #84e86d0a
|
|
31
|
+
@dataclass
|
|
32
|
+
class TokenSelectorState:
|
|
33
|
+
"""Mutable runtime state for a token selector instance."""
|
|
34
|
+
anchor:int = 0 # anchor position (gap index or token index)
|
|
35
|
+
focus:int = 0 # focus position (same as anchor in gap/word mode)
|
|
36
|
+
word_count:int = 0 # total number of tokens
|
|
37
|
+
active:bool = False # whether the key repeat engine is active
|
|
File without changes
|