@revikornmann/muka-ui 0.18.0 → 0.19.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/README.md +81 -60
- package/cli/bin/muka-ui.js +12 -5
- package/cli/commands/brand.js +33 -14
- package/cli/commands/init.js +21 -17
- package/cli/commands/install-skill.js +84 -26
- package/cli/templates/AGENTS.md +128 -0
- package/cli/templates/CLAUDE.md +8 -88
- package/cli/templates/muka-ui-guidelines.md +50 -14
- package/dist/cjs/components/Combobox/Combobox.css +116 -132
- package/dist/cjs/components/Combobox/Combobox.js +175 -52
- package/dist/cjs/components/Combobox/Combobox.js.map +1 -1
- package/dist/cjs/components/Combobox/index.js.map +1 -1
- package/dist/cjs/components/DropdownSelect/DropdownSelect.css +225 -0
- package/dist/cjs/components/DropdownSelect/DropdownSelect.js +106 -0
- package/dist/cjs/components/DropdownSelect/DropdownSelect.js.map +1 -0
- package/dist/cjs/components/DropdownSelect/index.js +6 -0
- package/dist/cjs/components/DropdownSelect/index.js.map +1 -0
- package/dist/cjs/components/Icon/custom/ArrowReturnIcon.js +11 -0
- package/dist/cjs/components/Icon/custom/ArrowReturnIcon.js.map +1 -0
- package/dist/cjs/components/Icon/iconRegistry.js +5 -0
- package/dist/cjs/components/Icon/iconRegistry.js.map +1 -1
- package/dist/cjs/components/Input/Input.css +45 -8
- package/dist/cjs/components/Input/Input.js +14 -3
- package/dist/cjs/components/Input/Input.js.map +1 -1
- package/dist/cjs/components/Menu/Menu.css +123 -37
- package/dist/cjs/components/Menu/Menu.js +283 -25
- package/dist/cjs/components/Menu/Menu.js.map +1 -1
- package/dist/cjs/components/ProgressTracker/ProgressTracker.css +273 -22
- package/dist/cjs/components/ProgressTracker/ProgressTracker.js +109 -9
- package/dist/cjs/components/ProgressTracker/ProgressTracker.js.map +1 -1
- package/dist/cjs/components/Scrollbar/Scrollbar.css +7 -6
- package/dist/cjs/components/index.js +5 -3
- package/dist/cjs/components/index.js.map +1 -1
- package/dist/esm/components/Combobox/Combobox.css +116 -132
- package/dist/esm/components/Combobox/Combobox.js +177 -54
- package/dist/esm/components/Combobox/Combobox.js.map +1 -1
- package/dist/esm/components/Combobox/index.js +1 -1
- package/dist/esm/components/Combobox/index.js.map +1 -1
- package/dist/esm/components/DropdownSelect/DropdownSelect.css +225 -0
- package/dist/esm/components/DropdownSelect/DropdownSelect.js +102 -0
- package/dist/esm/components/DropdownSelect/DropdownSelect.js.map +1 -0
- package/dist/esm/components/DropdownSelect/index.js +2 -0
- package/dist/esm/components/DropdownSelect/index.js.map +1 -0
- package/dist/esm/components/Icon/custom/ArrowReturnIcon.js +7 -0
- package/dist/esm/components/Icon/custom/ArrowReturnIcon.js.map +1 -0
- package/dist/esm/components/Icon/iconRegistry.js +5 -0
- package/dist/esm/components/Icon/iconRegistry.js.map +1 -1
- package/dist/esm/components/Input/Input.css +45 -8
- package/dist/esm/components/Input/Input.js +14 -3
- package/dist/esm/components/Input/Input.js.map +1 -1
- package/dist/esm/components/Menu/Menu.css +123 -37
- package/dist/esm/components/Menu/Menu.js +284 -26
- package/dist/esm/components/Menu/Menu.js.map +1 -1
- package/dist/esm/components/ProgressTracker/ProgressTracker.css +273 -22
- package/dist/esm/components/ProgressTracker/ProgressTracker.js +110 -10
- package/dist/esm/components/ProgressTracker/ProgressTracker.js.map +1 -1
- package/dist/esm/components/Scrollbar/Scrollbar.css +7 -6
- package/dist/esm/components/index.js +1 -0
- package/dist/esm/components/index.js.map +1 -1
- package/dist/styles/components/Combobox.css +116 -132
- package/dist/styles/components/DropdownSelect.css +225 -0
- package/dist/styles/components/Input.css +45 -8
- package/dist/styles/components/Menu.css +123 -37
- package/dist/styles/components/ProgressTracker.css +273 -22
- package/dist/styles/components/Scrollbar.css +7 -6
- package/dist/styles/components/Typography.css +89 -0
- package/dist/styles/index.css +1285 -605
- package/dist/styles/muka-dark.css +1285 -605
- package/dist/styles/muka-light.css +1285 -605
- package/dist/styles/tokens-bouwplan-dark.css +5 -4
- package/dist/styles/tokens-bouwplan-light.css +5 -4
- package/dist/styles/tokens-fscl-dark.css +25 -24
- package/dist/styles/tokens-fscl-light.css +25 -24
- package/dist/styles/tokens-grip-dark.css +4 -3
- package/dist/styles/tokens-grip-light.css +4 -3
- package/dist/styles/tokens-muka-dark.css +4 -3
- package/dist/styles/tokens-muka-light.css +4 -3
- package/dist/styles/tokens-wireframe-dark.css +49 -48
- package/dist/styles/tokens-wireframe-light.css +49 -48
- package/dist/styles/wireframe-dark.css +1330 -650
- package/dist/styles/wireframe-light.css +1330 -650
- package/dist/types/components/Combobox/Combobox.d.ts +38 -38
- package/dist/types/components/Combobox/Combobox.d.ts.map +1 -1
- package/dist/types/components/Combobox/index.d.ts +1 -1
- package/dist/types/components/Combobox/index.d.ts.map +1 -1
- package/dist/types/components/DropdownSelect/DropdownSelect.d.ts +71 -0
- package/dist/types/components/DropdownSelect/DropdownSelect.d.ts.map +1 -0
- package/dist/types/components/DropdownSelect/index.d.ts +2 -0
- package/dist/types/components/DropdownSelect/index.d.ts.map +1 -0
- package/dist/types/components/Icon/custom/ArrowReturnIcon.d.ts +9 -0
- package/dist/types/components/Icon/custom/ArrowReturnIcon.d.ts.map +1 -0
- package/dist/types/components/Icon/iconRegistry.d.ts.map +1 -1
- package/dist/types/components/Input/Input.d.ts +5 -0
- package/dist/types/components/Input/Input.d.ts.map +1 -1
- package/dist/types/components/Menu/Menu.d.ts +42 -2
- package/dist/types/components/Menu/Menu.d.ts.map +1 -1
- package/dist/types/components/ProgressTracker/ProgressTracker.d.ts +29 -8
- package/dist/types/components/ProgressTracker/ProgressTracker.d.ts.map +1 -1
- package/dist/types/components/index.d.ts +1 -0
- package/dist/types/components/index.d.ts.map +1 -1
- package/docs/consumers/README.md +116 -37
- package/docs/consumers/brand.md +261 -0
- package/docs/consumers/figma-console-mcp.md +116 -0
- package/docs/consumers/setup-instructions.md +98 -12
- package/docs/consumers/skills.md +79 -0
- package/package.json +4 -6
- package/scripts/postinstall-nudge.js +8 -4
- package/skills/add-brand/SKILL.md +204 -0
- package/skills/add-brand/reference.md +160 -0
- package/skills/figma-to-code/SKILL.md +127 -0
- package/skills/pull-from-figma/SKILL.md +123 -0
- package/skills/push-to-figma/SKILL.md +172 -0
- package/skills/setup-muka/SKILL.md +73 -13
- package/tokens/README.md +39 -17
- package/tokens/t2-alias/brand/bouwplan/fonts.json +1 -1
- package/tokens/t2-alias/brand/fscl/fonts.json +3 -3
- package/tokens/t2-alias/brand/wireframe/fonts.json +2 -2
- package/tokens/t4-components/menu.json +8 -3
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figma-to-code
|
|
3
|
+
description: Build a screen in this app from a Figma link, composing Muka UI components and design tokens instead of writing new styled markup.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
argument-hint: "[figma-url]"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Figma link → Muka UI screen
|
|
9
|
+
|
|
10
|
+
Turn a Figma design into working code in **this app** by assembling it from Muka
|
|
11
|
+
UI components. `$ARGUMENTS` is the Figma URL.
|
|
12
|
+
|
|
13
|
+
The goal is assembly, not reproduction. A Figma frame drawn with the Muka UI
|
|
14
|
+
Figma Library is already made of Muka components, so the right output is those
|
|
15
|
+
same components with props — not a pixel-matched rebuild in bespoke CSS. If the
|
|
16
|
+
result is a pile of `<div>`s with hardcoded colours, the design was
|
|
17
|
+
re-implemented instead of mapped, and it will not follow brand or theme changes.
|
|
18
|
+
|
|
19
|
+
**Prerequisite:** `@revikornmann/muka-ui` is installed (`/setup-muka`) and a
|
|
20
|
+
Figma MCP server is connected.
|
|
21
|
+
|
|
22
|
+
## Step 1: Read the design
|
|
23
|
+
|
|
24
|
+
Extract `fileKey` and `nodeId` from the URL:
|
|
25
|
+
|
|
26
|
+
- `figma.com/design/:fileKey/:fileName?node-id=:nodeId` — convert `-` to `:` in
|
|
27
|
+
the nodeId (`472-4248` → `472:4248`)
|
|
28
|
+
- `figma.com/design/:fileKey/branch/:branchKey/…` — use `branchKey` as the
|
|
29
|
+
fileKey
|
|
30
|
+
|
|
31
|
+
Then pull the design context:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
get_code_connect_map(fileKey, nodeId) # FIRST — Figma node → real component names
|
|
35
|
+
get_design_context(fileKey, nodeId) # structure, tokens, variants
|
|
36
|
+
get_screenshot(fileKey, nodeId) # visual reference to check yourself against
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Read `get_code_connect_map` **first**. Muka publishes Code Connect mappings for
|
|
40
|
+
its components, so this is what tells you a node is a Muka `Button` rather than a
|
|
41
|
+
rounded rectangle with a label. Treat its answer as authoritative over your own
|
|
42
|
+
reading of the layers.
|
|
43
|
+
|
|
44
|
+
Treat `get_design_context` output as a **reference, not final code**. It
|
|
45
|
+
describes the design in generic terms; your job is the translation.
|
|
46
|
+
|
|
47
|
+
## Step 2: Inventory what Muka already provides
|
|
48
|
+
|
|
49
|
+
Before writing any JSX, find out what exists. In order of reliability:
|
|
50
|
+
|
|
51
|
+
1. **Storybook MCP**, if Muka's Storybook is running — the component list and
|
|
52
|
+
live prop tables are the most accurate source there is.
|
|
53
|
+
2. **The published Storybook** at <https://muka.kornmann.com> — component APIs,
|
|
54
|
+
Playgrounds, and usage patterns.
|
|
55
|
+
3. **The installed types**:
|
|
56
|
+
```bash
|
|
57
|
+
grep 'export' node_modules/@revikornmann/muka-ui/dist/types/components/index.d.ts
|
|
58
|
+
```
|
|
59
|
+
4. **`npx muka-ui components -v`** — names and prop summaries from the package
|
|
60
|
+
manifest.
|
|
61
|
+
5. **`docs/muka-ui-guidelines.md`** in this repo, if `npx muka-ui init` has run.
|
|
62
|
+
|
|
63
|
+
Write out the mapping before you build, so the gaps are visible up front:
|
|
64
|
+
|
|
65
|
+
| Figma layer | Muka component | Props |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| "Primary CTA" | `Button` | `variant="primary" size="md"` |
|
|
68
|
+
| "Search field" | `Input` | `placeholder`, leading icon slot |
|
|
69
|
+
| "Nav bar" | `TopBar` | title, actions |
|
|
70
|
+
|
|
71
|
+
## Step 3: Compose the screen
|
|
72
|
+
|
|
73
|
+
Build from the outside in: page shell, then regions, then controls.
|
|
74
|
+
|
|
75
|
+
- **Use Muka components for anything Muka has.** Reach for a raw element only
|
|
76
|
+
when nothing fits.
|
|
77
|
+
- **Compose, don't fork.** Prefer slots and composition over copying a
|
|
78
|
+
component's source to tweak it. A forked component stops receiving upgrades
|
|
79
|
+
and brand changes.
|
|
80
|
+
- **Only Muka's CSS custom properties for styling.** For app-specific layout,
|
|
81
|
+
read T3 semantic properties:
|
|
82
|
+
```css
|
|
83
|
+
.checkout-summary {
|
|
84
|
+
background-color: var(--color-surface-level1);
|
|
85
|
+
color: var(--color-text-default-default);
|
|
86
|
+
padding: var(--spacing-4);
|
|
87
|
+
border-radius: var(--radius-md);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
Never paste a hex, px type size, or radius out of Figma. Those values belong to
|
|
91
|
+
one brand in one theme; the token survives both.
|
|
92
|
+
- **Match the design's intent, not its pixels.** If Figma shows 15px of padding
|
|
93
|
+
and the nearest token is 16, use the token. Report differences larger than a
|
|
94
|
+
step rather than hardcoding around them.
|
|
95
|
+
- **Mobile first.** Muka is designed mobile-up with the desktop breakpoint at
|
|
96
|
+
`1024px`. Build the narrow layout first, then enhance.
|
|
97
|
+
|
|
98
|
+
## Step 4: Handle what Muka does not have
|
|
99
|
+
|
|
100
|
+
When a design needs something Muka does not ship, say so explicitly rather than
|
|
101
|
+
quietly inventing a lookalike — a bespoke component that mimics a Muka one is
|
|
102
|
+
the most expensive outcome, because it looks correct until the brand changes.
|
|
103
|
+
|
|
104
|
+
1. Re-check whether composing existing primitives covers it.
|
|
105
|
+
2. If not, build it in **this repo** (not in Muka) styled entirely with Muka's
|
|
106
|
+
token custom properties, so it tracks brand and theme.
|
|
107
|
+
3. Tell the user it is app-local and worth requesting upstream at
|
|
108
|
+
<https://github.com/revikornmann/muka/issues> if it is generic.
|
|
109
|
+
|
|
110
|
+
## Step 5: Verify
|
|
111
|
+
|
|
112
|
+
- The screen renders and type-checks.
|
|
113
|
+
- **No hardcoded colours, type sizes, or radii.** Grep your diff for `#`,
|
|
114
|
+
`rgb(`, and `px` and justify every remaining hit.
|
|
115
|
+
- Compare against `get_screenshot` for structure, hierarchy, and spacing rhythm.
|
|
116
|
+
- **Switch theme and confirm nothing breaks.** Load the dark stylesheet and
|
|
117
|
+
re-check: anything that does not react was hardcoded. This is the single most
|
|
118
|
+
effective check that the mapping was done properly.
|
|
119
|
+
- Keyboard reachability: every interactive element is tabbable with a visible
|
|
120
|
+
focus ring.
|
|
121
|
+
- If this repo has a custom brand, confirm the screen picks it up.
|
|
122
|
+
|
|
123
|
+
## Next
|
|
124
|
+
|
|
125
|
+
- `/add-brand` — if the design is in a brand this repo does not have yet.
|
|
126
|
+
- `/pull-from-figma` — if a designer changed brand colours in Figma and the code
|
|
127
|
+
needs to catch up.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pull-from-figma
|
|
3
|
+
description: Pull brand colour and font changes a designer made in Figma back into this repo's brand token files.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Pull brand changes from Figma
|
|
8
|
+
|
|
9
|
+
A designer retuned your brand's colours in Figma; bring those edits back into
|
|
10
|
+
`brand/light.json`, `brand/dark.json`, and `brand/fonts.json`.
|
|
11
|
+
|
|
12
|
+
**Direction:** Figma → Code. **Scope:** only your brand's `alias/*` tokens.
|
|
13
|
+
Nothing is created or deleted — existing `$value` fields are updated in place.
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
1. This repo has a custom brand with modes pushed to Figma — run `/add-brand`
|
|
18
|
+
then `/push-to-figma` first.
|
|
19
|
+
2. **Figma Console MCP** is configured and the Desktop Bridge plugin is running.
|
|
20
|
+
One-time setup:
|
|
21
|
+
`node_modules/@revikornmann/muka-ui/docs/consumers/figma-console-mcp.md`
|
|
22
|
+
(also online at
|
|
23
|
+
<https://github.com/revikornmann/muka/blob/main/docs/consumers/figma-console-mcp.md>).
|
|
24
|
+
|
|
25
|
+
## Why only the alias layer
|
|
26
|
+
|
|
27
|
+
Your repo owns exactly one layer of the token system. Semantics and component
|
|
28
|
+
tokens come from the installed package and are overwritten on the next upgrade,
|
|
29
|
+
so pulling them in would produce changes that silently disappear. If a designer
|
|
30
|
+
changed `color/surface/level1` or a `button/*` variable, that is a **library**
|
|
31
|
+
change: it belongs in a pull request against
|
|
32
|
+
<https://github.com/revikornmann/muka>, and you should say so rather than writing
|
|
33
|
+
it into `brand/`.
|
|
34
|
+
|
|
35
|
+
## Step 1: Confirm the bridge is live
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
figma_get_status { probe: true }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Step 2: Find your brand's modes
|
|
42
|
+
|
|
43
|
+
Look collections and modes up by name — never hardcode IDs:
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
|
47
|
+
const theme = collections.find((c) => c.name === 'Theme');
|
|
48
|
+
const light = theme.modes.find((m) => m.name === 'Acme Light').modeId;
|
|
49
|
+
const dark = theme.modes.find((m) => m.name === 'Acme Dark').modeId;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Step 3: Read the alias variables
|
|
53
|
+
|
|
54
|
+
Read every variable whose name starts with `alias/`, taking the value for your
|
|
55
|
+
two modes only. Ignore all other modes — they are other brands.
|
|
56
|
+
|
|
57
|
+
## Step 4: Resolve each value back to a token reference
|
|
58
|
+
|
|
59
|
+
- **Variable alias** (the normal case, pointing into Primitives): resolve the
|
|
60
|
+
target variable's name and convert slashes to dots.
|
|
61
|
+
`color/violet/9` → `{color.violet.9}`
|
|
62
|
+
- **Concrete string** (font family or weight): keep the string, converting
|
|
63
|
+
Figma's style names back to CSS numerics — `SemiBold` → `600`, `Bold` → `700`,
|
|
64
|
+
`Medium` → `500`, `Regular` → `400`.
|
|
65
|
+
- **Raw colour value**: this is a problem, not a value to pull. It means the
|
|
66
|
+
variable was detached from the ramp. Report it and re-push that token as an
|
|
67
|
+
alias instead of writing a hex into the token files, which would opt it out of
|
|
68
|
+
the system permanently.
|
|
69
|
+
|
|
70
|
+
## Step 5: Map to files
|
|
71
|
+
|
|
72
|
+
| Variable prefix | Target file |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `alias/color/*` read at your **light** mode | `brand/light.json` |
|
|
75
|
+
| `alias/color/*` read at your **dark** mode | `brand/dark.json` |
|
|
76
|
+
| `alias/font/*` | `brand/fonts.json` |
|
|
77
|
+
|
|
78
|
+
Figma slash-paths map to token dot-paths directly:
|
|
79
|
+
`alias/color/accent/default` → `alias.color.accent.default`.
|
|
80
|
+
|
|
81
|
+
## Step 6: Apply the diff
|
|
82
|
+
|
|
83
|
+
For each token that already exists in the target file:
|
|
84
|
+
|
|
85
|
+
1. Compare the current `$value` with the resolved Figma value.
|
|
86
|
+
2. **Show the user the full diff before writing anything.**
|
|
87
|
+
3. Update only `$value`. Never touch `$type`, and preserve the `$schema` entry
|
|
88
|
+
and key order so the diff stays reviewable.
|
|
89
|
+
|
|
90
|
+
Safety rules:
|
|
91
|
+
|
|
92
|
+
- Only tokens already present in the files are updated.
|
|
93
|
+
- No tokens are created or deleted. A variable in Figma with no counterpart in
|
|
94
|
+
the files is reported, not added — it usually means the designer added
|
|
95
|
+
something that needs a code-side decision first.
|
|
96
|
+
- A token missing from Figma is left alone, not blanked.
|
|
97
|
+
|
|
98
|
+
## Step 7: Rebuild and check
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm run build:tokens
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then verify:
|
|
105
|
+
|
|
106
|
+
- No `undefined` in `styles/tokens-<brand>-*.css` — a failed reference resolution.
|
|
107
|
+
- Both light and dark still read correctly; a designer tuning light mode in
|
|
108
|
+
isolation is the usual way dark-mode contrast regresses.
|
|
109
|
+
- Muka components pick up the change in the app.
|
|
110
|
+
- The dark values reference `Dark` ramps (`{color.violetDark.9}`). If a dark
|
|
111
|
+
token came back pointing at a light ramp, the designer edited the wrong mode.
|
|
112
|
+
|
|
113
|
+
Commit `brand/**` with a message naming what the designer changed.
|
|
114
|
+
|
|
115
|
+
## Troubleshooting
|
|
116
|
+
|
|
117
|
+
| Symptom | Cause |
|
|
118
|
+
|---|---|
|
|
119
|
+
| Values come back as raw colours | The variable was detached from the ramp — re-push it as an alias |
|
|
120
|
+
| Everything appears unchanged | Reading the wrong modes; confirm the mode names match your brand |
|
|
121
|
+
| Dark values reference light ramps | The wrong Figma mode was edited |
|
|
122
|
+
| Semibold and bold pull the same weight | They were pushed as numerics and collapsed in Figma |
|
|
123
|
+
| `figma-console` tools absent | Session predates `.mcp.json`; approve via `/mcp` and restart |
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: push-to-figma
|
|
3
|
+
description: Push this repo's custom brand tokens into Figma as variables, so designers can design in your brand using the Muka UI Figma Library.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Push your brand to Figma
|
|
8
|
+
|
|
9
|
+
Publish this repo's brand layer (`brand/light.json`, `brand/dark.json`,
|
|
10
|
+
`brand/fonts.json`) into Figma as variables, so designers see your brand in the
|
|
11
|
+
Muka UI Figma Library instead of one of Muka's.
|
|
12
|
+
|
|
13
|
+
**Direction:** Code → Figma. **The codebase is the source of truth.** Use
|
|
14
|
+
`/pull-from-figma` for the other direction.
|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
1. This repo has a custom brand — run `/add-brand` first.
|
|
19
|
+
2. **Figma Console MCP** is configured and the Desktop Bridge plugin is running.
|
|
20
|
+
One-time setup:
|
|
21
|
+
`node_modules/@revikornmann/muka-ui/docs/consumers/figma-console-mcp.md`
|
|
22
|
+
(also online at
|
|
23
|
+
<https://github.com/revikornmann/muka/blob/main/docs/consumers/figma-console-mcp.md>).
|
|
24
|
+
3. You can edit the target Figma file (your own copy or branch of the Muka UI
|
|
25
|
+
Figma Library).
|
|
26
|
+
|
|
27
|
+
## What gets pushed, and what does not
|
|
28
|
+
|
|
29
|
+
Only your **brand alias layer** — the ~78 `alias/color/*` tokens plus
|
|
30
|
+
`alias/font/*`. That is the whole point: primitives, semantics, and component
|
|
31
|
+
tokens belong to the Muka library and are inherited, exactly as in code.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
component (button/color/primary/background/default) → library, do not touch
|
|
35
|
+
semantic (color/surface/level0, color/action/default) → library, do not touch
|
|
36
|
+
alias (alias/color/accent/default) → YOURS, pushed here
|
|
37
|
+
Primitives (color/violet/9) → library, raw values
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If you find yourself pushing a `button/*` or `color/surface/*` variable, stop —
|
|
41
|
+
that is a library-level change and belongs in a Muka pull request, not your
|
|
42
|
+
brand.
|
|
43
|
+
|
|
44
|
+
## Step 1: Confirm the bridge is live
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
figma_get_status { probe: true }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Expect `setup.valid: true`, `probeResult.success: true`, and the connected file
|
|
51
|
+
name. If the tools are missing entirely, the session started before the MCP
|
|
52
|
+
server was registered — restart it.
|
|
53
|
+
|
|
54
|
+
## Step 2: Locate the collections
|
|
55
|
+
|
|
56
|
+
Do **not** hardcode collection or mode IDs; look them up by name in the file you
|
|
57
|
+
are connected to. Muka's own IDs differ in every copy and branch of the library.
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
|
61
|
+
for (const c of collections) {
|
|
62
|
+
console.log(c.name, c.id, c.modes.map((m) => `${m.name}=${m.modeId}`));
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
You need two:
|
|
67
|
+
|
|
68
|
+
- **Primitives** — the raw ramps (`color/violet/9`). One mode. Read-only for you.
|
|
69
|
+
- **Theme** — one mode per brand/theme combination (`Muka Light`, `Muka Dark`, …).
|
|
70
|
+
This is where your brand's modes live.
|
|
71
|
+
|
|
72
|
+
Build a name → id map across **both** collections; you will alias into Primitives
|
|
73
|
+
by name.
|
|
74
|
+
|
|
75
|
+
## Step 3: Add modes for your brand
|
|
76
|
+
|
|
77
|
+
Your brand needs two modes in the Theme collection, named to match the existing
|
|
78
|
+
convention:
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
const theme = collections.find((c) => c.name === 'Theme');
|
|
82
|
+
const lightModeId = theme.addMode('Acme Light');
|
|
83
|
+
const darkModeId = theme.addMode('Acme Dark');
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Skip this if the modes already exist — find them by name and reuse their
|
|
87
|
+
`modeId`. Figma has a per-collection mode limit that depends on the plan; if
|
|
88
|
+
`addMode` throws, the file has hit it and modes must be freed before continuing.
|
|
89
|
+
|
|
90
|
+
## Step 4: Bind values — as aliases, never raw colours
|
|
91
|
+
|
|
92
|
+
**This is the step that goes wrong.** Every alias token must be a variable
|
|
93
|
+
**alias** pointing at a Primitive, not a resolved hex. A raw value is a
|
|
94
|
+
disconnected duplicate: it will not cascade when the ramp is edited, and it
|
|
95
|
+
drifts silently from your `{color.violet.9}` references in code.
|
|
96
|
+
|
|
97
|
+
Your `brand/light.json` already stores exactly the reference you need
|
|
98
|
+
(`{color.violet.9}` → the Primitives variable `color/violet/9`), so convert the
|
|
99
|
+
path and bind:
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
// '{color.violet.9}' → 'color/violet/9'
|
|
103
|
+
const target = value.replace(/[{}]/g, '').split('.').join('/');
|
|
104
|
+
|
|
105
|
+
const variable = await figma.variables.getVariableByIdAsync(aliasVarId);
|
|
106
|
+
variable.setValueForMode(lightModeId, {
|
|
107
|
+
type: 'VARIABLE_ALIAS',
|
|
108
|
+
id: nameToId[target],
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Each mode gets a **different** Primitives target — light mode points at
|
|
113
|
+
`color/violet/9`, dark mode at `color/violetDark/9`. Per-mode cross-collection
|
|
114
|
+
aliases do cascade, and that is what makes mode switching work.
|
|
115
|
+
|
|
116
|
+
> `figma_batch_create_variables` and `figma_batch_update_variables` accept only
|
|
117
|
+
> scalar/hex `valuesByMode` — they **cannot** set alias bindings. Use them to
|
|
118
|
+
> create bare variables in bulk, then bind in a separate `figma_execute` pass.
|
|
119
|
+
|
|
120
|
+
Raw `{r, g, b, a}` values are correct only in the Primitives collection. If you
|
|
121
|
+
ever must write one, pass an object — **never a hex string**, which drops the
|
|
122
|
+
alpha channel and flattens the `black-alpha` / `white-alpha` ramps to opaque.
|
|
123
|
+
|
|
124
|
+
## Step 5: Push the fonts
|
|
125
|
+
|
|
126
|
+
`alias/font/*` variables are strings, not aliases, so set them directly — with
|
|
127
|
+
two conversions:
|
|
128
|
+
|
|
129
|
+
- **Family**: reduce a CSS stack to the single installed face name. Pushing
|
|
130
|
+
`'IBM Plex Sans', ui-sans-serif, system-ui` makes every bound text node render
|
|
131
|
+
as a **missing font**; push `IBM Plex Sans`.
|
|
132
|
+
- **Weight**: Figma `fontStyle` bindings need style **names**, not the CSS
|
|
133
|
+
numerics your tokens hold — `600` → `SemiBold`, `700` → `Bold`, `500` →
|
|
134
|
+
`Medium`, `400` → `Regular`. Pushing numerics collapses semibold and bold onto
|
|
135
|
+
the same face.
|
|
136
|
+
|
|
137
|
+
The font must be installed locally or available to the file, or every bound text
|
|
138
|
+
node shows as missing.
|
|
139
|
+
|
|
140
|
+
> Repairing a font-family variable that *already* holds a missing font is
|
|
141
|
+
> blocked, because Figma cannot re-render bound text in a font that will not
|
|
142
|
+
> load. Temporarily flip the consuming containers' mode
|
|
143
|
+
> (`node.explicitVariableModes`), set the variable, then flip back. Do **not**
|
|
144
|
+
> override `node.fontName` on bound instance text — that detaches the binding and
|
|
145
|
+
> breaks the other modes.
|
|
146
|
+
|
|
147
|
+
## Step 6: Verify
|
|
148
|
+
|
|
149
|
+
1. **Every pushed token reads as an _Alias_** in the inspector — an alias pill,
|
|
150
|
+
not a colour swatch. Programmatically, assert every `valuesByMode` entry has
|
|
151
|
+
`type === 'VARIABLE_ALIAS'`. A swatch means Step 4 was done wrong.
|
|
152
|
+
2. **Resolve and diff against the source.** Follow each alias to a concrete
|
|
153
|
+
colour and confirm it matches `styles/tokens-<brand>-light.css`. A
|
|
154
|
+
concatenated-hex checksum per variable across both modes is a cheap way to
|
|
155
|
+
compare in bulk.
|
|
156
|
+
3. **Switch modes in Figma** and confirm the design visibly changes: your light
|
|
157
|
+
mode shows your accent colour, dark mode shows dark surfaces with readable
|
|
158
|
+
text.
|
|
159
|
+
4. **Spot-check a component instance.** A primary Button in your mode should be
|
|
160
|
+
your accent colour, which proves the whole component → semantic → alias →
|
|
161
|
+
primitive chain resolved.
|
|
162
|
+
|
|
163
|
+
## Troubleshooting
|
|
164
|
+
|
|
165
|
+
| Symptom | Cause |
|
|
166
|
+
|---|---|
|
|
167
|
+
| Tokens show a colour swatch, not an alias pill | Pushed as raw hex — rebind per Step 4 |
|
|
168
|
+
| Mode switching changes nothing | Every mode was bound to the same target; each needs its own |
|
|
169
|
+
| Text renders as missing font | A CSS stack or an uninstalled family was pushed |
|
|
170
|
+
| Semibold and bold look identical | Numeric weights were pushed instead of style names |
|
|
171
|
+
| Transparency ramps look opaque | A hex string was pushed instead of `{r, g, b, a}` |
|
|
172
|
+
| `figma-console` tools absent | Session predates `.mcp.json`; approve via `/mcp` and restart |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup-muka
|
|
3
|
-
description:
|
|
3
|
+
description: Install Muka UI in this repo from npm, wire up styles and fonts, add the auto-update workflow, and register as a consumer.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
argument-hint: "[brand]"
|
|
6
6
|
---
|
|
@@ -8,16 +8,17 @@ argument-hint: "[brand]"
|
|
|
8
8
|
# Set up Muka UI in this repo
|
|
9
9
|
|
|
10
10
|
Configure the current repo to consume the **public npm package**
|
|
11
|
-
`@revikornmann/muka-ui`
|
|
12
|
-
|
|
13
|
-
`bouwplan`); default brand is `muka`.
|
|
11
|
+
`@revikornmann/muka-ui`. The optional `$ARGUMENTS` is the brand to theme with
|
|
12
|
+
(`muka`, `wireframe`, `grip`, `fscl`, `bouwplan`); default is `muka`.
|
|
14
13
|
|
|
15
14
|
Muka UI is a public npm package — **no `.npmrc`, registry config, or auth token
|
|
16
|
-
is required**.
|
|
17
|
-
steps lives at
|
|
15
|
+
is required**. Once installed, the version-locked copy of these steps lives at
|
|
18
16
|
`node_modules/@revikornmann/muka-ui/docs/consumers/setup-instructions.md`; read
|
|
19
17
|
it and prefer it when present, since it always matches the installed version.
|
|
20
18
|
|
|
19
|
+
To build a **custom brand** for this repo instead of using one of Muka's five,
|
|
20
|
+
finish this skill first and then run `/add-brand`.
|
|
21
|
+
|
|
21
22
|
## Step 1: Install the package
|
|
22
23
|
|
|
23
24
|
If a previous git dependency exists (e.g.
|
|
@@ -39,26 +40,59 @@ npm ls @revikornmann/muka-ui # or: pnpm why … / yarn why …
|
|
|
39
40
|
|
|
40
41
|
## Step 2: Import styles and components
|
|
41
42
|
|
|
42
|
-
In the app's root/entry, import
|
|
43
|
+
In the app's root/entry, import one stylesheet, then components anywhere:
|
|
43
44
|
|
|
44
45
|
```ts
|
|
45
46
|
import '@revikornmann/muka-ui/styles';
|
|
46
47
|
import { Button, Card, Input } from '@revikornmann/muka-ui';
|
|
47
48
|
```
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
`/styles` is the **`muka-light`** bundle: base styles, the muka-light tokens,
|
|
51
|
+
every component's CSS, and muka's self-hosted fonts. Four pre-bundled
|
|
52
|
+
alternatives ship alongside it and are drop-in replacements:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import '@revikornmann/muka-ui/styles/muka-dark.css';
|
|
56
|
+
import '@revikornmann/muka-ui/styles/wireframe-light.css';
|
|
57
|
+
import '@revikornmann/muka-ui/styles/wireframe-dark.css';
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For `grip`, `fscl`, or `bouwplan` there is no pre-bundled file, so compose it
|
|
61
|
+
from three imports — base styles, the brand's raw token CSS, and the brand's
|
|
62
|
+
fonts (the raw token CSS carries no `@font-face`):
|
|
51
63
|
|
|
52
64
|
```ts
|
|
65
|
+
import '@revikornmann/muka-ui/styles/base.css';
|
|
53
66
|
import '@revikornmann/muka-ui/styles/tokens-<brand>-light.css';
|
|
67
|
+
import '@revikornmann/muka-ui/styles/fonts-<brand>.css';
|
|
54
68
|
```
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
|
|
70
|
+
### How brand and theme selection actually works
|
|
71
|
+
|
|
72
|
+
Every token stylesheet declares its variables on `:root`. There are **no
|
|
73
|
+
`[data-brand]` or `[data-theme]` selectors**, so you select a brand and theme by
|
|
74
|
+
**choosing which stylesheet is loaded**, and importing two of them means the
|
|
75
|
+
last one wins. Do not add `data-brand` / `data-theme` attributes expecting them
|
|
76
|
+
to switch anything.
|
|
77
|
+
|
|
78
|
+
For a runtime light/dark toggle, swap a `<link>` instead of importing both:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const link = document.getElementById('muka-theme') as HTMLLinkElement;
|
|
82
|
+
link.href = `/styles/tokens-${brand}-${mode}.css`;
|
|
58
83
|
```
|
|
59
84
|
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
Copy the brand's token CSS into the app's static/public directory (or let the
|
|
86
|
+
bundler emit it with a stable URL) so those hrefs resolve. This is exactly what
|
|
87
|
+
Muka's own Storybook does.
|
|
88
|
+
|
|
89
|
+
### Fonts come from Muka — do not load them yourself
|
|
90
|
+
|
|
91
|
+
Muka self-hosts every brand's fonts as `@font-face` backed by bundled WOFF2.
|
|
92
|
+
Do **not** load them again via `next/font`, a Google Fonts `<link>`, or another
|
|
93
|
+
loader, and do **not** override the `--alias-font-*-family` tokens — the token
|
|
94
|
+
values are the exact `@font-face` family names, so overriding them only breaks
|
|
95
|
+
resolution.
|
|
62
96
|
|
|
63
97
|
## Step 3: Add the auto-update workflow
|
|
64
98
|
|
|
@@ -82,6 +116,10 @@ lockfile, `lockfileVersion: 9.0` → pnpm 9). Without it, CI's corepack pulls th
|
|
|
82
116
|
`minimumReleaseAge` default rejecting a just-published version). Add it if
|
|
83
117
|
missing.
|
|
84
118
|
|
|
119
|
+
Muka is under active development and a new version can change component
|
|
120
|
+
behaviour or appearance. Keep CI checks in place so the update workflow's bump
|
|
121
|
+
is reviewed rather than trusted.
|
|
122
|
+
|
|
85
123
|
## Step 4: Register this repo as a consumer
|
|
86
124
|
|
|
87
125
|
So releases dispatch updates here, add `owner/repo` to Muka's
|
|
@@ -90,9 +128,31 @@ Open a PR against `revikornmann/muka` adding the line (skip if it is already
|
|
|
90
128
|
listed). The Muka `CONSUMER_DISPATCH_TOKEN` PAT must have **write** access to
|
|
91
129
|
this repo for the `repository_dispatch` to succeed.
|
|
92
130
|
|
|
131
|
+
## Step 5: Give future agents the house rules
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npx muka-ui init
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This writes `AGENTS.md`, `CLAUDE.md`, and `docs/muka-ui-guidelines.md` into the
|
|
138
|
+
repo (it prompts before overwriting any of them) so any agent working here knows
|
|
139
|
+
to compose Muka components and use token custom properties instead of hardcoded
|
|
140
|
+
values. It also warns about styling libraries that conflict with Muka.
|
|
141
|
+
|
|
142
|
+
`AGENTS.md` carries the rules — it is the filename most agents read — and
|
|
143
|
+
`CLAUDE.md` is a short pointer to it, so there is only one copy to keep current.
|
|
144
|
+
|
|
93
145
|
## Verify
|
|
94
146
|
|
|
95
147
|
- `npm ls @revikornmann/muka-ui` shows a semver version resolved from npmjs.
|
|
96
148
|
- The app builds/type-checks and Muka components render with brand tokens.
|
|
149
|
+
- A component's computed `background-color` resolves to a real value, not the
|
|
150
|
+
`var(...)` fallback — that is the check that the token CSS actually loaded.
|
|
97
151
|
- `.github/workflows/update-muka.yml` exists and is valid YAML.
|
|
98
152
|
- This repo appears in Muka's `.github/consumers.txt` (or a PR is open for it).
|
|
153
|
+
|
|
154
|
+
## Next
|
|
155
|
+
|
|
156
|
+
- `/add-brand` — build a custom brand that overrides Muka's brand layer.
|
|
157
|
+
- `/figma-to-code` — turn a Figma link into a screen built from Muka components.
|
|
158
|
+
- `npx muka-ui install-skill --list` — every skill shipped with the package.
|
package/tokens/README.md
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
# Design Tokens
|
|
2
2
|
|
|
3
3
|
## Directory Structure
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
tokens/
|
|
7
|
-
├── t1-primitives/ # Raw values: color.json, scale.json, typography.json
|
|
7
|
+
├── t1-primitives/ # Raw values: color.json, scale.json, typography.json, motion.json
|
|
8
8
|
├── t2-alias/
|
|
9
9
|
│ ├── base.json # Brand-agnostic aliases
|
|
10
|
-
│ ├── brand/
|
|
11
|
-
│ │ ├── muka/ #
|
|
12
|
-
│ │ ├── wireframe/
|
|
13
|
-
│ │
|
|
10
|
+
│ ├── brand/ # One directory per brand, each with fonts/light/dark.json
|
|
11
|
+
│ │ ├── muka/ # + radius.json
|
|
12
|
+
│ │ ├── wireframe/
|
|
13
|
+
│ │ ├── grip/
|
|
14
|
+
│ │ ├── fscl/
|
|
15
|
+
│ │ └── bouwplan/
|
|
14
16
|
│ └── layout/
|
|
15
17
|
│ ├── mobile.json # Mobile-first base (default)
|
|
16
18
|
│ ├── tablet.json # md breakpoint (768px) overrides
|
|
17
|
-
│
|
|
19
|
+
│ ├── desktop.json # lg breakpoint (1024px) overrides
|
|
20
|
+
│ └── wide.json # xl breakpoint (1280px) overrides
|
|
18
21
|
├── t3-semantics/ # ui.json — usage-intent tokens
|
|
19
|
-
├── t4-components/ #
|
|
22
|
+
├── t4-components/ # 35 component token files
|
|
20
23
|
├── $themes.json # Tokens Studio theme config
|
|
21
24
|
└── $metadata.json # Token metadata
|
|
22
25
|
```
|
|
23
26
|
|
|
27
|
+
Five brands × light/dark = **10 theme builds**, each emitting one
|
|
28
|
+
`styles/tokens-<brand>-<theme>.css`. The authoritative list is
|
|
29
|
+
`build/manifest.json`.
|
|
30
|
+
|
|
31
|
+
The whole `tokens/` tree ships in the npm package, so consumers can build a
|
|
32
|
+
custom brand against these primitives without forking. See
|
|
33
|
+
`docs/consumers/brand.md`.
|
|
34
|
+
|
|
24
35
|
## Naming Convention
|
|
25
36
|
|
|
26
37
|
```
|
|
@@ -38,14 +49,18 @@ tokens/
|
|
|
38
49
|
{ "$value": "{color.gray.11}" }
|
|
39
50
|
```
|
|
40
51
|
|
|
52
|
+
Dark-mode ramps append a **camelCase `Dark`** — `{color.mauveDark.1}`,
|
|
53
|
+
`{color.indigoDark.9}`. References are case-sensitive, so `{color.mauvedark.1}`
|
|
54
|
+
resolves to nothing.
|
|
55
|
+
|
|
41
56
|
## Inheritance Order
|
|
42
57
|
|
|
43
58
|
1. T1 Primitives (base values)
|
|
44
59
|
2. T2 Alias (brand-agnostic references)
|
|
45
60
|
3. T3 Semantic (meaningful abstractions)
|
|
46
61
|
4. T4 Component (component-specific)
|
|
47
|
-
5. Brand overrides (
|
|
48
|
-
6. Theme overrides (light.json
|
|
62
|
+
5. Brand overrides (`brand/<name>/`)
|
|
63
|
+
6. Theme overrides (`light.json`, `dark.json`)
|
|
49
64
|
|
|
50
65
|
## CSS Output
|
|
51
66
|
|
|
@@ -55,17 +70,23 @@ tokens/
|
|
|
55
70
|
|
|
56
71
|
## Brand/Theme Switching
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
Every generated stylesheet declares its variables on `:root`, so the active
|
|
74
|
+
brand and theme are determined by **which stylesheet is loaded** — there are no
|
|
75
|
+
`[data-brand]` / `[data-theme]` selectors, and loading two means the last one
|
|
76
|
+
wins.
|
|
77
|
+
|
|
78
|
+
Storybook switches by swapping a `<link href>` (`.storybook/ThemeDecorator.ts`);
|
|
79
|
+
consumers do the same, or import a single theme statically.
|
|
61
80
|
|
|
62
81
|
## Build Commands
|
|
63
82
|
|
|
64
83
|
```bash
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
node scripts/export-for-penpot.js # Export for Penpot
|
|
84
|
+
npm run build:tokens # Build all themes → styles/tokens-*.css
|
|
85
|
+
npm run build:fonts # Self-host brand fonts → styles/fonts-*.css
|
|
68
86
|
npm run validate:tokens # Validate JSON structure
|
|
87
|
+
node scripts/export-figma-variables.js # Export for Figma Variables
|
|
88
|
+
node scripts/export-for-penpot.js # Export for Penpot
|
|
89
|
+
node scripts/export-for-webflow.js # Export for Webflow
|
|
69
90
|
```
|
|
70
91
|
|
|
71
92
|
## Rules
|
|
@@ -92,5 +113,6 @@ to flow upward by reference; a brand file cannot override a `{component}.*` toke
|
|
|
92
113
|
## Human Docs
|
|
93
114
|
<!-- SYNC: Update paths when Storybook docs change -->
|
|
94
115
|
- Token architecture guide: stories/tokens/01-Introduction.mdx
|
|
95
|
-
- Design Tokens in Storybook: stories/tokens/*.mdx (
|
|
116
|
+
- Design Tokens in Storybook: stories/tokens/*.mdx (7 pages)
|
|
117
|
+
- Consumer brand guide: docs/consumers/brand.md
|
|
96
118
|
- All Storybook docs: `npm run dev` → localhost:6006
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"family": { "$type": "fontFamilies", "$value": "Red Hat Text" },
|
|
19
19
|
"weight": {
|
|
20
20
|
"regular": { "$type": "fontWeights", "$value": "400" },
|
|
21
|
-
"medium": { "$type": "fontWeights", "$value": "
|
|
21
|
+
"medium": { "$type": "fontWeights", "$value": "500" },
|
|
22
22
|
"semibold": { "$type": "fontWeights", "$value": "500" },
|
|
23
23
|
"bold": { "$type": "fontWeights", "$value": "600" }
|
|
24
24
|
}
|