Skip to content

Commit 822b8ef

Browse files
committed
feat: add advanced-data-grid, update side-navigation and storybook examples
1 parent eccae17 commit 822b8ef

102 files changed

Lines changed: 11425 additions & 4409 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
---
2+
name: figma-component-from-design-system
3+
description: >-
4+
Build Axis design-system-compliant Figma components from this codebase using the
5+
Figma MCP server. USE WHEN the user wants to create, mock up, or generate a UI
6+
component, screen, or pattern in the "Axis Global components" Figma file from an
7+
existing storybook composite, or asks to mirror a code component into Figma using
8+
real Axis Fluent 2 Web Components + bound design tokens (not raw hex/rectangles).
9+
Covers the code-first discovery loop, Plugin API gotchas, verified token/component
10+
keys, the boolean/text/instance-swap parameterization recipe, and the doc-template
11+
workflow for documenting a component from its storybook.
12+
---
13+
14+
# Building Figma components from Axis Fluent 2 (code → Figma)
15+
16+
Target file: **"Axis Global components"**, fileKey `0kVLp2qecBWQiQXEQidCeJ`.
17+
18+
Goal: recreate a codebase composite
19+
(`examples/src/storybook/ui-patterns/components/composites/*.tsx`) as a real Figma
20+
**main component** built from the org design system — real components + bound tokens,
21+
never raw hex or detached rectangles.
22+
23+
## Workflow
24+
25+
1. **Code-first.** Grep the component name under
26+
`examples/**/composites/*.tsx` and its `*.stories.tsx`. Mirror its tokens,
27+
spacing, layout, and variants from the source of truth.
28+
2. **`get_libraries(fileKey)`** to confirm the two libraries that matter:
29+
- **Axis Fluent 2 Web Components** libKey
30+
`lk-79792b042bb8ea6d9f072a72d0703aa5e6de468d3df881224f7d466047a61c492a70887e7bfc05c8dba114cf560b4264ca8ef2148b83c9c24d87695b50030c5f`
31+
- **Microsoft Fluent System Icons** libKey
32+
`lk-d18bb83ecc482ff3c41ebaaa01ef819aa8f0df14460446f03b83ff61703aca33f74ddc59d704f9f8d65b176a4298feb6d29af3dc4c4c0232396b3557d482f952`
33+
3. **`search_design_system`** scoped with `includeLibraryKeys` to fetch
34+
component / variable / style keys. Search is fuzzy — search exact names and
35+
ignore iOS / SF / Material look-alikes from other libraries.
36+
4. **Build with `use_figma`** (Plugin API JS). Pass `skillNames: "resource:figma-use"`.
37+
5. **Verify with `get_screenshot`** using `enableBase64Response: true` — local `*.png`
38+
download is blocked by Axis content-exclusion. `get_design_context` (with
39+
`disableCodeConnect: true`) also returns an inline screenshot and is the most
40+
reliable way to eyeball a node after each edit.
41+
42+
## ALWAYS document a new component with the Doc template
43+
44+
Whenever you create (or finish) a component, also produce a documentation page for it
45+
by cloning the shared **Doc template** — never hand-roll a doc layout. This keeps every
46+
component's docs visually consistent.
47+
48+
- The template is its own page **"Doc template"** (node `9:2957`); its single child
49+
frame is **"Tree"** (`9:4236`). This file is **dynamic-page**: a node on a page other
50+
than `figma.currentPage` is NOT reachable until you call `await page.loadAsync()` on
51+
its page first. `figma.getNodeById` returns `null` otherwise.
52+
- Workflow: `const dp = figma.getNodeById("9:2957"); await dp.loadAsync();`
53+
`const tpl = dp.children.find(n => n.name === "Tree");``const clone = tpl.clone();`
54+
→ append to the target page (`figma.getNodeById("0:1").appendChild(clone)`), rename,
55+
set `clone.x/clone.y` to park it beside existing content.
56+
- Template anatomy (names are stable; clone gets fresh ids — re-read with
57+
`get_metadata` on the clone):
58+
- `Header > Logo` — leave as-is.
59+
- `Component name > Title > Frame 13`: child `Title` (text = component name),
60+
sibling `Text` (the storybook component description), `Links` containing two
61+
`.link-tags``Primary` text nodes (set to e.g. "View documentation" /
62+
"Storybook").
63+
- `image 2` — a placeholder rounded-rectangle hero. Replace it: `remove()` it and
64+
`insertChild(1, showcase)` a centered auto-layout frame holding a real component
65+
instance. The `Title` frame is **auto-layout**, so appended children flow in order
66+
— use `insertChild`/`layoutAlign="STRETCH"`, do NOT rely on absolute `x/y`.
67+
- `Frame 6`: `Anatomy / Badge > Copy me` badge text (rename to "Anatomy"), and a
68+
`Group` of `Isolated` example cards each with a `Text` caption.
69+
- `HR` divider line, then `Variants` containing `Title` + subsections
70+
`Frame 9 / Frame 11 / Frame 12`, each a `Title` (+ optional subtitle) and an
71+
`Isolated` canvas. Repurpose each subsection to a storybook story group: clear the
72+
`Isolated` children, set it to `layoutMode="VERTICAL"` with padding/itemSpacing,
73+
and append labeled blocks (a Semibold caption text + a component instance).
74+
- Populate copy straight from the storybook: the `meta`/`docs.description.component`
75+
becomes the hero description; each exported `Story` (its `args` + its
76+
`docs.description.story`) becomes one labeled variant block.
77+
- Map storybook props → the **main component's** instance properties; build every
78+
example as an `createInstance()` + `setProperties()`. **Do not modify the documented
79+
component** — only instantiate it.
80+
81+
## Plugin API gotchas (each cost real iterations)
82+
83+
- Import variables via `figma.variables.importVariableByKeyAsync` (NOT
84+
`figma.importVariableByKeyAsync`).
85+
- Bind a color token:
86+
`figma.variables.setBoundVariableForPaint({type:"SOLID",color:{r:0,g:0,b:0},opacity:1},"color",v)`
87+
then assign to `node.fills` / `node.strokes`.
88+
- Text styles: `importStyleByKeyAsync``await loadFontAsync(style.fontName)`
89+
`setTextStyleIdAsync`.
90+
- Editing an existing TEXT node (`characters`, `textAutoResize`) requires loading
91+
ITS fonts first: `getRangeAllFontNames()` then `loadFontAsync` each.
92+
- Set `layoutSizingHorizontal/Vertical` **only after** the node is appended to an
93+
auto-layout parent.
94+
- For vertical hug: set the component `primaryAxisSizingMode="AUTO"` AND inner
95+
frames `counterAxisSizingMode="AUTO"` / `layoutSizingVertical="HUG"`, otherwise a
96+
wrapped multiline text overflows a fixed height (this once made a divider cut
97+
through the description text).
98+
- Icon component **sets** ARE importable directly by the `componentKey` that
99+
`search_design_system` returns for an `assetType: "component_set"` (Fluent System
100+
Icons): `const set = await figma.importComponentSetByKeyAsync(key)`. Then pick the
101+
variant child by `variantProperties` — Fluent icons expose `Size` (`"12".."48"`) and
102+
`Theme` (`"Regular"`/`"Filled"`); chevrons add `Direction` (`"Up"/"Down"/"Left"/"Right"`).
103+
`set.children.find(c => Object.entries(props).every(([k,v]) => (c.variantProperties||{})[k] === v))`
104+
`.createInstance()``.resize(size,size)`. Colorize by binding a token paint onto
105+
every descendant with a non-empty `fills` array
106+
(`inst.findAll(n => 'fills' in n && Array.isArray(n.fills) && n.fills.length > 0)`).
107+
`search_design_system` icon matching is fuzzy/noisy — query the exact icon name and
108+
read the first `component_set` result. (Older fallback, only if a key is unavailable:
109+
instantiate a host component, `findAll` instances, match `mainComponent.parent.name`.)
110+
- `loadAllPagesAsync` is NOT available here; load individual pages with
111+
`page.loadAsync()` (see doc-template workflow).
112+
- **`figma.combineAsVariants([comps], parent)`** derives the variant property from the
113+
component names (`"State=Expanded"` / `"State=Collapsed"` → property `State` with those
114+
values). DO NOT put the resulting variant set into auto-layout (`set.layoutMode="HORIZONTAL"`)
115+
— that **freezes each child component's height** at its current fixed value (a shorter
116+
variant got stuck tall and its footer overflowed the background). Instead set
117+
`set.layoutMode="NONE"`, per variant `v.primaryAxisSizingMode="AUTO"` (hug), position the
118+
variants manually side-by-side, then `set.resizeWithoutConstraints(w,h)` to bound the set.
119+
- When cloning the Doc template, the **Isolated canvas frames have FIXED height** from the
120+
template. If your component instance is taller than the template's example, it overflows
121+
and overlaps the next section (clipsContent=false). FIX: make each Isolated hug vertically —
122+
for a VERTICAL Isolated set `primaryAxisSizingMode="AUTO"`, for a HORIZONTAL one set
123+
`counterAxisSizingMode="AUTO"`. Ancestor section frames are already AUTO and reflow.
124+
- **Never `throw` to return diagnostics** — an uncaught error rolls back ALL mutations
125+
from that `use_figma` call. End with a plain final expression instead, or write data
126+
into a temp TEXT node and read it back with `get_design_context`, then `remove()` it.
127+
- An instance's runtime property keys are suffixed and differ from codegen prop names
128+
(e.g. `"Show actions#16:2"`, not `showActions`). Read the real keys from
129+
`componentPropertyDefinitions` (build a `baseName → fullKey` map by splitting on
130+
`#`) before calling `setProperties`; guessing silently no-ops the booleans.
131+
- In this **dynamic-page** file, sync `figma.getNodeById` is unreliable for deep
132+
descendants of a freshly-cloned subtree (returns `null` mid-script even when the
133+
node exists). Use `await figma.getNodeByIdAsync(id)` for every lookup of clone
134+
children.
135+
- `figma.currentPage.selection = [node]` throws if `node` lives on a non-active page
136+
(cross-page selection is illegal) — and that throw rolls back the whole script.
137+
Call `await figma.setCurrentPageAsync(page)` at the start, and/or wrap the final
138+
selection in `try/catch`. Same applies to `set figma.currentPage` (not supported —
139+
use `setCurrentPageAsync`).
140+
141+
## Button (Axis Fluent 2) — key `a39515bf9246e33a7ca60093544b639a4ab31bfa`
142+
143+
Variants: `Style[Primary | Secondary (Default) | Outline | Subtle | Transparent]`,
144+
`State`, `Size[Large | Medium (Default) | Small]`,
145+
`Layout[Icon and label (Default) | Icon only]`.
146+
Props include `Label#157663:151` and `Icon#157715:1057` (boolean).
147+
148+
To set a leading icon: enable `Icon#157715:1057`, then find the nested INSTANCE named
149+
**"Placeholder"** and `ph.swapComponent(await figma.importComponentByKeyAsync(iconKey))`.
150+
Do NOT pass icon keys to the Button's INSTANCE_SWAP property — that fails.
151+
152+
Other components: Card set `792c5f710359daacb9045f96efac19da2a082dcd`,
153+
Spin button set `dfb2afc7193d72dc5e4cd525e387104c1fac24e3` (resize wider, ~80px, or
154+
it wraps tall).
155+
156+
## Verified token keys (color variables)
157+
158+
| Token | Key |
159+
|---|---|
160+
| Neutral/Background/1/Rest | `fac2264608cbaab9b3ef758326f113387441343b` |
161+
| Neutral/Background/2/Rest | `927459e310c18205e5a4d8d4c0105d0bea0c966f` |
162+
| Neutral/Foreground/1/Rest | `7b4fababf67f3aa7dcb51d93aa147b48b47a9b30` |
163+
| Neutral/Foreground/2/Rest | `7bcc04c3456d2df7dba73ad15cab329ff9440e0a` |
164+
| Brand/Foreground/1/Rest | `4bc56a6988040de75286b200d47c3203b991f3e4` |
165+
| Brand/Background/1/Rest | `598ea29abf1410c6ca792571d01b4b14e664d29a` |
166+
| Brand/Stroke/1/Rest | `800d441d12a5a3a5094aea1fee06b914cb1df882` |
167+
| **Neutral/Stroke/1/Rest** (real divider / colorNeutralStroke1) | `62c3aea8fb72b2f4886958a3e15dc654133b1757` |
168+
| Neutral/Stroke/2/Rest | `9faa2843efdf7cac8bace16d9a375b44501264b8` |
169+
170+
> ⚠️ `ae2f0c2a9ccbf2b736857b83e400cbde1cd00607` is Neutral/Stroke/**on Brand**/1/Rest
171+
> (near-white) — NOT a normal divider. Picking it gives a wrong/invisible divider.
172+
173+
## Verified text style keys
174+
175+
| Style | Key |
176+
|---|---|
177+
| Subtitle 1 | `0f5baaeb806fe58b4bfac7c6c0747b2843715f05` |
178+
| Body 1 | `020362c24075dd02e2fc8965234d194899620770` |
179+
| Body 1 Strong | `29ef820c9a8d755de0b6999cd09cd75baee53cfe` |
180+
| Caption 1 | `4870c2b71d882387b33810d647e0514a882d448a` |
181+
| Caption 1 Strong | `87c3650fd4940e18abeae7576f6789fcb0acba05` |
182+
183+
## Parameterize instead of variant explosion
184+
185+
When the user asks for "proper variants / booleans / slots", add properties on the
186+
main COMPONENT rather than duplicating frames:
187+
188+
- `addComponentProperty(name, "BOOLEAN", true)` → set
189+
`node.componentPropertyReferences = { visible: id }` to toggle a region.
190+
- `addComponentProperty(name, "TEXT", def)``{ characters: id }` for editable copy.
191+
- `addComponentProperty(name, "INSTANCE_SWAP", defaultComponentId, { preferredValues:[{type:"COMPONENT_SET", key: BUTTON_KEY}] })`
192+
`{ mainComponent: id }` for swappable slots (e.g. action buttons).
193+
194+
Validate by `createInstance()` + `setProperties()`, read back, then remove the temp
195+
instance.
196+
197+
## Components already built in this file
198+
199+
- **File Upload** (Page 1) — Card-like dropzone + Button + Cloud Arrow Up / Document
200+
PDF / Dismiss icons + progress bars.
201+
- **Section Header** (Page 1, node `16:122`) — meta / title / description + 2 action
202+
button slots; booleans + text + instance-swap slots. Its **Doc template**-based
203+
documentation page lives on the same page (frame "Section Header — Documentation") —
204+
use it as the reference example for the doc-template workflow.
205+
- **Pagination** (page "Pagination", node `21:1608`) — row counter + Spin button page
206+
selector + prev/next chevron icon-only buttons; top border Neutral/Stroke/1;
207+
booleans (`Show row counter` / `Show page selector` / `Show navigation`) + text
208+
(`Row counter text` / `Page label before` / `Page label after`) + button slots. Its
209+
**Doc template**-based docs live on the same page (frame "Pagination —
210+
Documentation").
211+
- **Side Navigation** (page "Side Navigation") — now a **variant SET** (`42:193`) with a
212+
`State` property of `Expanded` / `Collapsed`. Expanded (260px rail):
213+
bg Neutral/Background/2 + right border Neutral/Stroke/2; toggle (Chevron Left) +
214+
item rows (44px, 60px icon column, Body 1 label, group chevron), selected item
215+
(bg Neutral/Background/2/Pressed, Foreground 1, Filled icon, 3×20 yellow indicator
216+
bound to Brand/Stroke/1/Rest, absolute-positioned at nav left edge), open sub-items
217+
(36px, paddingLeft 60), divider, footer items. Collapsed (68px icon-only rail):
218+
toggle (Chevron Right) + icon-only rows, selected item keeps the yellow indicator +
219+
grey bg + filled icon, divider, footer icons. Real Fluent System Icons (Home /
220+
Apps / Layer / Person / Settings / Chevron) imported via `importComponentSetByKeyAsync`
221+
then variant picked by `variantProperties` (`Size`/`Theme`/`Direction`). Its
222+
**Doc template**-based docs live on the same page (frame "Side Navigation —
223+
Documentation", `44:157`).
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
ignores:
2+
[
3+
"react-dom",
4+
"typescript",
5+
"@testing-library/jest-dom",
6+
"@testing-library/user-event",
7+
"@vitest/coverage-c8",
8+
]
9+
skip-missing: true
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# @axiscommunications/fluent-advanced-data-grid
2+
3+
An advanced data grid for Fluent UI v9 that combines a filter toolbar with a
4+
[`DataGrid`](https://react.fluentui.dev/?path=/docs/components-datagrid--default).
5+
It brings an advanced feature set to the grid using Fluent UI primitives.
6+
7+
> This package is currently **private** and not published.
8+
9+
## Features
10+
11+
- Global quick-filter search
12+
- Per-column filter panel with type-aware operators
13+
- Column sorting
14+
- Pagination with configurable page sizes
15+
- Row selection (single / multiselect)
16+
- Column visibility toggle
17+
- Density toggle (comfortable / medium / compact)
18+
- CSV export (formula-injection safe)
19+
- Column pinning (left / right) and reordering
20+
- Row grouping with aggregation (sum / avg / min / max / count)
21+
22+
## Usage
23+
24+
```tsx
25+
import {
26+
AdvancedDataGrid,
27+
AdvancedColumnDef,
28+
} from "@axiscommunications/fluent-advanced-data-grid";
29+
30+
type Person = { id: string; name: string; age: number; role: string };
31+
32+
const items: Person[] = [
33+
{ id: "1", name: "Robin", age: 21, role: "Sidekick" },
34+
{ id: "2", name: "Batman", age: 35, role: "Hero" },
35+
];
36+
37+
const columns: AdvancedColumnDef<Person>[] = [
38+
{ columnId: "name", header: "Name", getValue: (p) => p.name },
39+
{
40+
columnId: "age",
41+
header: "Age",
42+
type: "number",
43+
aggregation: "avg",
44+
getValue: (p) => p.age,
45+
},
46+
{
47+
columnId: "role",
48+
header: "Role",
49+
type: "select",
50+
groupable: true,
51+
selectOptions: [
52+
{ label: "Hero", value: "Hero" },
53+
{ label: "Sidekick", value: "Sidekick" },
54+
],
55+
getValue: (p) => p.role,
56+
},
57+
];
58+
59+
export function Example() {
60+
return (
61+
<AdvancedDataGrid
62+
items={items}
63+
columns={columns}
64+
getRowId={(p) => p.id}
65+
/>
66+
);
67+
}
68+
```
69+
70+
## Column definition
71+
72+
| Field | Description |
73+
| --- | --- |
74+
| `columnId` | Stable unique id. |
75+
| `header` | Header content. |
76+
| `getValue` | Returns the comparable / exportable value. |
77+
| `renderCell` | Optional custom cell renderer. |
78+
| `type` | `string` \| `number` \| `boolean` \| `date` \| `select`. |
79+
| `sortable` / `filterable` / `groupable` / `pinnable` / `hideable` | Feature opt-out flags. |
80+
| `aggregation` | `sum` \| `avg` \| `min` \| `max` \| `count`. |
81+
| `selectOptions` | Options for `select` columns. |
82+
| `width` / `minWidth` | Sizing hints (used for pinning offsets). |
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"name": "@axiscommunications/fluent-advanced-data-grid",
3+
"version": "0.1.0",
4+
"private": true,
5+
"description": "Advanced data grid with a filter toolbar for Fluent UI v9",
6+
"homepage": "https://github.com/AxisCommunications/fluent-components#readme",
7+
"repository": {
8+
"type": "git",
9+
"url": "https://github.com/AxisCommunications/fluent-components.git",
10+
"directory": "components/advanced-data-grid"
11+
},
12+
"license": "MIT",
13+
"author": "Axis Communications AB",
14+
"type": "module",
15+
"exports": {
16+
".": {
17+
"types": "./lib/index.d.ts",
18+
"import": "./lib/index.js"
19+
}
20+
},
21+
"files": ["lib"],
22+
"scripts": {
23+
"build": "pnpm build:types && pnpm build:esm",
24+
"build:esm": "esbuild --format=esm --bundle --sourcemap --packages=external --outdir=lib src/index.ts",
25+
"build:types": "tsc -p tsconfig.build.json",
26+
"check:unused-deps": "depcheck . --config=depcheck.yml",
27+
"lint": "tsc --noEmit && biome check",
28+
"test": "vitest run",
29+
"test:watch": "vitest watch"
30+
},
31+
"devDependencies": {
32+
"@testing-library/jest-dom": "^6.6.3",
33+
"@testing-library/react": "^16.3.2",
34+
"@testing-library/user-event": "^14.5.2",
35+
"@types/react": "^19.2.14",
36+
"@types/react-dom": "^19.2.3",
37+
"esbuild": "^0.28.0",
38+
"jsdom": "^29.0.2",
39+
"react": "^19.2.4",
40+
"react-dom": "^19.2.4",
41+
"typescript": "^5.8.3",
42+
"vitest": "^4.1.3"
43+
},
44+
"peerDependencies": {
45+
"@fluentui/react-components": "^9.72.7",
46+
"@fluentui/react-icons": "^2.0.315",
47+
"react": ">=16.14.0 <20.0.0",
48+
"react-dom": ">=16.14.0 <20.0.0"
49+
}
50+
}

0 commit comments

Comments
 (0)