openai/chatkit-python

Public

mirrored from https://github.com/openai/chatkit-pythonAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
main

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

chatkit/widgets.py

1193lines · modecode

1from __future__ import annotations
2
3import inspect
4import json
5from datetime import datetime
6from pathlib import Path
7from typing import Annotated, Any, Literal
8
9from jinja2 import Environment, StrictUndefined, Template
10from pydantic import (
11 BaseModel,
12 ConfigDict,
13 Field,
14 model_serializer,
15)
16from typing_extensions import NotRequired, TypedDict, deprecated
17
18from .actions import ActionConfig
19from .icons import IconName
20
21_jinja_env = Environment(undefined=StrictUndefined)
22
23_direct_usage_of_named_widget_types_deprecated = deprecated(
24 "Direct usage of named widget classes is deprecated. "
25 "Use WidgetTemplate to build widgets from .widget files instead. "
26 "Visit https://widgets.chatkit.studio/ to author widget files."
27)
28
29
30@_direct_usage_of_named_widget_types_deprecated
31class ThemeColor(TypedDict):
32 """Color values for light and dark themes."""
33
34 dark: str
35 """Color to use when the theme is dark."""
36 light: str
37 """Color to use when the theme is light."""
38
39
40@_direct_usage_of_named_widget_types_deprecated
41class Spacing(TypedDict):
42 """Shorthand spacing values applied to a widget."""
43
44 top: NotRequired[float | str]
45 """Top spacing; accepts a spacing unit or CSS string."""
46 right: NotRequired[float | str]
47 """Right spacing; accepts a spacing unit or CSS string."""
48 bottom: NotRequired[float | str]
49 """Bottom spacing; accepts a spacing unit or CSS string."""
50 left: NotRequired[float | str]
51 """Left spacing; accepts a spacing unit or CSS string."""
52 x: NotRequired[float | str]
53 """Horizontal spacing; accepts a spacing unit or CSS string."""
54 y: NotRequired[float | str]
55 """Vertical spacing; accepts a spacing unit or CSS string."""
56
57
58@_direct_usage_of_named_widget_types_deprecated
59class Border(TypedDict):
60 """Border style definition for an edge."""
61
62 size: int
63 """Thickness of the border in px."""
64 color: NotRequired[str | ThemeColor]
65 """Border color; accepts border color token, a primitive color token, a CSS string, or theme-aware `{ light, dark }`.
66
67 Valid tokens: `default` `subtle` `strong`
68
69 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
70 """
71 style: NotRequired[
72 Literal[
73 "solid", "dashed", "dotted", "double", "groove", "ridge", "inset", "outset"
74 ]
75 ]
76 """Border line style."""
77
78
79@_direct_usage_of_named_widget_types_deprecated
80class Borders(TypedDict):
81 """Composite border configuration applied across edges."""
82
83 top: NotRequired[int | Border]
84 """Top border or thickness in px."""
85 right: NotRequired[int | Border]
86 """Right border or thickness in px."""
87 bottom: NotRequired[int | Border]
88 """Bottom border or thickness in px."""
89 left: NotRequired[int | Border]
90 """Left border or thickness in px."""
91 x: NotRequired[int | Border]
92 """Horizontal borders or thickness in px."""
93 y: NotRequired[int | Border]
94 """Vertical borders or thickness in px."""
95
96
97@_direct_usage_of_named_widget_types_deprecated
98class MinMax(TypedDict):
99 """Integer minimum/maximum bounds."""
100
101 min: NotRequired[int]
102 """Minimum value (inclusive)."""
103 max: NotRequired[int]
104 """Maximum value (inclusive)."""
105
106
107@_direct_usage_of_named_widget_types_deprecated
108class EditableProps(TypedDict):
109 """Editable field options for text widgets."""
110
111 name: str
112 """The name of the form control field used when submitting forms."""
113 autoFocus: NotRequired[bool]
114 """Autofocus the editable input when it appears."""
115 autoSelect: NotRequired[bool]
116 """Select all text on focus."""
117 autoComplete: NotRequired[str]
118 """Native autocomplete hint for the input."""
119 allowAutofillExtensions: NotRequired[bool]
120 """Allow browser password/autofill extensions."""
121 pattern: NotRequired[str]
122 """Regex pattern for input validation."""
123 placeholder: NotRequired[str]
124 """Placeholder text for the editable input."""
125 required: NotRequired[bool]
126 """Mark the editable input as required."""
127
128
129RadiusValue = Literal[
130 "2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "full", "100%", "none"
131]
132"""Allowed corner radius tokens."""
133
134TextAlign = Literal["start", "center", "end"]
135"""Horizontal text alignment options."""
136
137TextSize = Literal["xs", "sm", "md", "lg", "xl"]
138"""Body text size tokens."""
139
140IconSize = Literal["xs", "sm", "md", "lg", "xl", "2xl", "3xl"]
141"""Icon size tokens."""
142
143TitleSize = Literal["sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl"]
144"""Title text size tokens."""
145
146CaptionSize = Literal["sm", "md", "lg"]
147"""Caption text size tokens."""
148
149Alignment = Literal["start", "center", "end", "baseline", "stretch"]
150"""Flexbox alignment options."""
151Justification = Literal[
152 "start", "center", "end", "between", "around", "evenly", "stretch"
153]
154"""Flexbox justification options."""
155
156ControlVariant = Literal["solid", "soft", "outline", "ghost"]
157"""Button and input style variants."""
158ControlSize = Literal["3xs", "2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl"]
159"""Button and input size variants."""
160
161
162def _drop_none(x):
163 """Recursively remove ``None`` values when serializing widgets."""
164 if isinstance(x, dict):
165 return {
166 k: _drop_none(v) for k, v in x.items() if k == "children" or v is not None
167 }
168 if isinstance(x, list):
169 return [_drop_none(v) for v in x if v is not None]
170 return x
171
172
173class WidgetComponentBase(BaseModel):
174 """Base Pydantic model for all ChatKit widget components."""
175
176 model_config = ConfigDict(serialize_by_alias=True)
177
178 key: str | None = None
179 id: str | None = None
180 type: str = Field(...)
181
182 # For nested model dumps (e.g. if Widget is not the top-level model)
183 @model_serializer(mode="wrap")
184 def serialize(self, next_):
185 dumped = next_(self)
186 # Recursively filter out None values when serialized.
187 # Do this explicitly instead of overriding model_dump_json and model_dump;
188 # the overrides will not be invoked unless the widget is the top-level model.
189 dumped = _drop_none(dumped)
190 # include type even when exlude_defaults is True
191 if isinstance(dumped, dict):
192 dumped["type"] = self.type
193
194 return dumped
195
196
197@_direct_usage_of_named_widget_types_deprecated
198class WidgetStatusWithFavicon(TypedDict):
199 """Widget status representation using a favicon."""
200
201 text: str
202 """Status text to display."""
203 favicon: NotRequired[str]
204 """URL of a favicon to render at the start of the status."""
205 frame: NotRequired[bool]
206 """Show a frame around the favicon for contrast."""
207
208
209@_direct_usage_of_named_widget_types_deprecated
210class WidgetStatusWithIcon(TypedDict):
211 """Widget status representation using an icon."""
212
213 text: str
214 """Status text to display."""
215 icon: NotRequired[WidgetIcon]
216 """Icon to render at the start of the status."""
217
218
219WidgetStatus = WidgetStatusWithFavicon | WidgetStatusWithIcon
220"""Union for representing widget status messaging."""
221
222
223@_direct_usage_of_named_widget_types_deprecated
224class ListViewItem(WidgetComponentBase):
225 """Single row inside a ``ListView`` component."""
226
227 type: Literal["ListViewItem"] = Field(default="ListViewItem", frozen=True) # pyright: ignore
228 children: list["WidgetComponent"]
229 """Content for the list item."""
230 onClickAction: ActionConfig | None = None
231 """Optional action triggered when the list item is clicked."""
232 gap: int | str | None = None
233 """Gap between children within the list item; spacing unit or CSS string."""
234 align: Alignment | None = None
235 """Y-axis alignment for content within the list item."""
236
237
238@_direct_usage_of_named_widget_types_deprecated
239class ListView(WidgetComponentBase):
240 """Container component for rendering collections of list items."""
241
242 type: Literal["ListView"] = Field(default="ListView", frozen=True) # pyright: ignore
243 children: list[ListViewItem]
244 """Items to render in the list."""
245 limit: int | Literal["auto"] | None = None
246 """Max number of items to show before a "Show more" control."""
247 status: WidgetStatus | None = None
248 """Optional status header displayed above the list."""
249 theme: Literal["light", "dark"] | None = None
250 """Force light or dark theme for this subtree."""
251
252
253@_direct_usage_of_named_widget_types_deprecated
254class CardAction(TypedDict):
255 """Configuration for confirm/cancel actions within a card."""
256
257 label: str
258 """Button label shown in the card footer."""
259 action: ActionConfig
260 """Declarative action dispatched to the host application."""
261
262
263@_direct_usage_of_named_widget_types_deprecated
264class Card(WidgetComponentBase):
265 """Versatile container used for structuring widget content."""
266
267 type: Literal["Card"] = Field(default="Card", frozen=True) # pyright: ignore
268 asForm: bool | None = None
269 """Treat the card as an HTML form so confirm/cancel capture form data."""
270 children: list["WidgetComponent"]
271 """Child components rendered inside the card."""
272 background: str | ThemeColor | None = None
273 """Background color; accepts background color token, a primitive color token, a CSS string, or theme-aware `{ light, dark }`.
274
275 Valid tokens: `surface` `surface-secondary` `surface-tertiary` `surface-elevated` `surface-elevated-secondary`
276
277 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
278 """
279 size: Literal["sm", "md", "lg", "full"] | None = None
280 """Visual size of the card; accepts a size token. No preset default is documented."""
281 padding: float | str | Spacing | None = None
282 """Inner spacing of the card; spacing unit, CSS string, or padding object."""
283 status: WidgetStatus | None = None
284 """Optional status header displayed above the card."""
285 collapsed: bool | None = None
286 """Collapse card body after the main action has completed."""
287 confirm: CardAction | None = None
288 """Confirmation action button shown in the card footer."""
289 cancel: CardAction | None = None
290 """Cancel action button shown in the card footer."""
291 theme: Literal["light", "dark"] | None = None
292 """Force light or dark theme for this subtree."""
293
294
295@_direct_usage_of_named_widget_types_deprecated
296class Markdown(WidgetComponentBase):
297 """Widget rendering Markdown content, optionally streamed."""
298
299 type: Literal["Markdown"] = Field(default="Markdown", frozen=True) # pyright: ignore
300 value: str
301 """Markdown source string to render."""
302 streaming: bool | None = None
303 """Applies streaming-friendly transitions for incremental updates."""
304
305
306@_direct_usage_of_named_widget_types_deprecated
307class Text(WidgetComponentBase):
308 """Widget rendering plain text with typography controls."""
309
310 type: Literal["Text"] = Field(default="Text", frozen=True) # pyright: ignore
311 value: str
312 """Text content to display."""
313 streaming: bool | None = None
314 """Enables streaming-friendly transitions for incremental updates."""
315 italic: bool | None = None
316 """Render text in italic style."""
317 lineThrough: bool | None = None
318 """Render text with a line-through decoration."""
319 color: str | ThemeColor | None = None
320 """
321 Text color; accepts a text color token, a primitive color token, a CSS color string, or a theme-aware `{ light, dark }`.
322
323 Text color tokens: `prose` `primary` `emphasis` `secondary` `tertiary` `success` `warning` `danger`
324
325 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
326 """
327 weight: Literal["normal", "medium", "semibold", "bold"] | None = None
328 """Font weight; accepts a font weight token."""
329 width: float | str | None = None
330 """Constrain the text container width; px or CSS string."""
331 size: TextSize | None = None
332 """Size of the text; accepts a text size token."""
333 textAlign: TextAlign | None = None
334 """Horizontal text alignment."""
335 truncate: bool | None = None
336 """Truncate overflow with ellipsis."""
337 minLines: int | None = None
338 """Reserve space for a minimum number of lines."""
339 maxLines: int | None = None
340 """Limit text to a maximum number of lines (line clamp)."""
341 editable: Literal[False] | EditableProps | None = None
342 """Enable inline editing for this text node."""
343
344
345@_direct_usage_of_named_widget_types_deprecated
346class Title(WidgetComponentBase):
347 """Widget rendering prominent headline text."""
348
349 type: Literal["Title"] = Field(default="Title", frozen=True) # pyright: ignore
350 value: str
351 """Text content to display."""
352 color: str | ThemeColor | None = None
353 """
354 Text color; accepts a text color token, a primitive color token, a CSS color string, or a theme-aware `{ light, dark }`.
355
356 Text color tokens: `prose` `primary` `emphasis` `secondary` `tertiary` `success` `warning` `danger`
357
358 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
359 """
360 weight: Literal["normal", "medium", "semibold", "bold"] | None = None
361 """Font weight; accepts a font weight token."""
362 size: TitleSize | None = None
363 """Size of the title text; accepts a title size token."""
364 textAlign: TextAlign | None = None
365 """Horizontal text alignment."""
366 truncate: bool | None = None
367 """Truncate overflow with ellipsis."""
368 maxLines: int | None = None
369 """Limit text to a maximum number of lines (line clamp)."""
370
371
372@_direct_usage_of_named_widget_types_deprecated
373class Caption(WidgetComponentBase):
374 """Widget rendering supporting caption text."""
375
376 type: Literal["Caption"] = Field(default="Caption", frozen=True) # pyright: ignore
377 value: str
378 """Text content to display."""
379 color: str | ThemeColor | None = None
380 """
381 Text color; accepts a text color token, a primitive color token, a CSS color string, or a theme-aware `{ light, dark }`.
382
383 Text color tokens: `prose` `primary` `emphasis` `secondary` `tertiary` `success` `warning` `danger`
384
385 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
386 """
387 weight: Literal["normal", "medium", "semibold", "bold"] | None = None
388 """Font weight; accepts a font weight token."""
389 size: CaptionSize | None = None
390 """Size of the caption text; accepts a caption size token."""
391 textAlign: TextAlign | None = None
392 """Horizontal text alignment."""
393 truncate: bool | None = None
394 """Truncate overflow with ellipsis."""
395 maxLines: int | None = None
396 """Limit text to a maximum number of lines (line clamp)."""
397
398
399@_direct_usage_of_named_widget_types_deprecated
400class Badge(WidgetComponentBase):
401 """Small badge indicating status or categorization."""
402
403 type: Literal["Badge"] = Field(default="Badge", frozen=True) # pyright: ignore
404 label: str
405 """Text to display inside the badge."""
406 color: (
407 Literal["secondary", "success", "danger", "warning", "info", "discovery"] | None
408 ) = None
409 """Color of the badge; accepts a badge color token."""
410 variant: Literal["solid", "soft", "outline"] | None = None
411 """Visual style of the badge."""
412 size: Literal["sm", "md", "lg"] | None = None
413 """Size of the badge."""
414 pill: bool | None = None
415 """Determines if the badge should be fully rounded (pill)."""
416
417
418@_direct_usage_of_named_widget_types_deprecated
419class BoxBase(BaseModel):
420 """Shared layout props for flexible container widgets."""
421
422 children: list["WidgetComponent"] | None = None
423 """Child components to render inside the container."""
424 align: Alignment | None = None
425 """Cross-axis alignment of children."""
426 justify: Justification | None = None
427 """Main-axis distribution of children."""
428 wrap: Literal["nowrap", "wrap", "wrap-reverse"] | None = None
429 """Wrap behavior for flex items."""
430 flex: int | str | None = None
431 """Flex growth/shrink factor."""
432 gap: int | str | None = None
433 """Gap between direct children; spacing unit or CSS string."""
434 height: float | str | None = None
435 """Explicit height; px or CSS string."""
436 width: float | str | None = None
437 """Explicit width; px or CSS string."""
438 size: float | str | None = None
439 """Shorthand to set both width and height; px or CSS string."""
440 minHeight: int | str | None = None
441 """Minimum height; px or CSS string."""
442 minWidth: int | str | None = None
443 """Minimum width; px or CSS string."""
444 minSize: int | str | None = None
445 """Shorthand to set both minWidth and minHeight; px or CSS string."""
446 maxHeight: int | str | None = None
447 """Maximum height; px or CSS string."""
448 maxWidth: int | str | None = None
449 """Maximum width; px or CSS string."""
450 maxSize: int | str | None = None
451 """Shorthand to set both maxWidth and maxHeight; px or CSS string."""
452 padding: float | str | Spacing | None = None
453 """Inner padding; spacing unit, CSS string, or padding object."""
454 margin: float | str | Spacing | None = None
455 """Outer margin; spacing unit, CSS string, or margin object."""
456 border: int | Border | Borders | None = None
457 """Border applied to the container; px or border object/shorthand."""
458 radius: RadiusValue | None = None
459 """Border radius; accepts a radius token."""
460 background: str | ThemeColor | None = None
461 """Background color; accepts background color token, a primitive color token, a CSS string, or theme-aware `{ light, dark }`.
462
463 Valid tokens: `surface` `surface-secondary` `surface-tertiary` `surface-elevated` `surface-elevated-secondary`
464
465 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
466 """
467 aspectRatio: float | str | None = None
468 """Aspect ratio of the box (e.g., 16/9); number or CSS string."""
469
470
471@_direct_usage_of_named_widget_types_deprecated
472class Box(WidgetComponentBase, BoxBase):
473 """Generic flex container with direction control."""
474
475 type: Literal["Box"] = Field(default="Box", frozen=True) # pyright: ignore
476 direction: Literal["row", "col"] | None = None
477 """Flex direction for content within this container."""
478
479
480@_direct_usage_of_named_widget_types_deprecated
481class Row(WidgetComponentBase, BoxBase):
482 """Horizontal flex container."""
483
484 type: Literal["Row"] = Field(default="Row", frozen=True) # pyright: ignore
485
486
487@_direct_usage_of_named_widget_types_deprecated
488class Col(WidgetComponentBase, BoxBase):
489 """Vertical flex container."""
490
491 type: Literal["Col"] = Field(default="Col", frozen=True) # pyright: ignore
492
493
494@_direct_usage_of_named_widget_types_deprecated
495class Form(WidgetComponentBase, BoxBase):
496 """Form wrapper capable of submitting ``onSubmitAction``."""
497
498 type: Literal["Form"] = Field(default="Form", frozen=True) # pyright: ignore
499 onSubmitAction: ActionConfig | None = None
500 """Action dispatched when the form is submitted."""
501 direction: Literal["row", "col"] | None = None
502 """Flex direction for laying out form children."""
503
504
505@_direct_usage_of_named_widget_types_deprecated
506class Divider(WidgetComponentBase):
507 """Visual divider separating content sections."""
508
509 type: Literal["Divider"] = Field(default="Divider", frozen=True) # pyright: ignore
510 color: str | ThemeColor | None = None
511 """Divider color; accepts border color token, a primitive color token, a CSS string, or theme-aware `{ light, dark }`.
512
513 Valid tokens: `default` `subtle` `strong`
514
515 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
516 """
517 size: int | str | None = None
518 """Thickness of the divider line; px or CSS string."""
519 spacing: int | str | None = None
520 """Outer spacing above and below the divider; spacing unit or CSS string."""
521 flush: bool | None = None
522 """Flush the divider to the container edge, removing surrounding padding."""
523
524
525@_direct_usage_of_named_widget_types_deprecated
526class Icon(WidgetComponentBase):
527 """Icon component referencing a built-in icon name."""
528
529 type: Literal["Icon"] = Field(default="Icon", frozen=True) # pyright: ignore
530 name: WidgetIcon
531 """Name of the icon to display."""
532 color: str | ThemeColor | None = None
533 """
534 Icon color; accepts a text color token, a primitive color token, a CSS color string, or a theme-aware `{ light, dark }`.
535
536 Text color tokens: `prose` `primary` `emphasis` `secondary` `tertiary` `success` `warning` `danger`
537
538 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
539 """
540 size: IconSize | None = None
541 """Size of the icon; accepts an icon size token."""
542
543
544@_direct_usage_of_named_widget_types_deprecated
545class Image(WidgetComponentBase):
546 """Image component with sizing and fitting controls."""
547
548 type: Literal["Image"] = Field(default="Image", frozen=True) # pyright: ignore
549 src: str
550 """Image URL source."""
551 alt: str | None = None
552 """Alternate text for accessibility."""
553 fit: Literal["cover", "contain", "fill", "scale-down", "none"] | None = None
554 """How the image should fit within the container."""
555 position: (
556 Literal[
557 "top left",
558 "top",
559 "top right",
560 "left",
561 "center",
562 "right",
563 "bottom left",
564 "bottom",
565 "bottom right",
566 ]
567 | None
568 ) = None
569 """Focal position of the image within the container."""
570 radius: RadiusValue | None = None
571 """Border radius; accepts a radius token."""
572 frame: bool | None = None
573 """Draw a subtle frame around the image."""
574 flush: bool | None = None
575 """Flush the image to the container edge, removing surrounding padding."""
576 height: int | str | None = None
577 """Explicit height; px or CSS string."""
578 width: int | str | None = None
579 """Explicit width; px or CSS string."""
580 size: int | str | None = None
581 """Shorthand to set both width and height; px or CSS string."""
582 minHeight: int | str | None = None
583 """Minimum height; px or CSS string."""
584 minWidth: int | str | None = None
585 """Minimum width; px or CSS string."""
586 minSize: int | str | None = None
587 """Shorthand to set both minWidth and minHeight; px or CSS string."""
588 maxHeight: int | str | None = None
589 """Maximum height; px or CSS string."""
590 maxWidth: int | str | None = None
591 """Maximum width; px or CSS string."""
592 maxSize: int | str | None = None
593 """Shorthand to set both maxWidth and maxHeight; px or CSS string."""
594 margin: int | str | Spacing | None = None
595 """Outer margin; spacing unit, CSS string, or margin object."""
596 background: str | ThemeColor | None = None
597 """Background color; accepts background color token, a primitive color token, a CSS string, or theme-aware `{ light, dark }`.
598
599 Valid tokens: `surface` `surface-secondary` `surface-tertiary` `surface-elevated` `surface-elevated-secondary`
600
601 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
602 """
603 aspectRatio: float | str | None = None
604 """Aspect ratio of the box (e.g., 16/9); number or CSS string."""
605 flex: int | str | None = None
606 """Flex growth/shrink factor."""
607
608
609@_direct_usage_of_named_widget_types_deprecated
610class Button(WidgetComponentBase):
611 """Button component optionally wired to an action."""
612
613 type: Literal["Button"] = Field(default="Button", frozen=True) # pyright: ignore
614 submit: bool | None = None
615 """Configure the button as a submit button for the nearest form."""
616 label: str | None = None
617 """Text to display inside the button."""
618 onClickAction: ActionConfig | None = None
619 """Action dispatched on click."""
620 iconStart: WidgetIcon | None = None
621 """Icon shown before the label; can be used for icon-only buttons."""
622 iconEnd: WidgetIcon | None = None
623 """Optional icon shown after the label."""
624 style: Literal["primary", "secondary"] | None = None
625 """Convenience preset for button style."""
626 iconSize: Literal["sm", "md", "lg", "xl", "2xl"] | None = None
627 """Controls the size of icons within the button; accepts an icon size token."""
628 color: (
629 Literal[
630 "primary",
631 "secondary",
632 "info",
633 "discovery",
634 "success",
635 "caution",
636 "warning",
637 "danger",
638 ]
639 | None
640 ) = None
641 """Color of the button; accepts a button color token."""
642 variant: ControlVariant | None = None
643 """Visual variant of the button; accepts a control variant token."""
644 size: ControlSize | None = None
645 """Controls the overall size of the button."""
646 pill: bool | None = None
647 """Determines if the button should be fully rounded (pill)."""
648 uniform: bool | None = None
649 """Determines if the button should have matching width and height."""
650 block: bool | None = None
651 """Extend the button to 100% of the available width."""
652 disabled: bool | None = None
653 """Disable interactions and apply disabled styles."""
654
655
656@_direct_usage_of_named_widget_types_deprecated
657class Spacer(WidgetComponentBase):
658 """Flexible spacer used to push content apart."""
659
660 type: Literal["Spacer"] = Field(default="Spacer", frozen=True) # pyright: ignore
661 minSize: int | str | None = None
662 """Minimum size the spacer should occupy along the flex direction."""
663
664
665@_direct_usage_of_named_widget_types_deprecated
666class SelectOption(TypedDict):
667 """Selectable option used by the ``Select`` widget."""
668
669 value: str
670 """Option value submitted with the form."""
671 label: str
672 """Human-readable label for the option."""
673 disabled: NotRequired[bool]
674 """Disable the option."""
675 description: NotRequired[str]
676 """Displayed as secondary text below the option `label`."""
677
678
679@_direct_usage_of_named_widget_types_deprecated
680class Select(WidgetComponentBase):
681 """Select dropdown component."""
682
683 type: Literal["Select"] = Field(default="Select", frozen=True) # pyright: ignore
684 name: str
685 """The name of the form control field used when submitting forms."""
686 options: list[SelectOption]
687 """List of selectable options."""
688 onChangeAction: ActionConfig | None = None
689 """Action dispatched when the value changes."""
690 placeholder: str | None = None
691 """Placeholder text shown when no value is selected."""
692 defaultValue: str | None = None
693 """Initial value of the select."""
694 variant: ControlVariant | None = None
695 """Visual style of the select; accepts a control variant token."""
696 size: ControlSize | None = None
697 """Controls the size of the select control."""
698 pill: bool | None = None
699 """Determines if the select should be fully rounded (pill)."""
700 block: bool | None = None
701 """Extend the select to 100% of the available width."""
702 clearable: bool | None = None
703 """Show a clear control to unset the value."""
704 disabled: bool | None = None
705 """Disable interactions and apply disabled styles."""
706 searchable: bool | None = None
707 """Enables the search input. Defaults to enabling search when there are more than 15 options."""
708
709
710@_direct_usage_of_named_widget_types_deprecated
711class DatePicker(WidgetComponentBase):
712 """Date picker input component."""
713
714 type: Literal["DatePicker"] = Field(default="DatePicker", frozen=True) # pyright: ignore
715 name: str
716 """The name of the form control field used when submitting forms."""
717 onChangeAction: ActionConfig | None = None
718 """Action dispatched when the date value changes."""
719 placeholder: str | None = None
720 """Placeholder text shown when no date is selected."""
721 defaultValue: datetime | None = None
722 """Initial value of the date picker."""
723 min: datetime | None = None
724 """Earliest selectable date (inclusive)."""
725 max: datetime | None = None
726 """Latest selectable date (inclusive)."""
727 variant: ControlVariant | None = None
728 """Visual variant of the datepicker control."""
729 size: ControlSize | None = None
730 """Controls the size of the datepicker control."""
731 side: Literal["top", "bottom", "left", "right"] | None = None
732 """Preferred side to render the calendar."""
733 align: Literal["start", "center", "end"] | None = None
734 """Preferred alignment of the calendar relative to the control."""
735 pill: bool | None = None
736 """Determines if the datepicker should be fully rounded (pill)."""
737 block: bool | None = None
738 """Extend the datepicker to 100% of the available width."""
739 clearable: bool | None = None
740 """Show a clear control to unset the value."""
741 disabled: bool | None = None
742 """Disable interactions and apply disabled styles."""
743
744
745@_direct_usage_of_named_widget_types_deprecated
746class Checkbox(WidgetComponentBase):
747 """Checkbox input component."""
748
749 type: Literal["Checkbox"] = Field(default="Checkbox", frozen=True) # pyright: ignore
750 name: str
751 """The name of the form control field used when submitting forms."""
752 label: str | None = None
753 """Optional label text rendered next to the checkbox."""
754 defaultChecked: bool | None = None
755 """The initial checked state of the checkbox."""
756 onChangeAction: ActionConfig | None = None
757 """Action dispatched when the checked state changes."""
758 disabled: bool | None = None
759 """Disable interactions and apply disabled styles."""
760 required: bool | None = None
761 """Mark the checkbox as required for form submission."""
762
763
764@_direct_usage_of_named_widget_types_deprecated
765class Input(WidgetComponentBase):
766 """Single-line text input component."""
767
768 type: Literal["Input"] = Field(default="Input", frozen=True) # pyright: ignore
769 name: str
770 """The name of the form control field used when submitting forms."""
771 inputType: Literal["number", "email", "text", "password", "tel", "url"] | None = (
772 None
773 )
774 """Native input type."""
775 defaultValue: str | None = None
776 """Initial value of the input."""
777 required: bool | None = None
778 """Mark the input as required for form submission."""
779 pattern: str | None = None
780 """Regex pattern for input validation."""
781 placeholder: str | None = None
782 """Placeholder text shown when empty."""
783 allowAutofillExtensions: bool | None = None
784 """Allow password managers / autofill extensions to appear."""
785 autoSelect: bool | None = None
786 """Select all contents of the input when it mounts."""
787 autoFocus: bool | None = None
788 """Autofocus the input when it mounts."""
789 disabled: bool | None = None
790 """Disable interactions and apply disabled styles."""
791 variant: Literal["soft", "outline"] | None = None
792 """Visual style of the input."""
793 size: ControlSize | None = None
794 """Controls the size of the input control."""
795 gutterSize: Literal["2xs", "xs", "sm", "md", "lg", "xl"] | None = None
796 """Controls gutter on the edges of the input; overrides value from `size`."""
797 pill: bool | None = None
798 """Determines if the input should be fully rounded (pill)."""
799
800
801@_direct_usage_of_named_widget_types_deprecated
802class Label(WidgetComponentBase):
803 """Form label associated with a field."""
804
805 type: Literal["Label"] = Field(default="Label", frozen=True) # pyright: ignore
806 value: str
807 """Text content of the label."""
808 fieldName: str
809 """Name of the field this label describes."""
810 size: TextSize | None = None
811 """Size of the label text; accepts a text size token."""
812 weight: Literal["normal", "medium", "semibold", "bold"] | None = None
813 """Font weight; accepts a font weight token."""
814 textAlign: TextAlign | None = None
815 """Horizontal text alignment."""
816 color: str | ThemeColor | None = None
817 """
818 Text color; accepts a text color token, a primitive color token, a CSS color string, or a theme-aware `{ light, dark }`.
819
820 Text color tokens: `prose` `primary` `emphasis` `secondary` `tertiary` `success` `warning` `danger`
821
822 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
823 """
824
825
826@_direct_usage_of_named_widget_types_deprecated
827class RadioOption(TypedDict):
828 """Option inside a ``RadioGroup`` widget."""
829
830 label: str
831 """Label displayed next to the radio option."""
832 value: str
833 """Value submitted when the radio option is selected."""
834 disabled: NotRequired[bool]
835 """Disables a specific radio option."""
836
837
838@_direct_usage_of_named_widget_types_deprecated
839class RadioGroup(WidgetComponentBase):
840 """Grouped radio input control."""
841
842 type: Literal["RadioGroup"] = Field(default="RadioGroup", frozen=True) # pyright: ignore
843 name: str
844 """The name of the form control field used when submitting forms."""
845 options: list[RadioOption] | None = None
846 """Array of options to render as radio items."""
847 ariaLabel: str | None = None
848 """Accessible label for the radio group; falls back to `name`."""
849 onChangeAction: ActionConfig | None = None
850 """Action dispatched when the selected value changes."""
851 defaultValue: str | None = None
852 """Initial selected value of the radio group."""
853 direction: Literal["row", "col"] | None = None
854 """Layout direction of the radio items."""
855 disabled: bool | None = None
856 """Disable interactions and apply disabled styles for the entire group."""
857 required: bool | None = None
858 """Mark the group as required for form submission."""
859
860
861@_direct_usage_of_named_widget_types_deprecated
862class Textarea(WidgetComponentBase):
863 """Multiline text input component."""
864
865 type: Literal["Textarea"] = Field(default="Textarea", frozen=True) # pyright: ignore
866 name: str
867 """The name of the form control field used when submitting forms."""
868 defaultValue: str | None = None
869 """Initial value of the textarea."""
870 required: bool | None = None
871 """Mark the textarea as required for form submission."""
872 pattern: str | None = None
873 """Regex pattern for input validation."""
874 placeholder: str | None = None
875 """Placeholder text shown when empty."""
876 autoSelect: bool | None = None
877 """Select all contents of the textarea when it mounts."""
878 autoFocus: bool | None = None
879 """Autofocus the textarea when it mounts."""
880 disabled: bool | None = None
881 """Disable interactions and apply disabled styles."""
882 variant: Literal["soft", "outline"] | None = None
883 """Visual style of the textarea."""
884 size: ControlSize | None = None
885 """Controls the size of the textarea control."""
886 gutterSize: Literal["2xs", "xs", "sm", "md", "lg", "xl"] | None = None
887 """Controls gutter on the edges of the textarea; overrides value from `size`."""
888 rows: int | None = None
889 """Initial number of visible rows."""
890 autoResize: bool | None = None
891 """Automatically grow/shrink to fit content."""
892 maxRows: int | None = None
893 """Maximum number of rows when auto-resizing."""
894 allowAutofillExtensions: bool | None = None
895 """Allow password managers / autofill extensions to appear."""
896
897
898@_direct_usage_of_named_widget_types_deprecated
899class Transition(WidgetComponentBase):
900 """Wrapper enabling transitions for a child component."""
901
902 type: Literal["Transition"] = Field(default="Transition", frozen=True) # pyright: ignore
903 children: WidgetComponent | None
904 """The child component to animate layout changes for."""
905
906
907@_direct_usage_of_named_widget_types_deprecated
908class Chart(WidgetComponentBase):
909 """Data visualization component for simple bar/line/area charts."""
910
911 type: Literal["Chart"] = Field(default="Chart", frozen=True) # pyright: ignore
912 data: list[dict[str, str | int | float]]
913 """Tabular data for the chart, where each row maps field names to values."""
914 series: list[Series]
915 """One or more series definitions that describe how to visualize data fields."""
916 xAxis: str | XAxisConfig
917 """X-axis configuration; either a `dataKey` string or a config object."""
918 showYAxis: bool | None = None
919 """Controls whether the Y axis is rendered."""
920 showLegend: bool | None = None
921 """Controls whether a legend is rendered."""
922 showTooltip: bool | None = None
923 """Controls whether a tooltip is rendered when hovering over a datapoint."""
924 barGap: int | None = None
925 """Gap between bars within the same category (in px)."""
926 barCategoryGap: int | None = None
927 """Gap between bar categories/groups (in px)."""
928 flex: int | str | None = None
929 """Flex growth/shrink factor for layout."""
930 height: int | str | None = None
931 """Explicit height; px or CSS string."""
932 width: int | str | None = None
933 """Explicit width; px or CSS string."""
934 size: int | str | None = None
935 """Shorthand to set both width and height; px or CSS string."""
936 minHeight: int | str | None = None
937 """Minimum height; px or CSS string."""
938 minWidth: int | str | None = None
939 """Minimum width; px or CSS string."""
940 minSize: int | str | None = None
941 """Shorthand to set both minWidth and minHeight; px or CSS string."""
942 maxHeight: int | str | None = None
943 """Maximum height; px or CSS string."""
944 maxWidth: int | str | None = None
945 """Maximum width; px or CSS string."""
946 maxSize: int | str | None = None
947 """Shorthand to set both maxWidth and maxHeight; px or CSS string."""
948 aspectRatio: float | str | None = None
949 """Aspect ratio of the chart area (e.g., 16/9); number or CSS string."""
950
951
952@_direct_usage_of_named_widget_types_deprecated
953class XAxisConfig(TypedDict):
954 """Configuration object for the X axis."""
955
956 dataKey: str
957 """Field name from each data row to use for X-axis categories."""
958 hide: NotRequired[bool]
959 """Hide the X axis line, ticks, and labels when true."""
960 labels: NotRequired[dict[str, str]]
961 """Custom mapping of tick values to display labels."""
962
963
964CurveType = Literal[
965 "basis",
966 "basisClosed",
967 "basisOpen",
968 "bumpX",
969 "bumpY",
970 "bump",
971 "linear",
972 "linearClosed",
973 "natural",
974 "monotoneX",
975 "monotoneY",
976 "monotone",
977 "step",
978 "stepBefore",
979 "stepAfter",
980]
981"""Interpolation curve types for `area` and `line` series."""
982
983
984@_direct_usage_of_named_widget_types_deprecated
985class BarSeries(BaseModel):
986 """A bar series plotted from a numeric `dataKey`. Supports stacking."""
987
988 type: Literal["bar"] = Field(default="bar", frozen=True)
989 label: str | None
990 """Legend label for the series."""
991 dataKey: str
992 """Field name from each data row that contains the numeric value."""
993 stack: str | None = None
994 """Optional stack group ID. Series with the same ID stack together."""
995 color: str | ThemeColor | None = None
996 """
997 Color for the series; accepts chart color token, a primitive color token, a CSS string, or theme-aware { light, dark }.
998
999 Chart color tokens: `blue` `purple` `orange` `green` `red` `yellow` `pink`
1000
1001 Primitive color token, e.g., `red-100`, `blue-900`, `gray-500`
1002
1003 Note: By default, a color will be sequentially assigned from the chart series colors.
1004 """
1005
1006
1007@_direct_usage_of_named_widget_types_deprecated
1008class AreaSeries(BaseModel):
1009 """An area series plotted from a numeric `dataKey`. Supports stacking and curves."""
1010
1011 type: Literal["area"] = Field(default="area", frozen=True)
1012 label: str | None
1013 """Legend label for the series."""
1014 dataKey: str
1015 """Field name from each data row that contains the numeric value."""
1016 stack: str | None = None
1017 """Optional stack group ID. Series with the same ID stack together."""
1018 color: str | ThemeColor | None = None
1019 """
1020 Color for the series; accepts chart color token, a primitive color token, a CSS string, or theme-aware { light, dark }.
1021
1022 Chart color tokens: `blue` `purple` `orange` `green` `red` `yellow` `pink`
1023
1024 Primitive color token, e.g., `red-100`, `blue-900`, `gray-500`
1025
1026 Note: By default, a color will be sequentially assigned from the chart series colors.
1027 """
1028 curveType: None | Literal[CurveType] = None
1029 """Interpolation curve type used to connect points."""
1030
1031
1032@_direct_usage_of_named_widget_types_deprecated
1033class LineSeries(BaseModel):
1034 """A line series plotted from a numeric `dataKey`. Supports curves."""
1035
1036 type: Literal["line"] = Field(default="line", frozen=True)
1037 label: str | None
1038 """Legend label for the series."""
1039 dataKey: str
1040 """Field name from each data row that contains the numeric value."""
1041 color: str | ThemeColor | None = None
1042 """
1043 Color for the series; accepts chart color token, a primitive color token, a CSS string, or theme-aware { light, dark }.
1044
1045 Chart color tokens: `blue` `purple` `orange` `green` `red` `yellow` `pink`
1046
1047 Primitive color token, e.g., `red-100`, `blue-900`, `gray-500`
1048
1049 Note: By default, a color will be sequentially assigned from the chart series colors.
1050 """
1051 curveType: None | Literal[CurveType] = None
1052 """Interpolation curve type used to connect points."""
1053
1054
1055Series = Annotated[
1056 BarSeries | AreaSeries | LineSeries,
1057 Field(discriminator="type"),
1058]
1059"""Union of all supported chart series types."""
1060
1061
1062class DynamicWidgetComponent(WidgetComponentBase):
1063 """
1064 A widget component with a statically defined base shape but dynamically
1065 defined additional fields loaded from a widget template or JSON schema.
1066 """
1067
1068 model_config = ConfigDict(extra="allow")
1069 children: DynamicWidgetComponent | list[DynamicWidgetComponent] | None = None
1070
1071
1072StrictWidgetComponent = Annotated[
1073 Text
1074 | Title
1075 | Caption
1076 | Chart
1077 | Badge
1078 | Markdown
1079 | Box
1080 | Row
1081 | Col
1082 | Divider
1083 | Icon
1084 | Image
1085 | ListViewItem
1086 | Button
1087 | Checkbox
1088 | Spacer
1089 | Select
1090 | DatePicker
1091 | Form
1092 | Input
1093 | Label
1094 | RadioGroup
1095 | Textarea
1096 | Transition,
1097 Field(discriminator="type"),
1098]
1099
1100
1101class BasicRoot(DynamicWidgetComponent):
1102 """Layout root capable of nesting components or other roots."""
1103
1104 type: Literal["Basic"] = Field(default="Basic", frozen=True) # pyright: ignore
1105
1106
1107StrictWidgetRoot = Annotated[
1108 Card | ListView | BasicRoot,
1109 Field(discriminator="type"),
1110]
1111
1112
1113class DynamicWidgetRoot(DynamicWidgetComponent):
1114 """Dynamic root widget restricted to root types."""
1115
1116 type: Literal["Card", "ListView", "Basic"] # pyright: ignore
1117
1118
1119WidgetComponent = StrictWidgetComponent | DynamicWidgetComponent
1120"""Union of all renderable widget components."""
1121
1122WidgetRoot = StrictWidgetRoot | DynamicWidgetRoot
1123"""Union of all renderable top-level widgets."""
1124
1125
1126WidgetIcon = IconName
1127"""Icon names accepted by widgets that render icons."""
1128
1129
1130class WidgetTemplate:
1131 """
1132 Utility for loading and building widgets from a .widget file.
1133
1134 Example using .widget file on disc:
1135 ```python
1136 template = WidgetTemplate.from_file("path/to/my_widget.widget")
1137 widget = template.build({"name": "Harry Potter"})
1138 ```
1139
1140 Example using already parsed widget definition:
1141 ```python
1142 template = WidgetTemplate(definition={"version": "1.0", "name": "...", "template": Template(...), "jsonSchema": {...}})
1143 widget = template.build({"name": "Harry Potter"})
1144 ```
1145 """
1146
1147 def __init__(self, definition: dict[str, Any]):
1148 self.version = definition["version"]
1149 if self.version != "1.0":
1150 raise ValueError(f"Unsupported widget spec version: {self.version}")
1151
1152 self.name = definition["name"]
1153 template = definition["template"]
1154 if isinstance(template, Template):
1155 self.template = template
1156 else:
1157 self.template = _jinja_env.from_string(template)
1158 self.data_schema = definition.get("jsonSchema", {})
1159
1160 @classmethod
1161 def from_file(cls, file_path: str) -> WidgetTemplate:
1162 path = Path(file_path)
1163 if not path.is_absolute():
1164 caller_frame = inspect.stack()[1]
1165 caller_path = Path(caller_frame.filename).resolve()
1166 path = caller_path.parent / path
1167
1168 with path.open("r", encoding="utf-8") as file:
1169 payload = json.load(file)
1170
1171 return cls(payload)
1172
1173 def build(
1174 self, data: dict[str, Any] | BaseModel | None = None
1175 ) -> DynamicWidgetRoot:
1176 """Render the widget template with the given data and return a DynamicWidgetRoot instance."""
1177 rendered = self.template.render(**self._normalize_data(data))
1178 widget_dict = json.loads(rendered)
1179 return DynamicWidgetRoot.model_validate(widget_dict)
1180
1181 @deprecated("WidgetTemplate.build_basic is deprecated. Use WidgetTemplate.build instead.")
1182 def build_basic(self, data: dict[str, Any] | BaseModel | None = None) -> BasicRoot:
1183 """Deprecated alias for building Basic root widgets."""
1184 rendered = self.template.render(**self._normalize_data(data))
1185 widget_dict = json.loads(rendered)
1186 return BasicRoot.model_validate(widget_dict)
1187
1188 def _normalize_data(
1189 self, data: dict[str, Any] | BaseModel | None
1190 ) -> dict[str, Any]:
1191 if data is None:
1192 return {}
1193 return data.model_dump() if isinstance(data, BaseModel) else data