@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,130 @@
1
+ # Input (`MInput`) โ€” Spec, Findings & Solutions
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Tier** | Atom (form control) |
6
+ | **Maturity** | ๐ŸŸข Stable (core input primitive) |
7
+ | **Source** | `@motadata/ui` โ†’ `ui/components/Input/Input.vue` (+ `InputNumber`, `InputSearch`, `InputGroup`); wraps Ant `a-input` |
8
+ | **Storybook** | `Atoms/Input` (Examples ยท Usage ยท Accessibility ยท Changelog) |
9
+ | **Registry** | [`../registry/input.json`](../registry/input.json) |
10
+ | **Family** | form controls โ€” almost always wrapped by **`FlotoFormItem`** (label + validation, 1783ร—, own entry TODO) |
11
+ | **Figma** | TODO |
12
+
13
+ ## Usage (product analytics)
14
+
15
+ - **`<MInput>` used 293ร—.** **`<FlotoFormItem>` used 1783ร—** โ€” the dominant form-field wrapper
16
+ (label + validation around a control).
17
+ - **`.material-input` 62ร—** (bottom-border style) ยท `full-border-text-area` 17ร— ยท
18
+ `full-bordered-addon-input` 6ร— ยท `no-border-input` 3ร— ยท `auto-height-input`/`text-lg-input` 2ร—.
19
+ - Kit siblings: `MInputNumber`, `MInputSearch`, `MInputGroup`.
20
+
21
+ ## Overview
22
+
23
+ The product's text input. `MInput` is a **type-router**: the `type` prop selects the rendered
24
+ control. v-model binds **`value` + `update`** (note: the event is `update`, not `input`).
25
+
26
+ | `type` | Renders |
27
+ | --- | --- |
28
+ | `text` (default) | `a-input` |
29
+ | `password` | `a-input` with `type=password` (no show/hide toggle โ€” see F2) |
30
+ | `number` | `MInputNumber` |
31
+ | `search` | `MInputSearch` (enter button, emits `search`) |
32
+ | `textarea` | `a-textarea` |
33
+ | `datetime` | date picker (separate concern) |
34
+
35
+ ## Anatomy
36
+
37
+ ```text
38
+ โ”Œโ”€ [prefix] โ”€ value โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ [suffix] โ”€โ” โ† optional prefix/suffix inside the field
39
+ โ”‚ addonBefore โ”‚ input โ”‚ addonAfter โ”‚ โ† optional attached segments
40
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
41
+ ```
42
+
43
+ Slots: **prefix ยท suffix ยท addonBefore ยท addonAfter ยท enterButton** (search).
44
+
45
+ ## Options
46
+
47
+ - **Type:** text ยท password ยท number (`MInputNumber`, 70ร— direct) ยท search ยท textarea (above).
48
+ - **Adornments:** `prefix`/`suffix` icons; `addonBefore`/`addonAfter` segments.
49
+ - **Clearable:** `allow-clear` (218ร— incl. pickers) โ€” an **ร—** to clear when there's a value.
50
+ - **maxlength:** native char cap (7ร—). *(No built-in char counter โ€” `show-count` is not
51
+ supported by this Ant 1.4 kit; the product's `show-count` usage is on other components.)*
52
+ - **Style classes (parallel API):** **`.material-input`** (62ร— โ€” bottom border only,
53
+ Material underline) ยท `full-border-text-area` (17ร—, bordered textarea) ยท
54
+ `full-bordered-addon-input` (6ร—) ยท `no-border-input` (3ร—, borderless e.g. inline edit) ยท
55
+ `auto-height-input` / `text-lg-input` (2ร— each).
56
+ - **States:** default ยท disabled ยท read-only ยท **error** (`.has-error` โ†’ red border `#f04e3e`,
57
+ applied by `FlotoFormItem` during validation).
58
+ - **Sizes:** Ant `small`/`default`/`large` exist (via `$attrs`) but are **effectively unused** โ€”
59
+ the product uses one height (`@input-height-base` 32px; search 36px).
60
+
61
+ ## Behaviors
62
+
63
+ - **v-model = `value` + `update`** โ€” bind `v-model` or `:value` + `@update`. Using `@input`
64
+ alone won't update the model (F1).
65
+ - **Search** (`type=search`): Enter emits `search`; an enter/search button shows.
66
+ - **Number** (`type=number`): `MInputNumber` with steppers.
67
+
68
+ ## Content & writing
69
+
70
+ Use a clear `placeholder` as a hint (not a substitute for a label โ€” the label comes from
71
+ `FlotoFormItem`). Don't put the field's name only in the placeholder.
72
+
73
+ ## Accessibility
74
+
75
+ - Native `<input>` / `<textarea>` โ€” correct roles, keyboard, and (via `FlotoFormItem`) a
76
+ programmatic label. Provide a label (`FlotoFormItem`) or `aria-label` for a bare input.
77
+ - โš ๏ธ **No visible focus ring** system-wide ([SF-001](../../findings/SF-001-focus-visible.md))
78
+ โ€” inputs strip `outline`.
79
+ - `placeholder` contrast: `--input-placeholder-color` is ~50% โ€” verify it meets contrast.
80
+
81
+ ## Findings & Inconsistencies
82
+
83
+ ### F1 โ€” v-model event is `update`, not `input` ยท Low ยท Documented
84
+
85
+ `MInput` uses `model: { event: 'update' }`. `v-model` works, but `@input` handlers won't fire
86
+ the model update โ€” use `@update` (or `v-model`). **Solution:** document loudly (done); align to
87
+ `input`/`update:value` in a future API pass.
88
+
89
+ ### F2 โ€” `type="password"` has no show/hide toggle ยท Low ยท Open
90
+
91
+ `type=password` falls through to `a-input[type=password]` (no visibility toggle). A separate
92
+ `PasswordInput` component exists for show/hide. **Solution:** route `type=password` to
93
+ `a-input-password` (Ant has it) or point users to `PasswordInput`.
94
+
95
+ ### F3 โ€” No visible focus indicator ยท High ยท Open *(a11y)* โ†’ see [SF-001](../../findings/SF-001-focus-visible.md)
96
+
97
+ Inputs strip the focus outline (system-wide).
98
+
99
+ ## Do / Don't
100
+
101
+ ### Do
102
+
103
+ - Wrap inputs in **`FlotoFormItem`** for a label + validation (the standard form pattern).
104
+ - Pick the `type` for the data (number/search/textarea/password); bind with `v-model`.
105
+ - Use `prefix`/`suffix` for context icons; `addonBefore`/`addonAfter` for units/protocols.
106
+
107
+ ### Don't
108
+
109
+ - Don't rely on `@input` to update the model โ€” it's `update` (F1).
110
+ - Don't use the placeholder as the only label.
111
+ - Don't expect a password show/hide toggle from `MInput type=password` (F2).
112
+
113
+ ## Related
114
+
115
+ `FlotoFormItem` (field wrapper โ€” label + validation; own entry TODO) ยท `MInputNumber` ยท
116
+ `MInputSearch` ยท `MInputGroup` ยท `LooseTags` (tag input) ยท date picker (`type=datetime`).
117
+
118
+ ## Changelog
119
+
120
+ - **2026-06-07** โ€” Added (decision-grade Usage from the start). Deep-dive of `Input.vue`
121
+ (293ร—): type-router (text/password/number/search/textarea), v-model `value`/`update`,
122
+ prefix/suffix/addon slots. Verified all types + adornments + the `.material-input` (62ร—,
123
+ bottom-border `0/0/1/0`) in Storybook. Findings F1 (`update` event), F2 (no password toggle),
124
+ F3 (focus โ†’ SF-001). Noted `FlotoFormItem` (1783ร—) as the field wrapper โ€” its own entry next.
125
+ - **2026-06-07** โ€” Deeper re-sweep (owner doubt re: missed variants). Added **Clearable**
126
+ (`allow-clear`, verified ร—) and **Error/validation state** (`.has-error` red border,
127
+ verified) stories; deepened the Usage page (per-type descriptions + real product examples,
128
+ adornments, styles/states, sizes). Corrected: **sizes are effectively unused** (one height);
129
+ **`show-count` char counter is NOT supported** by the Ant 1.4 kit (so not an Input feature);
130
+ `MInputNumber` used directly 70ร—.
@@ -0,0 +1,131 @@
1
+ # Link (`FlotoLink`) โ€” Spec, Findings & Solutions
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Tier** | Atom (navigation) |
6
+ | **Maturity** | ๐ŸŸข Stable |
7
+ | **Source** | `src/components/_base-app-link.vue` (global `FlotoLink`) |
8
+ | **Storybook** | `Atoms/Link` (Examples ยท Usage ยท Accessibility ยท Changelog) |
9
+ | **Registry** | [`../registry/link.json`](../registry/link.json) |
10
+ | **Family** | the Button family's **navigation** relative (the thing a button should *not* be used for) |
11
+ | **Figma** | TODO |
12
+
13
+ ## Usage (product analytics)
14
+
15
+ - **`<FlotoLink>` used 67ร— across 48 files.** `:to` (route) ~9ร— explicit (most pass it via
16
+ `v-bind`); `as-button` form used for navigation CTAs. Wraps logos, menu items, breadcrumbs,
17
+ and clickable values.
18
+
19
+ ## Overview
20
+
21
+ The product's **navigation** control. Two modes (one prop, `asButton`):
22
+
23
+ - **Default** โ†’ renders a **`RouterLink`** (`<a>`): an in-content/inline navigation link.
24
+ - **`as-button`** โ†’ renders an **`MButton`** that **pushes the route on click**: a
25
+ button-styled navigation CTA (takes any Button `variant`).
26
+
27
+ Everything except `asButton` passes through via `$attrs` (`to`, `target`, button props).
28
+
29
+ ## Anatomy
30
+
31
+ ```text
32
+ text link: โ€ฆgo to โŸจthe inventory listโŸฉโ€ฆ โ† <a href> (RouterLink), inherits text color
33
+ as-button: [ Go to settings ] โ† MButton; navigates via $router.push on click
34
+ ```
35
+
36
+ ## Options / API
37
+
38
+ - **`asButton`** (Boolean, default false) โ€” button-styled navigation vs a plain link.
39
+ - **`to`** (via `$attrs`) โ€” the route/target (router location or path).
40
+ - **`target`**, and (when `asButton`) any **MButton** prop (`variant`, `size`, โ€ฆ) โ€” all
41
+ passed through.
42
+ - Slot: link/button content.
43
+
44
+ ## Link variations (the full surface)
45
+
46
+ "Link" is broader than `FlotoLink`. The real variations in the product:
47
+
48
+ | Variation | How | Notes |
49
+ | --- | --- | --- |
50
+ | **Internal text link** | `FlotoLink` (RouterLink) | the default โ€” in-content/menu/breadcrumb navigation |
51
+ | **Navigation CTA** | `FlotoLink as-button` | button-styled internal navigation (F1) |
52
+ | **External / new-tab link** | **plain `<a href target="_blank">`** (147ร—) | `FlotoLink` is **internal-only** โ€” RouterLink can't resolve external URLs. โš ๏ธ Set `rel="noopener noreferrer"` ([SF-004](../../findings/SF-004-blank-rel-noopener.md)). |
53
+ | **In-table link style** | `class="k-link"` (25ร—) | subtle: `--page-text-color`, hover โ†’ pagination-active color (not brand-colored) |
54
+ | **Other class styles** | `resource-link` (8ร—, mostly legacy/commented), `link-label` (5ร—), `completed-link` (3ร—), `admin-page-link` (1ร—) | app-context link treatments |
55
+
56
+ Anchor appearance is also **context-scoped** (nav/header/dropdown/menu/steps style links
57
+ differently in `header.less`/`left-list.less`/`dropdown.less`/โ€ฆ ) โ€” chrome styling, not
58
+ reusable variants.
59
+
60
+ ## Behaviors
61
+
62
+ - **Default link** is a real `<a>` (RouterLink) โ†’ supports middle-click / open-in-new-tab.
63
+ - **`as-button`** navigates programmatically (`$router.push`) and only pushes if the target
64
+ differs from the current route.
65
+
66
+ ## Content & writing
67
+
68
+ Link text should name the destination ("the inventory list", "Settings") โ€” not "click here".
69
+
70
+ ## Accessibility
71
+
72
+ - **Default mode** is a true anchor โ€” correct link semantics, keyboard + new-tab support. โœ…
73
+ - โš ๏ธ **`as-button` is a `<button>`, not an anchor (F1):** it navigates via JS, so it loses
74
+ native link affordances (no `href`, no middle-click / open-in-new-tab / right-click menu)
75
+ and is announced as a button, not a link.
76
+ - **No visible focus ring** system-wide ([SF-001](../../findings/SF-001-focus-visible.md)).
77
+
78
+ ## Findings & Inconsistencies
79
+
80
+ ### F1 โ€” `as-button` navigates but isn't a real link ยท Medium ยท Open *(a11y/UX)*
81
+
82
+ `as-button` renders an `MButton` and calls `$router.push` on click โ€” so a *navigation* is
83
+ exposed as a *button* with no `href`. Users can't open it in a new tab / middle-click, and AT
84
+ announces "button" not "link". **Solution:** for navigation CTAs prefer a real anchor styled
85
+ as a button (RouterLink with a button class), so the href is present; reserve `as-button` for
86
+ cases where a true anchor isn't possible.
87
+
88
+ ### F2 โ€” Plain links have no built-in affordance ยท Low ยท Open
89
+
90
+ A default `FlotoLink` **inherits text color** โ€” there's no global link color/underline, so a
91
+ bare link can look like normal text. Most usages add `text-primary` (navy) for affordance.
92
+ **Solution:** consider a default link style (color + hover underline) so links are
93
+ distinguishable without per-use classes.
94
+
95
+ ### F3 โ€” No external-link component; `target="_blank"` lacks `rel` ยท Low ยท Open *(security)* โ†’ [SF-004](../../findings/SF-004-blank-rel-noopener.md)
96
+
97
+ `FlotoLink` is **internal-only** (RouterLink), so external/help/doc links are written as raw
98
+ `<a href target="_blank">` โ€” **147ร— across the app, none with `rel="noopener noreferrer"`**
99
+ (reverse-tabnabbing; mitigated by modern browsers but still flagged). **Solution:** a shared
100
+ `FlotoExternalLink` (or extend `FlotoLink` for an external `href`) that always emits
101
+ `target="_blank" rel="noopener noreferrer"`, plus a lint rule. Promoted to **SF-004**.
102
+
103
+ ## Do / Don't
104
+
105
+ ### Do
106
+
107
+ - Use `FlotoLink` for **navigation** (route/URL) โ€” inline links, menus, breadcrumbs, logos.
108
+ - Use `as-button` for a **prominent navigation CTA**; pass a Button `variant`.
109
+ - Give the link text that names the destination; add an affordance (`text-primary`) for inline links.
110
+
111
+ ### Don't
112
+
113
+ - Don't use a link for an **action** (save/delete/apply) โ€” use a **Button**.
114
+ - Don't rely on `as-button` where users expect to open in a new tab (F1).
115
+
116
+ ## Related
117
+
118
+ `MButton` (actions) ยท `MDropdown` (menus) ยท the Button family (this is its navigation member).
119
+
120
+ ## Changelog
121
+
122
+ - **2026-06-07** โ€” Added (decision-grade Usage from the start). Deep-dive of
123
+ `_base-app-link.vue` (67ร—/48 files): `RouterLink` default + `as-button` MButton route-push;
124
+ only own prop is `asButton`. Verified text link (`<a href>`) + as-button (navigating MButton)
125
+ in Storybook (RouterLink + `$router` stubbed in preview). Findings F1 (`as-button` not a real
126
+ link, a11y), F2 (no default link affordance). Closes the Button family's navigation relative.
127
+ - **2026-06-07** โ€” Thorough link-variation sweep (owner flagged possible missed variants).
128
+ Found and documented: **external links** (plain `<a target="_blank">`, 147ร—, FlotoLink is
129
+ internal-only) + **class-based link styles** (`k-link` 25ร—, `resource-link`, `link-label`, โ€ฆ);
130
+ added the **External link** story + variations table. New finding **F3** โ†’ promoted to
131
+ **SF-004** (`target="_blank"` links lack `rel="noopener noreferrer"`).
@@ -0,0 +1,139 @@
1
+ # LooseTags โ€” Spec, Findings & Solutions
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Tier** | Molecule (form input) |
6
+ | **Maturity** | ๐ŸŸข Stable (heavily used; a few small inconsistencies) |
7
+ | **Source** | `src/components/loose-tags.vue` (composes `MSelect mode="tags"` / `FlotoDropdownPicker` / `SelectedItemPills`) |
8
+ | **Storybook** | `Molecules/LooseTags` (Examples ยท Usage ยท Accessibility ยท Changelog) |
9
+ | **Registry** | [`../registry/loose-tags.json`](../registry/loose-tags.json) |
10
+ | **Family** | Tag family โ€” the **input** member (see [`tag.md`](./tag.md); classification per [D12](../../decisions/DECISIONS.md)) |
11
+ | **Figma** | TODO |
12
+
13
+ ## Usage (product analytics)
14
+
15
+ - **`<LooseTags>` used 94ร— across 80 files** โ€” one of the most-used form inputs.
16
+ - Dominant config is the **default editable mode with a `:counter`** (counter passed ~69ร—,
17
+ it scopes which existing tags are suggested).
18
+ - `asDropdown` ~10ร— ยท `tagType` ~24ร— ยท `userTagOnly` ~4ร— ยท `singleSelection` ~2ร—.
19
+
20
+ ## Overview
21
+
22
+ A free-form **tag input**: the user types a value and presses Enter to add a removable
23
+ teal pill; previously-used tags are offered as suggestions (fetched from the API). It is the
24
+ home of the teal `key:value` pills โ€” the default mode renders
25
+ `<MSelect mode="tags" class="loose-tags-input">`, and the `.loose-tags-input` context is what
26
+ paints the selected pills teal. v-model is an **array of strings**.
27
+
28
+ It renders one of **three modes**:
29
+
30
+ 1. **Default (editable)** โ€” `MSelect mode="tags"`; type-to-create + suggestions. *(Shown in
31
+ Storybook.)*
32
+ 2. **`disabled` (read-only)** โ€” `SelectedItemPills` teal pills, truncated with `+N`; no input,
33
+ no API call. *(Shown in Storybook.)*
34
+ 3. **`asDropdown`** โ€” a searchable, multi-select `FlotoDropdownPicker` rendered `as-input`.
35
+ *(Covered by the DropdownPicker spec.)*
36
+
37
+ ## Anatomy
38
+
39
+ ```text
40
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
41
+ โ”‚ โŒœweb-server ร—โŒŸ โŒœdatabase ร—โŒŸ โŒœprod ร—โŒŸ | typeโ€ฆ โ”‚ โ† teal pills (orange ร—) + free-text input
42
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
43
+ โ–ผ suggestions (existing tags, from API)
44
+ ```
45
+
46
+ ## Options / API
47
+
48
+ - **`value`** (Array, v-model) โ€” the tags. `model: { event: 'change' }`, so bind with
49
+ `v-model` or `:value` + `@change`.
50
+ - **`disabled`** (Boolean) โ€” read-only pills (mode 2).
51
+ - **`asDropdown`** (Boolean) โ€” searchable picker (mode 3).
52
+ - **`singleSelection`** (Boolean) โ€” caps selection to 1 (**only effective in `asDropdown`**
53
+ via `max-allowed-selection`).
54
+ - **`counter`** (Object `{ key }`) โ€” scopes suggestions; a `watch` refetches when it changes.
55
+ - **`tagType`** (String) / **`userTagOnly`** (Boolean) โ€” further filter the suggestion fetch.
56
+ - **`placeholder`** (String, default `' '`) โ€” **only used in `asDropdown`** (see F1).
57
+ - **`sm`** (Boolean, default true) โ€” **currently dead** (see F3).
58
+
59
+ Emits **`change`** (the normalized array). Slots: `clearIcon` (set to `times-circle`).
60
+
61
+ ## Behaviors
62
+
63
+ - **Default mode** normalizes on change: `lowercase` โ†’ `trim` โ†’ **de-duplicate** โ†’ drop empties.
64
+ - **`asDropdown` mode** only `trim`s + drops empties (**preserves case** โ€” see F2).
65
+ - **Suggestions** are fetched once on `created()` (when not `disabled`) and refetched when
66
+ `counter` changes.
67
+
68
+ ## Content & writing
69
+
70
+ Short, lowercase tag tokens (default mode lowercases anyway). Placeholder in the editable
71
+ mode is the fixed string **"Add Tags"** (the `placeholder` prop is ignored there โ€” F1).
72
+
73
+ ## Design tokens used
74
+
75
+ `--main-tags-bg-color` / `--main-tags-text-color` (teal pills, themed: `#cdf1ed`/`#218b81`
76
+ light โ†’ `#183a42`/`#2ec4b6` dark) ยท `--secondary-orange` (the pill remove ร—). Pill font is
77
+ **`JetBrains Mono`** (set on `.loose-tags-input`).
78
+
79
+ ## Accessibility
80
+
81
+ - Built on Ant Select (`mode="tags"`) โ€” combobox semantics; Enter adds, Backspace removes
82
+ the last pill, Arrow keys navigate suggestions.
83
+ - โš ๏ธ **No visible keyboard focus indicator** ([SF-001](../../findings/SF-001-focus-visible.md)).
84
+ - The pill remove ร— is Ant's control (orange); verify it exposes an accessible name.
85
+ - The native clear is hidden (`.ant-select-selection__clear { display: none }`), so removal
86
+ is per-pill only in the editable mode.
87
+
88
+ ## Findings & Inconsistencies
89
+
90
+ ### F1 โ€” `placeholder` prop ignored in the editable mode ยท Medium ยท Open
91
+
92
+ The default `MSelect` branch hardcodes `placeholder="Add Tags"`; the `placeholder` prop is
93
+ only wired to the `asDropdown` branch. Consumers setting `:placeholder` on a normal LooseTags
94
+ see no effect. **Solution:** bind `:placeholder="placeholder"` on the `MSelect` too (default
95
+ it to "Add Tags").
96
+
97
+ ### F2 โ€” Case handling differs by mode ยท Low ยท Open
98
+
99
+ Default mode **lowercases** every tag; `asDropdown` mode **preserves case**. The same
100
+ component yields differently-cased data depending on a boolean prop. **Solution:** pick one
101
+ normalization (or expose a `lowercase`/`normalize` prop) and apply it in both branches.
102
+
103
+ ### F3 โ€” `sm` prop + `size` computed are dead ยท Low ยท Open
104
+
105
+ `sm` (default true) drives a `size` computed returning `'small'`, but `size` is **never bound**
106
+ to the `MSelect`/picker โ€” so the input is always default size. **Solution:** bind `:size="size"`
107
+ or remove the dead prop/computed.
108
+
109
+ ### F4 โ€” Suggestion fetch has no error handling ยท Low ยท Open
110
+
111
+ `getAllTagsApi(...).then(...)` has **no `.catch`** โ€” a failed/offline request becomes an
112
+ unhandled rejection (visible as a 404 `pageerror` in Storybook). **Solution:** add `.catch`
113
+ to fall back to `tagOptions = []` (and optionally a non-blocking notice).
114
+
115
+ ## Do / Don't
116
+
117
+ ### Do
118
+
119
+ - Use LooseTags for capturing a **list of free-form or known tags** (v-model an array).
120
+ - Pass a `:counter` so suggestions are scoped to the relevant entity.
121
+ - Use `disabled` for read-only display of an existing tag list.
122
+
123
+ ### Don't
124
+
125
+ - Don't rely on the `placeholder` prop in the editable mode (F1) or on `sm` for sizing (F3).
126
+ - Don't assume case is preserved โ€” default mode lowercases (F2).
127
+ - Don't use `asDropdown` expecting this spec to cover it โ€” that path is the DropdownPicker.
128
+
129
+ ## Related
130
+
131
+ `MTag` / `MStatusTag` (the display members), `SelectedItemPills` (its read-only render),
132
+ `FlotoDropdownPicker` (its `asDropdown` render), `MSelect`. See the Tag family table in
133
+ [`tag.md`](./tag.md).
134
+
135
+ ## Changelog
136
+
137
+ - **2026-06-07** โ€” Added. Full deep-dive: 3 render modes mapped; teal pills + JetBrains Mono
138
+ verified in light + dark; findings F1โ€“F4; classified as the Tag family's input member (D12);
139
+ Storybook Examples + Usage/Accessibility/Changelog pages.
@@ -0,0 +1,88 @@
1
+ # Menu โ€” Spec, Findings & Solutions
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Tier** | Molecule (primitive) |
6
+ | **Maturity** | ๐ŸŸข Stable |
7
+ | **Source** | `@motadata/ui` `MMenu` / `MMenuItem` / `MMenuDivider` (the primitive) ยท `components/_base-grid-actions.vue` (`FlotoGridActions`, the context/action menu) ยท `MDropdown` (1ร— โ€” rich-text editor table options). |
8
+ | **Storybook** | Molecules/Menu |
9
+ | **Registry** | [`registry/menu.json`](../registry/menu.json) |
10
+ | **Family** | [Menu](../family-map.md) |
11
+
12
+ ## Why this is a family
13
+
14
+ **Menu** is the shared **primitive** โ€” a vertical list of selectable rows โ€” that several other
15
+ families are *built from*. It is catalogued in its own right (the building block), alongside its one
16
+ direct **usage** that isn't already a family of its own: the **context / action menu**.
17
+
18
+ ## The members
19
+
20
+ | Member | Source | Usage | What it is |
21
+ | --- | --- | --- | --- |
22
+ | **Menu** (primitive) | `MMenu` / `MMenuItem` / `MMenuDivider` | **13ร—** | a vertical list of selectable rows (icon + label), with dividers + danger/positive colours |
23
+ | **Context / Action menu** | `_base-grid-actions.vue` (`FlotoGridActions`) ยท `MDropdown` | grid rows ยท 1ร— | a **"โ‹ฏ" trigger** โ†’ an `MMenu` of **actions** (an action surface, not a value picker) |
24
+
25
+ ### Menu (the primitive)
26
+
27
+ - A panel of `MMenuItem`s โ€” each an **icon + label** row โ€” with **hover** (`--neutral-lighter`),
28
+ **selected** (`--primary` text on `--code-tag-background-color`), `MMenuDivider` (`--border-color`),
29
+ and **danger** (`--secondary-red`) / **positive** (`--secondary-green`) item colours.
30
+ - It is the building block beneath **Primary nav** & **Side menu** (Navigation), the **Dropdown picker**
31
+ (Organisms/DropdownPicker), and the **Context/Action menu** below. Reused โ€” never hand-rolled.
32
+
33
+ ### Context / Action menu
34
+
35
+ - The `FlotoGridActions` pattern: an `MPopover` (placement `bottomRight`) triggered by an
36
+ **ellipsis-v "โ‹ฏ"** icon, opening an `MMenu` of **action** items. Items are **permission-gated**
37
+ (create/edit/delete keys), support **dividers**, and colour **danger** actions red / **positive**
38
+ green. The lone `MDropdown` usage (editor table options) is the same shape via the kit component.
39
+ - It is an **action surface** ("do something") โ€” distinct from a value picker and from navigation.
40
+
41
+ ## Which menu? (decision)
42
+
43
+ 1. **A list of items to render inside something** (nav, picker, dropdown)? โ†’ **Menu** primitive.
44
+ 2. **A "โ‹ฏ" / button that opens a list of *actions*** on a row or object? โ†’ **Context / Action menu**.
45
+ 3. **Pick a *value* from options** (single/multi)? โ†’ **Dropdown picker** / **Select** (not here).
46
+ 4. **Go to a destination**? โ†’ **Navigation** (Primary nav / Side menu / Tabs).
47
+
48
+ ## Accessibility
49
+
50
+ - **Verify:** the trigger has an accessible name (the "โ‹ฏ" needs an `aria-label`), the open menu uses
51
+ **`role="menu"` / `role="menuitem"`**, **arrow-key** navigation + **Esc** to close, focus returns to
52
+ the trigger on close, and **danger** actions are not conveyed by colour alone (icon/label too).
53
+
54
+ ## Design tokens used
55
+
56
+ `--page-background-color` (menu bg) ยท `--border-color` (border / divider) ยท `--neutral-lighter`
57
+ (hover) ยท `--code-tag-background-color` (selected bg) ยท `--primary` (selected text) ยท
58
+ `--secondary-red` (danger) ยท `--secondary-green` (positive) ยท `--page-text-color`.
59
+
60
+ ## Findings & Inconsistencies
61
+
62
+ | # | Severity | Status | Finding |
63
+ | --- | --- | --- | --- |
64
+ | F1 | Low | Noted | The primitive is reproduced (the live `MMenu` needs Ant menu context); built from real tokens + `MIcon`. |
65
+ | F2 | Low (a11y) | Open | Verify `role=menu`/`menuitem`, arrow-key + Esc, trigger `aria-label`, focus return, non-colour-only danger. |
66
+ | F3 | Info | Noted | `MDropdown` appears only **1ร—** (editor); the product's standard action menu is `FlotoGridActions` (MPopover + MMenu). |
67
+
68
+ ## Do / Don't
69
+
70
+ - **Do** reuse the **Menu** primitive (and `FlotoGridActions` for row actions); colour danger actions
71
+ with `--secondary-red`; gate items by permission.
72
+ - **Don't** confuse a menu of **actions** with a **value picker** (use Dropdown picker / Select) or
73
+ with **navigation**; don't hand-roll a `<ul>` dropdown; don't rely on colour alone for danger.
74
+
75
+ ## Related components
76
+
77
+ **Navigation** (Primary nav / Side menu are built on `MMenu`) ยท **Dropdown picker** & **Select** (value
78
+ selection, built on the same primitive) ยท **Popover** (the floating container) ยท **Toolbars** (host the
79
+ "โ‹ฏ" action menu) ยท **Table** (grid rows host `FlotoGridActions`).
80
+
81
+ ## Changelog
82
+
83
+ - **2026-06-16** โ€” Added โ€” the **Menu** family: the **`MMenu` primitive** catalogued in its own right
84
+ (the building block beneath Primary nav, Side menu, the dropdown picker) **+** the **Context / Action
85
+ menu** (`FlotoGridActions` โ€” a "โ‹ฏ" trigger โ†’ `MMenu` of actions; the single `MDropdown` usage is the
86
+ same shape). Surfaced during the Navigation recheck (MMenu was "scoped out" as a primitive; the
87
+ context menu as an action surface) and promoted to real entries. Reproductions (real tokens + MIcon);
88
+ verified the primitive + the opened context menu render. Findings F1โ€“F3.
@@ -0,0 +1,176 @@
1
+ # Modal (`MModal`) โ€” Spec, Findings & Solutions
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Tier** | Organism (overlay) |
6
+ | **Maturity** | ๐ŸŸข Stable (core dialog) |
7
+ | **Source** | `@motadata/ui` โ†’ `ui/components/Modal/Modal.vue` (wraps Ant `a-modal`); confirm variant `src/components/_base-confirm-modal.vue` |
8
+ | **Storybook** | `Organisms/Modal` (Examples ยท Usage ยท Accessibility ยท Changelog) |
9
+ | **Registry** | [`../registry/modal.json`](../registry/modal.json) |
10
+ | **Family** | overlays โ€” `MModal` (dialog) ยท `FlotoConfirmModal` (confirm) ยท **`FlotoDrawer`** (side panel, 158ร— โ€” own entry) |
11
+ | **Figma** | TODO |
12
+
13
+ ## Usage (product analytics)
14
+
15
+ - **`MModal` 39ร—** ยท **`FlotoConfirmModal` 71ร—** (the confirm dialog) ยท **`FlotoDrawer` 158ร—**
16
+ (slide-in panel โ€” the most-used overlay) ยท `FlotoDrawerForm` 59ร—.
17
+ - ~40 app-specific `*Modal` / `*Drawer` composites (AnomalyModal, IncidentDetailsDrawer, โ€ฆ).
18
+
19
+ ## Overview
20
+
21
+ A centered **dialog** over a dimmed backdrop. `MModal` is the base; **`FlotoConfirmModal`** is
22
+ the confirm/destructive-action variant built on it. Open it via the **`trigger`** slot (which
23
+ gives an `open` fn) or, for the confirm, the **`open`** prop. Mounts in a portal on `body`.
24
+
25
+ ## Anatomy
26
+
27
+ ```text
28
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
29
+ โ”‚ Title โ”‚ โ† title slot (no ร— close โ€” see F1)
30
+ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
31
+ โ”‚ Body (default slot) โ”‚
32
+ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
33
+ โ”‚ [Cancel] [ Save ] โ”‚ โ† footer slot (cancel + success handlers)
34
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
35
+ (dimmed backdrop / mask)
36
+ ```
37
+
38
+ ## Options / API
39
+
40
+ **MModal:** `width` (px/%) ยท `centered` ยท `confirmLoading` (spinner on Ok during async) ยท
41
+ `preventAutoCloseOnConfirm` ยท `overlayClassName`. Slots: **`trigger`** (`{ open, close, toggle }`)
42
+ ยท `title` ยท default (body) ยท **`footer`** (`{ cancel, success }`). Emits **`success`** / **`cancel`**.
43
+
44
+ **FlotoConfirmModal:** `open` (Boolean, toggles) ยท `variant` (default `error`) ยท `width` (450) ยท
45
+ `successText` ('Yes') ยท `cancelText` ('No') ยท `hideIcon` ยท `iconShadow` ยท `disableAutoHide`.
46
+ Slots: `icon` ยท `message` ยท `header` ยท `cancel-action` / `confirm-action`. Emits `confirm` / `hide`.
47
+
48
+ ## Header pattern (the product convention)
49
+
50
+ Because MModal's built-in ร— is off, **real modals add their own header** in the `title` slot โ€”
51
+ a flex row with a **`text-primary` (navy) title** on the left and a **close ร—** (`MIcon name="times"`
52
+ wired to the modal's `hide()`) on the right. ~26 modals do this. Always include this header ร—
53
+ **or** a footer Cancel so there's a visible way out (F1).
54
+
55
+ ## Overlay variants (`overlay-class-name`)
56
+
57
+ Real, used modal style variants (defined in `src/design/modal.less`):
58
+
59
+ | Class | Effect | Usage |
60
+ | --- | --- | --- |
61
+ | `hide-footer` | removes the footer (read-only / detail modals) | 4ร— |
62
+ | `scrollable-modal` | fixed-height **flex-column** body (top 50px, rounded header) โ€” the body itself has **no `overflow`**, so wrap the content in a `flex:1; min-height:0; overflow-y:auto` child (else it spills out, F3) | several |
63
+ | `scrollable-modal.restrict-width` | caps width at 1020px | โ€” |
64
+ | `scrollable-modal.smaller-modal` | body height 50vh | 2ร— |
65
+ | `no-padding-modal` / `no-padding-confrim-modal` | strips content padding | 11ร— |
66
+
67
+ > `readable-content-overlay` was previously listed here โ€” it is a **Popover/Tooltip** overlay class
68
+ > (on `MPopover` 9ร— / `MTooltip` 5ร—, defined in `popover.less`), **not** a modal variant. Removed (see
69
+ > the correction note below).
70
+
71
+ The backdrop **blurs** the page behind it (`backdrop-filter: blur(3px)`).
72
+
73
+ ## Dimensions (verified)
74
+
75
+ | Part | Value | Notes |
76
+ | --- | --- | --- |
77
+ | Content corner radius | **16px** (regular) ยท **20px** (confirm, `@overlay-border-radius`) | confirm also has a 2px red/variant border |
78
+ | Header corner radius | **20px** (top corners) | note the 4px mismatch vs the 16px content โ€” a product inconsistency, kept faithful |
79
+ | Header padding | `5px 16px` | tight โ€” the title bar is short |
80
+ | Body padding | `24px` (Ant default โ€” not overridden in the kit) | a bare paragraph looks spacious; real forms fill it |
81
+ | Footer padding | `10px 16px` (Ant default; `.widget-form-modal` uses 12px) | |
82
+
83
+ All measured to match `src/design/modal.less`. The body's 24px is Ant's default โ€” modals look
84
+ right when filled with a real form (the demo uses a `FlotoForm`), but airy with just text.
85
+
86
+ ## Behaviors
87
+
88
+ - **Open/close:** trigger slot (`open`) or `open` prop; closes via the header ร— / footer
89
+ **Cancel** / **Escape** (verified) / `success`. **No built-in ร— and no backdrop-close**
90
+ (`closable`/`maskClosable` hardcoded false โ€” F1).
91
+ - **`destroyOnClose: true`** โ€” body content is re-created each open (fresh form state).
92
+ - **Confirm:** Cancel (`default`) on the left, the action on the right (matches the form-field
93
+ placement convention); `error` variant for destructive.
94
+
95
+ ## Content & writing
96
+
97
+ Title = a short noun/verb phrase ("Edit monitor", "Delete monitor?"). The confirm message
98
+ states the consequence ("This can't be undone."); the action button names the verb (**Delete**),
99
+ not "Yes/OK".
100
+
101
+ ## Accessibility
102
+
103
+ - Ant `a-modal` traps focus and restores it on close; **Escape closes** (works even though the
104
+ ร— is hidden). Renders with `role="dialog"`.
105
+ - โš ๏ธ **No visible close affordance besides the footer** (F1) โ€” provide a footer **Cancel** (or a
106
+ ร—) so mouse users aren't stuck relying on Escape.
107
+ - โš ๏ธ No visible focus ring inside ([SF-001](../../findings/SF-001-focus-visible.md)).
108
+
109
+ ## Findings & Inconsistencies
110
+
111
+ ### F1 โ€” No ร— close and no backdrop-close ยท Medium ยท Open *(UX/a11y)*
112
+
113
+ `Modal.vue` hardcodes `:closable="false"` and `:maskClosable="false"`, so there's **no
114
+ top-right ร—** and clicking the backdrop doesn't close. Escape works and the footer Cancel works,
115
+ but if a modal's footer omits a cancel, **mouse users have no discoverable way to dismiss it**.
116
+ **Solution:** make `closable` a prop (default a visible ร—), or require a footer Cancel; keep
117
+ `maskClosable` opt-in for forms (to avoid accidental data loss).
118
+
119
+ ### F2 โ€” No visible focus ring ยท High ยท Open *(a11y)* โ†’ [SF-001](../../findings/SF-001-focus-visible.md)
120
+
121
+ System-wide; matters inside dialogs where focus is trapped.
122
+
123
+ ### F3 โ€” `scrollable-modal` body has no overflow (content can spill) ยท Low ยท Open
124
+
125
+ `scrollable-modal` sets the body to a fixed height + `display:flex; flex-direction:column`, but
126
+ **doesn't set `overflow`** on the body itself โ€” so long content spilling directly into the body
127
+ **overflows the modal** instead of scrolling. The body's child must be the scroll container
128
+ (`flex:1; min-height:0; overflow-y:auto`). **Solution:** add `overflow:auto` (or a flex-1
129
+ scroll child) in the variant, or document the required structure (done). Caught in the catalog
130
+ (the demo overflowed until the content was wrapped in a scroll container).
131
+
132
+ ## Do / Don't
133
+
134
+ ### Do
135
+
136
+ - Use `MModal` for a focused task/dialog; **`FlotoConfirmModal`** for yes/no + destructive confirms.
137
+ - Always include a footer **Cancel** (so there's a visible way out โ€” F1).
138
+ - Use a **`FlotoDrawer`** instead when the content is long, or contextual to a record (details).
139
+
140
+ ### Don't
141
+
142
+ - Don't rely on a ร— or backdrop-click to close โ€” they're disabled (F1).
143
+ - Don't use a modal for content that's better inline or in a drawer.
144
+ - Don't write "Yes/OK" action labels โ€” name the verb (Delete, Save).
145
+
146
+ ## Related
147
+
148
+ `FlotoConfirmModal` (confirm) ยท **`FlotoDrawer`** / `FlotoDrawerForm` (side panel โ€” own entry) ยท
149
+ `MButton` (footer actions) ยท `FlotoFormItem` (fields inside).
150
+
151
+ ## Changelog
152
+
153
+ - **2026-06-07** โ€” Added (decision-grade Usage). Deep-dive of `Modal.vue` (39ร—) + the confirm
154
+ variant `_base-confirm-modal.vue` (71ร—): trigger-slot / `open`-prop opening; title/body/footer
155
+ slots; `width`/`centered`/`confirmLoading`. Registered `FlotoConfirmModal` in the preview;
156
+ verified Basic (trigger โ†’ dialog, 2 footer btns, backdrop) and Confirm (error, icon + Delete)
157
+ open correctly; Escape closes, no ร— (F1). Drawer (158ร—) flagged as the related own entry.
158
+ - **2026-06-08** โ€” Fidelity pass (owner flagged it didn't match the product). Found the real
159
+ modals add their **own header** (text-primary title + close ร—, ~26 modals) and use **overlay
160
+ variants** (`hide-footer` 4ร—, `scrollable-modal`+`restrict-width`/`smaller-modal`,
161
+ `no-padding-*` 11ร—) โ€” and the backdrop **blurs**. Reworked the
162
+ stories to the product header pattern + added **HideFooter** and **Scrollable** variant
163
+ stories; documented the header pattern + variants. Confirmed real usage (metric-explorer
164
+ Anomaly/Forecast/Outlier/Compare modals, error/detail modals; delete confirms in CRUD lists).
165
+ - **2026-06-11** โ€” Triple-check audit of the **whole overlay/dialog family** (owner: "make sure no
166
+ type of modal is missed"). Confirmed the family is complete at the **component level** โ€” MModal
167
+ (39ร—), FlotoConfirmModal (71ร—), FlotoDrawer (99ร—), FlotoDrawerForm (59ร—); `MDrawer` is internal
168
+ (wrapped by FlotoDrawer, 0 direct app use); `FlotoFormModal` doesn't exist; no programmatic
169
+ `Modal.confirm`/`$confirm` service; no lightbox/viewer/fullscreen mechanisms; all 42 custom
170
+ `*-modal/-drawer` files build on those 4 bases. Added the two **MModal variants that existed but
171
+ weren't showcased**: **`no-padding`** (body โ†’ 8px, full-bleed lists/grids; verified) and **`large
172
+ (restrict-width)`** (content forced to **1020px**; verified). **Correction:** `readable-content-overlay`
173
+ is a **popover/tooltip** class (on MPopover 9ร— / MTooltip 5ร—, defined in `popover.less`), **not** a
174
+ modal variant โ€” removed it from the modal variant list above. Open: the **Popover / Tooltip family**
175
+ (MPopover 35ร—, MTooltip 94ร—, MPopper 2ร—) is a separate overlay family **not yet catalogued** as its
176
+ own component.