@astryxdesign/cli 0.4.7 → 0.5.0-canary.055e75f

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 (87) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +53 -51
  3. package/api/docs/docs.doc.mjs +2 -2
  4. package/api/index.d.mts +1 -1
  5. package/api/index.mjs +1 -1
  6. package/api/search/search.mjs +50 -4
  7. package/api/search/search.test.mjs +71 -0
  8. package/api/template/template.doc.mjs +2 -2
  9. package/api/theme/build/build.mjs +12 -42
  10. package/api/theme/targets/targets.d.mts +18 -0
  11. package/api/theme/targets/targets.mjs +87 -0
  12. package/api/theme/targets/targets.test.mjs +66 -0
  13. package/api/theme/theme.d.mts +1 -0
  14. package/api/theme/theme.mjs +3 -1
  15. package/api/theme/theme.type.d.mts +23 -0
  16. package/api/theme/theme.type.mjs +21 -1
  17. package/api/theme/themeTargets.doc.d.mts +11 -0
  18. package/api/theme/themeTargets.doc.mjs +58 -0
  19. package/api/theme/themeTemplate.doc.mjs +3 -3
  20. package/assets/codemods/__tests__/registry.test.mjs +1 -0
  21. package/assets/codemods/registry.mjs +1 -0
  22. package/assets/codemods/transforms/v0.5.0/__tests__/next-codemods.test.mjs +127 -0
  23. package/assets/codemods/transforms/v0.5.0/banner-collapsible-content.mjs +171 -0
  24. package/assets/codemods/transforms/v0.5.0/index.mjs +20 -0
  25. package/assets/docs/README.md +50 -0
  26. package/assets/docs/cli-integrations.doc.mjs +4 -4
  27. package/assets/docs/theme.doc.dense.mjs +1 -1
  28. package/assets/docs/theme.doc.mjs +1 -1
  29. package/assets/docs/typography.doc.mjs +2 -2
  30. package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.doc.mjs +1 -1
  31. package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.tsx +1 -1
  32. package/assets/templates/blocks/components/DateInput/DateInputClearable.tsx +5 -1
  33. package/assets/templates/blocks/components/DateInput/DateInputDateRange.tsx +6 -1
  34. package/assets/templates/blocks/components/DateInput/DateInputFormats.tsx +5 -1
  35. package/assets/templates/blocks/components/DateInput/DateInputShowcase.tsx +4 -1
  36. package/assets/templates/blocks/components/DateInput/DateInputWithDescription.tsx +5 -1
  37. package/assets/templates/blocks/components/DateInput/DateInputWithValidation.tsx +5 -1
  38. package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.doc.mjs +25 -0
  39. package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.tsx +167 -0
  40. package/assets/templates/blocks/components/Dialog/DialogScrollingContent.tsx +1 -1
  41. package/assets/templates/blocks/components/HoverCard/HoverCardHookUsage.tsx +4 -1
  42. package/assets/templates/blocks/components/Step/StepContent.doc.mjs +14 -0
  43. package/assets/templates/blocks/components/Step/StepContent.tsx +32 -0
  44. package/assets/templates/blocks/components/Step/StepIndicator.doc.mjs +14 -0
  45. package/assets/templates/blocks/components/Step/StepIndicator.tsx +60 -0
  46. package/assets/templates/blocks/components/Step/StepShowcase.doc.mjs +15 -0
  47. package/assets/templates/blocks/components/Step/StepShowcase.tsx +26 -0
  48. package/assets/templates/blocks/components/Step/StepStates.doc.mjs +14 -0
  49. package/assets/templates/blocks/components/Step/StepStates.tsx +46 -0
  50. package/assets/templates/blocks/components/Stepper/StepperCustomContent.doc.mjs +22 -0
  51. package/assets/templates/blocks/components/Stepper/StepperCustomContent.tsx +126 -0
  52. package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.doc.mjs +1 -1
  53. package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.tsx +17 -5
  54. package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.doc.mjs +14 -0
  55. package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.tsx +25 -0
  56. package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.doc.mjs +2 -2
  57. package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.tsx +1 -1
  58. package/assets/templates/blocks/components/Stepper/StepperShowcase.doc.mjs +1 -1
  59. package/assets/templates/blocks/components/Stepper/StepperShowcase.tsx +6 -7
  60. package/assets/templates/blocks/components/Stepper/StepperStatus.tsx +1 -1
  61. package/assets/templates/pages/mixed-gallery/page.tsx +12 -3
  62. package/assets/templates/pages/table-grouped/page.tsx +151 -144
  63. package/assets/templates/pages/theme-showcase/page.tsx +36 -17
  64. package/assets/templates/themes/chocolate/chocolateTheme.ts +3 -1
  65. package/assets/templates/themes/matcha/matchaTheme.ts +3 -1
  66. package/assets/templates/themes/neutral/neutralTheme.ts +16 -9
  67. package/assets/templates/themes/stone/stoneTheme.ts +3 -1
  68. package/clients/cli/commands/build-theme.mjs +85 -0
  69. package/clients/cli/commands/dialog-adaptive-template.test.mjs +24 -0
  70. package/clients/cli/commands/theme-targets.behavior.test.mjs +64 -0
  71. package/clients/cli/commands/theme-targets.doc.mjs +38 -0
  72. package/clients/cli/commands/theme-template.doc.mjs +2 -2
  73. package/clients/cli/commands/theme.doc.mjs +4 -2
  74. package/clients/cli/index.mjs +1 -0
  75. package/clients/cli/lib/component-format.mjs +2 -2
  76. package/clients/cli/lib/manifest.mjs +2 -0
  77. package/foundation/discovery/theming-targets.d.mts +86 -0
  78. package/foundation/discovery/theming-targets.mjs +202 -0
  79. package/foundation/discovery/theming-targets.test.mjs +245 -0
  80. package/foundation/response/response-types.doc.mjs +7 -2
  81. package/package.json +9 -9
  82. package/assets/templates/blocks/components/Stepper/StepperHorizontal.doc.mjs +0 -14
  83. package/assets/templates/blocks/components/Stepper/StepperHorizontal.tsx +0 -24
  84. package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.doc.mjs +0 -14
  85. package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.tsx +0 -92
  86. package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.doc.mjs +0 -14
  87. package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.tsx +0 -40
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'Step',
7
+ name: 'Step — States',
8
+ displayName: 'Step — States',
9
+ description:
10
+ 'Every state a single Step can land in, each shown as one Step in its own Stepper. Completed, current, and upcoming are derived by comparing the step index against the parent activeStep, so they are never set directly; isDisabled and status are the two a step declares itself. Status is a separate axis from progress, which is why a completed step can still carry a warning.',
11
+ isReady: true,
12
+ aspectRatio: 4 / 3,
13
+ componentsUsed: ['Stepper', 'Step', 'Text'],
14
+ };
@@ -0,0 +1,46 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {Stepper, Step} from '@astryxdesign/core/Stepper';
6
+ import {Text} from '@astryxdesign/core/Text';
7
+
8
+ export default function StepStates() {
9
+ // A Step holds no state of its own — it compares its `step` index against the
10
+ // Stepper's `activeStep`. Each state below is therefore one Step in its own
11
+ // Stepper, with activeStep placed on one side of it or the other.
12
+ return (
13
+ <div style={{display: 'flex', flexWrap: 'wrap', gap: 32}}>
14
+ <div style={{width: 220}}>
15
+ <Text type="label">Completed</Text>
16
+ <Stepper activeStep={1} orientation="vertical">
17
+ <Step step={0} label="Verify email" />
18
+ </Stepper>
19
+ </div>
20
+ <div style={{width: 220}}>
21
+ <Text type="label">Current</Text>
22
+ <Stepper activeStep={0} orientation="vertical">
23
+ <Step step={0} label="Verify email" />
24
+ </Stepper>
25
+ </div>
26
+ <div style={{width: 220}}>
27
+ <Text type="label">Upcoming</Text>
28
+ <Stepper activeStep={0} orientation="vertical">
29
+ <Step step={1} label="Add a payment method" />
30
+ </Stepper>
31
+ </div>
32
+ <div style={{width: 220}}>
33
+ <Text type="label">Disabled</Text>
34
+ <Stepper activeStep={0} orientation="vertical" onStepClick={() => {}}>
35
+ <Step step={1} label="Add a payment method" isDisabled />
36
+ </Stepper>
37
+ </div>
38
+ <div style={{width: 220}}>
39
+ <Text type="label">Completed, with a status</Text>
40
+ <Stepper activeStep={1} orientation="vertical">
41
+ <Step step={0} label="Verify email" status="warning" />
42
+ </Stepper>
43
+ </div>
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,22 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'Stepper',
7
+ name: 'Stepper — Custom Content',
8
+ displayName: 'Stepper — Custom Content',
9
+ description:
10
+ 'A vertical stepper where each step owns a slice of the page. The content slot takes any node (form fields, a summary panel, a banner), so a stepper is not limited to multi-step forms. Rendering the slot only for the active step is what makes the flow expand one step at a time.',
11
+ isReady: true,
12
+ aspectRatio: 4 / 3,
13
+ componentsUsed: [
14
+ 'Stepper',
15
+ 'TextInput',
16
+ 'Button',
17
+ 'Text',
18
+ 'Card',
19
+ 'Badge',
20
+ 'Banner',
21
+ ],
22
+ };
@@ -0,0 +1,126 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {useState} from 'react';
6
+ import {Stepper, Step} from '@astryxdesign/core/Stepper';
7
+ import {TextInput} from '@astryxdesign/core/TextInput';
8
+ import {Button} from '@astryxdesign/core/Button';
9
+ import {Text} from '@astryxdesign/core/Text';
10
+ import {Card} from '@astryxdesign/core/Card';
11
+ import {Badge} from '@astryxdesign/core/Badge';
12
+ import {Banner} from '@astryxdesign/core/Banner';
13
+
14
+ export default function StepperCustomContent() {
15
+ const [active, setActive] = useState(0);
16
+ // The content slot takes any node, not just form fields — a panel, a summary,
17
+ // a notice. Gating it on `active` is what makes the flow expand a step at a
18
+ // time.
19
+ return (
20
+ <div style={{width: 680}}>
21
+ <Stepper
22
+ activeStep={active}
23
+ orientation="vertical"
24
+ onStepClick={setActive}>
25
+ <Step
26
+ step={0}
27
+ label="Project details"
28
+ description="Name it and point us at the source"
29
+ indicator="number">
30
+ {active === 0 && (
31
+ <div style={{display: 'flex', flexDirection: 'column', gap: 12}}>
32
+ <div style={{display: 'flex', gap: 12}}>
33
+ <TextInput
34
+ label="Project name"
35
+ placeholder="My awesome project"
36
+ value=""
37
+ width="100%"
38
+ />
39
+ <TextInput
40
+ label="Repository URL"
41
+ placeholder="https://github.com/..."
42
+ value=""
43
+ width="100%"
44
+ />
45
+ </div>
46
+ <div>
47
+ <Button
48
+ label="Continue"
49
+ variant="primary"
50
+ onClick={() => setActive(1)}
51
+ />
52
+ </div>
53
+ </div>
54
+ )}
55
+ </Step>
56
+ <Step
57
+ step={1}
58
+ label="Review the build"
59
+ description="What we detected from your default branch"
60
+ indicator="number">
61
+ {active === 1 && (
62
+ <div style={{display: 'flex', flexDirection: 'column', gap: 12}}>
63
+ <Card variant="muted" padding={3}>
64
+ <div style={{display: 'flex', flexDirection: 'column', gap: 8}}>
65
+ <div
66
+ style={{
67
+ display: 'flex',
68
+ alignItems: 'center',
69
+ gap: 8,
70
+ }}>
71
+ <Text type="label">Next.js 15</Text>
72
+ <Badge variant="success" label="Detected" />
73
+ </div>
74
+ <Text type="supporting">
75
+ Build command <code>next build</code> · Output{' '}
76
+ <code>.next</code> · Node 20
77
+ </Text>
78
+ </div>
79
+ </Card>
80
+ <div style={{display: 'flex', gap: 8}}>
81
+ <Button
82
+ label="Back"
83
+ variant="secondary"
84
+ onClick={() => setActive(0)}
85
+ />
86
+ <Button
87
+ label="Looks right"
88
+ variant="primary"
89
+ onClick={() => setActive(2)}
90
+ />
91
+ </div>
92
+ </div>
93
+ )}
94
+ </Step>
95
+ <Step
96
+ step={2}
97
+ label="Deploy"
98
+ description="Ships to production"
99
+ indicator="number">
100
+ {active === 2 && (
101
+ <div style={{display: 'flex', flexDirection: 'column', gap: 12}}>
102
+ <Banner
103
+ status="info"
104
+ title="First deploy takes a few minutes"
105
+ description="Later deploys reuse the build cache and finish in under a minute."
106
+ />
107
+ <div style={{display: 'flex', gap: 8}}>
108
+ <Button
109
+ label="Back"
110
+ variant="secondary"
111
+ onClick={() => setActive(1)}
112
+ />
113
+ <Button
114
+ label="Deploy now"
115
+ variant="primary"
116
+ onClick={() => setActive(3)}
117
+ />
118
+ </div>
119
+ </div>
120
+ )}
121
+ </Step>
122
+ <Step step={3} label="Live" indicator="number" />
123
+ </Stepper>
124
+ </div>
125
+ );
126
+ }
@@ -7,7 +7,7 @@ export const doc = {
7
7
  name: 'Stepper — Indicator Modes',
8
8
  displayName: 'Stepper — Indicator Modes',
9
9
  description:
10
- 'The indicator prop side by side: auto (check when done, ring when current, number ahead), always-number, and a custom icon per step.',
10
+ 'The indicator prop side by side: none (the bar and label carry the progress on their own), auto (check when done, ring when current, number ahead), always-number, and a custom icon per step.',
11
11
  isReady: true,
12
12
  aspectRatio: 4 / 3,
13
13
  componentsUsed: ['Stepper', 'Text', 'Icon'],
@@ -3,7 +3,7 @@
3
3
  'use client';
4
4
 
5
5
  import {useState} from 'react';
6
- import {Stepper, Step} from '@astryxdesign/lab';
6
+ import {Stepper, Step} from '@astryxdesign/core/Stepper';
7
7
  import {Text} from '@astryxdesign/core/Text';
8
8
  import {Icon} from '@astryxdesign/core/Icon';
9
9
 
@@ -11,6 +11,18 @@ export default function StepperIndicatorModes() {
11
11
  const [active, setActive] = useState(2);
12
12
  return (
13
13
  <div style={{display: 'flex', gap: 48, flexWrap: 'wrap'}}>
14
+ <div style={{maxWidth: 220}}>
15
+ <Text type="label">None</Text>
16
+ <Stepper
17
+ activeStep={active}
18
+ orientation="vertical"
19
+ onStepClick={setActive}>
20
+ <Step step={0} label="Account" indicator="none" />
21
+ <Step step={1} label="Profile" indicator="none" />
22
+ <Step step={2} label="Settings" indicator="none" />
23
+ <Step step={3} label="Review" indicator="none" />
24
+ </Stepper>
25
+ </div>
14
26
  <div style={{maxWidth: 220}}>
15
27
  <Text type="label">Auto</Text>
16
28
  <Stepper
@@ -44,22 +56,22 @@ export default function StepperIndicatorModes() {
44
56
  <Step
45
57
  step={0}
46
58
  label="Account"
47
- icon={<Icon icon="info" size="sm" />}
59
+ indicator={<Icon icon="info" size="sm" />}
48
60
  />
49
61
  <Step
50
62
  step={1}
51
63
  label="Profile"
52
- icon={<Icon icon="search" size="sm" />}
64
+ indicator={<Icon icon="search" size="sm" />}
53
65
  />
54
66
  <Step
55
67
  step={2}
56
68
  label="Settings"
57
- icon={<Icon icon="wrench" size="sm" />}
69
+ indicator={<Icon icon="wrench" size="sm" />}
58
70
  />
59
71
  <Step
60
72
  step={3}
61
73
  label="Review"
62
- icon={<Icon icon="check" size="sm" />}
74
+ indicator={<Icon icon="check" size="sm" />}
63
75
  />
64
76
  </Stepper>
65
77
  </div>
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'Stepper',
7
+ name: 'Stepper — On-Track Horizontal',
8
+ displayName: 'Stepper — On-Track Horizontal',
9
+ description:
10
+ 'The on-track layout in horizontal orientation: instead of sitting beside the label, each indicator is slotted into the connector itself, so the numbered nodes read as beads on one continuous line. Labels center under their node.',
11
+ isReady: true,
12
+ aspectRatio: 16 / 9,
13
+ componentsUsed: ['Stepper'],
14
+ };
@@ -0,0 +1,25 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {useState} from 'react';
6
+ import {Stepper, Step} from '@astryxdesign/core/Stepper';
7
+
8
+ export default function StepperOnTrackHorizontal() {
9
+ const [active, setActive] = useState(2);
10
+ return (
11
+ <div style={{width: 560}}>
12
+ <Stepper
13
+ activeStep={active}
14
+ orientation="horizontal"
15
+ indicatorPosition="on-track"
16
+ onStepClick={setActive}>
17
+ <Step step={0} label="Workspace" indicator="number" />
18
+ <Step step={1} label="Team" indicator="number" />
19
+ <Step step={2} label="Integrations" indicator="number" />
20
+ <Step step={3} label="Import" indicator="number" />
21
+ <Step step={4} label="Launch" indicator="number" />
22
+ </Stepper>
23
+ </div>
24
+ );
25
+ }
@@ -7,8 +7,8 @@ export const doc = {
7
7
  name: 'Stepper — On-Track Vertical',
8
8
  displayName: 'Stepper — On-Track Vertical',
9
9
  description:
10
- 'The on-track layout in vertical orientation: indicators sit inline on a continuous connector rail, with each label and description beside its node.',
10
+ 'The on-track layout in vertical orientation: indicators sit inline on a continuous connector rail, with each label and description beside its node. Compare with the separated layout, where the indicator sits alongside its own bar segment instead of on a shared rail.',
11
11
  isReady: true,
12
12
  aspectRatio: 4 / 3,
13
- componentsUsed: ['Stepper'],
13
+ componentsUsed: ['Stepper', 'Step'],
14
14
  };
@@ -3,7 +3,7 @@
3
3
  'use client';
4
4
 
5
5
  import {useState} from 'react';
6
- import {Stepper, Step} from '@astryxdesign/lab';
6
+ import {Stepper, Step} from '@astryxdesign/core/Stepper';
7
7
 
8
8
  export default function StepperOnTrackVertical() {
9
9
  const [active, setActive] = useState(2);
@@ -7,7 +7,7 @@ export const doc = {
7
7
  name: 'Stepper — Checkout Progress',
8
8
  displayName: 'Stepper — Checkout Progress',
9
9
  description:
10
- 'A horizontal on-track stepper for a checkout flow: numbered nodes sit on a continuous connector, with completed and current steps filled. Click any step to jump.',
10
+ 'The default stepper: a horizontal track where every step owns an equal segment of the progress bar above its label. The default auto indicator resolves itself per step — a check once the step is done, a ring on the current step, a number for the ones still ahead. Click any step to jump.',
11
11
  isReady: true,
12
12
  isShowcase: true,
13
13
  aspectRatio: 16 / 9,
@@ -3,7 +3,7 @@
3
3
  'use client';
4
4
 
5
5
  import {useState} from 'react';
6
- import {Stepper, Step} from '@astryxdesign/lab';
6
+ import {Stepper, Step} from '@astryxdesign/core/Stepper';
7
7
 
8
8
  export default function StepperShowcase() {
9
9
  const [active, setActive] = useState(2);
@@ -12,13 +12,12 @@ export default function StepperShowcase() {
12
12
  <Stepper
13
13
  activeStep={active}
14
14
  orientation="horizontal"
15
- indicatorPosition="on-track"
16
15
  onStepClick={setActive}>
17
- <Step step={0} label="Cart" indicator="number" />
18
- <Step step={1} label="Shipping" indicator="number" />
19
- <Step step={2} label="Payment" indicator="number" />
20
- <Step step={3} label="Review" indicator="number" />
21
- <Step step={4} label="Confirm" indicator="number" />
16
+ <Step step={0} label="Cart" />
17
+ <Step step={1} label="Shipping" />
18
+ <Step step={2} label="Payment" />
19
+ <Step step={3} label="Review" />
20
+ <Step step={4} label="Confirm" />
22
21
  </Stepper>
23
22
  </div>
24
23
  );
@@ -3,7 +3,7 @@
3
3
  'use client';
4
4
 
5
5
  import {useState} from 'react';
6
- import {Stepper, Step} from '@astryxdesign/lab';
6
+ import {Stepper, Step} from '@astryxdesign/core/Stepper';
7
7
 
8
8
  export default function StepperStatus() {
9
9
  const [active, setActive] = useState(3);
@@ -36,7 +36,11 @@ const clipStyle: CSSProperties = {
36
36
  // 3 columns on desktop, dropping straight to 1 column below 720px (no 2-col
37
37
  // middle state). minmax(0, 1fr) (not 1fr) so tracks split evenly and ignore the
38
38
  // images' intrinsic min-width. The hero spans 2 columns on desktop, then fills
39
- // the row once it's single-column.
39
+ // the row once it's single-column — and switches from 3:1 to 3:2 with a plain
40
+ // aspect-ratio override. AspectRatio's ratio is a class-level declaration in
41
+ // @layer astryx-base, and this <style> tag is unlayered, so the override wins
42
+ // regardless of specificity — every stacked tile is uniform without rendering
43
+ // the hero twice.
40
44
  const GALLERY_CSS = `
41
45
  .mixed-gallery-grid {
42
46
  display: grid;
@@ -52,6 +56,7 @@ const GALLERY_CSS = `
52
56
  }
53
57
  .mixed-gallery-hero {
54
58
  grid-column: 1 / -1;
59
+ aspect-ratio: 3 / 2;
55
60
  }
56
61
  }
57
62
  `;
@@ -134,9 +139,13 @@ export default function MixedGalleryTemplate() {
134
139
  of three. Every tile is 3:2 except the hero, which is 3:1 so that
135
140
  (being 2 columns wide) it matches the row height exactly. All
136
141
  rows are therefore the same height. Responsive via @container:
137
- 3 columns → 1 column at ≤720px. */}
142
+ 3 columns → 1 column at ≤720px, where the hero drops to 3:2 so
143
+ the stacked tiles are uniform. */}
138
144
  <div className="mixed-gallery-grid">
139
- {/* Hero — spans 2 columns; 3:1 keeps it level with the sidebar */}
145
+ {/* Hero — spans 2 columns; 3:1 keeps it level with the sidebar.
146
+ Stacked, the unlayered aspect-ratio override switches it to
147
+ 3:2 (see GALLERY_CSS) — one element, no duplicate hero
148
+ markup. */}
140
149
  <GalleryCard
141
150
  image={IMAGES[0]}
142
151
  ratio={3 / 1}