@treeui/mcp 0.5.0 → 0.7.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-21T20:31:07.075Z",
4
4
  packageName: "@treeui/vue",
5
- packageVersion: "0.16.0",
5
+ packageVersion: "0.19.1",
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,52 @@ 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: "brand-lockup",
378
+ name: "Product brand lockup in a shell header",
379
+ useWhen: [
380
+ "A shell header needs a logo plus a product name (and optional subtitle) that stays aligned as the sidebar collapses."
381
+ ],
382
+ compose: [
383
+ "TAppShell",
384
+ "TAvatar",
385
+ "TText",
386
+ "TStack"
387
+ ],
388
+ notes: [
389
+ "Put the lockup in the `header-start` slot, not `header`. Filling `header-start` switches the header to the shell's own grid, so the mark tracks `--tree-app-shell-sidebar-width` and animates with the collapse \u2014 no local column math.",
390
+ 'The slot is scoped: use its `collapsed` binding to drop the wordmark and keep only the mark (`v-if="!collapsed"`), rather than hiding text with CSS.',
391
+ 'Logo is `TAvatar` (`size`, `alt`, `initials` fallback) or a plain `<img>` sized with `--tree-size-icon-*`; name and subtitle are `TText` (`weight`, `tone="muted"`, `truncate`) \u2014 never raw `<strong>`/`<span>` with a local class.',
392
+ "TreeUI ships no `TBrand` component on purpose: the logo asset, the product name, and what survives collapse are product identity, which must stay in the consumer. See DECISIONS.md \u2192 Portability Boundary.",
393
+ "Company and service logos (AWS, LocalStack, social networks) are brand assets and are not TreeUI icons \u2014 source them from the app's brand policy, not the icon registry."
394
+ ]
395
+ },
396
+ {
397
+ id: "nav-menu-icons",
398
+ name: "Icons on navigation items",
399
+ useWhen: [
400
+ "Navigation items each carry an icon.",
401
+ "The navigation collapses to an icon-only rail, so the icon is the only thing left identifying a section.",
402
+ "Some of the icons the app needs are not TreeUI built-ins."
403
+ ],
404
+ compose: [
405
+ "TNavMenu",
406
+ "TIcon",
407
+ "registerTreeIcons",
408
+ "resetTreeIcons"
409
+ ],
410
+ notes: [
411
+ "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.",
412
+ "`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.",
413
+ "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.",
414
+ "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.",
415
+ "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.",
416
+ "Keep the icon vocabulary consistent: built-ins use TreeUI's descriptive kebab-case names, so register custom ones with the same convention.",
417
+ "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.",
418
+ "Set `to` for the route target on each item \u2014 `TNavMenuItem` has no `href` and no `id`; identity is `value`.",
419
+ "This recipe covers the icons only; the surrounding layout is its own recipe (`app-shell` or `sidebar-shell`)."
420
+ ]
421
+ },
368
422
  {
369
423
  id: "stat-grid",
370
424
  name: "Dashboard metric grid",
@@ -538,6 +592,150 @@ var treeui_catalog_default = {
538
592
  notes: [
539
593
  'Use `selectionMode="multiple"` when permissions or tag assignment spans nested nodes.'
540
594
  ]
595
+ },
596
+ {
597
+ id: "language-switcher",
598
+ name: "Application language switcher",
599
+ useWhen: [
600
+ "The app lets the user pick a language or locale, with an optional flag per option."
601
+ ],
602
+ compose: [
603
+ "TFormField",
604
+ "TLanguageSelect"
605
+ ],
606
+ notes: [
607
+ "The app owns the language list: TreeUI ships no locale dataset, so pass `options` with your own `label`, `value`, and optional `code` and `description`.",
608
+ "`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).",
609
+ "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.",
610
+ "Set `flagBaseUrl` once, at the switcher, when flag assets are self-hosted; it is forwarded to every nested `TFlag`.",
611
+ "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."
612
+ ]
613
+ }
614
+ ],
615
+ practices: [
616
+ {
617
+ id: "interaction-feedback",
618
+ title: "Visual interaction feedback",
619
+ summary: "Every interaction answers back. Hover, focus, active, loading, and disabled are designed states \u2014 never afterthoughts left to the consumer.",
620
+ rules: [
621
+ "Interactive surfaces ship hover, active, and focus-visible treatments out of the box.",
622
+ "Loading states announce themselves (aria-busy plus a labelled, localizable spinner), replace the resting glyph instead of stacking beside it, and block duplicate activation.",
623
+ "Disabled and loading elements never emit activation events.",
624
+ "Focus indicators stay visible in both light and dark themes."
625
+ ],
626
+ components: [
627
+ "TButton",
628
+ "TLink",
629
+ "TInput",
630
+ "TSelect",
631
+ "TCheckbox",
632
+ "TSwitch",
633
+ "TTabs",
634
+ "TPagination",
635
+ "TDropdown",
636
+ "TSelectableList"
637
+ ]
638
+ },
639
+ {
640
+ id: "accessible-by-default",
641
+ title: "Accessible by default",
642
+ summary: "Native semantics first, ARIA where the pattern requires it, full keyboard support, and focus management the consumer never has to write.",
643
+ rules: [
644
+ 'Native elements before roles; role="dialog", aria-modal, aria-labelledby only where semantics require them, and no composite-widget role (grid, tree) is claimed without its full keyboard model.',
645
+ "Tab/Shift+Tab order, Escape to close, Enter/Space to activate, arrow keys where the pattern expects them.",
646
+ "Overlays trap focus while open and restore it on close.",
647
+ "Clickable targets stay at or above 44\xD744px."
648
+ ],
649
+ components: [
650
+ "TModal",
651
+ "TDrawer",
652
+ "TConfirmDialog",
653
+ "TDropdown",
654
+ "TTooltip",
655
+ "TAccordion",
656
+ "TFormField",
657
+ "TTreeView",
658
+ "TToastProvider",
659
+ "TButton"
660
+ ]
661
+ },
662
+ {
663
+ id: "predictable-api",
664
+ title: "One predictable API",
665
+ 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.",
666
+ rules: [
667
+ "Sizes are sm | md | lg across the library.",
668
+ "Action variants come from one scale: solid, outline, ghost, soft, danger.",
669
+ "Form controls speak modelValue + update:modelValue (v-model).",
670
+ "Overlays speak open/defaultOpen + update:open/open-change."
671
+ ],
672
+ components: [
673
+ "TButton",
674
+ "TInput",
675
+ "TSelect",
676
+ "TBadge",
677
+ "TTag",
678
+ "TCard",
679
+ "TModal"
680
+ ]
681
+ },
682
+ {
683
+ id: "state-clarity",
684
+ title: "Empty, loading, and error are designed states",
685
+ summary: "Screens communicate status honestly: skeletons while loading, purposeful empty states, and status surfaces on one semantic tone scale.",
686
+ rules: [
687
+ "Default, hover, active, focus, disabled, loading, and error/empty states are designed and documented \u2014 not just the happy path.",
688
+ "Status tones map to semantic tokens (success, warning, error, info) and pass both themes.",
689
+ "Async feedback is immediate: skeleton or spinner first, toast or alert on completion."
690
+ ],
691
+ components: [
692
+ "TSkeleton",
693
+ "TSpinner",
694
+ "TProgress",
695
+ "TEmptyState",
696
+ "TAlert",
697
+ "TToastProvider",
698
+ "TBadge",
699
+ "TTable"
700
+ ]
701
+ },
702
+ {
703
+ id: "content-alignment",
704
+ title: "Alignment without custom CSS",
705
+ summary: "Layout primitives absorb spacing, alignment, and rhythm so product code never hand-rolls the same flexbox again.",
706
+ rules: [
707
+ "Spacing comes from the --tree-space-* scale, applied by layout components.",
708
+ "Compose TPage, TStack, TGrid, and TSplit before writing a custom layout div.",
709
+ "Forms align through TFormField + TStack + TGrid \u2014 labels, hints, and errors line up by contract.",
710
+ "Reading widths and page gutters come from TContainer/TPage, not local max-widths."
711
+ ],
712
+ components: [
713
+ "TStack",
714
+ "TGrid",
715
+ "TSplit",
716
+ "TContainer",
717
+ "TSpacer",
718
+ "TPage",
719
+ "TPageHeader",
720
+ "TFormField"
721
+ ]
722
+ },
723
+ {
724
+ id: "token-driven",
725
+ title: "Token-driven clarity",
726
+ 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.",
727
+ rules: [
728
+ "Zero hardcoded colors, spacing, or typography in component code.",
729
+ "Light and dark are first-class themes; every state passes both.",
730
+ "Themes switch by attribute (data-tree-theme) with no component changes."
731
+ ],
732
+ components: [
733
+ "TCard",
734
+ "TNavbar",
735
+ "TBadge",
736
+ "TTag",
737
+ "TChart"
738
+ ]
541
739
  }
542
740
  ],
543
741
  setup: {
@@ -612,6 +810,43 @@ var treeui_catalog_default = {
612
810
  "Use `maxVisibleTags` to control trigger density in compact layouts."
613
811
  ]
614
812
  },
813
+ TFlag: {
814
+ required: [
815
+ "Pass `code` as an ISO 3166-1 alpha-2 country code. Matching is case-insensitive \u2014 the value is trimmed and lowercased internally.",
816
+ "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."
817
+ ],
818
+ notes: [
819
+ "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`.",
820
+ "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.",
821
+ "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.",
822
+ "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.",
823
+ "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.",
824
+ "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.",
825
+ "`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.",
826
+ "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`.",
827
+ "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."
828
+ ]
829
+ },
830
+ TLanguageSelect: {
831
+ required: [
832
+ "Pass `options` as `{ label, value, code?, description?, disabled? }[]`. TreeUI ships no language, locale, or country dataset \u2014 the app owns its language list.",
833
+ "Apply locale side effects (i18n locale, `lang` attribute, persistence) in the app layer; the component only reports the selected `value`.",
834
+ "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."
835
+ ],
836
+ notes: [
837
+ "`variant` is `field` (default) or `switcher`, typed as `TLanguageSelectVariant` and exported from `@treeui/vue`.",
838
+ '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.',
839
+ '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.',
840
+ "A switcher sizes to its content, so `width` does not apply, and its listbox aligns to the trigger's right edge.",
841
+ "`iconOnly` is switcher-only and drops the visible language name, leaving the translate icon and the current flag.",
842
+ "`value` is an opaque application key, typically a BCP-47 tag such as `pt-BR`. TreeUI never parses or validates it.",
843
+ "`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.",
844
+ "Use `description` for a secondary line such as the endonym.",
845
+ "Set `flagBaseUrl` once on the select rather than per option; it is forwarded to every nested `TFlag`.",
846
+ "Bind `modelValue` to a `string`, and use `v-model:open` when the app controls open state.",
847
+ "The CDN and Content-Security-Policy requirements under `TFlag` apply whenever any option carries a `code`."
848
+ ]
849
+ },
615
850
  TToggleGroup: {
616
851
  required: [],
617
852
  notes: [
@@ -661,7 +896,20 @@ var treeui_catalog_default = {
661
896
  TNavMenu: {
662
897
  required: [],
663
898
  notes: [
664
- "When nested inside `TSidebar` or `TAppShell`, `collapsed` can be inherited from the sidebar provider."
899
+ "When nested inside `TSidebar` or `TAppShell`, `collapsed` can be inherited from the sidebar provider.",
900
+ "Pass `items` as `{ label, value, icon?, to?, badge?, description?, shortLabel?, disabled?, exact? }[]`.",
901
+ "`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.",
902
+ "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.",
903
+ "Register any icon the app needs beyond the built-ins first \u2014 see `icons` above."
904
+ ]
905
+ },
906
+ TIcon: {
907
+ required: [
908
+ "Use a registered name: one of the built-ins, or one the app added with `registerTreeIcons`. See `icons` above."
909
+ ],
910
+ notes: [
911
+ '`size` is a pixel count, not `sm | md | lg`. Passing `size="md"` renders an unsized svg.',
912
+ "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
913
  ]
666
914
  },
667
915
  TTreeView: {
@@ -733,6 +981,7 @@ var treeui_catalog_default = {
733
981
  ],
734
982
  commonCompositions: [
735
983
  "TFormField + input-like control",
984
+ "TFormField + TLanguageSelect",
736
985
  "TGrid + TStat",
737
986
  "TStat + TSparkline",
738
987
  "TCard + TChart",
@@ -756,15 +1005,83 @@ var treeui_catalog_default = {
756
1005
  storyFile: "apps/docs/src/stories/Accordion.stories.ts",
757
1006
  storyTitle: "Components/Data Display/Accordion",
758
1007
  manifestFile: "docs/ai/COMPONENTS/accordion.yaml",
759
- props: [],
760
- events: [],
761
- slots: [],
1008
+ props: [
1009
+ {
1010
+ name: "type",
1011
+ type: "'single' | 'multiple'",
1012
+ default: "'single'",
1013
+ values: [
1014
+ "single",
1015
+ "multiple"
1016
+ ],
1017
+ 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"
1018
+ },
1019
+ {
1020
+ name: "modelValue",
1021
+ type: "string | string[] | undefined",
1022
+ default: "undefined",
1023
+ 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'
1024
+ },
1025
+ {
1026
+ name: "defaultValue",
1027
+ type: "string | string[] | undefined",
1028
+ default: "undefined",
1029
+ 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"
1030
+ },
1031
+ {
1032
+ name: "disabled",
1033
+ type: "boolean",
1034
+ default: "false",
1035
+ 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"
1036
+ },
1037
+ {
1038
+ name: "collapsible",
1039
+ type: "boolean",
1040
+ default: "false",
1041
+ 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'
1042
+ }
1043
+ ],
1044
+ events: [
1045
+ {
1046
+ name: "update:modelValue",
1047
+ 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',
1048
+ payload: "string | string[] | undefined"
1049
+ }
1050
+ ],
1051
+ slots: [
1052
+ {
1053
+ name: "default",
1054
+ description: "Accepts TAccordionItem children."
1055
+ }
1056
+ ],
762
1057
  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"}]'
1058
+ "container_is_a_plain_div_with_no_aria_role: true",
1059
+ "trigger_is_a_heading_wrapped_native_button: h3 > button",
1060
+ "aria_expanded_on_trigger_reflects_open_state: true",
1061
+ "aria_controls_on_trigger_references_panel_id: true",
1062
+ "panel_uses_role_region: true",
1063
+ "aria_labelledby_on_panel_references_trigger_id: true",
1064
+ "disabled_items_use_the_native_disabled_attribute: true",
1065
+ "chevron_icon_is_aria_hidden: true",
1066
+ "trigger_hit_area_is_at_least_44px: via the .t-accordion__trigger::after overlay",
1067
+ "enter_space_toggle_the_focused_item: native button activation",
1068
+ "arrow_down_focuses_next_enabled_trigger_wrapping_to_first: true",
1069
+ "arrow_up_focuses_previous_enabled_trigger_wrapping_to_last: true",
1070
+ "home_focuses_first_enabled_trigger: true",
1071
+ "end_focuses_last_enabled_trigger: true"
1072
+ ],
1073
+ behavior: [
1074
+ "controlled_only_while_model_value_is_not_undefined: true",
1075
+ "default_value_seeds_uncontrolled_state_once: true",
1076
+ "single_mode_opening_an_item_closes_the_previous_one: true",
1077
+ "single_mode_collapsing_requires_collapsible: true",
1078
+ "multiple_mode_ignores_collapsible: true",
1079
+ "container_and_item_disabled_are_combined_with_or: true",
1080
+ "arrow_navigation_skips_disabled_triggers: true",
1081
+ "arrow_navigation_wraps_around: true",
1082
+ "trigger_order_follows_item_mount_order: true",
1083
+ "only_open_panels_are_rendered: true"
766
1084
  ],
767
- behavior: [],
768
1085
  selection: {
769
1086
  keywords: [
770
1087
  "accordion",
@@ -795,7 +1112,10 @@ var treeui_catalog_default = {
795
1112
  required: [],
796
1113
  notes: []
797
1114
  },
798
- recipes: []
1115
+ recipes: [],
1116
+ practices: [
1117
+ "accessible-by-default"
1118
+ ]
799
1119
  },
800
1120
  {
801
1121
  name: "TAlert",
@@ -812,14 +1132,15 @@ var treeui_catalog_default = {
812
1132
  props: [
813
1133
  {
814
1134
  name: "variant",
815
- type: "'info' | 'success' | 'warning' | 'danger'",
1135
+ type: "TAlertVariant",
816
1136
  default: "info",
817
1137
  values: [
818
1138
  "info",
819
1139
  "success",
820
1140
  "warning",
821
1141
  "danger"
822
- ]
1142
+ ],
1143
+ description: "Semantic tone. Sets the --tree-alert-* custom properties that colour the surface, border, icon, and dismiss button."
823
1144
  },
824
1145
  {
825
1146
  name: "size",
@@ -829,42 +1150,55 @@ var treeui_catalog_default = {
829
1150
  "sm",
830
1151
  "md",
831
1152
  "lg"
832
- ]
1153
+ ],
1154
+ description: "Adjusts padding and font size only. md is the baseline defined on .t-alert itself."
833
1155
  },
834
1156
  {
835
1157
  name: "dismissible",
836
1158
  type: "boolean",
837
- default: "false"
1159
+ default: "false",
1160
+ description: "Renders the trailing dismiss button. The button is the only thing gated by this prop; the alert never hides itself."
838
1161
  }
839
1162
  ],
840
1163
  events: [
841
1164
  {
842
1165
  name: "dismiss",
843
- description: "Emitted when the dismiss button is clicked.",
1166
+ 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
1167
  payload: "none"
845
1168
  }
846
1169
  ],
847
1170
  slots: [
848
1171
  {
849
1172
  name: "default",
850
- description: "Alert message content."
1173
+ description: "Alert message content, rendered inside .t-alert__content."
851
1174
  },
852
1175
  {
853
1176
  name: "icon",
854
- description: "Optional leading icon; rendered with aria-hidden."
1177
+ 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
1178
  },
856
1179
  {
857
1180
  name: "dismiss-icon",
858
- description: "Custom dismiss button content; replaces the default \xD7 icon."
1181
+ 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
1182
  }
860
1183
  ],
861
1184
  a11y: [
862
1185
  "role: alert",
1186
+ "role_is_on_root_element: true",
863
1187
  "icon_slot_marked_aria_hidden: true",
1188
+ "default_dismiss_icon_marked_aria_hidden: true",
864
1189
  "dismiss_button_aria_label: Dismiss",
865
- "keyboard: Tab to dismiss button, Enter/Space to activate"
1190
+ "dismiss_button_label_is_hardcoded: true",
1191
+ "dismiss_button_hit_area: 44x44 via a .t-alert__dismiss::after pseudo-element; the visible button is 1.5rem.",
1192
+ "keyboard: Tab to dismiss button, Enter/Space to activate",
1193
+ '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.'
1194
+ ],
1195
+ behavior: [
1196
+ "self_removes_on_dismiss: false",
1197
+ "icon_wrapper_rendered_only_when_icon_slot_provided: true",
1198
+ "dismiss_button_rendered_only_when_dismissible: true",
1199
+ "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.",
1200
+ 'variant_sets_local_custom_properties: ["--tree-alert-bg","--tree-alert-border","--tree-alert-text","--tree-alert-accent"]'
866
1201
  ],
867
- behavior: [],
868
1202
  selection: {
869
1203
  keywords: [
870
1204
  "alert",
@@ -895,7 +1229,10 @@ var treeui_catalog_default = {
895
1229
  required: [],
896
1230
  notes: []
897
1231
  },
898
- recipes: []
1232
+ recipes: [],
1233
+ practices: [
1234
+ "state-clarity"
1235
+ ]
899
1236
  },
900
1237
  {
901
1238
  name: "TAppShell",
@@ -913,7 +1250,8 @@ var treeui_catalog_default = {
913
1250
  {
914
1251
  name: "as",
915
1252
  type: "string",
916
- default: "div"
1253
+ default: "div",
1254
+ 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
1255
  },
918
1256
  {
919
1257
  name: "side",
@@ -927,164 +1265,254 @@ var treeui_catalog_default = {
927
1265
  {
928
1266
  name: "sidebarWidth",
929
1267
  type: "string",
930
- default: "17.5rem"
1268
+ default: "17.5rem",
1269
+ 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
1270
  },
932
1271
  {
933
1272
  name: "collapsedWidth",
934
1273
  type: "string",
935
- default: "4.75rem"
1274
+ default: "4.75rem",
1275
+ 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
1276
  },
937
1277
  {
938
1278
  name: "collapsible",
939
1279
  type: "boolean",
940
- default: "false"
1280
+ default: "false",
1281
+ 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
1282
  },
942
1283
  {
943
1284
  name: "collapsed",
944
1285
  type: "boolean",
945
- default: "undefined"
1286
+ default: "undefined",
1287
+ 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
1288
  },
947
1289
  {
948
1290
  name: "defaultCollapsed",
949
1291
  type: "boolean",
950
- default: "false"
1292
+ default: "false",
1293
+ note: "Initial value for uncontrolled use. Read once during setup; later changes to the prop are ignored."
951
1294
  },
952
1295
  {
953
1296
  name: "mobile",
954
1297
  type: "boolean",
955
- default: "undefined"
1298
+ default: "undefined",
1299
+ note: "Forces mobile mode when explicitly true or false. Leave undefined to auto-detect from `breakpoint`."
956
1300
  },
957
1301
  {
958
1302
  name: "breakpoint",
959
1303
  type: "string",
960
- default: "768px"
1304
+ default: "768px",
1305
+ 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
1306
  },
962
1307
  {
963
1308
  name: "sidebarOpen",
964
1309
  type: "boolean",
965
- default: "undefined"
1310
+ default: "undefined",
1311
+ 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
1312
  },
967
1313
  {
968
1314
  name: "defaultSidebarOpen",
969
1315
  type: "boolean",
970
- default: "false"
1316
+ default: "false",
1317
+ note: "Initial value for uncontrolled use. Read once during setup."
1318
+ },
1319
+ {
1320
+ name: "immersive",
1321
+ type: "boolean",
1322
+ default: "undefined",
1323
+ 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."
1324
+ },
1325
+ {
1326
+ name: "defaultImmersive",
1327
+ type: "boolean",
1328
+ default: "false",
1329
+ note: "Initial value for uncontrolled use. Read once during setup."
971
1330
  },
972
1331
  {
973
1332
  name: "closeOnEscape",
974
1333
  type: "boolean",
975
- default: "true"
1334
+ default: "true",
1335
+ note: "Drawer only. Escape is handled on the drawer surface, so it fires only while focus is inside the drawer."
976
1336
  },
977
1337
  {
978
1338
  name: "closeOnOverlay",
979
1339
  type: "boolean",
980
- default: "true"
1340
+ default: "true",
1341
+ note: "Drawer only. Set false to keep the drawer open on backdrop clicks."
981
1342
  },
982
1343
  {
983
1344
  name: "showMenuButton",
984
1345
  type: "boolean",
985
- default: "true"
1346
+ default: "true",
1347
+ 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
1348
  },
987
1349
  {
988
1350
  name: "showCollapseButton",
989
1351
  type: "boolean",
990
1352
  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."
1353
+ 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
1354
  },
993
1355
  {
994
1356
  name: "collapseLabel",
995
1357
  type: "string",
996
- default: "Collapse sidebar"
1358
+ default: "Collapse sidebar",
1359
+ note: "aria-label of the collapse toggle while the rail is expanded."
997
1360
  },
998
1361
  {
999
1362
  name: "expandLabel",
1000
1363
  type: "string",
1001
- default: "Expand sidebar"
1364
+ default: "Expand sidebar",
1365
+ note: "aria-label of the collapse toggle while the rail is collapsed."
1002
1366
  },
1003
1367
  {
1004
1368
  name: "menuLabel",
1005
1369
  type: "string",
1006
- default: "Open menu"
1370
+ default: "Open menu",
1371
+ note: "aria-label of the built-in hamburger. Static \u2014 it does not swap to a close label while the drawer is open."
1007
1372
  },
1008
1373
  {
1009
1374
  name: "closeLabel",
1010
1375
  type: "string",
1011
- default: "Close menu"
1376
+ default: "Close menu",
1377
+ note: "aria-label of the drawer's close button."
1012
1378
  },
1013
1379
  {
1014
1380
  name: "sidebarLabel",
1015
1381
  type: "string",
1016
- default: "Sidebar"
1382
+ default: "Sidebar",
1383
+ note: "aria-label applied to both the desktop <aside> and the drawer <aside>."
1017
1384
  }
1018
1385
  ],
1019
1386
  events: [
1020
1387
  {
1021
- name: "update:collapsed"
1388
+ name: "update:collapsed",
1389
+ description: "Desktop rail collapsed state changed. Enables v-model:collapsed.",
1390
+ payload: "[value: boolean]"
1022
1391
  },
1023
1392
  {
1024
- name: "collapse-change"
1393
+ name: "collapse-change",
1394
+ description: "Same change as update:collapsed, for non-v-model usage. Both always fire together.",
1395
+ payload: "[value: boolean]"
1396
+ },
1397
+ {
1398
+ name: "update:sidebarOpen",
1399
+ description: "Off-canvas drawer open state changed. Enables v-model:sidebarOpen.",
1400
+ payload: "[value: boolean]"
1025
1401
  },
1026
1402
  {
1027
- name: "update:sidebarOpen"
1403
+ name: "sidebar-open-change",
1404
+ description: "Same change as update:sidebarOpen, for non-v-model usage. Both always fire together.",
1405
+ payload: "[value: boolean]"
1406
+ },
1407
+ {
1408
+ name: "update:immersive",
1409
+ description: "Immersive state changed. Enables v-model:immersive.",
1410
+ payload: "[value: boolean]"
1028
1411
  },
1029
1412
  {
1030
- name: "sidebar-open-change"
1413
+ name: "immersive-change",
1414
+ description: "Same change as update:immersive, for non-v-model usage. Both always fire together.",
1415
+ payload: "[value: boolean]"
1031
1416
  }
1032
1417
  ],
1033
1418
  slots: [
1034
1419
  {
1035
- name: "header"
1420
+ name: "header",
1421
+ 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",
1422
+ note: "AppShellSlotProps \u2014 { mobile, collapsed, sidebarOpen, immersive, toggleSidebar, closeSidebar, toggleCollapsed, toggleImmersive }"
1036
1423
  },
1037
1424
  {
1038
- name: "header-start"
1425
+ name: "header-start",
1426
+ 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",
1427
+ note: "AppShellSlotProps"
1039
1428
  },
1040
1429
  {
1041
- name: "sidebar-header"
1430
+ name: "sidebar-header",
1431
+ 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",
1432
+ note: "AppShellSlotProps"
1042
1433
  },
1043
1434
  {
1044
- name: "sidebar"
1435
+ name: "sidebar",
1436
+ 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",
1437
+ note: "AppShellSlotProps"
1045
1438
  },
1046
1439
  {
1047
- name: "sidebar-footer"
1440
+ name: "sidebar-footer",
1441
+ 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",
1442
+ note: "AppShellSlotProps"
1048
1443
  },
1049
1444
  {
1050
- name: "default"
1445
+ name: "default",
1446
+ description: "Main content area. Scrolls independently of the header and sidebar.",
1447
+ 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
1448
  },
1052
1449
  {
1053
- name: "menu-icon"
1450
+ name: "menu-icon",
1451
+ 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",
1452
+ note: "{ sidebarOpen: boolean }"
1054
1453
  },
1055
1454
  {
1056
- name: "collapse-icon"
1455
+ name: "collapse-icon",
1456
+ description: "Replaces the default glyph inside the built-in collapse toggle. Renders inside the button, so it must be presentational only.\n",
1457
+ note: "{ collapsed: boolean }"
1057
1458
  }
1058
1459
  ],
1059
1460
  a11y: [
1060
- "role: dialog",
1461
+ "semantic_regions: <header>, <aside> and <main>; the shell root itself is a plain div and is not a landmark",
1462
+ "role: dialog \u2014 on the mobile drawer only",
1061
1463
  "aria_modal: true",
1062
- "focus_trap: true",
1063
- "restores_focus_on_close: true",
1464
+ "focus_trap: Tab and Shift+Tab cycle inside the drawer; with no focusable descendant, focus is pinned to the drawer itself",
1465
+ "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
1466
  "escape_closes_when_enabled: true",
1065
1467
  "menu_button_aria_expanded: true",
1066
- "menu_button_aria_controls_drawer: true",
1067
- "sidebar_labelled_by_sidebar_label: true"
1468
+ "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",
1469
+ "collapse_button_aria_expanded: reflects the expanded rail, i.e. !collapsed",
1470
+ "collapse_button_aria_controls_sidebar: true",
1471
+ "collapse_button_label_swaps_by_state: collapseLabel while expanded, expandLabel while collapsed",
1472
+ "drawer_close_button_labelled_by_close_label: true",
1473
+ "sidebar_labelled_by_sidebar_label: true",
1474
+ "drawer_focusable_via_tabindex_minus_one: true",
1475
+ "backdrop_is_aria_hidden: true",
1476
+ "built_in_icons_are_aria_hidden: true",
1477
+ "menu_and_drawer_close_buttons_are_44px: true"
1068
1478
  ],
1069
1479
  behavior: [
1070
1480
  "fills_viewport_full_width_and_height: true",
1481
+ "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
1482
  "header_spans_full_width_above_sidebar_and_content: true",
1072
1483
  "content_area_scrolls_independently: true",
1073
1484
  "auto_switches_to_mobile_at_breakpoint_when_mobile_undefined: true",
1074
1485
  "mobile_prop_forces_mobile_mode: true",
1075
1486
  "mobile_sidebar_becomes_off_canvas_drawer: true",
1487
+ "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",
1488
+ "drawer_is_rendered_only_while_mobile_and_open: true",
1076
1489
  "auto_menu_button_in_header_on_mobile: true",
1077
1490
  "controlled_and_uncontrolled_collapsed_state: true",
1078
1491
  "controlled_and_uncontrolled_sidebar_open_state: true",
1492
+ "controlled_and_uncontrolled_immersive_state: true",
1493
+ "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",
1494
+ "immersive_suppresses_the_mobile_drawer: while immersive the overlay is not rendered, since the trigger that opens it lives in the hidden header",
1495
+ "controlled_mode_does_not_self_update: when collapsed, sidebarOpen or immersive is bound, the component only emits \u2014 the parent must apply the value",
1496
+ "no_emit_when_the_value_is_unchanged: true",
1497
+ "toggle_collapsed_is_a_noop_when_collapsible_is_false: true",
1498
+ "collapse_toggle_requires_desktop_and_collapsible_and_show_collapse_button: true",
1079
1499
  "provides_collapsed_state_to_descendants: true",
1080
1500
  "drawer_reports_collapsed_false_to_descendants_and_slots: true",
1081
1501
  "left_and_right_variants: true",
1082
1502
  "locks_body_scroll_while_drawer_open: true",
1503
+ "releases_the_scroll_lock_when_leaving_mobile_while_open: true",
1504
+ "rearms_the_overlay_when_reentering_mobile_while_open: true",
1083
1505
  "overlay_click_closes_when_enabled: true",
1084
1506
  "exposes_slot_props_with_mobile_collapsed_and_toggles: true",
1085
1507
  "sidebar_footer_pinned_to_bottom: true",
1086
1508
  "sidebar_header_and_footer_inset_to_align_with_nav_icons: true",
1087
- "header_mirrors_shell_columns_when_header_start_is_used: true"
1509
+ "header_mirrors_shell_columns_when_header_start_is_used: true",
1510
+ "header_start_falls_back_to_a_single_flex_row_on_mobile: true",
1511
+ "animates_the_rail_width_via_grid_template_columns: true",
1512
+ "main_panel_has_a_rounded_inner_corner_on_the_sidebar_side_on_desktop: true",
1513
+ "drawer_width_is_capped_at_min_of_100_percent_and_sidebar_width: true",
1514
+ 'root_data_attribute: data-mobile="true" | "false" on the shell root',
1515
+ "respects_prefers_reduced_motion: true"
1088
1516
  ],
1089
1517
  selection: {
1090
1518
  keywords: [
@@ -1130,15 +1558,17 @@ var treeui_catalog_default = {
1130
1558
  ]
1131
1559
  },
1132
1560
  recipes: [
1133
- "app-shell"
1134
- ]
1561
+ "app-shell",
1562
+ "brand-lockup"
1563
+ ],
1564
+ practices: []
1135
1565
  },
1136
1566
  {
1137
1567
  name: "TAvatar",
1138
1568
  aliases: [],
1139
1569
  package: "@treeui/vue",
1140
1570
  category: "Data Display",
1141
- description: "User image with fallback initials and optional status indicator dot.",
1571
+ description: "User avatar that renders an image when `src` is set, otherwise initials derived from `alt`, plus an optional status indicator dot.",
1142
1572
  sourceFile: "packages/vue/src/components/TAvatar.vue",
1143
1573
  storyFile: "apps/docs/src/stories/Avatar.stories.ts",
1144
1574
  storyTitle: "Components/Data Display/Avatar",
@@ -1148,19 +1578,19 @@ var treeui_catalog_default = {
1148
1578
  name: "src",
1149
1579
  type: "string",
1150
1580
  required: false,
1151
- description: "URL of the avatar image."
1581
+ 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
1582
  },
1153
1583
  {
1154
1584
  name: "alt",
1155
1585
  type: "string",
1156
1586
  default: '""',
1157
- description: "Accessible label for the avatar. Also used to derive initials if `initials` is not set."
1587
+ 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
1588
  },
1159
1589
  {
1160
1590
  name: "initials",
1161
1591
  type: "string",
1162
1592
  required: false,
1163
- description: "Explicit initials to display when no image is provided. Overrides derived initials from `alt`."
1593
+ 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
1594
  },
1165
1595
  {
1166
1596
  name: "size",
@@ -1170,7 +1600,8 @@ var treeui_catalog_default = {
1170
1600
  "sm",
1171
1601
  "md",
1172
1602
  "lg"
1173
- ]
1603
+ ],
1604
+ description: "Fixed square dimensions \u2014 sm 2rem, md 2.5rem, lg 3.5rem \u2014 with matching font size for initials."
1174
1605
  },
1175
1606
  {
1176
1607
  name: "status",
@@ -1182,20 +1613,23 @@ var treeui_catalog_default = {
1182
1613
  "busy",
1183
1614
  "away"
1184
1615
  ],
1185
- description: "Shows a colored status dot on the avatar."
1616
+ 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
1617
  }
1187
1618
  ],
1188
1619
  events: [],
1189
1620
  slots: [
1190
1621
  {
1191
1622
  name: "default",
1192
- description: "Optional overlay content inside the avatar."
1623
+ 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
1624
  }
1194
1625
  ],
1195
1626
  a11y: [
1196
- "role: img",
1197
- "aria-label: Derived from `alt` prop.",
1198
- "status_dot: Has its own `aria-label` with the status value."
1627
+ "role: img (always set on the root span, including when only initials render)",
1628
+ 'aria-label: Set from `alt`. Omitted entirely when `alt` is empty (the default), which leaves the `role="img"` element without an accessible name.',
1629
+ 'initials: Marked `aria-hidden="true"` \u2014 the name comes from `alt`, not the visible text.',
1630
+ "image: Receives the `alt` prop as its native `alt` attribute.",
1631
+ "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.",
1632
+ 'slot_content: Not announced \u2014 the `role="img"` root makes descendant content presentational.'
1199
1633
  ],
1200
1634
  behavior: [],
1201
1635
  selection: {
@@ -1208,7 +1642,10 @@ var treeui_catalog_default = {
1208
1642
  required: [],
1209
1643
  notes: []
1210
1644
  },
1211
- recipes: []
1645
+ recipes: [
1646
+ "brand-lockup"
1647
+ ],
1648
+ practices: []
1212
1649
  },
1213
1650
  {
1214
1651
  name: "TBadge",
@@ -1233,7 +1670,8 @@ var treeui_catalog_default = {
1233
1670
  "ghost",
1234
1671
  "soft",
1235
1672
  "danger"
1236
- ]
1673
+ ],
1674
+ 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
1675
  },
1238
1676
  {
1239
1677
  name: "tone",
@@ -1246,7 +1684,7 @@ var treeui_catalog_default = {
1246
1684
  "danger",
1247
1685
  "info"
1248
1686
  ],
1249
- description: "Optional semantic tone layered on top of the visual variant."
1687
+ 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
1688
  },
1251
1689
  {
1252
1690
  name: "size",
@@ -1256,7 +1694,8 @@ var treeui_catalog_default = {
1256
1694
  "sm",
1257
1695
  "md",
1258
1696
  "lg"
1259
- ]
1697
+ ],
1698
+ description: "Changes min-height and font-size only; inline padding is constant across sizes."
1260
1699
  }
1261
1700
  ],
1262
1701
  events: [],
@@ -1269,10 +1708,19 @@ var treeui_catalog_default = {
1269
1708
  }
1270
1709
  ],
1271
1710
  a11y: [
1711
+ "semantic_element: span",
1272
1712
  "interactive_by_default: false",
1273
- "icon_slot_marked_aria_hidden: true"
1713
+ "icon_slot_marked_aria_hidden: true",
1714
+ "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."
1715
+ ],
1716
+ behavior: [
1717
+ "always_applies_one_modifier_per_prop: true",
1718
+ "tone_remaps_variant_color_variables: true",
1719
+ "tone_overrides_variant_danger_palette: true",
1720
+ "tone_neutral_has_no_stylesheet_rule: true",
1721
+ "icon_wrapper_rendered_only_when_slot_filled: true",
1722
+ "icon_renders_before_default_slot: true"
1274
1723
  ],
1275
- behavior: [],
1276
1724
  selection: {
1277
1725
  keywords: [],
1278
1726
  useWhen: [],
@@ -1285,6 +1733,11 @@ var treeui_catalog_default = {
1285
1733
  },
1286
1734
  recipes: [
1287
1735
  "pricing-section"
1736
+ ],
1737
+ practices: [
1738
+ "predictable-api",
1739
+ "state-clarity",
1740
+ "token-driven"
1288
1741
  ]
1289
1742
  },
1290
1743
  {
@@ -1304,23 +1757,32 @@ var treeui_catalog_default = {
1304
1757
  name: "separator",
1305
1758
  type: "string",
1306
1759
  default: "/",
1307
- description: "Character or string used to separate breadcrumb items."
1760
+ 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
1761
  }
1309
1762
  ],
1310
1763
  events: [],
1311
1764
  slots: [
1312
1765
  {
1313
- name: "default"
1766
+ name: "default",
1767
+ 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
1768
  }
1315
1769
  ],
1316
1770
  a11y: [
1317
1771
  "element: nav",
1318
1772
  "aria_label: Breadcrumb",
1773
+ "aria_label_hardcoded_but_overridable_via_fallthrough_attrs: true",
1319
1774
  "current_page_aria_current: page",
1320
1775
  "uses_ol_for_ordered_list: true",
1321
1776
  "interactive_by_default: false"
1322
1777
  ],
1323
- behavior: [],
1778
+ behavior: [
1779
+ "link_target_precedence: to + resolvable RouterLink, then href, then string to",
1780
+ "string_to_without_router_renders_native_anchor: true",
1781
+ "object_to_without_router_renders_current_span: true",
1782
+ "current_state_is_not_positional: true",
1783
+ "separator_rendered_by_css_pseudo_element: true",
1784
+ "separator_ignores_pointer_events: true"
1785
+ ],
1324
1786
  selection: {
1325
1787
  keywords: [],
1326
1788
  useWhen: [],
@@ -1334,7 +1796,8 @@ var treeui_catalog_default = {
1334
1796
  "Use `to` when the app has vue-router and breadcrumb items should render RouterLink."
1335
1797
  ]
1336
1798
  },
1337
- recipes: []
1799
+ recipes: [],
1800
+ practices: []
1338
1801
  },
1339
1802
  {
1340
1803
  name: "TButton",
@@ -1352,7 +1815,8 @@ var treeui_catalog_default = {
1352
1815
  {
1353
1816
  name: "as",
1354
1817
  type: "string",
1355
- default: "button"
1818
+ default: "button",
1819
+ 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
1820
  },
1357
1821
  {
1358
1822
  name: "variant",
@@ -1376,23 +1840,50 @@ var treeui_catalog_default = {
1376
1840
  "sm",
1377
1841
  "md",
1378
1842
  "lg"
1379
- ]
1843
+ ],
1844
+ 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
1845
  },
1381
1846
  {
1382
1847
  name: "disabled",
1383
1848
  type: "boolean",
1384
- default: "false"
1849
+ default: "false",
1850
+ 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
1851
  },
1386
1852
  {
1387
1853
  name: "loading",
1388
1854
  type: "boolean",
1389
1855
  default: "false",
1390
- note: "loading implies disabled behavior"
1856
+ 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`."
1857
+ },
1858
+ {
1859
+ name: "loadingLabel",
1860
+ type: "string",
1861
+ default: "Loading",
1862
+ 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)."
1863
+ },
1864
+ {
1865
+ name: "hideIconWhileLoading",
1866
+ type: "boolean",
1867
+ default: "true",
1868
+ 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."
1869
+ },
1870
+ {
1871
+ name: "iconOnly",
1872
+ type: "boolean",
1873
+ default: "false",
1874
+ 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."
1875
+ },
1876
+ {
1877
+ name: "label",
1878
+ type: "string",
1879
+ default: "undefined",
1880
+ note: "Accessible name, rendered as `aria-label`. Required whenever `iconOnly` is set; ignored for ordinary buttons that already have visible text."
1391
1881
  },
1392
1882
  {
1393
1883
  name: "type",
1394
1884
  type: "button | submit | reset",
1395
- default: "button"
1885
+ default: "button",
1886
+ 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
1887
  },
1397
1888
  {
1398
1889
  name: "block",
@@ -1404,12 +1895,14 @@ var treeui_catalog_default = {
1404
1895
  name: "align",
1405
1896
  type: "start | center | end",
1406
1897
  default: "center",
1407
- note: "Content alignment; only meaningful together with `block`. Use `start` for left-aligned icon+label menu rows."
1898
+ 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
1899
  }
1409
1900
  ],
1410
1901
  events: [
1411
1902
  {
1412
- name: "click"
1903
+ name: "click",
1904
+ description: "Emitted on activation. Suppressed while disabled or loading: the internal handler calls preventDefault() and stopPropagation() and returns without emitting.",
1905
+ payload: "[event: MouseEvent]"
1413
1906
  }
1414
1907
  ],
1415
1908
  slots: [
@@ -1422,13 +1915,22 @@ var treeui_catalog_default = {
1422
1915
  ],
1423
1916
  a11y: [
1424
1917
  "native_button_by_default: true",
1918
+ "native_disabled_attr_only_when_button: true",
1425
1919
  "aria_busy_when_loading: true",
1426
1920
  "aria_disabled_for_non_button: true",
1427
- "tabindex_minus_one_when_non_button_disabled: true"
1921
+ "tabindex_minus_one_when_non_button_disabled: true",
1922
+ "icon_slot_marked_aria_hidden: true",
1923
+ "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.",
1924
+ "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.",
1925
+ "touch_targets: 44\xD744px minimum via the .t-button::after overlay",
1926
+ "focus_visible: box-shadow focus ring on the root element"
1428
1927
  ],
1429
1928
  behavior: [
1430
1929
  "blocks_click_when_disabled_or_loading: true",
1431
- "renders_spinner_when_loading: true"
1930
+ "renders_spinner_when_loading: true",
1931
+ "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.",
1932
+ "icon_slot_wrapper_rendered_only_when_filled: true",
1933
+ "default_slot_wrapped_in_label_element_unless_icon_only: true"
1432
1934
  ],
1433
1935
  selection: {
1434
1936
  keywords: [],
@@ -1443,6 +1945,11 @@ var treeui_catalog_default = {
1443
1945
  recipes: [
1444
1946
  "confirm-destructive-action",
1445
1947
  "pricing-section"
1948
+ ],
1949
+ practices: [
1950
+ "interaction-feedback",
1951
+ "accessible-by-default",
1952
+ "predictable-api"
1446
1953
  ]
1447
1954
  },
1448
1955
  {
@@ -1452,7 +1959,7 @@ var treeui_catalog_default = {
1452
1959
  kind: "layout",
1453
1960
  category: "Data Display",
1454
1961
  status: "available",
1455
- purpose: "Surface container with optional header and footer slots.",
1962
+ purpose: "Surface container with optional header, actions, and footer slots.",
1456
1963
  sourceFile: "packages/vue/src/components/TCard.vue",
1457
1964
  storyFile: "apps/docs/src/stories/Card.stories.ts",
1458
1965
  storyTitle: "Components/Data Display/Card",
@@ -1461,7 +1968,8 @@ var treeui_catalog_default = {
1461
1968
  {
1462
1969
  name: "as",
1463
1970
  type: "string",
1464
- default: "section"
1971
+ default: "section",
1972
+ 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
1973
  },
1466
1974
  {
1467
1975
  name: "variant",
@@ -1477,37 +1985,44 @@ var treeui_catalog_default = {
1477
1985
  name: "size",
1478
1986
  type: "TSize",
1479
1987
  default: "md",
1480
- values: [
1481
- "sm",
1482
- "md",
1483
- "lg"
1484
- ]
1988
+ 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
1989
  },
1486
1990
  {
1487
1991
  name: "title",
1488
1992
  type: "string",
1489
1993
  default: "undefined",
1490
- description: "Optional card heading rendered inside the header area."
1994
+ 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
1995
  }
1492
1996
  ],
1493
1997
  events: [],
1494
1998
  slots: [
1495
1999
  {
1496
- name: "header"
2000
+ name: "header",
2001
+ 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
2002
  },
1498
2003
  {
1499
- name: "actions"
2004
+ name: "actions",
2005
+ 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
2006
  },
1501
2007
  {
1502
- name: "default"
2008
+ name: "default",
2009
+ description: "Card body. Always rendered, wrapped in `div.t-card__body`. Receives no slot props.\n"
1503
2010
  },
1504
2011
  {
1505
- name: "footer"
2012
+ name: "footer",
2013
+ description: "Renders `footer.t-card__footer` only when the slot is supplied; there is no `footer` prop fallback. Receives no slot props.\n"
1506
2014
  }
1507
2015
  ],
1508
- a11y: [],
2016
+ a11y: [
2017
+ "root_role: None applied by the component \u2014 semantics come entirely from `as` (default `section`).",
2018
+ "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.",
2019
+ "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."
2020
+ ],
1509
2021
  behavior: [
1510
2022
  "body_wrapper_class: t-card__body",
2023
+ "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.",
2024
+ "footer_render_condition: The `footer.t-card__footer` element renders only when the `footer` slot is supplied.",
2025
+ "slot_props: None. No slot on this component is scoped; all four are plain content slots.",
1511
2026
  '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
2027
  "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
2028
  ],
@@ -1542,6 +2057,10 @@ var treeui_catalog_default = {
1542
2057
  },
1543
2058
  recipes: [
1544
2059
  "chart-surface"
2060
+ ],
2061
+ practices: [
2062
+ "predictable-api",
2063
+ "token-driven"
1545
2064
  ]
1546
2065
  },
1547
2066
  {
@@ -1571,13 +2090,13 @@ var treeui_catalog_default = {
1571
2090
  name: "series",
1572
2091
  type: "TChartSeries[]",
1573
2092
  default: "[]",
1574
- note: "Each item is { name: string; data: number[]; color?: string }."
2093
+ 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
2094
  },
1576
2095
  {
1577
2096
  name: "labels",
1578
2097
  type: "string[]",
1579
2098
  default: "[]",
1580
- note: "X-axis categories, aligned index-for-index to each series' data."
2099
+ 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
2100
  },
1582
2101
  {
1583
2102
  name: "height",
@@ -1589,80 +2108,91 @@ var treeui_catalog_default = {
1589
2108
  name: "smooth",
1590
2109
  type: "boolean",
1591
2110
  default: "false",
1592
- note: "Curved line/area."
2111
+ note: "Curved line/area; ignored for bar."
1593
2112
  },
1594
2113
  {
1595
2114
  name: "stacked",
1596
2115
  type: "boolean",
1597
2116
  default: "false",
1598
- note: "Stacks bar series; ignored for line/area. Negative values are clamped to 0 in stacked mode (grouped mode renders them)."
2117
+ 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
2118
  },
1600
2119
  {
1601
2120
  name: "showGrid",
1602
2121
  type: "boolean",
1603
- default: "true"
2122
+ default: "true",
2123
+ note: "Horizontal gridlines at the y ticks. Independent of showYAxis, which controls the tick text."
1604
2124
  },
1605
2125
  {
1606
2126
  name: "showLegend",
1607
- type: "boolean",
1608
- default: "auto (true when more than one series)"
2127
+ type: "boolean | undefined",
2128
+ default: "auto (true when more than one series)",
2129
+ note: "The legend is suppressed whenever there is no data, even when set to true."
1609
2130
  },
1610
2131
  {
1611
2132
  name: "showXAxis",
1612
2133
  type: "boolean",
1613
- default: "true"
2134
+ default: "true",
2135
+ note: "When false the category labels are skipped and the bottom margin collapses from 28px to 10px."
1614
2136
  },
1615
2137
  {
1616
2138
  name: "showYAxis",
1617
2139
  type: "boolean",
1618
- default: "true"
2140
+ default: "true",
2141
+ note: "When false the tick text is skipped and the left margin collapses from 44px to 10px. Gridlines are unaffected."
1619
2142
  },
1620
2143
  {
1621
2144
  name: "showTooltip",
1622
2145
  type: "boolean",
1623
- default: "true"
2146
+ default: "true",
2147
+ note: "Gates hover tracking as a whole: the crosshair, the tooltip panel, and the point-click event all stop when false."
1624
2148
  },
1625
2149
  {
1626
2150
  name: "showPoints",
1627
2151
  type: "boolean",
1628
2152
  default: "false",
1629
- note: "Always draw markers on line/area points."
2153
+ note: "Always draw markers on line/area points; ignored for bar."
1630
2154
  },
1631
2155
  {
1632
2156
  name: "yTicks",
1633
2157
  type: "number",
1634
- default: "5"
2158
+ default: "5",
2159
+ note: "Target tick count handed to niceScale \u2014 the rendered count may differ once the bounds are rounded."
1635
2160
  },
1636
2161
  {
1637
2162
  name: "valueFormat",
1638
2163
  type: "(value: number) => string",
1639
- default: "locale integers for |value| >= 1000"
2164
+ default: "locale integers for |value| >= 1000",
2165
+ note: "Applies to y-axis ticks, tooltip values, and the a11y table cells."
1640
2166
  },
1641
2167
  {
1642
2168
  name: "minY",
1643
2169
  type: "number",
1644
- default: "auto"
2170
+ default: "auto (min(0, data min) for bar/area, data min for line)",
2171
+ note: "A hint, not a hard bound: niceScale rounds the low end down, so the rendered domain can start below it."
1645
2172
  },
1646
2173
  {
1647
2174
  name: "maxY",
1648
2175
  type: "number",
1649
- default: "auto"
2176
+ default: "auto",
2177
+ note: "A hint, not a hard bound: niceScale rounds the high end up, so the rendered domain can extend above it."
1650
2178
  },
1651
2179
  {
1652
2180
  name: "animated",
1653
2181
  type: "boolean",
1654
2182
  default: "true",
1655
- note: "Draw-in animation; disabled under prefers-reduced-motion."
2183
+ note: "Draw-in animation via the is-animated root class; disabled under prefers-reduced-motion."
1656
2184
  },
1657
2185
  {
1658
2186
  name: "loading",
1659
2187
  type: "boolean",
1660
- default: "false"
2188
+ default: "false",
2189
+ 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
2190
  },
1662
2191
  {
1663
2192
  name: "ariaLabel",
1664
2193
  type: "string",
1665
- default: "auto-generated summary"
2194
+ default: "auto-generated summary ('<type> chart with <n> series')",
2195
+ 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
2196
  }
1667
2197
  ],
1668
2198
  events: [
@@ -1684,14 +2214,19 @@ var treeui_catalog_default = {
1684
2214
  ],
1685
2215
  a11y: [
1686
2216
  "SVG is aria-hidden; a visually-hidden <table> mirrors the series data for assistive tech.",
2217
+ "The table's <caption> carries ariaLabel (or the auto-generated summary); the root div itself has no role or accessible name.",
1687
2218
  "Legend defaults on for multiple series so identity is not color-only.",
1688
- "Provide a meaningful ariaLabel describing what the chart shows."
2219
+ "Provide a meaningful ariaLabel describing what the chart shows.",
2220
+ "point-click has no keyboard equivalent \u2014 do not make it the only way to reach a behavior."
1689
2221
  ],
1690
2222
  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"
2223
+ "colors_auto_assign_from_chart_palette: --tree-color-chart-1..8, cycling by series index; per-series color overrides",
2224
+ "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.",
2225
+ "y_domain_uses_nice_ticks: every type runs through niceScale, which rounds the min down and the max up to round numbers",
2226
+ "hover_shows_crosshair_and_tooltip: only while showTooltip is true",
2227
+ "width_measured_with_resize_observer: falls back to 640px until the canvas is mounted and measured",
2228
+ "x_labels_decimate_when_crowded: keeps every max(2, floor(innerWidth / 52))-th label and hides the rest; the last category is always kept",
2229
+ "empty_state_when_no_series_has_data: true"
1695
2230
  ],
1696
2231
  selection: {
1697
2232
  keywords: [
@@ -1738,6 +2273,9 @@ var treeui_catalog_default = {
1738
2273
  recipes: [
1739
2274
  "chart-surface",
1740
2275
  "metric-dashboard"
2276
+ ],
2277
+ practices: [
2278
+ "token-driven"
1741
2279
  ]
1742
2280
  },
1743
2281
  {
@@ -1756,7 +2294,8 @@ var treeui_catalog_default = {
1756
2294
  {
1757
2295
  name: "modelValue",
1758
2296
  type: "boolean",
1759
- default: "false"
2297
+ default: "false",
2298
+ 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
2299
  },
1761
2300
  {
1762
2301
  name: "size",
@@ -1766,44 +2305,60 @@ var treeui_catalog_default = {
1766
2305
  "sm",
1767
2306
  "md",
1768
2307
  "lg"
1769
- ]
2308
+ ],
2309
+ description: "Control and label scale. Always rendered as a t-checkbox--<size> modifier."
1770
2310
  },
1771
2311
  {
1772
2312
  name: "disabled",
1773
2313
  type: "boolean",
1774
- default: "false"
2314
+ default: "false",
2315
+ 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
2316
  },
1776
2317
  {
1777
2318
  name: "indeterminate",
1778
2319
  type: "boolean",
1779
- default: "false"
2320
+ default: "false",
2321
+ 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.
2322
+ `
1780
2323
  },
1781
2324
  {
1782
2325
  name: "invalid",
1783
2326
  type: "boolean",
1784
- default: "false"
2327
+ default: "false",
2328
+ 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
2329
  }
1786
2330
  ],
1787
2331
  events: [
1788
2332
  {
1789
- name: "update:modelValue"
2333
+ name: "update:modelValue",
2334
+ 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",
2335
+ payload: "[value: boolean]"
1790
2336
  }
1791
2337
  ],
1792
2338
  slots: [
1793
2339
  {
1794
- name: "default"
2340
+ name: "default",
2341
+ 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
2342
  }
1796
2343
  ],
1797
2344
  a11y: [
1798
2345
  "role: native_checkbox",
2346
+ "accessible_name: From the default slot; absent when the slot is omitted \u2014 supply aria-label or aria-labelledby",
2347
+ "aria_checked_explicitly_bound: 'mixed' when indeterminate, otherwise the modelValue boolean",
1799
2348
  "aria_checked_mixed_when_indeterminate: true",
1800
2349
  "aria_invalid_when_invalid: true",
2350
+ "native_indeterminate_property_synced: true",
2351
+ "disabled_uses_native_attribute: true",
1801
2352
  "focus_visible_on_control: true",
2353
+ "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
2354
  "clickable_target_44x44: true"
1803
2355
  ],
1804
2356
  behavior: [
1805
2357
  "update_payload_type: boolean",
2358
+ "controlled_only: No internal state; the checkbox does not change appearance until the parent updates modelValue",
2359
+ "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
2360
  "indeterminate_clears_on_user_toggle: false",
2361
+ "indeterminate_is_parent_owned: The component never clears indeterminate \u2014 set it to false yourself when handling update:modelValue",
1807
2362
  "label_click_toggles_checkbox: true"
1808
2363
  ],
1809
2364
  selection: {
@@ -1836,7 +2391,10 @@ var treeui_catalog_default = {
1836
2391
  required: [],
1837
2392
  notes: []
1838
2393
  },
1839
- recipes: []
2394
+ recipes: [],
2395
+ practices: [
2396
+ "interaction-feedback"
2397
+ ]
1840
2398
  },
1841
2399
  {
1842
2400
  name: "TColorSwatch",
@@ -1855,19 +2413,19 @@ var treeui_catalog_default = {
1855
2413
  name: "modelValue",
1856
2414
  type: "string",
1857
2415
  default: "undefined",
1858
- note: "Selected colour as a hex string. Matching is case-insensitive."
2416
+ 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
2417
  },
1860
2418
  {
1861
2419
  name: "options",
1862
2420
  type: "TColorSwatchOption[]",
1863
2421
  default: "[]",
1864
- note: "Preset swatches, each `{ label, value }`. `label` becomes the button's accessible name."
2422
+ 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
2423
  },
1866
2424
  {
1867
2425
  name: "allowCustom",
1868
2426
  type: "boolean",
1869
2427
  default: "false",
1870
- note: 'Appends a native `<input type="color">` for values outside the presets.'
2428
+ 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
2429
  },
1872
2430
  {
1873
2431
  name: "size",
@@ -1877,23 +2435,26 @@ var treeui_catalog_default = {
1877
2435
  "sm",
1878
2436
  "md",
1879
2437
  "lg"
1880
- ]
2438
+ ],
2439
+ 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
2440
  },
1882
2441
  {
1883
2442
  name: "label",
1884
2443
  type: "string",
1885
2444
  default: "Color",
1886
- note: 'Accessible label for the swatch group (`role="group"`).'
2445
+ 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
2446
  },
1888
2447
  {
1889
2448
  name: "customLabel",
1890
2449
  type: "string",
1891
- default: "Custom color"
2450
+ default: "Custom color",
2451
+ note: "Accessible label (`aria-label`) for the native colour input, and its only accessible name. Applies only when `allowCustom` is set."
1892
2452
  },
1893
2453
  {
1894
2454
  name: "disabled",
1895
2455
  type: "boolean",
1896
- default: "false"
2456
+ default: "false",
2457
+ 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
2458
  }
1898
2459
  ],
1899
2460
  events: [
@@ -1903,14 +2464,21 @@ var treeui_catalog_default = {
1903
2464
  ],
1904
2465
  slots: [],
1905
2466
  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."
2467
+ 'Wrapper is `role="group"` labelled by `label` via `aria-label`.',
2468
+ '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.',
2469
+ "Selection is conveyed by an outline (not colour alone), since the swatch's own colour is the content.",
2470
+ 'The custom input is a native `<input type="color">` named only by `customLabel`.',
2471
+ "No roving focus and no arrow-key navigation \u2014 every swatch and the custom input is its own Tab stop."
1909
2472
  ],
1910
2473
  behavior: [
2474
+ "update_payload_type: string",
1911
2475
  "emits_on_preset_click: true",
1912
2476
  "emits_on_custom_input: true",
1913
- "blocks_interaction_when_disabled: true"
2477
+ "blocks_interaction_when_disabled: true",
2478
+ "selection_match_is_case_insensitive: true",
2479
+ "custom_input_rendered_only_when_allow_custom: true",
2480
+ "custom_input_emits_on_input_not_change: true",
2481
+ "fully_controlled_no_internal_selection_state: true"
1914
2482
  ],
1915
2483
  selection: {
1916
2484
  keywords: [],
@@ -1922,7 +2490,8 @@ var treeui_catalog_default = {
1922
2490
  required: [],
1923
2491
  notes: []
1924
2492
  },
1925
- recipes: []
2493
+ recipes: [],
2494
+ practices: []
1926
2495
  },
1927
2496
  {
1928
2497
  name: "TCombobox",
@@ -1940,7 +2509,8 @@ var treeui_catalog_default = {
1940
2509
  {
1941
2510
  name: "modelValue",
1942
2511
  type: "string",
1943
- default: ""
2512
+ default: "",
2513
+ 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
2514
  },
1945
2515
  {
1946
2516
  name: "options",
@@ -1951,12 +2521,14 @@ var treeui_catalog_default = {
1951
2521
  {
1952
2522
  name: "open",
1953
2523
  type: "boolean",
1954
- default: "undefined"
2524
+ default: "undefined",
2525
+ 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
2526
  },
1956
2527
  {
1957
2528
  name: "defaultOpen",
1958
2529
  type: "boolean",
1959
- default: "false"
2530
+ default: "false",
2531
+ note: "Uncontrolled initial open state, read once on mount. Later changes to it are ignored."
1960
2532
  },
1961
2533
  {
1962
2534
  name: "size",
@@ -1966,17 +2538,20 @@ var treeui_catalog_default = {
1966
2538
  "sm",
1967
2539
  "md",
1968
2540
  "lg"
1969
- ]
2541
+ ],
2542
+ note: "Sizes the control only. `md` has no CSS rule of its own \u2014 it is the base `.t-combobox__control` styling."
1970
2543
  },
1971
2544
  {
1972
2545
  name: "disabled",
1973
2546
  type: "boolean",
1974
- default: "false"
2547
+ default: "false",
2548
+ 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
2549
  },
1976
2550
  {
1977
2551
  name: "loading",
1978
2552
  type: "boolean",
1979
- default: "false"
2553
+ default: "false",
2554
+ 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
2555
  },
1981
2556
  {
1982
2557
  name: "invalid",
@@ -1991,56 +2566,102 @@ var treeui_catalog_default = {
1991
2566
  {
1992
2567
  name: "emptyText",
1993
2568
  type: "string",
1994
- default: "No results found."
2569
+ default: "No results found.",
2570
+ note: "Fallback content of the `empty` slot \u2014 supplying that slot shadows this prop entirely."
1995
2571
  }
1996
2572
  ],
1997
2573
  events: [
1998
2574
  {
1999
- name: "update:modelValue"
2575
+ name: "update:modelValue",
2576
+ 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).',
2577
+ payload: '["string"]'
2000
2578
  },
2001
2579
  {
2002
- name: "update:open"
2580
+ name: "update:open",
2581
+ description: "Emitted whenever the open state changes. Deduped \u2014 no emit when the next value equals the current one.",
2582
+ payload: '["boolean"]'
2003
2583
  },
2004
2584
  {
2005
- name: "open-change"
2585
+ name: "open-change",
2586
+ description: "Emitted immediately after `update:open`, with the same payload. Alias for consumers that do not use v-model:open.",
2587
+ payload: '["boolean"]'
2006
2588
  },
2007
2589
  {
2008
- name: "input-change"
2590
+ name: "input-change",
2591
+ 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`.",
2592
+ payload: '["string"]'
2009
2593
  }
2010
2594
  ],
2011
2595
  slots: [
2012
2596
  {
2013
- name: "prefix"
2597
+ name: "prefix",
2598
+ 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
2599
  },
2015
2600
  {
2016
- name: "option"
2601
+ name: "option",
2602
+ 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
2603
  },
2018
2604
  {
2019
- name: "empty"
2605
+ name: "empty",
2606
+ 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
2607
  }
2021
2608
  ],
2022
2609
  a11y: [
2023
2610
  "role: combobox",
2611
+ "combobox_element: input",
2612
+ "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
2613
  "trigger_aria_haspopup: listbox",
2025
2614
  "trigger_aria_expanded: true",
2615
+ "trigger_aria_controls_when_open: true",
2026
2616
  "aria_autocomplete: list",
2027
2617
  "aria_activedescendant_on_active_option: true",
2618
+ "aria_activedescendant_cleared_while_closed: true",
2028
2619
  "aria_invalid_when_invalid: true",
2029
2620
  "aria_busy_when_loading: true",
2621
+ "listbox_role: listbox",
2622
+ "option_role: option",
2623
+ "listbox_mirrors_aria_label_and_aria_labelledby: true",
2030
2624
  "aria_selected_on_options: true",
2031
2625
  "aria_disabled_on_disabled_options: true",
2626
+ "focus_stays_on_the_input_options_are_never_focused: true",
2032
2627
  "keyboard_arrow_navigation: true",
2628
+ "arrow_navigation_does_not_wrap_at_the_ends: true",
2629
+ "arrow_navigation_skips_disabled_options: true",
2033
2630
  "escape_closes_and_restores_previous_selection: true",
2631
+ "escape_is_ignored_while_closed: true",
2034
2632
  "enter_selects_active_option: true",
2633
+ "tab_closes_and_restores_previous_selection: true",
2035
2634
  "home_end_navigation: true"
2036
2635
  ],
2037
2636
  behavior: [
2038
2637
  "update_payload_type: string",
2638
+ "input_text_is_internal_state_not_model_value: true",
2039
2639
  "local_filtering_by_label_description_and_keywords: true",
2640
+ "filtering_is_case_insensitive_substring_on_a_trimmed_query: true",
2641
+ "empty_query_shows_all_options: true",
2642
+ "input_change_emits_the_raw_query_for_async_filtering: true",
2643
+ "async_results_are_fed_back_through_the_options_prop: true",
2040
2644
  "loading_does_not_disable_by_itself: true",
2645
+ "spinner_replaces_the_chevron_when_loading: true",
2646
+ "chevron_rotates_when_open: true",
2647
+ "check_icon_on_selected_option: true",
2648
+ "focus_opens_the_listbox: true",
2649
+ "click_on_input_opens_the_listbox: true",
2650
+ "typing_opens_the_listbox: true",
2651
+ "arrow_down_opens_the_listbox_with_the_first_enabled_option_active: true",
2652
+ "arrow_up_opens_the_listbox_with_the_last_enabled_option_active: true",
2653
+ "active_option_defaults_to_the_current_selection_when_it_is_in_the_filtered_list: true",
2654
+ "disabled_options_cannot_be_activated_selected_or_hovered: true",
2655
+ "selecting_an_option_sets_the_input_text_to_its_label_and_closes_without_restoring: true",
2041
2656
  "outside_click_closes_dropdown: true",
2657
+ "outside_click_is_detected_on_pointerdown_on_document: true",
2042
2658
  "query_clears_selection_when_closed_empty: true",
2043
- "selected_value_restores_when_search_is_abandoned: true"
2659
+ "selected_value_restores_when_search_is_abandoned: true",
2660
+ "external_model_value_and_options_changes_resync_the_input_text_only_while_closed: true",
2661
+ "active_option_is_revalidated_when_the_filtered_list_changes: true",
2662
+ "open_state_emits_are_deduped: true",
2663
+ "listbox_is_not_rendered_while_disabled: true",
2664
+ "listbox_renders_in_flow_with_no_portal_and_no_flip_positioning: true"
2044
2665
  ],
2045
2666
  selection: {
2046
2667
  keywords: [
@@ -2073,7 +2694,8 @@ var treeui_catalog_default = {
2073
2694
  "Pass `options` as labeled items and keep labels human-readable because search matches visible fields."
2074
2695
  ]
2075
2696
  },
2076
- recipes: []
2697
+ recipes: [],
2698
+ practices: []
2077
2699
  },
2078
2700
  {
2079
2701
  name: "TConfirmDialog",
@@ -2096,12 +2718,14 @@ var treeui_catalog_default = {
2096
2718
  {
2097
2719
  name: "defaultOpen",
2098
2720
  type: "boolean",
2099
- default: "false"
2721
+ default: "false",
2722
+ note: "Uncontrolled initial state. Read once on setup \u2014 later changes are ignored. Only meaningful when `open` is left undefined."
2100
2723
  },
2101
2724
  {
2102
2725
  name: "disabled",
2103
2726
  type: "boolean",
2104
- default: "false"
2727
+ default: "false",
2728
+ 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
2729
  },
2106
2730
  {
2107
2731
  name: "size",
@@ -2111,47 +2735,63 @@ var treeui_catalog_default = {
2111
2735
  "sm",
2112
2736
  "md",
2113
2737
  "lg"
2114
- ]
2738
+ ],
2739
+ 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
2740
  },
2116
2741
  {
2117
2742
  name: "title",
2118
2743
  type: "string",
2119
- default: ""
2744
+ default: "",
2745
+ 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
2746
  },
2121
2747
  {
2122
2748
  name: "description",
2123
2749
  type: "string",
2124
- default: ""
2750
+ default: "",
2751
+ 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
2752
  },
2126
2753
  {
2127
2754
  name: "confirmLabel",
2128
2755
  type: "string",
2129
- default: "Confirm"
2756
+ default: "Confirm",
2757
+ note: "Ignored when the `actions` slot is supplied."
2130
2758
  },
2131
2759
  {
2132
2760
  name: "cancelLabel",
2133
2761
  type: "string",
2134
- default: "Cancel"
2762
+ default: "Cancel",
2763
+ note: "Ignored when the `actions` slot is supplied."
2135
2764
  },
2136
2765
  {
2137
2766
  name: "confirmVariant",
2138
2767
  type: "TVariant",
2139
- default: "danger"
2768
+ default: "danger",
2769
+ values: [
2770
+ "solid",
2771
+ "outline",
2772
+ "ghost",
2773
+ "soft",
2774
+ "danger"
2775
+ ],
2776
+ note: "Ignored when the `actions` slot is supplied."
2140
2777
  },
2141
2778
  {
2142
2779
  name: "confirmDisabled",
2143
2780
  type: "boolean",
2144
- default: "false"
2781
+ default: "false",
2782
+ 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
2783
  },
2146
2784
  {
2147
2785
  name: "cancelDisabled",
2148
2786
  type: "boolean",
2149
- default: "false"
2787
+ default: "false",
2788
+ 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
2789
  },
2151
2790
  {
2152
2791
  name: "loading",
2153
2792
  type: "boolean",
2154
- default: "false"
2793
+ default: "false",
2794
+ 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
2795
  },
2156
2796
  {
2157
2797
  name: "closeOnEscape",
@@ -2161,27 +2801,32 @@ var treeui_catalog_default = {
2161
2801
  {
2162
2802
  name: "closeOnOverlay",
2163
2803
  type: "boolean",
2164
- default: "false"
2804
+ default: "false",
2805
+ 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
2806
  },
2166
2807
  {
2167
2808
  name: "showCloseButton",
2168
2809
  type: "boolean",
2169
- default: "false"
2810
+ default: "false",
2811
+ note: "Deliberately opt-in \u2014 TModal defaults this to true. Enabling it adds a header close button that dismisses without emitting `cancel`."
2170
2812
  },
2171
2813
  {
2172
2814
  name: "closeLabel",
2173
2815
  type: "string",
2174
- default: "Close dialog"
2816
+ default: "Close dialog",
2817
+ note: "Accessible name of the header close button. Only rendered when `showCloseButton` is true."
2175
2818
  },
2176
2819
  {
2177
2820
  name: "closeOnConfirm",
2178
2821
  type: "boolean",
2179
- default: "true"
2822
+ default: "true",
2823
+ 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
2824
  },
2181
2825
  {
2182
2826
  name: "closeOnCancel",
2183
2827
  type: "boolean",
2184
- default: "true"
2828
+ default: "true",
2829
+ note: "Set false to keep the dialog open after `cancel` \u2014 the caller then owns closing via `open`."
2185
2830
  }
2186
2831
  ],
2187
2832
  events: [
@@ -2200,16 +2845,20 @@ var treeui_catalog_default = {
2200
2845
  ],
2201
2846
  slots: [
2202
2847
  {
2203
- name: "trigger"
2848
+ name: "trigger",
2849
+ description: "Element that opens the dialog on click. Omit for imperative use."
2204
2850
  },
2205
2851
  {
2206
- name: "icon"
2852
+ name: "icon",
2853
+ description: "Decorative leading icon. Presence switches the block to the two-column icon layout."
2207
2854
  },
2208
2855
  {
2209
- name: "default"
2856
+ name: "default",
2857
+ description: "Extra body content rendered under the description."
2210
2858
  },
2211
2859
  {
2212
- name: "actions"
2860
+ name: "actions",
2861
+ description: "Replaces the whole footer. Scope gives you confirm(), cancel(), and loading."
2213
2862
  }
2214
2863
  ],
2215
2864
  a11y: [
@@ -2217,15 +2866,26 @@ var treeui_catalog_default = {
2217
2866
  "aria_modal: true",
2218
2867
  "focus_trap: inherited_from_modal",
2219
2868
  "restores_focus_on_close: inherited_from_modal",
2869
+ "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.",
2870
+ "accessible_name_source: `title` prop, via aria-labelledby; absent when `title` is empty",
2871
+ "accessible_description_source: `description` prop, via aria-describedby; the default slot body is not included",
2872
+ "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
2873
  "icon_slot_marked_aria_hidden: true"
2221
2874
  ],
2222
2875
  behavior: [
2223
2876
  "built_on_modal_contract: true",
2224
2877
  "default_confirm_button_variant: danger",
2878
+ "default_cancel_button_variant: ghost",
2225
2879
  "overlay_click_disabled_by_default: true",
2226
2880
  "close_button_hidden_by_default: true",
2227
2881
  "loading_state_applies_to_confirm_button: true",
2228
- "default_actions_close_dialog_when_enabled: true"
2882
+ "confirm_suppressed_when: confirmDisabled || loading \u2014 the handler returns before emitting, so neither `confirm` nor the close runs",
2883
+ "cancel_suppressed_when: cancelDisabled \u2014 the handler returns before emitting, so neither `cancel` nor the close runs",
2884
+ "closes_after_confirm_when: closeOnConfirm",
2885
+ "closes_after_cancel_when: closeOnCancel",
2886
+ "guards_and_auto_close_apply_to_actions_slot: true",
2887
+ "close_button_and_escape_do_not_emit_cancel: true",
2888
+ "inherits_teleport_scroll_lock_and_focus_trap_from_modal: true"
2229
2889
  ],
2230
2890
  selection: {
2231
2891
  keywords: [],
@@ -2239,6 +2899,9 @@ var treeui_catalog_default = {
2239
2899
  },
2240
2900
  recipes: [
2241
2901
  "confirm-destructive-action"
2902
+ ],
2903
+ practices: [
2904
+ "accessible-by-default"
2242
2905
  ]
2243
2906
  },
2244
2907
  {
@@ -2291,7 +2954,9 @@ var treeui_catalog_default = {
2291
2954
  }
2292
2955
  ],
2293
2956
  a11y: [
2294
- "Acts as a structural wrapper only; semantics come from the `as` prop and child content."
2957
+ "Acts as a structural wrapper only; semantics come from the `as` prop and child content.",
2958
+ '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',
2959
+ "`aria-*` and `role` attributes fall through to the root element (see `forwards_attrs_to`)."
2295
2960
  ],
2296
2961
  behavior: [
2297
2962
  "semantic_element_controlled_by_as: true",
@@ -2335,6 +3000,9 @@ var treeui_catalog_default = {
2335
3000
  },
2336
3001
  recipes: [
2337
3002
  "sidebar-shell"
3003
+ ],
3004
+ practices: [
3005
+ "content-alignment"
2338
3006
  ]
2339
3007
  },
2340
3008
  {
@@ -2343,6 +3011,8 @@ var treeui_catalog_default = {
2343
3011
  package: "@treeui/vue",
2344
3012
  category: "Overlay",
2345
3013
  status: "stable",
3014
+ 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",
3015
+ sourceFile: "packages/vue/src/components/TContextMenu.vue",
2346
3016
  storyFile: "apps/docs/src/stories/ContextMenu.stories.ts",
2347
3017
  storyTitle: "Components/Overlay/ContextMenu",
2348
3018
  manifestFile: "docs/ai/COMPONENTS/context-menu.yaml",
@@ -2351,73 +3021,84 @@ var treeui_catalog_default = {
2351
3021
  name: "items",
2352
3022
  type: "TContextMenuItem[]",
2353
3023
  default: "[]",
2354
- description: "Menu items to render."
3024
+ 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
3025
  },
2356
3026
  {
2357
3027
  name: "open",
2358
3028
  type: "boolean | undefined",
2359
3029
  default: "undefined",
2360
- description: "Controlled open state (v-model:open)."
3030
+ 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
3031
  },
2362
3032
  {
2363
3033
  name: "defaultOpen",
2364
3034
  type: "boolean",
2365
3035
  default: "false",
2366
- description: "Uncontrolled initial open state."
3036
+ description: "Uncontrolled initial open state. Read once during setup \u2014 later changes to this prop have no effect.\n"
2367
3037
  },
2368
3038
  {
2369
3039
  name: "disabled",
2370
3040
  type: "boolean",
2371
3041
  default: "false",
2372
- description: "Prevents the context menu from opening."
3042
+ 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
3043
  },
2374
3044
  {
2375
3045
  name: "size",
2376
3046
  type: "TSize",
2377
3047
  default: "md",
2378
- description: "Item sizing variant."
3048
+ values: [
3049
+ "sm",
3050
+ "md",
3051
+ "lg"
3052
+ ],
3053
+ 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
3054
  }
2380
3055
  ],
2381
3056
  events: [
2382
3057
  {
2383
3058
  name: "select",
2384
- description: "Emitted when a menu item is selected.",
3059
+ description: "Emitted with `item.value` when an enabled item is activated by click or Enter/Space. Disabled items emit nothing.\n",
2385
3060
  payload: "[value: string]"
2386
3061
  },
2387
3062
  {
2388
3063
  name: "update:open",
2389
- description: "Emitted when open state changes (v-model support).",
3064
+ 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
3065
  payload: "[value: boolean]"
2391
3066
  },
2392
3067
  {
2393
3068
  name: "open-change",
2394
- description: "Emitted when open state changes.",
3069
+ description: "Emitted when open state changes. Always fires together with `update:open`, including in controlled mode.\n",
2395
3070
  payload: "[value: boolean]"
2396
3071
  }
2397
3072
  ],
2398
3073
  slots: [
2399
3074
  {
2400
3075
  name: "default",
2401
- description: "Content that acts as the right-click trigger area."
3076
+ 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
3077
  },
2403
3078
  {
2404
3079
  name: "item",
2405
- description: "Custom rendering for each menu item."
3080
+ 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
3081
  }
2407
3082
  ],
2408
3083
  a11y: [
2409
- "role: menu (on the floating list)",
3084
+ "role: menu (on the teleported floating list)",
2410
3085
  "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"]'
3086
+ '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"]',
3087
+ '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"]',
3088
+ '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
3089
  ],
2415
3090
  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."
3091
+ "Opens at cursor position on right-click (contextmenu event), using event.clientX / clientY.",
3092
+ "Uses Teleport to body to escape stacking contexts; the menu is not a DOM descendant of the trigger.",
3093
+ "Uses position fixed at the raw cursor coordinates, stacked at var(--tree-z-dropdown).",
3094
+ "No collision detection or viewport clamping \u2014 a menu opened near the right or bottom edge overflows.",
3095
+ "Closes on pointerdown outside the menu (including on the trigger itself), Escape, or item selection.",
3096
+ "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.",
3097
+ "A document-level contextmenu listener covers keyboard-invoked menus (Shift+F10 / Menu key) raised outside the trigger.",
3098
+ "Prevents native browser context menu on trigger area, but only while not disabled.",
3099
+ 'Exposes data-state="open" | "closed" on the root element for external styling.',
3100
+ "Document pointerdown / contextmenu listeners are attached only while open, and removed on close and before unmount.",
3101
+ "Menu enter / leave uses the shared t-fade transition (opacity plus scale 0.95)."
2421
3102
  ],
2422
3103
  selection: {
2423
3104
  keywords: [
@@ -2442,7 +3123,8 @@ var treeui_catalog_default = {
2442
3123
  required: [],
2443
3124
  notes: []
2444
3125
  },
2445
- recipes: []
3126
+ recipes: [],
3127
+ practices: []
2446
3128
  },
2447
3129
  {
2448
3130
  name: "TDatePicker",
@@ -2460,17 +3142,20 @@ var treeui_catalog_default = {
2460
3142
  {
2461
3143
  name: "modelValue",
2462
3144
  type: "string",
2463
- default: ""
3145
+ default: "",
3146
+ 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
3147
  },
2465
3148
  {
2466
3149
  name: "open",
2467
3150
  type: "boolean",
2468
- default: "controlled_optional"
3151
+ default: "undefined",
3152
+ 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
3153
  },
2470
3154
  {
2471
3155
  name: "defaultOpen",
2472
3156
  type: "boolean",
2473
- default: "false"
3157
+ default: "false",
3158
+ 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
3159
  },
2475
3160
  {
2476
3161
  name: "size",
@@ -2480,41 +3165,48 @@ var treeui_catalog_default = {
2480
3165
  "sm",
2481
3166
  "md",
2482
3167
  "lg"
2483
- ]
3168
+ ],
3169
+ 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
3170
  },
2485
3171
  {
2486
3172
  name: "disabled",
2487
3173
  type: "boolean",
2488
- default: "false"
3174
+ default: "false",
3175
+ 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
3176
  },
2490
3177
  {
2491
3178
  name: "invalid",
2492
3179
  type: "boolean",
2493
- default: "false"
3180
+ default: "false",
3181
+ 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
3182
  },
2495
3183
  {
2496
3184
  name: "placeholder",
2497
3185
  type: "string",
2498
- default: "Select date"
3186
+ default: "Select date",
3187
+ note: "Rendered inside `.t-date-picker__value` with a `data-placeholder` attribute whenever `modelValue` does not parse."
2499
3188
  },
2500
3189
  {
2501
3190
  name: "locale",
2502
3191
  type: "string",
2503
- default: "en-US"
3192
+ default: "en-US",
3193
+ 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
3194
  },
2505
3195
  {
2506
3196
  name: "min",
2507
3197
  type: "string",
2508
- default: ""
3198
+ default: "",
3199
+ 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
3200
  },
2510
3201
  {
2511
3202
  name: "max",
2512
3203
  type: "string",
2513
- default: ""
3204
+ default: "",
3205
+ 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
3206
  },
2515
3207
  {
2516
3208
  name: "weekStartsOn",
2517
- type: "number",
3209
+ type: "0 | 1 | 2 | 3 | 4 | 5 | 6",
2518
3210
  default: "0",
2519
3211
  values: [
2520
3212
  "0",
@@ -2524,46 +3216,90 @@ var treeui_catalog_default = {
2524
3216
  "4",
2525
3217
  "5",
2526
3218
  "6"
2527
- ]
3219
+ ],
3220
+ 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
3221
  },
2529
3222
  {
2530
3223
  name: "id",
2531
3224
  type: "string",
2532
- default: "auto"
3225
+ default: "auto",
3226
+ 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
3227
  }
2534
3228
  ],
2535
3229
  events: [
2536
3230
  {
2537
- name: "update:modelValue"
3231
+ name: "update:modelValue",
3232
+ 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.',
3233
+ payload: '["string"]'
2538
3234
  },
2539
3235
  {
2540
- name: "update:open"
3236
+ name: "update:open",
3237
+ description: "Emitted whenever the open state changes. Deduped \u2014 no emit when the next value equals the current one.",
3238
+ payload: '["boolean"]'
2541
3239
  },
2542
3240
  {
2543
- name: "open-change"
3241
+ name: "open-change",
3242
+ description: "Emitted immediately after `update:open`, with the same payload. Alias for consumers that do not use v-model:open.",
3243
+ payload: '["boolean"]'
2544
3244
  },
2545
3245
  {
2546
- name: "select"
3246
+ name: "select",
3247
+ 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`.",
3248
+ payload: '["string"]'
2547
3249
  }
2548
3250
  ],
2549
3251
  slots: [
2550
3252
  {
2551
- name: "prefix"
3253
+ name: "prefix",
3254
+ 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
3255
  },
2553
3256
  {
2554
- name: "suffix"
3257
+ name: "suffix",
3258
+ 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
3259
  }
2556
3260
  ],
2557
3261
  a11y: [
3262
+ "role: dialog",
3263
+ "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
3264
  "trigger_has_popup_dialog: true",
3265
+ "trigger_aria_expanded: true",
3266
+ "trigger_aria_controls_when_open: true",
3267
+ "aria_invalid_when_invalid: true",
2559
3268
  "calendar_uses_grid_roles: true",
3269
+ "calendar_aria_modal: false",
3270
+ "calendar_labelled_by_month_caption: true",
3271
+ "weekday_header_is_aria_hidden: true",
3272
+ "aria_selected_rendered_on_every_gridcell: true",
3273
+ "aria_current_date_on_today: true",
3274
+ "day_aria_label_is_the_raw_iso_key: true",
3275
+ "nav_buttons_labelled_previous_month_and_next_month: true",
3276
+ "roving_tabindex_one_focusable_day: true",
2560
3277
  "escape_closes: true",
2561
- "returns_focus_to_trigger_after_selection: true"
3278
+ "returns_focus_to_trigger_after_selection: true",
3279
+ "returns_focus_to_trigger_on_escape_from_a_day: true",
3280
+ "focus_is_not_trapped_and_tab_leaves_the_open_calendar: true"
2562
3281
  ],
2563
3282
  behavior: [
3283
+ "value_format: YYYY-MM-DD",
3284
+ "controlled_and_uncontrolled_open: true",
3285
+ "open_state_emits_are_deduped: true",
2564
3286
  "selection_closes_popover: true",
2565
3287
  "hidden_input_created_when_name_attr_present: true",
2566
- "out_of_range_dates_disabled: true"
3288
+ "hidden_input_submits_the_raw_unparsed_model_value: true",
3289
+ "out_of_range_dates_disabled: true",
3290
+ "min_and_max_also_disable_the_month_nav_buttons: true",
3291
+ "popover_is_not_rendered_while_disabled: true",
3292
+ "outside_click_closes_calendar: true",
3293
+ "outside_click_is_detected_on_pointerdown_on_document: true",
3294
+ "outside_click_and_trigger_toggle_close_without_restoring_focus: true",
3295
+ "grid_is_always_six_weeks_forty_two_days: true",
3296
+ "leading_and_trailing_days_render_muted_and_stay_selectable: true",
3297
+ "opening_syncs_the_view_to_the_selected_date_else_the_last_focused_date_else_today: true",
3298
+ "external_model_value_changes_move_the_view_and_the_focused_day: true",
3299
+ "clearing_the_model_value_resets_the_view_to_today_only_while_closed: true",
3300
+ "month_nav_moves_the_focused_day_to_the_first_of_the_new_month_and_focuses_it: true",
3301
+ "today_is_computed_once_at_setup_not_re_evaluated_across_midnight: true",
3302
+ "popover_renders_in_flow_with_no_portal_and_no_flip_positioning: true"
2567
3303
  ],
2568
3304
  selection: {
2569
3305
  keywords: [
@@ -2591,13 +3327,14 @@ var treeui_catalog_default = {
2591
3327
  "Use `min` and `max` with the same format."
2592
3328
  ]
2593
3329
  },
2594
- recipes: []
3330
+ recipes: [],
3331
+ practices: []
2595
3332
  },
2596
3333
  {
2597
3334
  name: "TDateTimePicker",
2598
3335
  aliases: [],
2599
3336
  package: "@treeui/vue",
2600
- kind: "form",
3337
+ kind: "form_overlay",
2601
3338
  category: "Data Entry",
2602
3339
  status: "available",
2603
3340
  purpose: "Date and time picker with a calendar grid, time selection, and overlay trigger.",
@@ -2610,7 +3347,7 @@ var treeui_catalog_default = {
2610
3347
  name: "modelValue",
2611
3348
  type: "string",
2612
3349
  default: "",
2613
- note: "Uses `YYYY-MM-DDTHH:mm`."
3350
+ 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
3351
  },
2615
3352
  {
2616
3353
  name: "open",
@@ -2630,17 +3367,20 @@ var treeui_catalog_default = {
2630
3367
  "sm",
2631
3368
  "md",
2632
3369
  "lg"
2633
- ]
3370
+ ],
3371
+ note: "Renders as `t-date-picker--<size>` on the root \u2014 there is no `t-date-time-picker--<size>` modifier."
2634
3372
  },
2635
3373
  {
2636
3374
  name: "disabled",
2637
3375
  type: "boolean",
2638
- default: "false"
3376
+ default: "false",
3377
+ note: "Blocks opening via click/keyboard and suppresses the overlay entirely, even when `open` is true."
2639
3378
  },
2640
3379
  {
2641
3380
  name: "loading",
2642
3381
  type: "boolean",
2643
- default: "false"
3382
+ default: "false",
3383
+ note: "Only swaps the default suffix icon for a TSpinner; the trigger stays enabled and interactive."
2644
3384
  },
2645
3385
  {
2646
3386
  name: "invalid",
@@ -2655,33 +3395,38 @@ var treeui_catalog_default = {
2655
3395
  {
2656
3396
  name: "locale",
2657
3397
  type: "string",
2658
- default: "en-US"
3398
+ default: "en-US",
3399
+ note: "Feeds all four Intl.DateTimeFormat calls: trigger text, draft preview, month caption, weekday labels."
2659
3400
  },
2660
3401
  {
2661
3402
  name: "min",
2662
3403
  type: "string",
2663
- default: ""
3404
+ default: "",
3405
+ note: "`YYYY-MM-DD` (resolved to 00:00 of that day) or `YYYY-MM-DDTHH:mm`."
2664
3406
  },
2665
3407
  {
2666
3408
  name: "max",
2667
3409
  type: "string",
2668
- default: ""
3410
+ default: "",
3411
+ note: "`YYYY-MM-DD` (resolved to 23:59:59.999 of that day) or `YYYY-MM-DDTHH:mm`."
2669
3412
  },
2670
3413
  {
2671
3414
  name: "weekStartsOn",
2672
3415
  type: "0 | 1 | 2 | 3 | 4 | 5 | 6",
2673
- default: "0"
3416
+ default: "0",
3417
+ note: "0 = Sunday. Reorders the weekday header labels, the grid start date, and Home/End day navigation."
2674
3418
  },
2675
3419
  {
2676
3420
  name: "id",
2677
3421
  type: "string",
2678
- default: "auto"
3422
+ default: "auto",
3423
+ 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
3424
  },
2680
3425
  {
2681
3426
  name: "step",
2682
3427
  type: "number",
2683
3428
  default: "60",
2684
- note: "Seconds between time options."
3429
+ 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
3430
  }
2686
3431
  ],
2687
3432
  events: [
@@ -2701,12 +3446,28 @@ var treeui_catalog_default = {
2701
3446
  name: "select"
2702
3447
  }
2703
3448
  ],
2704
- slots: [],
3449
+ slots: [
3450
+ {
3451
+ name: "prefix"
3452
+ },
3453
+ {
3454
+ name: "suffix"
3455
+ }
3456
+ ],
2705
3457
  a11y: [
2706
3458
  "role: dialog",
3459
+ "aria_modal: false \u2014 the overlay is non-modal and does not trap focus",
2707
3460
  "trigger_aria_haspopup: dialog",
2708
3461
  "trigger_aria_expanded: true",
2709
- "focus_restores_on_close: true",
3462
+ "trigger_aria_controls_when_open: true",
3463
+ "overlay_labelled_by_month_caption: true",
3464
+ "calendar_uses_grid_roles: true",
3465
+ "day_buttons_use_roving_tabindex: true",
3466
+ "day_buttons_aria_pressed_when_selected: true",
3467
+ "gridcell_aria_selected: true",
3468
+ "weekday_row_aria_hidden: true",
3469
+ "time_selects_have_aria_labels: true",
3470
+ "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
3471
  "keyboard_calendar_navigation: true",
2711
3472
  "escape_closes_overlay: true",
2712
3473
  "aria_invalid_when_invalid: true",
@@ -2716,7 +3477,21 @@ var treeui_catalog_default = {
2716
3477
  "controlled_and_uncontrolled_open: true",
2717
3478
  "value_format: YYYY-MM-DDTHH:mm",
2718
3479
  "min_and_max_limit_available_values: true",
2719
- "time_options_generated_from_step: true"
3480
+ "time_options_generated_from_step: true",
3481
+ "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.",
3482
+ "apply_emit_order: update:modelValue, then change, then select \u2014 all carrying the same `YYYY-MM-DDTHH:mm` string.",
3483
+ "clear_emits_empty_string: Clear emits update:modelValue, change, and select with `''`, then closes.",
3484
+ "default_draft_when_value_is_empty: Now, rounded UP to the next `step` boundary (minimum 60s) with seconds zeroed, then clamped into min/max.",
3485
+ "draft_resyncs_from_model_value_only_while_closed: true",
3486
+ "draft_is_clamped_into_min_max_on_open_and_on_day_select: true",
3487
+ "minute_options_always_include_the_current_draft_minute: true",
3488
+ "hour_option_disabled_when_no_minute_in_that_hour_is_available: true",
3489
+ "month_nav_buttons_disabled_at_min_max_month: true",
3490
+ "out_of_range_days_disabled: true",
3491
+ "outside_pointerdown_closes: true",
3492
+ "overlay_is_not_rendered_while_disabled: true",
3493
+ "hidden_input_created_when_name_attr_present: true",
3494
+ "open_state_emits_are_deduped: true"
2720
3495
  ],
2721
3496
  selection: {
2722
3497
  keywords: [
@@ -2749,14 +3524,15 @@ var treeui_catalog_default = {
2749
3524
  },
2750
3525
  recipes: [
2751
3526
  "date-range-form"
2752
- ]
3527
+ ],
3528
+ practices: []
2753
3529
  },
2754
3530
  {
2755
3531
  name: "TDivider",
2756
3532
  aliases: [],
2757
3533
  package: "@treeui/vue",
2758
3534
  category: "Layout",
2759
- description: "Horizontal or vertical visual separator.",
3535
+ 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
3536
  sourceFile: "packages/vue/src/components/TDivider.vue",
2761
3537
  storyFile: "apps/docs/src/stories/Divider.stories.ts",
2762
3538
  storyTitle: "Components/Layout/Divider",
@@ -2764,34 +3540,41 @@ var treeui_catalog_default = {
2764
3540
  props: [
2765
3541
  {
2766
3542
  name: "orientation",
2767
- type: "string",
3543
+ type: '"horizontal" | "vertical"',
2768
3544
  default: "horizontal",
2769
3545
  values: [
2770
3546
  "horizontal",
2771
3547
  "vertical"
2772
- ]
3548
+ ],
3549
+ 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
3550
  },
2774
3551
  {
2775
3552
  name: "decorative",
2776
3553
  type: "boolean",
2777
3554
  default: "true",
2778
- description: "When true, renders role='none' (purely visual). When false, renders as role='separator'."
3555
+ 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
3556
  },
2780
3557
  {
2781
3558
  name: "label",
2782
3559
  type: "string",
2783
3560
  required: false,
2784
- description: "Accessible label for non-decorative dividers."
3561
+ 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
3562
  }
2786
3563
  ],
2787
3564
  events: [],
2788
3565
  slots: [],
2789
3566
  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."
3567
+ 'role: "none" when decorative (the default), "separator" when `decorative` is false.',
3568
+ '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.',
3569
+ "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).",
3570
+ '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.'
3571
+ ],
3572
+ behavior: [
3573
+ "Renders a native <hr> element as its single root node.",
3574
+ "Extra attrs and class/style fall through to that <hr> (attribute inheritance is not disabled).",
3575
+ "Applies exactly two classes; no state, size, or variant modifiers exist.",
3576
+ "A vertical divider needs a flex or grid parent to have any height."
2793
3577
  ],
2794
- behavior: [],
2795
3578
  selection: {
2796
3579
  keywords: [],
2797
3580
  useWhen: [],
@@ -2802,7 +3585,8 @@ var treeui_catalog_default = {
2802
3585
  required: [],
2803
3586
  notes: []
2804
3587
  },
2805
- recipes: []
3588
+ recipes: [],
3589
+ practices: []
2806
3590
  },
2807
3591
  {
2808
3592
  name: "TDonutChart",
@@ -2821,67 +3605,73 @@ var treeui_catalog_default = {
2821
3605
  name: "segments",
2822
3606
  type: "TDonutSegment[]",
2823
3607
  default: "[]",
2824
- note: "Each item is { label: string; value: number; color?: string }. Non-positive values are omitted."
3608
+ 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
3609
  },
2826
3610
  {
2827
3611
  name: "size",
2828
3612
  type: "number",
2829
3613
  default: "200",
2830
- note: "Square canvas in px."
3614
+ 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
3615
  },
2832
3616
  {
2833
3617
  name: "thickness",
2834
3618
  type: "number",
2835
3619
  default: "26",
2836
- note: "Ring thickness; 0 renders a full pie."
3620
+ 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
3621
  },
2838
3622
  {
2839
3623
  name: "gap",
2840
3624
  type: "number",
2841
3625
  default: "2",
2842
- note: "Gap between slices, in degrees."
3626
+ 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
3627
  },
2844
3628
  {
2845
3629
  name: "rounded",
2846
3630
  type: "boolean",
2847
3631
  default: "false",
2848
- note: "Round the ends of donut segments (widens the gap to make room). Best with a thinner ring."
3632
+ 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
3633
  },
2850
3634
  {
2851
3635
  name: "showLegend",
2852
3636
  type: "boolean",
2853
- default: "true"
3637
+ default: "true",
3638
+ 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
3639
  },
2855
3640
  {
2856
3641
  name: "showPercent",
2857
3642
  type: "boolean",
2858
- default: "true"
3643
+ default: "true",
3644
+ note: "Legend only. Percentages are rounded to whole numbers, so they may not sum to exactly 100%."
2859
3645
  },
2860
3646
  {
2861
3647
  name: "centerLabel",
2862
3648
  type: "string",
2863
- default: "Total",
2864
- note: "Donut only; ignored for pie."
3649
+ default: "undefined",
3650
+ 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
3651
  },
2866
3652
  {
2867
3653
  name: "centerValue",
2868
3654
  type: "string | number",
2869
- default: "auto (sum of values)"
3655
+ default: "undefined",
3656
+ 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
3657
  },
2871
3658
  {
2872
3659
  name: "valueFormat",
2873
3660
  type: "(value: number) => string",
2874
- default: "locale integers for |value| >= 1000"
3661
+ default: "locale integers for |value| >= 1000",
3662
+ note: "Applies to the legend values and to the center value. Not applied to percentages, nor to an explicitly supplied centerValue."
2875
3663
  },
2876
3664
  {
2877
3665
  name: "loading",
2878
3666
  type: "boolean",
2879
- default: "false"
3667
+ default: "false",
3668
+ 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
3669
  },
2881
3670
  {
2882
3671
  name: "ariaLabel",
2883
3672
  type: "string",
2884
- default: "auto-generated summary"
3673
+ default: "auto-generated summary ('Donut chart with <n> segments' / 'Pie chart with <n> segments')",
3674
+ 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
3675
  }
2886
3676
  ],
2887
3677
  events: [],
@@ -2894,13 +3684,20 @@ var treeui_catalog_default = {
2894
3684
  }
2895
3685
  ],
2896
3686
  a11y: [
2897
- "Renders role=img with an aria-label summarizing the chart.",
2898
- "Legend + percentages give a non-visual reading of each slice."
3687
+ "The inner <svg> carries role=img plus the ariaLabel (or the auto-generated summary); the root div has no role or accessible name.",
3688
+ "Legend + percentages give a non-visual reading of each slice.",
3689
+ "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.",
3690
+ "Legend swatches are aria-hidden, so color is never the only carrier of identity.",
3691
+ "Provide a meaningful ariaLabel describing what the chart shows."
2899
3692
  ],
2900
3693
  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"
3694
+ "colors_auto_assign_from_chart_palette: --tree-color-chart-1..8, cycling by visible-segment index; per-segment color overrides",
3695
+ "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",
3696
+ "hovering_a_legend_row_drives_the_same_highlight: legend items carry the same pointerenter/pointerleave handlers as the segments",
3697
+ "center_readout_precedence: hovered segment > centerValue > formatted total; the label half is hovered segment label > centerLabel > 'Total'",
3698
+ "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",
3699
+ "empty_state_when_no_visible_segment_has_value: total <= 0 renders the empty slot instead of the svg; loading wins over it",
3700
+ "segments_fade_in_staggered: 70ms animation delay per segment index; removed under prefers-reduced-motion"
2904
3701
  ],
2905
3702
  selection: {
2906
3703
  keywords: [
@@ -2938,7 +3735,8 @@ var treeui_catalog_default = {
2938
3735
  },
2939
3736
  recipes: [
2940
3737
  "metric-dashboard"
2941
- ]
3738
+ ],
3739
+ practices: []
2942
3740
  },
2943
3741
  {
2944
3742
  name: "TDrawer",
@@ -2956,17 +3754,20 @@ var treeui_catalog_default = {
2956
3754
  {
2957
3755
  name: "open",
2958
3756
  type: "boolean",
2959
- default: "controlled_optional"
3757
+ default: "controlled_optional",
3758
+ 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
3759
  },
2961
3760
  {
2962
3761
  name: "defaultOpen",
2963
3762
  type: "boolean",
2964
- default: "false"
3763
+ default: "false",
3764
+ 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
3765
  },
2966
3766
  {
2967
3767
  name: "disabled",
2968
3768
  type: "boolean",
2969
- default: "false"
3769
+ default: "false",
3770
+ 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
3771
  },
2971
3772
  {
2972
3773
  name: "size",
@@ -2976,7 +3777,8 @@ var treeui_catalog_default = {
2976
3777
  "sm",
2977
3778
  "md",
2978
3779
  "lg"
2979
- ]
3780
+ ],
3781
+ 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
3782
  },
2981
3783
  {
2982
3784
  name: "side",
@@ -2987,42 +3789,50 @@ var treeui_catalog_default = {
2987
3789
  "right",
2988
3790
  "bottom",
2989
3791
  "left"
2990
- ]
3792
+ ],
3793
+ note: "Applied as `t-drawer__surface--<side>`. Also selects the slide-in translate direction and which corners are rounded."
2991
3794
  },
2992
3795
  {
2993
3796
  name: "title",
2994
3797
  type: "string",
2995
- default: ""
3798
+ default: "",
3799
+ 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
3800
  },
2997
3801
  {
2998
3802
  name: "description",
2999
3803
  type: "string",
3000
- default: ""
3804
+ default: "",
3805
+ 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
3806
  },
3002
3807
  {
3003
3808
  name: "closeOnEscape",
3004
3809
  type: "boolean",
3005
- default: "true"
3810
+ default: "true",
3811
+ 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
3812
  },
3007
3813
  {
3008
3814
  name: "closeOnOverlay",
3009
3815
  type: "boolean",
3010
- default: "true"
3816
+ default: "true",
3817
+ 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
3818
  },
3012
3819
  {
3013
3820
  name: "showCloseButton",
3014
3821
  type: "boolean",
3015
- default: "true"
3822
+ default: "true",
3823
+ 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
3824
  },
3017
3825
  {
3018
3826
  name: "closeLabel",
3019
3827
  type: "string",
3020
- default: "Close drawer"
3828
+ default: "Close drawer",
3829
+ 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
3830
  },
3022
3831
  {
3023
3832
  name: "id",
3024
3833
  type: "string",
3025
- default: "auto"
3834
+ default: "auto",
3835
+ 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
3836
  }
3027
3837
  ],
3028
3838
  events: [
@@ -3035,34 +3845,53 @@ var treeui_catalog_default = {
3035
3845
  ],
3036
3846
  slots: [
3037
3847
  {
3038
- name: "trigger"
3848
+ name: "trigger",
3849
+ description: "Element that opens the drawer on click. Omit for controlled or imperative use."
3039
3850
  },
3040
3851
  {
3041
- name: "header"
3852
+ name: "header",
3853
+ description: "Replaces the default title heading in the topbar."
3042
3854
  },
3043
3855
  {
3044
- name: "default"
3856
+ name: "default",
3857
+ description: "Drawer body content."
3045
3858
  },
3046
3859
  {
3047
- name: "content"
3860
+ name: "content",
3861
+ description: "Drawer body content; takes precedence over the default slot."
3048
3862
  },
3049
3863
  {
3050
- name: "footer"
3864
+ name: "footer",
3865
+ description: "Action row pinned under the body."
3051
3866
  }
3052
3867
  ],
3053
3868
  a11y: [
3054
3869
  "role: dialog",
3055
3870
  "aria_modal: true",
3056
3871
  "focus_trap: true",
3057
- "restores_focus_on_close: true",
3058
- "escape_closes_when_enabled: true"
3872
+ '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"}',
3873
+ "escape_closes_when_enabled: true",
3874
+ "escape_scope: The keydown listener lives on the surface, so Escape only closes while focus is inside the drawer.",
3875
+ "accessible_name_source: the `t-drawer__header` div, via aria-labelledby; unset when `title` is empty and no `header` slot is supplied",
3876
+ "accessible_description_source: the `description` prop, via aria-describedby; the body slots are not included",
3877
+ '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.',
3878
+ "backdrop_marked_aria_hidden: true",
3879
+ "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
3880
  ],
3060
3881
  behavior: [
3061
3882
  "uses_teleport_to_body: true",
3062
3883
  "locks_body_scroll_while_open: true",
3063
3884
  "overlay_click_closes_when_enabled: true",
3064
3885
  "close_button_optional: true",
3065
- "slides_in_from_side: true"
3886
+ "slides_in_from_side: true",
3887
+ "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.",
3888
+ "topbar_rendered_when: title is non-empty OR the `header` slot is supplied OR showCloseButton is true",
3889
+ "body_slot_precedence: content > default",
3890
+ "footer_rendered_when: the `footer` slot is supplied",
3891
+ 'data_state_attribute: `data-state="open" | "closed"` on both the `t-drawer` root and the `t-drawer__portal`',
3892
+ "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.",
3893
+ '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"}',
3894
+ '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
3895
  ],
3067
3896
  selection: {
3068
3897
  keywords: [
@@ -3091,7 +3920,10 @@ var treeui_catalog_default = {
3091
3920
  required: [],
3092
3921
  notes: []
3093
3922
  },
3094
- recipes: []
3923
+ recipes: [],
3924
+ practices: [
3925
+ "accessible-by-default"
3926
+ ]
3095
3927
  },
3096
3928
  {
3097
3929
  name: "TDropdown",
@@ -3110,22 +3942,25 @@ var treeui_catalog_default = {
3110
3942
  name: "items",
3111
3943
  type: "TDropdownItem[]",
3112
3944
  default: "[]",
3113
- note: "Array of { label: string; value: string; disabled?: boolean }"
3945
+ 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
3946
  },
3115
3947
  {
3116
3948
  name: "open",
3117
3949
  type: "boolean",
3118
- default: "undefined"
3950
+ default: "undefined",
3951
+ 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
3952
  },
3120
3953
  {
3121
3954
  name: "defaultOpen",
3122
3955
  type: "boolean",
3123
- default: "false"
3956
+ default: "false",
3957
+ 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
3958
  },
3125
3959
  {
3126
3960
  name: "disabled",
3127
3961
  type: "boolean",
3128
- default: "false"
3962
+ default: "false",
3963
+ 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
3964
  },
3130
3965
  {
3131
3966
  name: "size",
@@ -3135,12 +3970,14 @@ var treeui_catalog_default = {
3135
3970
  "sm",
3136
3971
  "md",
3137
3972
  "lg"
3138
- ]
3973
+ ],
3974
+ 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
3975
  },
3140
3976
  {
3141
3977
  name: "label",
3142
3978
  type: "string",
3143
- default: ""
3979
+ default: "",
3980
+ 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
3981
  },
3145
3982
  {
3146
3983
  name: "align",
@@ -3150,18 +3987,30 @@ var treeui_catalog_default = {
3150
3987
  "start",
3151
3988
  "end"
3152
3989
  ],
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)."
3990
+ 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`."
3991
+ },
3992
+ {
3993
+ name: "id",
3994
+ type: "string",
3995
+ default: "undefined",
3996
+ 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
3997
  }
3155
3998
  ],
3156
3999
  events: [
3157
4000
  {
3158
- name: "select"
4001
+ name: "select",
4002
+ 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.",
4003
+ payload: '[{"value":"string"}]'
3159
4004
  },
3160
4005
  {
3161
- name: "update:open"
4006
+ name: "update:open",
4007
+ description: "Emitted whenever the open state changes. Deduped \u2014 no emit when the next value equals the current one.",
4008
+ payload: '["boolean"]'
3162
4009
  },
3163
4010
  {
3164
- name: "open-change"
4011
+ name: "open-change",
4012
+ description: "Emitted immediately after `update:open`, with the same payload. Alias for consumers that do not use v-model:open.",
4013
+ payload: '["boolean"]'
3165
4014
  }
3166
4015
  ],
3167
4016
  slots: [
@@ -3174,23 +4023,39 @@ var treeui_catalog_default = {
3174
4023
  ],
3175
4024
  a11y: [
3176
4025
  "role: menu",
4026
+ "menu_accessible_name_comes_from_the_label_prop: true",
3177
4027
  "trigger_aria_haspopup: menu",
3178
4028
  "trigger_aria_expanded: true",
3179
4029
  "trigger_aria_controls: menu id, while open",
4030
+ "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.",
4031
+ "item_role: menuitem",
3180
4032
  "aria_disabled_on_disabled_items: true",
4033
+ "disabled_items_get_tabindex_minus_1_but_stay_programmatically_focusable: true",
3181
4034
  "focus_visible_on_trigger: true",
4035
+ "items_receive_real_dom_focus_no_aria_activedescendant: true",
3182
4036
  "keyboard_arrow_navigation: true",
4037
+ "arrow_navigation_skips_disabled_items: true",
4038
+ "arrow_navigation_does_not_wrap_at_the_ends: true",
4039
+ "arrow_down_or_arrow_up_on_the_trigger_both_open_at_the_first_enabled_item: true",
3183
4040
  "escape_closes_menu: true",
3184
4041
  "enter_space_selects_item: true",
3185
- "home_end_navigation: true"
4042
+ "home_end_navigation: true",
4043
+ "no_typeahead: true",
4044
+ "tab_does_not_close_the_menu: true"
3186
4045
  ],
3187
4046
  behavior: [
3188
- "outside_click_closes_menu: true",
4047
+ '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
4048
  "outside_click_restores_focus: false",
3190
4049
  "chevron_rotates_when_open: true",
3191
4050
  "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}'
4051
+ "menu_scrolls_vertically_past_15rem: true",
4052
+ "menu_renders_in_flow_with_no_portal_and_no_flip_positioning: true",
4053
+ "menu_is_not_rendered_while_disabled: true",
4054
+ "open_state_emits_are_deduped: true",
4055
+ "controlled_open_requires_writing_the_value_back: true",
4056
+ '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"}',
4057
+ '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"}',
4058
+ '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
4059
  ],
3195
4060
  selection: {
3196
4061
  keywords: [
@@ -3219,7 +4084,11 @@ var treeui_catalog_default = {
3219
4084
  required: [],
3220
4085
  notes: []
3221
4086
  },
3222
- recipes: []
4087
+ recipes: [],
4088
+ practices: [
4089
+ "interaction-feedback",
4090
+ "accessible-by-default"
4091
+ ]
3223
4092
  },
3224
4093
  {
3225
4094
  name: "TEmptyState",
@@ -3311,7 +4180,10 @@ var treeui_catalog_default = {
3311
4180
  required: [],
3312
4181
  notes: []
3313
4182
  },
3314
- recipes: []
4183
+ recipes: [],
4184
+ practices: [
4185
+ "state-clarity"
4186
+ ]
3315
4187
  },
3316
4188
  {
3317
4189
  name: "TFileUpload",
@@ -3345,7 +4217,7 @@ var treeui_catalog_default = {
3345
4217
  name: "disabled",
3346
4218
  type: "boolean",
3347
4219
  default: "false",
3348
- description: "The ONLY prop that gates interaction. Disables the dropzone, the native input, Clear all, and every per-row Remove/Retry."
4220
+ 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
4221
  },
3350
4222
  {
3351
4223
  name: "loading",
@@ -3370,13 +4242,15 @@ var treeui_catalog_default = {
3370
4242
  },
3371
4243
  {
3372
4244
  name: "maxFiles",
3373
- type: "number",
3374
- default: "none"
4245
+ type: "number | null",
4246
+ default: "null",
4247
+ 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
4248
  },
3376
4249
  {
3377
4250
  name: "maxFileSize",
3378
- type: "number",
3379
- default: "none"
4251
+ type: "number | null",
4252
+ default: "null",
4253
+ description: "Per-file byte cap. Enforced only when it is a number greater than 0 \u2014 both `null` and `0` disable the check."
3380
4254
  },
3381
4255
  {
3382
4256
  name: "label",
@@ -3550,19 +4424,144 @@ var treeui_catalog_default = {
3550
4424
  "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
4425
  ],
3552
4426
  selection: {
3553
- keywords: [],
3554
- useWhen: [],
3555
- avoidWhen: [],
3556
- alternatives: []
4427
+ keywords: [],
4428
+ useWhen: [],
4429
+ avoidWhen: [],
4430
+ alternatives: []
4431
+ },
4432
+ setup: {
4433
+ required: [],
4434
+ notes: [
4435
+ "Bind `modelValue` to `File[]`.",
4436
+ "Use `accept`, `multiple`, and size limits in the app layer to constrain uploads."
4437
+ ]
4438
+ },
4439
+ recipes: [],
4440
+ practices: []
4441
+ },
4442
+ {
4443
+ name: "TFlag",
4444
+ aliases: [],
4445
+ package: "@treeui/vue",
4446
+ kind: "display",
4447
+ category: "Data Display",
4448
+ status: "available",
4449
+ 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.",
4450
+ sourceFile: "packages/vue/src/components/TFlag.vue",
4451
+ storyFile: "apps/docs/src/stories/Flag.stories.ts",
4452
+ storyTitle: "Components/Data Display/Flag",
4453
+ manifestFile: "docs/ai/COMPONENTS/flag.yaml",
4454
+ props: [
4455
+ {
4456
+ name: "code",
4457
+ type: "string",
4458
+ required: true,
4459
+ description: "ISO 3166-1 alpha-2 country code. Case-insensitive \u2014 trimmed and lowercased before the URL is built."
4460
+ },
4461
+ {
4462
+ name: "baseUrl",
4463
+ type: "string",
4464
+ default: "https://flagcdn.com",
4465
+ 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."
4466
+ },
4467
+ {
4468
+ name: "size",
4469
+ type: "TSize",
4470
+ default: "md",
4471
+ values: [
4472
+ "sm",
4473
+ "md",
4474
+ "lg"
4475
+ ],
4476
+ 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`."
4477
+ },
4478
+ {
4479
+ name: "label",
4480
+ type: "string",
4481
+ default: "undefined",
4482
+ 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.'
4483
+ },
4484
+ {
4485
+ name: "fallbackText",
4486
+ type: "string",
4487
+ default: "undefined",
4488
+ description: 'Text shown when the image cannot load. Defaults to the normalized `code` uppercased, so a padded `" br "` falls back to `BR`.'
4489
+ }
4490
+ ],
4491
+ events: [],
4492
+ slots: [],
4493
+ a11y: [
4494
+ "decorative_by_default: true",
4495
+ "becomes_image_when_labeled: true",
4496
+ '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.',
4497
+ '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.',
4498
+ '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.',
4499
+ "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."
4500
+ ],
4501
+ behavior: [
4502
+ "falls_back_to_text_when_image_fails: true",
4503
+ "failed_state_resets_when_resolved_src_changes: true",
4504
+ "code_is_trimmed_and_lowercased: true",
4505
+ "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.",
4506
+ "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.",
4507
+ "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.",
4508
+ "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.",
4509
+ "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.",
4510
+ "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.",
4511
+ "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).",
4512
+ "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`.",
4513
+ "no_country_or_locale_dataset_is_shipped: TreeUI never maps a language or locale to a country code \u2014 the application owns that decision."
4514
+ ],
4515
+ selection: {
4516
+ keywords: [
4517
+ "flag",
4518
+ "country flag",
4519
+ "country",
4520
+ "nationality",
4521
+ "region",
4522
+ "iso 3166",
4523
+ "alpha-2"
4524
+ ],
4525
+ useWhen: [
4526
+ "You need a country flag rendered from an ISO 3166-1 alpha-2 code.",
4527
+ "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.",
4528
+ "You want a text fallback when the flag asset cannot load."
4529
+ ],
4530
+ avoidWhen: [
4531
+ "The flag would be the only carrier of meaning \u2014 always pair it with adjacent text such as the language or country name.",
4532
+ "You need an interface glyph rather than a country flag.",
4533
+ "You are treating the flag as a language identity \u2014 a flag names a country, not a language."
4534
+ ],
4535
+ alternatives: [
4536
+ {
4537
+ component: "TIcon",
4538
+ when: "The graphic is a registered TreeUI icon, not a country flag."
4539
+ },
4540
+ {
4541
+ component: "TLanguageSelect",
4542
+ when: "The flag belongs to a language or locale picker rather than standing alone."
4543
+ }
4544
+ ]
3557
4545
  },
3558
4546
  setup: {
3559
- required: [],
4547
+ required: [
4548
+ "Pass `code` as an ISO 3166-1 alpha-2 country code. Matching is case-insensitive \u2014 the value is trimmed and lowercased internally.",
4549
+ "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."
4550
+ ],
3560
4551
  notes: [
3561
- "Bind `modelValue` to `File[]`.",
3562
- "Use `accept`, `multiple`, and size limits in the app layer to constrain uploads."
4552
+ "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`.",
4553
+ "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.",
4554
+ "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.",
4555
+ "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.",
4556
+ "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.",
4557
+ "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.",
4558
+ "`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.",
4559
+ "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`.",
4560
+ "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
4561
  ]
3564
4562
  },
3565
- recipes: []
4563
+ recipes: [],
4564
+ practices: []
3566
4565
  },
3567
4566
  {
3568
4567
  name: "TFormField",
@@ -3691,7 +4690,12 @@ var treeui_catalog_default = {
3691
4690
  "date-range-form",
3692
4691
  "color-field",
3693
4692
  "tag-input",
3694
- "hierarchy-picker"
4693
+ "hierarchy-picker",
4694
+ "language-switcher"
4695
+ ],
4696
+ practices: [
4697
+ "accessible-by-default",
4698
+ "content-alignment"
3695
4699
  ]
3696
4700
  },
3697
4701
  {
@@ -3793,6 +4797,9 @@ var treeui_catalog_default = {
3793
4797
  "date-range-form",
3794
4798
  "stat-grid",
3795
4799
  "metric-dashboard"
4800
+ ],
4801
+ practices: [
4802
+ "content-alignment"
3796
4803
  ]
3797
4804
  },
3798
4805
  {
@@ -3813,24 +4820,380 @@ var treeui_catalog_default = {
3813
4820
  type: "TIconName",
3814
4821
  required: true,
3815
4822
  values: [
4823
+ "account",
4824
+ "activity",
4825
+ "ai-studio",
3816
4826
  "alert-circle",
4827
+ "align-left",
4828
+ "app-window",
4829
+ "apps-grid",
4830
+ "archive",
4831
+ "archive-restore",
4832
+ "arrow-down",
4833
+ "arrow-left",
4834
+ "arrow-left-right",
4835
+ "arrow-right",
4836
+ "arrow-up",
4837
+ "arrow-up-right",
4838
+ "assistant",
4839
+ "automation-key",
4840
+ "automations",
4841
+ "badge",
4842
+ "badge-check",
4843
+ "badge-star",
4844
+ "ban",
4845
+ "bell",
4846
+ "book-open",
4847
+ "bookmark",
4848
+ "bot",
4849
+ "bot-badge",
4850
+ "bot-users",
4851
+ "boxes",
4852
+ "boxes-model",
4853
+ "braces",
4854
+ "brackets",
4855
+ "brain",
4856
+ "brain-circuit",
4857
+ "brain-lock",
4858
+ "browser",
4859
+ "brush",
4860
+ "brush-stroke",
4861
+ "bug",
4862
+ "building-2",
4863
+ "calculator",
3817
4864
  "calendar",
4865
+ "calendar-clock",
4866
+ "calendar-day",
4867
+ "calendar-days",
4868
+ "calendar-dot",
4869
+ "calendar-plus",
4870
+ "calendar-range",
4871
+ "calendar-x",
4872
+ "carousel",
4873
+ "catalog",
4874
+ "chart-column",
4875
+ "chart-line",
4876
+ "chart-pie",
4877
+ "chat",
3818
4878
  "check",
3819
- "chevron-left",
4879
+ "check-circle",
4880
+ "check-square",
3820
4881
  "chevron-down",
4882
+ "chevron-left",
3821
4883
  "chevron-right",
4884
+ "chevron-up",
4885
+ "chevron-updown",
4886
+ "chevrons-up-down",
4887
+ "circle-alert",
4888
+ "circle-check",
4889
+ "circle-dot",
4890
+ "circle-help",
4891
+ "circle-x",
4892
+ "clipboard-list",
4893
+ "clock",
4894
+ "clock-alert",
4895
+ "clock-sparkles",
4896
+ "clock-x",
4897
+ "close",
4898
+ "cloud",
4899
+ "cloud-off",
4900
+ "code",
4901
+ "code-2",
4902
+ "code-api",
4903
+ "coins",
4904
+ "comment",
4905
+ "companion",
4906
+ "compass",
4907
+ "connections",
4908
+ "contentpilot",
4909
+ "copy",
4910
+ "cpu",
4911
+ "cpu-chip",
4912
+ "credit-card",
4913
+ "crosshair",
4914
+ "crown",
4915
+ "cube",
4916
+ "database",
4917
+ "developer-settings",
4918
+ "device-link",
4919
+ "dollar-circle",
4920
+ "dollar-limit",
4921
+ "download",
4922
+ "download-video",
4923
+ "draw",
4924
+ "droplet",
4925
+ "ellipsis",
4926
+ "ellipsis-vertical",
4927
+ "embed-code",
4928
+ "eraser",
4929
+ "extension",
4930
+ "external-link",
4931
+ "eye",
4932
+ "eye-off",
4933
+ "file",
4934
+ "file-archive",
4935
+ "file-audio",
4936
+ "file-code",
4937
+ "file-edit",
4938
+ "file-image",
4939
+ "file-pdf",
4940
+ "file-plus",
4941
+ "file-scan",
4942
+ "file-text",
4943
+ "file-video",
4944
+ "file-warning",
4945
+ "files",
4946
+ "film",
4947
+ "filter",
4948
+ "filter-capabilities",
4949
+ "fingerprint",
4950
+ "flag",
4951
+ "flask-play",
4952
+ "folder",
4953
+ "folder-input",
4954
+ "folder-kanban",
4955
+ "folder-open",
4956
+ "folder-plus",
4957
+ "folder-shared",
4958
+ "folder-tree",
4959
+ "folder-x",
4960
+ "folders",
4961
+ "gauge",
4962
+ "gauge-high",
4963
+ "gauge-low",
4964
+ "gauge-medium",
4965
+ "gavel",
4966
+ "git-branch",
4967
+ "git-fork",
4968
+ "globe",
4969
+ "globe-check",
4970
+ "grid",
4971
+ "grip-vertical",
4972
+ "hand-check",
4973
+ "hard-drive",
4974
+ "hard-drive-alert",
4975
+ "hard-drive-off",
4976
+ "hash",
4977
+ "heart",
4978
+ "heart-chart-up",
4979
+ "heart-pulse",
4980
+ "help",
4981
+ "hierarchy",
4982
+ "history",
4983
+ "home",
4984
+ "hourglass",
4985
+ "house",
4986
+ "human-lock",
4987
+ "id-badge",
4988
+ "image",
4989
+ "image-minus",
4990
+ "image-plus",
4991
+ "image-up",
4992
+ "inbox",
4993
+ "inbox-empty",
3822
4994
  "info",
4995
+ "install",
4996
+ "journal",
4997
+ "key",
4998
+ "key-off",
4999
+ "key-round",
5000
+ "languages",
5001
+ "laptop",
5002
+ "laptop-bridge",
5003
+ "layers",
5004
+ "layout-dashboard",
5005
+ "layout-grid",
5006
+ "layout-kanban",
5007
+ "leaf",
5008
+ "library-books",
5009
+ "life-buoy",
5010
+ "lightbulb",
5011
+ "lightbulb-sparkles",
5012
+ "line-width",
5013
+ "link",
5014
+ "link-2",
5015
+ "link-off",
5016
+ "list",
5017
+ "list-checks",
5018
+ "list-ordered",
5019
+ "list-rule",
5020
+ "list-todo",
5021
+ "llm",
3823
5022
  "loader-circle",
5023
+ "lock",
5024
+ "lock-keyhole",
5025
+ "log-in",
5026
+ "log-out",
5027
+ "log-out-all",
5028
+ "magic-wand",
5029
+ "mail",
5030
+ "mail-check",
5031
+ "mail-open",
5032
+ "mail-plus",
5033
+ "mail-warning",
5034
+ "mails",
5035
+ "market",
5036
+ "maximize-2",
5037
+ "megaphone",
5038
+ "memory-stick",
5039
+ "menu",
5040
+ "message-circle",
5041
+ "message-square",
5042
+ "message-square-plus",
5043
+ "messages-square",
5044
+ "mic",
5045
+ "microphone",
5046
+ "minimize-2",
5047
+ "minus",
5048
+ "minus-square",
5049
+ "monitor-home",
5050
+ "monitor-smartphone",
5051
+ "moon",
5052
+ "more-horizontal",
5053
+ "mouse-pointer-2",
5054
+ "move-horizontal",
5055
+ "network",
5056
+ "network-nodes",
5057
+ "newsletter",
5058
+ "newspaper",
5059
+ "octagon-x",
5060
+ "package",
5061
+ "package-download",
5062
+ "page-snapshot",
5063
+ "palette",
5064
+ "panel-left",
5065
+ "panel-right",
5066
+ "panels-top-left",
5067
+ "paper-plane",
5068
+ "paperclip",
5069
+ "pause",
5070
+ "pause-circle",
5071
+ "pencil",
5072
+ "persona",
5073
+ "piggy-bank",
5074
+ "pipette",
5075
+ "play",
5076
+ "play-circle",
5077
+ "plug",
5078
+ "plug-cloud",
5079
+ "plug-off",
5080
+ "plug-plus",
5081
+ "plugin",
5082
+ "plus",
5083
+ "price-tag",
5084
+ "publish",
5085
+ "quote",
5086
+ "radio",
5087
+ "radio-tower",
5088
+ "receipt",
5089
+ "refresh",
5090
+ "refresh-cw",
5091
+ "refresh-cw-off",
5092
+ "repeat",
5093
+ "repeat-2",
5094
+ "repeat-fallback",
5095
+ "repeat-interval",
5096
+ "responses-list",
5097
+ "rocket",
5098
+ "rotate-ccw",
5099
+ "rotate-cw",
5100
+ "route",
5101
+ "rss",
5102
+ "save",
5103
+ "scale",
5104
+ "scan",
3824
5105
  "search",
3825
- "x"
5106
+ "search-x",
5107
+ "send",
5108
+ "send-check",
5109
+ "send-request",
5110
+ "server",
5111
+ "server-api",
5112
+ "server-environment",
5113
+ "settings",
5114
+ "settings-2",
5115
+ "share",
5116
+ "share-nodes",
5117
+ "shield",
5118
+ "shield-check",
5119
+ "shield-lock",
5120
+ "shield-question",
5121
+ "shield-x",
5122
+ "shopping-basket",
5123
+ "shopping-cart",
5124
+ "shuffle",
5125
+ "signal",
5126
+ "signal-high",
5127
+ "signal-low",
5128
+ "signal-medium",
5129
+ "signal-off",
5130
+ "siren",
5131
+ "sliders-horizontal",
5132
+ "smartphone",
5133
+ "sparkles",
5134
+ "square",
5135
+ "square-check",
5136
+ "square-plus",
5137
+ "square-terminal",
5138
+ "star",
5139
+ "sticker",
5140
+ "storage",
5141
+ "store",
5142
+ "story",
5143
+ "sun",
5144
+ "support",
5145
+ "target",
5146
+ "target-choice",
5147
+ "tasks",
5148
+ "terminal",
5149
+ "ticket",
5150
+ "ticket-plus",
5151
+ "tickets",
5152
+ "timeline",
5153
+ "timer",
5154
+ "toggle-left",
5155
+ "toggle-right",
5156
+ "token-input",
5157
+ "token-output",
5158
+ "trail",
5159
+ "trash-2",
5160
+ "trend-up",
5161
+ "triangle-alert",
5162
+ "type",
5163
+ "unlink",
5164
+ "unplug",
5165
+ "upload",
5166
+ "upload-cloud",
5167
+ "user",
5168
+ "user-check",
5169
+ "user-cog",
5170
+ "user-minus",
5171
+ "user-plus",
5172
+ "user-round",
5173
+ "user-x",
5174
+ "users",
5175
+ "users-round",
5176
+ "vault",
5177
+ "volume-2",
5178
+ "wallet",
5179
+ "wand-sparkles",
5180
+ "warehouse",
5181
+ "workflow",
5182
+ "workspace",
5183
+ "wrench",
5184
+ "wrench-zap",
5185
+ "x",
5186
+ "zap"
3826
5187
  ],
3827
- description: "Icon identifier from the @treeui/icons registry."
5188
+ description: "Icon identifier from the @treeui/icons registry. Widens as `TIconRegistry` is augmented, so registered app icons are valid values too.",
5189
+ 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
5190
  },
3829
5191
  {
3830
5192
  name: "size",
3831
5193
  type: "number | string",
3832
5194
  default: "20",
3833
- description: "Pixel size for both width and height of the rendered svg."
5195
+ description: "Pixel size for both width and height of the rendered svg.",
5196
+ 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
5197
  },
3835
5198
  {
3836
5199
  name: "strokeWidth",
@@ -3857,33 +5220,60 @@ var treeui_catalog_default = {
3857
5220
  "becomes_image_when_labeled: true",
3858
5221
  "rule: Provide `label` only when the icon conveys meaning that is not already in adjacent text."
3859
5222
  ],
3860
- behavior: [],
5223
+ behavior: [
5224
+ "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.",
5225
+ '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).',
5226
+ "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.",
5227
+ "resolved_per_render: TIcon resolves its name in a computed, not once at setup, so an icon registered after the component mounted still appears.",
5228
+ "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.",
5229
+ "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."
5230
+ ],
3861
5231
  selection: {
3862
5232
  keywords: [
3863
5233
  "icon",
3864
5234
  "svg",
3865
5235
  "glyph",
3866
- "pictogram"
5236
+ "pictogram",
5237
+ "register",
5238
+ "register icon",
5239
+ "custom icon",
5240
+ "icon registry",
5241
+ "registerTreeIcons",
5242
+ "createTreeIcon"
3867
5243
  ],
3868
5244
  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."
5245
+ "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.",
5246
+ 'The icon is one of the built-ins: `<TIcon name="cpu" />`. `listTreeIcons()` returns every registered name at runtime.',
5247
+ "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
5248
  ],
3872
5249
  avoidWhen: [
3873
- "The icon is not in the @treeui/icons registry \u2014 import or register it instead."
5250
+ "The graphic is a country flag \u2014 a flag names a country, not an interface concept.",
5251
+ "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
5252
  ],
3875
5253
  alternatives: [
3876
5254
  {
3877
- component: "named import from @treeui/icons",
3878
- when: "You need tree-shaken, statically-imported icon components."
5255
+ component: "a component passed straight to `TNavMenuItem.icon`, or rendered into an `#icon` slot",
5256
+ 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."
5257
+ },
5258
+ {
5259
+ component: "TFlag",
5260
+ when: "The graphic is a country flag rendered from an ISO 3166-1 alpha-2 code."
3879
5261
  }
3880
5262
  ]
3881
5263
  },
3882
5264
  setup: {
3883
- required: [],
3884
- notes: []
5265
+ required: [
5266
+ "Use a registered name: one of the built-ins, or one the app added with `registerTreeIcons`. See `icons` above."
5267
+ ],
5268
+ notes: [
5269
+ '`size` is a pixel count, not `sm | md | lg`. Passing `size="md"` renders an unsized svg.',
5270
+ "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."
5271
+ ]
3885
5272
  },
3886
- recipes: []
5273
+ recipes: [
5274
+ "nav-menu-icons"
5275
+ ],
5276
+ practices: []
3887
5277
  },
3888
5278
  {
3889
5279
  name: "TInput",
@@ -4009,6 +5399,10 @@ var treeui_catalog_default = {
4009
5399
  recipes: [
4010
5400
  "form-field-input",
4011
5401
  "tag-input"
5402
+ ],
5403
+ practices: [
5404
+ "interaction-feedback",
5405
+ "predictable-api"
4012
5406
  ]
4013
5407
  },
4014
5408
  {
@@ -4046,7 +5440,245 @@ var treeui_catalog_default = {
4046
5440
  required: [],
4047
5441
  notes: []
4048
5442
  },
4049
- recipes: []
5443
+ recipes: [],
5444
+ practices: []
5445
+ },
5446
+ {
5447
+ name: "TLanguageSelect",
5448
+ aliases: [],
5449
+ package: "@treeui/vue",
5450
+ kind: "form_overlay",
5451
+ category: "Data Entry",
5452
+ status: "available",
5453
+ 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.",
5454
+ sourceFile: "packages/vue/src/components/TLanguageSelect.vue",
5455
+ storyFile: "apps/docs/src/stories/LanguageSelect.stories.ts",
5456
+ storyTitle: "Components/Data Entry/LanguageSelect",
5457
+ manifestFile: "docs/ai/COMPONENTS/language-select.yaml",
5458
+ props: [
5459
+ {
5460
+ name: "modelValue",
5461
+ type: "string",
5462
+ default: ""
5463
+ },
5464
+ {
5465
+ name: "options",
5466
+ type: "TLanguageOption[]",
5467
+ default: "[]",
5468
+ 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."
5469
+ },
5470
+ {
5471
+ name: "open",
5472
+ type: "boolean",
5473
+ default: "undefined"
5474
+ },
5475
+ {
5476
+ name: "defaultOpen",
5477
+ type: "boolean",
5478
+ default: "false"
5479
+ },
5480
+ {
5481
+ name: "size",
5482
+ type: "TSize",
5483
+ default: "md",
5484
+ values: [
5485
+ "sm",
5486
+ "md",
5487
+ "lg"
5488
+ ]
5489
+ },
5490
+ {
5491
+ name: "variant",
5492
+ type: "TLanguageSelectVariant",
5493
+ default: "field",
5494
+ values: [
5495
+ "field",
5496
+ "switcher"
5497
+ ],
5498
+ 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."
5499
+ },
5500
+ {
5501
+ name: "iconOnly",
5502
+ type: "boolean",
5503
+ default: "false",
5504
+ 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`."
5505
+ },
5506
+ {
5507
+ name: "width",
5508
+ type: "TFieldWidth",
5509
+ default: "full",
5510
+ values: [
5511
+ "xs",
5512
+ "sm",
5513
+ "md",
5514
+ "lg",
5515
+ "xl",
5516
+ "full"
5517
+ ],
5518
+ 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."
5519
+ },
5520
+ {
5521
+ name: "disabled",
5522
+ type: "boolean",
5523
+ default: "false"
5524
+ },
5525
+ {
5526
+ name: "loading",
5527
+ type: "boolean",
5528
+ default: "false"
5529
+ },
5530
+ {
5531
+ name: "invalid",
5532
+ type: "boolean",
5533
+ default: "false"
5534
+ },
5535
+ {
5536
+ name: "placeholder",
5537
+ type: "string",
5538
+ default: ""
5539
+ },
5540
+ {
5541
+ name: "flagBaseUrl",
5542
+ type: "string",
5543
+ default: "undefined",
5544
+ note: "Forwarded to every nested TFlag. When omitted, TFlag's own default origin applies. Set once here to self-host flag assets."
5545
+ },
5546
+ {
5547
+ name: "emptyText",
5548
+ type: "string",
5549
+ default: "No languages available."
5550
+ }
5551
+ ],
5552
+ events: [
5553
+ {
5554
+ name: "update:modelValue"
5555
+ },
5556
+ {
5557
+ name: "update:open"
5558
+ },
5559
+ {
5560
+ name: "open-change"
5561
+ }
5562
+ ],
5563
+ slots: [
5564
+ {
5565
+ name: "prefix",
5566
+ description: "Leading content inside the trigger, before everything else \u2014 the selected flag and label in `field`, the translate icon in `switcher`."
5567
+ },
5568
+ {
5569
+ name: "option",
5570
+ description: `Replaces the default option body (flag, label, description). Renders INSIDE the row's <li role="option">, so it must contain only presentational content.
5571
+ `
5572
+ },
5573
+ {
5574
+ name: "empty",
5575
+ description: "Replaces `emptyText` when `options` is empty."
5576
+ }
5577
+ ],
5578
+ a11y: [
5579
+ "role: listbox",
5580
+ "trigger_aria_haspopup: listbox",
5581
+ "trigger_aria_expanded: true",
5582
+ "trigger_aria_controls_when_open: true",
5583
+ "aria_invalid_when_invalid: true",
5584
+ "aria_busy_when_loading: true",
5585
+ "aria_selected_on_options: true",
5586
+ "aria_disabled_on_disabled_options: true",
5587
+ "focus_visible_on_trigger: true",
5588
+ "keyboard_arrow_navigation: true",
5589
+ "escape_closes_dropdown: true",
5590
+ "enter_space_selects_option: true",
5591
+ "home_end_navigation: true",
5592
+ "home_end_target_first_last_enabled_option: true",
5593
+ "focus_restores_to_trigger_on_close: true",
5594
+ "icon_only_removes_the_visible_language_name: true",
5595
+ "icon_only_requires_a_consumer_supplied_aria_label_on_the_trigger: true",
5596
+ "translate_icon_and_flags_are_decorative_not_the_accessible_name: true"
5597
+ ],
5598
+ behavior: [
5599
+ "update_payload_type: string",
5600
+ "loading_does_not_disable_by_itself: true",
5601
+ "placeholder_shown_as_muted_text: true",
5602
+ "chevron_rotates_when_open: true",
5603
+ "check_icon_on_selected_option: true",
5604
+ "outside_click_closes_dropdown: true",
5605
+ "listbox_flips_above_trigger_when_space_below_is_insufficient: true",
5606
+ "options_without_a_code_render_without_a_flag: true",
5607
+ "empty_options_render_the_empty_slot_or_empty_text: true",
5608
+ "field_variant_leads_the_trigger_with_the_selected_flag: true",
5609
+ "field_variant_trigger_fills_its_container: true",
5610
+ "switcher_variant_leads_the_trigger_with_a_translate_icon: true",
5611
+ "switcher_variant_trails_the_current_flag_before_the_chevron: true",
5612
+ "switcher_variant_trigger_sizes_to_its_content: true",
5613
+ "switcher_variant_ignores_the_width_prop: true",
5614
+ "switcher_listbox_aligns_to_the_trigger_right_edge: true",
5615
+ "switcher_listbox_may_be_wider_than_the_trigger: true",
5616
+ "icon_only_applies_to_the_switcher_variant_only: true",
5617
+ "root_classes: t-language-select--field | t-language-select--switcher, plus t-language-select--icon-only"
5618
+ ],
5619
+ selection: {
5620
+ keywords: [
5621
+ "language select",
5622
+ "language picker",
5623
+ "locale",
5624
+ "locale picker",
5625
+ "i18n",
5626
+ "internationalization",
5627
+ "translation",
5628
+ "country flag"
5629
+ ],
5630
+ useWhen: [
5631
+ "The user is picking the interface language or locale.",
5632
+ '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.',
5633
+ '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`.',
5634
+ "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.",
5635
+ "Each language should render with its country flag, and optionally an endonym as a second line.",
5636
+ "The app owns the language list and applies the locale side effects itself."
5637
+ ],
5638
+ avoidWhen: [
5639
+ "The values are not languages or locales.",
5640
+ "The language list is long enough that the user must type to narrow it down.",
5641
+ "You expect TreeUI to supply the locale dataset \u2014 it ships none, so pass your own `options`."
5642
+ ],
5643
+ alternatives: [
5644
+ {
5645
+ component: "TSelect",
5646
+ when: "The values are ordinary application data rather than languages or locales."
5647
+ },
5648
+ {
5649
+ component: "TCombobox",
5650
+ when: "The language list is long enough that typing to narrow it down is required."
5651
+ },
5652
+ {
5653
+ component: "TFlag",
5654
+ when: "You only need to render one country flag, without a picker around it."
5655
+ }
5656
+ ]
5657
+ },
5658
+ setup: {
5659
+ required: [
5660
+ "Pass `options` as `{ label, value, code?, description?, disabled? }[]`. TreeUI ships no language, locale, or country dataset \u2014 the app owns its language list.",
5661
+ "Apply locale side effects (i18n locale, `lang` attribute, persistence) in the app layer; the component only reports the selected `value`.",
5662
+ "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."
5663
+ ],
5664
+ notes: [
5665
+ "`variant` is `field` (default) or `switcher`, typed as `TLanguageSelectVariant` and exported from `@treeui/vue`.",
5666
+ '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.',
5667
+ '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.',
5668
+ "A switcher sizes to its content, so `width` does not apply, and its listbox aligns to the trigger's right edge.",
5669
+ "`iconOnly` is switcher-only and drops the visible language name, leaving the translate icon and the current flag.",
5670
+ "`value` is an opaque application key, typically a BCP-47 tag such as `pt-BR`. TreeUI never parses or validates it.",
5671
+ "`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.",
5672
+ "Use `description` for a secondary line such as the endonym.",
5673
+ "Set `flagBaseUrl` once on the select rather than per option; it is forwarded to every nested `TFlag`.",
5674
+ "Bind `modelValue` to a `string`, and use `v-model:open` when the app controls open state.",
5675
+ "The CDN and Content-Security-Policy requirements under `TFlag` apply whenever any option carries a `code`."
5676
+ ]
5677
+ },
5678
+ recipes: [
5679
+ "language-switcher"
5680
+ ],
5681
+ practices: []
4050
5682
  },
4051
5683
  {
4052
5684
  name: "TLink",
@@ -4147,7 +5779,10 @@ var treeui_catalog_default = {
4147
5779
  required: [],
4148
5780
  notes: []
4149
5781
  },
4150
- recipes: []
5782
+ recipes: [],
5783
+ practices: [
5784
+ "interaction-feedback"
5785
+ ]
4151
5786
  },
4152
5787
  {
4153
5788
  name: "TMarkdownEditor",
@@ -4256,7 +5891,8 @@ var treeui_catalog_default = {
4256
5891
  required: [],
4257
5892
  notes: []
4258
5893
  },
4259
- recipes: []
5894
+ recipes: [],
5895
+ practices: []
4260
5896
  },
4261
5897
  {
4262
5898
  name: "TModal",
@@ -4404,7 +6040,11 @@ var treeui_catalog_default = {
4404
6040
  "Use `v-model:open` for the idiomatic Vue controlled pattern."
4405
6041
  ]
4406
6042
  },
4407
- recipes: []
6043
+ recipes: [],
6044
+ practices: [
6045
+ "accessible-by-default",
6046
+ "predictable-api"
6047
+ ]
4408
6048
  },
4409
6049
  {
4410
6050
  name: "TMultiSelect",
@@ -4563,7 +6203,8 @@ var treeui_catalog_default = {
4563
6203
  },
4564
6204
  recipes: [
4565
6205
  "searchable-multi-choice"
4566
- ]
6206
+ ],
6207
+ practices: []
4567
6208
  },
4568
6209
  {
4569
6210
  name: "TNavbar",
@@ -4661,6 +6302,9 @@ var treeui_catalog_default = {
4661
6302
  },
4662
6303
  recipes: [
4663
6304
  "sidebar-shell"
6305
+ ],
6306
+ practices: [
6307
+ "token-driven"
4664
6308
  ]
4665
6309
  },
4666
6310
  {
@@ -4753,10 +6397,13 @@ var treeui_catalog_default = {
4753
6397
  keywords: [
4754
6398
  "nav menu",
4755
6399
  "sidebar nav",
4756
- "section nav"
6400
+ "section nav",
6401
+ "nav icons",
6402
+ "sidebar rail"
4757
6403
  ],
4758
6404
  useWhen: [
4759
- "You need app-section navigation inside a shell or sidebar."
6405
+ "You need app-section navigation inside a shell or sidebar.",
6406
+ "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
6407
  ],
4761
6408
  avoidWhen: [
4762
6409
  "You need tabs for small panel switching."
@@ -4775,13 +6422,19 @@ var treeui_catalog_default = {
4775
6422
  setup: {
4776
6423
  required: [],
4777
6424
  notes: [
4778
- "When nested inside `TSidebar` or `TAppShell`, `collapsed` can be inherited from the sidebar provider."
6425
+ "When nested inside `TSidebar` or `TAppShell`, `collapsed` can be inherited from the sidebar provider.",
6426
+ "Pass `items` as `{ label, value, icon?, to?, badge?, description?, shortLabel?, disabled?, exact? }[]`.",
6427
+ "`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.",
6428
+ "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.",
6429
+ "Register any icon the app needs beyond the built-ins first \u2014 see `icons` above."
4779
6430
  ]
4780
6431
  },
4781
6432
  recipes: [
4782
6433
  "app-shell",
4783
- "sidebar-shell"
4784
- ]
6434
+ "sidebar-shell",
6435
+ "nav-menu-icons"
6436
+ ],
6437
+ practices: []
4785
6438
  },
4786
6439
  {
4787
6440
  name: "TNumberInput",
@@ -4888,7 +6541,8 @@ var treeui_catalog_default = {
4888
6541
  required: [],
4889
6542
  notes: []
4890
6543
  },
4891
- recipes: []
6544
+ recipes: [],
6545
+ practices: []
4892
6546
  },
4893
6547
  {
4894
6548
  name: "TPage",
@@ -4981,7 +6635,10 @@ var treeui_catalog_default = {
4981
6635
  required: [],
4982
6636
  notes: []
4983
6637
  },
4984
- recipes: []
6638
+ recipes: [],
6639
+ practices: [
6640
+ "content-alignment"
6641
+ ]
4985
6642
  },
4986
6643
  {
4987
6644
  name: "TPageHeader",
@@ -5075,7 +6732,10 @@ var treeui_catalog_default = {
5075
6732
  required: [],
5076
6733
  notes: []
5077
6734
  },
5078
- recipes: []
6735
+ recipes: [],
6736
+ practices: [
6737
+ "content-alignment"
6738
+ ]
5079
6739
  },
5080
6740
  {
5081
6741
  name: "TPagination",
@@ -5188,6 +6848,9 @@ var treeui_catalog_default = {
5188
6848
  },
5189
6849
  recipes: [
5190
6850
  "data-listing"
6851
+ ],
6852
+ practices: [
6853
+ "interaction-feedback"
5191
6854
  ]
5192
6855
  },
5193
6856
  {
@@ -5247,7 +6910,7 @@ var treeui_catalog_default = {
5247
6910
  slots: [
5248
6911
  {
5249
6912
  name: "trigger",
5250
- description: "Custom trigger element. Receives current open state."
6913
+ 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
6914
  },
5252
6915
  {
5253
6916
  name: "default",
@@ -5256,9 +6919,9 @@ var treeui_catalog_default = {
5256
6919
  ],
5257
6920
  a11y: [
5258
6921
  "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"]',
6922
+ '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
6923
  '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"]',
6924
+ '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
6925
  'dismiss: ["Outside click closes popover","Escape key closes popover"]'
5263
6926
  ],
5264
6927
  behavior: [],
@@ -5291,7 +6954,8 @@ var treeui_catalog_default = {
5291
6954
  },
5292
6955
  recipes: [
5293
6956
  "anchored-help"
5294
- ]
6957
+ ],
6958
+ practices: []
5295
6959
  },
5296
6960
  {
5297
6961
  name: "TPricing",
@@ -5355,7 +7019,8 @@ var treeui_catalog_default = {
5355
7019
  },
5356
7020
  recipes: [
5357
7021
  "pricing-section"
5358
- ]
7022
+ ],
7023
+ practices: []
5359
7024
  },
5360
7025
  {
5361
7026
  name: "TPricingCard",
@@ -5484,7 +7149,8 @@ var treeui_catalog_default = {
5484
7149
  },
5485
7150
  recipes: [
5486
7151
  "pricing-section"
5487
- ]
7152
+ ],
7153
+ practices: []
5488
7154
  },
5489
7155
  {
5490
7156
  name: "TProgress",
@@ -5546,7 +7212,10 @@ var treeui_catalog_default = {
5546
7212
  required: [],
5547
7213
  notes: []
5548
7214
  },
5549
- recipes: []
7215
+ recipes: [],
7216
+ practices: [
7217
+ "state-clarity"
7218
+ ]
5550
7219
  },
5551
7220
  {
5552
7221
  name: "TRadio",
@@ -5614,7 +7283,8 @@ var treeui_catalog_default = {
5614
7283
  required: [],
5615
7284
  notes: []
5616
7285
  },
5617
- recipes: []
7286
+ recipes: [],
7287
+ practices: []
5618
7288
  },
5619
7289
  {
5620
7290
  name: "TRadioGroup",
@@ -5689,7 +7359,8 @@ var treeui_catalog_default = {
5689
7359
  required: [],
5690
7360
  notes: []
5691
7361
  },
5692
- recipes: []
7362
+ recipes: [],
7363
+ practices: []
5693
7364
  },
5694
7365
  {
5695
7366
  name: "TSelect",
@@ -5844,6 +7515,10 @@ var treeui_catalog_default = {
5844
7515
  },
5845
7516
  recipes: [
5846
7517
  "chart-surface"
7518
+ ],
7519
+ practices: [
7520
+ "interaction-feedback",
7521
+ "predictable-api"
5847
7522
  ]
5848
7523
  },
5849
7524
  {
@@ -5954,7 +7629,10 @@ var treeui_catalog_default = {
5954
7629
  required: [],
5955
7630
  notes: []
5956
7631
  },
5957
- recipes: []
7632
+ recipes: [],
7633
+ practices: [
7634
+ "interaction-feedback"
7635
+ ]
5958
7636
  },
5959
7637
  {
5960
7638
  name: "TSidebar",
@@ -6102,7 +7780,8 @@ var treeui_catalog_default = {
6102
7780
  },
6103
7781
  recipes: [
6104
7782
  "sidebar-shell"
6105
- ]
7783
+ ],
7784
+ practices: []
6106
7785
  },
6107
7786
  {
6108
7787
  name: "TSkeleton",
@@ -6165,7 +7844,10 @@ var treeui_catalog_default = {
6165
7844
  required: [],
6166
7845
  notes: []
6167
7846
  },
6168
- recipes: []
7847
+ recipes: [],
7848
+ practices: [
7849
+ "state-clarity"
7850
+ ]
6169
7851
  },
6170
7852
  {
6171
7853
  name: "TSpacer",
@@ -6205,7 +7887,10 @@ var treeui_catalog_default = {
6205
7887
  required: [],
6206
7888
  notes: []
6207
7889
  },
6208
- recipes: []
7890
+ recipes: [],
7891
+ practices: [
7892
+ "content-alignment"
7893
+ ]
6209
7894
  },
6210
7895
  {
6211
7896
  name: "TSparkline",
@@ -6328,7 +8013,8 @@ var treeui_catalog_default = {
6328
8013
  },
6329
8014
  recipes: [
6330
8015
  "metric-dashboard"
6331
- ]
8016
+ ],
8017
+ practices: []
6332
8018
  },
6333
8019
  {
6334
8020
  name: "TSpinner",
@@ -6377,7 +8063,10 @@ var treeui_catalog_default = {
6377
8063
  required: [],
6378
8064
  notes: []
6379
8065
  },
6380
- recipes: []
8066
+ recipes: [],
8067
+ practices: [
8068
+ "state-clarity"
8069
+ ]
6381
8070
  },
6382
8071
  {
6383
8072
  name: "TSplit",
@@ -6453,7 +8142,10 @@ var treeui_catalog_default = {
6453
8142
  required: [],
6454
8143
  notes: []
6455
8144
  },
6456
- recipes: []
8145
+ recipes: [],
8146
+ practices: [
8147
+ "content-alignment"
8148
+ ]
6457
8149
  },
6458
8150
  {
6459
8151
  name: "TStack",
@@ -6554,9 +8246,13 @@ var treeui_catalog_default = {
6554
8246
  notes: []
6555
8247
  },
6556
8248
  recipes: [
8249
+ "brand-lockup",
6557
8250
  "tag-input",
6558
8251
  "data-listing",
6559
8252
  "chart-surface"
8253
+ ],
8254
+ practices: [
8255
+ "content-alignment"
6560
8256
  ]
6561
8257
  },
6562
8258
  {
@@ -6631,7 +8327,8 @@ var treeui_catalog_default = {
6631
8327
  required: [],
6632
8328
  notes: []
6633
8329
  },
6634
- recipes: []
8330
+ recipes: [],
8331
+ practices: []
6635
8332
  },
6636
8333
  {
6637
8334
  name: "TStat",
@@ -6753,7 +8450,8 @@ var treeui_catalog_default = {
6753
8450
  recipes: [
6754
8451
  "stat-grid",
6755
8452
  "metric-dashboard"
6756
- ]
8453
+ ],
8454
+ practices: []
6757
8455
  },
6758
8456
  {
6759
8457
  name: "TSteps",
@@ -6857,7 +8555,8 @@ var treeui_catalog_default = {
6857
8555
  "Pass explicit `status` per item only when the app should override the computed complete/current/upcoming logic."
6858
8556
  ]
6859
8557
  },
6860
- recipes: []
8558
+ recipes: [],
8559
+ practices: []
6861
8560
  },
6862
8561
  {
6863
8562
  name: "TSwitch",
@@ -6946,7 +8645,10 @@ var treeui_catalog_default = {
6946
8645
  required: [],
6947
8646
  notes: []
6948
8647
  },
6949
- recipes: []
8648
+ recipes: [],
8649
+ practices: [
8650
+ "interaction-feedback"
8651
+ ]
6950
8652
  },
6951
8653
  {
6952
8654
  name: "TTable",
@@ -6986,6 +8688,13 @@ var treeui_catalog_default = {
6986
8688
  type: "TTableSortState",
6987
8689
  required: false,
6988
8690
  description: "Controlled sort state. When provided, overrides internal sort tracking."
8691
+ },
8692
+ {
8693
+ name: "caption",
8694
+ type: "string",
8695
+ required: false,
8696
+ description: "Visible `<caption>` naming the table.",
8697
+ 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
8698
  }
6990
8699
  ],
6991
8700
  events: [
@@ -7010,7 +8719,9 @@ var treeui_catalog_default = {
7010
8719
  }
7011
8720
  ],
7012
8721
  a11y: [
7013
- "role: grid",
8722
+ "role: table (native). No explicit role is emitted.",
8723
+ '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.',
8724
+ "accessible_name: Use `caption` for a visible name or `aria-label` / `aria-labelledby` for an invisible one. Both land on the `<table>`.",
7014
8725
  "aria-sort: Set on sortable column headers (ascending, descending, none).",
7015
8726
  "keyboard: Sortable headers are focusable and activated with Enter or Space.",
7016
8727
  "scope: col on all th elements."
@@ -7047,6 +8758,9 @@ var treeui_catalog_default = {
7047
8758
  },
7048
8759
  recipes: [
7049
8760
  "data-listing"
8761
+ ],
8762
+ practices: [
8763
+ "state-clarity"
7050
8764
  ]
7051
8765
  },
7052
8766
  {
@@ -7153,6 +8867,9 @@ var treeui_catalog_default = {
7153
8867
  },
7154
8868
  recipes: [
7155
8869
  "router-backed-tabs"
8870
+ ],
8871
+ practices: [
8872
+ "interaction-feedback"
7156
8873
  ]
7157
8874
  },
7158
8875
  {
@@ -7233,6 +8950,10 @@ var treeui_catalog_default = {
7233
8950
  },
7234
8951
  recipes: [
7235
8952
  "tag-input"
8953
+ ],
8954
+ practices: [
8955
+ "predictable-api",
8956
+ "token-driven"
7236
8957
  ]
7237
8958
  },
7238
8959
  {
@@ -7297,6 +9018,12 @@ var treeui_catalog_default = {
7297
9018
  type: "boolean",
7298
9019
  default: "false",
7299
9020
  note: "Clip to a single line with an ellipsis."
9021
+ },
9022
+ {
9023
+ name: "preserveWhitespace",
9024
+ type: "boolean",
9025
+ default: "false",
9026
+ 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
9027
  }
7301
9028
  ],
7302
9029
  events: [],
@@ -7310,7 +9037,8 @@ var treeui_catalog_default = {
7310
9037
  ],
7311
9038
  behavior: [
7312
9039
  "semantic_element_controlled_by_as: true",
7313
- "token_driven_size_tone_weight: true"
9040
+ "token_driven_size_tone_weight: true",
9041
+ "truncate_takes_precedence_over_preserve_whitespace: true"
7314
9042
  ],
7315
9043
  selection: {
7316
9044
  keywords: [],
@@ -7322,7 +9050,10 @@ var treeui_catalog_default = {
7322
9050
  required: [],
7323
9051
  notes: []
7324
9052
  },
7325
- recipes: []
9053
+ recipes: [
9054
+ "brand-lockup"
9055
+ ],
9056
+ practices: []
7326
9057
  },
7327
9058
  {
7328
9059
  name: "TTextarea",
@@ -7439,7 +9170,8 @@ var treeui_catalog_default = {
7439
9170
  required: [],
7440
9171
  notes: []
7441
9172
  },
7442
- recipes: []
9173
+ recipes: [],
9174
+ practices: []
7443
9175
  },
7444
9176
  {
7445
9177
  name: "TTimeline",
@@ -7499,7 +9231,8 @@ var treeui_catalog_default = {
7499
9231
  required: [],
7500
9232
  notes: []
7501
9233
  },
7502
- recipes: []
9234
+ recipes: [],
9235
+ practices: []
7503
9236
  },
7504
9237
  {
7505
9238
  name: "TToast",
@@ -7562,7 +9295,8 @@ var treeui_catalog_default = {
7562
9295
  },
7563
9296
  recipes: [
7564
9297
  "confirm-destructive-action"
7565
- ]
9298
+ ],
9299
+ practices: []
7566
9300
  },
7567
9301
  {
7568
9302
  name: "TToggleGroup",
@@ -7679,7 +9413,8 @@ var treeui_catalog_default = {
7679
9413
  'Use `selectionMode="multiple"` for small visible multi-choice sets.'
7680
9414
  ]
7681
9415
  },
7682
- recipes: []
9416
+ recipes: [],
9417
+ practices: []
7683
9418
  },
7684
9419
  {
7685
9420
  name: "TTooltip",
@@ -7791,6 +9526,9 @@ var treeui_catalog_default = {
7791
9526
  },
7792
9527
  recipes: [
7793
9528
  "anchored-help"
9529
+ ],
9530
+ practices: [
9531
+ "accessible-by-default"
7794
9532
  ]
7795
9533
  },
7796
9534
  {
@@ -7803,7 +9541,7 @@ var treeui_catalog_default = {
7803
9541
  purpose: "Hierarchical tree view with expansion, keyboard navigation, and single or multiple selection.",
7804
9542
  sourceFile: "packages/vue/src/components/TTreeView.vue",
7805
9543
  storyFile: "apps/docs/src/stories/TreeView.stories.ts",
7806
- storyTitle: "Components/Navigation & Disclosure/TView",
9544
+ storyTitle: "Components/Navigation & Disclosure/TreeView",
7807
9545
  manifestFile: "docs/ai/COMPONENTS/tree-view.yaml",
7808
9546
  props: [
7809
9547
  {
@@ -7913,6 +9651,9 @@ var treeui_catalog_default = {
7913
9651
  },
7914
9652
  recipes: [
7915
9653
  "hierarchy-picker"
9654
+ ],
9655
+ practices: [
9656
+ "accessible-by-default"
7916
9657
  ]
7917
9658
  }
7918
9659
  ],
@@ -9069,7 +10810,8 @@ var formatComponentSummary = (component) => {
9069
10810
  "Setup",
9070
10811
  [...component.setup.required, ...component.setup.notes]
9071
10812
  ),
9072
- component.recipes.length > 0 ? `Recipes: ${component.recipes.join(", ")}` : void 0
10813
+ component.recipes.length > 0 ? `Recipes: ${component.recipes.join(", ")}` : void 0,
10814
+ component.practices.length > 0 ? `Practices: ${component.practices.join(", ")}` : void 0
9073
10815
  ].filter(Boolean);
9074
10816
  return lines.join("\n");
9075
10817
  };
@@ -9144,6 +10886,12 @@ var resourceList = [
9144
10886
  description: "Reusable multi-component composition recipes.",
9145
10887
  mimeType: "application/json"
9146
10888
  },
10889
+ {
10890
+ uri: "treeui://practices",
10891
+ name: "TreeUI practices",
10892
+ description: "Named UX practices and the components that follow them.",
10893
+ mimeType: "application/json"
10894
+ },
9147
10895
  {
9148
10896
  uri: "treeui://tokens",
9149
10897
  name: "TreeUI design tokens",
@@ -9177,6 +10925,9 @@ var readResourcePayload = (uri) => {
9177
10925
  if (uri === "treeui://recipes") {
9178
10926
  return catalog.recipes;
9179
10927
  }
10928
+ if (uri === "treeui://practices") {
10929
+ return catalog.practices;
10930
+ }
9180
10931
  if (uri === "treeui://tokens") {
9181
10932
  return catalog.tokens;
9182
10933
  }