@treeui/mcp 0.5.0 → 0.6.0

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.
@@ -1,8 +1,8 @@
1
1
  // src/generated/treeui.catalog.json
2
2
  var treeui_catalog_default = {
3
- generatedAt: "2026-07-18T23:35:26.876Z",
3
+ generatedAt: "2026-07-21T19:17:20.408Z",
4
4
  packageName: "@treeui/vue",
5
- packageVersion: "0.16.0",
5
+ packageVersion: "0.19.0",
6
6
  decisionGuides: [
7
7
  {
8
8
  id: "single-choice",
@@ -53,6 +53,14 @@ var treeui_catalog_default = {
53
53
  "The options are hierarchical."
54
54
  ],
55
55
  notes: []
56
+ },
57
+ {
58
+ use: "TLanguageSelect",
59
+ when: [
60
+ "The value is the application language or locale.",
61
+ "Each language should show a country flag beside its name."
62
+ ],
63
+ notes: []
56
64
  }
57
65
  ]
58
66
  },
@@ -365,6 +373,32 @@ var treeui_catalog_default = {
365
373
  "Prefer `TAppShell` (recipe `app-shell`) when you also need full-viewport sizing and a responsive mobile drawer."
366
374
  ]
367
375
  },
376
+ {
377
+ id: "nav-menu-icons",
378
+ name: "Icons on navigation items",
379
+ useWhen: [
380
+ "Navigation items each carry an icon.",
381
+ "The navigation collapses to an icon-only rail, so the icon is the only thing left identifying a section.",
382
+ "Some of the icons the app needs are not TreeUI built-ins."
383
+ ],
384
+ compose: [
385
+ "TNavMenu",
386
+ "TIcon",
387
+ "registerTreeIcons",
388
+ "resetTreeIcons"
389
+ ],
390
+ notes: [
391
+ "Set `icon` on the item, as a registered NAME: `{ label: 'Dashboard', value: 'dashboard', icon: 'layout-dashboard' }`. Do not import an icon component per item, and do not put a `TIcon` in the `#item` slot just to get an icon \u2014 the item renders it for you.",
392
+ "`TNavMenuItem.icon` is `TIconInput`, so a component is accepted too. Prefer the name: `items` normally lives in a `ref`, a `computed`, or a route config, and a string is never made reactive \u2014 a component object there needs `markRaw` or Vue warns.",
393
+ "An item with no icon (or an unregistered name) falls back to a letter marker, and a `console.warn` lists the valid names \u2014 once per name, in production builds too. Give every item an icon before shipping a collapsible rail, or the collapsed state is a column of letters.",
394
+ "Icons the app needs but TreeUI does not ship are REGISTERED, not worked around: one `registerTreeIcons` call in `main.ts` and they are usable by name everywhere. Never hand-roll an `<svg>` \u2014 without the `absoluteStrokeWidth` correction its stroke scales with the icon (1.67px at size 20, 4px at 48), so it looks bolder than its neighbours at every size above 24.",
395
+ "Check the name is not already taken before registering it \u2014 `hasTreeIcon(name)` answers that. Registering one of the 364 built-in names replaces that built-in everywhere, silently, which is a supported swap but never what you want when the goal was adding an icon.",
396
+ "Keep the icon vocabulary consistent: built-ins use TreeUI's descriptive kebab-case names, so register custom ones with the same convention.",
397
+ "Icon registration is process-wide state. In component tests that register icons, call `resetTreeIcons()` in an `afterEach` so one case's icons do not leak into the next.",
398
+ "Set `to` for the route target on each item \u2014 `TNavMenuItem` has no `href` and no `id`; identity is `value`.",
399
+ "This recipe covers the icons only; the surrounding layout is its own recipe (`app-shell` or `sidebar-shell`)."
400
+ ]
401
+ },
368
402
  {
369
403
  id: "stat-grid",
370
404
  name: "Dashboard metric grid",
@@ -538,6 +572,148 @@ var treeui_catalog_default = {
538
572
  notes: [
539
573
  'Use `selectionMode="multiple"` when permissions or tag assignment spans nested nodes.'
540
574
  ]
575
+ },
576
+ {
577
+ id: "language-switcher",
578
+ name: "Application language switcher",
579
+ useWhen: [
580
+ "The app lets the user pick a language or locale, with an optional flag per option."
581
+ ],
582
+ compose: [
583
+ "TFormField",
584
+ "TLanguageSelect"
585
+ ],
586
+ notes: [
587
+ "The app owns the language list: TreeUI ships no locale dataset, so pass `options` with your own `label`, `value`, and optional `code` and `description`.",
588
+ "`TLanguageSelect` only emits the chosen `value` through `update:modelValue`; the app performs every locale side effect (i18n locale, persistence, `document.documentElement.lang`, route or API updates).",
589
+ "Set `code` only where a flag is meaningful \u2014 a language is not a country \u2014 and omit it to render that option without a flag.",
590
+ "Set `flagBaseUrl` once, at the switcher, when flag assets are self-hosted; it is forwarded to every nested `TFlag`.",
591
+ "Wrap with `TFormField` when the switcher sits in a form and needs a label, hint, or error text; drop the wrapper for a bare toolbar or header control."
592
+ ]
593
+ }
594
+ ],
595
+ practices: [
596
+ {
597
+ id: "interaction-feedback",
598
+ title: "Visual interaction feedback",
599
+ summary: "Every interaction answers back. Hover, focus, active, loading, and disabled are designed states \u2014 never afterthoughts left to the consumer.",
600
+ rules: [
601
+ "Interactive surfaces ship hover, active, and focus-visible treatments out of the box.",
602
+ "Loading states announce themselves (aria-busy, spinner) and block duplicate activation.",
603
+ "Disabled and loading elements never emit activation events.",
604
+ "Focus indicators stay visible in both light and dark themes."
605
+ ],
606
+ components: [
607
+ "TButton",
608
+ "TLink",
609
+ "TInput",
610
+ "TSelect",
611
+ "TCheckbox",
612
+ "TSwitch",
613
+ "TTabs",
614
+ "TPagination",
615
+ "TDropdown",
616
+ "TSelectableList"
617
+ ]
618
+ },
619
+ {
620
+ id: "accessible-by-default",
621
+ title: "Accessible by default",
622
+ summary: "Native semantics first, ARIA where the pattern requires it, full keyboard support, and focus management the consumer never has to write.",
623
+ rules: [
624
+ 'Native elements before roles; role="dialog", aria-modal, aria-labelledby only where semantics require them.',
625
+ "Tab/Shift+Tab order, Escape to close, Enter/Space to activate, arrow keys where the pattern expects them.",
626
+ "Overlays trap focus while open and restore it on close.",
627
+ "Clickable targets stay at or above 44\xD744px."
628
+ ],
629
+ components: [
630
+ "TModal",
631
+ "TDrawer",
632
+ "TConfirmDialog",
633
+ "TDropdown",
634
+ "TTooltip",
635
+ "TAccordion",
636
+ "TFormField",
637
+ "TTreeView",
638
+ "TToastProvider"
639
+ ]
640
+ },
641
+ {
642
+ id: "predictable-api",
643
+ title: "One predictable API",
644
+ summary: "The same contracts everywhere: one size scale, one action-variant scale, v-model for form controls, open/close for overlays. Learn one component and you know them all.",
645
+ rules: [
646
+ "Sizes are sm | md | lg across the library.",
647
+ "Action variants come from one scale: solid, outline, ghost, soft, danger.",
648
+ "Form controls speak modelValue + update:modelValue (v-model).",
649
+ "Overlays speak open/defaultOpen + update:open/open-change."
650
+ ],
651
+ components: [
652
+ "TButton",
653
+ "TInput",
654
+ "TSelect",
655
+ "TBadge",
656
+ "TTag",
657
+ "TCard",
658
+ "TModal"
659
+ ]
660
+ },
661
+ {
662
+ id: "state-clarity",
663
+ title: "Empty, loading, and error are designed states",
664
+ summary: "Screens communicate status honestly: skeletons while loading, purposeful empty states, and status surfaces on one semantic tone scale.",
665
+ rules: [
666
+ "Default, hover, active, focus, disabled, loading, and error/empty states are designed and documented \u2014 not just the happy path.",
667
+ "Status tones map to semantic tokens (success, warning, error, info) and pass both themes.",
668
+ "Async feedback is immediate: skeleton or spinner first, toast or alert on completion."
669
+ ],
670
+ components: [
671
+ "TSkeleton",
672
+ "TSpinner",
673
+ "TProgress",
674
+ "TEmptyState",
675
+ "TAlert",
676
+ "TToastProvider",
677
+ "TBadge"
678
+ ]
679
+ },
680
+ {
681
+ id: "content-alignment",
682
+ title: "Alignment without custom CSS",
683
+ summary: "Layout primitives absorb spacing, alignment, and rhythm so product code never hand-rolls the same flexbox again.",
684
+ rules: [
685
+ "Spacing comes from the --tree-space-* scale, applied by layout components.",
686
+ "Compose TPage, TStack, TGrid, and TSplit before writing a custom layout div.",
687
+ "Forms align through TFormField + TStack + TGrid \u2014 labels, hints, and errors line up by contract.",
688
+ "Reading widths and page gutters come from TContainer/TPage, not local max-widths."
689
+ ],
690
+ components: [
691
+ "TStack",
692
+ "TGrid",
693
+ "TSplit",
694
+ "TContainer",
695
+ "TSpacer",
696
+ "TPage",
697
+ "TPageHeader",
698
+ "TFormField"
699
+ ]
700
+ },
701
+ {
702
+ id: "token-driven",
703
+ title: "Token-driven clarity",
704
+ summary: "No raw hex, px, or font values anywhere. Every visual decision references a --tree-* token, so light/dark and brand theming stay coherent by construction.",
705
+ rules: [
706
+ "Zero hardcoded colors, spacing, or typography in component code.",
707
+ "Light and dark are first-class themes; every state passes both.",
708
+ "Themes switch by attribute (data-tree-theme) with no component changes."
709
+ ],
710
+ components: [
711
+ "TCard",
712
+ "TNavbar",
713
+ "TBadge",
714
+ "TTag",
715
+ "TChart"
716
+ ]
541
717
  }
542
718
  ],
543
719
  setup: {
@@ -612,6 +788,43 @@ var treeui_catalog_default = {
612
788
  "Use `maxVisibleTags` to control trigger density in compact layouts."
613
789
  ]
614
790
  },
791
+ TFlag: {
792
+ required: [
793
+ "Pass `code` as an ISO 3166-1 alpha-2 country code. Matching is case-insensitive \u2014 the value is trimmed and lowercased internally.",
794
+ "Allow `https://flagcdn.com` in the app `Content-Security-Policy` `img-src` directive, or point `baseUrl` at a self-hosted mirror \u2014 flag images are fetched from a third-party CDN at runtime."
795
+ ],
796
+ notes: [
797
+ "A mirror must serve `{baseUrl}/{segment}/{code}.png` for both steps of every size: 1x `h20` (sm), `h20` (md), `h24` (lg); 2x `h40` (sm), `h40` (md), `h60` (lg). The 2x asset is emitted through `srcset`.",
798
+ "Those fixed-height segments are part of the contract, and they are the CDN's flat artwork \u2014 a mirror must serve flat flags, not the waving ones.",
799
+ "Do not substitute the CDN's ratio-normalised width-by-height endpoints. They equalise aspect ratio only by switching to the waving artwork, which reads as decoration next to flat UI icons.",
800
+ "The fixed-height assets keep each flag's true proportions \u2014 Nepal is a pennant, Switzerland is square, Qatar is elongated. A shared height, not a shared ratio, is what aligns a column of flags.",
801
+ "CSS puts the image in a fixed 3:2 box (3:2 being the most common national flag ratio) with `object-fit: contain`, so outliers centre inside the box instead of being cropped or distorted.",
802
+ "Each retina segment is at least twice its box height \u2014 boxes are `--tree-size-icon-sm/md/lg` = 16px/20px/24px \u2014 so 2x screens never upscale.",
803
+ "`baseUrl` may be written with or without a trailing slash; trailing slashes are stripped before the path is built. An unknown `size` falls back to the `md` assets.",
804
+ "When the image cannot load \u2014 offline, blocked by CSP, unknown code \u2014 the component renders the uppercased `code` as text. Override that text with `fallbackText`.",
805
+ "The flag is `aria-hidden` by default. Pass `label` only when the flag carries meaning on its own; beside a visible country or language name it should stay decorative."
806
+ ]
807
+ },
808
+ TLanguageSelect: {
809
+ required: [
810
+ "Pass `options` as `{ label, value, code?, description?, disabled? }[]`. TreeUI ships no language, locale, or country dataset \u2014 the app owns its language list.",
811
+ "Apply locale side effects (i18n locale, `lang` attribute, persistence) in the app layer; the component only reports the selected `value`.",
812
+ "Pass an `aria-label` whenever `iconOnly` is set \u2014 the visible language name is gone, so it is the control's only accessible name. It also labels the listbox."
813
+ ],
814
+ notes: [
815
+ "`variant` is `field` (default) or `switcher`, typed as `TLanguageSelectVariant` and exported from `@treeui/vue`.",
816
+ 'Use `variant="field"` inside a form, next to other inputs, or wrapped in `TFormField`: the flag leads the trigger, the trigger fills its container, and `width` applies. A visible field label already says what the control changes.',
817
+ 'Use `variant="switcher"` for a page-level control in a navbar or app bar, where nothing nearby labels it. A translate icon (`TIcon name="languages"`) leads and the current flag trails before the chevron, so the control reads as "page language, now set to this" without an external label.',
818
+ "A switcher sizes to its content, so `width` does not apply, and its listbox aligns to the trigger's right edge.",
819
+ "`iconOnly` is switcher-only and drops the visible language name, leaving the translate icon and the current flag.",
820
+ "`value` is an opaque application key, typically a BCP-47 tag such as `pt-BR`. TreeUI never parses or validates it.",
821
+ "`code` is the ISO 3166-1 alpha-2 code used for the option flag. Omit it to render the option without a flag \u2014 a language is not a country, so many entries have no honest flag.",
822
+ "Use `description` for a secondary line such as the endonym.",
823
+ "Set `flagBaseUrl` once on the select rather than per option; it is forwarded to every nested `TFlag`.",
824
+ "Bind `modelValue` to a `string`, and use `v-model:open` when the app controls open state.",
825
+ "The CDN and Content-Security-Policy requirements under `TFlag` apply whenever any option carries a `code`."
826
+ ]
827
+ },
615
828
  TToggleGroup: {
616
829
  required: [],
617
830
  notes: [
@@ -661,7 +874,20 @@ var treeui_catalog_default = {
661
874
  TNavMenu: {
662
875
  required: [],
663
876
  notes: [
664
- "When nested inside `TSidebar` or `TAppShell`, `collapsed` can be inherited from the sidebar provider."
877
+ "When nested inside `TSidebar` or `TAppShell`, `collapsed` can be inherited from the sidebar provider.",
878
+ "Pass `items` as `{ label, value, icon?, to?, badge?, description?, shortLabel?, disabled?, exact? }[]`.",
879
+ "`icon` is `TIconInput` \u2014 a registered icon name (`icon: 'layout-dashboard'`) or a component. Use the name: it needs no import, and no `markRaw` when the item list is reactive.",
880
+ "An item with no icon, or with a name that is not registered, shows a letter marker instead. That marker is also all a collapsed rail can show, so give every item an icon when the sidebar collapses.",
881
+ "Register any icon the app needs beyond the built-ins first \u2014 see `icons` above."
882
+ ]
883
+ },
884
+ TIcon: {
885
+ required: [
886
+ "Use a registered name: one of the built-ins, or one the app added with `registerTreeIcons`. See `icons` above."
887
+ ],
888
+ notes: [
889
+ '`size` is a pixel count, not `sm | md | lg`. Passing `size="md"` renders an unsized svg.',
890
+ "The icon is `aria-hidden` by default. Pass `label` only when the icon carries meaning on its own; beside visible text it should stay decorative."
665
891
  ]
666
892
  },
667
893
  TTreeView: {
@@ -733,6 +959,7 @@ var treeui_catalog_default = {
733
959
  ],
734
960
  commonCompositions: [
735
961
  "TFormField + input-like control",
962
+ "TFormField + TLanguageSelect",
736
963
  "TGrid + TStat",
737
964
  "TStat + TSparkline",
738
965
  "TCard + TChart",
@@ -756,15 +983,83 @@ var treeui_catalog_default = {
756
983
  storyFile: "apps/docs/src/stories/Accordion.stories.ts",
757
984
  storyTitle: "Components/Data Display/Accordion",
758
985
  manifestFile: "docs/ai/COMPONENTS/accordion.yaml",
759
- props: [],
760
- events: [],
761
- slots: [],
986
+ props: [
987
+ {
988
+ name: "type",
989
+ type: "'single' | 'multiple'",
990
+ default: "'single'",
991
+ values: [
992
+ "single",
993
+ "multiple"
994
+ ],
995
+ description: "single \u2013 at most one item open at a time (opening one closes the rest). multiple \u2013 any number of items may be open simultaneously.\n"
996
+ },
997
+ {
998
+ name: "modelValue",
999
+ type: "string | string[] | undefined",
1000
+ default: "undefined",
1001
+ description: 'Controlled open state. For type="single" pass a string (the item value). For type="multiple" pass an array of strings. The accordion treats itself as controlled only while modelValue !== undefined; passing undefined leaves it uncontrolled and it falls back to its internal state.\n'
1002
+ },
1003
+ {
1004
+ name: "defaultValue",
1005
+ type: "string | string[] | undefined",
1006
+ default: "undefined",
1007
+ description: "Initial open state for uncontrolled usage. Read once when the accordion is created \u2014 changing it after mount has no effect. Ignored while modelValue is set. Do not combine it with modelValue: see notes.\n"
1008
+ },
1009
+ {
1010
+ name: "disabled",
1011
+ type: "boolean",
1012
+ default: "false",
1013
+ description: "Disables all items. OR-ed with each item's own disabled prop, so an item is disabled when either is true. Provided to items as a computed ref, so flipping it after mount updates every item.\n"
1014
+ },
1015
+ {
1016
+ name: "collapsible",
1017
+ type: "boolean",
1018
+ default: "false",
1019
+ description: 'Only applies when type="single". When true, the open item can be collapsed by clicking its trigger again, which emits undefined. When false, clicking the already-open trigger is a no-op and emits nothing. In type="multiple" any item can always be collapsed and this prop is ignored.\n'
1020
+ }
1021
+ ],
1022
+ events: [
1023
+ {
1024
+ name: "update:modelValue",
1025
+ description: 'Emitted when the open state changes (v-model support). type="single" emits the open item value as a string, or undefined when it is collapsed. type="multiple" always emits a string[] of the open values.\n',
1026
+ payload: "string | string[] | undefined"
1027
+ }
1028
+ ],
1029
+ slots: [
1030
+ {
1031
+ name: "default",
1032
+ description: "Accepts TAccordionItem children."
1033
+ }
1034
+ ],
762
1035
  a11y: [
763
- 'roles: {"trigger":"heading > button","panel":"region"}',
764
- 'attributes: {"trigger":["aria-expanded (reflects open state)","aria-controls (references panel id)"],"panel":["role=\\"region\\"","aria-labelledby (references trigger id)"]}',
765
- 'keyboard: [{"key":"Enter / Space","action":"Toggle the focused item"},{"key":"ArrowDown","action":"Move focus to the next item trigger"},{"key":"ArrowUp","action":"Move focus to the previous item trigger"},{"key":"Home","action":"Move focus to the first item trigger"},{"key":"End","action":"Move focus to the last item trigger"}]'
1036
+ "container_is_a_plain_div_with_no_aria_role: true",
1037
+ "trigger_is_a_heading_wrapped_native_button: h3 > button",
1038
+ "aria_expanded_on_trigger_reflects_open_state: true",
1039
+ "aria_controls_on_trigger_references_panel_id: true",
1040
+ "panel_uses_role_region: true",
1041
+ "aria_labelledby_on_panel_references_trigger_id: true",
1042
+ "disabled_items_use_the_native_disabled_attribute: true",
1043
+ "chevron_icon_is_aria_hidden: true",
1044
+ "trigger_hit_area_is_at_least_44px: via the .t-accordion__trigger::after overlay",
1045
+ "enter_space_toggle_the_focused_item: native button activation",
1046
+ "arrow_down_focuses_next_enabled_trigger_wrapping_to_first: true",
1047
+ "arrow_up_focuses_previous_enabled_trigger_wrapping_to_last: true",
1048
+ "home_focuses_first_enabled_trigger: true",
1049
+ "end_focuses_last_enabled_trigger: true"
1050
+ ],
1051
+ behavior: [
1052
+ "controlled_only_while_model_value_is_not_undefined: true",
1053
+ "default_value_seeds_uncontrolled_state_once: true",
1054
+ "single_mode_opening_an_item_closes_the_previous_one: true",
1055
+ "single_mode_collapsing_requires_collapsible: true",
1056
+ "multiple_mode_ignores_collapsible: true",
1057
+ "container_and_item_disabled_are_combined_with_or: true",
1058
+ "arrow_navigation_skips_disabled_triggers: true",
1059
+ "arrow_navigation_wraps_around: true",
1060
+ "trigger_order_follows_item_mount_order: true",
1061
+ "only_open_panels_are_rendered: true"
766
1062
  ],
767
- behavior: [],
768
1063
  selection: {
769
1064
  keywords: [
770
1065
  "accordion",
@@ -795,7 +1090,10 @@ var treeui_catalog_default = {
795
1090
  required: [],
796
1091
  notes: []
797
1092
  },
798
- recipes: []
1093
+ recipes: [],
1094
+ practices: [
1095
+ "accessible-by-default"
1096
+ ]
799
1097
  },
800
1098
  {
801
1099
  name: "TAlert",
@@ -812,14 +1110,15 @@ var treeui_catalog_default = {
812
1110
  props: [
813
1111
  {
814
1112
  name: "variant",
815
- type: "'info' | 'success' | 'warning' | 'danger'",
1113
+ type: "TAlertVariant",
816
1114
  default: "info",
817
1115
  values: [
818
1116
  "info",
819
1117
  "success",
820
1118
  "warning",
821
1119
  "danger"
822
- ]
1120
+ ],
1121
+ description: "Semantic tone. Sets the --tree-alert-* custom properties that colour the surface, border, icon, and dismiss button."
823
1122
  },
824
1123
  {
825
1124
  name: "size",
@@ -829,42 +1128,55 @@ var treeui_catalog_default = {
829
1128
  "sm",
830
1129
  "md",
831
1130
  "lg"
832
- ]
1131
+ ],
1132
+ description: "Adjusts padding and font size only. md is the baseline defined on .t-alert itself."
833
1133
  },
834
1134
  {
835
1135
  name: "dismissible",
836
1136
  type: "boolean",
837
- default: "false"
1137
+ default: "false",
1138
+ description: "Renders the trailing dismiss button. The button is the only thing gated by this prop; the alert never hides itself."
838
1139
  }
839
1140
  ],
840
1141
  events: [
841
1142
  {
842
1143
  name: "dismiss",
843
- description: "Emitted when the dismiss button is clicked.",
1144
+ description: "Emitted when the dismiss button is clicked. TAlert is stateless \u2014 it does not hide, unmount, or animate itself out. The consumer owns removal (v-if, a list splice, or a store flag); without a handler the button is inert.\n",
844
1145
  payload: "none"
845
1146
  }
846
1147
  ],
847
1148
  slots: [
848
1149
  {
849
1150
  name: "default",
850
- description: "Alert message content."
1151
+ description: "Alert message content, rendered inside .t-alert__content."
851
1152
  },
852
1153
  {
853
1154
  name: "icon",
854
- description: "Optional leading icon; rendered with aria-hidden."
1155
+ description: 'Optional leading icon. The .t-alert__icon wrapper span is rendered only when the slot is provided (v-if="$slots.icon") and is marked aria-hidden, so the icon is decorative \u2014 never put meaning there that is not also in the message text. The wrapper inherits the variant colour via --tree-alert-accent.\n'
855
1156
  },
856
1157
  {
857
1158
  name: "dismiss-icon",
858
- description: "Custom dismiss button content; replaces the default \xD7 icon."
1159
+ description: 'Replaces the default 16x16 \xD7 SVG inside the dismiss button. Rendered only when dismissible is true, since the slot lives inside the button. Has a working default, so it is optional. The button keeps its aria-label="Dismiss" regardless of content.\n'
859
1160
  }
860
1161
  ],
861
1162
  a11y: [
862
1163
  "role: alert",
1164
+ "role_is_on_root_element: true",
863
1165
  "icon_slot_marked_aria_hidden: true",
1166
+ "default_dismiss_icon_marked_aria_hidden: true",
864
1167
  "dismiss_button_aria_label: Dismiss",
865
- "keyboard: Tab to dismiss button, Enter/Space to activate"
1168
+ "dismiss_button_label_is_hardcoded: true",
1169
+ "dismiss_button_hit_area: 44x44 via a .t-alert__dismiss::after pseudo-element; the visible button is 1.5rem.",
1170
+ "keyboard: Tab to dismiss button, Enter/Space to activate",
1171
+ 'live_region_caveat: role="alert" is an assertive live region. An alert that is already in the DOM on first paint is generally not announced \u2014 mount it dynamically (v-if / list insert) when you need the message read out.'
1172
+ ],
1173
+ behavior: [
1174
+ "self_removes_on_dismiss: false",
1175
+ "icon_wrapper_rendered_only_when_icon_slot_provided: true",
1176
+ "dismiss_button_rendered_only_when_dismissible: true",
1177
+ "size_md_emits_class_without_css_rule: t-alert--md is always emitted but carries no CSS rule of its own; .t-alert is the md baseline and only --sm and --lg override padding and font size.",
1178
+ 'variant_sets_local_custom_properties: ["--tree-alert-bg","--tree-alert-border","--tree-alert-text","--tree-alert-accent"]'
866
1179
  ],
867
- behavior: [],
868
1180
  selection: {
869
1181
  keywords: [
870
1182
  "alert",
@@ -895,7 +1207,10 @@ var treeui_catalog_default = {
895
1207
  required: [],
896
1208
  notes: []
897
1209
  },
898
- recipes: []
1210
+ recipes: [],
1211
+ practices: [
1212
+ "state-clarity"
1213
+ ]
899
1214
  },
900
1215
  {
901
1216
  name: "TAppShell",
@@ -913,7 +1228,8 @@ var treeui_catalog_default = {
913
1228
  {
914
1229
  name: "as",
915
1230
  type: "string",
916
- default: "div"
1231
+ default: "div",
1232
+ note: "Tag or component rendered as the shell root. The root is a plain `div` by default and is therefore not a landmark \u2014 the header/sidebar/main regions carry the semantics."
917
1233
  },
918
1234
  {
919
1235
  name: "side",
@@ -927,164 +1243,254 @@ var treeui_catalog_default = {
927
1243
  {
928
1244
  name: "sidebarWidth",
929
1245
  type: "string",
930
- default: "17.5rem"
1246
+ default: "17.5rem",
1247
+ note: "Any CSS length. Sets `--tree-app-shell-sidebar-width` on the root, which drives the desktop rail track and also caps the mobile drawer at `min(100%, sidebarWidth)`."
931
1248
  },
932
1249
  {
933
1250
  name: "collapsedWidth",
934
1251
  type: "string",
935
- default: "4.75rem"
1252
+ default: "4.75rem",
1253
+ note: "Any CSS length. Sets `--tree-app-shell-collapsed-width` on the root, but it only takes effect while the root carries `is-collapsed` (desktop + `collapsible` + collapsed)."
936
1254
  },
937
1255
  {
938
1256
  name: "collapsible",
939
1257
  type: "boolean",
940
- default: "false"
1258
+ default: "false",
1259
+ note: "Master gate for the desktop rail. While false the root never gets `is-collapsed`, the built-in collapse toggle is not rendered, and `toggleCollapsed` is a no-op."
941
1260
  },
942
1261
  {
943
1262
  name: "collapsed",
944
1263
  type: "boolean",
945
- default: "undefined"
1264
+ default: "undefined",
1265
+ note: "Controlled desktop rail state (v-model:collapsed). Leave undefined for uncontrolled. When set, the component only emits \u2014 the parent must apply the new value."
946
1266
  },
947
1267
  {
948
1268
  name: "defaultCollapsed",
949
1269
  type: "boolean",
950
- default: "false"
1270
+ default: "false",
1271
+ note: "Initial value for uncontrolled use. Read once during setup; later changes to the prop are ignored."
951
1272
  },
952
1273
  {
953
1274
  name: "mobile",
954
1275
  type: "boolean",
955
- default: "undefined"
1276
+ default: "undefined",
1277
+ note: "Forces mobile mode when explicitly true or false. Leave undefined to auto-detect from `breakpoint`."
956
1278
  },
957
1279
  {
958
1280
  name: "breakpoint",
959
1281
  type: "string",
960
- default: "768px"
1282
+ default: "768px",
1283
+ note: "Any CSS length. Evaluated as `(max-width: <breakpoint>)` via matchMedia, and only consulted while `mobile` is undefined. Changing it tears down and rebuilds the media query listener."
961
1284
  },
962
1285
  {
963
1286
  name: "sidebarOpen",
964
1287
  type: "boolean",
965
- default: "undefined"
1288
+ default: "undefined",
1289
+ note: "Controlled off-canvas drawer state (v-model:sidebarOpen). Mobile-only \u2014 the flag is tracked in desktop mode but nothing renders from it."
966
1290
  },
967
1291
  {
968
1292
  name: "defaultSidebarOpen",
969
1293
  type: "boolean",
970
- default: "false"
1294
+ default: "false",
1295
+ note: "Initial value for uncontrolled use. Read once during setup."
1296
+ },
1297
+ {
1298
+ name: "immersive",
1299
+ type: "boolean",
1300
+ default: "undefined",
1301
+ note: "Controlled immersive state (v-model:immersive). Hides the header and sidebar so the content fills the viewport. The chrome is hidden with CSS rather than unmounted, so toggling never remounts the default slot. While immersive, the mobile drawer is not rendered."
1302
+ },
1303
+ {
1304
+ name: "defaultImmersive",
1305
+ type: "boolean",
1306
+ default: "false",
1307
+ note: "Initial value for uncontrolled use. Read once during setup."
971
1308
  },
972
1309
  {
973
1310
  name: "closeOnEscape",
974
1311
  type: "boolean",
975
- default: "true"
1312
+ default: "true",
1313
+ note: "Drawer only. Escape is handled on the drawer surface, so it fires only while focus is inside the drawer."
976
1314
  },
977
1315
  {
978
1316
  name: "closeOnOverlay",
979
1317
  type: "boolean",
980
- default: "true"
1318
+ default: "true",
1319
+ note: "Drawer only. Set false to keep the drawer open on backdrop clicks."
981
1320
  },
982
1321
  {
983
1322
  name: "showMenuButton",
984
1323
  type: "boolean",
985
- default: "true"
1324
+ default: "true",
1325
+ note: "Renders the built-in hamburger in the header. Mobile only. Set false to supply your own trigger and call the `toggleSidebar` slot prop from the `header` or `header-start` slot."
986
1326
  },
987
1327
  {
988
1328
  name: "showCollapseButton",
989
1329
  type: "boolean",
990
1330
  default: "true",
991
- note: "Renders the built-in collapse toggle in the sidebar footer. Desktop only, and only when `collapsible` is set \u2014 no need to wire your own."
1331
+ note: "Renders the built-in collapse toggle in the sidebar footer. Desktop only, and only when `collapsible` is set \u2014 no need to wire your own. Never rendered in the mobile drawer."
992
1332
  },
993
1333
  {
994
1334
  name: "collapseLabel",
995
1335
  type: "string",
996
- default: "Collapse sidebar"
1336
+ default: "Collapse sidebar",
1337
+ note: "aria-label of the collapse toggle while the rail is expanded."
997
1338
  },
998
1339
  {
999
1340
  name: "expandLabel",
1000
1341
  type: "string",
1001
- default: "Expand sidebar"
1342
+ default: "Expand sidebar",
1343
+ note: "aria-label of the collapse toggle while the rail is collapsed."
1002
1344
  },
1003
1345
  {
1004
1346
  name: "menuLabel",
1005
1347
  type: "string",
1006
- default: "Open menu"
1348
+ default: "Open menu",
1349
+ note: "aria-label of the built-in hamburger. Static \u2014 it does not swap to a close label while the drawer is open."
1007
1350
  },
1008
1351
  {
1009
1352
  name: "closeLabel",
1010
1353
  type: "string",
1011
- default: "Close menu"
1354
+ default: "Close menu",
1355
+ note: "aria-label of the drawer's close button."
1012
1356
  },
1013
1357
  {
1014
1358
  name: "sidebarLabel",
1015
1359
  type: "string",
1016
- default: "Sidebar"
1360
+ default: "Sidebar",
1361
+ note: "aria-label applied to both the desktop <aside> and the drawer <aside>."
1017
1362
  }
1018
1363
  ],
1019
1364
  events: [
1020
1365
  {
1021
- name: "update:collapsed"
1366
+ name: "update:collapsed",
1367
+ description: "Desktop rail collapsed state changed. Enables v-model:collapsed.",
1368
+ payload: "[value: boolean]"
1022
1369
  },
1023
1370
  {
1024
- name: "collapse-change"
1371
+ name: "collapse-change",
1372
+ description: "Same change as update:collapsed, for non-v-model usage. Both always fire together.",
1373
+ payload: "[value: boolean]"
1374
+ },
1375
+ {
1376
+ name: "update:sidebarOpen",
1377
+ description: "Off-canvas drawer open state changed. Enables v-model:sidebarOpen.",
1378
+ payload: "[value: boolean]"
1025
1379
  },
1026
1380
  {
1027
- name: "update:sidebarOpen"
1381
+ name: "sidebar-open-change",
1382
+ description: "Same change as update:sidebarOpen, for non-v-model usage. Both always fire together.",
1383
+ payload: "[value: boolean]"
1384
+ },
1385
+ {
1386
+ name: "update:immersive",
1387
+ description: "Immersive state changed. Enables v-model:immersive.",
1388
+ payload: "[value: boolean]"
1028
1389
  },
1029
1390
  {
1030
- name: "sidebar-open-change"
1391
+ name: "immersive-change",
1392
+ description: "Same change as update:immersive, for non-v-model usage. Both always fire together.",
1393
+ payload: "[value: boolean]"
1031
1394
  }
1032
1395
  ],
1033
1396
  slots: [
1034
1397
  {
1035
- name: "header"
1398
+ name: "header",
1399
+ description: "Main header region. Spans the full header width by default; once `header-start` is filled it is constrained to the content column so it lines up with the panel underneath. On mobile the header falls back to a single flex row either way.\n",
1400
+ note: "AppShellSlotProps \u2014 { mobile, collapsed, sidebarOpen, immersive, toggleSidebar, closeSidebar, toggleCollapsed, toggleImmersive }"
1036
1401
  },
1037
1402
  {
1038
- name: "header-start"
1403
+ name: "header-start",
1404
+ description: "Header region sitting over the sidebar rail (typically the brand mark). Filling it adds `has-start` to the header, which switches the header to the shell's own two-column grid and tracks the rail width as it collapses.\n",
1405
+ note: "AppShellSlotProps"
1039
1406
  },
1040
1407
  {
1041
- name: "sidebar-header"
1408
+ name: "sidebar-header",
1409
+ description: "Pinned above the sidebar body, inset to line up with nav item icons. Rendered in both the desktop aside and the mobile drawer.\n",
1410
+ note: "AppShellSlotProps"
1042
1411
  },
1043
1412
  {
1044
- name: "sidebar"
1413
+ name: "sidebar",
1414
+ description: "Sidebar body \u2014 the natural home for a TNavMenu, which picks up the collapsed state through provide/inject. Rendered in both the desktop aside and the mobile drawer, and takes the vertical slack so the footer pins to the bottom.\n",
1415
+ note: "AppShellSlotProps"
1045
1416
  },
1046
1417
  {
1047
- name: "sidebar-footer"
1418
+ name: "sidebar-footer",
1419
+ description: "Pinned to the bottom of the sidebar, inset to line up with nav item icons. In the desktop aside it shares one container with the built-in collapse toggle, which is rendered after the slot content; in the mobile drawer the container holds the slot content alone.\n",
1420
+ note: "AppShellSlotProps"
1048
1421
  },
1049
1422
  {
1050
- name: "default"
1423
+ name: "default",
1424
+ description: "Main content area. Scrolls independently of the header and sidebar.",
1425
+ note: "{ mobile: boolean, collapsed: boolean, immersive: boolean, toggleImmersive: () => void } \u2014 a subset of AppShellSlotProps. toggleImmersive is included because immersive hides the chrome, so the content is the only place an exit control can live."
1051
1426
  },
1052
1427
  {
1053
- name: "menu-icon"
1428
+ name: "menu-icon",
1429
+ description: "Replaces the default hamburger glyph inside the built-in menu button. Use `sidebarOpen` to swap the icon while the drawer is open. Renders inside the button, so it must be presentational only.\n",
1430
+ note: "{ sidebarOpen: boolean }"
1054
1431
  },
1055
1432
  {
1056
- name: "collapse-icon"
1433
+ name: "collapse-icon",
1434
+ description: "Replaces the default glyph inside the built-in collapse toggle. Renders inside the button, so it must be presentational only.\n",
1435
+ note: "{ collapsed: boolean }"
1057
1436
  }
1058
1437
  ],
1059
1438
  a11y: [
1060
- "role: dialog",
1439
+ "semantic_regions: <header>, <aside> and <main>; the shell root itself is a plain div and is not a landmark",
1440
+ "role: dialog \u2014 on the mobile drawer only",
1061
1441
  "aria_modal: true",
1062
- "focus_trap: true",
1063
- "restores_focus_on_close: true",
1442
+ "focus_trap: Tab and Shift+Tab cycle inside the drawer; with no focusable descendant, focus is pinned to the drawer itself",
1443
+ "restores_focus_on_close: only when the drawer was opened through the built-in menu button or the toggleSidebar slot prop \u2014 those capture document.activeElement first",
1064
1444
  "escape_closes_when_enabled: true",
1065
1445
  "menu_button_aria_expanded: true",
1066
- "menu_button_aria_controls_drawer: true",
1067
- "sidebar_labelled_by_sidebar_label: true"
1446
+ "menu_button_aria_controls_drawer: aria-controls always points at the sidebar id, and on mobile that element exists only while the drawer is open",
1447
+ "collapse_button_aria_expanded: reflects the expanded rail, i.e. !collapsed",
1448
+ "collapse_button_aria_controls_sidebar: true",
1449
+ "collapse_button_label_swaps_by_state: collapseLabel while expanded, expandLabel while collapsed",
1450
+ "drawer_close_button_labelled_by_close_label: true",
1451
+ "sidebar_labelled_by_sidebar_label: true",
1452
+ "drawer_focusable_via_tabindex_minus_one: true",
1453
+ "backdrop_is_aria_hidden: true",
1454
+ "built_in_icons_are_aria_hidden: true",
1455
+ "menu_and_drawer_close_buttons_are_44px: true"
1068
1456
  ],
1069
1457
  behavior: [
1070
1458
  "fills_viewport_full_width_and_height: true",
1459
+ "resets_body_margin_while_a_shell_is_mounted: body:has(.t-app-shell) { margin: 0 } \u2014 a deliberate global rule, scoped so it only applies to pages that mount a shell",
1071
1460
  "header_spans_full_width_above_sidebar_and_content: true",
1072
1461
  "content_area_scrolls_independently: true",
1073
1462
  "auto_switches_to_mobile_at_breakpoint_when_mobile_undefined: true",
1074
1463
  "mobile_prop_forces_mobile_mode: true",
1075
1464
  "mobile_sidebar_becomes_off_canvas_drawer: true",
1465
+ "desktop_sidebar_is_removed_from_the_dom_on_mobile: crossing the breakpoint unmounts and remounts the sidebar slot, so component state inside it is not preserved",
1466
+ "drawer_is_rendered_only_while_mobile_and_open: true",
1076
1467
  "auto_menu_button_in_header_on_mobile: true",
1077
1468
  "controlled_and_uncontrolled_collapsed_state: true",
1078
1469
  "controlled_and_uncontrolled_sidebar_open_state: true",
1470
+ "controlled_and_uncontrolled_immersive_state: true",
1471
+ "immersive_hides_chrome_without_remounting_content: the header and sidebar are hidden with CSS and the grid collapses to a single `main` area, so the default slot keeps its component state across the toggle \u2014 unlike the mobile breakpoint, which does remount the sidebar slot",
1472
+ "immersive_suppresses_the_mobile_drawer: while immersive the overlay is not rendered, since the trigger that opens it lives in the hidden header",
1473
+ "controlled_mode_does_not_self_update: when collapsed, sidebarOpen or immersive is bound, the component only emits \u2014 the parent must apply the value",
1474
+ "no_emit_when_the_value_is_unchanged: true",
1475
+ "toggle_collapsed_is_a_noop_when_collapsible_is_false: true",
1476
+ "collapse_toggle_requires_desktop_and_collapsible_and_show_collapse_button: true",
1079
1477
  "provides_collapsed_state_to_descendants: true",
1080
1478
  "drawer_reports_collapsed_false_to_descendants_and_slots: true",
1081
1479
  "left_and_right_variants: true",
1082
1480
  "locks_body_scroll_while_drawer_open: true",
1481
+ "releases_the_scroll_lock_when_leaving_mobile_while_open: true",
1482
+ "rearms_the_overlay_when_reentering_mobile_while_open: true",
1083
1483
  "overlay_click_closes_when_enabled: true",
1084
1484
  "exposes_slot_props_with_mobile_collapsed_and_toggles: true",
1085
1485
  "sidebar_footer_pinned_to_bottom: true",
1086
1486
  "sidebar_header_and_footer_inset_to_align_with_nav_icons: true",
1087
- "header_mirrors_shell_columns_when_header_start_is_used: true"
1487
+ "header_mirrors_shell_columns_when_header_start_is_used: true",
1488
+ "header_start_falls_back_to_a_single_flex_row_on_mobile: true",
1489
+ "animates_the_rail_width_via_grid_template_columns: true",
1490
+ "main_panel_has_a_rounded_inner_corner_on_the_sidebar_side_on_desktop: true",
1491
+ "drawer_width_is_capped_at_min_of_100_percent_and_sidebar_width: true",
1492
+ 'root_data_attribute: data-mobile="true" | "false" on the shell root',
1493
+ "respects_prefers_reduced_motion: true"
1088
1494
  ],
1089
1495
  selection: {
1090
1496
  keywords: [
@@ -1131,14 +1537,15 @@ var treeui_catalog_default = {
1131
1537
  },
1132
1538
  recipes: [
1133
1539
  "app-shell"
1134
- ]
1540
+ ],
1541
+ practices: []
1135
1542
  },
1136
1543
  {
1137
1544
  name: "TAvatar",
1138
1545
  aliases: [],
1139
1546
  package: "@treeui/vue",
1140
1547
  category: "Data Display",
1141
- description: "User image with fallback initials and optional status indicator dot.",
1548
+ description: "User avatar that renders an image when `src` is set, otherwise initials derived from `alt`, plus an optional status indicator dot.",
1142
1549
  sourceFile: "packages/vue/src/components/TAvatar.vue",
1143
1550
  storyFile: "apps/docs/src/stories/Avatar.stories.ts",
1144
1551
  storyTitle: "Components/Data Display/Avatar",
@@ -1148,19 +1555,19 @@ var treeui_catalog_default = {
1148
1555
  name: "src",
1149
1556
  type: "string",
1150
1557
  required: false,
1151
- description: "URL of the avatar image."
1558
+ description: "URL of the avatar image. When set, the `<img>` branch renders and the initials span is NOT in the DOM at all (`v-else`). If the image fails to load, the `<img>` is hidden via inline `display: none` \u2014 the component does NOT fall back to initials, leaving an empty circle. Pass no `src` if you want initials.\n"
1152
1559
  },
1153
1560
  {
1154
1561
  name: "alt",
1155
1562
  type: "string",
1156
1563
  default: '""',
1157
- description: "Accessible label for the avatar. Also used to derive initials if `initials` is not set."
1564
+ description: "Accessible label for the avatar and the `alt` attribute of the `<img>`. Also the source for derived initials when `initials` is not set. Defaults to an empty string, in which case no `aria-label` is rendered at all (`alt || undefined`) and derived initials are empty \u2014 always pass a meaningful `alt`.\n"
1158
1565
  },
1159
1566
  {
1160
1567
  name: "initials",
1161
1568
  type: "string",
1162
1569
  required: false,
1163
- description: "Explicit initials to display when no image is provided. Overrides derived initials from `alt`."
1570
+ description: 'Explicit initials, rendered only when `src` is unset. Takes precedence over initials derived from `alt`. When omitted, initials are derived from `alt` by splitting on spaces, taking the first character of the first two words, and uppercasing (e.g. "Jane Doe" -> "JD").\n'
1164
1571
  },
1165
1572
  {
1166
1573
  name: "size",
@@ -1170,7 +1577,8 @@ var treeui_catalog_default = {
1170
1577
  "sm",
1171
1578
  "md",
1172
1579
  "lg"
1173
- ]
1580
+ ],
1581
+ description: "Fixed square dimensions \u2014 sm 2rem, md 2.5rem, lg 3.5rem \u2014 with matching font size for initials."
1174
1582
  },
1175
1583
  {
1176
1584
  name: "status",
@@ -1182,20 +1590,23 @@ var treeui_catalog_default = {
1182
1590
  "busy",
1183
1591
  "away"
1184
1592
  ],
1185
- description: "Shows a colored status dot on the avatar."
1593
+ description: "Shows a colored status dot pinned to the bottom-right of the avatar bounding box. Renders alongside either branch (image or initials). The dot sits on the corner outside the circular mask, so `.t-avatar` deliberately has no `overflow: hidden`.\n"
1186
1594
  }
1187
1595
  ],
1188
1596
  events: [],
1189
1597
  slots: [
1190
1598
  {
1191
1599
  name: "default",
1192
- description: "Optional overlay content inside the avatar."
1600
+ description: 'Optional overlay content, rendered inside the `.t-avatar` span after the image/initials and before the status dot. The parent is `position: relative`, so children can be absolutely positioned. Note the wrapper carries `role="img"`, which makes its subtree presentational to assistive technology \u2014 slot content is visual only and will not be announced.\n'
1193
1601
  }
1194
1602
  ],
1195
1603
  a11y: [
1196
- "role: img",
1197
- "aria-label: Derived from `alt` prop.",
1198
- "status_dot: Has its own `aria-label` with the status value."
1604
+ "role: img (always set on the root span, including when only initials render)",
1605
+ 'aria-label: Set from `alt`. Omitted entirely when `alt` is empty (the default), which leaves the `role="img"` element without an accessible name.',
1606
+ 'initials: Marked `aria-hidden="true"` \u2014 the name comes from `alt`, not the visible text.',
1607
+ "image: Receives the `alt` prop as its native `alt` attribute.",
1608
+ "status_dot: Carries `aria-label` with the status value, but the element is a bare `<span>` with no role, so the label is not exposed to assistive technology. Convey status in text if it matters.",
1609
+ 'slot_content: Not announced \u2014 the `role="img"` root makes descendant content presentational.'
1199
1610
  ],
1200
1611
  behavior: [],
1201
1612
  selection: {
@@ -1208,7 +1619,8 @@ var treeui_catalog_default = {
1208
1619
  required: [],
1209
1620
  notes: []
1210
1621
  },
1211
- recipes: []
1622
+ recipes: [],
1623
+ practices: []
1212
1624
  },
1213
1625
  {
1214
1626
  name: "TBadge",
@@ -1233,7 +1645,8 @@ var treeui_catalog_default = {
1233
1645
  "ghost",
1234
1646
  "soft",
1235
1647
  "danger"
1236
- ]
1648
+ ],
1649
+ description: "Surface treatment. Selects which --tree-badge-<variant>-* colour variables the badge consumes; danger is a surface style, not a semantic guarantee \u2014 its red palette is replaced when tone is success, warning, or info."
1237
1650
  },
1238
1651
  {
1239
1652
  name: "tone",
@@ -1246,7 +1659,7 @@ var treeui_catalog_default = {
1246
1659
  "danger",
1247
1660
  "info"
1248
1661
  ],
1249
- description: "Optional semantic tone layered on top of the visual variant."
1662
+ description: "Semantic palette. Re-maps the --tree-badge-* colour variables that the chosen variant consumes, so it recolours the badge without changing its surface treatment. neutral keeps the brand palette."
1250
1663
  },
1251
1664
  {
1252
1665
  name: "size",
@@ -1256,7 +1669,8 @@ var treeui_catalog_default = {
1256
1669
  "sm",
1257
1670
  "md",
1258
1671
  "lg"
1259
- ]
1672
+ ],
1673
+ description: "Changes min-height and font-size only; inline padding is constant across sizes."
1260
1674
  }
1261
1675
  ],
1262
1676
  events: [],
@@ -1269,10 +1683,19 @@ var treeui_catalog_default = {
1269
1683
  }
1270
1684
  ],
1271
1685
  a11y: [
1686
+ "semantic_element: span",
1272
1687
  "interactive_by_default: false",
1273
- "icon_slot_marked_aria_hidden: true"
1688
+ "icon_slot_marked_aria_hidden: true",
1689
+ "tone_conveys_meaning_by_color_only: No role or aria state is set, so put the status word in the badge text rather than relying on tone alone."
1690
+ ],
1691
+ behavior: [
1692
+ "always_applies_one_modifier_per_prop: true",
1693
+ "tone_remaps_variant_color_variables: true",
1694
+ "tone_overrides_variant_danger_palette: true",
1695
+ "tone_neutral_has_no_stylesheet_rule: true",
1696
+ "icon_wrapper_rendered_only_when_slot_filled: true",
1697
+ "icon_renders_before_default_slot: true"
1274
1698
  ],
1275
- behavior: [],
1276
1699
  selection: {
1277
1700
  keywords: [],
1278
1701
  useWhen: [],
@@ -1285,6 +1708,11 @@ var treeui_catalog_default = {
1285
1708
  },
1286
1709
  recipes: [
1287
1710
  "pricing-section"
1711
+ ],
1712
+ practices: [
1713
+ "predictable-api",
1714
+ "state-clarity",
1715
+ "token-driven"
1288
1716
  ]
1289
1717
  },
1290
1718
  {
@@ -1304,23 +1732,32 @@ var treeui_catalog_default = {
1304
1732
  name: "separator",
1305
1733
  type: "string",
1306
1734
  default: "/",
1307
- description: "Character or string used to separate breadcrumb items."
1735
+ description: "Character or string shown between items. Applied as the inline CSS custom property --tree-breadcrumb-separator on the <ol>, wrapped in single quotes, and drawn by the .t-breadcrumb__item:not(:last-child)::after pseudo-element \u2014 it never enters the DOM as text. A value containing a single quote or a backslash produces an invalid CSS value and the separator stops rendering."
1308
1736
  }
1309
1737
  ],
1310
1738
  events: [],
1311
1739
  slots: [
1312
1740
  {
1313
- name: "default"
1741
+ name: "default",
1742
+ description: "Breadcrumb trail content, rendered inside the <ol class='t-breadcrumb__list'>. Expects TBreadcrumbItem children so each entry becomes an <li>. No scoped props."
1314
1743
  }
1315
1744
  ],
1316
1745
  a11y: [
1317
1746
  "element: nav",
1318
1747
  "aria_label: Breadcrumb",
1748
+ "aria_label_hardcoded_but_overridable_via_fallthrough_attrs: true",
1319
1749
  "current_page_aria_current: page",
1320
1750
  "uses_ol_for_ordered_list: true",
1321
1751
  "interactive_by_default: false"
1322
1752
  ],
1323
- behavior: [],
1753
+ behavior: [
1754
+ "link_target_precedence: to + resolvable RouterLink, then href, then string to",
1755
+ "string_to_without_router_renders_native_anchor: true",
1756
+ "object_to_without_router_renders_current_span: true",
1757
+ "current_state_is_not_positional: true",
1758
+ "separator_rendered_by_css_pseudo_element: true",
1759
+ "separator_ignores_pointer_events: true"
1760
+ ],
1324
1761
  selection: {
1325
1762
  keywords: [],
1326
1763
  useWhen: [],
@@ -1334,7 +1771,8 @@ var treeui_catalog_default = {
1334
1771
  "Use `to` when the app has vue-router and breadcrumb items should render RouterLink."
1335
1772
  ]
1336
1773
  },
1337
- recipes: []
1774
+ recipes: [],
1775
+ practices: []
1338
1776
  },
1339
1777
  {
1340
1778
  name: "TButton",
@@ -1352,7 +1790,8 @@ var treeui_catalog_default = {
1352
1790
  {
1353
1791
  name: "as",
1354
1792
  type: "string",
1355
- default: "button"
1793
+ default: "button",
1794
+ note: "Root tag rendered through `<component :is>`. Any value other than `button` switches the component onto the non-native accessibility path (see `a11y`) and stops the `type` attribute from being rendered."
1356
1795
  },
1357
1796
  {
1358
1797
  name: "variant",
@@ -1376,23 +1815,50 @@ var treeui_catalog_default = {
1376
1815
  "sm",
1377
1816
  "md",
1378
1817
  "lg"
1379
- ]
1818
+ ],
1819
+ note: "`t-button--md` is emitted but carries no CSS rule of its own \u2014 md is the base sizing, and only sm/lg override height, padding, and font size."
1380
1820
  },
1381
1821
  {
1382
1822
  name: "disabled",
1383
1823
  type: "boolean",
1384
- default: "false"
1824
+ default: "false",
1825
+ note: 'Applied as the native `disabled` attribute only when `as` is `button`; for any other tag the component falls back to `aria-disabled="true"` plus `tabindex="-1"`. Clicks are blocked in both cases.'
1385
1826
  },
1386
1827
  {
1387
1828
  name: "loading",
1388
1829
  type: "boolean",
1389
1830
  default: "false",
1390
- note: "loading implies disabled behavior"
1831
+ note: "loading implies disabled behavior \u2014 the effective disabled state is `disabled || loading`, so `loading` on its own disables the button and applies `is-disabled`."
1832
+ },
1833
+ {
1834
+ name: "loadingLabel",
1835
+ type: "string",
1836
+ default: "Loading",
1837
+ note: "Accessible announcement while loading, forwarded to the nested TSpinner's `label`. The default is English because TreeUI ships no i18n layer \u2014 pass the active locale's string to localize it (see DECISIONS.md \u2192 Locale Data)."
1838
+ },
1839
+ {
1840
+ name: "hideIconWhileLoading",
1841
+ type: "boolean",
1842
+ default: "true",
1843
+ note: "While loading, the `icon` slot is not rendered so the spinner replaces the icon instead of rendering beside it. Set to `false` to restore the pre-0.19 behavior of showing both."
1844
+ },
1845
+ {
1846
+ name: "iconOnly",
1847
+ type: "boolean",
1848
+ default: "false",
1849
+ note: "Square, icon-only button \u2014 collapses the inline padding to a 1:1 control matching the size token and suppresses the `t-button__label` wrapper. Requires an accessible name via `label` (or `aria-label`), since the icon slot is aria-hidden."
1850
+ },
1851
+ {
1852
+ name: "label",
1853
+ type: "string",
1854
+ default: "undefined",
1855
+ note: "Accessible name, rendered as `aria-label`. Required whenever `iconOnly` is set; ignored for ordinary buttons that already have visible text."
1391
1856
  },
1392
1857
  {
1393
1858
  name: "type",
1394
1859
  type: "button | submit | reset",
1395
- default: "button"
1860
+ default: "button",
1861
+ note: 'Only forwarded when `as` is `button`; for any other tag no `type` attribute is rendered. Set `type="submit"` for the submit control of an enclosing form.'
1396
1862
  },
1397
1863
  {
1398
1864
  name: "block",
@@ -1404,12 +1870,14 @@ var treeui_catalog_default = {
1404
1870
  name: "align",
1405
1871
  type: "start | center | end",
1406
1872
  default: "center",
1407
- note: "Content alignment; only meaningful together with `block`. Use `start` for left-aligned icon+label menu rows."
1873
+ note: "Content alignment; only meaningful once the button is wider than its content, which in practice means together with `block`, since the base rule is `min-width: max-content`. Use `start` for left-aligned icon+label menu rows. The default `center` emits no modifier class \u2014 it is the base rule."
1408
1874
  }
1409
1875
  ],
1410
1876
  events: [
1411
1877
  {
1412
- name: "click"
1878
+ name: "click",
1879
+ description: "Emitted on activation. Suppressed while disabled or loading: the internal handler calls preventDefault() and stopPropagation() and returns without emitting.",
1880
+ payload: "[event: MouseEvent]"
1413
1881
  }
1414
1882
  ],
1415
1883
  slots: [
@@ -1422,13 +1890,22 @@ var treeui_catalog_default = {
1422
1890
  ],
1423
1891
  a11y: [
1424
1892
  "native_button_by_default: true",
1893
+ "native_disabled_attr_only_when_button: true",
1425
1894
  "aria_busy_when_loading: true",
1426
1895
  "aria_disabled_for_non_button: true",
1427
- "tabindex_minus_one_when_non_button_disabled: true"
1896
+ "tabindex_minus_one_when_non_button_disabled: true",
1897
+ "icon_slot_marked_aria_hidden: true",
1898
+ "loading_spinner_is_announced: The nested TSpinner carries role=\"status\" and a visually hidden label, so the button's accessible name gains that text while loading. The text comes from `loadingLabel`, so it follows the app's locale.",
1899
+ "icon_only_requires_an_accessible_name: With `iconOnly` there is no visible text and the icon slot is aria-hidden, so the name must come from `label` or `aria-label`. This is a documented requirement, not a runtime assertion \u2014 the package ships ESM+CJS with no dev-only build define, so there is no dev-stripped warning.",
1900
+ "touch_targets: 44\xD744px minimum via the .t-button::after overlay",
1901
+ "focus_visible: box-shadow focus ring on the root element"
1428
1902
  ],
1429
1903
  behavior: [
1430
1904
  "blocks_click_when_disabled_or_loading: true",
1431
- "renders_spinner_when_loading: true"
1905
+ "renders_spinner_when_loading: true",
1906
+ "spinner_replaces_the_icon_slot_while_loading: The spinner takes the icon's leading position, so by default the icon slot is withheld while loading and the order is spinner, label. Set `hideIconWhileLoading: false` for the older spinner, icon, label order.",
1907
+ "icon_slot_wrapper_rendered_only_when_filled: true",
1908
+ "default_slot_wrapped_in_label_element_unless_icon_only: true"
1432
1909
  ],
1433
1910
  selection: {
1434
1911
  keywords: [],
@@ -1443,6 +1920,10 @@ var treeui_catalog_default = {
1443
1920
  recipes: [
1444
1921
  "confirm-destructive-action",
1445
1922
  "pricing-section"
1923
+ ],
1924
+ practices: [
1925
+ "interaction-feedback",
1926
+ "predictable-api"
1446
1927
  ]
1447
1928
  },
1448
1929
  {
@@ -1452,7 +1933,7 @@ var treeui_catalog_default = {
1452
1933
  kind: "layout",
1453
1934
  category: "Data Display",
1454
1935
  status: "available",
1455
- purpose: "Surface container with optional header and footer slots.",
1936
+ purpose: "Surface container with optional header, actions, and footer slots.",
1456
1937
  sourceFile: "packages/vue/src/components/TCard.vue",
1457
1938
  storyFile: "apps/docs/src/stories/Card.stories.ts",
1458
1939
  storyTitle: "Components/Data Display/Card",
@@ -1461,7 +1942,8 @@ var treeui_catalog_default = {
1461
1942
  {
1462
1943
  name: "as",
1463
1944
  type: "string",
1464
- default: "section"
1945
+ default: "section",
1946
+ description: 'Tag or component used for the polymorphic root, rendered through `<component :is="as">`. Change it to `article`, `aside`, `li`, or `div` when the default `section` element is wrong for the surrounding document structure. It only swaps the element \u2014 classes, slots, and layout are unchanged.\n'
1465
1947
  },
1466
1948
  {
1467
1949
  name: "variant",
@@ -1477,37 +1959,44 @@ var treeui_catalog_default = {
1477
1959
  name: "size",
1478
1960
  type: "TSize",
1479
1961
  default: "md",
1480
- values: [
1481
- "sm",
1482
- "md",
1483
- "lg"
1484
- ]
1962
+ description: "Padding scale only \u2014 it does not change typography, border, or radius. `sm` uses --tree-space-3, `md` --tree-space-4, `lg` --tree-space-5.\n"
1485
1963
  },
1486
1964
  {
1487
1965
  name: "title",
1488
1966
  type: "string",
1489
1967
  default: "undefined",
1490
- description: "Optional card heading rendered inside the header area."
1968
+ description: "Optional card heading. Rendered as the fallback content of the `header` slot inside a `span.t-card__title`, so supplying `#header` replaces it entirely. Setting it also forces the header element to render. Because it is declared as a prop it is consumed by the component and never reaches the root element as the native HTML `title` tooltip attribute.\n"
1491
1969
  }
1492
1970
  ],
1493
1971
  events: [],
1494
1972
  slots: [
1495
1973
  {
1496
- name: "header"
1974
+ name: "header",
1975
+ description: "Replaces the `title` fallback inside `header.t-card__header`. When this slot is supplied the `title` prop renders nothing. Receives no slot props.\n"
1497
1976
  },
1498
1977
  {
1499
- name: "actions"
1978
+ name: "actions",
1979
+ description: "Trailing controls wrapped in `span.t-card__actions`, rendered as a sibling after the header content inside the same `header.t-card__header`. Supplying it is enough to render the header on its own, with no `title` and no `header` slot. Receives no slot props.\n"
1500
1980
  },
1501
1981
  {
1502
- name: "default"
1982
+ name: "default",
1983
+ description: "Card body. Always rendered, wrapped in `div.t-card__body`. Receives no slot props.\n"
1503
1984
  },
1504
1985
  {
1505
- name: "footer"
1986
+ name: "footer",
1987
+ description: "Renders `footer.t-card__footer` only when the slot is supplied; there is no `footer` prop fallback. Receives no slot props.\n"
1506
1988
  }
1507
1989
  ],
1508
- a11y: [],
1990
+ a11y: [
1991
+ "root_role: None applied by the component \u2014 semantics come entirely from `as` (default `section`).",
1992
+ "title_is_not_a_heading: `title` renders a `span.t-card__title`, styled at heading size and weight but carrying no heading semantics, so it does not enter the document outline. When the card must be reachable by heading navigation, put a real `<h2>`/`<h3>` in the `header` slot instead of using the prop.",
1993
+ "unnamed_section_root: The default `section` root has no accessible name, so assistive tech exposes it as generic rather than as a region. The component sets no `inheritAttrs: false` and has a single root, so `aria-label` / `aria-labelledby` passed by the consumer fall through to that root when a named region is wanted."
1994
+ ],
1509
1995
  behavior: [
1510
1996
  "body_wrapper_class: t-card__body",
1997
+ "header_render_condition: The `header.t-card__header` element renders when ANY of the `header` slot, a truthy `title` prop, or the `actions` slot is present. All three branches are independent \u2014 actions alone still produces a header.",
1998
+ "footer_render_condition: The `footer.t-card__footer` element renders only when the `footer` slot is supplied.",
1999
+ "slot_props: None. No slot on this component is scoped; all four are plain content slots.",
1511
2000
  'variant_effects: {"outline":"Base surface with a default border. The styling baseline \u2014 the `t-card--outline` class is emitted but carries no CSS rule of its own.","soft":"Subtle tinted surface (bg-subtle), same border and shadow as outline.","inset":"Same tinted surface as soft, but with no shadow, so it reads as recessed inside another surface."}',
1512
2001
  "note: No card variant inverts its surface, so nested content inherits the normal color tokens. For a high-emphasis card, use a brand-colored border rather than an inverted background."
1513
2002
  ],
@@ -1542,6 +2031,10 @@ var treeui_catalog_default = {
1542
2031
  },
1543
2032
  recipes: [
1544
2033
  "chart-surface"
2034
+ ],
2035
+ practices: [
2036
+ "predictable-api",
2037
+ "token-driven"
1545
2038
  ]
1546
2039
  },
1547
2040
  {
@@ -1571,13 +2064,13 @@ var treeui_catalog_default = {
1571
2064
  name: "series",
1572
2065
  type: "TChartSeries[]",
1573
2066
  default: "[]",
1574
- note: "Each item is { name: string; data: number[]; color?: string }."
2067
+ note: "Each item is { name: string; data: number[]; color?: string }. Series with an empty data array are dropped before rendering \u2014 they never reach the plot, legend, tooltip, or a11y table."
1575
2068
  },
1576
2069
  {
1577
2070
  name: "labels",
1578
2071
  type: "string[]",
1579
2072
  default: "[]",
1580
- note: "X-axis categories, aligned index-for-index to each series' data."
2073
+ note: "X-axis categories, aligned index-for-index to each series' data. Labels past the longest series are ignored; a missing label falls back to '#<n>' in the tooltip and to an empty header cell in the a11y table."
1581
2074
  },
1582
2075
  {
1583
2076
  name: "height",
@@ -1589,80 +2082,91 @@ var treeui_catalog_default = {
1589
2082
  name: "smooth",
1590
2083
  type: "boolean",
1591
2084
  default: "false",
1592
- note: "Curved line/area."
2085
+ note: "Curved line/area; ignored for bar."
1593
2086
  },
1594
2087
  {
1595
2088
  name: "stacked",
1596
2089
  type: "boolean",
1597
2090
  default: "false",
1598
- note: "Stacks bar series; ignored for line/area. Negative values are clamped to 0 in stacked mode (grouped mode renders them)."
2091
+ note: "Stacks bar series; ignored for line/area. Negative values are clamped to 0 in stacked mode (grouped mode renders them below the baseline). The tooltip always reports the raw per-series values, not the cumulative stack."
1599
2092
  },
1600
2093
  {
1601
2094
  name: "showGrid",
1602
2095
  type: "boolean",
1603
- default: "true"
2096
+ default: "true",
2097
+ note: "Horizontal gridlines at the y ticks. Independent of showYAxis, which controls the tick text."
1604
2098
  },
1605
2099
  {
1606
2100
  name: "showLegend",
1607
- type: "boolean",
1608
- default: "auto (true when more than one series)"
2101
+ type: "boolean | undefined",
2102
+ default: "auto (true when more than one series)",
2103
+ note: "The legend is suppressed whenever there is no data, even when set to true."
1609
2104
  },
1610
2105
  {
1611
2106
  name: "showXAxis",
1612
2107
  type: "boolean",
1613
- default: "true"
2108
+ default: "true",
2109
+ note: "When false the category labels are skipped and the bottom margin collapses from 28px to 10px."
1614
2110
  },
1615
2111
  {
1616
2112
  name: "showYAxis",
1617
2113
  type: "boolean",
1618
- default: "true"
2114
+ default: "true",
2115
+ note: "When false the tick text is skipped and the left margin collapses from 44px to 10px. Gridlines are unaffected."
1619
2116
  },
1620
2117
  {
1621
2118
  name: "showTooltip",
1622
2119
  type: "boolean",
1623
- default: "true"
2120
+ default: "true",
2121
+ note: "Gates hover tracking as a whole: the crosshair, the tooltip panel, and the point-click event all stop when false."
1624
2122
  },
1625
2123
  {
1626
2124
  name: "showPoints",
1627
2125
  type: "boolean",
1628
2126
  default: "false",
1629
- note: "Always draw markers on line/area points."
2127
+ note: "Always draw markers on line/area points; ignored for bar."
1630
2128
  },
1631
2129
  {
1632
2130
  name: "yTicks",
1633
2131
  type: "number",
1634
- default: "5"
2132
+ default: "5",
2133
+ note: "Target tick count handed to niceScale \u2014 the rendered count may differ once the bounds are rounded."
1635
2134
  },
1636
2135
  {
1637
2136
  name: "valueFormat",
1638
2137
  type: "(value: number) => string",
1639
- default: "locale integers for |value| >= 1000"
2138
+ default: "locale integers for |value| >= 1000",
2139
+ note: "Applies to y-axis ticks, tooltip values, and the a11y table cells."
1640
2140
  },
1641
2141
  {
1642
2142
  name: "minY",
1643
2143
  type: "number",
1644
- default: "auto"
2144
+ default: "auto (min(0, data min) for bar/area, data min for line)",
2145
+ note: "A hint, not a hard bound: niceScale rounds the low end down, so the rendered domain can start below it."
1645
2146
  },
1646
2147
  {
1647
2148
  name: "maxY",
1648
2149
  type: "number",
1649
- default: "auto"
2150
+ default: "auto",
2151
+ note: "A hint, not a hard bound: niceScale rounds the high end up, so the rendered domain can extend above it."
1650
2152
  },
1651
2153
  {
1652
2154
  name: "animated",
1653
2155
  type: "boolean",
1654
2156
  default: "true",
1655
- note: "Draw-in animation; disabled under prefers-reduced-motion."
2157
+ note: "Draw-in animation via the is-animated root class; disabled under prefers-reduced-motion."
1656
2158
  },
1657
2159
  {
1658
2160
  name: "loading",
1659
2161
  type: "boolean",
1660
- default: "false"
2162
+ default: "false",
2163
+ note: "Replaces the plot with five pulsing skeleton bars and adds the is-loading root class. Takes precedence over the empty state; the legend still renders when data is present."
1661
2164
  },
1662
2165
  {
1663
2166
  name: "ariaLabel",
1664
2167
  type: "string",
1665
- default: "auto-generated summary"
2168
+ default: "auto-generated summary ('<type> chart with <n> series')",
2169
+ note: "Rendered as the <caption> of the visually-hidden data table \u2014 it is not applied as an aria-label attribute on the root."
1666
2170
  }
1667
2171
  ],
1668
2172
  events: [
@@ -1684,14 +2188,19 @@ var treeui_catalog_default = {
1684
2188
  ],
1685
2189
  a11y: [
1686
2190
  "SVG is aria-hidden; a visually-hidden <table> mirrors the series data for assistive tech.",
2191
+ "The table's <caption> carries ariaLabel (or the auto-generated summary); the root div itself has no role or accessible name.",
1687
2192
  "Legend defaults on for multiple series so identity is not color-only.",
1688
- "Provide a meaningful ariaLabel describing what the chart shows."
2193
+ "Provide a meaningful ariaLabel describing what the chart shows.",
2194
+ "point-click has no keyboard equivalent \u2014 do not make it the only way to reach a behavior."
1689
2195
  ],
1690
2196
  behavior: [
1691
- "colors_auto_assign_from_chart_palette: --tree-color-chart-1..8, cycling; per-series color overrides",
1692
- "bar_zero_baseline: true",
1693
- "line_domain_uses_nice_ticks: true",
1694
- "hover_shows_crosshair_and_tooltip: true"
2197
+ "colors_auto_assign_from_chart_palette: --tree-color-chart-1..8, cycling by series index; per-series color overrides",
2198
+ "zero_baseline_for_bar_and_area: the y domain is extended to include 0 for bar and area; line uses the data extent. minY overrides it.",
2199
+ "y_domain_uses_nice_ticks: every type runs through niceScale, which rounds the min down and the max up to round numbers",
2200
+ "hover_shows_crosshair_and_tooltip: only while showTooltip is true",
2201
+ "width_measured_with_resize_observer: falls back to 640px until the canvas is mounted and measured",
2202
+ "x_labels_decimate_when_crowded: keeps every max(2, floor(innerWidth / 52))-th label and hides the rest; the last category is always kept",
2203
+ "empty_state_when_no_series_has_data: true"
1695
2204
  ],
1696
2205
  selection: {
1697
2206
  keywords: [
@@ -1738,6 +2247,9 @@ var treeui_catalog_default = {
1738
2247
  recipes: [
1739
2248
  "chart-surface",
1740
2249
  "metric-dashboard"
2250
+ ],
2251
+ practices: [
2252
+ "token-driven"
1741
2253
  ]
1742
2254
  },
1743
2255
  {
@@ -1756,7 +2268,8 @@ var treeui_catalog_default = {
1756
2268
  {
1757
2269
  name: "modelValue",
1758
2270
  type: "boolean",
1759
- default: "false"
2271
+ default: "false",
2272
+ description: "Checked state. Fully controlled \u2014 the component holds no internal state and never mutates this prop; it emits update:modelValue and the parent must write the value back.\n"
1760
2273
  },
1761
2274
  {
1762
2275
  name: "size",
@@ -1766,44 +2279,60 @@ var treeui_catalog_default = {
1766
2279
  "sm",
1767
2280
  "md",
1768
2281
  "lg"
1769
- ]
2282
+ ],
2283
+ description: "Control and label scale. Always rendered as a t-checkbox--<size> modifier."
1770
2284
  },
1771
2285
  {
1772
2286
  name: "disabled",
1773
2287
  type: "boolean",
1774
- default: "false"
2288
+ default: "false",
2289
+ description: "Sets the native disabled attribute on the input and adds is-disabled to the root. No aria-disabled is emitted \u2014 the native attribute carries the state.\n"
1775
2290
  },
1776
2291
  {
1777
2292
  name: "indeterminate",
1778
2293
  type: "boolean",
1779
- default: "false"
2294
+ default: "false",
2295
+ description: `Mixed state. Renders the dash icon and sets aria-checked="mixed", taking precedence over modelValue for both. Synced to the input's native indeterminate DOM property.
2296
+ `
1780
2297
  },
1781
2298
  {
1782
2299
  name: "invalid",
1783
2300
  type: "boolean",
1784
- default: "false"
2301
+ default: "false",
2302
+ description: 'Adds is-invalid to the root and aria-invalid="true" to the input. When false the aria-invalid attribute is omitted entirely, not set to "false".\n'
1785
2303
  }
1786
2304
  ],
1787
2305
  events: [
1788
2306
  {
1789
- name: "update:modelValue"
2307
+ name: "update:modelValue",
2308
+ description: "Emitted on the native input's change event. The payload is always the negation of the current modelValue prop, not the DOM checked state. Enables v-model binding.\n",
2309
+ payload: "[value: boolean]"
1790
2310
  }
1791
2311
  ],
1792
2312
  slots: [
1793
2313
  {
1794
- name: "default"
2314
+ name: "default",
2315
+ description: 'Label content, wrapped in <span class="t-checkbox__label">. Rendered only when the slot is supplied (v-if="$slots.default"). The root element is a <label> that wraps the input, so slot text labels the control implicitly \u2014 no for/id pairing is needed.\n'
1795
2316
  }
1796
2317
  ],
1797
2318
  a11y: [
1798
2319
  "role: native_checkbox",
2320
+ "accessible_name: From the default slot; absent when the slot is omitted \u2014 supply aria-label or aria-labelledby",
2321
+ "aria_checked_explicitly_bound: 'mixed' when indeterminate, otherwise the modelValue boolean",
1799
2322
  "aria_checked_mixed_when_indeterminate: true",
1800
2323
  "aria_invalid_when_invalid: true",
2324
+ "native_indeterminate_property_synced: true",
2325
+ "disabled_uses_native_attribute: true",
1801
2326
  "focus_visible_on_control: true",
2327
+ "input_is_visually_hidden_but_focusable: clip-rect hidden input keeps native semantics and tab order; the focus ring is drawn on t-checkbox__control",
1802
2328
  "clickable_target_44x44: true"
1803
2329
  ],
1804
2330
  behavior: [
1805
2331
  "update_payload_type: boolean",
2332
+ "controlled_only: No internal state; the checkbox does not change appearance until the parent updates modelValue",
2333
+ "indeterminate_precedence: indeterminate wins over modelValue for the rendered icon and for aria-checked; is-checked and is-indeterminate can both be present at once",
1806
2334
  "indeterminate_clears_on_user_toggle: false",
2335
+ "indeterminate_is_parent_owned: The component never clears indeterminate \u2014 set it to false yourself when handling update:modelValue",
1807
2336
  "label_click_toggles_checkbox: true"
1808
2337
  ],
1809
2338
  selection: {
@@ -1836,7 +2365,10 @@ var treeui_catalog_default = {
1836
2365
  required: [],
1837
2366
  notes: []
1838
2367
  },
1839
- recipes: []
2368
+ recipes: [],
2369
+ practices: [
2370
+ "interaction-feedback"
2371
+ ]
1840
2372
  },
1841
2373
  {
1842
2374
  name: "TColorSwatch",
@@ -1855,19 +2387,19 @@ var treeui_catalog_default = {
1855
2387
  name: "modelValue",
1856
2388
  type: "string",
1857
2389
  default: "undefined",
1858
- note: "Selected colour as a hex string. Matching is case-insensitive."
2390
+ note: "Selected colour as a hex string. Matched against `options[].value` by case-insensitive exact string comparison, so `#0969DA` matches `#0969da` but `#fff` never matches `#ffffff`. A value matching no preset leaves every swatch unselected and is still written to the custom input."
1859
2391
  },
1860
2392
  {
1861
2393
  name: "options",
1862
2394
  type: "TColorSwatchOption[]",
1863
2395
  default: "[]",
1864
- note: "Preset swatches, each `{ label, value }`. `label` becomes the button's accessible name."
2396
+ note: "Preset swatches, each `{ label, value }`. `value` is applied raw as the button `background`, so any CSS colour renders, but keep it `#rrggbb` so matching and the custom input agree. `label` is the button's only accessible name \u2014 the button renders no text content. An empty array renders an empty group (only the custom input, when enabled)."
1865
2397
  },
1866
2398
  {
1867
2399
  name: "allowCustom",
1868
2400
  type: "boolean",
1869
2401
  default: "false",
1870
- note: 'Appends a native `<input type="color">` for values outside the presets.'
2402
+ note: 'Appends a native `<input type="color">` after the presets for values outside them. When false the input is not rendered at all and `customLabel` has no effect.'
1871
2403
  },
1872
2404
  {
1873
2405
  name: "size",
@@ -1877,23 +2409,26 @@ var treeui_catalog_default = {
1877
2409
  "sm",
1878
2410
  "md",
1879
2411
  "lg"
1880
- ]
2412
+ ],
2413
+ note: "Drives `--tree-color-swatch-size` (sm 1.5rem, md 1.75rem, lg 2rem). Swatches are square; the custom input is one `--tree-space-2` wider."
1881
2414
  },
1882
2415
  {
1883
2416
  name: "label",
1884
2417
  type: "string",
1885
2418
  default: "Color",
1886
- note: 'Accessible label for the swatch group (`role="group"`).'
2419
+ note: 'Accessible label for the swatch group (`role="group"`). Rendered as `aria-label` on the root, so an `aria-label` passed by the consumer falls through to the same element and overrides it.'
1887
2420
  },
1888
2421
  {
1889
2422
  name: "customLabel",
1890
2423
  type: "string",
1891
- default: "Custom color"
2424
+ default: "Custom color",
2425
+ note: "Accessible label (`aria-label`) for the native colour input, and its only accessible name. Applies only when `allowCustom` is set."
1892
2426
  },
1893
2427
  {
1894
2428
  name: "disabled",
1895
2429
  type: "boolean",
1896
- default: "false"
2430
+ default: "false",
2431
+ note: "Sets the native `disabled` attribute on every preset button and on the custom input, so the group stops emitting and drops out of the Tab order. Not reflected on the wrapper, and there is no `is-disabled` class."
1897
2432
  }
1898
2433
  ],
1899
2434
  events: [
@@ -1903,14 +2438,21 @@ var treeui_catalog_default = {
1903
2438
  ],
1904
2439
  slots: [],
1905
2440
  a11y: [
1906
- 'Wrapper is `role="group"` labelled by `label`.',
1907
- "Each swatch is a native button with `aria-pressed` reflecting selection and `aria-label` from the option label.",
1908
- "Selection is conveyed by an outline (not colour alone), since the swatch's own colour is the content."
2441
+ 'Wrapper is `role="group"` labelled by `label` via `aria-label`.',
2442
+ 'Each swatch is a native `<button type="button">` with `aria-pressed` reflecting selection and `aria-label` from the option label; it has no text content, so an option without a `label` is unnamed.',
2443
+ "Selection is conveyed by an outline (not colour alone), since the swatch's own colour is the content.",
2444
+ 'The custom input is a native `<input type="color">` named only by `customLabel`.',
2445
+ "No roving focus and no arrow-key navigation \u2014 every swatch and the custom input is its own Tab stop."
1909
2446
  ],
1910
2447
  behavior: [
2448
+ "update_payload_type: string",
1911
2449
  "emits_on_preset_click: true",
1912
2450
  "emits_on_custom_input: true",
1913
- "blocks_interaction_when_disabled: true"
2451
+ "blocks_interaction_when_disabled: true",
2452
+ "selection_match_is_case_insensitive: true",
2453
+ "custom_input_rendered_only_when_allow_custom: true",
2454
+ "custom_input_emits_on_input_not_change: true",
2455
+ "fully_controlled_no_internal_selection_state: true"
1914
2456
  ],
1915
2457
  selection: {
1916
2458
  keywords: [],
@@ -1922,7 +2464,8 @@ var treeui_catalog_default = {
1922
2464
  required: [],
1923
2465
  notes: []
1924
2466
  },
1925
- recipes: []
2467
+ recipes: [],
2468
+ practices: []
1926
2469
  },
1927
2470
  {
1928
2471
  name: "TCombobox",
@@ -1940,7 +2483,8 @@ var treeui_catalog_default = {
1940
2483
  {
1941
2484
  name: "modelValue",
1942
2485
  type: "string",
1943
- default: ""
2486
+ default: "",
2487
+ note: "The SELECTED option value, never the text in the input. The input renders an internal `query` string that is re-synced from the selected option's label whenever the listbox is closed. A `modelValue` with no matching entry in `options` resolves to an empty label."
1944
2488
  },
1945
2489
  {
1946
2490
  name: "options",
@@ -1951,12 +2495,14 @@ var treeui_catalog_default = {
1951
2495
  {
1952
2496
  name: "open",
1953
2497
  type: "boolean",
1954
- default: "undefined"
2498
+ default: "undefined",
2499
+ note: "Controlled open state. Once a boolean is passed the component stops tracking open state internally \u2014 it only emits update:open / open-change and the consumer MUST write the value back, or the listbox never opens."
1955
2500
  },
1956
2501
  {
1957
2502
  name: "defaultOpen",
1958
2503
  type: "boolean",
1959
- default: "false"
2504
+ default: "false",
2505
+ note: "Uncontrolled initial open state, read once on mount. Later changes to it are ignored."
1960
2506
  },
1961
2507
  {
1962
2508
  name: "size",
@@ -1966,17 +2512,20 @@ var treeui_catalog_default = {
1966
2512
  "sm",
1967
2513
  "md",
1968
2514
  "lg"
1969
- ]
2515
+ ],
2516
+ note: "Sizes the control only. `md` has no CSS rule of its own \u2014 it is the base `.t-combobox__control` styling."
1970
2517
  },
1971
2518
  {
1972
2519
  name: "disabled",
1973
2520
  type: "boolean",
1974
- default: "false"
2521
+ default: "false",
2522
+ note: 'Sets the native `disabled` attribute on the input, adds `is-disabled` to the root, and suppresses the listbox entirely (`v-if="isOpen && !disabled"`) \u2014 even while `open` is true. Also short-circuits opening, typing side effects, and keydown handling.'
1975
2523
  },
1976
2524
  {
1977
2525
  name: "loading",
1978
2526
  type: "boolean",
1979
- default: "false"
2527
+ default: "false",
2528
+ note: 'Presentational only. Replaces the chevron indicator with `<TSpinner size="sm" label="Loading" />` and sets aria-busy; it never blocks typing, opening, filtering, or selection.'
1980
2529
  },
1981
2530
  {
1982
2531
  name: "invalid",
@@ -1991,56 +2540,102 @@ var treeui_catalog_default = {
1991
2540
  {
1992
2541
  name: "emptyText",
1993
2542
  type: "string",
1994
- default: "No results found."
2543
+ default: "No results found.",
2544
+ note: "Fallback content of the `empty` slot \u2014 supplying that slot shadows this prop entirely."
1995
2545
  }
1996
2546
  ],
1997
2547
  events: [
1998
2548
  {
1999
- name: "update:modelValue"
2549
+ name: "update:modelValue",
2550
+ description: 'Emitted with the chosen option\'s `value` when an option is selected, and with `""` when the listbox closes while the input text is blank and a value was set (the clear-by-erasing path).',
2551
+ payload: '["string"]'
2000
2552
  },
2001
2553
  {
2002
- name: "update:open"
2554
+ name: "update:open",
2555
+ description: "Emitted whenever the open state changes. Deduped \u2014 no emit when the next value equals the current one.",
2556
+ payload: '["boolean"]'
2003
2557
  },
2004
2558
  {
2005
- name: "open-change"
2559
+ name: "open-change",
2560
+ description: "Emitted immediately after `update:open`, with the same payload. Alias for consumers that do not use v-model:open.",
2561
+ payload: '["boolean"]'
2006
2562
  },
2007
2563
  {
2008
- name: "input-change"
2564
+ name: "input-change",
2565
+ description: "Emitted on every keystroke with the raw, untrimmed input text \u2014 the same string the `empty` slot receives as `query`. This is the only hook for server-side / async filtering; it does not replace or disable the built-in local filtering, so an async consumer must feed results back through `options`.",
2566
+ payload: '["string"]'
2009
2567
  }
2010
2568
  ],
2011
2569
  slots: [
2012
2570
  {
2013
- name: "prefix"
2571
+ name: "prefix",
2572
+ description: 'Leading content inside `.t-combobox__control`, before the input. Rendered only when the slot is supplied, wrapped in `<span class="t-combobox__slot t-combobox__slot--prefix">`. No scoped props.'
2014
2573
  },
2015
2574
  {
2016
- name: "option"
2575
+ name: "option",
2576
+ description: 'Replaces the default option body (label, description, and the selected check icon). Renders INSIDE the row\'s `<li role="option">`, which owns the click and mouseenter handlers, so it must contain only presentational content. `selected` is `option.value === modelValue`; `active` is the keyboard-highlighted row, which is NOT DOM focus \u2014 focus stays on the input at all times.\n'
2017
2577
  },
2018
2578
  {
2019
- name: "empty"
2579
+ name: "empty",
2580
+ description: 'Replaces `emptyText` inside `<li class="t-combobox__empty">` when the filter matches no option. `query` is the current raw input text, untrimmed and uncased. Passing both this slot and `emptyText` renders only the slot.'
2020
2581
  }
2021
2582
  ],
2022
2583
  a11y: [
2023
2584
  "role: combobox",
2585
+ "combobox_element: input",
2586
+ "accessible_name: None is generated \u2014 there is no label element and no default slot. Pass aria-label or aria-labelledby (which also names the listbox), or pass `id` and wrap in TFormField with a matching `html-for`; TFormField does not link the label on its own.",
2024
2587
  "trigger_aria_haspopup: listbox",
2025
2588
  "trigger_aria_expanded: true",
2589
+ "trigger_aria_controls_when_open: true",
2026
2590
  "aria_autocomplete: list",
2027
2591
  "aria_activedescendant_on_active_option: true",
2592
+ "aria_activedescendant_cleared_while_closed: true",
2028
2593
  "aria_invalid_when_invalid: true",
2029
2594
  "aria_busy_when_loading: true",
2595
+ "listbox_role: listbox",
2596
+ "option_role: option",
2597
+ "listbox_mirrors_aria_label_and_aria_labelledby: true",
2030
2598
  "aria_selected_on_options: true",
2031
2599
  "aria_disabled_on_disabled_options: true",
2600
+ "focus_stays_on_the_input_options_are_never_focused: true",
2032
2601
  "keyboard_arrow_navigation: true",
2602
+ "arrow_navigation_does_not_wrap_at_the_ends: true",
2603
+ "arrow_navigation_skips_disabled_options: true",
2033
2604
  "escape_closes_and_restores_previous_selection: true",
2605
+ "escape_is_ignored_while_closed: true",
2034
2606
  "enter_selects_active_option: true",
2607
+ "tab_closes_and_restores_previous_selection: true",
2035
2608
  "home_end_navigation: true"
2036
2609
  ],
2037
2610
  behavior: [
2038
2611
  "update_payload_type: string",
2612
+ "input_text_is_internal_state_not_model_value: true",
2039
2613
  "local_filtering_by_label_description_and_keywords: true",
2614
+ "filtering_is_case_insensitive_substring_on_a_trimmed_query: true",
2615
+ "empty_query_shows_all_options: true",
2616
+ "input_change_emits_the_raw_query_for_async_filtering: true",
2617
+ "async_results_are_fed_back_through_the_options_prop: true",
2040
2618
  "loading_does_not_disable_by_itself: true",
2619
+ "spinner_replaces_the_chevron_when_loading: true",
2620
+ "chevron_rotates_when_open: true",
2621
+ "check_icon_on_selected_option: true",
2622
+ "focus_opens_the_listbox: true",
2623
+ "click_on_input_opens_the_listbox: true",
2624
+ "typing_opens_the_listbox: true",
2625
+ "arrow_down_opens_the_listbox_with_the_first_enabled_option_active: true",
2626
+ "arrow_up_opens_the_listbox_with_the_last_enabled_option_active: true",
2627
+ "active_option_defaults_to_the_current_selection_when_it_is_in_the_filtered_list: true",
2628
+ "disabled_options_cannot_be_activated_selected_or_hovered: true",
2629
+ "selecting_an_option_sets_the_input_text_to_its_label_and_closes_without_restoring: true",
2041
2630
  "outside_click_closes_dropdown: true",
2631
+ "outside_click_is_detected_on_pointerdown_on_document: true",
2042
2632
  "query_clears_selection_when_closed_empty: true",
2043
- "selected_value_restores_when_search_is_abandoned: true"
2633
+ "selected_value_restores_when_search_is_abandoned: true",
2634
+ "external_model_value_and_options_changes_resync_the_input_text_only_while_closed: true",
2635
+ "active_option_is_revalidated_when_the_filtered_list_changes: true",
2636
+ "open_state_emits_are_deduped: true",
2637
+ "listbox_is_not_rendered_while_disabled: true",
2638
+ "listbox_renders_in_flow_with_no_portal_and_no_flip_positioning: true"
2044
2639
  ],
2045
2640
  selection: {
2046
2641
  keywords: [
@@ -2073,7 +2668,8 @@ var treeui_catalog_default = {
2073
2668
  "Pass `options` as labeled items and keep labels human-readable because search matches visible fields."
2074
2669
  ]
2075
2670
  },
2076
- recipes: []
2671
+ recipes: [],
2672
+ practices: []
2077
2673
  },
2078
2674
  {
2079
2675
  name: "TConfirmDialog",
@@ -2096,12 +2692,14 @@ var treeui_catalog_default = {
2096
2692
  {
2097
2693
  name: "defaultOpen",
2098
2694
  type: "boolean",
2099
- default: "false"
2695
+ default: "false",
2696
+ note: "Uncontrolled initial state. Read once on setup \u2014 later changes are ignored. Only meaningful when `open` is left undefined."
2100
2697
  },
2101
2698
  {
2102
2699
  name: "disabled",
2103
2700
  type: "boolean",
2104
- default: "false"
2701
+ default: "false",
2702
+ note: "Only blocks opening from the `trigger` slot (the trigger wrapper gets `aria-disabled` and its click is swallowed). It does NOT block a programmatic open through `open`/`defaultOpen`, and it does not disable the confirm/cancel buttons \u2014 use `confirmDisabled`/`cancelDisabled` for those."
2105
2703
  },
2106
2704
  {
2107
2705
  name: "size",
@@ -2111,47 +2709,63 @@ var treeui_catalog_default = {
2111
2709
  "sm",
2112
2710
  "md",
2113
2711
  "lg"
2114
- ]
2712
+ ],
2713
+ note: "Forwarded to TModal and applied as `t-modal__surface--<size>` \u2014 it sizes the dialog surface, not the confirm-dialog block, and it does not size the default footer buttons (those are always TButton `md`)."
2115
2714
  },
2116
2715
  {
2117
2716
  name: "title",
2118
2717
  type: "string",
2119
- default: ""
2718
+ default: "",
2719
+ note: "Rendered by TModal as `t-modal__title` and referenced by `aria-labelledby`. TConfirmDialog does not expose TModal's `header` slot, so an empty title leaves the dialog without an accessible name."
2120
2720
  },
2121
2721
  {
2122
2722
  name: "description",
2123
2723
  type: "string",
2124
- default: ""
2724
+ default: "",
2725
+ note: "Rendered by TModal as `t-modal__description` and referenced by `aria-describedby`. The default slot body is NOT part of the accessible description."
2125
2726
  },
2126
2727
  {
2127
2728
  name: "confirmLabel",
2128
2729
  type: "string",
2129
- default: "Confirm"
2730
+ default: "Confirm",
2731
+ note: "Ignored when the `actions` slot is supplied."
2130
2732
  },
2131
2733
  {
2132
2734
  name: "cancelLabel",
2133
2735
  type: "string",
2134
- default: "Cancel"
2736
+ default: "Cancel",
2737
+ note: "Ignored when the `actions` slot is supplied."
2135
2738
  },
2136
2739
  {
2137
2740
  name: "confirmVariant",
2138
2741
  type: "TVariant",
2139
- default: "danger"
2742
+ default: "danger",
2743
+ values: [
2744
+ "solid",
2745
+ "outline",
2746
+ "ghost",
2747
+ "soft",
2748
+ "danger"
2749
+ ],
2750
+ note: "Ignored when the `actions` slot is supplied."
2140
2751
  },
2141
2752
  {
2142
2753
  name: "confirmDisabled",
2143
2754
  type: "boolean",
2144
- default: "false"
2755
+ default: "false",
2756
+ note: "Disables the default confirm button AND short-circuits the confirm handler \u2014 no `confirm` emit and no close, including when the handler is invoked from the `actions` slot scope."
2145
2757
  },
2146
2758
  {
2147
2759
  name: "cancelDisabled",
2148
2760
  type: "boolean",
2149
- default: "false"
2761
+ default: "false",
2762
+ note: "Disables the default cancel button AND short-circuits the cancel handler \u2014 no `cancel` emit and no close, including when the handler is invoked from the `actions` slot scope."
2150
2763
  },
2151
2764
  {
2152
2765
  name: "loading",
2153
2766
  type: "boolean",
2154
- default: "false"
2767
+ default: "false",
2768
+ note: "Puts the default confirm button into its spinner state (TButton adds `is-loading`, `aria-busy`, and the native `disabled` attribute) and short-circuits the confirm handler exactly like `confirmDisabled`. It does not affect the cancel button. Also passed to the `actions` slot scope so a custom footer can mirror the state."
2155
2769
  },
2156
2770
  {
2157
2771
  name: "closeOnEscape",
@@ -2161,27 +2775,32 @@ var treeui_catalog_default = {
2161
2775
  {
2162
2776
  name: "closeOnOverlay",
2163
2777
  type: "boolean",
2164
- default: "false"
2778
+ default: "false",
2779
+ note: "Deliberately opt-in \u2014 TModal defaults this to true, TConfirmDialog flips it to false so a confirmation is not dismissed by a stray backdrop click."
2165
2780
  },
2166
2781
  {
2167
2782
  name: "showCloseButton",
2168
2783
  type: "boolean",
2169
- default: "false"
2784
+ default: "false",
2785
+ note: "Deliberately opt-in \u2014 TModal defaults this to true. Enabling it adds a header close button that dismisses without emitting `cancel`."
2170
2786
  },
2171
2787
  {
2172
2788
  name: "closeLabel",
2173
2789
  type: "string",
2174
- default: "Close dialog"
2790
+ default: "Close dialog",
2791
+ note: "Accessible name of the header close button. Only rendered when `showCloseButton` is true."
2175
2792
  },
2176
2793
  {
2177
2794
  name: "closeOnConfirm",
2178
2795
  type: "boolean",
2179
- default: "true"
2796
+ default: "true",
2797
+ note: "Set false for async confirmation: `confirm` fires but the dialog stays open, so the caller can hold it open with `loading` while the request resolves and then close it via `open`."
2180
2798
  },
2181
2799
  {
2182
2800
  name: "closeOnCancel",
2183
2801
  type: "boolean",
2184
- default: "true"
2802
+ default: "true",
2803
+ note: "Set false to keep the dialog open after `cancel` \u2014 the caller then owns closing via `open`."
2185
2804
  }
2186
2805
  ],
2187
2806
  events: [
@@ -2200,16 +2819,20 @@ var treeui_catalog_default = {
2200
2819
  ],
2201
2820
  slots: [
2202
2821
  {
2203
- name: "trigger"
2822
+ name: "trigger",
2823
+ description: "Element that opens the dialog on click. Omit for imperative use."
2204
2824
  },
2205
2825
  {
2206
- name: "icon"
2826
+ name: "icon",
2827
+ description: "Decorative leading icon. Presence switches the block to the two-column icon layout."
2207
2828
  },
2208
2829
  {
2209
- name: "default"
2830
+ name: "default",
2831
+ description: "Extra body content rendered under the description."
2210
2832
  },
2211
2833
  {
2212
- name: "actions"
2834
+ name: "actions",
2835
+ description: "Replaces the whole footer. Scope gives you confirm(), cancel(), and loading."
2213
2836
  }
2214
2837
  ],
2215
2838
  a11y: [
@@ -2217,15 +2840,26 @@ var treeui_catalog_default = {
2217
2840
  "aria_modal: true",
2218
2841
  "focus_trap: inherited_from_modal",
2219
2842
  "restores_focus_on_close: inherited_from_modal",
2843
+ "restore_focus_caveat: TModal only records the previously focused element when the dialog is opened by clicking its `trigger` slot. A dialog opened programmatically \u2014 by setting `open` from a row action, a menu item, or `defaultOpen` \u2014 has nothing recorded, so focus is not restored on close. Move focus back yourself in that case.",
2844
+ "accessible_name_source: `title` prop, via aria-labelledby; absent when `title` is empty",
2845
+ "accessible_description_source: `description` prop, via aria-describedby; the default slot body is not included",
2846
+ "initial_focus: First focusable element inside the dialog surface. With the default footer and `showCloseButton: false` that is the cancel button, which is the safe landing spot for a destructive confirmation. Enabling `showCloseButton` moves initial focus to the header close button instead. A `loading` or `confirmDisabled` confirm button carries the native `disabled` attribute and is skipped.",
2220
2847
  "icon_slot_marked_aria_hidden: true"
2221
2848
  ],
2222
2849
  behavior: [
2223
2850
  "built_on_modal_contract: true",
2224
2851
  "default_confirm_button_variant: danger",
2852
+ "default_cancel_button_variant: ghost",
2225
2853
  "overlay_click_disabled_by_default: true",
2226
2854
  "close_button_hidden_by_default: true",
2227
2855
  "loading_state_applies_to_confirm_button: true",
2228
- "default_actions_close_dialog_when_enabled: true"
2856
+ "confirm_suppressed_when: confirmDisabled || loading \u2014 the handler returns before emitting, so neither `confirm` nor the close runs",
2857
+ "cancel_suppressed_when: cancelDisabled \u2014 the handler returns before emitting, so neither `cancel` nor the close runs",
2858
+ "closes_after_confirm_when: closeOnConfirm",
2859
+ "closes_after_cancel_when: closeOnCancel",
2860
+ "guards_and_auto_close_apply_to_actions_slot: true",
2861
+ "close_button_and_escape_do_not_emit_cancel: true",
2862
+ "inherits_teleport_scroll_lock_and_focus_trap_from_modal: true"
2229
2863
  ],
2230
2864
  selection: {
2231
2865
  keywords: [],
@@ -2239,6 +2873,9 @@ var treeui_catalog_default = {
2239
2873
  },
2240
2874
  recipes: [
2241
2875
  "confirm-destructive-action"
2876
+ ],
2877
+ practices: [
2878
+ "accessible-by-default"
2242
2879
  ]
2243
2880
  },
2244
2881
  {
@@ -2291,7 +2928,9 @@ var treeui_catalog_default = {
2291
2928
  }
2292
2929
  ],
2293
2930
  a11y: [
2294
- "Acts as a structural wrapper only; semantics come from the `as` prop and child content."
2931
+ "Acts as a structural wrapper only; semantics come from the `as` prop and child content.",
2932
+ 'Renders a non-semantic `div` by default. Pass `as` to make it a landmark (e.g. `as="main"`, `as="section"`, `as="header"`) \u2014 avoid nesting a second `main` inside an app shell or a TPage that already provides one.\n',
2933
+ "`aria-*` and `role` attributes fall through to the root element (see `forwards_attrs_to`)."
2295
2934
  ],
2296
2935
  behavior: [
2297
2936
  "semantic_element_controlled_by_as: true",
@@ -2335,6 +2974,9 @@ var treeui_catalog_default = {
2335
2974
  },
2336
2975
  recipes: [
2337
2976
  "sidebar-shell"
2977
+ ],
2978
+ practices: [
2979
+ "content-alignment"
2338
2980
  ]
2339
2981
  },
2340
2982
  {
@@ -2343,6 +2985,8 @@ var treeui_catalog_default = {
2343
2985
  package: "@treeui/vue",
2344
2986
  category: "Overlay",
2345
2987
  status: "stable",
2988
+ description: "Right-click (contextmenu) menu teleported to body and positioned at the cursor. The component renders no chrome of its own \u2014 the root wrapper is `display: contents` and only wraps the slotted trigger area.\n",
2989
+ sourceFile: "packages/vue/src/components/TContextMenu.vue",
2346
2990
  storyFile: "apps/docs/src/stories/ContextMenu.stories.ts",
2347
2991
  storyTitle: "Components/Overlay/ContextMenu",
2348
2992
  manifestFile: "docs/ai/COMPONENTS/context-menu.yaml",
@@ -2351,73 +2995,84 @@ var treeui_catalog_default = {
2351
2995
  name: "items",
2352
2996
  type: "TContextMenuItem[]",
2353
2997
  default: "[]",
2354
- description: "Menu items to render."
2998
+ description: 'Menu items to render, one `role="menuitem"` <li> each. An empty array is allowed and still opens an empty `role="menu"` list with nothing focused.\n'
2355
2999
  },
2356
3000
  {
2357
3001
  name: "open",
2358
3002
  type: "boolean | undefined",
2359
3003
  default: "undefined",
2360
- description: "Controlled open state (v-model:open)."
3004
+ description: "Controlled open state (v-model:open). When defined it wins over the internal state and `defaultOpen` is ignored; the component still emits and expects the parent to write the new value back. `disabled` overrides this \u2014 a disabled component renders no menu even while `open` is true.\n"
2361
3005
  },
2362
3006
  {
2363
3007
  name: "defaultOpen",
2364
3008
  type: "boolean",
2365
3009
  default: "false",
2366
- description: "Uncontrolled initial open state."
3010
+ description: "Uncontrolled initial open state. Read once during setup \u2014 later changes to this prop have no effect.\n"
2367
3011
  },
2368
3012
  {
2369
3013
  name: "disabled",
2370
3014
  type: "boolean",
2371
3015
  default: "false",
2372
- description: "Prevents the context menu from opening."
3016
+ description: "Prevents the context menu from opening, and hides it if it is already open. While disabled the component does not call preventDefault on `contextmenu`, so the native browser menu appears instead.\n"
2373
3017
  },
2374
3018
  {
2375
3019
  name: "size",
2376
3020
  type: "TSize",
2377
3021
  default: "md",
2378
- description: "Item sizing variant."
3022
+ values: [
3023
+ "sm",
3024
+ "md",
3025
+ "lg"
3026
+ ],
3027
+ description: "Item sizing variant. The modifier class is applied to both the root and the teleported menu, but only the copy on the menu is effective (the root is teleport-detached from the menu subtree).\n"
2379
3028
  }
2380
3029
  ],
2381
3030
  events: [
2382
3031
  {
2383
3032
  name: "select",
2384
- description: "Emitted when a menu item is selected.",
3033
+ description: "Emitted with `item.value` when an enabled item is activated by click or Enter/Space. Disabled items emit nothing.\n",
2385
3034
  payload: "[value: string]"
2386
3035
  },
2387
3036
  {
2388
3037
  name: "update:open",
2389
- description: "Emitted when open state changes (v-model support).",
3038
+ description: "Emitted when open state changes (v-model support). Only fires on an actual transition \u2014 setting the same value again is a no-op.\n",
2390
3039
  payload: "[value: boolean]"
2391
3040
  },
2392
3041
  {
2393
3042
  name: "open-change",
2394
- description: "Emitted when open state changes.",
3043
+ description: "Emitted when open state changes. Always fires together with `update:open`, including in controlled mode.\n",
2395
3044
  payload: "[value: boolean]"
2396
3045
  }
2397
3046
  ],
2398
3047
  slots: [
2399
3048
  {
2400
3049
  name: "default",
2401
- description: "Content that acts as the right-click trigger area."
3050
+ description: "Content that acts as the right-click trigger area. It is rendered inside the root <div>, which is `display: contents` and therefore contributes no box of its own \u2014 the slotted content keeps its own layout, and the `contextmenu` handler still fires through event bubbling.\n"
2402
3051
  },
2403
3052
  {
2404
3053
  name: "item",
2405
- description: "Custom rendering for each menu item."
3054
+ description: 'Custom rendering for each menu item; scoped props `{ item, index }`, falls back to `{{ item.label }}`. Rendered inside the `role="menuitem"` <li>, which already owns click, keydown, tabindex and aria-disabled \u2014 keep the content presentational, nested interactive controls are not reachable inside a menuitem.\n'
2406
3055
  }
2407
3056
  ],
2408
3057
  a11y: [
2409
- "role: menu (on the floating list)",
3058
+ "role: menu (on the teleported floating list)",
2410
3059
  "items_role: menuitem",
2411
- 'keyboard: ["Right-click / contextmenu event opens menu at cursor","ArrowDown / ArrowUp navigates items","Home / End jumps to first / last enabled item","Enter / Space selects focused item","Escape closes menu and restores focus"]',
2412
- 'aria: ["aria-disabled on disabled items","aria-label on menu element"]',
2413
- 'focus: ["First enabled item receives focus on open","Focus restored to trigger area on close"]'
3060
+ 'keyboard: ["Right-click / contextmenu event opens menu at cursor","ArrowDown / ArrowUp move focus to the next / previous enabled item (clamped, no wrap)","Home / End jumps to first / last enabled item","Enter / Space selects focused item","Escape closes menu and restores focus","All key handling lives on the items \u2014 keys are only handled while an item has focus"]',
3061
+ 'aria: ["aria-disabled on disabled items (omitted, not false, when enabled)","aria-label=\\"Context menu\\" on the menu element \u2014 hardcoded, not configurable or localizable","Menu id is generated with createId(\'t-context-menu\'); there is no `id` prop","tabindex 0 on enabled items, -1 on disabled items \u2014 no roving tabindex, every enabled item is tab-reachable"]',
3062
+ 'focus: ["First enabled item receives focus on open","When every item is disabled, focus still goes to the first item; with an empty `items` array nothing is focused","Focus restored to trigger area on close from Escape or item selection","An outside click closes without restoring focus, leaving it where the pointer put it"]'
2414
3063
  ],
2415
3064
  behavior: [
2416
- "Opens at cursor position on right-click (contextmenu event).",
2417
- "Uses Teleport to body to escape stacking contexts.",
2418
- "Closes on click outside, Escape key, or item selection.",
2419
- "Prevents native browser context menu on trigger area.",
2420
- "Uses position fixed for viewport-relative placement."
3065
+ "Opens at cursor position on right-click (contextmenu event), using event.clientX / clientY.",
3066
+ "Uses Teleport to body to escape stacking contexts; the menu is not a DOM descendant of the trigger.",
3067
+ "Uses position fixed at the raw cursor coordinates, stacked at var(--tree-z-dropdown).",
3068
+ "No collision detection or viewport clamping \u2014 a menu opened near the right or bottom edge overflows.",
3069
+ "Closes on pointerdown outside the menu (including on the trigger itself), Escape, or item selection.",
3070
+ "Right-clicking again inside the trigger area repositions the menu \u2014 the outside pointerdown closes it and the following contextmenu reopens it at the new coordinates.",
3071
+ "A document-level contextmenu listener covers keyboard-invoked menus (Shift+F10 / Menu key) raised outside the trigger.",
3072
+ "Prevents native browser context menu on trigger area, but only while not disabled.",
3073
+ 'Exposes data-state="open" | "closed" on the root element for external styling.',
3074
+ "Document pointerdown / contextmenu listeners are attached only while open, and removed on close and before unmount.",
3075
+ "Menu enter / leave uses the shared t-fade transition (opacity plus scale 0.95)."
2421
3076
  ],
2422
3077
  selection: {
2423
3078
  keywords: [
@@ -2442,7 +3097,8 @@ var treeui_catalog_default = {
2442
3097
  required: [],
2443
3098
  notes: []
2444
3099
  },
2445
- recipes: []
3100
+ recipes: [],
3101
+ practices: []
2446
3102
  },
2447
3103
  {
2448
3104
  name: "TDatePicker",
@@ -2460,17 +3116,20 @@ var treeui_catalog_default = {
2460
3116
  {
2461
3117
  name: "modelValue",
2462
3118
  type: "string",
2463
- default: ""
3119
+ default: "",
3120
+ note: "Parsed with `parseDateValue`, which accepts ONLY a strict `YYYY-MM-DD` string that resolves to a real calendar date. Anything else (empty, `2026-13-01`, an ISO datetime, a Date object stringified) resolves to null and the trigger falls back to `placeholder`. The hidden input, when present, still submits the raw prop verbatim \u2014 an unparseable value renders as the placeholder but is submitted as-is."
2464
3121
  },
2465
3122
  {
2466
3123
  name: "open",
2467
3124
  type: "boolean",
2468
- default: "controlled_optional"
3125
+ default: "undefined",
3126
+ note: "Controlled open state. Once a boolean is passed the component stops tracking open state internally \u2014 it only emits update:open / open-change and the consumer MUST write the value back, or the calendar never opens."
2469
3127
  },
2470
3128
  {
2471
3129
  name: "defaultOpen",
2472
3130
  type: "boolean",
2473
- default: "false"
3131
+ default: "false",
3132
+ note: "Uncontrolled initial open state, read once on mount. Later changes to it are ignored. Opening this way skips `openCalendar`, so no day button receives DOM focus until the user tabs or arrows into the grid."
2474
3133
  },
2475
3134
  {
2476
3135
  name: "size",
@@ -2480,41 +3139,48 @@ var treeui_catalog_default = {
2480
3139
  "sm",
2481
3140
  "md",
2482
3141
  "lg"
2483
- ]
3142
+ ],
3143
+ note: "Sizes the trigger only, never the popover. `md` has no CSS rule of its own \u2014 it is the base `.t-date-picker__trigger` styling \u2014 but the `t-date-picker--md` class is still rendered."
2484
3144
  },
2485
3145
  {
2486
3146
  name: "disabled",
2487
3147
  type: "boolean",
2488
- default: "false"
3148
+ default: "false",
3149
+ note: 'Sets the native `disabled` attribute on the trigger, adds `is-disabled` to the root, short-circuits `openCalendar` and trigger keydown, makes `isDateDisabled` return true for every date, and suppresses the popover entirely (`v-if="isOpen && !disabled"`) even while `open` is true.'
2489
3150
  },
2490
3151
  {
2491
3152
  name: "invalid",
2492
3153
  type: "boolean",
2493
- default: "false"
3154
+ default: "false",
3155
+ note: 'Adds `is-invalid` to the root and `aria-invalid="true"` to the trigger. Purely presentational \u2014 it does not block opening or selection.'
2494
3156
  },
2495
3157
  {
2496
3158
  name: "placeholder",
2497
3159
  type: "string",
2498
- default: "Select date"
3160
+ default: "Select date",
3161
+ note: "Rendered inside `.t-date-picker__value` with a `data-placeholder` attribute whenever `modelValue` does not parse."
2499
3162
  },
2500
3163
  {
2501
3164
  name: "locale",
2502
3165
  type: "string",
2503
- default: "en-US"
3166
+ default: "en-US",
3167
+ note: "BCP-47 tag passed to three separate `Intl.DateTimeFormat` calls: the trigger text (day numeric / month long / year numeric), the popover month caption (month long / year numeric), and the weekday header (weekday short). It does NOT localize the per-day `aria-label`, which is always the raw `YYYY-MM-DD` key."
2504
3168
  },
2505
3169
  {
2506
3170
  name: "min",
2507
3171
  type: "string",
2508
- default: ""
3172
+ default: "",
3173
+ note: "Inclusive lower bound. Days before it get the native `disabled` attribute and cannot be selected or arrowed onto; the Previous-month nav button is disabled once the view reaches the min month. A value that fails `parseDateValue` is ignored, i.e. treated as no bound."
2509
3174
  },
2510
3175
  {
2511
3176
  name: "max",
2512
3177
  type: "string",
2513
- default: ""
3178
+ default: "",
3179
+ note: "Inclusive upper bound, mirroring `min`: out-of-range days are disabled and the Next-month nav button is disabled once the view reaches the max month."
2514
3180
  },
2515
3181
  {
2516
3182
  name: "weekStartsOn",
2517
- type: "number",
3183
+ type: "0 | 1 | 2 | 3 | 4 | 5 | 6",
2518
3184
  default: "0",
2519
3185
  values: [
2520
3186
  "0",
@@ -2524,46 +3190,90 @@ var treeui_catalog_default = {
2524
3190
  "4",
2525
3191
  "5",
2526
3192
  "6"
2527
- ]
3193
+ ],
3194
+ note: "0 = Sunday. Drives the weekday header order, the leading offset of the day grid, and the Home/End keys (which jump to the first/last day of the week as defined by this prop)."
2528
3195
  },
2529
3196
  {
2530
3197
  name: "id",
2531
3198
  type: "string",
2532
- default: "auto"
3199
+ default: "auto",
3200
+ note: "Names the CALENDAR POPOVER, not the trigger. It becomes the popover's `id` (referenced by the trigger's `aria-controls` while open) and seeds the month caption id as `${id}-caption`. Read once at setup \u2014 later changes are ignored. Because `id` is a declared prop it never falls through to the trigger button, so the trigger has no id and a `<label for>` / `TFormField html-for` cannot target it; name the control with `aria-label` instead."
2533
3201
  }
2534
3202
  ],
2535
3203
  events: [
2536
3204
  {
2537
- name: "update:modelValue"
3205
+ name: "update:modelValue",
3206
+ description: 'Emitted with the chosen day as `YYYY-MM-DD` from `selectDate` only \u2014 a click or Enter/Space on an enabled day button. Nothing else in the component emits it; there is no clear affordance, so the value never becomes `""` on its own.',
3207
+ payload: '["string"]'
2538
3208
  },
2539
3209
  {
2540
- name: "update:open"
3210
+ name: "update:open",
3211
+ description: "Emitted whenever the open state changes. Deduped \u2014 no emit when the next value equals the current one.",
3212
+ payload: '["boolean"]'
2541
3213
  },
2542
3214
  {
2543
- name: "open-change"
3215
+ name: "open-change",
3216
+ description: "Emitted immediately after `update:open`, with the same payload. Alias for consumers that do not use v-model:open.",
3217
+ payload: '["boolean"]'
2544
3218
  },
2545
3219
  {
2546
- name: "select"
3220
+ name: "select",
3221
+ description: "Emitted from `selectDate` immediately after `update:modelValue`, with the same `YYYY-MM-DD` payload. It is the user-intent signal: it fires only on an actual day activation and never when `modelValue` changes from outside, which is what distinguishes it from `update:modelValue`.",
3222
+ payload: '["string"]'
2547
3223
  }
2548
3224
  ],
2549
3225
  slots: [
2550
3226
  {
2551
- name: "prefix"
3227
+ name: "prefix",
3228
+ description: 'Leading content inside the trigger button, before the value text. Rendered only when the slot is supplied, wrapped in `<span class="t-date-picker__slot t-date-picker__slot--prefix">`. No scoped props. Content is inside a `<button>`, so it must be presentational only.'
2552
3229
  },
2553
3230
  {
2554
- name: "suffix"
3231
+ name: "suffix",
3232
+ description: 'Trailing content inside the trigger button, in `<span class="t-date-picker__slot t-date-picker__slot--suffix">`. Unlike `prefix` the wrapper is always rendered; supplying the slot replaces the default calendar icon. No scoped props, and the same presentational-only restriction applies.'
2555
3233
  }
2556
3234
  ],
2557
3235
  a11y: [
3236
+ "role: dialog",
3237
+ "accessible_name: None is generated for the trigger beyond its own text content, which is the formatted date or the placeholder \u2014 that changes as the user picks, so it is not a stable label. Pass `aria-label` (see example). The `id` prop cannot be used to link an external <label>.",
2558
3238
  "trigger_has_popup_dialog: true",
3239
+ "trigger_aria_expanded: true",
3240
+ "trigger_aria_controls_when_open: true",
3241
+ "aria_invalid_when_invalid: true",
2559
3242
  "calendar_uses_grid_roles: true",
3243
+ "calendar_aria_modal: false",
3244
+ "calendar_labelled_by_month_caption: true",
3245
+ "weekday_header_is_aria_hidden: true",
3246
+ "aria_selected_rendered_on_every_gridcell: true",
3247
+ "aria_current_date_on_today: true",
3248
+ "day_aria_label_is_the_raw_iso_key: true",
3249
+ "nav_buttons_labelled_previous_month_and_next_month: true",
3250
+ "roving_tabindex_one_focusable_day: true",
2560
3251
  "escape_closes: true",
2561
- "returns_focus_to_trigger_after_selection: true"
3252
+ "returns_focus_to_trigger_after_selection: true",
3253
+ "returns_focus_to_trigger_on_escape_from_a_day: true",
3254
+ "focus_is_not_trapped_and_tab_leaves_the_open_calendar: true"
2562
3255
  ],
2563
3256
  behavior: [
3257
+ "value_format: YYYY-MM-DD",
3258
+ "controlled_and_uncontrolled_open: true",
3259
+ "open_state_emits_are_deduped: true",
2564
3260
  "selection_closes_popover: true",
2565
3261
  "hidden_input_created_when_name_attr_present: true",
2566
- "out_of_range_dates_disabled: true"
3262
+ "hidden_input_submits_the_raw_unparsed_model_value: true",
3263
+ "out_of_range_dates_disabled: true",
3264
+ "min_and_max_also_disable_the_month_nav_buttons: true",
3265
+ "popover_is_not_rendered_while_disabled: true",
3266
+ "outside_click_closes_calendar: true",
3267
+ "outside_click_is_detected_on_pointerdown_on_document: true",
3268
+ "outside_click_and_trigger_toggle_close_without_restoring_focus: true",
3269
+ "grid_is_always_six_weeks_forty_two_days: true",
3270
+ "leading_and_trailing_days_render_muted_and_stay_selectable: true",
3271
+ "opening_syncs_the_view_to_the_selected_date_else_the_last_focused_date_else_today: true",
3272
+ "external_model_value_changes_move_the_view_and_the_focused_day: true",
3273
+ "clearing_the_model_value_resets_the_view_to_today_only_while_closed: true",
3274
+ "month_nav_moves_the_focused_day_to_the_first_of_the_new_month_and_focuses_it: true",
3275
+ "today_is_computed_once_at_setup_not_re_evaluated_across_midnight: true",
3276
+ "popover_renders_in_flow_with_no_portal_and_no_flip_positioning: true"
2567
3277
  ],
2568
3278
  selection: {
2569
3279
  keywords: [
@@ -2591,13 +3301,14 @@ var treeui_catalog_default = {
2591
3301
  "Use `min` and `max` with the same format."
2592
3302
  ]
2593
3303
  },
2594
- recipes: []
3304
+ recipes: [],
3305
+ practices: []
2595
3306
  },
2596
3307
  {
2597
3308
  name: "TDateTimePicker",
2598
3309
  aliases: [],
2599
3310
  package: "@treeui/vue",
2600
- kind: "form",
3311
+ kind: "form_overlay",
2601
3312
  category: "Data Entry",
2602
3313
  status: "available",
2603
3314
  purpose: "Date and time picker with a calendar grid, time selection, and overlay trigger.",
@@ -2610,7 +3321,7 @@ var treeui_catalog_default = {
2610
3321
  name: "modelValue",
2611
3322
  type: "string",
2612
3323
  default: "",
2613
- note: "Uses `YYYY-MM-DDTHH:mm`."
3324
+ note: "Always emitted as `YYYY-MM-DDTHH:mm`. On input it also parses `YYYY-MM-DD` (midnight) and `YYYY-MM-DDTHH:mm:ss`; anything else parses to null and renders the placeholder."
2614
3325
  },
2615
3326
  {
2616
3327
  name: "open",
@@ -2630,17 +3341,20 @@ var treeui_catalog_default = {
2630
3341
  "sm",
2631
3342
  "md",
2632
3343
  "lg"
2633
- ]
3344
+ ],
3345
+ note: "Renders as `t-date-picker--<size>` on the root \u2014 there is no `t-date-time-picker--<size>` modifier."
2634
3346
  },
2635
3347
  {
2636
3348
  name: "disabled",
2637
3349
  type: "boolean",
2638
- default: "false"
3350
+ default: "false",
3351
+ note: "Blocks opening via click/keyboard and suppresses the overlay entirely, even when `open` is true."
2639
3352
  },
2640
3353
  {
2641
3354
  name: "loading",
2642
3355
  type: "boolean",
2643
- default: "false"
3356
+ default: "false",
3357
+ note: "Only swaps the default suffix icon for a TSpinner; the trigger stays enabled and interactive."
2644
3358
  },
2645
3359
  {
2646
3360
  name: "invalid",
@@ -2655,33 +3369,38 @@ var treeui_catalog_default = {
2655
3369
  {
2656
3370
  name: "locale",
2657
3371
  type: "string",
2658
- default: "en-US"
3372
+ default: "en-US",
3373
+ note: "Feeds all four Intl.DateTimeFormat calls: trigger text, draft preview, month caption, weekday labels."
2659
3374
  },
2660
3375
  {
2661
3376
  name: "min",
2662
3377
  type: "string",
2663
- default: ""
3378
+ default: "",
3379
+ note: "`YYYY-MM-DD` (resolved to 00:00 of that day) or `YYYY-MM-DDTHH:mm`."
2664
3380
  },
2665
3381
  {
2666
3382
  name: "max",
2667
3383
  type: "string",
2668
- default: ""
3384
+ default: "",
3385
+ note: "`YYYY-MM-DD` (resolved to 23:59:59.999 of that day) or `YYYY-MM-DDTHH:mm`."
2669
3386
  },
2670
3387
  {
2671
3388
  name: "weekStartsOn",
2672
3389
  type: "0 | 1 | 2 | 3 | 4 | 5 | 6",
2673
- default: "0"
3390
+ default: "0",
3391
+ note: "0 = Sunday. Reorders the weekday header labels, the grid start date, and Home/End day navigation."
2674
3392
  },
2675
3393
  {
2676
3394
  name: "id",
2677
3395
  type: "string",
2678
- default: "auto"
3396
+ default: "auto",
3397
+ note: "Id of the overlay dialog only \u2014 not of the root or the trigger. Read once at setup, so later changes are ignored. The trigger points at it with aria-controls while open."
2679
3398
  },
2680
3399
  {
2681
3400
  name: "step",
2682
3401
  type: "number",
2683
3402
  default: "60",
2684
- note: "Seconds between time options."
3403
+ note: "Seconds between minute options. Must be a positive multiple of 60 below 3600; any other value falls back to 1-minute options. Hours are always 0-23 regardless of step."
2685
3404
  }
2686
3405
  ],
2687
3406
  events: [
@@ -2701,12 +3420,28 @@ var treeui_catalog_default = {
2701
3420
  name: "select"
2702
3421
  }
2703
3422
  ],
2704
- slots: [],
3423
+ slots: [
3424
+ {
3425
+ name: "prefix"
3426
+ },
3427
+ {
3428
+ name: "suffix"
3429
+ }
3430
+ ],
2705
3431
  a11y: [
2706
3432
  "role: dialog",
3433
+ "aria_modal: false \u2014 the overlay is non-modal and does not trap focus",
2707
3434
  "trigger_aria_haspopup: dialog",
2708
3435
  "trigger_aria_expanded: true",
2709
- "focus_restores_on_close: true",
3436
+ "trigger_aria_controls_when_open: true",
3437
+ "overlay_labelled_by_month_caption: true",
3438
+ "calendar_uses_grid_roles: true",
3439
+ "day_buttons_use_roving_tabindex: true",
3440
+ "day_buttons_aria_pressed_when_selected: true",
3441
+ "gridcell_aria_selected: true",
3442
+ "weekday_row_aria_hidden: true",
3443
+ "time_selects_have_aria_labels: true",
3444
+ "focus_restores_on_close: Only when closing via Apply, Clear, or Escape from a day button. Closing by clicking the trigger, Escape on the trigger, or an outside pointerdown leaves focus where it is.",
2710
3445
  "keyboard_calendar_navigation: true",
2711
3446
  "escape_closes_overlay: true",
2712
3447
  "aria_invalid_when_invalid: true",
@@ -2716,7 +3451,21 @@ var treeui_catalog_default = {
2716
3451
  "controlled_and_uncontrolled_open: true",
2717
3452
  "value_format: YYYY-MM-DDTHH:mm",
2718
3453
  "min_and_max_limit_available_values: true",
2719
- "time_options_generated_from_step: true"
3454
+ "time_options_generated_from_step: true",
3455
+ "draft_then_commit: Choosing a day and changing the Hour/Minute selects mutate an internal draft only. Nothing is emitted and the overlay does NOT close until Apply \u2014 unlike TDatePicker, where selecting a day commits and closes.",
3456
+ "apply_emit_order: update:modelValue, then change, then select \u2014 all carrying the same `YYYY-MM-DDTHH:mm` string.",
3457
+ "clear_emits_empty_string: Clear emits update:modelValue, change, and select with `''`, then closes.",
3458
+ "default_draft_when_value_is_empty: Now, rounded UP to the next `step` boundary (minimum 60s) with seconds zeroed, then clamped into min/max.",
3459
+ "draft_resyncs_from_model_value_only_while_closed: true",
3460
+ "draft_is_clamped_into_min_max_on_open_and_on_day_select: true",
3461
+ "minute_options_always_include_the_current_draft_minute: true",
3462
+ "hour_option_disabled_when_no_minute_in_that_hour_is_available: true",
3463
+ "month_nav_buttons_disabled_at_min_max_month: true",
3464
+ "out_of_range_days_disabled: true",
3465
+ "outside_pointerdown_closes: true",
3466
+ "overlay_is_not_rendered_while_disabled: true",
3467
+ "hidden_input_created_when_name_attr_present: true",
3468
+ "open_state_emits_are_deduped: true"
2720
3469
  ],
2721
3470
  selection: {
2722
3471
  keywords: [
@@ -2749,14 +3498,15 @@ var treeui_catalog_default = {
2749
3498
  },
2750
3499
  recipes: [
2751
3500
  "date-range-form"
2752
- ]
3501
+ ],
3502
+ practices: []
2753
3503
  },
2754
3504
  {
2755
3505
  name: "TDivider",
2756
3506
  aliases: [],
2757
3507
  package: "@treeui/vue",
2758
3508
  category: "Layout",
2759
- description: "Horizontal or vertical visual separator.",
3509
+ description: 'Horizontal or vertical visual separator. Renders a single native <hr> and nothing else \u2014 there is no default slot, so the "text in the middle of a rule" pattern (an "OR" divider) is NOT supported; compose that yourself from a flex row plus TText. The `label` prop is an accessible name only, never visible text. There are no size, variant, tone, or spacing props.\n',
2760
3510
  sourceFile: "packages/vue/src/components/TDivider.vue",
2761
3511
  storyFile: "apps/docs/src/stories/Divider.stories.ts",
2762
3512
  storyTitle: "Components/Layout/Divider",
@@ -2764,34 +3514,41 @@ var treeui_catalog_default = {
2764
3514
  props: [
2765
3515
  {
2766
3516
  name: "orientation",
2767
- type: "string",
3517
+ type: '"horizontal" | "vertical"',
2768
3518
  default: "horizontal",
2769
3519
  values: [
2770
3520
  "horizontal",
2771
3521
  "vertical"
2772
- ]
3522
+ ],
3523
+ description: "Selects the `t-divider--{orientation}` modifier \u2014 the only class the component varies. `horizontal` spans the full inline width (width 100%, height 0) with `margin-block` spacing. `vertical` is a zero-width rule (width 0, height auto) that takes its height from `align-self: stretch`, so it is only visible inside a flex or grid row \u2014 in a plain block container it collapses to zero height and renders nothing.\n"
2773
3524
  },
2774
3525
  {
2775
3526
  name: "decorative",
2776
3527
  type: "boolean",
2777
3528
  default: "true",
2778
- description: "When true, renders role='none' (purely visual). When false, renders as role='separator'."
3529
+ description: 'When true (the default) renders `role="none"` and no ARIA attributes at all \u2014 purely visual, removed from the accessibility tree, and the `label` prop is dropped. When false renders `role="separator"` plus `aria-orientation`, and `aria-label` when `label` is set.\n'
2779
3530
  },
2780
3531
  {
2781
3532
  name: "label",
2782
3533
  type: "string",
2783
3534
  required: false,
2784
- description: "Accessible label for non-decorative dividers."
3535
+ description: 'Accessible name, applied as `aria-label` ONLY when `decorative` is false. Because `decorative` defaults to true, `<TDivider label="Section" />` on its own renders no label at all \u2014 pass `:decorative="false"` as well. Never rendered as visible text.\n'
2785
3536
  }
2786
3537
  ],
2787
3538
  events: [],
2788
3539
  slots: [],
2789
3540
  a11y: [
2790
- "role: none (decorative) or separator (semantic)",
2791
- "aria-orientation: Set when non-decorative.",
2792
- "aria-label: Set from `label` prop when non-decorative."
3541
+ 'role: "none" when decorative (the default), "separator" when `decorative` is false.',
3542
+ 'aria-orientation: Set to the `orientation` value only when `decorative` is false; omitted otherwise. Note that "horizontal" is already the implicit default for `role="separator"`, so it is redundant there but harmless.',
3543
+ "aria-label: Set from the `label` prop only when `decorative` is false. Omitted when `label` is unset, so a non-decorative divider without a label is an unnamed separator (valid \u2014 separators need no name).",
3544
+ 'notes: The decorative default is the right one for most dividers: a purely visual rule should not be announced. Use `:decorative="false"` only when the rule genuinely separates two sections that assistive technology should perceive as distinct, and pair it with `label` when the sections need naming.'
3545
+ ],
3546
+ behavior: [
3547
+ "Renders a native <hr> element as its single root node.",
3548
+ "Extra attrs and class/style fall through to that <hr> (attribute inheritance is not disabled).",
3549
+ "Applies exactly two classes; no state, size, or variant modifiers exist.",
3550
+ "A vertical divider needs a flex or grid parent to have any height."
2793
3551
  ],
2794
- behavior: [],
2795
3552
  selection: {
2796
3553
  keywords: [],
2797
3554
  useWhen: [],
@@ -2802,7 +3559,8 @@ var treeui_catalog_default = {
2802
3559
  required: [],
2803
3560
  notes: []
2804
3561
  },
2805
- recipes: []
3562
+ recipes: [],
3563
+ practices: []
2806
3564
  },
2807
3565
  {
2808
3566
  name: "TDonutChart",
@@ -2821,67 +3579,73 @@ var treeui_catalog_default = {
2821
3579
  name: "segments",
2822
3580
  type: "TDonutSegment[]",
2823
3581
  default: "[]",
2824
- note: "Each item is { label: string; value: number; color?: string }. Non-positive values are omitted."
3582
+ note: "Each item is { label: string; value: number; color?: string }. Values <= 0 are filtered out before anything renders \u2014 they reach neither the chart, the legend, nor the segment count in the auto aria-label. Because the palette cycles by *visible* index, dropping a non-positive item shifts the colors of every segment after it."
2825
3583
  },
2826
3584
  {
2827
3585
  name: "size",
2828
3586
  type: "number",
2829
3587
  default: "200",
2830
- note: "Square canvas in px."
3588
+ note: "Square canvas in px; applied to both the svg and the figure box. Donut geometry insets the ring by thickness/2 + 3px so the hover growth cannot clip; pie geometry uses a radius of size/2 - 2."
2831
3589
  },
2832
3590
  {
2833
3591
  name: "thickness",
2834
3592
  type: "number",
2835
3593
  default: "26",
2836
- note: "Ring thickness; 0 renders a full pie."
3594
+ note: "Ring thickness in px. `0` switches the whole render mode: segments become filled wedges to the center (a pie) and the center block is not rendered at all. Hovering a donut segment temporarily draws it at thickness + 4."
2837
3595
  },
2838
3596
  {
2839
3597
  name: "gap",
2840
3598
  type: "number",
2841
3599
  default: "2",
2842
- note: "Gap between slices, in degrees."
3600
+ note: "Gap between slices, in degrees. In donut mode it is converted to arc length on the mid-radius circle and then floored \u2014 the effective gap is at least 2px, or at least thickness + 2px when rounded \u2014 so small values (including 0) still leave a visible seam. Forced to 0 whenever exactly one segment is visible."
2843
3601
  },
2844
3602
  {
2845
3603
  name: "rounded",
2846
3604
  type: "boolean",
2847
3605
  default: "false",
2848
- note: "Round the ends of donut segments (widens the gap to make room). Best with a thinner ring."
3606
+ note: "Round the ends of donut segments (widens the gap to make room). Best with a thinner ring. Donut only \u2014 stroke-linecap is not applied to pie wedges, so it is ignored when thickness is 0."
2849
3607
  },
2850
3608
  {
2851
3609
  name: "showLegend",
2852
3610
  type: "boolean",
2853
- default: "true"
3611
+ default: "true",
3612
+ note: "The legend only renders when there is data \u2014 it is suppressed in the empty state even when true, but it is NOT suppressed while loading, so real values sit next to the skeleton. The has-legend root class tracks the prop alone, not whether the legend actually rendered."
2854
3613
  },
2855
3614
  {
2856
3615
  name: "showPercent",
2857
3616
  type: "boolean",
2858
- default: "true"
3617
+ default: "true",
3618
+ note: "Legend only. Percentages are rounded to whole numbers, so they may not sum to exactly 100%."
2859
3619
  },
2860
3620
  {
2861
3621
  name: "centerLabel",
2862
3622
  type: "string",
2863
- default: "Total",
2864
- note: "Donut only; ignored for pie."
3623
+ default: "undefined",
3624
+ note: "Static label under the center value. Falls back to the literal string 'Total' when unset, and is replaced by the hovered segment's label while a segment is hovered. Donut only; the center block is not rendered for a pie."
2865
3625
  },
2866
3626
  {
2867
3627
  name: "centerValue",
2868
3628
  type: "string | number",
2869
- default: "auto (sum of values)"
3629
+ default: "undefined",
3630
+ note: "Overrides the auto total in the center readout; falls back to the formatted sum of the visible values. Interpolated as-is (no valueFormat). Lower precedence than hover: hovering a segment replaces it with that segment's formatted value. Donut only."
2870
3631
  },
2871
3632
  {
2872
3633
  name: "valueFormat",
2873
3634
  type: "(value: number) => string",
2874
- default: "locale integers for |value| >= 1000"
3635
+ default: "locale integers for |value| >= 1000",
3636
+ note: "Applies to the legend values and to the center value. Not applied to percentages, nor to an explicitly supplied centerValue."
2875
3637
  },
2876
3638
  {
2877
3639
  name: "loading",
2878
3640
  type: "boolean",
2879
- default: "false"
3641
+ default: "false",
3642
+ note: "Replaces the figure with a pulsing skeleton disc and adds the is-loading root class. Takes precedence over the empty state; the legend still renders when data is present."
2880
3643
  },
2881
3644
  {
2882
3645
  name: "ariaLabel",
2883
3646
  type: "string",
2884
- default: "auto-generated summary"
3647
+ default: "auto-generated summary ('Donut chart with <n> segments' / 'Pie chart with <n> segments')",
3648
+ note: 'Applied to the inner <svg role="img">, which only exists in the data state \u2014 neither the loading nor the empty state carries it.'
2885
3649
  }
2886
3650
  ],
2887
3651
  events: [],
@@ -2894,13 +3658,20 @@ var treeui_catalog_default = {
2894
3658
  }
2895
3659
  ],
2896
3660
  a11y: [
2897
- "Renders role=img with an aria-label summarizing the chart.",
2898
- "Legend + percentages give a non-visual reading of each slice."
3661
+ "The inner <svg> carries role=img plus the ariaLabel (or the auto-generated summary); the root div has no role or accessible name.",
3662
+ "Legend + percentages give a non-visual reading of each slice.",
3663
+ "The hover readout is pointer-only \u2014 segments are not focusable and there is no keyboard equivalent, so never make it the only way to reach a value. The legend already carries the same numbers.",
3664
+ "Legend swatches are aria-hidden, so color is never the only carrier of identity.",
3665
+ "Provide a meaningful ariaLabel describing what the chart shows."
2899
3666
  ],
2900
3667
  behavior: [
2901
- "colors_auto_assign_from_chart_palette: --tree-color-chart-1..8, cycling; per-segment color overrides",
2902
- "hovering_a_slice_dims_the_rest_and_updates_center: true",
2903
- "center_shows_total_or_the_hovered_segment: true"
3668
+ "colors_auto_assign_from_chart_palette: --tree-color-chart-1..8, cycling by visible-segment index; per-segment color overrides",
3669
+ "hovering_a_slice_dims_the_rest_and_updates_center: the other segments get is-dimmed, the hovered donut segment grows to thickness + 4, and its legend row gets is-active",
3670
+ "hovering_a_legend_row_drives_the_same_highlight: legend items carry the same pointerenter/pointerleave handlers as the segments",
3671
+ "center_readout_precedence: hovered segment > centerValue > formatted total; the label half is hovered segment label > centerLabel > 'Total'",
3672
+ "single_segment_drops_the_gap: with exactly one visible segment the gap is forced to 0 in both donut and pie mode, so the ring/disc closes",
3673
+ "empty_state_when_no_visible_segment_has_value: total <= 0 renders the empty slot instead of the svg; loading wins over it",
3674
+ "segments_fade_in_staggered: 70ms animation delay per segment index; removed under prefers-reduced-motion"
2904
3675
  ],
2905
3676
  selection: {
2906
3677
  keywords: [
@@ -2938,7 +3709,8 @@ var treeui_catalog_default = {
2938
3709
  },
2939
3710
  recipes: [
2940
3711
  "metric-dashboard"
2941
- ]
3712
+ ],
3713
+ practices: []
2942
3714
  },
2943
3715
  {
2944
3716
  name: "TDrawer",
@@ -2956,17 +3728,20 @@ var treeui_catalog_default = {
2956
3728
  {
2957
3729
  name: "open",
2958
3730
  type: "boolean",
2959
- default: "controlled_optional"
3731
+ default: "controlled_optional",
3732
+ note: "Runtime default is `undefined`. While it holds anything other than `undefined` the drawer is fully controlled \u2014 the internal state is ignored and the panel only moves when the parent updates this prop."
2960
3733
  },
2961
3734
  {
2962
3735
  name: "defaultOpen",
2963
3736
  type: "boolean",
2964
- default: "false"
3737
+ default: "false",
3738
+ note: "Uncontrolled initial state. Read once on setup \u2014 later changes are ignored. Only meaningful while `open` is undefined. Opening this way skips the on-open side effects; see behavior.initial_open_skips_open_side_effects."
2965
3739
  },
2966
3740
  {
2967
3741
  name: "disabled",
2968
3742
  type: "boolean",
2969
- default: "false"
3743
+ default: "false",
3744
+ note: "Only blocks opening from the `trigger` slot \u2014 the trigger wrapper gets `aria-disabled` and its click is swallowed (preventDefault + stopPropagation). It does NOT block a programmatic open through `open`/`defaultOpen`, and it disables nothing inside the drawer."
2970
3745
  },
2971
3746
  {
2972
3747
  name: "size",
@@ -2976,7 +3751,8 @@ var treeui_catalog_default = {
2976
3751
  "sm",
2977
3752
  "md",
2978
3753
  "lg"
2979
- ]
3754
+ ],
3755
+ note: "Applied as `t-drawer__surface--<size>`. Sizes the cross axis of the panel: width for `side: left | right`, height for `side: top | bottom`."
2980
3756
  },
2981
3757
  {
2982
3758
  name: "side",
@@ -2987,42 +3763,50 @@ var treeui_catalog_default = {
2987
3763
  "right",
2988
3764
  "bottom",
2989
3765
  "left"
2990
- ]
3766
+ ],
3767
+ note: "Applied as `t-drawer__surface--<side>`. Also selects the slide-in translate direction and which corners are rounded."
2991
3768
  },
2992
3769
  {
2993
3770
  name: "title",
2994
3771
  type: "string",
2995
- default: ""
3772
+ default: "",
3773
+ note: "Rendered as `t-drawer__title` inside the fallback content of the `header` slot. An empty title with no `header` slot removes the header block entirely and leaves `aria-labelledby` unset \u2014 the dialog then has no accessible name."
2996
3774
  },
2997
3775
  {
2998
3776
  name: "description",
2999
3777
  type: "string",
3000
- default: ""
3778
+ default: "",
3779
+ note: "Rendered as `t-drawer__description` between the topbar and the body, and referenced by `aria-describedby`. Only rendered when non-empty. The `content`/`default` body is NOT part of the accessible description."
3001
3780
  },
3002
3781
  {
3003
3782
  name: "closeOnEscape",
3004
3783
  type: "boolean",
3005
- default: "true"
3784
+ default: "true",
3785
+ note: "Escape is handled on the surface's own keydown, so it only fires while focus is inside the drawer. When enabled the handler also calls preventDefault."
3006
3786
  },
3007
3787
  {
3008
3788
  name: "closeOnOverlay",
3009
3789
  type: "boolean",
3010
- default: "true"
3790
+ default: "true",
3791
+ note: "Applies to clicks on `t-drawer__backdrop` only. The backdrop is a sibling of the surface, not an ancestor, so clicks inside the panel never reach it regardless of this prop."
3011
3792
  },
3012
3793
  {
3013
3794
  name: "showCloseButton",
3014
3795
  type: "boolean",
3015
- default: "true"
3796
+ default: "true",
3797
+ note: "The topbar renders when `title` or the `header` slot is present OR this is true. Setting it false with no title and no `header` slot collapses the topbar entirely."
3016
3798
  },
3017
3799
  {
3018
3800
  name: "closeLabel",
3019
3801
  type: "string",
3020
- default: "Close drawer"
3802
+ default: "Close drawer",
3803
+ note: "Accessible name (aria-label) of the header close button, which is icon-only and has no text. Only rendered when `showCloseButton` is true. The component's only localisation hook."
3021
3804
  },
3022
3805
  {
3023
3806
  name: "id",
3024
3807
  type: "string",
3025
- default: "auto"
3808
+ default: "auto",
3809
+ note: "Id of the dialog surface; seeds `<id>-title` and `<id>-description`. Read once on setup (not reactive). Auto-generated via `createId('t-drawer')` when omitted."
3026
3810
  }
3027
3811
  ],
3028
3812
  events: [
@@ -3035,34 +3819,53 @@ var treeui_catalog_default = {
3035
3819
  ],
3036
3820
  slots: [
3037
3821
  {
3038
- name: "trigger"
3822
+ name: "trigger",
3823
+ description: "Element that opens the drawer on click. Omit for controlled or imperative use."
3039
3824
  },
3040
3825
  {
3041
- name: "header"
3826
+ name: "header",
3827
+ description: "Replaces the default title heading in the topbar."
3042
3828
  },
3043
3829
  {
3044
- name: "default"
3830
+ name: "default",
3831
+ description: "Drawer body content."
3045
3832
  },
3046
3833
  {
3047
- name: "content"
3834
+ name: "content",
3835
+ description: "Drawer body content; takes precedence over the default slot."
3048
3836
  },
3049
3837
  {
3050
- name: "footer"
3838
+ name: "footer",
3839
+ description: "Action row pinned under the body."
3051
3840
  }
3052
3841
  ],
3053
3842
  a11y: [
3054
3843
  "role: dialog",
3055
3844
  "aria_modal: true",
3056
3845
  "focus_trap: true",
3057
- "restores_focus_on_close: true",
3058
- "escape_closes_when_enabled: true"
3846
+ 'restores_focus_on_close: {"value":"partial","detail":"The previously focused element is recorded only in the trigger-click path. A drawer opened programmatically \u2014 by setting `open` from elsewhere, or by `defaultOpen` \u2014 records nothing, so focus is not restored on close. Move focus back yourself in that case.\\n"}',
3847
+ "escape_closes_when_enabled: true",
3848
+ "escape_scope: The keydown listener lives on the surface, so Escape only closes while focus is inside the drawer.",
3849
+ "accessible_name_source: the `t-drawer__header` div, via aria-labelledby; unset when `title` is empty and no `header` slot is supplied",
3850
+ "accessible_description_source: the `description` prop, via aria-describedby; the body slots are not included",
3851
+ 'initial_focus: First focusable element inside the surface, else the surface itself (`tabindex=\\"-1\\"`, with a visible `:focus-visible` ring). Applied on the closed \u2192 open transition only.',
3852
+ "backdrop_marked_aria_hidden: true",
3853
+ "trigger_state_not_announced: The trigger wrapper gets no aria-haspopup, aria-expanded, or aria-controls, and no scoped props are exposed to build them. A trigger that needs to announce the drawer must wire those attributes from consumer state."
3059
3854
  ],
3060
3855
  behavior: [
3061
3856
  "uses_teleport_to_body: true",
3062
3857
  "locks_body_scroll_while_open: true",
3063
3858
  "overlay_click_closes_when_enabled: true",
3064
3859
  "close_button_optional: true",
3065
- "slides_in_from_side: true"
3860
+ "slides_in_from_side: true",
3861
+ "root_uses_display_contents_without_trigger: `t-drawer` is `display: inline-flex`, but a root with no `t-drawer__trigger` child switches to `display: contents` so the empty wrapper does not generate a line box and add stray height to the page.",
3862
+ "topbar_rendered_when: title is non-empty OR the `header` slot is supplied OR showCloseButton is true",
3863
+ "body_slot_precedence: content > default",
3864
+ "footer_rendered_when: the `footer` slot is supplied",
3865
+ 'data_state_attribute: `data-state="open" | "closed"` on both the `t-drawer` root and the `t-drawer__portal`',
3866
+ "focus_trap_detail: Tab and Shift+Tab cycle within the surface. With no focusable descendants, Tab is swallowed and focus is pushed back to the surface itself.",
3867
+ 'initial_open_skips_open_side_effects: {"value":true,"detail":"The on-open work \u2014 body scroll lock and initial focus \u2014 runs from a watcher on the open state that is not immediate, so a drawer that starts open via `defaultOpen: true` (or an `open` prop already true on first render) never locks scroll and never moves focus into the surface. Focus trap and Escape therefore stay dormant until the user tabs in. Start closed and open on the next tick if you need the full behavior.\\n"}',
3868
+ 'body_scroll_lock_scope: {"value":"per_component_instance","detail":"The overlay counter and the saved `overflow` value are per instance, not shared across overlays. Two overlays open at once each restore what they individually saw, so closing the first-opened one while a second is still open unlocks page scroll early.\\n"}'
3066
3869
  ],
3067
3870
  selection: {
3068
3871
  keywords: [
@@ -3091,7 +3894,10 @@ var treeui_catalog_default = {
3091
3894
  required: [],
3092
3895
  notes: []
3093
3896
  },
3094
- recipes: []
3897
+ recipes: [],
3898
+ practices: [
3899
+ "accessible-by-default"
3900
+ ]
3095
3901
  },
3096
3902
  {
3097
3903
  name: "TDropdown",
@@ -3110,22 +3916,25 @@ var treeui_catalog_default = {
3110
3916
  name: "items",
3111
3917
  type: "TDropdownItem[]",
3112
3918
  default: "[]",
3113
- note: "Array of { label: string; value: string; disabled?: boolean }"
3919
+ note: 'Array of { label: string; value: string; disabled?: boolean }. Each entry renders one `<li role="menuitem">`, keyed by `value` \u2014 values must be unique, because the internal element map that drives focus is also keyed by `value` and duplicates collide. With the default `[]` the component still renders an empty `<ul role="menu">` and focus stays on the trigger; there is no empty-state affordance (no `emptyText`, no `empty` slot), so guard on `items.length` yourself.'
3114
3920
  },
3115
3921
  {
3116
3922
  name: "open",
3117
3923
  type: "boolean",
3118
- default: "undefined"
3924
+ default: "undefined",
3925
+ note: "Controlled open state. Once a boolean is passed the component stops tracking open state internally \u2014 it only emits update:open / open-change and the consumer MUST write the value back, or the menu never opens. Flipping it externally does not run the open routine: no item is focused and the outside-click listener is only attached from the transition, so an initially-`true` value opens the menu with focus untouched."
3119
3926
  },
3120
3927
  {
3121
3928
  name: "defaultOpen",
3122
3929
  type: "boolean",
3123
- default: "false"
3930
+ default: "false",
3931
+ note: "Uncontrolled initial open state, read once on mount. Later changes to it are ignored. It renders the menu already open but does NOT focus the first item (focusedIndex stays -1, so no `is-focused` row) and does NOT arm the outside-click listener until the open state changes at least once \u2014 see behavior.outside_click_closes_menu."
3124
3932
  },
3125
3933
  {
3126
3934
  name: "disabled",
3127
3935
  type: "boolean",
3128
- default: "false"
3936
+ default: "false",
3937
+ note: 'Sets the native `disabled` attribute on the built-in trigger button, adds `is-disabled` to the root, and suppresses the menu entirely (`v-if="isOpen && !disabled"`) \u2014 even while `open` is true. Also short-circuits openMenu, toggleMenu and the trigger keydown handler. It is bound only to the default button, so a custom `trigger` slot must render its own disabled state.'
3129
3938
  },
3130
3939
  {
3131
3940
  name: "size",
@@ -3135,12 +3944,14 @@ var treeui_catalog_default = {
3135
3944
  "sm",
3136
3945
  "md",
3137
3946
  "lg"
3138
- ]
3947
+ ],
3948
+ note: "Sizes the trigger and the menu rows. `t-dropdown--md` is emitted but has no CSS rule of its own \u2014 it is the base `.t-dropdown__trigger` / `.t-dropdown__item` styling."
3139
3949
  },
3140
3950
  {
3141
3951
  name: "label",
3142
3952
  type: "string",
3143
- default: ""
3953
+ default: "",
3954
+ note: 'Text of the built-in trigger button AND the menu\'s accessible name (`:aria-label="label || undefined"` on the `<ul role="menu">`). A custom `trigger` slot does not render `label`, but it still names the menu \u2014 keep passing it, or the menu has no accessible name at all.'
3144
3955
  },
3145
3956
  {
3146
3957
  name: "align",
@@ -3150,18 +3961,30 @@ var treeui_catalog_default = {
3150
3961
  "start",
3151
3962
  "end"
3152
3963
  ],
3153
- note: "Horizontal alignment of the menu relative to the trigger. Use `end` when the trigger sits near the right edge of a clipping container (e.g. a table actions column)."
3964
+ note: "Horizontal alignment of the menu relative to the trigger \u2014 `start` pins `left: 0`, `end` pins `right: 0`. Use `end` when the trigger sits near the right edge of a clipping container (e.g. a table actions column). There is no collision detection: the menu is a plain absolutely-positioned child of the root, so it never flips and is clipped by any ancestor with `overflow: hidden`."
3965
+ },
3966
+ {
3967
+ name: "id",
3968
+ type: "string",
3969
+ default: "undefined",
3970
+ note: "Custom id for the menu element. Auto-generated when omitted; also exposed to the `trigger` slot as `menuId` so a custom trigger can wire aria-controls. Resolved once during setup \u2014 it is NOT reactive, so changing `id` after mount has no effect."
3154
3971
  }
3155
3972
  ],
3156
3973
  events: [
3157
3974
  {
3158
- name: "select"
3975
+ name: "select",
3976
+ description: "Emitted with the chosen item's `value`. Disabled items never emit. Fires before the menu closes; the close that follows restores focus to the trigger.",
3977
+ payload: '[{"value":"string"}]'
3159
3978
  },
3160
3979
  {
3161
- name: "update:open"
3980
+ name: "update:open",
3981
+ description: "Emitted whenever the open state changes. Deduped \u2014 no emit when the next value equals the current one.",
3982
+ payload: '["boolean"]'
3162
3983
  },
3163
3984
  {
3164
- name: "open-change"
3985
+ name: "open-change",
3986
+ description: "Emitted immediately after `update:open`, with the same payload. Alias for consumers that do not use v-model:open.",
3987
+ payload: '["boolean"]'
3165
3988
  }
3166
3989
  ],
3167
3990
  slots: [
@@ -3174,23 +3997,39 @@ var treeui_catalog_default = {
3174
3997
  ],
3175
3998
  a11y: [
3176
3999
  "role: menu",
4000
+ "menu_accessible_name_comes_from_the_label_prop: true",
3177
4001
  "trigger_aria_haspopup: menu",
3178
4002
  "trigger_aria_expanded: true",
3179
4003
  "trigger_aria_controls: menu id, while open",
4004
+ "custom_trigger_caveat: The three trigger_aria_* guarantees above describe the BUILT-IN trigger button only. A custom `trigger` slot replaces that button, so the consumer must bind them from the `isOpen` and `menuId` scoped props \u2014 see slots.trigger.",
4005
+ "item_role: menuitem",
3180
4006
  "aria_disabled_on_disabled_items: true",
4007
+ "disabled_items_get_tabindex_minus_1_but_stay_programmatically_focusable: true",
3181
4008
  "focus_visible_on_trigger: true",
4009
+ "items_receive_real_dom_focus_no_aria_activedescendant: true",
3182
4010
  "keyboard_arrow_navigation: true",
4011
+ "arrow_navigation_skips_disabled_items: true",
4012
+ "arrow_navigation_does_not_wrap_at_the_ends: true",
4013
+ "arrow_down_or_arrow_up_on_the_trigger_both_open_at_the_first_enabled_item: true",
3183
4014
  "escape_closes_menu: true",
3184
4015
  "enter_space_selects_item: true",
3185
- "home_end_navigation: true"
4016
+ "home_end_navigation: true",
4017
+ "no_typeahead: true",
4018
+ "tab_does_not_close_the_menu: true"
3186
4019
  ],
3187
4020
  behavior: [
3188
- "outside_click_closes_menu: true",
4021
+ 'outside_click_closes_menu: {"value":true,"detail":"Detected on a document `pointerdown` listener that is attached by a watcher on the open state. The watcher is not immediate, so a menu that is already open on mount \u2014 via `defaultOpen` or an initially-`true` `open` \u2014 has no listener until the open state changes at least once, and outside clicks do not close it.\\n"}',
3189
4022
  "outside_click_restores_focus: false",
3190
4023
  "chevron_rotates_when_open: true",
3191
4024
  "menu_grows_to_fit_items_without_wrapping: true",
3192
- "opening_focuses_first_enabled_item: true",
3193
- 'focus_restores_on_close: {"value":true,"detail":"Focus returns to the trigger when the menu closes from inside it \u2014 selecting an item or pressing Escape. It deliberately does NOT move focus on an outside click, where the pointer has already placed focus elsewhere.\\n","applies_to_custom_trigger":true}'
4025
+ "menu_scrolls_vertically_past_15rem: true",
4026
+ "menu_renders_in_flow_with_no_portal_and_no_flip_positioning: true",
4027
+ "menu_is_not_rendered_while_disabled: true",
4028
+ "open_state_emits_are_deduped: true",
4029
+ "controlled_open_requires_writing_the_value_back: true",
4030
+ 'opening_focuses_first_enabled_item: {"value":true,"detail":"Applies to the open routine only \u2014 a click or Enter/Space/ArrowDown/ArrowUp on the trigger. `defaultOpen` and an externally driven `open` skip it, leaving focusedIndex at -1 with no row focused and no `is-focused` class. With an empty `items` array focus stays on the trigger; when every item is disabled, focus lands on the first (disabled) item.\\n"}',
4031
+ 'keyboard_activation_on_the_trigger_never_closes_the_menu: {"value":true,"detail":"Clicking the trigger toggles, but Enter/Space/Arrow keys always call the open routine, so pressing them while the menu is open re-focuses the first enabled item instead of closing. Escape is the keyboard close.\\n"}',
4032
+ 'focus_restores_on_close: {"value":true,"detail":"Focus returns to the trigger when the menu closes from inside it \u2014 selecting an item or pressing Escape on an item. Closing from the trigger itself (clicking it again, or Escape while the trigger holds focus) does not run the restore, because focus is already there. It deliberately does NOT move focus on an outside click, where the pointer has already placed focus elsewhere.\\n","applies_to_custom_trigger":true}'
3194
4033
  ],
3195
4034
  selection: {
3196
4035
  keywords: [
@@ -3219,7 +4058,11 @@ var treeui_catalog_default = {
3219
4058
  required: [],
3220
4059
  notes: []
3221
4060
  },
3222
- recipes: []
4061
+ recipes: [],
4062
+ practices: [
4063
+ "interaction-feedback",
4064
+ "accessible-by-default"
4065
+ ]
3223
4066
  },
3224
4067
  {
3225
4068
  name: "TEmptyState",
@@ -3311,7 +4154,10 @@ var treeui_catalog_default = {
3311
4154
  required: [],
3312
4155
  notes: []
3313
4156
  },
3314
- recipes: []
4157
+ recipes: [],
4158
+ practices: [
4159
+ "state-clarity"
4160
+ ]
3315
4161
  },
3316
4162
  {
3317
4163
  name: "TFileUpload",
@@ -3345,7 +4191,7 @@ var treeui_catalog_default = {
3345
4191
  name: "disabled",
3346
4192
  type: "boolean",
3347
4193
  default: "false",
3348
- description: "The ONLY prop that gates interaction. Disables the dropzone, the native input, Clear all, and every per-row Remove/Retry."
4194
+ description: "The ONLY prop that gates interaction. Disables the dropzone, the native input, Clear all, and every per-row Remove/Retry, and makes the paste and drop handlers early-return."
3349
4195
  },
3350
4196
  {
3351
4197
  name: "loading",
@@ -3370,13 +4216,15 @@ var treeui_catalog_default = {
3370
4216
  },
3371
4217
  {
3372
4218
  name: "maxFiles",
3373
- type: "number",
3374
- default: "none"
4219
+ type: "number | null",
4220
+ default: "null",
4221
+ description: "Cap on the TOTAL list size, not on one batch \u2014 remaining slots are counted against the files already selected. `multiple: false` overrides it with a hard cap of 1. Values below 0 are clamped to 0, which rejects everything."
3375
4222
  },
3376
4223
  {
3377
4224
  name: "maxFileSize",
3378
- type: "number",
3379
- default: "none"
4225
+ type: "number | null",
4226
+ default: "null",
4227
+ description: "Per-file byte cap. Enforced only when it is a number greater than 0 \u2014 both `null` and `0` disable the check."
3380
4228
  },
3381
4229
  {
3382
4230
  name: "label",
@@ -3550,19 +4398,144 @@ var treeui_catalog_default = {
3550
4398
  "thumbnails_are_client_only: Object URLs are minted after mount and revoked on removal and unmount. SSR and the hydrating render both show the extension chip, so there is no mismatch."
3551
4399
  ],
3552
4400
  selection: {
3553
- keywords: [],
3554
- useWhen: [],
3555
- avoidWhen: [],
3556
- alternatives: []
4401
+ keywords: [],
4402
+ useWhen: [],
4403
+ avoidWhen: [],
4404
+ alternatives: []
4405
+ },
4406
+ setup: {
4407
+ required: [],
4408
+ notes: [
4409
+ "Bind `modelValue` to `File[]`.",
4410
+ "Use `accept`, `multiple`, and size limits in the app layer to constrain uploads."
4411
+ ]
4412
+ },
4413
+ recipes: [],
4414
+ practices: []
4415
+ },
4416
+ {
4417
+ name: "TFlag",
4418
+ aliases: [],
4419
+ package: "@treeui/vue",
4420
+ kind: "display",
4421
+ category: "Data Display",
4422
+ status: "available",
4423
+ purpose: "Render a flat country flag image from a CDN at a fixed height, centred in a 3:2 box, with a text fallback when the request fails.",
4424
+ sourceFile: "packages/vue/src/components/TFlag.vue",
4425
+ storyFile: "apps/docs/src/stories/Flag.stories.ts",
4426
+ storyTitle: "Components/Data Display/Flag",
4427
+ manifestFile: "docs/ai/COMPONENTS/flag.yaml",
4428
+ props: [
4429
+ {
4430
+ name: "code",
4431
+ type: "string",
4432
+ required: true,
4433
+ description: "ISO 3166-1 alpha-2 country code. Case-insensitive \u2014 trimmed and lowercased before the URL is built."
4434
+ },
4435
+ {
4436
+ name: "baseUrl",
4437
+ type: "string",
4438
+ default: "https://flagcdn.com",
4439
+ description: "Origin serving the flag assets. The component appends a fixed path built from the CDN's fixed-height segments, so a self-hosted mirror must reproduce it exactly: 1x is `${baseUrl}/h20/{code}.png` (sm), `${baseUrl}/h20/{code}.png` (md), `${baseUrl}/h24/{code}.png` (lg); the matching 2x asset is emitted via srcset as `${baseUrl}/h40/{code}.png` (sm), `${baseUrl}/h40/{code}.png` (md), `${baseUrl}/h60/{code}.png` (lg). A trailing slash is stripped, so `https://cdn.example.com/flags/` and `https://cdn.example.com/flags` behave identically."
4440
+ },
4441
+ {
4442
+ name: "size",
4443
+ type: "TSize",
4444
+ default: "md",
4445
+ values: [
4446
+ "sm",
4447
+ "md",
4448
+ "lg"
4449
+ ],
4450
+ description: "Box height, taken from `--tree-size-icon-sm/md/lg` (16px / 20px / 24px); the box width is 3:2 of that height. Also selects which CDN asset pair is requested \u2014 see `baseUrl`."
4451
+ },
4452
+ {
4453
+ name: "label",
4454
+ type: "string",
4455
+ default: "undefined",
4456
+ description: 'Accessible label. When set, the flag becomes role="img" with aria-label; otherwise it is aria-hidden. An empty string counts as unset \u2014 the flag stays decorative.'
4457
+ },
4458
+ {
4459
+ name: "fallbackText",
4460
+ type: "string",
4461
+ default: "undefined",
4462
+ description: 'Text shown when the image cannot load. Defaults to the normalized `code` uppercased, so a padded `" br "` falls back to `BR`.'
4463
+ }
4464
+ ],
4465
+ events: [],
4466
+ slots: [],
4467
+ a11y: [
4468
+ "decorative_by_default: true",
4469
+ "becomes_image_when_labeled: true",
4470
+ 'image_alt_is_always_empty: The `<img>` always carries `alt=""`, so the accessible name comes only from `label` on the root span \u2014 never from the image itself.',
4471
+ 'fallback_text_is_hidden_when_decorative: The fallback text renders inside the root span, so without `label` it inherits `aria-hidden="true"` and is not announced. It is a visual affordance, not an accessible name.',
4472
+ 'consumer_aria_label_does_not_replace_label: Fallthrough attributes land on the same root span, so a consumer-supplied `aria-label` overrides the one built from `label` \u2014 but it does not set `role="img"` or clear `aria-hidden="true"`, leaving the flag hidden and the label unannounced. Use the `label` prop instead.',
4473
+ "rule: Provide `label` only when the flag carries meaning that is not already in adjacent text \u2014 inside a language option the label is redundant."
4474
+ ],
4475
+ behavior: [
4476
+ "falls_back_to_text_when_image_fails: true",
4477
+ "failed_state_resets_when_resolved_src_changes: true",
4478
+ "code_is_trimmed_and_lowercased: true",
4479
+ "fixed_height_endpoints_serve_flat_artwork_at_true_proportions: The CDN's ratio-normalised (width-by-height) endpoints do force every flag into one box, but only by serving the WAVING artwork, which reads as decoration next to flat UI icons. The fixed-height endpoints used here (h20, h24, h40, h60) serve the flat artwork at each flag's true proportions instead: Nepal stays a pennant, Switzerland stays square, Qatar stays elongated.",
4480
+ "shared_height_not_shared_ratio_is_what_aligns_a_column: Within a size step every flag is requested at the same asset height, so a stacked list lines up even though the widths differ. CSS then places each flag in a fixed 3:2 box (3:2 being the most common national flag ratio) with `object-fit: contain`, so the majority fill the box and the outliers centre inside it rather than being cropped or distorted.",
4481
+ "retina_asset_is_at_least_twice_the_box_height: Box heights come from `--tree-size-icon-sm/md/lg` (16px / 20px / 24px) and the srcset asset is h40 (sm, md) or h60 (lg), so a 2x screen never upscales.",
4482
+ "size_outside_the_union_falls_back_to_md: A JS consumer can pass a size outside `TSize`; the asset lookup falls back to the md pair rather than throwing on an undefined entry.",
4483
+ "base_url_trailing_slash_is_normalised: Trailing slashes are stripped from `baseUrl` before the path is appended, so a self-hosted mirror cannot silently produce a double-slashed URL.",
4484
+ "sm_and_md_share_one_asset_pair: sm and md both request h20 (h40 at 2x); only lg differs (h24, h60 at 2x). Four distinct segments exist in total, not one pair per size step, so a mirror needs four directories per flag.",
4485
+ "path_template_is_public_contract: Changing `baseUrl` is a config change, not a breaking one, as long as the mirror serves the same fixed-height segments (h20, h24, h40, h60).",
4486
+ "image_element_is_removed_not_hidden_on_failure: The `<img>` is behind `v-if`, so a failed load takes it out of the DOM and swaps in the fallback span \u2014 it is not left in place with `display: none`.",
4487
+ "no_country_or_locale_dataset_is_shipped: TreeUI never maps a language or locale to a country code \u2014 the application owns that decision."
4488
+ ],
4489
+ selection: {
4490
+ keywords: [
4491
+ "flag",
4492
+ "country flag",
4493
+ "country",
4494
+ "nationality",
4495
+ "region",
4496
+ "iso 3166",
4497
+ "alpha-2"
4498
+ ],
4499
+ useWhen: [
4500
+ "You need a country flag rendered from an ISO 3166-1 alpha-2 code.",
4501
+ "Flags stacked in a list must share one height so rows stay aligned \u2014 the flat assets keep each flag's true proportions and centre inside a fixed 3:2 box.",
4502
+ "You want a text fallback when the flag asset cannot load."
4503
+ ],
4504
+ avoidWhen: [
4505
+ "The flag would be the only carrier of meaning \u2014 always pair it with adjacent text such as the language or country name.",
4506
+ "You need an interface glyph rather than a country flag.",
4507
+ "You are treating the flag as a language identity \u2014 a flag names a country, not a language."
4508
+ ],
4509
+ alternatives: [
4510
+ {
4511
+ component: "TIcon",
4512
+ when: "The graphic is a registered TreeUI icon, not a country flag."
4513
+ },
4514
+ {
4515
+ component: "TLanguageSelect",
4516
+ when: "The flag belongs to a language or locale picker rather than standing alone."
4517
+ }
4518
+ ]
3557
4519
  },
3558
4520
  setup: {
3559
- required: [],
4521
+ required: [
4522
+ "Pass `code` as an ISO 3166-1 alpha-2 country code. Matching is case-insensitive \u2014 the value is trimmed and lowercased internally.",
4523
+ "Allow `https://flagcdn.com` in the app `Content-Security-Policy` `img-src` directive, or point `baseUrl` at a self-hosted mirror \u2014 flag images are fetched from a third-party CDN at runtime."
4524
+ ],
3560
4525
  notes: [
3561
- "Bind `modelValue` to `File[]`.",
3562
- "Use `accept`, `multiple`, and size limits in the app layer to constrain uploads."
4526
+ "A mirror must serve `{baseUrl}/{segment}/{code}.png` for both steps of every size: 1x `h20` (sm), `h20` (md), `h24` (lg); 2x `h40` (sm), `h40` (md), `h60` (lg). The 2x asset is emitted through `srcset`.",
4527
+ "Those fixed-height segments are part of the contract, and they are the CDN's flat artwork \u2014 a mirror must serve flat flags, not the waving ones.",
4528
+ "Do not substitute the CDN's ratio-normalised width-by-height endpoints. They equalise aspect ratio only by switching to the waving artwork, which reads as decoration next to flat UI icons.",
4529
+ "The fixed-height assets keep each flag's true proportions \u2014 Nepal is a pennant, Switzerland is square, Qatar is elongated. A shared height, not a shared ratio, is what aligns a column of flags.",
4530
+ "CSS puts the image in a fixed 3:2 box (3:2 being the most common national flag ratio) with `object-fit: contain`, so outliers centre inside the box instead of being cropped or distorted.",
4531
+ "Each retina segment is at least twice its box height \u2014 boxes are `--tree-size-icon-sm/md/lg` = 16px/20px/24px \u2014 so 2x screens never upscale.",
4532
+ "`baseUrl` may be written with or without a trailing slash; trailing slashes are stripped before the path is built. An unknown `size` falls back to the `md` assets.",
4533
+ "When the image cannot load \u2014 offline, blocked by CSP, unknown code \u2014 the component renders the uppercased `code` as text. Override that text with `fallbackText`.",
4534
+ "The flag is `aria-hidden` by default. Pass `label` only when the flag carries meaning on its own; beside a visible country or language name it should stay decorative."
3563
4535
  ]
3564
4536
  },
3565
- recipes: []
4537
+ recipes: [],
4538
+ practices: []
3566
4539
  },
3567
4540
  {
3568
4541
  name: "TFormField",
@@ -3691,7 +4664,12 @@ var treeui_catalog_default = {
3691
4664
  "date-range-form",
3692
4665
  "color-field",
3693
4666
  "tag-input",
3694
- "hierarchy-picker"
4667
+ "hierarchy-picker",
4668
+ "language-switcher"
4669
+ ],
4670
+ practices: [
4671
+ "accessible-by-default",
4672
+ "content-alignment"
3695
4673
  ]
3696
4674
  },
3697
4675
  {
@@ -3793,6 +4771,9 @@ var treeui_catalog_default = {
3793
4771
  "date-range-form",
3794
4772
  "stat-grid",
3795
4773
  "metric-dashboard"
4774
+ ],
4775
+ practices: [
4776
+ "content-alignment"
3796
4777
  ]
3797
4778
  },
3798
4779
  {
@@ -3813,24 +4794,380 @@ var treeui_catalog_default = {
3813
4794
  type: "TIconName",
3814
4795
  required: true,
3815
4796
  values: [
4797
+ "account",
4798
+ "activity",
4799
+ "ai-studio",
3816
4800
  "alert-circle",
4801
+ "align-left",
4802
+ "app-window",
4803
+ "apps-grid",
4804
+ "archive",
4805
+ "archive-restore",
4806
+ "arrow-down",
4807
+ "arrow-left",
4808
+ "arrow-left-right",
4809
+ "arrow-right",
4810
+ "arrow-up",
4811
+ "arrow-up-right",
4812
+ "assistant",
4813
+ "automation-key",
4814
+ "automations",
4815
+ "badge",
4816
+ "badge-check",
4817
+ "badge-star",
4818
+ "ban",
4819
+ "bell",
4820
+ "book-open",
4821
+ "bookmark",
4822
+ "bot",
4823
+ "bot-badge",
4824
+ "bot-users",
4825
+ "boxes",
4826
+ "boxes-model",
4827
+ "braces",
4828
+ "brackets",
4829
+ "brain",
4830
+ "brain-circuit",
4831
+ "brain-lock",
4832
+ "browser",
4833
+ "brush",
4834
+ "brush-stroke",
4835
+ "bug",
4836
+ "building-2",
4837
+ "calculator",
3817
4838
  "calendar",
4839
+ "calendar-clock",
4840
+ "calendar-day",
4841
+ "calendar-days",
4842
+ "calendar-dot",
4843
+ "calendar-plus",
4844
+ "calendar-range",
4845
+ "calendar-x",
4846
+ "carousel",
4847
+ "catalog",
4848
+ "chart-column",
4849
+ "chart-line",
4850
+ "chart-pie",
4851
+ "chat",
3818
4852
  "check",
3819
- "chevron-left",
4853
+ "check-circle",
4854
+ "check-square",
3820
4855
  "chevron-down",
4856
+ "chevron-left",
3821
4857
  "chevron-right",
4858
+ "chevron-up",
4859
+ "chevron-updown",
4860
+ "chevrons-up-down",
4861
+ "circle-alert",
4862
+ "circle-check",
4863
+ "circle-dot",
4864
+ "circle-help",
4865
+ "circle-x",
4866
+ "clipboard-list",
4867
+ "clock",
4868
+ "clock-alert",
4869
+ "clock-sparkles",
4870
+ "clock-x",
4871
+ "close",
4872
+ "cloud",
4873
+ "cloud-off",
4874
+ "code",
4875
+ "code-2",
4876
+ "code-api",
4877
+ "coins",
4878
+ "comment",
4879
+ "companion",
4880
+ "compass",
4881
+ "connections",
4882
+ "contentpilot",
4883
+ "copy",
4884
+ "cpu",
4885
+ "cpu-chip",
4886
+ "credit-card",
4887
+ "crosshair",
4888
+ "crown",
4889
+ "cube",
4890
+ "database",
4891
+ "developer-settings",
4892
+ "device-link",
4893
+ "dollar-circle",
4894
+ "dollar-limit",
4895
+ "download",
4896
+ "download-video",
4897
+ "draw",
4898
+ "droplet",
4899
+ "ellipsis",
4900
+ "ellipsis-vertical",
4901
+ "embed-code",
4902
+ "eraser",
4903
+ "extension",
4904
+ "external-link",
4905
+ "eye",
4906
+ "eye-off",
4907
+ "file",
4908
+ "file-archive",
4909
+ "file-audio",
4910
+ "file-code",
4911
+ "file-edit",
4912
+ "file-image",
4913
+ "file-pdf",
4914
+ "file-plus",
4915
+ "file-scan",
4916
+ "file-text",
4917
+ "file-video",
4918
+ "file-warning",
4919
+ "files",
4920
+ "film",
4921
+ "filter",
4922
+ "filter-capabilities",
4923
+ "fingerprint",
4924
+ "flag",
4925
+ "flask-play",
4926
+ "folder",
4927
+ "folder-input",
4928
+ "folder-kanban",
4929
+ "folder-open",
4930
+ "folder-plus",
4931
+ "folder-shared",
4932
+ "folder-tree",
4933
+ "folder-x",
4934
+ "folders",
4935
+ "gauge",
4936
+ "gauge-high",
4937
+ "gauge-low",
4938
+ "gauge-medium",
4939
+ "gavel",
4940
+ "git-branch",
4941
+ "git-fork",
4942
+ "globe",
4943
+ "globe-check",
4944
+ "grid",
4945
+ "grip-vertical",
4946
+ "hand-check",
4947
+ "hard-drive",
4948
+ "hard-drive-alert",
4949
+ "hard-drive-off",
4950
+ "hash",
4951
+ "heart",
4952
+ "heart-chart-up",
4953
+ "heart-pulse",
4954
+ "help",
4955
+ "hierarchy",
4956
+ "history",
4957
+ "home",
4958
+ "hourglass",
4959
+ "house",
4960
+ "human-lock",
4961
+ "id-badge",
4962
+ "image",
4963
+ "image-minus",
4964
+ "image-plus",
4965
+ "image-up",
4966
+ "inbox",
4967
+ "inbox-empty",
3822
4968
  "info",
4969
+ "install",
4970
+ "journal",
4971
+ "key",
4972
+ "key-off",
4973
+ "key-round",
4974
+ "languages",
4975
+ "laptop",
4976
+ "laptop-bridge",
4977
+ "layers",
4978
+ "layout-dashboard",
4979
+ "layout-grid",
4980
+ "layout-kanban",
4981
+ "leaf",
4982
+ "library-books",
4983
+ "life-buoy",
4984
+ "lightbulb",
4985
+ "lightbulb-sparkles",
4986
+ "line-width",
4987
+ "link",
4988
+ "link-2",
4989
+ "link-off",
4990
+ "list",
4991
+ "list-checks",
4992
+ "list-ordered",
4993
+ "list-rule",
4994
+ "list-todo",
4995
+ "llm",
3823
4996
  "loader-circle",
4997
+ "lock",
4998
+ "lock-keyhole",
4999
+ "log-in",
5000
+ "log-out",
5001
+ "log-out-all",
5002
+ "magic-wand",
5003
+ "mail",
5004
+ "mail-check",
5005
+ "mail-open",
5006
+ "mail-plus",
5007
+ "mail-warning",
5008
+ "mails",
5009
+ "market",
5010
+ "maximize-2",
5011
+ "megaphone",
5012
+ "memory-stick",
5013
+ "menu",
5014
+ "message-circle",
5015
+ "message-square",
5016
+ "message-square-plus",
5017
+ "messages-square",
5018
+ "mic",
5019
+ "microphone",
5020
+ "minimize-2",
5021
+ "minus",
5022
+ "minus-square",
5023
+ "monitor-home",
5024
+ "monitor-smartphone",
5025
+ "moon",
5026
+ "more-horizontal",
5027
+ "mouse-pointer-2",
5028
+ "move-horizontal",
5029
+ "network",
5030
+ "network-nodes",
5031
+ "newsletter",
5032
+ "newspaper",
5033
+ "octagon-x",
5034
+ "package",
5035
+ "package-download",
5036
+ "page-snapshot",
5037
+ "palette",
5038
+ "panel-left",
5039
+ "panel-right",
5040
+ "panels-top-left",
5041
+ "paper-plane",
5042
+ "paperclip",
5043
+ "pause",
5044
+ "pause-circle",
5045
+ "pencil",
5046
+ "persona",
5047
+ "piggy-bank",
5048
+ "pipette",
5049
+ "play",
5050
+ "play-circle",
5051
+ "plug",
5052
+ "plug-cloud",
5053
+ "plug-off",
5054
+ "plug-plus",
5055
+ "plugin",
5056
+ "plus",
5057
+ "price-tag",
5058
+ "publish",
5059
+ "quote",
5060
+ "radio",
5061
+ "radio-tower",
5062
+ "receipt",
5063
+ "refresh",
5064
+ "refresh-cw",
5065
+ "refresh-cw-off",
5066
+ "repeat",
5067
+ "repeat-2",
5068
+ "repeat-fallback",
5069
+ "repeat-interval",
5070
+ "responses-list",
5071
+ "rocket",
5072
+ "rotate-ccw",
5073
+ "rotate-cw",
5074
+ "route",
5075
+ "rss",
5076
+ "save",
5077
+ "scale",
5078
+ "scan",
3824
5079
  "search",
3825
- "x"
5080
+ "search-x",
5081
+ "send",
5082
+ "send-check",
5083
+ "send-request",
5084
+ "server",
5085
+ "server-api",
5086
+ "server-environment",
5087
+ "settings",
5088
+ "settings-2",
5089
+ "share",
5090
+ "share-nodes",
5091
+ "shield",
5092
+ "shield-check",
5093
+ "shield-lock",
5094
+ "shield-question",
5095
+ "shield-x",
5096
+ "shopping-basket",
5097
+ "shopping-cart",
5098
+ "shuffle",
5099
+ "signal",
5100
+ "signal-high",
5101
+ "signal-low",
5102
+ "signal-medium",
5103
+ "signal-off",
5104
+ "siren",
5105
+ "sliders-horizontal",
5106
+ "smartphone",
5107
+ "sparkles",
5108
+ "square",
5109
+ "square-check",
5110
+ "square-plus",
5111
+ "square-terminal",
5112
+ "star",
5113
+ "sticker",
5114
+ "storage",
5115
+ "store",
5116
+ "story",
5117
+ "sun",
5118
+ "support",
5119
+ "target",
5120
+ "target-choice",
5121
+ "tasks",
5122
+ "terminal",
5123
+ "ticket",
5124
+ "ticket-plus",
5125
+ "tickets",
5126
+ "timeline",
5127
+ "timer",
5128
+ "toggle-left",
5129
+ "toggle-right",
5130
+ "token-input",
5131
+ "token-output",
5132
+ "trail",
5133
+ "trash-2",
5134
+ "trend-up",
5135
+ "triangle-alert",
5136
+ "type",
5137
+ "unlink",
5138
+ "unplug",
5139
+ "upload",
5140
+ "upload-cloud",
5141
+ "user",
5142
+ "user-check",
5143
+ "user-cog",
5144
+ "user-minus",
5145
+ "user-plus",
5146
+ "user-round",
5147
+ "user-x",
5148
+ "users",
5149
+ "users-round",
5150
+ "vault",
5151
+ "volume-2",
5152
+ "wallet",
5153
+ "wand-sparkles",
5154
+ "warehouse",
5155
+ "workflow",
5156
+ "workspace",
5157
+ "wrench",
5158
+ "wrench-zap",
5159
+ "x",
5160
+ "zap"
3826
5161
  ],
3827
- description: "Icon identifier from the @treeui/icons registry."
5162
+ description: "Icon identifier from the @treeui/icons registry. Widens as `TIconRegistry` is augmented, so registered app icons are valid values too.",
5163
+ note: "Names use TreeUI's descriptive kebab-case vocabulary. The catalog covers common application needs; anything missing is one `registerTreeIcons` call away, and `listTreeIcons()` returns the live list at runtime."
3828
5164
  },
3829
5165
  {
3830
5166
  name: "size",
3831
5167
  type: "number | string",
3832
5168
  default: "20",
3833
- description: "Pixel size for both width and height of the rendered svg."
5169
+ description: "Pixel size for both width and height of the rendered svg.",
5170
+ note: 'This is a PIXEL COUNT, not the sm | md | lg token scale used by nearly every\nother TreeUI component. `<TIcon size="md" />` does not fail loudly: it writes\nwidth="md" / height="md" onto the svg, which the browser discards, so the\nicon becomes unsized and stretches to fill its container. Pass a number\n(20, 32) or a numeric string ("20"). To size an icon from tokens, leave `size`\nalone and set width/height in CSS from the icon scale \u2014 --tree-size-icon-sm |\n-md | -lg | -xl | -2xl (1rem / 1.25rem / 1.5rem / 2rem / 3rem) \u2014 which is what\nTLanguageSelect\'s switcher variant does.\n'
3834
5171
  },
3835
5172
  {
3836
5173
  name: "strokeWidth",
@@ -3857,33 +5194,60 @@ var treeui_catalog_default = {
3857
5194
  "becomes_image_when_labeled: true",
3858
5195
  "rule: Provide `label` only when the icon conveys meaning that is not already in adjacent text."
3859
5196
  ],
3860
- behavior: [],
5197
+ behavior: [
5198
+ "unknown_name_renders_nothing: An unregistered name makes `resolveTreeIcon` return `undefined`, and `TIcon`'s `v-if` then renders no element at all \u2014 no empty box, no placeholder, no error. A `console.warn` names the missing icon, suggests the `registerTreeIcons` call, and lists every valid name. It warns once per NAME (not per message \u2014 the message embeds the valid-name list, which changes on every registration), so a miss inside a render loop cannot flood the console. The warning is NOT gated on `process.env.NODE_ENV`: it fires in production builds too, deliberately, because a missing icon is a misconfiguration worth hearing about once and `process` is undefined in most browser bundles. Components with a marker fallback (TNavMenu) show that instead \u2014 see nav-menu.yaml.",
5199
+ 'absolute_stroke_width_math: Icons draw on `viewBox="0 0 24 24"` with the svg sized to `size`, so painted pixels = (stroke-width attribute) * size / 24. With `absoluteStrokeWidth` (the default) the attribute is set to `(strokeWidth * 24) / size`, which cancels the scale: at the default `strokeWidth: 2` the attribute is 2.4 at size 20, 1.5 at 32, and 1.0 at 48 \u2014 a constant 2px painted at every size. That is the entire point of the correction: a 48px icon sits beside a 20px one at the same optical weight. A `size` of 0 or a non-numeric value keeps the nominal stroke rather than producing NaN/Infinity. Setting `absoluteStrokeWidth: false` pins the attribute at `strokeWidth`, which is the hand-rolled behaviour: the painted stroke then scales WITH the icon (1.67px at 20, 2.67px at 32, 4px at 48 \u2014 double the library weight).',
5200
+ "icons_are_built_lazily_and_cached: Geometry lives in a data registry; a component is created on first lookup and reused after that, so an unused icon stays as DATA instead of becoming a `defineComponent`. Note what this does not claim: the geometry for all 364 built-ins is retained by any import of the package (the registry copies `builtinTreeIconNodes` into a `Map` at module scope, so a live reference exists and nothing tree-shakes it away \u2014 `sideEffects: false` does not change that). The saving is component construction, not bundle bytes.",
5201
+ "resolved_per_render: TIcon resolves its name in a computed, not once at setup, so an icon registered after the component mounted still appears.",
5202
+ "migration_aliases: Twelve compatibility names resolve to the same geometry as their canonical names: grid \u2192 layout-grid, share \u2192 share-nodes, chevron-updown \u2192 chevrons-up-down, refresh \u2192 refresh-cw, comment/chat \u2192 message-circle, check-square \u2192 square-check, help \u2192 circle-help, automations \u2192 zap, connections \u2192 external-link, persona \u2192 user-round, and close \u2192 x.",
5203
+ "company_logos_are_app_owned: Company logos are not built-ins. When an application needs one, it imports the official SVG as its own component and passes or registers that component under an application-owned name."
5204
+ ],
3861
5205
  selection: {
3862
5206
  keywords: [
3863
5207
  "icon",
3864
5208
  "svg",
3865
5209
  "glyph",
3866
- "pictogram"
5210
+ "pictogram",
5211
+ "register",
5212
+ "register icon",
5213
+ "custom icon",
5214
+ "icon registry",
5215
+ "registerTreeIcons",
5216
+ "createTreeIcon"
3867
5217
  ],
3868
5218
  useWhen: [
3869
- "You need to render a registered TreeUI icon by name.",
3870
- "You want consistent sizing and a11y defaults without importing the icon component directly."
5219
+ "You need to render an interface icon. `TIcon` is the way to render one \u2014 it resolves the name through the registry and applies the shared size, stroke, and a11y defaults.",
5220
+ 'The icon is one of the built-ins: `<TIcon name="cpu" />`. `listTreeIcons()` returns every registered name at runtime.',
5221
+ "The icon is your own: register it once with `registerTreeIcons`, then use it by name exactly like a built-in. A missing icon is registered, not worked around."
3871
5222
  ],
3872
5223
  avoidWhen: [
3873
- "The icon is not in the @treeui/icons registry \u2014 import or register it instead."
5224
+ "The graphic is a country flag \u2014 a flag names a country, not an interface concept.",
5225
+ "A slot already renders the icon for you (`#icon` on TAlert, TBadge, TButton, TConfirmDialog, TEmptyState, TFileUpload, TStat, TTag) \u2014 put the `TIcon` inside that slot rather than beside it. None of those components has an `icon` PROP; the slot is the whole interface."
3874
5226
  ],
3875
5227
  alternatives: [
3876
5228
  {
3877
- component: "named import from @treeui/icons",
3878
- when: "You need tree-shaken, statically-imported icon components."
5229
+ component: "a component passed straight to `TNavMenuItem.icon`, or rendered into an `#icon` slot",
5230
+ when: "You want the escape hatch with no registry entry \u2014 a one-off icon, an imported SVG component, or the result of `createTreeIcon(...)`. `TNavMenuItem.icon` is `TIconInput` and takes the component directly; everywhere else, render it inside the component's `#icon` slot. `TIcon` itself will not take a component \u2014 its `name` is `TIconName`. Prefer registering when the icon is used in more than one place."
5231
+ },
5232
+ {
5233
+ component: "TFlag",
5234
+ when: "The graphic is a country flag rendered from an ISO 3166-1 alpha-2 code."
3879
5235
  }
3880
5236
  ]
3881
5237
  },
3882
5238
  setup: {
3883
- required: [],
3884
- notes: []
5239
+ required: [
5240
+ "Use a registered name: one of the built-ins, or one the app added with `registerTreeIcons`. See `icons` above."
5241
+ ],
5242
+ notes: [
5243
+ '`size` is a pixel count, not `sm | md | lg`. Passing `size="md"` renders an unsized svg.',
5244
+ "The icon is `aria-hidden` by default. Pass `label` only when the icon carries meaning on its own; beside visible text it should stay decorative."
5245
+ ]
3885
5246
  },
3886
- recipes: []
5247
+ recipes: [
5248
+ "nav-menu-icons"
5249
+ ],
5250
+ practices: []
3887
5251
  },
3888
5252
  {
3889
5253
  name: "TInput",
@@ -4009,6 +5373,10 @@ var treeui_catalog_default = {
4009
5373
  recipes: [
4010
5374
  "form-field-input",
4011
5375
  "tag-input"
5376
+ ],
5377
+ practices: [
5378
+ "interaction-feedback",
5379
+ "predictable-api"
4012
5380
  ]
4013
5381
  },
4014
5382
  {
@@ -4046,7 +5414,245 @@ var treeui_catalog_default = {
4046
5414
  required: [],
4047
5415
  notes: []
4048
5416
  },
4049
- recipes: []
5417
+ recipes: [],
5418
+ practices: []
5419
+ },
5420
+ {
5421
+ name: "TLanguageSelect",
5422
+ aliases: [],
5423
+ package: "@treeui/vue",
5424
+ kind: "form_overlay",
5425
+ category: "Data Entry",
5426
+ status: "available",
5427
+ purpose: "Language/locale picker with a listbox, a flag per option, and the shared overlay contract; controlled and uncontrolled via v-model and open/defaultOpen.",
5428
+ sourceFile: "packages/vue/src/components/TLanguageSelect.vue",
5429
+ storyFile: "apps/docs/src/stories/LanguageSelect.stories.ts",
5430
+ storyTitle: "Components/Data Entry/LanguageSelect",
5431
+ manifestFile: "docs/ai/COMPONENTS/language-select.yaml",
5432
+ props: [
5433
+ {
5434
+ name: "modelValue",
5435
+ type: "string",
5436
+ default: ""
5437
+ },
5438
+ {
5439
+ name: "options",
5440
+ type: "TLanguageOption[]",
5441
+ default: "[]",
5442
+ note: "Array of { label: string; value: string; code?: string; description?: string; disabled?: boolean }. `value` is an opaque application key (typically a BCP-47 tag) \u2014 never parsed or validated. `code` is an ISO 3166-1 alpha-2 country code for the flag; omit it to render the option without one. `description` is a secondary line, e.g. the endonym."
5443
+ },
5444
+ {
5445
+ name: "open",
5446
+ type: "boolean",
5447
+ default: "undefined"
5448
+ },
5449
+ {
5450
+ name: "defaultOpen",
5451
+ type: "boolean",
5452
+ default: "false"
5453
+ },
5454
+ {
5455
+ name: "size",
5456
+ type: "TSize",
5457
+ default: "md",
5458
+ values: [
5459
+ "sm",
5460
+ "md",
5461
+ "lg"
5462
+ ]
5463
+ },
5464
+ {
5465
+ name: "variant",
5466
+ type: "TLanguageSelectVariant",
5467
+ default: "field",
5468
+ values: [
5469
+ "field",
5470
+ "switcher"
5471
+ ],
5472
+ note: "`field` is a form control, sized and laid out like any other input. `switcher` is a page-level control for a navbar/appbar where nothing nearby labels it: it leads with a translate icon, trails the current flag, and sizes to its content."
5473
+ },
5474
+ {
5475
+ name: "iconOnly",
5476
+ type: "boolean",
5477
+ default: "false",
5478
+ note: "`switcher` only \u2014 ignored by `field`. Drops the visible language name, leaving the translate icon, the current flag, and the chevron. The trigger then has no text, so the consumer MUST pass an `aria-label`."
5479
+ },
5480
+ {
5481
+ name: "width",
5482
+ type: "TFieldWidth",
5483
+ default: "full",
5484
+ values: [
5485
+ "xs",
5486
+ "sm",
5487
+ "md",
5488
+ "lg",
5489
+ "xl",
5490
+ "full"
5491
+ ],
5492
+ note: "Inline-size cap (max-inline-size). Fluid by default; use instead of an inline width style. Applies to `variant: field` only \u2014 a `switcher` sizes to its content and ignores it."
5493
+ },
5494
+ {
5495
+ name: "disabled",
5496
+ type: "boolean",
5497
+ default: "false"
5498
+ },
5499
+ {
5500
+ name: "loading",
5501
+ type: "boolean",
5502
+ default: "false"
5503
+ },
5504
+ {
5505
+ name: "invalid",
5506
+ type: "boolean",
5507
+ default: "false"
5508
+ },
5509
+ {
5510
+ name: "placeholder",
5511
+ type: "string",
5512
+ default: ""
5513
+ },
5514
+ {
5515
+ name: "flagBaseUrl",
5516
+ type: "string",
5517
+ default: "undefined",
5518
+ note: "Forwarded to every nested TFlag. When omitted, TFlag's own default origin applies. Set once here to self-host flag assets."
5519
+ },
5520
+ {
5521
+ name: "emptyText",
5522
+ type: "string",
5523
+ default: "No languages available."
5524
+ }
5525
+ ],
5526
+ events: [
5527
+ {
5528
+ name: "update:modelValue"
5529
+ },
5530
+ {
5531
+ name: "update:open"
5532
+ },
5533
+ {
5534
+ name: "open-change"
5535
+ }
5536
+ ],
5537
+ slots: [
5538
+ {
5539
+ name: "prefix",
5540
+ description: "Leading content inside the trigger, before everything else \u2014 the selected flag and label in `field`, the translate icon in `switcher`."
5541
+ },
5542
+ {
5543
+ name: "option",
5544
+ description: `Replaces the default option body (flag, label, description). Renders INSIDE the row's <li role="option">, so it must contain only presentational content.
5545
+ `
5546
+ },
5547
+ {
5548
+ name: "empty",
5549
+ description: "Replaces `emptyText` when `options` is empty."
5550
+ }
5551
+ ],
5552
+ a11y: [
5553
+ "role: listbox",
5554
+ "trigger_aria_haspopup: listbox",
5555
+ "trigger_aria_expanded: true",
5556
+ "trigger_aria_controls_when_open: true",
5557
+ "aria_invalid_when_invalid: true",
5558
+ "aria_busy_when_loading: true",
5559
+ "aria_selected_on_options: true",
5560
+ "aria_disabled_on_disabled_options: true",
5561
+ "focus_visible_on_trigger: true",
5562
+ "keyboard_arrow_navigation: true",
5563
+ "escape_closes_dropdown: true",
5564
+ "enter_space_selects_option: true",
5565
+ "home_end_navigation: true",
5566
+ "home_end_target_first_last_enabled_option: true",
5567
+ "focus_restores_to_trigger_on_close: true",
5568
+ "icon_only_removes_the_visible_language_name: true",
5569
+ "icon_only_requires_a_consumer_supplied_aria_label_on_the_trigger: true",
5570
+ "translate_icon_and_flags_are_decorative_not_the_accessible_name: true"
5571
+ ],
5572
+ behavior: [
5573
+ "update_payload_type: string",
5574
+ "loading_does_not_disable_by_itself: true",
5575
+ "placeholder_shown_as_muted_text: true",
5576
+ "chevron_rotates_when_open: true",
5577
+ "check_icon_on_selected_option: true",
5578
+ "outside_click_closes_dropdown: true",
5579
+ "listbox_flips_above_trigger_when_space_below_is_insufficient: true",
5580
+ "options_without_a_code_render_without_a_flag: true",
5581
+ "empty_options_render_the_empty_slot_or_empty_text: true",
5582
+ "field_variant_leads_the_trigger_with_the_selected_flag: true",
5583
+ "field_variant_trigger_fills_its_container: true",
5584
+ "switcher_variant_leads_the_trigger_with_a_translate_icon: true",
5585
+ "switcher_variant_trails_the_current_flag_before_the_chevron: true",
5586
+ "switcher_variant_trigger_sizes_to_its_content: true",
5587
+ "switcher_variant_ignores_the_width_prop: true",
5588
+ "switcher_listbox_aligns_to_the_trigger_right_edge: true",
5589
+ "switcher_listbox_may_be_wider_than_the_trigger: true",
5590
+ "icon_only_applies_to_the_switcher_variant_only: true",
5591
+ "root_classes: t-language-select--field | t-language-select--switcher, plus t-language-select--icon-only"
5592
+ ],
5593
+ selection: {
5594
+ keywords: [
5595
+ "language select",
5596
+ "language picker",
5597
+ "locale",
5598
+ "locale picker",
5599
+ "i18n",
5600
+ "internationalization",
5601
+ "translation",
5602
+ "country flag"
5603
+ ],
5604
+ useWhen: [
5605
+ "The user is picking the interface language or locale.",
5606
+ 'The control is a labelled form field for the language value \u2014 `variant="field"` (the default), where the flag leads the trigger and `width` applies.',
5607
+ 'The control is a page-level language switcher in a navbar or app bar, where nothing nearby says what it changes \u2014 `variant="switcher"` leads with a translate icon and trails the current flag, sizes to its content, and ignores `width`.',
5608
+ "A navbar switcher should shrink to icon and flag only \u2014 `iconOnly` drops the language name, so pass an `aria-label` to name the control.",
5609
+ "Each language should render with its country flag, and optionally an endonym as a second line.",
5610
+ "The app owns the language list and applies the locale side effects itself."
5611
+ ],
5612
+ avoidWhen: [
5613
+ "The values are not languages or locales.",
5614
+ "The language list is long enough that the user must type to narrow it down.",
5615
+ "You expect TreeUI to supply the locale dataset \u2014 it ships none, so pass your own `options`."
5616
+ ],
5617
+ alternatives: [
5618
+ {
5619
+ component: "TSelect",
5620
+ when: "The values are ordinary application data rather than languages or locales."
5621
+ },
5622
+ {
5623
+ component: "TCombobox",
5624
+ when: "The language list is long enough that typing to narrow it down is required."
5625
+ },
5626
+ {
5627
+ component: "TFlag",
5628
+ when: "You only need to render one country flag, without a picker around it."
5629
+ }
5630
+ ]
5631
+ },
5632
+ setup: {
5633
+ required: [
5634
+ "Pass `options` as `{ label, value, code?, description?, disabled? }[]`. TreeUI ships no language, locale, or country dataset \u2014 the app owns its language list.",
5635
+ "Apply locale side effects (i18n locale, `lang` attribute, persistence) in the app layer; the component only reports the selected `value`.",
5636
+ "Pass an `aria-label` whenever `iconOnly` is set \u2014 the visible language name is gone, so it is the control's only accessible name. It also labels the listbox."
5637
+ ],
5638
+ notes: [
5639
+ "`variant` is `field` (default) or `switcher`, typed as `TLanguageSelectVariant` and exported from `@treeui/vue`.",
5640
+ 'Use `variant="field"` inside a form, next to other inputs, or wrapped in `TFormField`: the flag leads the trigger, the trigger fills its container, and `width` applies. A visible field label already says what the control changes.',
5641
+ 'Use `variant="switcher"` for a page-level control in a navbar or app bar, where nothing nearby labels it. A translate icon (`TIcon name="languages"`) leads and the current flag trails before the chevron, so the control reads as "page language, now set to this" without an external label.',
5642
+ "A switcher sizes to its content, so `width` does not apply, and its listbox aligns to the trigger's right edge.",
5643
+ "`iconOnly` is switcher-only and drops the visible language name, leaving the translate icon and the current flag.",
5644
+ "`value` is an opaque application key, typically a BCP-47 tag such as `pt-BR`. TreeUI never parses or validates it.",
5645
+ "`code` is the ISO 3166-1 alpha-2 code used for the option flag. Omit it to render the option without a flag \u2014 a language is not a country, so many entries have no honest flag.",
5646
+ "Use `description` for a secondary line such as the endonym.",
5647
+ "Set `flagBaseUrl` once on the select rather than per option; it is forwarded to every nested `TFlag`.",
5648
+ "Bind `modelValue` to a `string`, and use `v-model:open` when the app controls open state.",
5649
+ "The CDN and Content-Security-Policy requirements under `TFlag` apply whenever any option carries a `code`."
5650
+ ]
5651
+ },
5652
+ recipes: [
5653
+ "language-switcher"
5654
+ ],
5655
+ practices: []
4050
5656
  },
4051
5657
  {
4052
5658
  name: "TLink",
@@ -4147,7 +5753,10 @@ var treeui_catalog_default = {
4147
5753
  required: [],
4148
5754
  notes: []
4149
5755
  },
4150
- recipes: []
5756
+ recipes: [],
5757
+ practices: [
5758
+ "interaction-feedback"
5759
+ ]
4151
5760
  },
4152
5761
  {
4153
5762
  name: "TMarkdownEditor",
@@ -4256,7 +5865,8 @@ var treeui_catalog_default = {
4256
5865
  required: [],
4257
5866
  notes: []
4258
5867
  },
4259
- recipes: []
5868
+ recipes: [],
5869
+ practices: []
4260
5870
  },
4261
5871
  {
4262
5872
  name: "TModal",
@@ -4404,7 +6014,11 @@ var treeui_catalog_default = {
4404
6014
  "Use `v-model:open` for the idiomatic Vue controlled pattern."
4405
6015
  ]
4406
6016
  },
4407
- recipes: []
6017
+ recipes: [],
6018
+ practices: [
6019
+ "accessible-by-default",
6020
+ "predictable-api"
6021
+ ]
4408
6022
  },
4409
6023
  {
4410
6024
  name: "TMultiSelect",
@@ -4563,7 +6177,8 @@ var treeui_catalog_default = {
4563
6177
  },
4564
6178
  recipes: [
4565
6179
  "searchable-multi-choice"
4566
- ]
6180
+ ],
6181
+ practices: []
4567
6182
  },
4568
6183
  {
4569
6184
  name: "TNavbar",
@@ -4661,6 +6276,9 @@ var treeui_catalog_default = {
4661
6276
  },
4662
6277
  recipes: [
4663
6278
  "sidebar-shell"
6279
+ ],
6280
+ practices: [
6281
+ "token-driven"
4664
6282
  ]
4665
6283
  },
4666
6284
  {
@@ -4753,10 +6371,13 @@ var treeui_catalog_default = {
4753
6371
  keywords: [
4754
6372
  "nav menu",
4755
6373
  "sidebar nav",
4756
- "section nav"
6374
+ "section nav",
6375
+ "nav icons",
6376
+ "sidebar rail"
4757
6377
  ],
4758
6378
  useWhen: [
4759
- "You need app-section navigation inside a shell or sidebar."
6379
+ "You need app-section navigation inside a shell or sidebar.",
6380
+ "Each section should carry an icon \u2014 set `icon` on the item to a registered name (`icon: 'layout-dashboard'`). Items with no icon fall back to a letter marker, which is also what a collapsed rail shows."
4760
6381
  ],
4761
6382
  avoidWhen: [
4762
6383
  "You need tabs for small panel switching."
@@ -4775,13 +6396,19 @@ var treeui_catalog_default = {
4775
6396
  setup: {
4776
6397
  required: [],
4777
6398
  notes: [
4778
- "When nested inside `TSidebar` or `TAppShell`, `collapsed` can be inherited from the sidebar provider."
6399
+ "When nested inside `TSidebar` or `TAppShell`, `collapsed` can be inherited from the sidebar provider.",
6400
+ "Pass `items` as `{ label, value, icon?, to?, badge?, description?, shortLabel?, disabled?, exact? }[]`.",
6401
+ "`icon` is `TIconInput` \u2014 a registered icon name (`icon: 'layout-dashboard'`) or a component. Use the name: it needs no import, and no `markRaw` when the item list is reactive.",
6402
+ "An item with no icon, or with a name that is not registered, shows a letter marker instead. That marker is also all a collapsed rail can show, so give every item an icon when the sidebar collapses.",
6403
+ "Register any icon the app needs beyond the built-ins first \u2014 see `icons` above."
4779
6404
  ]
4780
6405
  },
4781
6406
  recipes: [
4782
6407
  "app-shell",
4783
- "sidebar-shell"
4784
- ]
6408
+ "sidebar-shell",
6409
+ "nav-menu-icons"
6410
+ ],
6411
+ practices: []
4785
6412
  },
4786
6413
  {
4787
6414
  name: "TNumberInput",
@@ -4888,7 +6515,8 @@ var treeui_catalog_default = {
4888
6515
  required: [],
4889
6516
  notes: []
4890
6517
  },
4891
- recipes: []
6518
+ recipes: [],
6519
+ practices: []
4892
6520
  },
4893
6521
  {
4894
6522
  name: "TPage",
@@ -4981,7 +6609,10 @@ var treeui_catalog_default = {
4981
6609
  required: [],
4982
6610
  notes: []
4983
6611
  },
4984
- recipes: []
6612
+ recipes: [],
6613
+ practices: [
6614
+ "content-alignment"
6615
+ ]
4985
6616
  },
4986
6617
  {
4987
6618
  name: "TPageHeader",
@@ -5075,7 +6706,10 @@ var treeui_catalog_default = {
5075
6706
  required: [],
5076
6707
  notes: []
5077
6708
  },
5078
- recipes: []
6709
+ recipes: [],
6710
+ practices: [
6711
+ "content-alignment"
6712
+ ]
5079
6713
  },
5080
6714
  {
5081
6715
  name: "TPagination",
@@ -5188,6 +6822,9 @@ var treeui_catalog_default = {
5188
6822
  },
5189
6823
  recipes: [
5190
6824
  "data-listing"
6825
+ ],
6826
+ practices: [
6827
+ "interaction-feedback"
5191
6828
  ]
5192
6829
  },
5193
6830
  {
@@ -5247,7 +6884,7 @@ var treeui_catalog_default = {
5247
6884
  slots: [
5248
6885
  {
5249
6886
  name: "trigger",
5250
- description: "Custom trigger element. Receives current open state."
6887
+ description: "Custom trigger element. Receives the current open state and the id of the content panel, so aria-expanded and aria-controls can be wired by hand.\n"
5251
6888
  },
5252
6889
  {
5253
6890
  name: "default",
@@ -5256,9 +6893,9 @@ var treeui_catalog_default = {
5256
6893
  ],
5257
6894
  a11y: [
5258
6895
  "role: dialog",
5259
- 'attrs: ["aria-haspopup=\\"dialog\\" on trigger","aria-expanded on trigger","aria-controls linking trigger to content","tabindex=\\"-1\\" on content for focus management"]',
6896
+ 'attrs: ["aria-haspopup=\\"dialog\\" on the built-in trigger (custom triggers must supply it)","aria-expanded on the built-in trigger (custom triggers bind it from `isOpen`)","aria-controls linking trigger to content (custom triggers bind it from `contentId`)","tabindex=\\"-1\\" on content for focus management"]',
5260
6897
  'keyboard: ["Click or Enter/Space on trigger toggles popover","Escape closes popover and restores focus to trigger","Tab navigates within popover content"]',
5261
- 'focus: ["On open, focuses first focusable element inside content (or content itself)","On Escape close, restores focus to trigger"]',
6898
+ 'focus: ["On open, focuses first focusable element inside content (or content itself)","On Escape close, restores focus to trigger","Focus restore works with a custom trigger too: the ref lives on the `t-popover__anchor` wrapper, not on the built-in button, and focus returns to the first focusable element inside it.\\n"]',
5262
6899
  'dismiss: ["Outside click closes popover","Escape key closes popover"]'
5263
6900
  ],
5264
6901
  behavior: [],
@@ -5291,7 +6928,8 @@ var treeui_catalog_default = {
5291
6928
  },
5292
6929
  recipes: [
5293
6930
  "anchored-help"
5294
- ]
6931
+ ],
6932
+ practices: []
5295
6933
  },
5296
6934
  {
5297
6935
  name: "TPricing",
@@ -5355,7 +6993,8 @@ var treeui_catalog_default = {
5355
6993
  },
5356
6994
  recipes: [
5357
6995
  "pricing-section"
5358
- ]
6996
+ ],
6997
+ practices: []
5359
6998
  },
5360
6999
  {
5361
7000
  name: "TPricingCard",
@@ -5484,7 +7123,8 @@ var treeui_catalog_default = {
5484
7123
  },
5485
7124
  recipes: [
5486
7125
  "pricing-section"
5487
- ]
7126
+ ],
7127
+ practices: []
5488
7128
  },
5489
7129
  {
5490
7130
  name: "TProgress",
@@ -5546,7 +7186,10 @@ var treeui_catalog_default = {
5546
7186
  required: [],
5547
7187
  notes: []
5548
7188
  },
5549
- recipes: []
7189
+ recipes: [],
7190
+ practices: [
7191
+ "state-clarity"
7192
+ ]
5550
7193
  },
5551
7194
  {
5552
7195
  name: "TRadio",
@@ -5614,7 +7257,8 @@ var treeui_catalog_default = {
5614
7257
  required: [],
5615
7258
  notes: []
5616
7259
  },
5617
- recipes: []
7260
+ recipes: [],
7261
+ practices: []
5618
7262
  },
5619
7263
  {
5620
7264
  name: "TRadioGroup",
@@ -5689,7 +7333,8 @@ var treeui_catalog_default = {
5689
7333
  required: [],
5690
7334
  notes: []
5691
7335
  },
5692
- recipes: []
7336
+ recipes: [],
7337
+ practices: []
5693
7338
  },
5694
7339
  {
5695
7340
  name: "TSelect",
@@ -5844,6 +7489,10 @@ var treeui_catalog_default = {
5844
7489
  },
5845
7490
  recipes: [
5846
7491
  "chart-surface"
7492
+ ],
7493
+ practices: [
7494
+ "interaction-feedback",
7495
+ "predictable-api"
5847
7496
  ]
5848
7497
  },
5849
7498
  {
@@ -5954,7 +7603,10 @@ var treeui_catalog_default = {
5954
7603
  required: [],
5955
7604
  notes: []
5956
7605
  },
5957
- recipes: []
7606
+ recipes: [],
7607
+ practices: [
7608
+ "interaction-feedback"
7609
+ ]
5958
7610
  },
5959
7611
  {
5960
7612
  name: "TSidebar",
@@ -6102,7 +7754,8 @@ var treeui_catalog_default = {
6102
7754
  },
6103
7755
  recipes: [
6104
7756
  "sidebar-shell"
6105
- ]
7757
+ ],
7758
+ practices: []
6106
7759
  },
6107
7760
  {
6108
7761
  name: "TSkeleton",
@@ -6165,7 +7818,10 @@ var treeui_catalog_default = {
6165
7818
  required: [],
6166
7819
  notes: []
6167
7820
  },
6168
- recipes: []
7821
+ recipes: [],
7822
+ practices: [
7823
+ "state-clarity"
7824
+ ]
6169
7825
  },
6170
7826
  {
6171
7827
  name: "TSpacer",
@@ -6205,7 +7861,10 @@ var treeui_catalog_default = {
6205
7861
  required: [],
6206
7862
  notes: []
6207
7863
  },
6208
- recipes: []
7864
+ recipes: [],
7865
+ practices: [
7866
+ "content-alignment"
7867
+ ]
6209
7868
  },
6210
7869
  {
6211
7870
  name: "TSparkline",
@@ -6328,7 +7987,8 @@ var treeui_catalog_default = {
6328
7987
  },
6329
7988
  recipes: [
6330
7989
  "metric-dashboard"
6331
- ]
7990
+ ],
7991
+ practices: []
6332
7992
  },
6333
7993
  {
6334
7994
  name: "TSpinner",
@@ -6377,7 +8037,10 @@ var treeui_catalog_default = {
6377
8037
  required: [],
6378
8038
  notes: []
6379
8039
  },
6380
- recipes: []
8040
+ recipes: [],
8041
+ practices: [
8042
+ "state-clarity"
8043
+ ]
6381
8044
  },
6382
8045
  {
6383
8046
  name: "TSplit",
@@ -6453,7 +8116,10 @@ var treeui_catalog_default = {
6453
8116
  required: [],
6454
8117
  notes: []
6455
8118
  },
6456
- recipes: []
8119
+ recipes: [],
8120
+ practices: [
8121
+ "content-alignment"
8122
+ ]
6457
8123
  },
6458
8124
  {
6459
8125
  name: "TStack",
@@ -6557,6 +8223,9 @@ var treeui_catalog_default = {
6557
8223
  "tag-input",
6558
8224
  "data-listing",
6559
8225
  "chart-surface"
8226
+ ],
8227
+ practices: [
8228
+ "content-alignment"
6560
8229
  ]
6561
8230
  },
6562
8231
  {
@@ -6631,7 +8300,8 @@ var treeui_catalog_default = {
6631
8300
  required: [],
6632
8301
  notes: []
6633
8302
  },
6634
- recipes: []
8303
+ recipes: [],
8304
+ practices: []
6635
8305
  },
6636
8306
  {
6637
8307
  name: "TStat",
@@ -6753,7 +8423,8 @@ var treeui_catalog_default = {
6753
8423
  recipes: [
6754
8424
  "stat-grid",
6755
8425
  "metric-dashboard"
6756
- ]
8426
+ ],
8427
+ practices: []
6757
8428
  },
6758
8429
  {
6759
8430
  name: "TSteps",
@@ -6857,7 +8528,8 @@ var treeui_catalog_default = {
6857
8528
  "Pass explicit `status` per item only when the app should override the computed complete/current/upcoming logic."
6858
8529
  ]
6859
8530
  },
6860
- recipes: []
8531
+ recipes: [],
8532
+ practices: []
6861
8533
  },
6862
8534
  {
6863
8535
  name: "TSwitch",
@@ -6946,7 +8618,10 @@ var treeui_catalog_default = {
6946
8618
  required: [],
6947
8619
  notes: []
6948
8620
  },
6949
- recipes: []
8621
+ recipes: [],
8622
+ practices: [
8623
+ "interaction-feedback"
8624
+ ]
6950
8625
  },
6951
8626
  {
6952
8627
  name: "TTable",
@@ -6986,6 +8661,13 @@ var treeui_catalog_default = {
6986
8661
  type: "TTableSortState",
6987
8662
  required: false,
6988
8663
  description: "Controlled sort state. When provided, overrides internal sort tracking."
8664
+ },
8665
+ {
8666
+ name: "caption",
8667
+ type: "string",
8668
+ required: false,
8669
+ description: "Visible `<caption>` naming the table.",
8670
+ note: "For a name without visible text, pass `aria-label` / `aria-labelledby` instead \u2014 attribute inheritance is disabled and every non-class/style attr is forwarded to the `<table>`, so the name lands on the named element rather than the scroll wrapper."
6989
8671
  }
6990
8672
  ],
6991
8673
  events: [
@@ -7010,7 +8692,9 @@ var treeui_catalog_default = {
7010
8692
  }
7011
8693
  ],
7012
8694
  a11y: [
7013
- "role: grid",
8695
+ "role: table (native). No explicit role is emitted.",
8696
+ 'why_not_grid: TTable previously set role="grid" without implementing the composite-widget keyboard model \u2014 no roving tabindex, no gridcell/row roles, no 2D arrow-key navigation. Screen readers switched to application/focus mode and offered cell navigation that did not exist. The role was removed so the component is honestly a reading table. An opt-in interactive grid mode is deliberately NOT offered until the keyboard model ships.',
8697
+ "accessible_name: Use `caption` for a visible name or `aria-label` / `aria-labelledby` for an invisible one. Both land on the `<table>`.",
7014
8698
  "aria-sort: Set on sortable column headers (ascending, descending, none).",
7015
8699
  "keyboard: Sortable headers are focusable and activated with Enter or Space.",
7016
8700
  "scope: col on all th elements."
@@ -7047,7 +8731,8 @@ var treeui_catalog_default = {
7047
8731
  },
7048
8732
  recipes: [
7049
8733
  "data-listing"
7050
- ]
8734
+ ],
8735
+ practices: []
7051
8736
  },
7052
8737
  {
7053
8738
  name: "TTabs",
@@ -7153,6 +8838,9 @@ var treeui_catalog_default = {
7153
8838
  },
7154
8839
  recipes: [
7155
8840
  "router-backed-tabs"
8841
+ ],
8842
+ practices: [
8843
+ "interaction-feedback"
7156
8844
  ]
7157
8845
  },
7158
8846
  {
@@ -7233,6 +8921,10 @@ var treeui_catalog_default = {
7233
8921
  },
7234
8922
  recipes: [
7235
8923
  "tag-input"
8924
+ ],
8925
+ practices: [
8926
+ "predictable-api",
8927
+ "token-driven"
7236
8928
  ]
7237
8929
  },
7238
8930
  {
@@ -7297,6 +8989,12 @@ var treeui_catalog_default = {
7297
8989
  type: "boolean",
7298
8990
  default: "false",
7299
8991
  note: "Clip to a single line with an ellipsis."
8992
+ },
8993
+ {
8994
+ name: "preserveWhitespace",
8995
+ type: "boolean",
8996
+ default: "false",
8997
+ note: "Render authored line breaks and blank lines as written (`white-space: pre-wrap`) while still wrapping long lines. Use for plain-text output such as AI responses, so consumers never need a local `white-space: pre-wrap` class. Mutually exclusive with `truncate`, which needs `nowrap`; when both are set `truncate` wins and no `is-pre-wrap` class is emitted."
7300
8998
  }
7301
8999
  ],
7302
9000
  events: [],
@@ -7310,7 +9008,8 @@ var treeui_catalog_default = {
7310
9008
  ],
7311
9009
  behavior: [
7312
9010
  "semantic_element_controlled_by_as: true",
7313
- "token_driven_size_tone_weight: true"
9011
+ "token_driven_size_tone_weight: true",
9012
+ "truncate_takes_precedence_over_preserve_whitespace: true"
7314
9013
  ],
7315
9014
  selection: {
7316
9015
  keywords: [],
@@ -7322,7 +9021,8 @@ var treeui_catalog_default = {
7322
9021
  required: [],
7323
9022
  notes: []
7324
9023
  },
7325
- recipes: []
9024
+ recipes: [],
9025
+ practices: []
7326
9026
  },
7327
9027
  {
7328
9028
  name: "TTextarea",
@@ -7439,7 +9139,8 @@ var treeui_catalog_default = {
7439
9139
  required: [],
7440
9140
  notes: []
7441
9141
  },
7442
- recipes: []
9142
+ recipes: [],
9143
+ practices: []
7443
9144
  },
7444
9145
  {
7445
9146
  name: "TTimeline",
@@ -7499,7 +9200,8 @@ var treeui_catalog_default = {
7499
9200
  required: [],
7500
9201
  notes: []
7501
9202
  },
7502
- recipes: []
9203
+ recipes: [],
9204
+ practices: []
7503
9205
  },
7504
9206
  {
7505
9207
  name: "TToast",
@@ -7562,7 +9264,8 @@ var treeui_catalog_default = {
7562
9264
  },
7563
9265
  recipes: [
7564
9266
  "confirm-destructive-action"
7565
- ]
9267
+ ],
9268
+ practices: []
7566
9269
  },
7567
9270
  {
7568
9271
  name: "TToggleGroup",
@@ -7679,7 +9382,8 @@ var treeui_catalog_default = {
7679
9382
  'Use `selectionMode="multiple"` for small visible multi-choice sets.'
7680
9383
  ]
7681
9384
  },
7682
- recipes: []
9385
+ recipes: [],
9386
+ practices: []
7683
9387
  },
7684
9388
  {
7685
9389
  name: "TTooltip",
@@ -7791,6 +9495,9 @@ var treeui_catalog_default = {
7791
9495
  },
7792
9496
  recipes: [
7793
9497
  "anchored-help"
9498
+ ],
9499
+ practices: [
9500
+ "accessible-by-default"
7794
9501
  ]
7795
9502
  },
7796
9503
  {
@@ -7803,7 +9510,7 @@ var treeui_catalog_default = {
7803
9510
  purpose: "Hierarchical tree view with expansion, keyboard navigation, and single or multiple selection.",
7804
9511
  sourceFile: "packages/vue/src/components/TTreeView.vue",
7805
9512
  storyFile: "apps/docs/src/stories/TreeView.stories.ts",
7806
- storyTitle: "Components/Navigation & Disclosure/TView",
9513
+ storyTitle: "Components/Navigation & Disclosure/TreeView",
7807
9514
  manifestFile: "docs/ai/COMPONENTS/tree-view.yaml",
7808
9515
  props: [
7809
9516
  {
@@ -7913,6 +9620,9 @@ var treeui_catalog_default = {
7913
9620
  },
7914
9621
  recipes: [
7915
9622
  "hierarchy-picker"
9623
+ ],
9624
+ practices: [
9625
+ "accessible-by-default"
7916
9626
  ]
7917
9627
  }
7918
9628
  ],
@@ -9069,7 +10779,8 @@ var formatComponentSummary = (component) => {
9069
10779
  "Setup",
9070
10780
  [...component.setup.required, ...component.setup.notes]
9071
10781
  ),
9072
- component.recipes.length > 0 ? `Recipes: ${component.recipes.join(", ")}` : void 0
10782
+ component.recipes.length > 0 ? `Recipes: ${component.recipes.join(", ")}` : void 0,
10783
+ component.practices.length > 0 ? `Practices: ${component.practices.join(", ")}` : void 0
9073
10784
  ].filter(Boolean);
9074
10785
  return lines.join("\n");
9075
10786
  };
@@ -9144,6 +10855,12 @@ var resourceList = [
9144
10855
  description: "Reusable multi-component composition recipes.",
9145
10856
  mimeType: "application/json"
9146
10857
  },
10858
+ {
10859
+ uri: "treeui://practices",
10860
+ name: "TreeUI practices",
10861
+ description: "Named UX practices and the components that follow them.",
10862
+ mimeType: "application/json"
10863
+ },
9147
10864
  {
9148
10865
  uri: "treeui://tokens",
9149
10866
  name: "TreeUI design tokens",
@@ -9177,6 +10894,9 @@ var readResourcePayload = (uri) => {
9177
10894
  if (uri === "treeui://recipes") {
9178
10895
  return catalog.recipes;
9179
10896
  }
10897
+ if (uri === "treeui://practices") {
10898
+ return catalog.practices;
10899
+ }
9180
10900
  if (uri === "treeui://tokens") {
9181
10901
  return catalog.tokens;
9182
10902
  }