nicegui-bootstrap-components 0.1.0__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 (103) hide show
  1. nicegui_bootstrap_components/__init__.py +7 -0
  2. nicegui_bootstrap_components/_base.py +590 -0
  3. nicegui_bootstrap_components/_host.py +259 -0
  4. nicegui_bootstrap_components/assets.py +371 -0
  5. nicegui_bootstrap_components/bs/__init__.py +267 -0
  6. nicegui_bootstrap_components/bs/_actions.py +232 -0
  7. nicegui_bootstrap_components/bs/_alert_spinner.py +423 -0
  8. nicegui_bootstrap_components/bs/_carousel.py +312 -0
  9. nicegui_bootstrap_components/bs/_choices.py +779 -0
  10. nicegui_bootstrap_components/bs/_content.py +1343 -0
  11. nicegui_bootstrap_components/bs/_forms.py +378 -0
  12. nicegui_bootstrap_components/bs/_inputs.py +560 -0
  13. nicegui_bootstrap_components/bs/_layout.py +341 -0
  14. nicegui_bootstrap_components/bs/_nav.py +650 -0
  15. nicegui_bootstrap_components/bs/_navbar.py +445 -0
  16. nicegui_bootstrap_components/bs/_overlay_root.py +73 -0
  17. nicegui_bootstrap_components/bs/_overlays.py +2633 -0
  18. nicegui_bootstrap_components/bs/_scope.py +28 -0
  19. nicegui_bootstrap_components/bs/_tabs.py +605 -0
  20. nicegui_bootstrap_components/bs/_toast.py +137 -0
  21. nicegui_bootstrap_components/bs/_tooltip_popover.py +537 -0
  22. nicegui_bootstrap_components/contracts/dbc-2.0.4.json +16811 -0
  23. nicegui_bootstrap_components/dbc/__init__.py +108 -0
  24. nicegui_bootstrap_components/icons.py +29 -0
  25. nicegui_bootstrap_components/persist.py +201 -0
  26. nicegui_bootstrap_components/py.typed +0 -0
  27. nicegui_bootstrap_components/static/dist/fonts/bootstrap-icons.woff +0 -0
  28. nicegui_bootstrap_components/static/dist/fonts/bootstrap-icons.woff2 +0 -0
  29. nicegui_bootstrap_components/static/dist/ngbs-bootstrap-unscoped.css +12047 -0
  30. nicegui_bootstrap_components/static/dist/ngbs-bootstrap.css +12057 -0
  31. nicegui_bootstrap_components/static/dist/ngbs-cerulean-unscoped.css +12338 -0
  32. nicegui_bootstrap_components/static/dist/ngbs-cerulean.css +12348 -0
  33. nicegui_bootstrap_components/static/dist/ngbs-compat-unscoped.css +12 -0
  34. nicegui_bootstrap_components/static/dist/ngbs-compat.css +12 -0
  35. nicegui_bootstrap_components/static/dist/ngbs-cosmo-unscoped.css +11825 -0
  36. nicegui_bootstrap_components/static/dist/ngbs-cosmo.css +11835 -0
  37. nicegui_bootstrap_components/static/dist/ngbs-cyborg-unscoped.css +12213 -0
  38. nicegui_bootstrap_components/static/dist/ngbs-cyborg.css +12223 -0
  39. nicegui_bootstrap_components/static/dist/ngbs-darkly-unscoped.css +12143 -0
  40. nicegui_bootstrap_components/static/dist/ngbs-darkly.css +12153 -0
  41. nicegui_bootstrap_components/static/dist/ngbs-flatly-unscoped.css +12129 -0
  42. nicegui_bootstrap_components/static/dist/ngbs-flatly.css +12139 -0
  43. nicegui_bootstrap_components/static/dist/ngbs-grid-unscoped.css +4084 -0
  44. nicegui_bootstrap_components/static/dist/ngbs-grid.css +4094 -0
  45. nicegui_bootstrap_components/static/dist/ngbs-host-unscoped.css +9 -0
  46. nicegui_bootstrap_components/static/dist/ngbs-host.css +9 -0
  47. nicegui_bootstrap_components/static/dist/ngbs-icons-bootstrap.css +5 -0
  48. nicegui_bootstrap_components/static/dist/ngbs-icons-fontawesome.css +9 -0
  49. nicegui_bootstrap_components/static/dist/ngbs-journal-unscoped.css +12112 -0
  50. nicegui_bootstrap_components/static/dist/ngbs-journal.css +12122 -0
  51. nicegui_bootstrap_components/static/dist/ngbs-litera-unscoped.css +12214 -0
  52. nicegui_bootstrap_components/static/dist/ngbs-litera.css +12224 -0
  53. nicegui_bootstrap_components/static/dist/ngbs-lumen-unscoped.css +12377 -0
  54. nicegui_bootstrap_components/static/dist/ngbs-lumen.css +12387 -0
  55. nicegui_bootstrap_components/static/dist/ngbs-lux-unscoped.css +12048 -0
  56. nicegui_bootstrap_components/static/dist/ngbs-lux.css +12058 -0
  57. nicegui_bootstrap_components/static/dist/ngbs-materia-unscoped.css +13197 -0
  58. nicegui_bootstrap_components/static/dist/ngbs-materia.css +13207 -0
  59. nicegui_bootstrap_components/static/dist/ngbs-minty-unscoped.css +12184 -0
  60. nicegui_bootstrap_components/static/dist/ngbs-minty.css +12194 -0
  61. nicegui_bootstrap_components/static/dist/ngbs-morph-unscoped.css +12829 -0
  62. nicegui_bootstrap_components/static/dist/ngbs-morph.css +12839 -0
  63. nicegui_bootstrap_components/static/dist/ngbs-pulse-unscoped.css +11901 -0
  64. nicegui_bootstrap_components/static/dist/ngbs-pulse.css +11911 -0
  65. nicegui_bootstrap_components/static/dist/ngbs-quartz-unscoped.css +12625 -0
  66. nicegui_bootstrap_components/static/dist/ngbs-quartz.css +12635 -0
  67. nicegui_bootstrap_components/static/dist/ngbs-sandstone-unscoped.css +12214 -0
  68. nicegui_bootstrap_components/static/dist/ngbs-sandstone.css +12224 -0
  69. nicegui_bootstrap_components/static/dist/ngbs-simplex-unscoped.css +12199 -0
  70. nicegui_bootstrap_components/static/dist/ngbs-simplex.css +12209 -0
  71. nicegui_bootstrap_components/static/dist/ngbs-sketchy-unscoped.css +12518 -0
  72. nicegui_bootstrap_components/static/dist/ngbs-sketchy.css +12528 -0
  73. nicegui_bootstrap_components/static/dist/ngbs-slate-unscoped.css +12527 -0
  74. nicegui_bootstrap_components/static/dist/ngbs-slate.css +12537 -0
  75. nicegui_bootstrap_components/static/dist/ngbs-solar-unscoped.css +12152 -0
  76. nicegui_bootstrap_components/static/dist/ngbs-solar.css +12162 -0
  77. nicegui_bootstrap_components/static/dist/ngbs-spacelab-unscoped.css +12279 -0
  78. nicegui_bootstrap_components/static/dist/ngbs-spacelab.css +12289 -0
  79. nicegui_bootstrap_components/static/dist/ngbs-superhero-unscoped.css +12223 -0
  80. nicegui_bootstrap_components/static/dist/ngbs-superhero.css +12233 -0
  81. nicegui_bootstrap_components/static/dist/ngbs-united-unscoped.css +12090 -0
  82. nicegui_bootstrap_components/static/dist/ngbs-united.css +12100 -0
  83. nicegui_bootstrap_components/static/dist/ngbs-vapor-unscoped.css +12556 -0
  84. nicegui_bootstrap_components/static/dist/ngbs-vapor.css +12566 -0
  85. nicegui_bootstrap_components/static/dist/ngbs-yeti-unscoped.css +12333 -0
  86. nicegui_bootstrap_components/static/dist/ngbs-yeti.css +12343 -0
  87. nicegui_bootstrap_components/static/dist/ngbs-zephyr-unscoped.css +12296 -0
  88. nicegui_bootstrap_components/static/dist/ngbs-zephyr.css +12306 -0
  89. nicegui_bootstrap_components/static/prototype-ngbs.css +541 -0
  90. nicegui_bootstrap_components/static/webfonts/fa-brands-400.ttf +0 -0
  91. nicegui_bootstrap_components/static/webfonts/fa-brands-400.woff2 +0 -0
  92. nicegui_bootstrap_components/static/webfonts/fa-regular-400.ttf +0 -0
  93. nicegui_bootstrap_components/static/webfonts/fa-regular-400.woff2 +0 -0
  94. nicegui_bootstrap_components/static/webfonts/fa-solid-900.ttf +0 -0
  95. nicegui_bootstrap_components/static/webfonts/fa-solid-900.woff2 +0 -0
  96. nicegui_bootstrap_components/static/webfonts/fa-v4compatibility.ttf +0 -0
  97. nicegui_bootstrap_components/static/webfonts/fa-v4compatibility.woff2 +0 -0
  98. nicegui_bootstrap_components/theme.py +238 -0
  99. nicegui_bootstrap_components/themes.py +131 -0
  100. nicegui_bootstrap_components-0.1.0.dist-info/METADATA +234 -0
  101. nicegui_bootstrap_components-0.1.0.dist-info/RECORD +103 -0
  102. nicegui_bootstrap_components-0.1.0.dist-info/WHEEL +4 -0
  103. nicegui_bootstrap_components-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,650 @@
1
+ """Navigation family: Nav, NavItem, NavLink, Breadcrumb, Pagination."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping
6
+ from contextlib import suppress
7
+ from typing import Any
8
+
9
+ from nicegui import ui
10
+
11
+ from .._base import (
12
+ BootstrapElement,
13
+ ChildrenError,
14
+ make_surface_classes,
15
+ normalize_style,
16
+ style_to_css,
17
+ )
18
+ from .._host import set_element_text
19
+
20
+ __all__ = [
21
+ "Breadcrumb",
22
+ "DbcBreadcrumb",
23
+ "DbcNav",
24
+ "DbcNavItem",
25
+ "DbcNavLink",
26
+ "DbcPagination",
27
+ "Nav",
28
+ "NavItem",
29
+ "NavLink",
30
+ "Pagination",
31
+ "breadcrumb",
32
+ "breadcrumb_item_classes",
33
+ "clamp_page",
34
+ "nav",
35
+ "nav_classes",
36
+ "nav_item",
37
+ "nav_link",
38
+ "nav_link_classes",
39
+ "pagination",
40
+ "pagination_tokens",
41
+ "validate_breadcrumb_items",
42
+ ]
43
+
44
+ _VERTICAL_BPS = frozenset({"xs", "sm", "md", "lg", "xl"})
45
+ _HORIZONTAL = frozenset({"start", "center", "end", "between", "around"})
46
+ _PAGINATION_SIZES = frozenset({"sm", "lg"})
47
+ _PAGINATION_GLYPHS = {
48
+ "first": "«",
49
+ "prev": "‹",
50
+ "next": "›",
51
+ "last": "»",
52
+ }
53
+
54
+
55
+ def nav_classes(
56
+ *,
57
+ pills: bool = False,
58
+ vertical: bool | str | None = False,
59
+ horizontal: str | None = None,
60
+ fill: bool = False,
61
+ justified: bool = False,
62
+ card: bool = False,
63
+ navbar: bool = False,
64
+ navbar_scroll: bool = False,
65
+ ) -> list[str]:
66
+ """Return Bootstrap ``nav`` classes."""
67
+ classes = ["navbar-nav" if navbar else "nav"]
68
+ if pills:
69
+ classes.append("nav-pills")
70
+ if navbar and navbar_scroll:
71
+ classes.append("navbar-nav-scroll")
72
+ if fill:
73
+ classes.append("nav-fill")
74
+ if justified:
75
+ classes.append("nav-justified")
76
+ if card and pills:
77
+ classes.append("card-header-pills")
78
+ if vertical is True:
79
+ classes.append("flex-column")
80
+ elif isinstance(vertical, str):
81
+ if vertical not in _VERTICAL_BPS:
82
+ raise ValueError(f"Invalid nav vertical breakpoint: {vertical!r}")
83
+ classes.append(f"flex-{vertical}-column")
84
+ elif vertical not in (False, None):
85
+ raise ValueError(f"Invalid nav vertical: {vertical!r}")
86
+ if horizontal is not None:
87
+ if horizontal not in _HORIZONTAL:
88
+ raise ValueError(f"Invalid nav horizontal: {horizontal!r}")
89
+ classes.append(f"justify-content-{horizontal}")
90
+ return classes
91
+
92
+
93
+ def nav_link_classes(*, active: bool | str = False, disabled: bool = False) -> list[str]:
94
+ """Return Bootstrap ``nav-link`` classes."""
95
+ classes = ["nav-link"]
96
+ if active:
97
+ classes.append("active")
98
+ if disabled:
99
+ classes.append("disabled")
100
+ return classes
101
+
102
+
103
+ def breadcrumb_item_classes(active: bool = False) -> list[str]:
104
+ """Return Bootstrap ``breadcrumb-item`` classes."""
105
+ classes = ["breadcrumb-item"]
106
+ if active:
107
+ classes.append("active")
108
+ return classes
109
+
110
+
111
+ def validate_breadcrumb_items(items: list[Any] | None) -> list[dict[str, Any]]:
112
+ """Copy and validate breadcrumb item dicts."""
113
+ if items is None:
114
+ return []
115
+ validated: list[dict[str, Any]] = []
116
+ for entry in items:
117
+ if not isinstance(entry, Mapping):
118
+ raise TypeError(f"Breadcrumb items must be mappings, got {type(entry)!r}")
119
+ if "label" not in entry:
120
+ raise ValueError("Breadcrumb item missing label")
121
+ label = entry["label"]
122
+ if label is None or label == "":
123
+ raise ValueError("Breadcrumb item missing label")
124
+ validated.append(dict(entry))
125
+ return validated
126
+
127
+
128
+ def _rounded_max_value(min_value: int, max_value: int, step: int) -> int:
129
+ remainder = (max_value - min_value) % step
130
+ if remainder != 0:
131
+ return max_value + step - remainder
132
+ return max_value
133
+
134
+
135
+ def pagination_tokens(
136
+ *,
137
+ min_value: int,
138
+ max_value: int,
139
+ step: int,
140
+ active_page: int,
141
+ fully_expanded: bool = True,
142
+ previous_next: bool = False,
143
+ first_last: bool = False,
144
+ ) -> list[str | int]:
145
+ """Return ordered pagination tokens (page numbers and sentinels)."""
146
+ max_value = _rounded_max_value(min_value, max_value, step)
147
+ tokens: list[str | int] = []
148
+ if first_last:
149
+ tokens.append("first")
150
+ if previous_next:
151
+ tokens.append("prev")
152
+ page_count = (max_value - min_value) // step + 1
153
+ if fully_expanded or page_count <= 7:
154
+ page = min_value
155
+ while page <= max_value:
156
+ tokens.append(page)
157
+ page += step
158
+ else:
159
+ tokens.append(min_value)
160
+ if active_page <= min_value + 3 * step:
161
+ tokens.extend(
162
+ [
163
+ min_value + step,
164
+ min_value + 2 * step,
165
+ min_value + 3 * step,
166
+ min_value + 4 * step,
167
+ ]
168
+ )
169
+ tokens.append("ellipsis")
170
+ elif active_page >= max_value - 3 * step:
171
+ tokens.append("ellipsis")
172
+ tokens.extend(
173
+ [
174
+ max_value - 4 * step,
175
+ max_value - 3 * step,
176
+ max_value - 2 * step,
177
+ max_value - step,
178
+ ]
179
+ )
180
+ else:
181
+ tokens.extend(
182
+ [
183
+ "ellipsis",
184
+ active_page - step,
185
+ active_page,
186
+ active_page + step,
187
+ "ellipsis",
188
+ ]
189
+ )
190
+ tokens.append(max_value)
191
+ if previous_next:
192
+ tokens.append("next")
193
+ if first_last:
194
+ tokens.append("last")
195
+ return tokens
196
+
197
+
198
+ def clamp_page(value: int, *, min_value: int, max_value: int) -> int:
199
+ """Clamp ``value`` into ``[min_value, max_value]``."""
200
+ if value < min_value:
201
+ return min_value
202
+ if value > max_value:
203
+ return max_value
204
+ return int(value)
205
+
206
+
207
+ def _apply_dom_attrs(element: Any, attrs: dict[str, Any]) -> None:
208
+ parts: list[str] = []
209
+ for key, val in attrs.items():
210
+ if val is True:
211
+ parts.append(str(key))
212
+ elif val is False or val is None or val == "":
213
+ continue
214
+ else:
215
+ parts.append(f"{key}={val}")
216
+ if parts:
217
+ element.props(" ".join(parts))
218
+
219
+
220
+ def _set_text(element: Any, text: str) -> None:
221
+ setter = getattr(element, "set_text", None)
222
+ if callable(setter):
223
+ setter(text)
224
+ return
225
+ set_element_text(element, text)
226
+
227
+
228
+ def _flatten_children(children: object) -> list[object]:
229
+ if children is None:
230
+ return []
231
+ if isinstance(children, str):
232
+ return [children]
233
+ if isinstance(children, (list, tuple)):
234
+ flat: list[object] = []
235
+ for child in children:
236
+ flat.extend(_flatten_children(child))
237
+ return flat
238
+ return [children]
239
+
240
+
241
+ def _nav_link_dom_attrs(
242
+ *,
243
+ href: str | None,
244
+ external_link: bool | None,
245
+ disabled: bool,
246
+ target: str | None,
247
+ ) -> tuple[str, dict[str, Any]]:
248
+ if href:
249
+ attrs: dict[str, Any] = {"href": href}
250
+ if external_link is True:
251
+ attrs["target"] = "_blank"
252
+ attrs["rel"] = "noopener noreferrer"
253
+ elif target:
254
+ attrs["target"] = target
255
+ if disabled:
256
+ attrs["aria-disabled"] = "true"
257
+ attrs["tabindex"] = "-1"
258
+ return ("a", attrs)
259
+ button_attrs: dict[str, Any] = {"type": "button"}
260
+ if disabled:
261
+ button_attrs["disabled"] = True
262
+ return ("button", button_attrs)
263
+
264
+
265
+ def _add_breadcrumb_item(
266
+ entry: dict[str, Any],
267
+ *,
268
+ item_style: dict[str, Any] | None,
269
+ item_class_name: str | None,
270
+ ) -> None:
271
+ active = bool(entry.get("active"))
272
+ label = entry.get("label")
273
+ href = entry.get("href")
274
+ li = ui.element("li")
275
+ classes = breadcrumb_item_classes(active=active)
276
+ if item_class_name:
277
+ classes.append(str(item_class_name))
278
+ li.classes(" ".join(classes))
279
+ if item_style:
280
+ css = style_to_css(normalize_style(dict(item_style)))
281
+ if css:
282
+ li.style(css)
283
+ if active:
284
+ _apply_dom_attrs(li, {"aria-current": "page"})
285
+ _set_text(li, str(label))
286
+ return
287
+ if href:
288
+ with li:
289
+ anchor = ui.element("a")
290
+ attrs: dict[str, Any] = {"href": href}
291
+ if entry.get("external_link") is True:
292
+ attrs["target"] = "_blank"
293
+ attrs["rel"] = "noopener noreferrer"
294
+ elif entry.get("target"):
295
+ attrs["target"] = entry.get("target")
296
+ title = entry.get("title")
297
+ if title is not None:
298
+ attrs["title"] = title
299
+ _apply_dom_attrs(anchor, attrs)
300
+ _set_text(anchor, str(label))
301
+ return
302
+ _set_text(li, str(label))
303
+
304
+
305
+ def _adopt_breadcrumb_child(host: Any, child: object) -> None:
306
+ if isinstance(child, (str, int, float)):
307
+ with host:
308
+ li = ui.element("li")
309
+ li.classes("breadcrumb-item")
310
+ _set_text(li, str(child))
311
+ return
312
+ mover = getattr(child, "move", None)
313
+ if callable(mover):
314
+ mover(host)
315
+ return
316
+ raise TypeError(f"Unsupported Breadcrumb child type: {type(child)!r}")
317
+
318
+
319
+ class _NavImpl(BootstrapElement):
320
+ """Nav can be used to group together a collection of navigation links."""
321
+
322
+ component_name = "Nav"
323
+ children_kind = "auto"
324
+
325
+ def __init__(
326
+ self,
327
+ children: object = None,
328
+ *,
329
+ _surface: str,
330
+ pills: bool = False,
331
+ vertical: bool | str = False,
332
+ horizontal: str | None = None,
333
+ fill: bool = False,
334
+ justified: bool = False,
335
+ card: bool = False,
336
+ navbar: bool = False,
337
+ navbar_scroll: bool = False,
338
+ **kwargs: Any,
339
+ ) -> None:
340
+ self._surface = _surface
341
+ self._structural_classes = tuple(
342
+ nav_classes(
343
+ pills=pills,
344
+ vertical=vertical,
345
+ horizontal=horizontal,
346
+ fill=fill,
347
+ justified=justified,
348
+ card=card,
349
+ navbar=navbar,
350
+ navbar_scroll=navbar_scroll,
351
+ )
352
+ )
353
+ super().__init__(children, tag="ul", **kwargs)
354
+
355
+
356
+ class _NavItemImpl(BootstrapElement):
357
+ """A list item for grouping navigation links."""
358
+
359
+ component_name = "NavItem"
360
+ children_kind = "auto"
361
+ reserved_classes = ("nav-item",)
362
+
363
+ def __init__(
364
+ self,
365
+ children: object = None,
366
+ *,
367
+ _surface: str,
368
+ **kwargs: Any,
369
+ ) -> None:
370
+ self._surface = _surface
371
+ self._structural_classes = ("nav-item",)
372
+ super().__init__(children, tag="li", **kwargs)
373
+
374
+
375
+ class _NavLinkImpl(BootstrapElement):
376
+ """Add a link to a Nav. Can be used as a child of NavItem or of Nav directly."""
377
+
378
+ component_name = "NavLink"
379
+ children_kind = "phrasing"
380
+ reserved_classes = ("nav-link",)
381
+
382
+ def __init__(
383
+ self,
384
+ children: object = None,
385
+ *,
386
+ _surface: str,
387
+ href: str | None = None,
388
+ n_clicks: int = 0,
389
+ active: bool | str = False,
390
+ disabled: bool = False,
391
+ external_link: bool | None = None,
392
+ target: str | None = None,
393
+ on_click: Callable[..., Any] | None = None,
394
+ **kwargs: Any,
395
+ ) -> None:
396
+ self._surface = _surface
397
+ tag, attrs = _nav_link_dom_attrs(
398
+ href=href,
399
+ external_link=external_link,
400
+ disabled=disabled,
401
+ target=target,
402
+ )
403
+ self._structural_classes = tuple(nav_link_classes(active=active, disabled=disabled))
404
+ self._n_clicks = int(n_clicks)
405
+ self.disabled = bool(disabled)
406
+ self._on_click = on_click
407
+ super().__init__(children, tag=tag, **kwargs)
408
+ _apply_dom_attrs(self, attrs)
409
+ self.on("click", self._handle_click)
410
+
411
+ @property
412
+ def n_clicks(self) -> int:
413
+ """Compat click counter (read-only observable)."""
414
+ return self._n_clicks
415
+
416
+ def _handle_click(self, *_args: Any, **_kwargs: Any) -> None:
417
+ if self.disabled:
418
+ return
419
+ self._n_clicks += 1
420
+ if self._on_click is not None:
421
+ self._on_click()
422
+
423
+
424
+ class _BreadcrumbImpl(BootstrapElement):
425
+ """Use breadcrumbs to create a navigation breadcrumb in your app."""
426
+
427
+ component_name = "Breadcrumb"
428
+ children_kind = "auto"
429
+
430
+ def __init__(
431
+ self,
432
+ children: object = None,
433
+ *,
434
+ _surface: str,
435
+ items: list[dict[str, Any]] | None = None,
436
+ item_style: dict[str, Any] | None = None,
437
+ item_class_name: str | None = None,
438
+ tag: str = "nav",
439
+ **kwargs: Any,
440
+ ) -> None:
441
+ self._surface = _surface
442
+ alias = kwargs.pop("itemClassName", None)
443
+ if item_class_name is None:
444
+ item_class_name = alias
445
+ validated = validate_breadcrumb_items(items)
446
+ super().__init__(None, tag=tag, **kwargs)
447
+ _apply_dom_attrs(self, {"aria-label": "breadcrumb"})
448
+ with self:
449
+ self._list = ui.element("ol")
450
+ self._list.classes("breadcrumb")
451
+ with self._list:
452
+ for entry in validated:
453
+ _add_breadcrumb_item(
454
+ entry,
455
+ item_style=item_style,
456
+ item_class_name=item_class_name,
457
+ )
458
+ for child in _flatten_children(children):
459
+ _adopt_breadcrumb_child(self._list, child)
460
+ self._slot_host = self._list
461
+
462
+ def __enter__(self) -> Any:
463
+ host = getattr(self, "_slot_host", None)
464
+ if host is not None:
465
+ host.__enter__()
466
+ return self
467
+ return super().__enter__()
468
+
469
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> Any:
470
+ host = getattr(self, "_slot_host", None)
471
+ if host is not None:
472
+ return host.__exit__(exc_type, exc, tb)
473
+ parent_exit = getattr(super(), "__exit__", None)
474
+ if parent_exit is not None:
475
+ return parent_exit(exc_type, exc, tb)
476
+ return None
477
+
478
+
479
+ class _PaginationImpl(BootstrapElement):
480
+ """The container for presentational components for building a pagination UI."""
481
+
482
+ component_name = "Pagination"
483
+ children_kind = "grouping"
484
+
485
+ def __init__(
486
+ self,
487
+ children: object = None,
488
+ *,
489
+ _surface: str,
490
+ max_value: int,
491
+ min_value: int = 1,
492
+ step: int = 1,
493
+ active_page: int = 1,
494
+ size: str | None = None,
495
+ fully_expanded: bool = True,
496
+ previous_next: bool = False,
497
+ first_last: bool = False,
498
+ on_change: Callable[..., Any] | None = None,
499
+ **kwargs: Any,
500
+ ) -> None:
501
+ self._surface = _surface
502
+ if children is not None:
503
+ raise ChildrenError("Pagination generates its own items; do not pass children")
504
+ if isinstance(max_value, bool) or not isinstance(max_value, int):
505
+ raise ValueError(f"max_value must be an int, got {max_value!r}")
506
+ if isinstance(min_value, bool) or not isinstance(min_value, int):
507
+ raise ValueError(f"min_value must be an int, got {min_value!r}")
508
+ if max_value < min_value:
509
+ raise ValueError(f"max_value must be >= min_value, got {max_value} < {min_value}")
510
+ if isinstance(step, bool) or not isinstance(step, int) or step < 1:
511
+ raise ValueError(f"step must be an int >= 1, got {step!r}")
512
+ if size is not None and size not in _PAGINATION_SIZES:
513
+ raise ValueError(f"Invalid Pagination size: {size!r}")
514
+ max_value = _rounded_max_value(min_value, max_value, step)
515
+ self._min_value = min_value
516
+ self._max_value = max_value
517
+ self._step = step
518
+ self._size = size
519
+ self._fully_expanded = bool(fully_expanded)
520
+ self._previous_next = bool(previous_next)
521
+ self._first_last = bool(first_last)
522
+ self._on_change = on_change
523
+ self._active_page = clamp_page(int(active_page), min_value=min_value, max_value=max_value)
524
+ super().__init__(None, tag="nav", **kwargs)
525
+ _apply_dom_attrs(self, {"aria-label": "pagination"})
526
+ self._render_items()
527
+
528
+ @property
529
+ def active_page(self) -> int:
530
+ """Currently selected page number (clamped)."""
531
+ return self._active_page
532
+
533
+ @active_page.setter
534
+ def active_page(self, value: int) -> None:
535
+ clamped = clamp_page(int(value), min_value=self._min_value, max_value=self._max_value)
536
+ if clamped == self._active_page:
537
+ return
538
+ self._active_page = clamped
539
+ self._render_items()
540
+ if self._on_change is not None:
541
+ self._on_change(clamped)
542
+
543
+ def _render_items(self) -> None:
544
+ clearer = getattr(self, "clear", None)
545
+ if callable(clearer):
546
+ with suppress(Exception):
547
+ clearer()
548
+ classes = ["pagination"]
549
+ if self._size == "sm":
550
+ classes.append("pagination-sm")
551
+ elif self._size == "lg":
552
+ classes.append("pagination-lg")
553
+ tokens = pagination_tokens(
554
+ min_value=self._min_value,
555
+ max_value=self._max_value,
556
+ step=self._step,
557
+ active_page=self._active_page,
558
+ fully_expanded=self._fully_expanded,
559
+ previous_next=self._previous_next,
560
+ first_last=self._first_last,
561
+ )
562
+ with self:
563
+ self._list = ui.element("ul")
564
+ self._list.classes(" ".join(classes))
565
+ with self._list:
566
+ for token in tokens:
567
+ self._render_token(token)
568
+
569
+ def _special_disabled(self, token: str) -> bool:
570
+ if token in {"first", "prev"}:
571
+ return self._active_page <= self._min_value
572
+ return self._active_page >= self._max_value
573
+
574
+ def _target_for_token(self, token: str) -> int:
575
+ if token == "first":
576
+ return self._min_value
577
+ if token == "prev":
578
+ return self._active_page - self._step
579
+ if token == "next":
580
+ return self._active_page + self._step
581
+ return self._max_value
582
+
583
+ def _bind_page_click(self, element: Any, page: int) -> None:
584
+ def _on_click(*_args: Any, **_kwargs: Any) -> None:
585
+ self.active_page = page
586
+
587
+ element.on("click", _on_click)
588
+
589
+ def _render_token(self, token: str | int) -> None:
590
+ if token == "ellipsis":
591
+ li = ui.element("li")
592
+ li.classes("page-item disabled")
593
+ with li:
594
+ span = ui.element("span")
595
+ span.classes("page-link")
596
+ _set_text(span, "…")
597
+ return
598
+ is_active = isinstance(token, int) and token == self._active_page
599
+ is_disabled = isinstance(token, str) and self._special_disabled(token)
600
+ li_classes = ["page-item"]
601
+ if is_active:
602
+ li_classes.append("active")
603
+ if is_disabled:
604
+ li_classes.append("disabled")
605
+ li = ui.element("li")
606
+ li.classes(" ".join(li_classes))
607
+ if is_active:
608
+ _apply_dom_attrs(li, {"aria-current": "page"})
609
+ with li:
610
+ anchor = ui.element("a")
611
+ anchor.classes("page-link")
612
+ if isinstance(token, int):
613
+ if is_active:
614
+ with anchor:
615
+ _set_text(ui.element("span"), str(token))
616
+ hidden = ui.element("span")
617
+ hidden.classes("visually-hidden")
618
+ _set_text(hidden, "(current)")
619
+ else:
620
+ _set_text(anchor, str(token))
621
+ self._bind_page_click(anchor, token)
622
+ return
623
+ glyph = _PAGINATION_GLYPHS[token]
624
+ hidden_label: str | None = None
625
+ if not is_disabled and token == "prev":
626
+ hidden_label = "Previous"
627
+ elif not is_disabled and token == "next":
628
+ hidden_label = "Next"
629
+ if hidden_label is None:
630
+ _set_text(anchor, glyph)
631
+ else:
632
+ with anchor:
633
+ _set_text(ui.element("span"), glyph)
634
+ hidden = ui.element("span")
635
+ hidden.classes("visually-hidden")
636
+ _set_text(hidden, hidden_label)
637
+ if not is_disabled:
638
+ self._bind_page_click(anchor, self._target_for_token(token))
639
+
640
+
641
+ Nav, DbcNav = make_surface_classes("Nav", globals())
642
+ nav = Nav
643
+ NavItem, DbcNavItem = make_surface_classes("NavItem", globals())
644
+ nav_item = NavItem
645
+ NavLink, DbcNavLink = make_surface_classes("NavLink", globals())
646
+ nav_link = NavLink
647
+ Breadcrumb, DbcBreadcrumb = make_surface_classes("Breadcrumb", globals())
648
+ breadcrumb = Breadcrumb
649
+ Pagination, DbcPagination = make_surface_classes("Pagination", globals())
650
+ pagination = Pagination