@mtdt/observeops-ds-spec 0.1.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.
Files changed (79) hide show
  1. package/AGENTS.md +102 -0
  2. package/README.md +73 -0
  3. package/components/index.json +1270 -0
  4. package/components/recipes/README.md +41 -0
  5. package/components/recipes/recipes.json +922 -0
  6. package/components/registry/README.md +44 -0
  7. package/components/registry/_schema.json +47 -0
  8. package/components/registry/button.json +368 -0
  9. package/components/registry/checkbox.json +177 -0
  10. package/components/registry/data-viz-tooltips.json +409 -0
  11. package/components/registry/date-time-pickers.json +296 -0
  12. package/components/registry/drawer.json +222 -0
  13. package/components/registry/dropdown-picker.json +388 -0
  14. package/components/registry/filters.json +155 -0
  15. package/components/registry/form-item.json +281 -0
  16. package/components/registry/input.json +277 -0
  17. package/components/registry/link.json +186 -0
  18. package/components/registry/loose-tags.json +196 -0
  19. package/components/registry/menu.json +145 -0
  20. package/components/registry/modal.json +265 -0
  21. package/components/registry/navigation.json +425 -0
  22. package/components/registry/popover.json +216 -0
  23. package/components/registry/radio.json +238 -0
  24. package/components/registry/scheduler.json +188 -0
  25. package/components/registry/select.json +247 -0
  26. package/components/registry/severity.json +179 -0
  27. package/components/registry/switch.json +177 -0
  28. package/components/registry/table.json +275 -0
  29. package/components/registry/tabs.json +264 -0
  30. package/components/registry/tag.json +345 -0
  31. package/components/registry/tags-list.json +115 -0
  32. package/components/registry/toolbars.json +240 -0
  33. package/components/registry/tooltip.json +175 -0
  34. package/components/specs/README.md +72 -0
  35. package/components/specs/button.md +230 -0
  36. package/components/specs/checkbox.md +162 -0
  37. package/components/specs/data-viz-tooltips.md +93 -0
  38. package/components/specs/date-time-pickers.md +161 -0
  39. package/components/specs/drawer.md +162 -0
  40. package/components/specs/dropdown-picker.md +161 -0
  41. package/components/specs/filters.md +118 -0
  42. package/components/specs/form-item.md +130 -0
  43. package/components/specs/input.md +130 -0
  44. package/components/specs/link.md +131 -0
  45. package/components/specs/loose-tags.md +139 -0
  46. package/components/specs/menu.md +88 -0
  47. package/components/specs/modal.md +176 -0
  48. package/components/specs/navigation.md +181 -0
  49. package/components/specs/popover.md +118 -0
  50. package/components/specs/radio.md +144 -0
  51. package/components/specs/scheduler.md +133 -0
  52. package/components/specs/select.md +118 -0
  53. package/components/specs/switch.md +124 -0
  54. package/components/specs/table.md +115 -0
  55. package/components/specs/tabs.md +136 -0
  56. package/components/specs/tag.md +196 -0
  57. package/components/specs/tags-list.md +105 -0
  58. package/components/specs/toolbars.md +108 -0
  59. package/components/specs/tooltip.md +112 -0
  60. package/foundation/README.md +39 -0
  61. package/foundation/layout-shells.md +67 -0
  62. package/foundation/page-templates.md +69 -0
  63. package/foundation/panel-behaviours.md +61 -0
  64. package/foundation/screen-regions.md +62 -0
  65. package/index.js +75 -0
  66. package/layout/grid.json +34 -0
  67. package/layout/layouts.json +310 -0
  68. package/llms.txt +60 -0
  69. package/package.json +42 -0
  70. package/spec.manifest.json +407 -0
  71. package/tokens/README.md +125 -0
  72. package/tokens/component.json +34 -0
  73. package/tokens/kit-accents.json +14 -0
  74. package/tokens/primitive.json +130 -0
  75. package/tokens/purpose-map.json +67 -0
  76. package/tokens/semantic.dark.json +90 -0
  77. package/tokens/semantic.light.json +90 -0
  78. package/tokens/structural.json +35 -0
  79. package/tokens/variables.json +2018 -0
@@ -0,0 +1,118 @@
1
+ # Select (`MSelect`) — Spec, Findings & Solutions
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Tier** | Atom (form control) |
6
+ | **Maturity** | ⚠️ **Low-use** (2×) — the product's standard select is `FlotoDropdownPicker` |
7
+ | **Source** | `@motadata/ui` → `ui/components/Select/Select.vue` (wraps Ant `a-select`) |
8
+ | **Storybook** | `Atoms/Select` (Examples · Usage · Accessibility · Changelog) |
9
+ | **Registry** | [`../registry/select.json`](../registry/select.json) |
10
+ | **Family** | select cluster — `FlotoDropdownPicker` (the real select) · `LooseTags` · `MTreeSelect` · this |
11
+ | **Figma** | N/A (low-use; build `DropdownPicker` instead) |
12
+
13
+ ## Usage (product analytics)
14
+
15
+ - **`<MSelect>` used 2×** — both `mode="tags"` (in `LooseTags` and `object-tag-picker`).
16
+ - **`FlotoDropdownPicker` used 510×** — the de-facto select. **Use that**, not MSelect.
17
+ - `MTreeSelect` 0× · `MSelectOption`/`OptGroup` 0× direct.
18
+
19
+ ## Overview
20
+
21
+ The kit's raw **Ant `a-select`** wrapper. Full-featured (modes, search, clear, sizes,
22
+ loading), but the product standardized on `FlotoDropdownPicker` instead — so MSelect survives
23
+ only as the low-level primitive behind a couple of tag inputs. Documented for completeness;
24
+ **reach for `FlotoDropdownPicker`** for an actual select. v-model is `value` + `change`.
25
+
26
+ ## Options / API
27
+
28
+ - **`mode`** — `default` (single) · `multiple` (chips) · `tags` (free-type to create) · combobox.
29
+ - **`options`** — `{ value, text }` (label resolves `text||label||title||name`); or slot options.
30
+ - **`showSearch`** (default false) · **`allowClear`** (default **true**) · **`size`**
31
+ (small/default/large) · **`loading`** (chevron → spinner) · `disabled` · `maxTagCount` ·
32
+ `labelInValue` · `filterOption` (default case-insensitive on label) · `tokenSeparators`.
33
+ - **Slots:** default/`option` · `suffixIcon` · `clearIcon` · `removeIcon` ·
34
+ `menuItemSelectedIcon` · `notFoundContent` (→ `MNoData`) · `dropdownRender`.
35
+ - **Emits:** `change` (model) · `search` · `select` · `deselect` · `blur` · `focus` · more.
36
+
37
+ ## Behaviors
38
+
39
+ - **Single/multiple/tags** like Ant. **Multiple/tags** render removable chips in the field.
40
+ - **Search** (`show-search`) filters options on the label; empty → `MNoData`.
41
+ - **Loading** swaps the chevron for a spinner.
42
+
43
+ ## Accessibility
44
+
45
+ - Built on Ant `a-select` → it **IS a real combobox** (`role`, `aria-expanded`,
46
+ `aria-activedescendant`, listbox/options, keyboard open + type-ahead). **Notably, this
47
+ low-use component is *more* accessible than the 510×-used `FlotoDropdownPicker`** (which has
48
+ no combobox semantics — [SF-003](../../findings/SF-003-combobox-a11y.md)). A good reference
49
+ for what fixing SF-003 should achieve.
50
+ - ⚠️ No visible focus ring system-wide ([SF-001](../../findings/SF-001-focus-visible.md)).
51
+
52
+ ## Findings & Inconsistencies
53
+
54
+ ### F1 — Superseded by `FlotoDropdownPicker` · Medium · Documented
55
+
56
+ The product chose the custom `FlotoDropdownPicker` (510×) over MSelect (2×) — likely for the
57
+ virtualized menu, custom triggers, and inline-add. So MSelect is effectively legacy/low-level.
58
+ **Solution:** use `FlotoDropdownPicker` for selects (and `LooseTags` for tag inputs); keep
59
+ MSelect only as the Ant primitive. Don't expand its usage.
60
+
61
+ ### F2 — The accessible select is the unused one · Medium · Open *(a11y, ironic)*
62
+
63
+ MSelect (Ant) has proper combobox a11y; the widely-used `FlotoDropdownPicker` does not
64
+ ([SF-003](../../findings/SF-003-combobox-a11y.md)). **Solution:** when fixing SF-003, mirror
65
+ Ant `a-select`'s ARIA — or, longer term, consider consolidating onto an accessible base.
66
+
67
+ ### F3 — No visible focus ring · High · Open *(a11y)* → [SF-001](../../findings/SF-001-focus-visible.md)
68
+
69
+ System-wide.
70
+
71
+ ### F4 — Clear "×" is invisible (icon prefix mismatch) · Low · Open
72
+
73
+ `Select.vue` hardcodes the clear icon as **`<MIcon name="times-circle" type="fas">`** (Font
74
+ Awesome **solid**), but the product registers **only `fal` (light)** icons (639 `fal`, 0 `fas`
75
+ in `icons.js`). So `fas times-circle` isn't in the library and the clear control renders
76
+ **empty** (`<i class="ant-select-clear-icon"><!----></i>`, no svg) — the **clear × is blank**.
77
+ The chip **remove** × works (it uses `name="times"`, default `fal`). (`type="fas"` is used 3×:
78
+ `Select.vue`, `TreeSelect.vue`, `navbar.vue` — all latently broken.) **Solution:** drop
79
+ `type="fas"` (use the default `fal`), or register the `fas` solid set.
80
+
81
+ ### F5 — Clear "×" overlaps the dropdown chevron · Low · Open
82
+
83
+ When `allow-clear` shows the clear control, it sits **on top of** the custom `suffixIcon`
84
+ chevron (both absolutely positioned at the same right edge). Ant's clear masks the *default*
85
+ arrow with a white box, but MSelect's **custom chevron `MIcon` is wider** than that box, so the
86
+ chevron's left edge **pokes out beside the ×** (measured: chevron `left 123`, clear box
87
+ `left 129` — single; same in multiple). Result: the close icon appears *over/beside* the
88
+ dropdown icon. **Solution:** hide `.ant-select-arrow` while the clear is shown, or size the
89
+ clear mask to the custom chevron. **Net (F4+F5): MSelect's clear-all affordance is broken** —
90
+ low impact (MSelect is 2×, both `mode="tags"`, never with `allow-clear`). The catalog stories
91
+ therefore omit `allow-clear` and show removal via the working chip ×; for a clearable select
92
+ use **`FlotoDropdownPicker`**.
93
+
94
+ ## Do / Don't
95
+
96
+ ### Do
97
+
98
+ - For a real select, use **`FlotoDropdownPicker`** (single/multi/search/virtualized/custom trigger).
99
+ - For a free-form tag input, use **`LooseTags`**.
100
+ - Use MSelect only as the low-level Ant primitive when something specifically needs raw `a-select`.
101
+
102
+ ### Don't
103
+
104
+ - Don't introduce new `MSelect` usages as "the select" — that's `FlotoDropdownPicker`.
105
+ - Don't reach for `mode="tags"` here — use `LooseTags`.
106
+
107
+ ## Related
108
+
109
+ `FlotoDropdownPicker` (the product select) · `LooseTags` (tag input) · `MTreeSelect` (tree) ·
110
+ `MSelectOption`/`MSelectOptGroup` · `FlotoFormItem` (wrap it for label + validation).
111
+
112
+ ## Changelog
113
+
114
+ - **2026-06-07** — Added (badged **low-use**, decision-grade Usage). Deep-dive of `Select.vue`:
115
+ Ant `a-select` wrapper — modes (default/multiple/tags), `show-search`, `allow-clear` (default
116
+ true), sizes, loading, rich slots (`notFoundContent` → `MNoData`); v-model `value`/`change`.
117
+ Verified single/multiple/tags render. Findings F1 (superseded by DropdownPicker, 2× vs 510×),
118
+ **F2** (ironically the *accessible* combobox vs SF-003), F3 (focus → SF-001).
@@ -0,0 +1,124 @@
1
+ # Switch (`MSwitch`) — Spec, Findings & Solutions
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Tier** | Atom |
6
+ | **Maturity** | 🟢 Stable (core) — open a11y findings (F3, F4) |
7
+ | **Source** | `@motadata/ui` kit → `ui/components/Switch/Switch.vue` (wraps Ant `a-switch`). Not overridden; not excluded. |
8
+ | **Storybook** | `Atoms/Switch` (Examples · Usage · Accessibility · Changelog) |
9
+ | **Registry** | [`../registry/switch.json`](../registry/switch.json) |
10
+ | **Figma** | TODO |
11
+
12
+ ## Usage (product analytics)
13
+
14
+ - **`<MSwitch>` used 136× across 88 files.**
15
+ - `size="small"` ~76 (approx, shared prop). checked/unchecked **label slots: 0 files** (unused).
16
+
17
+ ## Overview
18
+
19
+ An **instant** on/off toggle for a setting (applies immediately — no Save). Wraps Ant
20
+ `a-switch`; **controlled** (bind `v-model` / `:checked`).
21
+
22
+ ## Anatomy
23
+
24
+ ```text
25
+ off: ◖○────────◗ gray knob, white track
26
+ on: ◖────────●◗ green knob, white track ← track color unchanged; knob signals state
27
+ ```
28
+
29
+ Track · knob (gray→green) · optional `#checked` / `#unchecked` label slots (unused).
30
+
31
+ ## Options / States
32
+
33
+ - **Sizes:** `default` (22×44px) · `small`.
34
+ - **States:** off · on · disabled · loading (controlled via `:checked`/`v-model`).
35
+
36
+ **Context re-check (2026-06-07):** swept all switch styling (kit override, `form.less`,
37
+ `table.less`, `input.less`) for hidden/context variants like Radio's segmented set. Result:
38
+ **none** — no consumer `*-switch` classes exist, and the table/form rules only set the track
39
+ token (`--switch-bg`) + alignment. The track stays static (`--switch-bg`/white) in both states;
40
+ only the knob colors green (verified: track `#fff`, knob `#14b053`) — this is product-accurate
41
+ (F2). The switch surface is complete (no segmented/variant explosion).
42
+
43
+ ## Behaviors
44
+
45
+ - **Controlled:** always reflects `:checked`; toggling emits `change(value)`.
46
+ - **Loading:** `:loading` shows a spinner in the knob during async persistence.
47
+ - **Track is static color:** only the knob changes (gray off → green on) + slides position.
48
+
49
+ ## Content & writing
50
+
51
+ Pair with a label naming what turns on ("Enable notifications"). On/off text slots exist
52
+ but the product doesn't use them.
53
+
54
+ ## Accessibility
55
+
56
+ - **Semantics:** `role="switch"`; Space/Enter toggle. ✅
57
+ - **State:** `aria-checked="true"` when on ✅; **missing when off** (F4).
58
+ - **Target size:** ~22px tall — below the 44–48px touch target (F5).
59
+ - **Focus:** no visible focus ring (F3).
60
+ - **Theming:** track/border theme in dark (`#fff→#172336`); knob colors (`#a5bad0` off,
61
+ `#14b053` on) are static but read acceptably in both themes.
62
+
63
+ ## Props / API
64
+
65
+ `checked` (v-model) · `size` (default/small) · `disabled` · `loading` · `defaultChecked`
66
+ (**no-op — F1**) · `autoFocus`. Slots `checked` / `unchecked`. Emits `change`.
67
+ Machine spec: [`../registry/switch.json`](../registry/switch.json).
68
+
69
+ ## Design tokens used
70
+
71
+ Kit LESS vars (static): `@white` (track) · `@neutral-light` (off knob/border) ·
72
+ `@secondary-green` (on knob). Track/border also pick up CSS-var theming in dark.
73
+
74
+ ## Findings & Inconsistencies
75
+
76
+ ### F1 — `defaultChecked` is a no-op · Medium · Open
77
+
78
+ `MSwitch` always passes `:checked` (default `false`) to `a-switch`, which overrides
79
+ `:defaultChecked` → the switch never starts on via `defaultChecked`. **Solution:** use
80
+ `:checked`/`v-model`; or in the kit, don't bind `:checked` when uncontrolled. Document
81
+ that `defaultChecked` does nothing.
82
+
83
+ ### F2 — Track color doesn't change; state is knob-color-only · Medium · Open
84
+
85
+ Off and on share the same (white/dark) track; only the knob color (gray→green) + position
86
+ differ. Subtler than a track-fill switch and leans on color. **Solution:** confirm intent;
87
+ consider a checked **track** fill (e.g. `--severity-clear` tint) for a stronger signal.
88
+
89
+ ### F3 — No visible focus indicator · High · Open *(a11y)* → see SF-001
90
+
91
+ `outline: none`, no focus shadow (WCAG 2.4.7). **System-wide** — tracked as
92
+ [SF-001](../../findings/SF-001-focus-visible.md); fix once globally.
93
+
94
+ ### F4 — `aria-checked` absent in the OFF state · Medium · Open *(a11y)*
95
+
96
+ On exposes `aria-checked="true"`; off is null → AT may not announce "off". **Solution:**
97
+ ensure `aria-checked="false"` is set when unchecked.
98
+
99
+ ### F5 — 22px height below touch target · Low · Open *(a11y)*
100
+
101
+ **Solution:** ensure ≥44px hit area (padding) on touch surfaces.
102
+
103
+ ## Do / Don't
104
+
105
+ ### Do
106
+
107
+ - Use for **immediate** on/off settings; show `:loading` while persisting.
108
+ - Control via `v-model` / `:checked`.
109
+
110
+ ### Don't
111
+
112
+ - Don't use a switch in a form that needs explicit Save (use `MCheckbox`).
113
+ - Don't rely on `:defaultChecked` (F1).
114
+
115
+ ## Related components
116
+
117
+ `MCheckbox` (needs Save) · `MRadio` (mutually exclusive).
118
+
119
+ ## Changelog
120
+
121
+ - **2026-06-05** — Added (Playground · States · Sizes).
122
+ - **2026-06-06** — Full deep-dive: examples fixed to `:checked` (F1); on/off rendering
123
+ measured (white track + gray→green knob, light/dark). Enhanced standard + Option B pages;
124
+ a11y findings F3 (focus ring) + F4 (off-state aria-checked) + F5 (target size).
@@ -0,0 +1,115 @@
1
+ # Table / Grid (`MGrid`) — Spec, Findings & Solutions
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Tier** | Organism |
6
+ | **Maturity** | 🟢 Stable |
7
+ | **Source** | `src/components/crud/_base-grid.vue` (**MGrid**) — wraps the **Kendo Vue Grid** (`@progress/kendo-vue-grid`) + `gridWorker` / `arrayWorker`. Fetch+paginate wrapper: **`FlotoPaginatedCrud`**. |
8
+ | **Storybook** | Organisms/Table |
9
+ | **Registry** | [`registry/table.json`](../registry/table.json) |
10
+ | **Family** | [Data Table](../family-map.md) |
11
+
12
+ ## Usage analytics
13
+
14
+ - **MGrid 75×** across 69 files · **FlotoPaginatedCrud 76×** (the usual driver). Top modules:
15
+ settings, apm, ncm, slo, alert — i.e. **every list / inventory / drill-down view**.
16
+ - States in the wild (from the sweep): **expandable** 8× · **selectable** 7× · `hide-selection-info`
17
+ 5× · `disable-pre-selected-item` 4× · grouping (`default-group`). Classes: `hide-expand-column`
18
+ (6×), `hide-header`, `hide-grouping`, `tabular-content-grid`, `service-grid`, `rum-grid`.
19
+
20
+ ## Overview
21
+
22
+ The product's **primary data table**. `MGrid` wraps the Kendo grid and offloads sort/filter/group/
23
+ page to web workers; **`FlotoPaginatedCrud`** wraps `MGrid` to own fetch + paging + search + filters
24
+ for list pages. Features: resizable / reorderable / sortable columns, **client + server paging**,
25
+ **grouping**, **selection** (→ bulk-action bar), **expandable detail rows**, and **per-column cell
26
+ slots**. For a few static key/values, use a list/cards instead.
27
+
28
+ ## Anatomy
29
+
30
+ - **Header** — column titles on `--grid-header-bg`; sortable, resizable, reorderable. Optional
31
+ select-all checkbox.
32
+ - **Rows** — `--border-color` bottom divider; **hover** highlight; **selected** = `--neutral-lightest`
33
+ + a `--primary` left accent; optional **detail row** (expand); optional **group header** rows.
34
+ - **Footer** — pager (range + page controls) when `paging`.
35
+ - **Selection tag** — "N items selected" (`MTag`) above the grid unless `hide-selection-info`.
36
+
37
+ ## Options (key props)
38
+
39
+ | Prop | Default | Notes |
40
+ | --- | --- | --- |
41
+ | `columns` | — | column defs (field/title/width/cell/sortable) |
42
+ | `data` | — | rows (client mode) |
43
+ | `selectable` | `false` | checkbox column + selection |
44
+ | `expandable` | `false` | chevron + `detailRow` slot |
45
+ | `paging` / `default-page-size` | `false` / `50` | client paging |
46
+ | `external-take` / `external-skip` / `total-count` | — | **server paging** |
47
+ | `default-sort` / `default-group` | — | initial sort / grouping |
48
+ | `filters` / `search-term` / `use-search-term-loading` | — | filtering / search (+ spinner) |
49
+ | `max-allowed-selection` / `pre-selected-items` / `selection-disabled-items` / `disable-pre-selected-item` | — | selection control |
50
+ | `hide-selection-info` | `false` | hide the "N selected" tag |
51
+ | `row-height` | — | virtualization (non-paging) |
52
+
53
+ ## Behaviors
54
+
55
+ - **Workers:** sort/filter/group/page run in `gridWorker`/`arrayWorker`; the grid shows a
56
+ `FlotoContentLoader` while `processingData`.
57
+ - **Server vs client:** `external-*` + `total-count` + `@data-state-change` = server-side; otherwise
58
+ the grid pages/sorts in memory.
59
+ - **Columns:** resizable + reorderable; `@column-change` persists layout. Some columns are
60
+ sort-disabled (`SORT_DISABLED_COLUMNS`).
61
+
62
+ ## Content & writing
63
+
64
+ - Column titles short and consistent; empty → **"No records found"**.
65
+
66
+ ## Accessibility
67
+
68
+ - Verify Kendo grid + **custom cell slots** keep table roles (`row`/`columnheader`/`gridcell`),
69
+ **`aria-sort`** on sortable headers, **`aria-selected`** on selected rows, an accessible
70
+ select-all, and **focus rings** on cell controls (SF-001). Announce empty/loading via a live
71
+ region. See the Accessibility page (**F2**).
72
+
73
+ ## Props / API
74
+
75
+ See the table above and [`registry/table.json`](../registry/table.json).
76
+
77
+ ## Design tokens used
78
+
79
+ `--grid-header-bg` · `--border-color` · `--neutral-lightest` (selected) · `--neutral-lighter`
80
+ (group header) · `--primary` (selected accent) · `--neutral-dark` (header text) ·
81
+ `--page-background-color`.
82
+
83
+ ## Findings & Inconsistencies
84
+
85
+ | # | Severity | Status | Finding |
86
+ | --- | --- | --- | --- |
87
+ | F1 | Low | Open | Heavy: Kendo grid + 2 web workers — not trivially renderable outside the app (Storybook shows **reproductions**, not the live grid). |
88
+ | F2 | Medium (a11y) | Open | Verify table roles / `aria-sort` / `aria-selected` / focus survive the Kendo grid + custom cell slots. |
89
+
90
+ ## Recommended solutions
91
+
92
+ - **F1:** keep the catalog as faithful reproductions; if a live grid is wanted later, sandbox the
93
+ Kendo grid with mocked workers in a dedicated story.
94
+ - **F2:** audit the rendered grid for ARIA (sort/selected/roles) and adopt the SF-001 focus ring on
95
+ cell controls.
96
+
97
+ ## Do / Don't
98
+
99
+ - **Do** use `FlotoPaginatedCrud` for fetched lists; server-page large data; row actions in a kebab;
100
+ multi-select via the bulk bar; keep the header in empty/loading.
101
+ - **Don't** use a grid for a few static key/values; don't client-load huge datasets; don't
102
+ hand-roll selection/sorting/paging.
103
+
104
+ ## Related components
105
+
106
+ `FlotoPaginatedCrud` · `FlotoGridActions` (row kebab) · bulk-action bar · `MStatusTag` / `MTag`
107
+ (cells) · `MCheckbox` (selection) · `FlotoDropdownPicker` (filters).
108
+
109
+ ## Changelog
110
+
111
+ - **2026-06-12** — Added (decision-grade Usage). `/component-sweep` on `MGrid` (75×) +
112
+ `FlotoPaginatedCrud` (76×): Kendo grid + workers; states selectable/expandable/grouping/paging;
113
+ per-column cell slots. 8 reference-reproduction stories (Basic · Selectable · Expandable · Cell
114
+ types · Grouping · Empty · Loading · Pagination) on the real `table.less` chrome; verified render
115
+ + the selected-row accent. Findings F1 (heavy/reproductions), F2 (a11y).
@@ -0,0 +1,136 @@
1
+ # Tabs (`MTab` / `MTabPane`) — Spec, Findings & Solutions
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Tier** | Molecule |
6
+ | **Maturity** | 🟢 Stable |
7
+ | **Source** | `ui/components/Tabs/Tab.vue` (`MTab` — wraps Ant `a-tabs`) + `ui/components/Tabs/TabPane.vue` (`MTabPane`). Persisted state: `src/components/_base-persisted-tab.vue` (`MPersistedTab` — renderless). |
8
+ | **Storybook** | Molecules/Tabs |
9
+ | **Registry** | [`registry/tabs.json`](../registry/tabs.json) |
10
+ | **Family** | [Navigation / Tabs](../family-map.md) |
11
+
12
+ ## Usage analytics
13
+
14
+ - **86×** across **70** files. Top areas: `settings` (20), shared `components` (17), `rum` (7),
15
+ `alert` (5), `log` (4), `apm` (3).
16
+ - **Always line / top / default.** No real `<MTab>` passes `type=`, `position=`, or `size=` — so
17
+ despite Ant supporting **card / editable-card** types, **vertical** position, **size** variants,
18
+ and a **`tabBarExtraContent`** slot, the product uses **none** of them.
19
+ - **Variant classes (the real API):** **`no-border`** (21×, dominant) · **`sticky-tab`** (8×) ·
20
+ `topology-hierarchy-tab` (3×, icon-only) · `metric-picker-tabs` (1×) · `grey-tab` (defined, ~0) ·
21
+ `flex-tabs` (content flex helper).
22
+ - **Label patterns:** **count appended to the label string** (`Alerts (${count})`,
23
+ `${tab.title} (${getCounts(tab)})`) — *not* a separate badge; **leading icon** via the pane's
24
+ `tab` slot; **dynamic** `v-for` over a tabs config array.
25
+
26
+ ## Overview
27
+
28
+ In-page **tabbed navigation** that switches between **sibling views of one context** (Overview /
29
+ Performance / Logs of a monitor; Attributes / Metric / Style of a widget editor). `MTab` wraps Ant
30
+ `a-tabs` and applies its `variant` prop **as a CSS class**; panes are `MTabPane`. For page-to-page
31
+ navigation use the router/menu, not tabs.
32
+
33
+ ## Anatomy
34
+
35
+ - **Tab bar** — the strip of tab triggers; bottom border `--border-color` (removed by `no-border`).
36
+ - **Tab trigger** — active = `--primary` text + a `--primary` underline/ink bar; inactive =
37
+ `--tabs-text-color`; disabled = `--neutral-light`.
38
+ - **Tab pane** (`MTabPane`) — the content for the active key; label via the `tab` prop or `tab` slot.
39
+
40
+ ## Options (props)
41
+
42
+ ### `MTab`
43
+
44
+ | Prop | Default | Notes |
45
+ | --- | --- | --- |
46
+ | `value` | — | active key; `v-model` via `@change` (41× bind change) |
47
+ | `variant` | `''` | applied as a **class** on `a-tabs` — e.g. `no-border` |
48
+ | `defaultActive` | — | `defaultActiveKey` when uncontrolled |
49
+ | `animated` | `false` | slide animation between panes |
50
+ | `size` | `default` | **unused** in product |
51
+ | `position` | `top` | `tabPosition`; **unused** beyond `top` (no vertical tabs) |
52
+ | `type` | `line` | **unused** beyond `line` (no card/editable-card) |
53
+ | `tabBarGutter` / `tabBarStyle` | — | spacing / inline style of the bar |
54
+
55
+ ### `MTabPane`
56
+
57
+ | Prop | Default | Notes |
58
+ | --- | --- | --- |
59
+ | `tab` | — | the label **string** (use the `tab` **slot** for icon/count markup) |
60
+ | `key` | — | the pane key — matches `MTab` `value` |
61
+ | `forceRender` | `false` | render content even when the pane is inactive |
62
+
63
+ ### `MPersistedTab` (renderless)
64
+
65
+ | Prop | Default | Notes |
66
+ | --- | --- | --- |
67
+ | `moduleKey` | **required** | localStorage key → `${moduleKey}-tab` |
68
+ | `useLocalStorageTab` | `true` | persist the active tab |
69
+ | `defaultValue` / `value` | — | initial tab when nothing is stored |
70
+
71
+ Exposes a **scoped slot** `{ tab, setTab }` — wire to `<MTab :value="tab" @change="setTab">`.
72
+
73
+ ## Behaviors
74
+
75
+ - **Controlled** via `value` + `@change` (the dominant pattern) or **uncontrolled** via
76
+ `defaultActive`.
77
+ - **`no-border`** drops the bar's bottom rule (tabs on a card). **`sticky-tab`** pins the bar to the
78
+ top of a scroll container (`position:sticky; top:0; background:var(--page-background-color)`).
79
+ - **`MPersistedTab`** remembers the active tab across reloads/navigation in localStorage; its sibling
80
+ **`MPersistedColumns`** does the same for grid columns.
81
+
82
+ ## Content & writing
83
+
84
+ - Short noun labels (Overview, Performance, Logs). Append a count as `Label (N)` when it helps the
85
+ user gauge volume. Don't sentence-case or punctuate labels.
86
+
87
+ ## Accessibility
88
+
89
+ - **Provided by Ant `a-tabs`:** `role="tablist"` / `role="tab"` / `role="tabpanel"`,
90
+ **`aria-selected`** on the active tab, and **Left/Right arrow-key** navigation between tabs.
91
+ - **Verify:** keyboard **focus visibility** on the tab triggers (catalog-wide **SF-001**
92
+ `:focus-visible` ring); and that **icon-only** tabs carry a meaningful accessible name.
93
+
94
+ ## Design tokens used
95
+
96
+ `--primary` (active text + underline + ink bar) · `--tabs-text-color` (inactive) · `--border-color`
97
+ (bar) · `--page-background-color` (sticky bg) · `--neutral-light` (disabled / prev-next) ·
98
+ `--neutral-lightest` (`grey-tab` bar).
99
+
100
+ ## Findings & Inconsistencies
101
+
102
+ | # | Severity | Status | Finding |
103
+ | --- | --- | --- | --- |
104
+ | F1 | Low (a11y) | Open | Catalog-wide focus-ring removal (**SF-001**) may suppress the tab trigger's `:focus-visible` outline — verify keyboard focus is visible. |
105
+ | N1 | Info | — | Ant offers card/editable-card/vertical/size/`tabBarExtraContent`; the product uses none. Treat any of these as a **new pattern** requiring a deliberate decision, not a default. |
106
+
107
+ ## Recommended solutions
108
+
109
+ - **F1:** adopt the shared `:focus-visible` ring (SF-001) so tab triggers show a visible focus state.
110
+ - **N1:** keep the line/top/default constraint unless a product need justifies a new tab style; if
111
+ introduced, add it to this spec + a story so it's a documented variant, not an ad-hoc divergence.
112
+
113
+ ## Do / Don't
114
+
115
+ - **Do** use tabs for sibling views of one context; use `no-border` on cards; `sticky-tab` on long
116
+ panes; append `(N)` counts; build data-driven sets with `v-for`; use `MPersistedTab` for
117
+ remembered tabs.
118
+ - **Don't** use tabs for page navigation; don't introduce card/vertical/closable tabs without a
119
+ decision; don't build a separate count badge; don't depend on `tabBarExtraContent` to match the
120
+ product.
121
+
122
+ ## Related components
123
+
124
+ `FlotoDropdownPicker` (select) · `MGrid` / Table (tabbed detail panes often sit above a grid) ·
125
+ `MRadioGroup` `as-button` (a *segmented* control — a different "pick one" pattern, not view tabs).
126
+
127
+ ## Changelog
128
+
129
+ - **2026-06-15** — Added (decision-grade). Deep-dive of `MTab`/`MTabPane` (86× / 70 files) + the
130
+ renderless `MPersistedTab`. Established the **line/top/default-only** fidelity rule (no real usage
131
+ passes type/position/size) and the class-based variant API (`no-border` 21×, `sticky-tab` 8×).
132
+ Stories built with the **real** `MTab`/`MTabPane`: Basic · No border · Sticky · With counts ·
133
+ Dynamic · With icons · Persisted. Verified the active indicator renders **`--primary` navy**
134
+ (`rgb(17,28,44)`, text + underline + ink bar), interactivity (click switches panes), and no console
135
+ errors; caught + fixed a blank `map` icon (not in the 543-set → `sitemap`). Finding F1 (focus ring
136
+ → SF-001).