@rogieking/figui3 8.9.10 → 8.9.12

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.
@@ -1,54 +1,34 @@
1
1
  ---
2
2
  name: propkit
3
- description: Guides creation and refinement of Figma-style property panel patterns ("PropKit") using FigUI3 components. Applies when building or modifying property fields in the playground app (`/propkit` route), generating consistent field prompts, composing horizontal `fig-field` rows, or tuning panel UX for controls like image, color, fill, slider, switch, dropdown, segmented control, easing, and angle.
3
+ description: >-
4
+ Guides Figma-style property panel composition in the /propskit playground using
5
+ fig-field rows and FigUI3 controls. Use when building or editing
6
+ playground/src/data/sections.ts, generating field prompts, or choosing between
7
+ raw fig-* rows and propskit-* wrappers (fig-lab).
4
8
  user-invocable: false
5
9
  ---
6
10
 
7
11
  # PropKit
8
12
 
9
- Patterns for composing clean, production-ready Figma property panels with FigUI3.
13
+ Patterns for Figma property panels. Two layers:
10
14
 
11
- > IMPORTANT: Favor composition and consistency over custom one-off controls. Build panels from existing `fig-*` elements first.
15
+ | Surface | Route | What to use |
16
+ |---|---|---|
17
+ | **PropsKit playground** | `/propskit` | Horizontal `fig-field` + core `fig-*` |
18
+ | **Lab wrappers** | `/propskit/lab` | `propskit-*` (see `fig-lab` skill) |
12
19
 
13
- ## Current Project Context
14
-
15
- ```json
16
- !`node -e "const fs=require('fs'); const ok=fs.existsSync('playground/src/main.tsx'); console.log(JSON.stringify({playground:ok, route:'/propkit', example:'horizontal fig-field + label + fig-* control'},null,2))" 2>/dev/null || echo '{"error":"context unavailable"}'`
17
- ```
20
+ Canonical `/propskit` examples: `playground/src/data/sections.ts`.
21
+ Core control APIs: `figui3` skill. Select/fill picker: `fig-editor`. Labeled wrappers: `fig-lab`.
18
22
 
19
23
  ## Principles
20
24
 
21
- 1. **Use horizontal property rows by default.** PropKit fields are primarily `fig-field direction="horizontal"`.
22
- 2. **One clear label per control.** Keep labels concise and aligned with Figma property language.
23
- 3. **Prefer native FigUI3 controls.** Use `fig-input-fill`, `fig-slider`, `fig-dropdown`, `fig-switch`, etc.
24
- 4. **Use realistic panel widths and spacing.** Match the property panel feel (`~240px` panel blocks in demos).
25
- 5. **Keep prompts and examples deterministic.** Prompt text should describe exact structure and key attributes.
26
-
27
- ## React + Vite PropKit Usage
28
-
29
- ### Include FigUI3 in React projects
30
-
31
- - Import once in app bootstrap:
32
- - `import "@rogieking/figui3/fig.css";`
33
- - `await import("@rogieking/figui3/fig.js");`
34
- - Register components before first React render to avoid undefined custom elements.
35
- - Keep this setup in entry files (`main.tsx` / `main.jsx`), not scattered across feature components.
36
-
37
- ### Vite setup and tree-shaking behavior
25
+ 1. Default to horizontal `fig-field` rows.
26
+ 2. One concise label per control.
27
+ 3. For new labeled property controls in lab, prefer `propskit-*` over duplicating field chrome.
28
+ 4. In `/propskit` demos, keep composing from `fig-*` so examples stay core-only unless the section needs lab.
29
+ 5. Panel width ~240px. Match existing section density.
38
30
 
39
- - Base Vite React config is sufficient in most cases:
40
-
41
- ```ts
42
- import { defineConfig } from "vite";
43
- import react from "@vitejs/plugin-react";
44
-
45
- export default defineConfig({
46
- plugins: [react()],
47
- });
48
- ```
49
-
50
- - In production, FigUI3 side-effect registration can be tree-shaken if only imported for side effects.
51
- - Preferred pattern (from `webgpu-effects`) is explicit async bootstrap:
31
+ ## React bootstrap
52
32
 
53
33
  ```tsx
54
34
  import "@rogieking/figui3/fig.css";
@@ -57,169 +37,70 @@ const bootstrap = async () => {
57
37
  await import("@rogieking/figui3/fig.js");
58
38
  createRoot(document.getElementById("app")!).render(<App />);
59
39
  };
60
-
61
40
  bootstrap();
62
41
  ```
63
42
 
64
- ### React composition conventions for PropKit rows
65
-
66
- - Continue using canonical row shape in JSX:
67
- - `<fig-field direction="horizontal">` + `<label>` + one primary `fig-*` control.
68
- - For customized built-ins in React (`<dialog is="fig-popup">` / `<dialog is="fig-dialog">`), use `class`, not `className`.
69
- - Use refs and native event listeners (`input`, `change`) for reliable control updates.
70
-
71
- ## Critical Rules
72
-
73
- ### Field Composition
74
-
75
- - Default pattern: label + single primary control inside one horizontal `fig-field`.
76
- - Keep control-specific options on the component itself (not hidden wrapper logic).
77
- - Use `full` where property controls should stretch within row constraints.
78
- - Avoid mixing unrelated controls in a single field row unless intentionally grouped.
79
-
80
- ### Prompt Generation Style
81
-
82
- - Write prompts as imperative build instructions.
83
- - Include field direction, control tag, and meaningful attrs.
84
- - Prefer short explicit phrasing over vague prose.
85
- - Keep wording consistent:
86
- - `Use a horizontal fig-field...`
87
- - `With a label of ...`
88
- - Include concrete defaults when relevant (value, min/max, step, units, mode, variant) so generated fields are deterministic.
89
- - Avoid placeholder-only prompts for numeric controls; always specify range semantics.
90
-
91
- ### Control Guidance
92
-
93
- - **Image:** prefer `fig-image` with `upload`, `fit`, and `aspect-ratio` where needed.
94
- - **Color:** use `fig-input-color` with `text="true"` and optional `alpha`.
95
- - **Fill:** use `fig-input-fill` for multi-mode fills; keep value JSON valid.
96
- - **Slider:** choose proper type (`range`, `opacity`, `hue`, `stepper`, `delta`) and include units/transform intentionally.
97
- - **Dropdown:** use `fig-dropdown`; include sensible default options.
98
- - **Boolean:** use `fig-switch`; avoid using dropdowns for true/false.
99
- - **Discrete choices:** use `fig-segmented-control` + `fig-segment`.
100
- - **Motion easing:** use `fig-easing-curve` with/without presets depending on context.
101
- - **Angle:** use `fig-input-angle` with `text="true"` for precision workflows.
102
-
103
- ### Slider Types and Variants
104
-
105
- - Default to `type="range"` for generic numeric properties (opacity %, size, spacing, intensity).
106
- - Use `type="opacity"` when color context is needed (set `color` and usually `units="%"`).
107
- - Use `type="hue"` only for hue selection workflows.
108
- - Use `type="stepper"` for discrete snap points (include a `datalist` with valid stops).
109
- - Use `type="delta"` for offset/relative adjustments around a neutral point (typically include `default`, and often symmetric min/max).
110
- - Text input is shown by default; use `text="false"` only for compact/simplified rows.
111
- - Use `transform` when internal value scale differs from UI display (example: internal `0..1`, display `0..100%`).
112
- - Variants:
113
- - Default variant for most property panels.
114
- - `variant="classic"` only when the previous slider appearance is needed.
115
- - Always set explicit `min`, `max`, and `step` (and `units` where applicable) to keep behavior predictable.
116
- - Set `default` on PropsKit value controls when reset behavior should differ from the initial value.
117
- - PropsKit value controls support `resetToDefault()` and a right-click **Reset** menu; `propskit-slider` also supports double-click reset.
118
-
119
- ### Control Selection Heuristics
120
-
121
- - Use `fig-slider` for scrub-friendly continuous values (opacity, intensity, scale, blur amount).
122
- - Use `fig-input-number` for precise direct entry (sizes, coordinates, exact typed values).
123
- - Use slider + text (`text="true"`) when users need both quick scrubbing and precise adjustment.
124
- - Use `fig-segmented-control` for small discrete sets (2-5 fixed options).
125
- - Use `fig-dropdown` for larger or less frequently switched option sets.
126
- - Use `fig-switch` for binary state, never slider/dropdown for pure on/off.
127
-
128
- ### UX Consistency
129
-
130
- - Keep panel patterns visually consistent across sections.
131
- - Preserve theme behavior (light/dark) and avoid non-token color overrides.
132
- - Ensure labels and controls remain keyboard and screen-reader usable.
133
-
134
- ## Key Patterns
43
+ Add `fig-editor` when using `fig-select` / fill picker. Add `fig-lab` when using `propskit-*`.
44
+
45
+ On `fig-*` and `<dialog is="fig-...">`, use `class` not `className`.
46
+
47
+ ## Field composition
135
48
 
136
49
  ```html
137
- <!-- Canonical PropKit row -->
138
50
  <fig-field direction="horizontal">
139
51
  <label>Opacity</label>
140
52
  <fig-slider value="75" min="0" max="100" text="true" units="%" full></fig-slider>
141
53
  </fig-field>
142
54
  ```
143
55
 
144
- ```html
145
- <!-- Non-horizontal (stacked/default column) field -->
146
- <fig-field>
147
- <label>Opacity</label>
148
- <fig-slider value="75" min="0" max="100" text="true" units="%"></fig-slider>
149
- </fig-field>
150
- ```
151
-
152
- ```html
153
- <!-- Fill + blend pair -->
154
- <fig-field direction="horizontal">
155
- <label>Fill</label>
156
- <fig-input-fill value='{"type":"solid","color":"#667eea"}'></fig-input-fill>
157
- </fig-field>
158
- <fig-field direction="horizontal">
159
- <label>Blend</label>
160
- <fig-dropdown full>
161
- <option selected>Normal</option>
162
- <option>Multiply</option>
163
- </fig-dropdown>
164
- </fig-field>
165
- ```
56
+ - Put control attrs on the control, not a wrapper.
57
+ - Use `full` when the control should stretch.
58
+ - Do not mix unrelated controls in one row unless grouped on purpose.
59
+
60
+ ## Control heuristics
61
+
62
+ | Intent | `/propskit` (core) | Lab wrapper |
63
+ |---|---|---|
64
+ | Boolean | `fig-switch` | `propskit-switch` |
65
+ | Continuous number | `fig-slider` | `propskit-slider` |
66
+ | Exact number | `fig-input-number` | `propskit-number` |
67
+ | Text | `fig-input-text` | `propskit-text` |
68
+ | Small discrete set (2–5) | `fig-segmented-control` | — |
69
+ | Larger / rich list | `fig-select` (editor) | `propskit-select` |
70
+ | Native select only | `fig-dropdown` | — |
71
+ | Color | `fig-input-color` `text="true"` | `propskit-color` |
72
+ | Fill | `fig-input-fill` | — |
73
+ | Gradient | `fig-input-gradient` | `propskit-gradient` |
74
+ | Image | `fig-image` `upload` | — |
75
+ | Easing | `fig-easing-curve` | — |
76
+ | Angle | `fig-input-angle` (**lab**) | — |
77
+ | X/Y | `fig-joystick` or two numbers | `propskit-position` |
78
+
79
+ Do not use dropdown/slider for pure on/off. Do not use `fig-dropdown` for Figma-style property selects when `fig-select` is available.
80
+
81
+ ## Slider rules
82
+
83
+ - Default `type="range"`. Always set `min`, `max`, `step`.
84
+ - `opacity`: set `color`, usually `units="%"`
85
+ - `hue`: hue workflows only
86
+ - `stepper`: include a datalist of stops
87
+ - `delta`: include `default`, often symmetric min/max
88
+ - Text field on by default; `text="false"` for compact rows
89
+ - `transform` when internal scale ≠ display scale
90
+ - `variant="classic"` only when the old look is required
91
+
92
+ Prompt style: imperative, include direction, tag, and behavior-critical attrs.
166
93
 
167
94
  ```txt
168
- Prompt pattern:
169
95
  Use a horizontal fig-field, with a fig-slider, min=0 max=100 text=true units=%. With a label of Opacity.
170
96
  ```
171
97
 
172
- ```html
173
- <!-- Slider type/variant examples -->
174
- <fig-field direction="horizontal">
175
- <label>Opacity</label>
176
- <fig-slider type="opacity" value="0.75" color="#0D99FF" units="%" text="true" full></fig-slider>
177
- </fig-field>
178
- <fig-field direction="horizontal">
179
- <label>Hue</label>
180
- <fig-slider type="hue" value="180" text="true" full></fig-slider>
181
- </fig-field>
182
- <fig-field direction="horizontal">
183
- <label>Offset</label>
184
- <fig-slider type="delta" value="0" default="0" min="-5" max="5" step="0.25" text="true" full></fig-slider>
185
- </fig-field>
186
- ```
187
-
188
98
  ## Workflow
189
99
 
190
- 1. **Identify property intent.** Determine if control is boolean, discrete choice, continuous numeric, color/fill, media, or motion.
191
- 2. **Pick the canonical FigUI3 control.** Avoid custom alternatives unless required.
192
- 3. **Compose row structure.** Use horizontal `fig-field`, then label + control.
193
- 4. **Set defaults and attrs explicitly.** Include values/ranges/units so behavior is deterministic.
194
- 5. **Verify panel consistency.** Check row spacing, width, and theme parity against existing PropKit sections.
195
- 6. **Validate events and interactions.** Ensure controls emit usable `input`/`change` and behave well in keyboard workflows.
196
-
197
- ## Delivery Checklist
198
-
199
- - Confirm prompts include all behavior-critical attrs (`value`, `min`, `max`, `step`, `units`, `type`, `variant` as needed).
200
- - Confirm control choice matches intent (continuous vs discrete vs boolean vs exact numeric entry).
201
- - Verify row density and panel width feel consistent with existing PropKit sections.
202
- - Verify keyboard navigation and label association for every field row.
203
- - Verify changes in `playground/src/data/sections.ts` still mirror recommended patterns in this skill.
204
-
205
- ## Quick Reference
206
-
207
- ```txt
208
- Common PropKit controls:
209
- - fig-image
210
- - fig-input-color
211
- - fig-input-fill
212
- - fig-slider
213
- - fig-switch
214
- - fig-dropdown
215
- - fig-segmented-control
216
- - fig-easing-curve
217
- - fig-input-angle
218
- ```
219
-
220
- ## Primary Files
100
+ 1. Identify intent (boolean, discrete, continuous, color/fill, media, motion).
101
+ 2. Pick core vs lab wrapper.
102
+ 3. Compose the row; set defaults explicitly.
103
+ 4. Check `/propskit` or `/propskit/lab` for an existing example before inventing markup.
104
+ 5. Verify `input`/`change` and keyboard.
221
105
 
222
- - `playground/src/data/sections.ts` - canonical PropKit examples and prompt-copy behavior
223
- - `fig.js` - control behavior and emitted events
224
- - `components.css` - visual treatment and layout constraints
225
- - `README.md` - component API details and usage
106
+ Primary files: `playground/src/data/sections.ts`, `playground/src/data/labSections.ts`, `fig.js`, `fig-lab.js`.
package/README.md CHANGED
@@ -72,6 +72,13 @@ Or use a CDN:
72
72
  <script type="module" src="https://unpkg.com/@rogieking/figui3@latest/dist/fig.js"></script>
73
73
  ```
74
74
 
75
+ Agent skills ship in `.cursor/skills/` (included in the npm package):
76
+
77
+ - `figui3` — core `fig.js` components
78
+ - `fig-editor` — `fig-select` and `fig-fill-picker`
79
+ - `fig-lab` — experimental `propskit-*`, canvas, AI, angle, reorder
80
+ - `propkit` — `/propskit` property-row composition
81
+
75
82
  Minimal example:
76
83
 
77
84
  ```html
@@ -90,6 +97,7 @@ Minimal example:
90
97
  |---|---|---|
91
98
  | [Button](#button) | `<fig-button>` | Buttons with variants, toggle, select, upload |
92
99
  | [Dropdown](#dropdown) | `<fig-dropdown>` | Native select wrapper with Figma styling |
100
+ | [Select](#select) | `<fig-select>` | Custom listbox select (requires `fig-editor.js`) |
93
101
  | [Combo Input](#combo-input) | `<fig-combo-input>` | Text input with dropdown suggestions |
94
102
  | [Checkbox](#checkbox) | `<fig-checkbox>` | Checkbox with indeterminate state |
95
103
  | [Radio](#radio) | `<fig-radio>` | Radio button |
@@ -192,6 +200,7 @@ Minimal example:
192
200
  | `value` | string | — | Selected value |
193
201
  | `type` | string | `"select"` | `"select"` or `"dropdown"` |
194
202
  | `label` | string | — | Accessible label for the generated native `<select>` |
203
+ | `variant` | string | — | `"ghost"` for a borderless control with secondary hover fill |
195
204
  | `disabled` | boolean | `false` | Disabled state |
196
205
 
197
206
  ```html
@@ -205,6 +214,38 @@ Keyboard activation follows the native select pattern. Enter opens the closed pi
205
214
 
206
215
  ---
207
216
 
217
+ #### Select
218
+
219
+ `<fig-select>` — [demo](https://rog.ie/figui3/#select)
220
+
221
+ Custom listbox select with overflow chevrons, grouped options, and sticky separators. Import `fig-editor.js` and `fig-editor.css`. Prefer this over `fig-dropdown` for Figma-style menus.
222
+
223
+ | Attribute | Type | Default | Description |
224
+ |---|---|---|---|
225
+ | `value` | string | — | Selected option value |
226
+ | `label` | string | — | Closed-state / accessible label |
227
+ | `options` | string | — | Comma, newline, or JSON options if no authored `fig-select-option` children |
228
+ | `variant` | string | — | `"ghost"` for a borderless control with secondary hover fill |
229
+ | `full` | boolean | `false` | Stretch to available width |
230
+ | `position` | string | `"bottom left"` | Popup position |
231
+ | `disabled` | boolean | `false` | Disabled state |
232
+
233
+ Author options in `<fig-select-options>`, or pass `options`. Use `label` on `<fig-select-option>` when the option content is rich. `fig-separator` with `sticky` pins group labels while scrolling.
234
+
235
+ ```html
236
+ <fig-select value="center" label="Align">
237
+ <fig-select-options>
238
+ <fig-select-option value="left">Left</fig-select-option>
239
+ <fig-select-option value="center">Center</fig-select-option>
240
+ <fig-select-option value="right">Right</fig-select-option>
241
+ </fig-select-options>
242
+ </fig-select>
243
+ ```
244
+
245
+ **Events:** `input`, `change`, `optionhover` (`detail` is the hovered option value).
246
+
247
+ ---
248
+
208
249
  #### Combo Input
209
250
 
210
251
  `<fig-combo-input>` — [demo](https://rog.ie/figui3/#combo-input)
@@ -1328,6 +1369,7 @@ An anchored floating surface built on `<dialog>` with collision-aware positionin
1328
1369
  | `drag` | boolean | `false` | Draggable |
1329
1370
  | `handle` | string | — | CSS selector for drag handle |
1330
1371
  | `autoresize` | boolean | `false` | Auto-resize to content |
1372
+ | `title` | string | — | Auto-generated header (same as `fig-dialog`) |
1331
1373
 
1332
1374
  ```html
1333
1375
  <dialog is="fig-popup" anchor="#my-button" position="center right" variant="popover">
@@ -1410,6 +1452,10 @@ A visual divider between content groups. The optional `label` attribute adds a g
1410
1452
 
1411
1453
  Triggered menu with native keyboard patterns. The trigger gets `aria-haspopup="menu"`, `aria-expanded`, and `aria-controls`; menu items use `role="menuitem"` and disabled items are skipped by keyboard navigation.
1412
1454
 
1455
+ Items stay in the menu's light DOM and project into the popup through slots (same pattern as `fig-select` / `fig-select-options`). The trigger is assigned `slot="trigger"` automatically; items use the default slot. React can add or remove `fig-menu-item` children without `removeChild` errors.
1456
+
1457
+ `fig-menu-item` also works as a list row outside `fig-menu` — typically in `<dialog is="fig-popup">` with `<fig-content padding="none">`, sticky `<fig-separator>`s, and a nested `<fig-menu>` for row actions. Item color inherits from the parent surface.
1458
+
1413
1459
  | Attribute | Type | Default | Description |
1414
1460
  |---|---|---|---|
1415
1461
  | `open` | boolean | `false` | Open state |
@@ -1435,6 +1481,25 @@ Triggered menu with native keyboard patterns. The trigger gets `aria-haspopup="m
1435
1481
  </fig-menu>
1436
1482
  ```
1437
1483
 
1484
+ Popup list (no wrapping `fig-menu`):
1485
+
1486
+ ```html
1487
+ <dialog is="fig-popup" title="Version history" anchor="#versions" position="bottom left">
1488
+ <fig-content padding="none">
1489
+ <fig-separator sticky label="Today"></fig-separator>
1490
+ <fig-menu-item value="v9" subtle>
1491
+ Version 9
1492
+ <fig-menu position="bottom right">
1493
+ <fig-button fig-menu-trigger variant="ghost" icon aria-label="More">
1494
+ <fig-icon name="more"></fig-icon>
1495
+ </fig-button>
1496
+ <fig-menu-item value="restore">Restore this version</fig-menu-item>
1497
+ </fig-menu>
1498
+ </fig-menu-item>
1499
+ </fig-content>
1500
+ </dialog>
1501
+ ```
1502
+
1438
1503
  `fig-separator` and `fig-menu-separator` accept optional `label` — renders the rule, then secondary group text underneath.
1439
1504
 
1440
1505
  ---
@@ -1456,7 +1521,7 @@ A section header component.
1456
1521
  `<fig-layer>` — [demo](https://rog.ie/figui3/#layer)
1457
1522
 
1458
1523
  A collapsible layer list item with expand/collapse and visibility toggling. Supports nesting and exposes `role="treeitem"`, `aria-expanded`, `aria-hidden`, `aria-disabled`, and a keyboard-toggleable chevron button.
1459
- Import `fig-layer.js` and `fig-layer.css` to register and style it. `fig-editor.js` also includes the layer registration.
1524
+ Import `fig-layer.js` and `fig-layer.css` to register and style it.
1460
1525
 
1461
1526
  | Attribute | Type | Default | Description |
1462
1527
  |---|---|---|---|
package/components.css CHANGED
@@ -3143,6 +3143,27 @@ dialog[is="fig-popup"] {
3143
3143
  display: block;
3144
3144
  }
3145
3145
 
3146
+ &[open]:not([open="false"]):has(> fig-content) {
3147
+ display: flex;
3148
+ flex-direction: column;
3149
+ overflow: hidden;
3150
+ }
3151
+
3152
+ > fig-header {
3153
+ position: sticky;
3154
+ top: 0;
3155
+ z-index: 2;
3156
+ background: var(--fig-popup-bg-color, var(--figma-color-bg));
3157
+ }
3158
+
3159
+ &:has(> fig-header) {
3160
+ --fig-sticky-header-size: var(--spacer-6);
3161
+
3162
+ > :is(fig-separator, fig-menu-separator)[sticky]:not([sticky="false"]) {
3163
+ top: var(--fig-sticky-header-size);
3164
+ }
3165
+ }
3166
+
3146
3167
 
3147
3168
  &[theme="dark"] {
3148
3169
  color-scheme: dark;
@@ -3346,6 +3367,20 @@ fig-dropdown {
3346
3367
  display: flex;
3347
3368
  width: 100%;
3348
3369
  }
3370
+
3371
+ &[variant="ghost"] {
3372
+ border-radius: var(--radius-medium);
3373
+ background: transparent;
3374
+
3375
+ > select {
3376
+ box-shadow: none;
3377
+ background: transparent;
3378
+ }
3379
+
3380
+ &:hover:not([disabled]):not([disabled="false"]) {
3381
+ background-color: var(--figma-color-bg-secondary);
3382
+ }
3383
+ }
3349
3384
  }
3350
3385
 
3351
3386
  fig-checkbox,
@@ -3597,13 +3632,17 @@ fig-content {
3597
3632
  }
3598
3633
  }
3599
3634
 
3600
- fig-content[padding]:not([padding="false"]):not(:empty) {
3601
- padding-inline: var(--spacer-3);
3602
- }
3603
-
3604
- fig-content:empty,
3605
- fig-content:not(:has(*)) {
3606
- padding: 0;
3635
+ fig-content{
3636
+ &[padding]:not([padding="false"]):not([padding="none"]):not(:empty) {
3637
+ padding-inline: var(--spacer-3);
3638
+ }
3639
+ &:empty,
3640
+ &::not(:has(*)) {
3641
+ padding: 0;
3642
+ }
3643
+ &[padding="none"]{
3644
+ padding: 0;
3645
+ }
3607
3646
  }
3608
3647
 
3609
3648
  vstack,
@@ -5521,23 +5560,49 @@ fig-color-tip {
5521
5560
  /* Menu */
5522
5561
  fig-menu {
5523
5562
  display: contents;
5563
+ }
5524
5564
 
5525
- & > fig-menu-item,
5526
- & > :is(fig-separator, fig-menu-separator) {
5527
- display: none;
5528
- }
5565
+ /*
5566
+ * Internal menu is a fig-popup inside fig-menu's shadow, so document
5567
+ * dialog[is="fig-popup"] rules don't apply. Expose part=listbox and mirror
5568
+ * the menu-theme popup chrome (same tokens as fig-select::part(listbox)).
5569
+ */
5570
+ fig-menu::part(listbox) {
5571
+ --z-index: 999999;
5572
+ --fig-popup-radius: var(--radius-large);
5573
+ --fig-popup-bg-color: var(--figma-color-bg-menu);
5574
+
5575
+ z-index: var(--z-index);
5576
+ position: fixed;
5577
+ margin: 0;
5578
+ border: 0;
5579
+ outline: 0;
5580
+ padding: 0;
5581
+ overflow: hidden;
5582
+ border-radius: var(--fig-popup-radius);
5583
+ background-color: var(--fig-popup-bg-color);
5584
+ color: var(--figma-color-text-menu);
5585
+ color-scheme: dark;
5586
+ box-shadow: var(--figma-elevation-400-menu-panel);
5587
+ min-width: 6rem;
5588
+ max-height: calc(100vh - 1rem);
5589
+ height: max-content;
5529
5590
  }
5530
5591
 
5531
5592
  fig-menu-item {
5532
- --fig-menu-item-padding: var(--spacer-4);
5593
+ --fig-menu-item-padding: var(--spacer-1) var(--spacer-4);
5594
+ box-sizing: border-box;
5533
5595
  display: flex;
5596
+ flex-shrink: 0;
5534
5597
  align-items: center;
5535
5598
  gap: var(--spacer-2);
5536
- padding: 0 var(--fig-menu-item-padding);
5537
- height: var(--spacer-4);
5599
+ width: 100%;
5600
+ padding: var(--fig-menu-item-padding);
5601
+ min-height: var(--spacer-4);
5602
+ height: auto;
5538
5603
  cursor: default;
5539
5604
  border-radius: var(--radius-medium);
5540
- color: var(--figma-color-text-menu);
5605
+ color: inherit;
5541
5606
  font-weight: var(--body-medium-fontWeight);
5542
5607
  position: relative;
5543
5608
 
@@ -5545,6 +5610,23 @@ fig-menu-item {
5545
5610
  margin: 0 0 var(--spacer-1) 0;
5546
5611
  }
5547
5612
 
5613
+ label {
5614
+ color: var(--figma-color-text-secondary);
5615
+ }
5616
+
5617
+ > :not(fig-icon):not(fig-menu) {
5618
+ flex: 1 1 auto;
5619
+ min-width: 0;
5620
+ }
5621
+
5622
+ > fig-menu {
5623
+ flex: 0 0 auto;
5624
+ }
5625
+
5626
+ &[size="large"]{
5627
+ --fig-menu-item-padding: var(--spacer-2) var(--spacer-4);
5628
+ }
5629
+
5548
5630
  &::before {
5549
5631
  content: "";
5550
5632
  display: block;
@@ -5582,14 +5664,15 @@ fig-menu-item {
5582
5664
  :is(fig-separator, fig-menu-separator) {
5583
5665
  --fig-separator-color: var(--figma-color-border);
5584
5666
  --fig-separator-label-color: var(--figma-color-text-secondary);
5585
- --fig-menu-inline-padding: 0 var(--spacer-3);
5667
+ --fig-separator-padding: 0 var(--spacer-3);
5586
5668
 
5587
5669
  display: block;
5588
5670
  box-sizing: border-box;
5671
+ flex-shrink: 0;
5589
5672
  width: 100%;
5590
5673
  height: 1px;
5591
5674
  margin: var(--spacer-2) 0;
5592
- padding: 0;
5675
+ padding: var(--fig-separator-padding);
5593
5676
  border: 0;
5594
5677
  background: var(--fig-separator-color);
5595
5678
  color: var(--fig-separator-label-color);
@@ -5621,7 +5704,6 @@ fig-menu-item {
5621
5704
  height: auto;
5622
5705
  margin: var(--spacer-2) 0;
5623
5706
  margin-bottom: var(--spacer-1);
5624
- padding: var(--fig-menu-inline-padding);
5625
5707
  background: transparent;
5626
5708
 
5627
5709
  &::before {
@@ -5652,7 +5734,14 @@ fig-menu-item {
5652
5734
  z-index: 1;
5653
5735
  background: var(--fig-popup-bg-color, var(--figma-color-bg));
5654
5736
 
5655
- :is(fig-select-options, .fig-menu-options).overflow-start > & {
5737
+ &[stuck] {
5738
+ --fig-separator-padding: var(--spacer-2) var(--spacer-3);
5739
+ &::before {
5740
+ display: none;
5741
+ }
5742
+ }
5743
+
5744
+ :is(fig-select-options, .fig-menu-options, fig-menu).overflow-start > & {
5656
5745
  top: var(--fig-vertical-overflow-size, var(--spacer-4));
5657
5746
  }
5658
5747
  }
@@ -5666,18 +5755,8 @@ fig-select-options > :is(fig-separator, fig-menu-separator):first-child[label]:n
5666
5755
  }
5667
5756
 
5668
5757
  dialog[is="fig-popup"][theme="menu"] :is(fig-separator, fig-menu-separator),
5758
+ fig-menu > :is(fig-separator, fig-menu-separator),
5669
5759
  fig-select-options > :is(fig-separator, fig-menu-separator) {
5670
5760
  --fig-separator-color: var(--figma-color-border-menu);
5671
5761
  --fig-separator-label-color: var(--figma-color-text-menu-secondary);
5672
5762
  }
5673
-
5674
- dialog[is="fig-popup"][theme="menu"] :is(fig-separator, fig-menu-separator) {
5675
- --fig-menu-inline-padding: 0
5676
- var(--fig-menu-item-padding, var(--spacer-4));
5677
-
5678
- &[label]:not([label=""])::before {
5679
- margin-inline: calc(
5680
- -1 * var(--fig-menu-item-padding, var(--spacer-4))
5681
- );
5682
- }
5683
- }