@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.
- package/AGENTS.md +102 -0
- package/README.md +73 -0
- package/components/index.json +1270 -0
- package/components/recipes/README.md +41 -0
- package/components/recipes/recipes.json +922 -0
- package/components/registry/README.md +44 -0
- package/components/registry/_schema.json +47 -0
- package/components/registry/button.json +368 -0
- package/components/registry/checkbox.json +177 -0
- package/components/registry/data-viz-tooltips.json +409 -0
- package/components/registry/date-time-pickers.json +296 -0
- package/components/registry/drawer.json +222 -0
- package/components/registry/dropdown-picker.json +388 -0
- package/components/registry/filters.json +155 -0
- package/components/registry/form-item.json +281 -0
- package/components/registry/input.json +277 -0
- package/components/registry/link.json +186 -0
- package/components/registry/loose-tags.json +196 -0
- package/components/registry/menu.json +145 -0
- package/components/registry/modal.json +265 -0
- package/components/registry/navigation.json +425 -0
- package/components/registry/popover.json +216 -0
- package/components/registry/radio.json +238 -0
- package/components/registry/scheduler.json +188 -0
- package/components/registry/select.json +247 -0
- package/components/registry/severity.json +179 -0
- package/components/registry/switch.json +177 -0
- package/components/registry/table.json +275 -0
- package/components/registry/tabs.json +264 -0
- package/components/registry/tag.json +345 -0
- package/components/registry/tags-list.json +115 -0
- package/components/registry/toolbars.json +240 -0
- package/components/registry/tooltip.json +175 -0
- package/components/specs/README.md +72 -0
- package/components/specs/button.md +230 -0
- package/components/specs/checkbox.md +162 -0
- package/components/specs/data-viz-tooltips.md +93 -0
- package/components/specs/date-time-pickers.md +161 -0
- package/components/specs/drawer.md +162 -0
- package/components/specs/dropdown-picker.md +161 -0
- package/components/specs/filters.md +118 -0
- package/components/specs/form-item.md +130 -0
- package/components/specs/input.md +130 -0
- package/components/specs/link.md +131 -0
- package/components/specs/loose-tags.md +139 -0
- package/components/specs/menu.md +88 -0
- package/components/specs/modal.md +176 -0
- package/components/specs/navigation.md +181 -0
- package/components/specs/popover.md +118 -0
- package/components/specs/radio.md +144 -0
- package/components/specs/scheduler.md +133 -0
- package/components/specs/select.md +118 -0
- package/components/specs/switch.md +124 -0
- package/components/specs/table.md +115 -0
- package/components/specs/tabs.md +136 -0
- package/components/specs/tag.md +196 -0
- package/components/specs/tags-list.md +105 -0
- package/components/specs/toolbars.md +108 -0
- package/components/specs/tooltip.md +112 -0
- package/foundation/README.md +39 -0
- package/foundation/layout-shells.md +67 -0
- package/foundation/page-templates.md +69 -0
- package/foundation/panel-behaviours.md +61 -0
- package/foundation/screen-regions.md +62 -0
- package/index.js +75 -0
- package/layout/grid.json +34 -0
- package/layout/layouts.json +310 -0
- package/llms.txt +60 -0
- package/package.json +42 -0
- package/spec.manifest.json +407 -0
- package/tokens/README.md +125 -0
- package/tokens/component.json +34 -0
- package/tokens/kit-accents.json +14 -0
- package/tokens/primitive.json +130 -0
- package/tokens/purpose-map.json +67 -0
- package/tokens/semantic.dark.json +90 -0
- package/tokens/semantic.light.json +90 -0
- package/tokens/structural.json +35 -0
- 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.
|