@maltjoy/mcp-server 0.4.0 → 0.6.0-beta.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/LICENSE +21 -0
- package/README.md +30 -3
- package/dist/doc/VJoyAdminBanner/guideline.md +284 -0
- package/dist/doc/VJoyAvailability/guideline.md +352 -0
- package/dist/doc/VJoyAvatar/guideline.md +192 -0
- package/dist/doc/VJoyAvatarsList/guideline.md +328 -0
- package/dist/doc/VJoyBadge/guideline.md +169 -0
- package/dist/doc/VJoyBadgeLevel/guideline.md +166 -0
- package/dist/doc/VJoyBlockSkeleton/guideline.md +150 -0
- package/dist/doc/VJoyBottomSheet/guideline.md +345 -0
- package/dist/doc/VJoyBottomSheetTrigger/guideline.md +156 -0
- package/dist/doc/VJoyButton/guideline.md +371 -0
- package/dist/doc/VJoyButton/metadata.json +0 -16
- package/dist/doc/VJoyButtonCard/guideline.md +384 -0
- package/dist/doc/VJoyCheckbox/guideline.md +317 -0
- package/dist/doc/VJoyCheckboxCard/guideline.md +381 -0
- package/dist/doc/VJoyCheckboxGroup/guideline.md +363 -0
- package/dist/doc/VJoyCheckboxGroup/metadata.json +7 -7
- package/dist/doc/VJoyCollapse/guideline.md +362 -0
- package/dist/doc/VJoyCollapseItem/guideline.md +394 -0
- package/dist/doc/VJoyCompanyAvatar/guideline.md +162 -0
- package/dist/doc/VJoyDialog/guideline.md +425 -0
- package/dist/doc/VJoyDialogTrigger/guideline.md +186 -0
- package/dist/doc/VJoyDot/guideline.md +177 -0
- package/dist/doc/VJoyDrawer/guideline.md +392 -0
- package/dist/doc/VJoyDrawerTrigger/guideline.md +145 -0
- package/dist/doc/VJoyDropdown/guideline.md +393 -0
- package/dist/doc/VJoyDropdown/metadata.json +6 -6
- package/dist/doc/VJoyDropdownList/guideline.md +396 -0
- package/dist/doc/VJoyDropdownList/metadata.json +7 -7
- package/dist/doc/VJoyDropzone/guideline.md +390 -0
- package/dist/doc/VJoyDropzone/metadata.json +36 -1
- package/dist/doc/VJoyFilterBar/guideline.md +419 -0
- package/dist/doc/VJoyFilterBarButton/guideline.md +359 -0
- package/dist/doc/VJoyFloatingActionsBar/guideline.md +372 -0
- package/dist/doc/VJoyFormError/guideline.md +325 -0
- package/dist/doc/VJoyFormError/metadata.json +0 -14
- package/dist/doc/VJoyFormFieldSkeleton/guideline.md +159 -0
- package/dist/doc/VJoyFormGroup/guideline.md +285 -0
- package/dist/doc/VJoyFunnel/guideline.md +349 -0
- package/dist/doc/VJoyFunnelFooter/guideline.md +178 -0
- package/dist/doc/VJoyFunnelHeader/guideline.md +204 -0
- package/dist/doc/VJoyHighlight/guideline.md +169 -0
- package/dist/doc/VJoyIcon/JoyIcon.types.ts +1 -1
- package/dist/doc/VJoyIcon/guideline.md +168 -0
- package/dist/doc/VJoyIcon/metadata.json +0 -1
- package/dist/doc/VJoyIconButton/guideline.md +169 -0
- package/dist/doc/VJoyIconButton/metadata.json +1 -1
- package/dist/doc/VJoyIndicator/guideline.md +168 -0
- package/dist/doc/VJoyIndicators/guideline.md +183 -0
- package/dist/doc/VJoyInput/JoyInput.types.ts +1 -6
- package/dist/doc/VJoyInput/guideline.md +401 -0
- package/dist/doc/VJoyInput/metadata.json +5 -14
- package/dist/doc/VJoyInputDigit/guideline.md +381 -0
- package/dist/doc/VJoyInputNumber/guideline.md +414 -0
- package/dist/doc/VJoyInputNumber/metadata.json +22 -17
- package/dist/doc/VJoyInputPassword/guideline.md +379 -0
- package/dist/doc/VJoyInputPassword/metadata.json +0 -14
- package/dist/doc/VJoyLabel/guideline.md +200 -0
- package/dist/doc/VJoyLink/guideline.md +360 -0
- package/dist/doc/VJoyListItem/guideline.md +335 -0
- package/dist/doc/VJoyMenu/guideline.md +304 -0
- package/dist/doc/VJoyMenu/metadata.json +21 -21
- package/dist/doc/VJoyMenuItem/guideline.md +352 -0
- package/dist/doc/VJoyPagination/guideline.md +355 -0
- package/dist/doc/VJoyPanel/guideline.md +238 -0
- package/dist/doc/VJoyPanelSection/guideline.md +160 -0
- package/dist/doc/VJoyPasswordRequirement/guideline.md +336 -0
- package/dist/doc/VJoyPasswordRequirement/metadata.json +1 -1
- package/dist/doc/VJoyProductTour/JoyProductTour.types.ts +0 -1
- package/dist/doc/VJoyProductTour/guideline.md +455 -0
- package/dist/doc/VJoyProductTour/metadata.json +1 -1
- package/dist/doc/VJoyProductTourTrigger/guideline.md +167 -0
- package/dist/doc/VJoyProgressBar/guideline.md +348 -0
- package/dist/doc/VJoyRadio/guideline.md +346 -0
- package/dist/doc/VJoyRadioGroup/guideline.md +331 -0
- package/dist/doc/VJoyRatingStars/guideline.md +306 -0
- package/dist/doc/VJoyScreenLoader/guideline.md +172 -0
- package/dist/doc/VJoySelect/guideline.md +396 -0
- package/dist/doc/VJoySelectableItem/guideline.md +351 -0
- package/dist/doc/VJoySelectableItemGroup/guideline.md +370 -0
- package/dist/doc/VJoySeparator/guideline.md +161 -0
- package/dist/doc/VJoySnackbar/guideline.md +402 -0
- package/dist/doc/VJoySpinner/guideline.md +168 -0
- package/dist/doc/VJoyStep/guideline.md +182 -0
- package/dist/doc/VJoyStepper/guideline.md +390 -0
- package/dist/doc/VJoyTab/guideline.md +298 -0
- package/dist/doc/VJoyTabs/guideline.md +353 -0
- package/dist/doc/VJoyTag/guideline.md +226 -0
- package/dist/doc/VJoyTagsInput/guideline.md +387 -0
- package/dist/doc/VJoyTagsList/guideline.md +311 -0
- package/dist/doc/VJoyTagsList/metadata.json +27 -27
- package/dist/doc/VJoyTemplate/guideline.md +366 -0
- package/dist/doc/VJoyText/guideline.md +175 -0
- package/dist/doc/VJoyText/metadata.json +1 -1
- package/dist/doc/VJoyTextarea/guideline.md +378 -0
- package/dist/doc/VJoyTitleBrand/guideline.md +155 -0
- package/dist/doc/VJoyToggle/guideline.md +327 -0
- package/dist/doc/VJoyTooltip/guideline.md +322 -0
- package/dist/doc/VJoyUserCard/guideline.md +268 -0
- package/dist/doc/VJoyUserCard/metadata.json +3 -17
- package/dist/doc/VJoyWalkthrough/guideline.md +351 -0
- package/dist/doc/VJoyWalkthroughTrigger/guideline.md +171 -0
- package/dist/doc/VJoyWrapper/guideline.md +173 -0
- package/dist/doc/css-classes.json +1 -15
- package/dist/doc/design-tokens.json +1 -0
- package/dist/doc/index.json +20 -30
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/dist/tools/get-component-guideline.d.ts +30 -0
- package/dist/tools/get-component-guideline.d.ts.map +1 -0
- package/dist/tools/get-component-guideline.js +51 -0
- package/dist/tools/get-component-guideline.js.map +1 -0
- package/dist/tools/get-documentation-page.d.ts +22 -0
- package/dist/tools/get-documentation-page.d.ts.map +1 -0
- package/dist/tools/get-documentation-page.js +21 -0
- package/dist/tools/get-documentation-page.js.map +1 -0
- package/dist/tools/list-documentation-pages.d.ts +22 -0
- package/dist/tools/list-documentation-pages.d.ts.map +1 -0
- package/dist/tools/list-documentation-pages.js +17 -0
- package/dist/tools/list-documentation-pages.js.map +1 -0
- package/dist/tools/search-documentation.d.ts +41 -0
- package/dist/tools/search-documentation.d.ts.map +1 -0
- package/dist/tools/search-documentation.js +25 -0
- package/dist/tools/search-documentation.js.map +1 -0
- package/dist/zeroheight-parser.d.ts +27 -0
- package/dist/zeroheight-parser.d.ts.map +1 -0
- package/dist/zeroheight-parser.js +77 -0
- package/dist/zeroheight-parser.js.map +1 -0
- package/package.json +10 -10
- package/dist/doc/VJoyMultiCheckbox/metadata.json +0 -332
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: VJoyTag
|
|
3
|
+
status: draft
|
|
4
|
+
language: en
|
|
5
|
+
dsds: JoyTag.dsds.json
|
|
6
|
+
dsdsVersion: "0.15.2"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Tag
|
|
10
|
+
|
|
11
|
+
Use **`VJoyTag`** when you need a compact label — status, category, filter chip, or removable token in a list.
|
|
12
|
+
|
|
13
|
+
It supports semantic color variants, four sizes, optional remove and drag-handle icons, and selectable or link styling on `primary` / `secondary`. Pass a `value` when tags are managed by **`VJoyTagsList`** or when remove/selection handlers need a stable key.
|
|
14
|
+
|
|
15
|
+
| | |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| **Code** | `VJoyTag` from `@maltjoy/core-vue` |
|
|
18
|
+
| **Design** | [Figma — Joy Core Design System](https://www.figma.com/design/jfndp2QBJGS9yeuWbeGuvD/) |
|
|
19
|
+
| **Storybook** | [VJoyTag docs](https://doc.malt.tech/joy-storybook/?path=/docs/components-view-vjoytag--docs) |
|
|
20
|
+
| **DSDS (machine)** | [`JoyTag.dsds.json`](./JoyTag.dsds.json) |
|
|
21
|
+
| **Live API** | `joy_get_component_info("VJoyTag")` |
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## When to use this component
|
|
26
|
+
|
|
27
|
+
Use a tag to surface **short, scannable metadata** — skill names, filter chips, workflow status (`pending`, `inactive`), pricing hints, or AI-related labels. Keep copy to a few words; pair with an icon in the default slot when the meaning benefits from a pictogram.
|
|
28
|
+
|
|
29
|
+
Use **`removable`** when the user can dismiss a tag (for example tokens already added to a profile). Use **`selectable`** + **`selected`** on **`primary`** or **`secondary`** for toggle-style filter chips — listen to `@selected` and update selection in the parent. For several tags with shared list behavior, prefer **`VJoyTagsList`** over hand-rolling spacing and events.
|
|
30
|
+
|
|
31
|
+
### When not to use this component
|
|
32
|
+
|
|
33
|
+
| Need | Use instead |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| Typing and committing multiple values in one field | `VJoyTagsInput` |
|
|
36
|
+
| Laying out many tags with list selection / removal wiring | `VJoyTagsList` wrapping `VJoyTag` |
|
|
37
|
+
| Numeric notification count on an icon or tab | `VJoyBadge` or `VJoyDot` |
|
|
38
|
+
| Achievement / level recognition (SuperMalter, verified, …) | `VJoyBadgeLevel` |
|
|
39
|
+
| Real navigation via URL | `@click` + your router — `link` is **style only** |
|
|
40
|
+
| Custom chip markup / ad-hoc CSS | Still use `VJoyTag` + Joy tokens |
|
|
41
|
+
|
|
42
|
+
> **Deprecated:** `strikethrough` — avoid in new work; the prop remains for legacy screens.
|
|
43
|
+
|
|
44
|
+
> **Not in the Vue API:** legacy Stencil `joy-tag` props `href`, `target`, `rel`, and `clickable`. **`VJoyTag` has no `href`.** Use `link` for appearance and handle navigation in `@click`.
|
|
45
|
+
|
|
46
|
+
> **`draggable`** only shows a drag-handle icon and `cursor: grab` — it does **not** implement drag-and-drop. Wire your own DnD library if reordering is required.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## How it works
|
|
51
|
+
|
|
52
|
+
### Anatomy
|
|
53
|
+
|
|
54
|
+
| Part | Required? | What it is |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| Root `<span>` | Yes | `.joy-tag` — inline-flex chip; auto-generated `id` unless `id` is set |
|
|
57
|
+
| Label | Yes* | `label` prop or default slot content (*slot replaces the built-in label) |
|
|
58
|
+
| Drag handle | No | Leading `VJoyIcon` (`drag`) when `draggable` — visual only |
|
|
59
|
+
| Remove control | No | Trailing `VJoyIcon` (`cross`, `role="button"`) when `removable`; emits `remove` with `{ value }` |
|
|
60
|
+
| Focus ring | Yes* | Token-based outline on `:focus` and when `selected` — suppressed when `disabled` |
|
|
61
|
+
|
|
62
|
+
### Variants and sizes (no separate sections — pick by meaning)
|
|
63
|
+
|
|
64
|
+
**Variants** (`TJoyTagVariants`):
|
|
65
|
+
|
|
66
|
+
| Group | Values | Typical use |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| Interactive filters | `primary`, `secondary` | Selectable chips; `selected` / hover / active styles apply |
|
|
69
|
+
| Status / emphasis | `important`, `pending`, `inactive`, `pricing`, `special` | Read-only semantic labels |
|
|
70
|
+
| AI surfaces | `ai-primary`, `ai-secondary` | AI-feature context only |
|
|
71
|
+
| Placeholder / add | `empty` | Dashed-border “add” style; supports selectable **styling** but `@selected` fires only for `primary` / `secondary` |
|
|
72
|
+
|
|
73
|
+
**Sizes** (`TJoyTagSizes`): `xsmall`, `small`, `medium` (default), `large`, `xxlarge`. Tokenized heights and typography are defined for `xsmall`–`large` in component styles; prefer those in product UI unless design specifies otherwise.
|
|
74
|
+
|
|
75
|
+
### Default values
|
|
76
|
+
|
|
77
|
+
- `size` defaults to **`medium`**
|
|
78
|
+
- `variant` defaults to **`primary`**
|
|
79
|
+
- `draggable`, `link`, `removable`, `strikethrough`, `disabled` default to **`false`**
|
|
80
|
+
- `value` defaults to **`undefined`** — set it when handlers or `VJoyTagsList` need a stable key
|
|
81
|
+
|
|
82
|
+
Props, events, and slots: use Joy MCP or Storybook — this page focuses on **when** and **how**, not a full API dump.
|
|
83
|
+
|
|
84
|
+
### Events and interaction
|
|
85
|
+
|
|
86
|
+
| Event | When |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `click` | Tag surface clicked and not `disabled` |
|
|
89
|
+
| `selected` | `selectable` is true **and** `variant` is `primary` or `secondary` — payload `{ selected, value }` toggles from current `selected` prop |
|
|
90
|
+
| `remove` | Remove icon clicked — payload `{ value }` |
|
|
91
|
+
|
|
92
|
+
`link` and `selectable` set `role="button"` and `tabindex="0"` (unless `disabled`). **`link` does not navigate** — add `@click` and call your router (see Storybook `Link` story).
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Best practices
|
|
97
|
+
|
|
98
|
+
Rules use RFC 2119 strength (**MUST** / **SHOULD** / **MUST NOT** / **SHOULD NOT**).
|
|
99
|
+
|
|
100
|
+
### Variant and copy
|
|
101
|
+
|
|
102
|
+
- **SHOULD** pick `variant` by **semantic meaning** (status, pricing, AI), not decoration alone.
|
|
103
|
+
- **SHOULD** keep tag text **short** (roughly 1–3 words); move longer detail to tooltips or body copy.
|
|
104
|
+
- **MUST** use `ai-primary` / `ai-secondary` only on AI-related surfaces.
|
|
105
|
+
- **MUST NOT** use `selectable` on static variants expecting `@selected` — only `primary` and `secondary` emit it.
|
|
106
|
+
|
|
107
|
+
### Lists and removal
|
|
108
|
+
|
|
109
|
+
- **SHOULD** pass a stable **`value`** on every tag in a managed list (alongside `label`).
|
|
110
|
+
- **SHOULD** use **`VJoyTagsList`** when multiple tags share selection or removal logic.
|
|
111
|
+
- **MUST** handle `@remove` in the parent — the tag does not remove itself from an array.
|
|
112
|
+
- **MUST NOT** rely on **`draggable`** for reordering without your own DnD implementation.
|
|
113
|
+
|
|
114
|
+
### Implementation
|
|
115
|
+
|
|
116
|
+
- **MUST** use only documented props and enums from MCP or `JoyTag.types.ts`.
|
|
117
|
+
- **MUST** use Joy tokens — no custom `.joy-tag` background, border, or radius overrides in apps.
|
|
118
|
+
- **MUST NOT** invent props (`href`, `color`, `clickable`, …).
|
|
119
|
+
- **SHOULD** use the **default slot** when content includes icons or markup; use **`label`** for plain text only.
|
|
120
|
+
|
|
121
|
+
### Do / don’t
|
|
122
|
+
|
|
123
|
+
| Do | Don’t |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `variant="pending"` for in-progress status | Random variant colors for decoration |
|
|
126
|
+
| `removable` + `@remove` + `value` in lists | Expect the chip to update the parent array by itself |
|
|
127
|
+
| `link` + `@click` + `router.push` for navigation | `href` on `VJoyTag` (not in API) |
|
|
128
|
+
| `VJoyTagsInput` for free-form entry | One `VJoyTag` per keystroke field |
|
|
129
|
+
| `VJoyWrapper` / `VJoyTagsList` for spacing | Negative margins between chips |
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Accessibility
|
|
134
|
+
|
|
135
|
+
Behaviors (what must happen), not slogans.
|
|
136
|
+
|
|
137
|
+
| Behavior | Expectation |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| Role | `role="button"` when `link` or `selectable`; otherwise plain text in a `<span>` |
|
|
140
|
+
| Keyboard | `tabindex="0"` on interactive tags (`link`, or `selectable` when not `disabled`); **Enter** on the remove icon triggers removal |
|
|
141
|
+
| Focus | Visible outline via Joy form focus tokens — do not remove in product CSS |
|
|
142
|
+
| Name | Expose meaningful text via `label` or slot content; icon-only tags need visible or adjacent text |
|
|
143
|
+
| Disabled | `disabled` blocks click/selection, sets `cursor: not-allowed`, and clears focus outline |
|
|
144
|
+
| Remove control | Separate `role="button"` on the cross icon; use `@click.stop` so it does not toggle selection |
|
|
145
|
+
|
|
146
|
+
Target: **WCAG 2.2 AA** where the product requires it.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Examples
|
|
151
|
+
|
|
152
|
+
```vue
|
|
153
|
+
<script setup lang="ts">
|
|
154
|
+
import { ref } from 'vue'
|
|
155
|
+
import { useRouter } from 'vue-router'
|
|
156
|
+
import { VJoyTag, VJoyIcon, VJoyWrapper } from '@maltjoy/core-vue'
|
|
157
|
+
|
|
158
|
+
const router = useRouter()
|
|
159
|
+
const filters = ref<string[]>(['remote'])
|
|
160
|
+
|
|
161
|
+
function toggleFilter(value: string, selected: boolean) {
|
|
162
|
+
if (selected) {
|
|
163
|
+
filters.value = [...filters.value, value]
|
|
164
|
+
} else {
|
|
165
|
+
filters.value = filters.value.filter((v) => v !== value)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function removeSkill(value: string) {
|
|
170
|
+
// update parent collection
|
|
171
|
+
}
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<template>
|
|
175
|
+
<!-- Status label -->
|
|
176
|
+
<VJoyTag variant="pending" label="Pending review" />
|
|
177
|
+
|
|
178
|
+
<!-- Icon + text via slot -->
|
|
179
|
+
<VJoyTag variant="special">
|
|
180
|
+
<VJoyIcon name="heart-fill" />
|
|
181
|
+
Featured
|
|
182
|
+
</VJoyTag>
|
|
183
|
+
|
|
184
|
+
<!-- Removable token -->
|
|
185
|
+
<VJoyTag
|
|
186
|
+
removable
|
|
187
|
+
label="Vue.js"
|
|
188
|
+
value="vue"
|
|
189
|
+
@remove="({ value }) => removeSkill(String(value))"
|
|
190
|
+
/>
|
|
191
|
+
|
|
192
|
+
<!-- Selectable filter chip -->
|
|
193
|
+
<VJoyTag
|
|
194
|
+
selectable
|
|
195
|
+
:selected="filters.includes('remote')"
|
|
196
|
+
value="remote"
|
|
197
|
+
label="Remote"
|
|
198
|
+
@selected="({ selected, value }) => toggleFilter(String(value), selected)"
|
|
199
|
+
/>
|
|
200
|
+
|
|
201
|
+
<!-- Link styling + manual navigation -->
|
|
202
|
+
<VJoyTag link label="View profile" @click="router.push('/profile')" />
|
|
203
|
+
|
|
204
|
+
<!-- Row of tags -->
|
|
205
|
+
<VJoyWrapper style="flex-wrap: wrap">
|
|
206
|
+
<VJoyTag variant="secondary" label="Design" />
|
|
207
|
+
<VJoyTag variant="secondary" label="Frontend" />
|
|
208
|
+
</VJoyWrapper>
|
|
209
|
+
</template>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
In Nuxt (or apps with auto-import), omit the import if `VJoyTag` is already global.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Related
|
|
217
|
+
|
|
218
|
+
| Component | Use when |
|
|
219
|
+
| --- | --- |
|
|
220
|
+
| `VJoyTagsList` | Multiple tags with shared variant, selection, or removal |
|
|
221
|
+
| `VJoyTagsInput` | User types and commits many values into tag chips |
|
|
222
|
+
| `VJoyWrapper` | Flex layout and gap between standalone tags |
|
|
223
|
+
| `VJoyBadge` | Small status pill or numeric counter |
|
|
224
|
+
| `VJoyBadgeLevel` | User achievement / level badges |
|
|
225
|
+
| `VJoyRadio` | Short metadata in `radio-header-right` (display-only) |
|
|
226
|
+
| `VJoyIcon` | Leading icons inside the default slot |
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: VJoyTagsInput
|
|
3
|
+
status: draft
|
|
4
|
+
language: en
|
|
5
|
+
dsds: JoyTagsInput.dsds.json
|
|
6
|
+
dsdsVersion: "0.15.2"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Tags input
|
|
10
|
+
|
|
11
|
+
Use **`VJoyTagsInput`** when the user must enter **multiple free-form tokens** — skills, keywords, invitee emails, or other values that appear as removable tags inside one field.
|
|
12
|
+
|
|
13
|
+
It binds to `v-model` as a `string[]`, commits tags on **Enter**, **comma**, or **blur** (and **space** when `validation="email"`), and can detect duplicates and invalid emails with built-in error messages.
|
|
14
|
+
|
|
15
|
+
| | |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| **Code** | `VJoyTagsInput` from `@maltjoy/core-vue` |
|
|
18
|
+
| **Design** | [Figma — Joy Core Design System](https://www.figma.com/design/jfndp2QBJGS9yeuWbeGuvD/) |
|
|
19
|
+
| **Storybook** | [VJoyTagsInput docs](https://doc.malt.tech/joy-storybook/?path=/docs/components-forms-vjoytagsinput--docs) |
|
|
20
|
+
| **DSDS (machine)** | [`JoyTagsInput.dsds.json`](./JoyTagsInput.dsds.json) |
|
|
21
|
+
| **Live API** | `joy_get_component_info("VJoyTagsInput")` |
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## When to use this component
|
|
26
|
+
|
|
27
|
+
Use a tags input when someone adds **several values of the same kind** in one step — for example listing skills on a profile, tagging a project, or collecting multiple email addresses for invitations.
|
|
28
|
+
|
|
29
|
+
Each committed value becomes a **`VJoyTag`** chip with a remove control. The inline text field stays available for the next entry. Wrap the field with **`VJoyLabel`** (or the deprecated default slot on legacy screens) and pair validation errors with clear copy via `duplicationError` / `invalidEmailError`.
|
|
30
|
+
|
|
31
|
+
### When not to use this component
|
|
32
|
+
|
|
33
|
+
| Need | Use instead |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| A single line of text (one value) | `VJoyInput` |
|
|
36
|
+
| Multi-line paragraph | `VJoyTextarea` |
|
|
37
|
+
| Picking from a fixed list (single or multi) | `VJoySelect`, `VJoyCheckbox`, or `VJoySelectableItemGroup` |
|
|
38
|
+
| Read-only display of existing tags | `VJoyTag` / `VJoyTagsList` — not an input |
|
|
39
|
+
| Search with one query string | `VJoyInput` with `icon="search"` and optional `clearable` |
|
|
40
|
+
| Custom token field with ad-hoc chip CSS | Still use `VJoyTagsInput` + Joy tokens |
|
|
41
|
+
|
|
42
|
+
> **Deprecated:** do not use the default slot for labels on new work — prefer **`VJoyLabel`** with matching `name` / `for`. Storybook’s default-slot label example is legacy.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
### Anatomy
|
|
49
|
+
|
|
50
|
+
| Part | Required? | What it is |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| Wrapper | Yes | Root `.joy-tags-input__wrapper` |
|
|
53
|
+
| Label | No | `VJoyLabel` (recommended) or deprecated default slot |
|
|
54
|
+
| Inner container | Yes | Bordered flex region (`.joy-tags-input__inner`) holding tags + input |
|
|
55
|
+
| Tag chips | No (0+) | One `VJoyTag` per committed value (`removable`, `label` = tag text) |
|
|
56
|
+
| Native `<input>` | Yes | Free-text entry; `data-joy-form-field`; `name` / `id` from `name` prop |
|
|
57
|
+
| Error block | No | `VJoyFormError` with duplicate and/or email messages when validation fails |
|
|
58
|
+
| Focus / invalid chrome | Yes | `--focused` / `--invalid` classes on the inner container — do not override in apps |
|
|
59
|
+
|
|
60
|
+
### Default values
|
|
61
|
+
|
|
62
|
+
- `placeholder` defaults to **`"Add your items here"`**
|
|
63
|
+
- `variant` defaults to **`primary`** (passed to each `VJoyTag`)
|
|
64
|
+
- `size` defaults to **`small`** (passed to each `VJoyTag`)
|
|
65
|
+
- `modelValue` defaults to **`[]`**
|
|
66
|
+
- `invalid` defaults to **`false`**
|
|
67
|
+
- `validation` defaults to **`none`**
|
|
68
|
+
|
|
69
|
+
Props, events, and slots: use Joy MCP or Storybook — this page focuses on **when** and **how**, not a full API dump.
|
|
70
|
+
|
|
71
|
+
### Committing and removing tags
|
|
72
|
+
|
|
73
|
+
- **Add:** type text, then press **Enter** or **,** — or **blur** the field (pending text is committed on blur). With `validation="email"`, **Space** also commits the current token.
|
|
74
|
+
- **Remove:** click a tag’s remove control, or press **Backspace** when the text input is empty (removes the last tag).
|
|
75
|
+
- **Model:** each successful add/remove emits `update:modelValue` with the full `string[]`. Initialize with `v-model` bound to an array (not `undefined`).
|
|
76
|
+
|
|
77
|
+
### Built-in validation
|
|
78
|
+
|
|
79
|
+
| Mode | Behavior |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `validation="none"` | Duplicates blocked when `duplicationError` is set; no email format check |
|
|
82
|
+
| `validation="email"` | Email-like format required (see `checkEmailsValidity`); duplicates still detected |
|
|
83
|
+
|
|
84
|
+
On failure the inner border turns invalid, `validation:error` fires (`DUPLICATED_ENTRY` or `INVALID_EMAIL`), and the matching error string renders when provided. Trimming is applied when comparing duplicates.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Variants
|
|
89
|
+
|
|
90
|
+
`variant` controls the **chip style** inside the field (same enum as `VJoyTag`). Pick by semantic meaning of the tokens, not decoration.
|
|
91
|
+
|
|
92
|
+
### `primary` (default)
|
|
93
|
+
|
|
94
|
+
Use for standard form tags — skills, topics, generic keywords.
|
|
95
|
+
|
|
96
|
+
### `secondary`
|
|
97
|
+
|
|
98
|
+
Use when tags should read as lower emphasis than surrounding primary chips (rare inside one input — prefer one variant per field).
|
|
99
|
+
|
|
100
|
+
### Status / pricing variants (`important`, `inactive`, `pending`, `pricing`, `special`)
|
|
101
|
+
|
|
102
|
+
Use only when token meaning matches the tag semantics (e.g. status labels). Avoid mixing unrelated semantics in one tags field.
|
|
103
|
+
|
|
104
|
+
### `empty`
|
|
105
|
+
|
|
106
|
+
Minimal chip chrome. Use when tags should feel lightweight inside dense UI.
|
|
107
|
+
|
|
108
|
+
### AI variants (`ai-primary`, `ai-secondary`)
|
|
109
|
+
|
|
110
|
+
Use only when the field is part of an AI feature surface.
|
|
111
|
+
|
|
112
|
+
### Not in the API: per-tag variants
|
|
113
|
+
|
|
114
|
+
`VJoyTagsInput` applies one `variant` to every tag. For heterogeneous tag styles, use standalone `VJoyTag` lists — not this component.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Sizes
|
|
119
|
+
|
|
120
|
+
`size` is passed to every `VJoyTag` in the field. Heights and chip density come from Joy tag tokens — **do not hardcode chip or input heights** in apps.
|
|
121
|
+
|
|
122
|
+
| Size | Use when |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `small` | Default — compact forms and multi-tag rows |
|
|
125
|
+
| `medium` | Slightly larger chips when the field is a focal form control |
|
|
126
|
+
| `large` / `xxlarge` | Prominent tagging UI (use sparingly) |
|
|
127
|
+
| `xsmall` | Very dense layouts (confirm with design — default is `small`) |
|
|
128
|
+
|
|
129
|
+
The inline `<input>` uses `min-height: var(--joy-core-spacing-6)` and grows within the wrapping flex row.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## States
|
|
134
|
+
|
|
135
|
+
| State | What the user sees / can do |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| Default | Empty or showing tags; neutral border; text input ready |
|
|
138
|
+
| Focused | Inner border uses `--joy-color-state-focus`; user can type or remove via keyboard |
|
|
139
|
+
| Hover | Border uses form hover tokens (pointer devices) |
|
|
140
|
+
| Valid | Tags listed; new tokens commit on Enter / comma / blur (and Space for email mode) |
|
|
141
|
+
| Duplicate error | Invalid border; `duplicationError` message when duplicate detected and prop is set |
|
|
142
|
+
| Invalid email | Invalid border; `invalidEmailError` when `validation="email"` and format fails |
|
|
143
|
+
| Removing tags | Remove icon on each tag; Backspace on empty input drops last tag |
|
|
144
|
+
|
|
145
|
+
There is no `disabled` or `readonly` prop on `VJoyTagsInput`. If the collection must not be edited, show `VJoyTagsList` / `VJoyTag` instead.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Best practices
|
|
150
|
+
|
|
151
|
+
Rules use RFC 2119 strength (**MUST** / **SHOULD** / **MUST NOT** / **SHOULD NOT**).
|
|
152
|
+
|
|
153
|
+
### Labels and forms
|
|
154
|
+
|
|
155
|
+
- **MUST** provide a visible label via `VJoyLabel` (with `for` matching `name`) — not placeholder-only.
|
|
156
|
+
- **MUST** set `name` so the label, input `id`, and form submission stay aligned.
|
|
157
|
+
- **MUST** bind `v-model` to a `string[]` (initialize to `[]` when empty).
|
|
158
|
+
- **SHOULD** wrap related fields in `VJoyWrapper` for consistent spacing.
|
|
159
|
+
|
|
160
|
+
### Implementation
|
|
161
|
+
|
|
162
|
+
- **MUST** use only documented prop / enum values from MCP or `JoyTagsInput.types.ts` / `JoyTag.types.ts`.
|
|
163
|
+
- **MUST** style with Joy tokens — no custom `.joy-tags-input` border, padding, or chip overrides.
|
|
164
|
+
- **MUST NOT** invent props (`maxTags`, `separator`, `disabled`, `clearable`, …).
|
|
165
|
+
- **MUST** pass `duplicationError` when duplicate detection should surface copy to the user.
|
|
166
|
+
- **MUST** pass `invalidEmailError` when using `validation="email"`.
|
|
167
|
+
- **SHOULD** listen to `validation:error` if the parent form needs to block submit or scroll to the field.
|
|
168
|
+
|
|
169
|
+
### Validation and errors
|
|
170
|
+
|
|
171
|
+
- **MUST** supply explicit error strings — the component does not render a generic invalid state without `duplicationError` / `invalidEmailError`.
|
|
172
|
+
- **MUST NOT** rely on border color alone; error text lives in `VJoyFormError`.
|
|
173
|
+
- **SHOULD** use `validation="email"` only for email collections (invites, notifications), not arbitrary keywords.
|
|
174
|
+
|
|
175
|
+
### Interaction
|
|
176
|
+
|
|
177
|
+
- **SHOULD** use placeholder text that hints at the commit gesture (e.g. “Type an email and press Enter”).
|
|
178
|
+
- **SHOULD** keep tag values short; long tokens wrap inside the flex container (see Storybook “Multi line”).
|
|
179
|
+
- **MUST NOT** expect Space to commit tags unless `validation="email"`.
|
|
180
|
+
|
|
181
|
+
### Do / don’t
|
|
182
|
+
|
|
183
|
+
| Do | Don’t |
|
|
184
|
+
| --- | --- |
|
|
185
|
+
| `VJoyLabel` + `name` / `for` | Placeholder as the only label |
|
|
186
|
+
| `v-model` on `string[]` | `v-model` on a single string |
|
|
187
|
+
| `validation="email"` + `invalidEmailError` for invite lists | Email mode without error copy |
|
|
188
|
+
| `duplicationError` when duplicates matter | Silent duplicate rejection |
|
|
189
|
+
| One `variant` per field | Mixed chip semantics without design sign-off |
|
|
190
|
+
| Joy form-field / tag tokens | Hardcoded chip heights or borders |
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Content
|
|
195
|
+
|
|
196
|
+
Labels, placeholders, and error messages must tell users **what** to enter and **how** tokens are added.
|
|
197
|
+
|
|
198
|
+
### Label formula
|
|
199
|
+
|
|
200
|
+
**Noun phrase** for the collection (not the commit action):
|
|
201
|
+
|
|
202
|
+
- ✅ Skills
|
|
203
|
+
- ✅ Invitees
|
|
204
|
+
- ✅ Project tags
|
|
205
|
+
|
|
206
|
+
Use sentence case. Prefer plural when multiple items are expected.
|
|
207
|
+
|
|
208
|
+
### Placeholder formula
|
|
209
|
+
|
|
210
|
+
**Hint at entry + commit** — not a repeat of the label:
|
|
211
|
+
|
|
212
|
+
- ✅ `Add a skill and press Enter` (under label “Skills”)
|
|
213
|
+
- ✅ `email@company.com` (under label “Invitees” with email validation)
|
|
214
|
+
|
|
215
|
+
- ❌ `Skills` as placeholder when the label already says “Skills”
|
|
216
|
+
|
|
217
|
+
Default placeholder is `Add your items here` — override when context is clearer.
|
|
218
|
+
|
|
219
|
+
### Error messages
|
|
220
|
+
|
|
221
|
+
| Prop | When | Example |
|
|
222
|
+
| --- | --- | --- |
|
|
223
|
+
| `duplicationError` | Duplicate token (trimmed match) | `This skill is already listed` |
|
|
224
|
+
| `invalidEmailError` | `validation="email"` and format fails | `Enter a valid email address` |
|
|
225
|
+
|
|
226
|
+
State what went wrong and how to fix it — ✅ `This email is already added` · ❌ `Error`.
|
|
227
|
+
|
|
228
|
+
### Writing rules
|
|
229
|
+
|
|
230
|
+
| Strength | Rule |
|
|
231
|
+
| --- | --- |
|
|
232
|
+
| **MUST** | Every field has a visible label |
|
|
233
|
+
| **MUST** | Error messages are specific and actionable |
|
|
234
|
+
| **MUST NOT** | Use labels that only make sense after failure |
|
|
235
|
+
| **SHOULD** | Keep individual tag text short (display truncates/wraps in narrow containers) |
|
|
236
|
+
| **SHOULD** | Use email validation copy that matches product terminology (“Invitee”, “Recipient”) |
|
|
237
|
+
| **SHOULD** (FR) | Follow French grammar for labels and error strings when the UI is localized |
|
|
238
|
+
|
|
239
|
+
### Patterns
|
|
240
|
+
|
|
241
|
+
**Profile skills** — `validation="none"`, `duplicationError` set, placeholder mentions Enter or comma.
|
|
242
|
+
|
|
243
|
+
**Email invites** — `validation="email"`, `invalidEmailError` + `duplicationError`, placeholder shows example address.
|
|
244
|
+
|
|
245
|
+
**Narrow layouts** — Tags wrap to multiple lines; keep labels concise so the field doesn’t feel crowded.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Accessibility
|
|
250
|
+
|
|
251
|
+
Behaviors (what must happen), not slogans.
|
|
252
|
+
|
|
253
|
+
| Behavior | Expectation |
|
|
254
|
+
| --- | --- |
|
|
255
|
+
| Role | Native text `<input>` for entry; tags are `VJoyTag` with removable button controls |
|
|
256
|
+
| Name | From associated `VJoyLabel` / `<label for="…">`; `name` sets input `id` |
|
|
257
|
+
| Value | Controlled list via `v-model` (`string[]`); input holds only the pending token |
|
|
258
|
+
| Keyboard | Enter / comma commit; Backspace on empty input removes last tag; Space commits only in email mode |
|
|
259
|
+
| Focus | Visible focus border on inner container; removing a tag refocuses the input |
|
|
260
|
+
| Invalid | Error border on inner container; messages in `VJoyFormError` below the field |
|
|
261
|
+
| Remove | Each tag exposes a remove control (`VJoyTag` removable) — must be keyboard-activatable via Joy tag implementation |
|
|
262
|
+
| Pending input | Standard text editing in the inline input; Tab moves focus out (blur may commit) |
|
|
263
|
+
|
|
264
|
+
Target: **WCAG 2.2 AA** where the product requires it.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Examples
|
|
269
|
+
|
|
270
|
+
```vue
|
|
271
|
+
<script setup lang="ts">
|
|
272
|
+
import { ref } from 'vue'
|
|
273
|
+
import { VJoyTagsInput, VJoyLabel, VJoyWrapper } from '@maltjoy/core-vue'
|
|
274
|
+
|
|
275
|
+
const skills = ref<string[]>(['Vue', 'TypeScript'])
|
|
276
|
+
const invitees = ref<string[]>([])
|
|
277
|
+
</script>
|
|
278
|
+
|
|
279
|
+
<template>
|
|
280
|
+
<!-- Skills with duplicate detection -->
|
|
281
|
+
<VJoyWrapper direction="column" row-gap="4">
|
|
282
|
+
<VJoyLabel for="skills">Skills</VJoyLabel>
|
|
283
|
+
<VJoyTagsInput
|
|
284
|
+
v-model="skills"
|
|
285
|
+
name="skills"
|
|
286
|
+
placeholder="Add a skill and press Enter"
|
|
287
|
+
duplication-error="This skill is already listed"
|
|
288
|
+
/>
|
|
289
|
+
</VJoyWrapper>
|
|
290
|
+
|
|
291
|
+
<!-- Email invites -->
|
|
292
|
+
<VJoyLabel for="invitees">Invitees</VJoyLabel>
|
|
293
|
+
<VJoyTagsInput
|
|
294
|
+
v-model="invitees"
|
|
295
|
+
name="invitees"
|
|
296
|
+
validation="email"
|
|
297
|
+
invalid-email-error="Enter a valid email address"
|
|
298
|
+
duplication-error="This email is already added"
|
|
299
|
+
placeholder="name@company.com"
|
|
300
|
+
@validation:error="(type) => console.warn(type)"
|
|
301
|
+
/>
|
|
302
|
+
|
|
303
|
+
<!-- Custom tag slot -->
|
|
304
|
+
<VJoyTagsInput v-model="skills" name="skills-custom">
|
|
305
|
+
<template #tag="{ name }">
|
|
306
|
+
<strong>{{ name }}</strong>
|
|
307
|
+
</template>
|
|
308
|
+
</VJoyTagsInput>
|
|
309
|
+
</template>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
In Nuxt (or apps with auto-import), omit the import if `VJoyTagsInput` is already global.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Related
|
|
317
|
+
|
|
318
|
+
| Component | Use when |
|
|
319
|
+
| --- | --- |
|
|
320
|
+
| `VJoyLabel` | Accessible label (preferred over default slot) |
|
|
321
|
+
| `VJoyTag` | Single tag or custom tag layouts outside this input |
|
|
322
|
+
| `VJoyTagsList` | Read-only or pre-styled lists of tags |
|
|
323
|
+
| `VJoyFormError` | Used internally; use standalone errors for group-level messages if needed |
|
|
324
|
+
| `VJoyWrapper` | Form layout and spacing |
|
|
325
|
+
| `VJoyInput` | Single-value text fields |
|
|
326
|
+
| `VJoySelect` | Choosing from a predefined set |
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## For agents
|
|
331
|
+
|
|
332
|
+
Optional for readers; **required** for vibe-coding quality. Maps to `agentDocumentBlocks` in [`JoyTagsInput.dsds.json`](./JoyTagsInput.dsds.json).
|
|
333
|
+
|
|
334
|
+
### Pick the right component
|
|
335
|
+
|
|
336
|
+
| If the intent is… | Emit… | Do not emit… |
|
|
337
|
+
| --- | --- | --- |
|
|
338
|
+
| Multiple free-form tokens in one field | `VJoyTagsInput` | Repeated `VJoyInput` or manual chip divs |
|
|
339
|
+
| Multiple email addresses | `VJoyTagsInput` + `validation="email"` | `VJoyInput` with comma-separated string |
|
|
340
|
+
| Single text value | `VJoyInput` | `VJoyTagsInput` with one-item array hack |
|
|
341
|
+
| Display-only tags | `VJoyTagsList` / `VJoyTag` | `VJoyTagsInput` with fake `modelValue` |
|
|
342
|
+
| Pick from fixed options | `VJoySelect` / checkboxes | Tags input with suggested datalist |
|
|
343
|
+
| Skills / keywords with duplicates blocked | `VJoyTagsInput` + `duplicationError` | Custom duplicate logic on raw input |
|
|
344
|
+
|
|
345
|
+
### Hard rules
|
|
346
|
+
|
|
347
|
+
1. **MUST** call `joy_get_component_info("VJoyTagsInput")` (or read types) before generating props.
|
|
348
|
+
2. **MUST** bind `v-model` to `string[]` (default `[]`).
|
|
349
|
+
3. **MUST** set `name` and associate `VJoyLabel` with `for` / `name`.
|
|
350
|
+
4. **MUST** pass `invalidEmailError` when `validation="email"`.
|
|
351
|
+
5. **MUST** pass `duplicationError` when duplicate feedback is required.
|
|
352
|
+
6. **MUST NOT** invent props (`disabled`, `maxTags`, `separator`, `clearable`, …).
|
|
353
|
+
7. **MUST NOT** override `.joy-tags-input__inner` border, padding, or tag chip styles.
|
|
354
|
+
8. **MUST NOT** use the default slot for new labels — use `VJoyLabel`.
|
|
355
|
+
9. **SHOULD** use `variant` / `size` from `TJoyTagVariants` / `TJoyTagSizes` only.
|
|
356
|
+
10. **SHOULD** handle `validation:error` when parent submit must wait for a valid list.
|
|
357
|
+
|
|
358
|
+
### Checklist
|
|
359
|
+
|
|
360
|
+
- [ ] Correct component (`VJoyTagsInput` or named alternative)
|
|
361
|
+
- [ ] Props / enums from MCP or types only
|
|
362
|
+
- [ ] `v-model` as `string[]` + `name` present
|
|
363
|
+
- [ ] Visible `VJoyLabel` (not placeholder-only)
|
|
364
|
+
- [ ] `duplicationError` / `invalidEmailError` set when validation applies
|
|
365
|
+
- [ ] `validation="email"` only for email collections
|
|
366
|
+
- [ ] No invented props / no custom tags-input chrome CSS
|
|
367
|
+
- [ ] Read-only tag display → `VJoyTagsList` / `VJoyTag`, not this input
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## DSDS mapping
|
|
372
|
+
|
|
373
|
+
This Markdown mirrors [`JoyTagsInput.dsds.json`](./JoyTagsInput.dsds.json) (DSDS **0.15.2**).
|
|
374
|
+
|
|
375
|
+
| Section here | DSDS block |
|
|
376
|
+
| --- | --- |
|
|
377
|
+
| When to use / not | `use-cases` |
|
|
378
|
+
| Anatomy | `anatomy` |
|
|
379
|
+
| Variants / sizes | `variants` |
|
|
380
|
+
| States | `states` |
|
|
381
|
+
| Best practices / content rules | `guidelines` (+ `content`) |
|
|
382
|
+
| Accessibility | `accessibility` |
|
|
383
|
+
| Examples | `sections` |
|
|
384
|
+
| For agents | `agentDocumentBlocks` |
|
|
385
|
+
| Props detail | `api` in JSON + Joy MCP |
|
|
386
|
+
| Imports | `imports` in JSON |
|
|
387
|
+
| Token sizing | `design-specifications` in JSON (`--joy-core-spacing-*`, `--joy-form-border-width`, tag size tokens) |
|