@astryxdesign/cli 0.1.1-canary.2b52cdc → 0.1.1-canary.36fb9fb

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.
Files changed (35) hide show
  1. package/docs/working-with-ai.doc.mjs +6 -6
  2. package/package.json +7 -7
  3. package/src/codemods/__tests__/registry.test.mjs +1 -0
  4. package/src/codemods/registry.mjs +1 -0
  5. package/src/codemods/transforms/v0.1.2/__tests__/rename-text-color-active-to-accent.test.mjs +120 -0
  6. package/src/codemods/transforms/v0.1.2/index.mjs +19 -0
  7. package/src/codemods/transforms/v0.1.2/rename-text-color-active-to-accent.mjs +136 -0
  8. package/src/commands/upgrade.mjs +1 -1
  9. package/src/lib/xle/browser.d.ts +11 -2
  10. package/templates/blocks/components/Card/ClickableCardWithNestedButton.doc.mjs +1 -1
  11. package/templates/blocks/components/Card/SelectableCardMulti.doc.mjs +1 -1
  12. package/templates/blocks/components/ChatDictationButton/ChatDictationButtonShowcase.tsx +3 -3
  13. package/templates/blocks/components/FileInput/FileInputShowcase.tsx +4 -2
  14. package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +14 -0
  15. package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.tsx +43 -0
  16. package/templates/blocks/components/MultiSelector/MultiSelectorShowcase.tsx +6 -2
  17. package/templates/blocks/components/NumberInput/NumberInputShowcase.tsx +6 -2
  18. package/templates/blocks/components/RadioList/RadioListShowcase.tsx +8 -1
  19. package/templates/blocks/components/SegmentedControl/SegmentedControlShowcase.tsx +3 -1
  20. package/templates/blocks/components/Selector/SelectorShowcase.tsx +4 -1
  21. package/templates/blocks/components/Tab/TabShowcase.tsx +3 -1
  22. package/templates/blocks/components/TabList/TabListShowcase.tsx +3 -1
  23. package/templates/blocks/components/TabMenu/TabMenuShowcase.tsx +3 -1
  24. package/templates/blocks/components/Table/ColumnResizeHookUsage.doc.mjs +14 -0
  25. package/templates/blocks/components/Table/ColumnResizeHookUsage.tsx +59 -0
  26. package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +14 -0
  27. package/templates/blocks/components/Table/StickyColumnsHookUsage.tsx +104 -0
  28. package/templates/blocks/components/Text/TextColors.tsx +20 -5
  29. package/templates/blocks/components/TextArea/TextAreaShowcase.tsx +4 -2
  30. package/templates/blocks/components/TextInput/TextInputShowcase.tsx +4 -2
  31. package/templates/blocks/components/Thumbnail/ThumbnailGallery.tsx +25 -5
  32. package/templates/blocks/components/Timestamp/TimestampColors.tsx +2 -2
  33. package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +8 -6
  34. package/templates/blocks/components/Typeahead/TypeaheadShowcase.tsx +6 -2
  35. package/templates/pages/theme-showcase/page.tsx +8 -19
@@ -34,11 +34,11 @@ export const docs = {
34
34
  type: 'code',
35
35
  lang: 'text',
36
36
  label: 'Paste this into your AI',
37
- code: 'Install @astryxdesign/cli and run `npx astryx agent-docs` to set up your Astryx context. Read the generated file.',
37
+ code: 'Install @astryxdesign/cli and run `npx astryx init --features agents` to set up your Astryx context. Read the generated file.',
38
38
  },
39
39
  {
40
40
  type: 'prose',
41
- text: 'That\'s it. The agent-docs command generates everything your AI needs (component index, behavioral rules, CLI reference) pulled from your installed version. After a version bump, run it again to update in place.',
41
+ text: 'That\'s it. The `init --features agents` command generates everything your AI needs (component index, behavioral rules, CLI reference) pulled from your installed version. After a version bump, run it again to update in place.',
42
42
  },
43
43
  {
44
44
  type: 'prose',
@@ -48,9 +48,9 @@ export const docs = {
48
48
  type: 'code',
49
49
  lang: 'bash',
50
50
  label: 'Manual options',
51
- code: `npx astryx agent-docs --agent claude # CLAUDE.md
52
- npx astryx agent-docs --agent cursor # .cursorrules
53
- npx astryx agent-docs --agent codex # AGENTS.md (Copilot, Codex, etc.)`,
51
+ code: `npx astryx init --features agents --agent claude # CLAUDE.md
52
+ npx astryx init --features agents --agent cursor # .cursorrules
53
+ npx astryx init --features agents --agent codex # AGENTS.md (Copilot, Codex, etc.)`,
54
54
  },
55
55
  ],
56
56
  },
@@ -88,7 +88,7 @@ npx astryx agent-docs --agent codex # AGENTS.md (Copilot, Codex, etc.)`,
88
88
  lang: 'bash',
89
89
  label: 'Install as a Cursor user rule',
90
90
  code: `mkdir -p ~/.cursor/rules
91
- npx astryx agent-docs --agent-docs-path ~/.cursor/rules/xds.mdc`,
91
+ npx astryx init --features agents --agent-docs-path ~/.cursor/rules/xds.mdc`,
92
92
  },
93
93
  ],
94
94
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.1.1-canary.2b52cdc",
3
+ "version": "0.1.1-canary.36fb9fb",
4
4
  "displayName": "CLI",
5
5
  "description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
6
6
  "author": "Meta Open Source",
@@ -63,9 +63,9 @@
63
63
  "zod": "^4.4.3"
64
64
  },
65
65
  "peerDependencies": {
66
- "@astryxdesign/core": "0.1.1-canary.2b52cdc",
67
- "@astryxdesign/lab": "0.1.1-canary.2b52cdc",
68
- "@astryxdesign/theme-neutral": "0.1.1-canary.2b52cdc",
66
+ "@astryxdesign/core": "0.1.1-canary.36fb9fb",
67
+ "@astryxdesign/lab": "0.1.1-canary.36fb9fb",
68
+ "@astryxdesign/theme-neutral": "0.1.1-canary.36fb9fb",
69
69
  "gpt-tokenizer": "^2.0.0"
70
70
  },
71
71
  "peerDependenciesMeta": {
@@ -80,9 +80,9 @@
80
80
  }
81
81
  },
82
82
  "devDependencies": {
83
- "@astryxdesign/core": "0.1.1-canary.2b52cdc",
84
- "@astryxdesign/lab": "0.1.1-canary.2b52cdc",
85
- "@astryxdesign/theme-neutral": "0.1.1-canary.2b52cdc",
83
+ "@astryxdesign/core": "0.1.1-canary.36fb9fb",
84
+ "@astryxdesign/lab": "0.1.1-canary.36fb9fb",
85
+ "@astryxdesign/theme-neutral": "0.1.1-canary.36fb9fb",
86
86
  "gpt-tokenizer": "^2.0.0"
87
87
  },
88
88
  "scripts": {
@@ -17,6 +17,7 @@ describe('registry', () => {
17
17
  '0.0.14',
18
18
  '0.0.15',
19
19
  '0.1.0',
20
+ '0.1.2',
20
21
  ]);
21
22
  });
22
23
  });
@@ -18,6 +18,7 @@ const registry = new Map([
18
18
  ['0.0.14', () => import('./transforms/v0.0.14/index.mjs')],
19
19
  ['0.0.15', () => import('./transforms/v0.0.15/index.mjs')],
20
20
  ['0.1.0', () => import('./transforms/v0.1.0/index.mjs')],
21
+ ['0.1.2', () => import('./transforms/v0.1.2/index.mjs')],
21
22
  ]);
22
23
 
23
24
  // Re-export from the shared utility so registry callers and other consumers
@@ -0,0 +1,120 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, it, expect} from 'vitest';
4
+
5
+ async function applyTransform(source) {
6
+ const {default: transform} =
7
+ await import('../rename-text-color-active-to-accent.mjs');
8
+ const jscodeshift = (await import('jscodeshift')).default;
9
+ const j = jscodeshift.withParser('tsx');
10
+ const api = {jscodeshift: j, stats: () => {}, report: () => {}};
11
+ const file = {source, path: 'test.tsx'};
12
+ const result = transform(file, api);
13
+ return result ?? source;
14
+ }
15
+
16
+ describe('rename-text-color-active-to-accent', () => {
17
+ it('renames Text color="active" to color="accent"', async () => {
18
+ const input = `<Text color="active">Hi</Text>`;
19
+ const output = await applyTransform(input);
20
+ expect(output).toContain("'accent'");
21
+ expect(output).not.toContain('active');
22
+ });
23
+
24
+ it('renames Heading color="active" to color="accent"', async () => {
25
+ const input = `<Heading level={2} color="active">Title</Heading>`;
26
+ const output = await applyTransform(input);
27
+ expect(output).toContain("'accent'");
28
+ expect(output).not.toContain('active');
29
+ });
30
+
31
+ it('renames Link color="active" to color="accent"', async () => {
32
+ const input = `<Link href="/x" color="active">Go</Link>`;
33
+ const output = await applyTransform(input);
34
+ expect(output).toContain("'accent'");
35
+ expect(output).not.toContain('active');
36
+ });
37
+
38
+ it('renames Timestamp color="active" to color="accent"', async () => {
39
+ const input = `<Timestamp value={d} color="active" />`;
40
+ const output = await applyTransform(input);
41
+ expect(output).toContain("'accent'");
42
+ expect(output).not.toContain('active');
43
+ });
44
+
45
+ it("renames expression-container color={'active'}", async () => {
46
+ const input = `<Text color={'active'}>Hi</Text>`;
47
+ const output = await applyTransform(input);
48
+ expect(output).toContain("'accent'");
49
+ expect(output).not.toContain("'active'");
50
+ });
51
+
52
+ it('renames active inside a ternary color expression', async () => {
53
+ const input = `<Text color={isOn ? 'active' : 'secondary'}>Hi</Text>`;
54
+ const output = await applyTransform(input);
55
+ expect(output).toContain("'accent'");
56
+ expect(output).toContain("'secondary'");
57
+ expect(output).not.toContain("'active'");
58
+ });
59
+
60
+ it('does not change other color values', async () => {
61
+ const input = `<Text color="secondary">Hi</Text>`;
62
+ const output = await applyTransform(input);
63
+ expect(output).toBe(input);
64
+ });
65
+
66
+ it('does not touch color="active" on non-target components', async () => {
67
+ const input = `<StatusDot color="active" />`;
68
+ const output = await applyTransform(input);
69
+ expect(output).toBe(input);
70
+ });
71
+
72
+ it('does not touch unrelated "active" string literals', async () => {
73
+ const input = `const node = {id: 'active', label: 'Active Users'};`;
74
+ const output = await applyTransform(input);
75
+ expect(output).toBe(input);
76
+ });
77
+
78
+ it('does not touch a status enum value named active', async () => {
79
+ const input = `const filters = {status: 'active'};`;
80
+ const output = await applyTransform(input);
81
+ expect(output).toBe(input);
82
+ });
83
+
84
+ it('renames object color prop in files importing a target component', async () => {
85
+ const input = `import {Text} from '@xds/core/Text';
86
+ const cfg = {color: 'active'};`;
87
+ const output = await applyTransform(input);
88
+ expect(output).toContain("color: 'accent'");
89
+ expect(output).not.toContain("'active'");
90
+ });
91
+
92
+ it("renames color: 'active' as const in files importing a target component", async () => {
93
+ const input = `import {Text} from '@xds/core/Text';
94
+ const COLORS = [{color: 'active' as const, description: 'Accent'}];`;
95
+ const output = await applyTransform(input);
96
+ expect(output).toContain("'accent' as const");
97
+ expect(output).not.toContain("'active'");
98
+ });
99
+
100
+ it('does not rename object color prop when no target component is imported', async () => {
101
+ const input = `const cfg = {color: 'active'};`;
102
+ const output = await applyTransform(input);
103
+ expect(output).toBe(input);
104
+ });
105
+
106
+ it('renames active in Storybook color argTypes options', async () => {
107
+ const input = `import {Text} from '@xds/core/Text';
108
+ const meta = {argTypes: {color: {control: 'select', options: ['primary', 'active', 'inherit']}}};`;
109
+ const output = await applyTransform(input);
110
+ expect(output).toContain("'accent'");
111
+ expect(output).not.toContain("'active'");
112
+ });
113
+
114
+ it('returns undefined (no change) when nothing matches', async () => {
115
+ const input = `<Text color="primary">Hi</Text>`;
116
+ const output = await applyTransform(input);
117
+ // applyTransform falls back to source when transform returns undefined
118
+ expect(output).toBe(input);
119
+ });
120
+ });
@@ -0,0 +1,19 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file v0.1.2 transform manifest
5
+ *
6
+ * Lists all codemods for the v0.1.2 release in the order they should run.
7
+ */
8
+
9
+ import renameTextColorActiveToAccent, {
10
+ meta as renameTextColorActiveToAccentMeta,
11
+ } from './rename-text-color-active-to-accent.mjs';
12
+
13
+ export default [
14
+ {
15
+ name: 'rename-text-color-active-to-accent',
16
+ transform: renameTextColorActiveToAccent,
17
+ meta: renameTextColorActiveToAccentMeta,
18
+ },
19
+ ];
@@ -0,0 +1,136 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Codemod: Rename Text/Heading/Link/Timestamp color "active" to "accent"
5
+ * @see https://github.com/facebook/astryx/issues/2863
6
+ *
7
+ * The `active` text-color value is renamed to `accent` and now maps to the
8
+ * dedicated `--color-text-accent` token (legible accent ink) instead of
9
+ * `--color-accent` (the interactive/base accent surface). This gives a
10
+ * first-class, type-safe way to render accent-colored text and resolves the
11
+ * gap reported in #2863.
12
+ *
13
+ * Transforms (scoped to Text, Heading, Link, and Timestamp):
14
+ * - <Text color="active" /> → <Text color="accent" />
15
+ * - <Heading color={'active'} /> → <Heading color={'accent'} />
16
+ * - <Text color={cond ? 'active' : x}> → <Text color={cond ? 'accent' : x}>
17
+ * - Storybook argTypes: color: { options: [..., 'active', ...] }
18
+ * → replaces 'active' with 'accent' (files importing a target component)
19
+ * - Object properties: { color: 'active' } / { color: 'active' as const }
20
+ * → { color: 'accent' } (files importing a target component)
21
+ *
22
+ * The transform deliberately does NOT touch bare `'active'` strings that are
23
+ * not a `color` value (e.g. status enums, ids, filter values), so it is safe
24
+ * to run across an entire codebase.
25
+ */
26
+
27
+ export const meta = {
28
+ title: 'Rename Text/Heading/Link/Timestamp color "active" to "accent"',
29
+ description:
30
+ 'Renames the `color="active"` value to `color="accent"` on Text, Heading, ' +
31
+ 'Link, and Timestamp. The `accent` value maps to `--color-text-accent` ' +
32
+ '(the dedicated accent text ink). Also updates Storybook color argTypes ' +
33
+ 'options and object-literal `color` props in files that import a target component.',
34
+ pr: '#2863',
35
+ };
36
+
37
+ const OLD_VALUE = 'active';
38
+ const NEW_VALUE = 'accent';
39
+
40
+ /** Components whose `color` prop accepts a TextColor value. */
41
+ const TARGET_COMPONENTS = new Set(['Text', 'Heading', 'Link', 'Timestamp']);
42
+
43
+ export default function transformer(file, api) {
44
+ const j = api.jscodeshift;
45
+ const root = j(file.source);
46
+ let hasChanges = false;
47
+
48
+ /** Rewrite a string-literal node when it equals the old value. */
49
+ function renameStringLiteral(node) {
50
+ if (!node) return false;
51
+ if (
52
+ (node.type === 'StringLiteral' || node.type === 'Literal') &&
53
+ node.value === OLD_VALUE
54
+ ) {
55
+ node.value = NEW_VALUE;
56
+ if (node.raw) node.raw = `'${NEW_VALUE}'`;
57
+ return true;
58
+ }
59
+ return false;
60
+ }
61
+
62
+ /** Recursively rewrite the old value inside ternary/logical expressions. */
63
+ function renameInExpression(node) {
64
+ if (!node) return false;
65
+ let changed = false;
66
+ if (node.type === 'StringLiteral' || node.type === 'Literal') {
67
+ changed = renameStringLiteral(node) || changed;
68
+ } else if (node.type === 'ConditionalExpression') {
69
+ changed = renameInExpression(node.consequent) || changed;
70
+ changed = renameInExpression(node.alternate) || changed;
71
+ } else if (node.type === 'LogicalExpression') {
72
+ changed = renameInExpression(node.left) || changed;
73
+ changed = renameInExpression(node.right) || changed;
74
+ } else if (node.type === 'TSAsExpression') {
75
+ // `'active' as const` → rewrite the inner expression
76
+ changed = renameInExpression(node.expression) || changed;
77
+ }
78
+ return changed;
79
+ }
80
+
81
+ // --- 1. JSX attribute: color="active" / color={'active'} on target components ---
82
+ root.find(j.JSXOpeningElement).forEach(path => {
83
+ const name = path.node.name;
84
+ const componentName = name.type === 'JSXIdentifier' ? name.name : null;
85
+ if (!componentName || !TARGET_COMPONENTS.has(componentName)) return;
86
+
87
+ path.node.attributes.forEach(attr => {
88
+ if (attr.type !== 'JSXAttribute') return;
89
+ if (!attr.name || attr.name.name !== 'color') return;
90
+
91
+ const value = attr.value;
92
+ if (!value) return;
93
+
94
+ if (value.type === 'StringLiteral' || value.type === 'Literal') {
95
+ if (renameStringLiteral(value)) hasChanges = true;
96
+ } else if (value.type === 'JSXExpressionContainer') {
97
+ if (renameInExpression(value.expression)) hasChanges = true;
98
+ }
99
+ });
100
+ });
101
+
102
+ // Object-property / argTypes transforms only run in files that use a target
103
+ // component — keeps unrelated `{ color: 'active' }` / `'active'` strings safe.
104
+ const importsTarget =
105
+ root
106
+ .find(j.ImportSpecifier)
107
+ .filter(p => TARGET_COMPONENTS.has(p.node.imported?.name))
108
+ .size() > 0;
109
+
110
+ if (importsTarget) {
111
+ const PropertyType = j.ObjectProperty ?? j.Property;
112
+
113
+ // --- 2. Object property: { color: 'active' } / { color: 'active' as const } ---
114
+ root.find(PropertyType, {key: {name: 'color'}}).forEach(path => {
115
+ if (renameInExpression(path.node.value)) hasChanges = true;
116
+ });
117
+
118
+ // --- 3. Storybook argTypes: color: { options: [..., 'active', ...] } ---
119
+ root.find(PropertyType, {key: {name: 'color'}}).forEach(path => {
120
+ const value = path.node.value;
121
+ if (!value || value.type !== 'ObjectExpression') return;
122
+
123
+ const optionsProp = value.properties.find(
124
+ p => p.key && (p.key.name === 'options' || p.key.value === 'options'),
125
+ );
126
+ if (optionsProp && optionsProp.value.type === 'ArrayExpression') {
127
+ optionsProp.value.elements.forEach(el => {
128
+ if (renameStringLiteral(el)) hasChanges = true;
129
+ });
130
+ }
131
+ });
132
+ }
133
+
134
+ if (!hasChanges) return undefined;
135
+ return root.toSource({quote: 'single'});
136
+ }
@@ -438,7 +438,7 @@ export function registerUpgrade(program) {
438
438
  } catch {
439
439
  if (!json) {
440
440
  p.log.warn(
441
- `Could not update agent docs. Run \`${getRunPrefix()} astryx agent-docs\` to update manually.`,
441
+ `Could not update agent docs. Run \`${getRunPrefix()} astryx init --features agents\` to update manually.`,
442
442
  );
443
443
  }
444
444
  }
@@ -79,8 +79,17 @@ export function hydrateRegistry(json: SerializedRegistry): object;
79
79
  export function serializeRegistry(registry: object): SerializedRegistry;
80
80
  export function parse(source: string, opts?: {form?: string}): unknown;
81
81
  export function detectForm(source: string): 'compact' | 'outline';
82
- export function validate(doc: unknown, registry: object, blocks?: XLEBlock[], opts?: {loose?: boolean}): {errors: XLEIssue[]; warnings: XLEIssue[]};
83
- export function expand(doc: unknown, registry: object, opts?: {componentName?: string}): {code: string; componentsUsed: string[]; states: number; todos: string[]};
82
+ export function validate(
83
+ doc: unknown,
84
+ registry: object,
85
+ blocks?: XLEBlock[],
86
+ opts?: {loose?: boolean},
87
+ ): {errors: XLEIssue[]; warnings: XLEIssue[]};
88
+ export function expand(
89
+ doc: unknown,
90
+ registry: object,
91
+ opts?: {componentName?: string},
92
+ ): {code: string; componentsUsed: string[]; states: number; todos: string[]};
84
93
  export function toCompact(doc: unknown): string;
85
94
  export function toOutline(doc: unknown): string;
86
95
  export const ALIAS_TABLE: Record<string, string>;
@@ -5,7 +5,7 @@ export const doc = {
5
5
  type: 'block',
6
6
  exampleFor: 'ClickableCard',
7
7
  name: 'ClickableCardWithNestedButton',
8
- displayName: 'Clickable Card With Nested Button',
8
+ displayName: 'Clickable Card Nested Button',
9
9
  description:
10
10
  'A product card that navigates on click but has an independent "Add to cart" button inside.',
11
11
  isReady: true,
@@ -5,7 +5,7 @@ export const doc = {
5
5
  type: 'block',
6
6
  exampleFor: 'SelectableCard',
7
7
  name: 'SelectableCardMulti',
8
- displayName: 'Selectable Card Multi',
8
+ displayName: 'Selectable Card Multi-select',
9
9
  description:
10
10
  'Multi-select tag picker using color variant selectable cards with color-matched selection borders.',
11
11
  isReady: true,
@@ -19,7 +19,7 @@ export default function ChatDictationButtonShowcase() {
19
19
  const dictation = useChatDictation({
20
20
  inputRef,
21
21
  hasSounds: true,
22
- onResult: (text) => {
22
+ onResult: text => {
23
23
  console.log('Dictation result:', text);
24
24
  },
25
25
  });
@@ -31,7 +31,7 @@ export default function ChatDictationButtonShowcase() {
31
31
  input.
32
32
  </Text>
33
33
  <ChatComposer
34
- onSubmit={(v) => console.log('Submit:', v)}
34
+ onSubmit={v => console.log('Submit:', v)}
35
35
  input={<ChatComposerInput handleRef={inputRef} />}
36
36
  sendActions={<ChatDictationButton dictation={dictation} />}
37
37
  />
@@ -63,7 +63,7 @@ export default function ChatDictationButtonShowcase() {
63
63
  </HStack>
64
64
  )}
65
65
  {!dictation.isSupported && (
66
- <Text type="supporting" color="active">
66
+ <Text type="supporting" color="accent">
67
67
  SpeechRecognition is not supported in this browser.
68
68
  </Text>
69
69
  )}
@@ -2,15 +2,17 @@
2
2
 
3
3
  'use client';
4
4
 
5
+ import {useState} from 'react';
5
6
  import {FileInput} from '@astryxdesign/core/FileInput';
6
7
 
7
8
  export default function FileInputShowcase() {
9
+ const [value, setValue] = useState<File | File[] | null>(null);
8
10
  return (
9
11
  <div style={{width: 350}}>
10
12
  <FileInput
11
13
  label="Upload file"
12
- value={null}
13
- onChange={() => {}}
14
+ value={value}
15
+ onChange={setValue}
14
16
  placeholder="Drag files here or click to browse"
15
17
  />
16
18
  </div>
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'LinkProvider',
7
+ name: 'Link Provider — Custom Link Component',
8
+ displayName: 'Link Provider — Custom Link Component',
9
+ description:
10
+ 'Routes every Astryx link through a custom component that intercepts the click — the hook frameworks like Next.js use for client-side navigation. Click the link to see the custom handler fire instead of a full-page load.',
11
+ isReady: true,
12
+ aspectRatio: 4 / 3,
13
+ componentsUsed: ['LinkProvider', 'Link'],
14
+ };
@@ -0,0 +1,43 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import type {AnchorHTMLAttributes} from 'react';
6
+ import {Link, LinkProvider} from '@astryxdesign/core/Link';
7
+
8
+ /**
9
+ * A stand-in for a framework router link (e.g. Next.js `Link` or React
10
+ * Router `Link`). Instead of letting the browser do a full-page navigation,
11
+ * it intercepts the click and hands the destination to a custom handler —
12
+ * the same hook real routers use for client-side navigation. Here it just
13
+ * announces the target so the behavior swap is observable in the preview.
14
+ */
15
+ function RouterLink({
16
+ href,
17
+ onClick,
18
+ children,
19
+ ...props
20
+ }: AnchorHTMLAttributes<HTMLAnchorElement>) {
21
+ return (
22
+ <a
23
+ href={href}
24
+ onClick={e => {
25
+ e.preventDefault();
26
+ onClick?.(e);
27
+ alert(`RouterLink intercepted navigation to: ${href}`);
28
+ }}
29
+ {...props}>
30
+ {children}
31
+ </a>
32
+ );
33
+ }
34
+
35
+ export default function LinkProviderCustomLink() {
36
+ return (
37
+ <LinkProvider component={RouterLink}>
38
+ <Link href="/dashboard" isStandalone>
39
+ Go to dashboard
40
+ </Link>
41
+ </LinkProvider>
42
+ );
43
+ }
@@ -1,15 +1,19 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ 'use client';
4
+
5
+ import {useState} from 'react';
3
6
  import {MultiSelector} from '@astryxdesign/core/MultiSelector';
4
7
 
5
8
  export default function MultiSelectorShowcase() {
9
+ const [value, setValue] = useState<string[]>([]);
6
10
  return (
7
11
  <div style={{width: 300}}>
8
12
  <MultiSelector
9
13
  label="Columns"
10
14
  options={['Name', 'Email', 'Role', 'Status', 'Created']}
11
- value={[]}
12
- onChange={() => {}}
15
+ value={value}
16
+ onChange={setValue}
13
17
  placeholder="Select columns..."
14
18
  />
15
19
  </div>
@@ -1,15 +1,19 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ 'use client';
4
+
5
+ import {useState} from 'react';
3
6
  import {NumberInput} from '@astryxdesign/core/NumberInput';
4
7
 
5
8
  export default function NumberInputShowcase() {
9
+ const [value, setValue] = useState<number | null>(0);
6
10
  return (
7
11
  <div style={{width: 300}}>
8
12
  <NumberInput
9
13
  label="Quantity"
10
14
  placeholder="Enter quantity"
11
- value={0}
12
- onChange={() => {}}
15
+ value={value}
16
+ onChange={setValue}
13
17
  />
14
18
  </div>
15
19
  );
@@ -1,10 +1,17 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ 'use client';
4
+
5
+ import {useState} from 'react';
3
6
  import {RadioList, RadioListItem} from '@astryxdesign/core/RadioList';
4
7
 
5
8
  export default function RadioListShowcase() {
9
+ const [value, setValue] = useState('');
6
10
  return (
7
- <RadioList label="Notification preference" value="" onChange={() => {}}>
11
+ <RadioList
12
+ label="Notification preference"
13
+ value={value}
14
+ onChange={setValue}>
8
15
  <RadioListItem label="Email" value="email" />
9
16
  <RadioListItem label="SMS" value="sms" />
10
17
  <RadioListItem label="Push notification" value="push" />
@@ -2,14 +2,16 @@
2
2
 
3
3
  'use client';
4
4
 
5
+ import {useState} from 'react';
5
6
  import {
6
7
  SegmentedControl,
7
8
  SegmentedControlItem,
8
9
  } from '@astryxdesign/core/SegmentedControl';
9
10
 
10
11
  export default function SegmentedControlShowcase() {
12
+ const [value, setValue] = useState('grid');
11
13
  return (
12
- <SegmentedControl value="grid" onChange={() => {}} label="View mode">
14
+ <SegmentedControl value={value} onChange={setValue} label="View mode">
13
15
  <SegmentedControlItem value="grid" label="Grid" />
14
16
  <SegmentedControlItem value="list" label="List" />
15
17
  <SegmentedControlItem value="table" label="Table" />
@@ -2,16 +2,19 @@
2
2
 
3
3
  'use client';
4
4
 
5
+ import {useState} from 'react';
5
6
  import {Selector} from '@astryxdesign/core/Selector';
6
7
 
7
8
  export default function SelectorShowcase() {
9
+ const [value, setValue] = useState<string | undefined>();
8
10
  return (
9
11
  <Selector
10
12
  style={{width: 300}}
11
13
  label="Fruit"
12
14
  options={['Apple', 'Banana', 'Orange', 'Mango', 'Pineapple']}
13
15
  placeholder="Select a fruit..."
14
- onChange={() => {}}
16
+ value={value}
17
+ onChange={setValue}
15
18
  />
16
19
  );
17
20
  }
@@ -2,12 +2,14 @@
2
2
 
3
3
  'use client';
4
4
 
5
+ import {useState} from 'react';
5
6
  import {TabList, Tab} from '@astryxdesign/core/TabList';
6
7
  import {Badge} from '@astryxdesign/core/Badge';
7
8
 
8
9
  export default function TabShowcase() {
10
+ const [value, setValue] = useState('inbox');
9
11
  return (
10
- <TabList value="inbox" onChange={() => {}}>
12
+ <TabList value={value} onChange={setValue}>
11
13
  <Tab
12
14
  value="inbox"
13
15
  label="Inbox"
@@ -2,11 +2,13 @@
2
2
 
3
3
  'use client';
4
4
 
5
+ import {useState} from 'react';
5
6
  import {TabList, Tab} from '@astryxdesign/core/TabList';
6
7
 
7
8
  export default function TabListShowcase() {
9
+ const [value, setValue] = useState('home');
8
10
  return (
9
- <TabList value="home" onChange={() => {}}>
11
+ <TabList value={value} onChange={setValue}>
10
12
  <Tab value="home" label="Home" />
11
13
  <Tab value="projects" label="Projects" />
12
14
  <Tab value="settings" label="Settings" />
@@ -2,11 +2,13 @@
2
2
 
3
3
  'use client';
4
4
 
5
+ import {useState} from 'react';
5
6
  import {TabList, Tab, TabMenu} from '@astryxdesign/core/TabList';
6
7
 
7
8
  export default function TabMenuShowcase() {
9
+ const [value, setValue] = useState('settings');
8
10
  return (
9
- <TabList value="settings" onChange={() => {}}>
11
+ <TabList value={value} onChange={setValue}>
10
12
  <Tab value="overview" label="Overview" />
11
13
  <Tab value="activity" label="Activity" />
12
14
  <TabMenu
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'useTableColumnResize',
7
+ name: 'useTableColumnResize — Draggable Columns',
8
+ displayName: 'useTableColumnResize — Draggable Columns',
9
+ description:
10
+ 'A Table using useTableColumnResize. Drag the right edge of any column header to resize; widths are committed on release. The last proportional column flexes to fill remaining space.',
11
+ isReady: true,
12
+ aspectRatio: 16 / 9,
13
+ componentsUsed: ['Table'],
14
+ };
@@ -0,0 +1,59 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {useState} from 'react';
6
+ import {
7
+ Table,
8
+ useTableColumnResize,
9
+ proportional,
10
+ pixel,
11
+ } from '@astryxdesign/core/Table';
12
+
13
+ interface User extends Record<string, unknown> {
14
+ id: string;
15
+ name: string;
16
+ email: string;
17
+ role: string;
18
+ }
19
+
20
+ const users: User[] = [
21
+ {
22
+ id: '1',
23
+ name: 'Alice Johnson',
24
+ email: 'alice@example.com',
25
+ role: 'Engineer',
26
+ },
27
+ {id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'Designer'},
28
+ {id: '3', name: 'Charlie Brown', email: 'charlie@example.com', role: 'PM'},
29
+ {id: '4', name: 'Diana Prince', email: 'diana@example.com', role: 'Engineer'},
30
+ {id: '5', name: 'Eve Davis', email: 'eve@example.com', role: 'Analyst'},
31
+ ];
32
+
33
+ const columns = [
34
+ {key: 'name', header: 'Name'},
35
+ {key: 'email', header: 'Email', width: proportional(2)},
36
+ {key: 'role', header: 'Role', width: pixel(120)},
37
+ ];
38
+
39
+ export default function ColumnResizeHookUsage() {
40
+ const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
41
+
42
+ const columnResize = useTableColumnResize({
43
+ columnWidths,
44
+ columns,
45
+ onColumnResizeEnd: updates => {
46
+ setColumnWidths(prev => ({...prev, ...updates}));
47
+ },
48
+ });
49
+
50
+ return (
51
+ <Table
52
+ data={users}
53
+ columns={columns}
54
+ idKey="id"
55
+ hasHover
56
+ plugins={{columnResize}}
57
+ />
58
+ );
59
+ }
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'useTableStickyColumns',
7
+ name: 'useTableStickyColumns — Pinned Columns',
8
+ displayName: 'useTableStickyColumns — Pinned Columns',
9
+ description:
10
+ 'A Table using useTableStickyColumns to pin the Name column to the start edge and Status to the end edge. Scroll horizontally — pinned columns stay in view with a soft shadow over the scrolling content.',
11
+ isReady: true,
12
+ aspectRatio: 16 / 9,
13
+ componentsUsed: ['Table'],
14
+ };
@@ -0,0 +1,104 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {Table, useTableStickyColumns, pixel} from '@astryxdesign/core/Table';
6
+
7
+ interface Employee extends Record<string, unknown> {
8
+ id: string;
9
+ name: string;
10
+ email: string;
11
+ team: string;
12
+ role: string;
13
+ location: string;
14
+ startDate: string;
15
+ status: string;
16
+ }
17
+
18
+ const employees: Employee[] = [
19
+ {
20
+ id: '1',
21
+ name: 'Alice Johnson',
22
+ email: 'alice@example.com',
23
+ team: 'Design Systems',
24
+ role: 'Staff Engineer',
25
+ location: 'San Francisco',
26
+ startDate: '2019-03-12',
27
+ status: 'Active',
28
+ },
29
+ {
30
+ id: '2',
31
+ name: 'Bob Smith',
32
+ email: 'bob@example.com',
33
+ team: 'Design Systems',
34
+ role: 'Senior Designer',
35
+ location: 'New York',
36
+ startDate: '2020-07-01',
37
+ status: 'Active',
38
+ },
39
+ {
40
+ id: '3',
41
+ name: 'Charlie Brown',
42
+ email: 'charlie@example.com',
43
+ team: 'Platform',
44
+ role: 'Engineering Manager',
45
+ location: 'London',
46
+ startDate: '2017-11-20',
47
+ status: 'On leave',
48
+ },
49
+ {
50
+ id: '4',
51
+ name: 'Diana Prince',
52
+ email: 'diana@example.com',
53
+ team: 'Platform',
54
+ role: 'Staff Engineer',
55
+ location: 'Remote',
56
+ startDate: '2021-01-15',
57
+ status: 'Active',
58
+ },
59
+ {
60
+ id: '5',
61
+ name: 'Eve Davis',
62
+ email: 'eve@example.com',
63
+ team: 'Growth',
64
+ role: 'Product Engineer',
65
+ location: 'Berlin',
66
+ startDate: '2022-05-30',
67
+ status: 'Active',
68
+ },
69
+ ];
70
+
71
+ // Wide fixed-width columns so the table overflows and scrolls horizontally,
72
+ // keeping the pinned Name (start) and Status (end) columns in view.
73
+ const columns = [
74
+ {key: 'name', header: 'Name', width: pixel(180)},
75
+ {key: 'email', header: 'Email', width: pixel(220)},
76
+ {key: 'team', header: 'Team', width: pixel(180)},
77
+ {key: 'role', header: 'Role', width: pixel(200)},
78
+ {key: 'location', header: 'Location', width: pixel(160)},
79
+ {key: 'startDate', header: 'Start date', width: pixel(140)},
80
+ {key: 'status', header: 'Status', width: pixel(140)},
81
+ ];
82
+
83
+ export default function StickyColumnsHookUsage() {
84
+ const stickyColumns = useTableStickyColumns<Employee>({
85
+ startKeys: ['name'],
86
+ endKeys: ['status'],
87
+ });
88
+
89
+ return (
90
+ // Constrain the width so the Table's own horizontal scroll container is
91
+ // narrower than its columns — that scroll container is what sticky columns
92
+ // pin against. Without a width cap the table renders full-width and never
93
+ // scrolls internally, so nothing sticks.
94
+ <div style={{width: 560, maxWidth: '100%'}}>
95
+ <Table
96
+ data={employees}
97
+ columns={columns}
98
+ idKey="id"
99
+ hasHover
100
+ plugins={{stickyColumns}}
101
+ />
102
+ </div>
103
+ );
104
+ }
@@ -6,11 +6,26 @@ import {Text} from '@astryxdesign/core/Text';
6
6
  import {Stack} from '@astryxdesign/core/Stack';
7
7
 
8
8
  const COLORS = [
9
- {color: 'primary' as const, description: 'Primary — Default for headings and body text'},
10
- {color: 'secondary' as const, description: 'Secondary — Supporting details and metadata'},
11
- {color: 'active' as const, description: 'ActiveLinks, success states, and emphasis'},
12
- {color: 'disabled' as const, description: 'Disabled — Unavailable or inactive content'},
13
- {color: 'placeholder' as const, description: 'Placeholder — Empty field hints'},
9
+ {
10
+ color: 'primary' as const,
11
+ description: 'PrimaryDefault for headings and body text',
12
+ },
13
+ {
14
+ color: 'secondary' as const,
15
+ description: 'Secondary — Supporting details and metadata',
16
+ },
17
+ {
18
+ color: 'accent' as const,
19
+ description: 'Accent — Links, emphasis, and accent-colored text',
20
+ },
21
+ {
22
+ color: 'disabled' as const,
23
+ description: 'Disabled — Unavailable or inactive content',
24
+ },
25
+ {
26
+ color: 'placeholder' as const,
27
+ description: 'Placeholder — Empty field hints',
28
+ },
14
29
  ];
15
30
 
16
31
  export default function TextColors() {
@@ -2,15 +2,17 @@
2
2
 
3
3
  'use client';
4
4
 
5
+ import {useState} from 'react';
5
6
  import {TextArea} from '@astryxdesign/core/TextArea';
6
7
 
7
8
  export default function TextAreaShowcase() {
9
+ const [value, setValue] = useState('');
8
10
  return (
9
11
  <div style={{width: 400}}>
10
12
  <TextArea
11
13
  label="Description"
12
- value=""
13
- onChange={() => {}}
14
+ value={value}
15
+ onChange={setValue}
14
16
  placeholder="Enter a description..."
15
17
  />
16
18
  </div>
@@ -2,15 +2,17 @@
2
2
 
3
3
  'use client';
4
4
 
5
+ import {useState} from 'react';
5
6
  import {TextInput} from '@astryxdesign/core/TextInput';
6
7
 
7
8
  export default function TextInputShowcase() {
9
+ const [value, setValue] = useState('');
8
10
  return (
9
11
  <div style={{width: 300}}>
10
12
  <TextInput
11
13
  label="Name"
12
- value=""
13
- onChange={() => {}}
14
+ value={value}
15
+ onChange={setValue}
14
16
  placeholder="Enter your name"
15
17
  />
16
18
  </div>
@@ -8,10 +8,30 @@ import {Stack} from '@astryxdesign/core/Layout';
8
8
  import {Text} from '@astryxdesign/core/Text';
9
9
 
10
10
  const ATTACHMENTS = [
11
- {id: 1, src: 'https://lookaside.facebook.com/assets/astryx/illustrative-vertical-1.png', alt: 'River through a valley', label: 'valley.jpg'},
12
- {id: 2, src: 'https://lookaside.facebook.com/assets/astryx/illustrative-vertical-2.png', alt: 'Foggy mountain peak', label: 'mountain.jpg'},
13
- {id: 3, src: 'https://lookaside.facebook.com/assets/astryx/illustrative-vertical-3.png', alt: 'Golden retriever puppy', label: 'puppy.jpg'},
14
- {id: 4, src: 'https://lookaside.facebook.com/assets/astryx/illustrative-vertical-4.png', alt: 'Bridge at sunset', label: 'bridge.jpg'},
11
+ {
12
+ id: 1,
13
+ src: 'https://lookaside.facebook.com/assets/astryx/illustrative-vertical-1.png',
14
+ alt: 'River through a valley',
15
+ label: 'valley.jpg',
16
+ },
17
+ {
18
+ id: 2,
19
+ src: 'https://lookaside.facebook.com/assets/astryx/illustrative-vertical-2.png',
20
+ alt: 'Foggy mountain peak',
21
+ label: 'mountain.jpg',
22
+ },
23
+ {
24
+ id: 3,
25
+ src: 'https://lookaside.facebook.com/assets/astryx/illustrative-vertical-3.png',
26
+ alt: 'Golden retriever puppy',
27
+ label: 'puppy.jpg',
28
+ },
29
+ {
30
+ id: 4,
31
+ src: 'https://lookaside.facebook.com/assets/astryx/illustrative-vertical-4.png',
32
+ alt: 'Bridge at sunset',
33
+ label: 'bridge.jpg',
34
+ },
15
35
  ];
16
36
 
17
37
  export default function ThumbnailGallery() {
@@ -38,7 +58,7 @@ export default function ThumbnailGallery() {
38
58
  ))}
39
59
  </Stack>
40
60
  {selected != null && (
41
- <Text type="supporting" color="active">
61
+ <Text type="supporting" color="accent">
42
62
  Previewing: {selected}
43
63
  </Text>
44
64
  )}
@@ -31,9 +31,9 @@ export default function TimestampColors() {
31
31
  </Stack>
32
32
  <Stack direction="vertical" gap={1}>
33
33
  <Text type="supporting" color="secondary">
34
- Active
34
+ Accent
35
35
  </Text>
36
- <Timestamp value={DATE} format="date_time" color="active" />
36
+ <Timestamp value={DATE} format="date_time" color="accent" />
37
37
  </Stack>
38
38
  </Stack>
39
39
  );
@@ -2,8 +2,9 @@
2
2
 
3
3
  'use client';
4
4
 
5
+ import {useState} from 'react';
5
6
  import {Tokenizer} from '@astryxdesign/core/Tokenizer';
6
- import type {SearchSource} from '@astryxdesign/core/Typeahead';
7
+ import type {SearchableItem, SearchSource} from '@astryxdesign/core/Typeahead';
7
8
 
8
9
  const source: SearchSource = {
9
10
  search: () => [],
@@ -11,16 +12,17 @@ const source: SearchSource = {
11
12
  };
12
13
 
13
14
  export default function TokenizerShowcase() {
15
+ const [value, setValue] = useState<SearchableItem[]>([
16
+ {id: '1', label: 'Design'},
17
+ {id: '2', label: 'Engineering'},
18
+ ]);
14
19
  return (
15
20
  <Tokenizer
16
21
  label="Tags"
17
22
  placeholder="Search..."
18
23
  searchSource={source}
19
- value={[
20
- {id: '1', label: 'Design'},
21
- {id: '2', label: 'Engineering'},
22
- ]}
23
- onChange={() => {}}
24
+ value={value}
25
+ onChange={setValue}
24
26
  style={{width: 400}}
25
27
  />
26
28
  );
@@ -1,5 +1,8 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ 'use client';
4
+
5
+ import {useState} from 'react';
3
6
  import {Typeahead} from '@astryxdesign/core/Typeahead';
4
7
  import type {SearchableItem, SearchSource} from '@astryxdesign/core/Typeahead';
5
8
 
@@ -21,14 +24,15 @@ const fruitSource: SearchSource = {
21
24
  };
22
25
 
23
26
  export default function TypeaheadShowcase() {
27
+ const [value, setValue] = useState<SearchableItem | null>(null);
24
28
  return (
25
29
  <div style={{width: 320}}>
26
30
  <Typeahead
27
31
  label="Fruit"
28
32
  placeholder="Search fruits..."
29
33
  searchSource={fruitSource}
30
- value={null}
31
- onChange={() => {}}
34
+ value={value}
35
+ onChange={setValue}
32
36
  />
33
37
  </div>
34
38
  );
@@ -143,12 +143,6 @@ const styles: Record<string, CSSProperties> = {
143
143
  textAlign: 'center' as const,
144
144
  maxWidth: 560,
145
145
  },
146
- // Hero headline painted with the theme's accent-text ink. Text has no
147
- // 'accent' color value, so we set the dedicated --color-text-accent token
148
- // directly (distinct from --color-accent / the 'active' color).
149
- heroHeadline: {
150
- color: 'var(--color-text-accent)',
151
- },
152
146
  centerText: {
153
147
  textAlign: 'center',
154
148
  },
@@ -438,7 +432,7 @@ function StorePreview({
438
432
  <VStack gap={10} style={{...styles.content, ...styles.contentFluid}}>
439
433
  <Center>
440
434
  <VStack gap={4} hAlign="center" style={styles.heroText}>
441
- <Text type="display-2" style={styles.heroHeadline}>
435
+ <Text type="display-2" color="accent">
442
436
  Little joys,
443
437
  <br />
444
438
  everywhere you go
@@ -462,10 +456,7 @@ function StorePreview({
462
456
  />
463
457
  </AspectRatio>
464
458
  <div style={inlineStyles.cardBody}>
465
- <VStack
466
- gap={2}
467
- hAlign="center"
468
- style={styles.cardStack}>
459
+ <VStack gap={2} hAlign="center" style={styles.cardStack}>
469
460
  <HStack>
470
461
  <Badge label={p.badge} variant={p.badgeVariant} />
471
462
  </HStack>
@@ -475,7 +466,10 @@ function StorePreview({
475
466
  <Text
476
467
  type="supporting"
477
468
  color="secondary"
478
- style={{...styles.cardDescription, ...styles.centerText}}>
469
+ style={{
470
+ ...styles.cardDescription,
471
+ ...styles.centerText,
472
+ }}>
479
473
  {p.description}
480
474
  </Text>
481
475
  <HStack gap={2} vAlign="center" hAlign="center">
@@ -746,9 +740,7 @@ function ChatCard() {
746
740
 
747
741
  <ChatMessage sender="assistant">
748
742
  <VStack gap={3}>
749
- <Text type="body">
750
- Here’s everything I have on order #1043:
751
- </Text>
743
+ <Text type="body">Here’s everything I have on order #1043:</Text>
752
744
  <Card padding={3}>
753
745
  <VStack gap={1}>
754
746
  <Item
@@ -1107,10 +1099,7 @@ function InventoryCard({
1107
1099
  ).length;
1108
1100
  return (
1109
1101
  <Card padding={0} style={styles.inventoryCard}>
1110
- <HStack
1111
- hAlign="between"
1112
- vAlign="center"
1113
- style={styles.inventoryHeader}>
1102
+ <HStack hAlign="between" vAlign="center" style={styles.inventoryHeader}>
1114
1103
  <Heading level={2}>Inventory</Heading>
1115
1104
  <Button
1116
1105
  label="Add item"