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.
Files changed (31) hide show
  1. cjm_fasthtml_token_selector/__init__.py +1 -0
  2. cjm_fasthtml_token_selector/_modidx.py +104 -0
  3. cjm_fasthtml_token_selector/components/__init__.py +0 -0
  4. cjm_fasthtml_token_selector/components/inputs.py +46 -0
  5. cjm_fasthtml_token_selector/components/tokens.py +217 -0
  6. cjm_fasthtml_token_selector/core/__init__.py +0 -0
  7. cjm_fasthtml_token_selector/core/config.py +57 -0
  8. cjm_fasthtml_token_selector/core/constants.py +50 -0
  9. cjm_fasthtml_token_selector/core/html_ids.py +51 -0
  10. cjm_fasthtml_token_selector/core/models.py +37 -0
  11. cjm_fasthtml_token_selector/helpers/__init__.py +0 -0
  12. cjm_fasthtml_token_selector/helpers/tokenizer.py +76 -0
  13. cjm_fasthtml_token_selector/js/__init__.py +0 -0
  14. cjm_fasthtml_token_selector/js/core.py +181 -0
  15. cjm_fasthtml_token_selector/js/display.py +146 -0
  16. cjm_fasthtml_token_selector/js/navigation.py +228 -0
  17. cjm_fasthtml_token_selector/js/repeat.py +119 -0
  18. cjm_fasthtml_token_selector/keyboard/__init__.py +0 -0
  19. cjm_fasthtml_token_selector/keyboard/actions.py +157 -0
  20. cjm_fasthtml_token_selector-0.0.1.dist-info/METADATA +760 -0
  21. cjm_fasthtml_token_selector-0.0.1.dist-info/RECORD +31 -0
  22. cjm_fasthtml_token_selector-0.0.1.dist-info/WHEEL +5 -0
  23. cjm_fasthtml_token_selector-0.0.1.dist-info/entry_points.txt +2 -0
  24. cjm_fasthtml_token_selector-0.0.1.dist-info/licenses/LICENSE +201 -0
  25. cjm_fasthtml_token_selector-0.0.1.dist-info/top_level.txt +2 -0
  26. demos/__init__.py +0 -0
  27. demos/data.py +17 -0
  28. demos/gap.py +93 -0
  29. demos/shared.py +148 -0
  30. demos/span.py +92 -0
  31. 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