@astryxdesign/cli 0.7.1-canary.ff40b78 → 0.7.2-canary.c68f5e8

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 (33) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/api/build/_adapter.d.mts +1 -0
  3. package/api/build/_adapter.mjs +7 -0
  4. package/api/build/build.mjs +4 -1
  5. package/api/search/search.mjs +12 -0
  6. package/api/template/table-filter-view-tabs.test.mjs +45 -0
  7. package/api/theme/build/build.core-on-demand.test.mjs +113 -0
  8. package/api/theme/build/build.d.mts +13 -13
  9. package/api/theme/build/build.mjs +72 -37
  10. package/assets/docs/getting-started.doc.mjs +0 -10
  11. package/assets/docs/styling-overview.doc.mjs +54 -15
  12. package/assets/docs/tree/migration-setup.doc.mjs +31 -6
  13. package/assets/docs/tree/responsive.doc.mjs +37 -0
  14. package/assets/docs/tree/tailwind.doc.mjs +12 -0
  15. package/assets/docs/tree/tokens-and-setup.doc.mjs +3 -2
  16. package/assets/docs/tree/tokens-syntax.doc.mjs +20 -0
  17. package/assets/docs/tree/tokens.doc.mjs +1 -1
  18. package/assets/templates/blocks/components/CommandPalette/CommandPaletteAsyncSearch.tsx +6 -1
  19. package/assets/templates/pages/table-filter/page.tsx +3 -2
  20. package/assets/templates/themes/butter/butterTheme.ts +53 -11
  21. package/assets/templates/themes/chocolate/chocolateTheme.ts +29 -5
  22. package/assets/templates/themes/gothic/gothicTheme.ts +44 -9
  23. package/assets/templates/themes/stone/stoneTheme.ts +58 -11
  24. package/assets/templates/themes/y2k/y2kTheme.ts +60 -10
  25. package/clients/cli/commands/build-theme.mjs +32 -4
  26. package/clients/cli/commands/build-theme.watch.test.mjs +63 -2
  27. package/foundation/discovery/component-discovery.mjs +27 -26
  28. package/foundation/discovery/hook-discovery.mjs +13 -12
  29. package/foundation/fs/listing-cache.d.mts +36 -0
  30. package/foundation/fs/listing-cache.mjs +95 -0
  31. package/foundation/fs/listing-cache.test.mjs +88 -0
  32. package/foundation/fs/paths.mjs +3 -2
  33. package/package.json +9 -9
@@ -129,15 +129,31 @@ export function AppRoot({children}: {children: React.ReactNode}) {
129
129
  },
130
130
  {
131
131
  type: 'prose',
132
- text: 'On Tailwind v3 there is no preflight.css to import, so wrap the @tailwind base directive in a named layer instead. Keep utilities unlayered so existing app utility classes still win everywhere.',
132
+ text: 'A layer\'s rank is set by the first stylesheet on the page that names it, so the global CSS file (or a `layers.css` holding only the `@layer` line) must be the first import in the root layout or app entry. Import it before any module that imports Astryx or theme CSS, including the `astryx-themes` module generated by `astryx theme add` and a root component like the `AppRoot` above that imports `theme.css`. If a theme stylesheet loads first, Tailwind preflight overrides Astryx base styles and theme overrides lose.',
133
133
  },
134
134
  {
135
135
  type: 'code',
136
- lang: 'css',
137
- label: 'Tailwind v3 coexistence',
138
- code: `@layer reset, tw-preflight, astryx-base, astryx-theme;
136
+ lang: 'tsx',
137
+ label: 'Root layout or app entry',
138
+ code: `import './globals.css'; // first: declares the cascade-layer order
139
139
 
140
- @import "@astryxdesign/core/reset.css";
140
+ import {AppRoot} from './AppRoot';`,
141
+ },
142
+ {
143
+ type: 'prose',
144
+ text: 'On Tailwind v3 there is no preflight.css to import, so wrap the @tailwind base directive in a named layer instead. Keep utilities unlayered so existing app utility classes still win everywhere. Put the `@layer` order line in its own `layers.css` and import it first, before `globals.css`: Next.js (webpack or Turbopack) and other bundlers that hoist CSS imports move the imported files above an inline `@layer` line, so the order would arrive too late.',
145
+ },
146
+ {
147
+ type: 'code',
148
+ lang: 'css',
149
+ label: 'layers.css (imported first)',
150
+ code: `@layer reset, tw-preflight, astryx-base, astryx-theme;`,
151
+ },
152
+ {
153
+ type: 'code',
154
+ lang: 'css',
155
+ label: 'globals.css (Tailwind v3 coexistence)',
156
+ code: `@import "@astryxdesign/core/reset.css";
141
157
  @import "@astryxdesign/core/astryx.css";
142
158
  @import "@astryxdesign/theme-neutral/theme.css";
143
159
 
@@ -147,6 +163,15 @@ export function AppRoot({children}: {children: React.ReactNode}) {
147
163
  @tailwind components;
148
164
  @tailwind utilities; /* unlayered: legacy utility classes keep winning */`,
149
165
  },
166
+ {
167
+ type: 'code',
168
+ lang: 'tsx',
169
+ label: 'Root layout or app entry (Tailwind v3)',
170
+ code: `import './layers.css'; // first: declares the cascade-layer order
171
+ import './globals.css';
172
+
173
+ import {AppRoot} from './AppRoot';`,
174
+ },
150
175
  ],
151
176
  },
152
177
  {
@@ -186,7 +211,7 @@ export function AppRoot({children}: {children: React.ReactNode}) {
186
211
  type: 'list',
187
212
  style: 'unordered',
188
213
  items: [
189
- 'Declare the canonical @layer order once, before any @import. With webpack-based bundlers (including Next.js) the order declaration must live in its own CSS file imported first, such as layers.css, because webpack hoists @import content above the inline CSS that follows it.',
214
+ 'Declare the canonical @layer order once, before any @import. With Next.js (webpack or Turbopack) and other bundlers that hoist CSS imports, the order declaration must live in its own CSS file imported first, such as layers.css, because the bundler hoists @import content above the inline CSS that follows it.',
190
215
  'Audit every pre-existing global or reset stylesheet and assign each one to a layer deliberately. Top-level imports without layer() stay unlayered and beat every layer; imports nested inside a layered file inherit that layer.',
191
216
  'Remove or demote the app legacy reset. The design system ships its own :where() reset in the lowest layer, so any app reset belongs in that same reset layer and never in a layer above astryx-base.',
192
217
  'Layer Tailwind preflight. On Tailwind v4, import preflight.css with layer(base). On Tailwind v3, wrap the @tailwind base directive in a named layer (see the snippet in Theme and CSS Setup). Unlayered preflight overrides theme CSS silently.',
@@ -21,6 +21,9 @@ export const docs = {
21
21
  'Dialog',
22
22
  'BottomSheet',
23
23
  'contract',
24
+ 'Grid',
25
+ 'columns',
26
+ 'minWidth',
24
27
  ],
25
28
 
26
29
  sections: [
@@ -94,5 +97,39 @@ const isNarrow = useMediaQuery('(max-width: 1024px)');
94
97
  },
95
98
  ],
96
99
  },
100
+ {
101
+ id: 'columns-that-reflow',
102
+ title: 'Columns that reflow',
103
+ content: [
104
+ {
105
+ type: 'prose',
106
+ text: 'For columns that reflow on small screens, use `Grid columns={{minWidth, max}}`. A numeric `columns` is exactly N columns at every width and never reflows: `columns={4}` on a 390px phone renders four tracks under 100px wide, and the row overflows the page as soon as their content cannot shrink to fit. `{minWidth: 240, max: 4}` renders at most four columns, each at least 240px, and drops to one full-width column when the container is narrow; `max` is what lets that last column shrink to the container, and `gap` counts toward the room the columns need.',
107
+ },
108
+ {
109
+ type: 'code',
110
+ lang: 'tsx',
111
+ label: 'Reflowing stat row versus a fixed week row',
112
+ code: `// Reflows: 4 columns on desktop, 3 in a 768px-wide grid, 1 on a phone.
113
+ <Grid columns={{minWidth: 240, max: 4}} gap={3}>
114
+ {stats.map(stat => <StatCard key={stat.id} {...stat} />)}
115
+ </Grid>
116
+
117
+ // Fixed: exactly 7 columns at every width (a number never reflows).
118
+ <Grid columns={7} gap={1}>
119
+ {days.map(day => <DayCell key={day} day={day} />)}
120
+ </Grid>`,
121
+ },
122
+ {
123
+ type: 'list',
124
+ style: 'unordered',
125
+ items: [
126
+ '`{minWidth}` alone: as many columns as fit at the floor, no cap; a container narrower than `minWidth` overflows, so add `max` for anything that reaches a phone',
127
+ "`{minWidth, repeat: 'fit'}`: empty tracks collapse so present items stretch",
128
+ "A numeric `GridSpan` wider than the grid's current columns adds implicit columns to the whole grid; items placed before and after the span flow into them, and `columns='full'` then covers only the explicit columns. In a reflowing grid keep spans within the smallest count it can reach, or use `'full'`, which adds none",
129
+ 'Styling that changes the room columns have to fit changes the count a reflowing shape reaches: a `columnGap` or `gap` step, inline padding on the grid, or `grid-auto-flow: column` through `xstyle`, which places items into extra implicit columns. A `className` rule replaces the computed track list only when it wins the cascade against the generated styles; prefer `xstyle` for overrides',
130
+ ],
131
+ },
132
+ ],
133
+ },
97
134
  ],
98
135
  };
@@ -47,6 +47,18 @@ export const docs = {
47
47
  type: 'prose',
48
48
  text: 'Pre-declare every layer before any imports. This keeps reset lowest, Tailwind preflight above reset, component/theme styles in the middle, and Tailwind utilities last so utility classes on `className` can intentionally override component defaults.',
49
49
  },
50
+ {
51
+ type: 'prose',
52
+ text: 'A layer\'s rank is set by the first stylesheet on the page that names it, so this order only holds when `globals.css` loads first. Make it the first import in the root layout or app entry, before any module that imports Astryx or theme CSS. That includes the `astryx-themes` module generated by `astryx theme add` and any root component that imports a theme\'s `theme.css`. If a theme stylesheet loads first, Tailwind preflight overrides Astryx base styles and theme overrides lose. If `globals.css` cannot come first, move the `@layer` line into its own `layers.css` and import that first instead.',
53
+ },
54
+ {
55
+ type: 'code',
56
+ lang: 'tsx',
57
+ label: 'Root layout or app entry',
58
+ code: `import './globals.css'; // first: declares the cascade-layer order
59
+
60
+ import {themes, defaultThemeSlug} from './astryx-themes';`,
61
+ },
50
62
  {
51
63
  type: 'code',
52
64
  lang: 'tsx',
@@ -77,7 +77,7 @@ const styles = stylex.create({
77
77
  content: [
78
78
  {
79
79
  type: 'prose',
80
- text: 'Astryx components ship pre-compiled, so consuming the published package needs no StyleX setup. But `astryx swizzle <Component>` copies the raw StyleX *source* into your app, and StyleX source requires a build-time StyleX compiler to produce atomic CSS. Without one the component compiles but renders completely unstyled: no error, no warning. If a swizzled component looks unstyled, a missing StyleX compiler is almost always why. The same applies if you author your own StyleX with `stylex.create()`.',
80
+ text: 'Astryx components ship pre-compiled: `astryx.css` and each package\'s own stylesheet hold all of their styles, so consuming the published packages needs no StyleX setup. But `astryx swizzle <Component>` copies the raw StyleX *source* into your app, and StyleX source requires a build-time StyleX compiler to produce atomic CSS. Without one the component compiles but renders completely unstyled: no error, no warning. If a swizzled component looks unstyled, a missing StyleX compiler is almost always why. The same applies if you author your own StyleX with `stylex.create()`.',
81
81
  },
82
82
  {
83
83
  type: 'table',
@@ -105,7 +105,7 @@ const styles = stylex.create({
105
105
 
106
106
  export default stylexPlugin({
107
107
  rsOptions: {
108
- // Resolve @astryxdesign/core's StyleX so swizzled component source compiles.
108
+ // Resolve the Astryx token modules that swizzled source imports.
109
109
  aliases: {'@/*': ['./src/*']},
110
110
  unstable_moduleResolution: {type: 'commonJS'},
111
111
  },
@@ -120,6 +120,7 @@ export default stylexPlugin({
120
120
  'Symptom of a missing compiler: swizzled component renders with no styles, but no build or runtime error.',
121
121
  'A Babel config turns off SWC in Next.js; skip it if you need `next/font`.',
122
122
  'Pure theming (defineTheme + astryx theme build) needs NO StyleX compiler; only swizzled/authored StyleX source does.',
123
+ 'Keep loading `astryx.css` with a StyleX compiler in place. The compiler builds your own and swizzled source only; never add `node_modules/@astryxdesign` to its inputs.',
123
124
  ],
124
125
  },
125
126
  ],
@@ -88,6 +88,26 @@ export const docs = {
88
88
  [
89
89
  "--color-syntax-background",
90
90
  "var(--color-background-muted)"
91
+ ],
92
+ [
93
+ "--color-syntax-added",
94
+ "color-mix(in srgb, var(--color-success-muted) 25%, transparent)"
95
+ ],
96
+ [
97
+ "--color-syntax-removed",
98
+ "color-mix(in srgb, var(--color-error-muted) 25%, transparent)"
99
+ ],
100
+ [
101
+ "--color-syntax-diff-header",
102
+ "var(--color-background-muted)"
103
+ ],
104
+ [
105
+ "--color-syntax-added-marker",
106
+ "var(--color-text-green)"
107
+ ],
108
+ [
109
+ "--color-syntax-removed-marker",
110
+ "var(--color-text-red)"
91
111
  ]
92
112
  ]
93
113
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  // AUTO-GENERATED — do not edit manually.
4
4
  // Run: node scripts/generate-token-docs.mjs
5
- // Total: 258 tokens across 15 categories.
5
+ // Total: 263 tokens across 15 categories.
6
6
 
7
7
  /** @type {import('@astryxdesign/cli/authoring').NamespaceDoc} */
8
8
  export const docs = {
@@ -39,7 +39,12 @@ export default function CommandPaletteAsyncSearch() {
39
39
  isInline
40
40
  onOpenChange={() => {}}
41
41
  searchSource={source}
42
- input={<CommandPaletteInput placeholder="Search files..." />}
42
+ input={
43
+ <CommandPaletteInput
44
+ label="Search files"
45
+ placeholder="Search files..."
46
+ />
47
+ }
43
48
  emptyBootstrapText="Type a filename to search"
44
49
  emptySearchText="No files found"
45
50
  />
@@ -4099,7 +4099,7 @@ export default function TableFilterTemplate() {
4099
4099
  restates the selected tab word for word, and the strip already
4100
4100
  marks which one that is. On a phone that is a line of vertical
4101
4101
  space spent saying it twice. The panel keeps the name for anyone
4102
- not seeing the strip, as its accessible label. */}
4102
+ not seeing the strip: its selected tab labels it. */}
4103
4103
  <TabList
4104
4104
  value={viewSection}
4105
4105
  onChange={value => setViewSection(value as ViewSection)}
@@ -4109,6 +4109,7 @@ export default function TableFilterTemplate() {
4109
4109
  {VIEW_SECTIONS.map(section => (
4110
4110
  <Tab
4111
4111
  key={section.key}
4112
+ id={`${viewPanelId}-tab-${section.key}`}
4112
4113
  value={section.key}
4113
4114
  label={section.label}
4114
4115
  icon={section.icon}
@@ -4126,7 +4127,7 @@ export default function TableFilterTemplate() {
4126
4127
  gap={0}
4127
4128
  id={viewPanelId}
4128
4129
  role="tabpanel"
4129
- aria-label={activeSection?.title}
4130
+ aria-labelledby={`${viewPanelId}-tab-${viewSection}`}
4130
4131
  paddingBlockStart={3}>
4131
4132
  {viewPanelBody()}
4132
4133
  </VStack>
@@ -43,6 +43,55 @@ const butterSyntax = defineSyntaxTheme({
43
43
  },
44
44
  });
45
45
 
46
+ /**
47
+ * The secondary Button treatment. Selector, MultiSelector, and ComplexSelector
48
+ * reuse it for their `secondary` trigger so every secondary control in this
49
+ * theme matches (AST-079 FR2).
50
+ *
51
+ * Secondary: blue outline + label in light, butter yellow in dark.
52
+ */
53
+ const SECONDARY_BUTTON = {
54
+ backgroundColor: 'transparent',
55
+ borderWidth: '1.5px',
56
+ borderStyle: 'solid',
57
+ borderColor: 'light-dark(#225BFF, #FDEE8C)',
58
+ color: 'light-dark(#225BFF, #FDEE8C)',
59
+ ':hover': {
60
+ backgroundColor: 'light-dark(#225BFF14, #FDEE8C14)',
61
+ },
62
+ } as const;
63
+
64
+ /**
65
+ * The secondary Button treatment on Selector and MultiSelector triggers
66
+ * (AST-079 FR2). Inside an InputGroup a trigger keeps the neutral fill and the
67
+ * group's edges instead (FR6). The hover stays off disabled and read-only
68
+ * triggers, which carry `data-disabled` and `data-readonly`.
69
+ */
70
+ const SECONDARY_SELECTOR = {
71
+ ':not(.astryx-input-group *)': {
72
+ backgroundColor: SECONDARY_BUTTON.backgroundColor,
73
+ borderWidth: SECONDARY_BUTTON.borderWidth,
74
+ borderStyle: SECONDARY_BUTTON.borderStyle,
75
+ borderColor: SECONDARY_BUTTON.borderColor,
76
+ color: SECONDARY_BUTTON.color,
77
+ },
78
+ ':hover:not([data-disabled], [data-readonly], .astryx-input-group *)':
79
+ SECONDARY_BUTTON[':hover'],
80
+ } as const;
81
+
82
+ /**
83
+ * The secondary Button treatment on ComplexSelector triggers (AST-079 FR2), with
84
+ * the hover kept off a disabled trigger, whose inner button is disabled.
85
+ */
86
+ const SECONDARY_COMPLEX_SELECTOR = {
87
+ backgroundColor: SECONDARY_BUTTON.backgroundColor,
88
+ borderWidth: SECONDARY_BUTTON.borderWidth,
89
+ borderStyle: SECONDARY_BUTTON.borderStyle,
90
+ borderColor: SECONDARY_BUTTON.borderColor,
91
+ color: SECONDARY_BUTTON.color,
92
+ ':hover:not(:has(> :disabled))': SECONDARY_BUTTON[':hover'],
93
+ } as const;
94
+
46
95
  export const butterTheme = defineTheme({
47
96
  name: 'butter',
48
97
 
@@ -276,17 +325,7 @@ export const butterTheme = defineTheme({
276
325
  paddingBlock: 'var(--spacing-3)',
277
326
  paddingInline: 'var(--spacing-4)',
278
327
  },
279
- // Secondary: blue outline + label in light, butter yellow in dark.
280
- 'variant:secondary': {
281
- backgroundColor: 'transparent',
282
- borderWidth: '1.5px',
283
- borderStyle: 'solid',
284
- borderColor: 'light-dark(#225BFF, #FDEE8C)',
285
- color: 'light-dark(#225BFF, #FDEE8C)',
286
- ':hover': {
287
- backgroundColor: 'light-dark(#225BFF14, #FDEE8C14)',
288
- },
289
- },
328
+ 'variant:secondary': SECONDARY_BUTTON,
290
329
  // Ghost: same accent color as secondary, no background.
291
330
  'variant:ghost': {
292
331
  color: 'light-dark(#225BFF, #FDEE8C)',
@@ -534,6 +573,7 @@ export const butterTheme = defineTheme({
534
573
  'status:success': {'--color-success': '#91D143'},
535
574
  'status:warning': {'--color-warning': '#ffc502'},
536
575
  'status:error': {'--color-error': '#fc473b'},
576
+ 'variant:secondary': SECONDARY_SELECTOR,
537
577
  },
538
578
  'multi-selector': {
539
579
  base: {
@@ -544,7 +584,9 @@ export const butterTheme = defineTheme({
544
584
  'status:success': {'--color-success': '#91D143'},
545
585
  'status:warning': {'--color-warning': '#ffc502'},
546
586
  'status:error': {'--color-error': '#fc473b'},
587
+ 'variant:secondary': SECONDARY_SELECTOR,
547
588
  },
589
+ 'complex-selector': {'variant:secondary': SECONDARY_COMPLEX_SELECTOR},
548
590
  typeahead: {
549
591
  base: {
550
592
  paddingBlock: 'var(--spacing-2)',
@@ -34,6 +34,30 @@ const chocolateSyntax = defineSyntaxTheme({
34
34
  },
35
35
  });
36
36
 
37
+ /**
38
+ * The secondary Button treatment. Selector, MultiSelector, and ComplexSelector
39
+ * reuse it for their `secondary` trigger so every secondary control in this
40
+ * theme matches (AST-079 FR2).
41
+ */
42
+ const SECONDARY_BUTTON = {
43
+ borderWidth: '1px',
44
+ borderStyle: 'solid',
45
+ borderColor: 'var(--color-border-emphasized)',
46
+ } as const;
47
+
48
+ /**
49
+ * The secondary Button treatment on Selector and MultiSelector triggers
50
+ * (AST-079 FR2). Inside an InputGroup a trigger keeps the neutral fill and the
51
+ * group's edges instead (FR6).
52
+ */
53
+ const SECONDARY_SELECTOR = {
54
+ ':not(.astryx-input-group *)': {
55
+ borderWidth: SECONDARY_BUTTON.borderWidth,
56
+ borderStyle: SECONDARY_BUTTON.borderStyle,
57
+ borderColor: SECONDARY_BUTTON.borderColor,
58
+ },
59
+ } as const;
60
+
37
61
  export const chocolateTheme = defineTheme({
38
62
  name: 'chocolate',
39
63
 
@@ -219,12 +243,12 @@ export const chocolateTheme = defineTheme({
219
243
  base: {
220
244
  borderRadius: 'var(--radius-full)',
221
245
  },
222
- 'variant:secondary': {
223
- borderWidth: '1px',
224
- borderStyle: 'solid',
225
- borderColor: 'var(--color-border-emphasized)',
226
- },
246
+ 'variant:secondary': SECONDARY_BUTTON,
227
247
  },
248
+ // Secondary selector triggers match the secondary Button (AST-079 FR2).
249
+ selector: {'variant:secondary': SECONDARY_SELECTOR},
250
+ 'multi-selector': {'variant:secondary': SECONDARY_SELECTOR},
251
+ 'complex-selector': {'variant:secondary': SECONDARY_BUTTON},
228
252
 
229
253
  card: {
230
254
  base: {
@@ -40,9 +40,48 @@ const gothicSyntax = defineSyntaxTheme({
40
40
  property: '#7cc5b3', // Verdigris
41
41
  punctuation: '#7a8290', // Mid neutral
42
42
  background: '#101314',
43
+ // Diff rows on the near-black background: any lighter wash lowers every
44
+ // light token's contrast, so the rows take opaque hues at the
45
+ // background's own luminance — a deep green and a deep red that read as
46
+ // colour, not brightness — and every token keeps the contrast it has on
47
+ // the plain background. Markers: forest moss and blood crimson.
48
+ added: '#081408',
49
+ removed: '#1e0a0a',
50
+ 'added-marker': '#a3c987',
51
+ 'removed-marker': '#d97580',
43
52
  },
44
53
  });
45
54
 
55
+ /**
56
+ * The secondary Button treatment. Selector, MultiSelector, and ComplexSelector
57
+ * reuse it for their `secondary` trigger so every secondary control in this
58
+ * theme matches (AST-079 FR2).
59
+ *
60
+ * Primary inherits default — light pill with dark text via
61
+ * --color-accent / --color-on-accent (matches the cream badge).
62
+ * Secondary uses the dark-slate "neutral" badge treatment.
63
+ */
64
+ const SECONDARY_BUTTON = {
65
+ backgroundColor: 'var(--color-background-gray)',
66
+ color: 'var(--color-text-gray)',
67
+ borderColor: 'transparent',
68
+ borderWidth: '0',
69
+ } as const;
70
+
71
+ /**
72
+ * The secondary Button treatment on Selector and MultiSelector triggers
73
+ * (AST-079 FR2). Inside an InputGroup a trigger keeps the neutral fill and the
74
+ * group's edges instead (FR6).
75
+ */
76
+ const SECONDARY_SELECTOR = {
77
+ ':not(.astryx-input-group *)': {
78
+ backgroundColor: SECONDARY_BUTTON.backgroundColor,
79
+ color: SECONDARY_BUTTON.color,
80
+ borderColor: SECONDARY_BUTTON.borderColor,
81
+ borderWidth: SECONDARY_BUTTON.borderWidth,
82
+ },
83
+ } as const;
84
+
46
85
  export const gothicTheme = defineTheme({
47
86
  name: 'gothic',
48
87
 
@@ -227,15 +266,7 @@ export const gothicTheme = defineTheme({
227
266
 
228
267
  components: {
229
268
  button: {
230
- // Primary inherits default — light pill with dark text via
231
- // --color-accent / --color-on-accent (matches the cream badge).
232
- // Secondary uses the dark-slate "neutral" badge treatment.
233
- 'variant:secondary': {
234
- backgroundColor: 'var(--color-background-gray)',
235
- color: 'var(--color-text-gray)',
236
- borderColor: 'transparent',
237
- borderWidth: '0',
238
- },
269
+ 'variant:secondary': SECONDARY_BUTTON,
239
270
  'variant:ghost': {
240
271
  ':hover': {
241
272
  backgroundColor: 'var(--color-overlay-hover)',
@@ -248,6 +279,10 @@ export const gothicTheme = defineTheme({
248
279
  color: 'var(--color-text-red)',
249
280
  },
250
281
  },
282
+ // Secondary selector triggers match the secondary Button (AST-079 FR2).
283
+ selector: {'variant:secondary': SECONDARY_SELECTOR},
284
+ 'multi-selector': {'variant:secondary': SECONDARY_SELECTOR},
285
+ 'complex-selector': {'variant:secondary': SECONDARY_BUTTON},
251
286
 
252
287
  badge: {
253
288
  base: {
@@ -56,9 +56,60 @@ const stoneSyntax = defineSyntaxTheme({
56
56
  property: ['#4e6357', '#94b2a0'], // Teal T40 / T70
57
57
  punctuation: ['#5e5e5e', '#ababb0'], // Stone Neutral T40 / T70
58
58
  background: ['#f3f3f5', '#171719'],
59
+ // Diff rows: the muted green/red at 25% in light and 20% in dark keep
60
+ // every token at or above 4.5:1 on the tint; the markers are the tinted
61
+ // text tokens (8.1:1 light, 8.8:1 dark on their tints).
62
+ added: ['#d0e9ce40', '#b4cdb233'],
63
+ removed: ['#f9dcd740', '#dcc0bc33'],
64
+ 'added-marker': ['#374c36', '#d0e9ce'],
65
+ 'removed-marker': ['#58413e', '#f9dcd7'],
59
66
  },
60
67
  });
61
68
 
69
+ /**
70
+ * The secondary Button treatment. Selector, MultiSelector, and ComplexSelector
71
+ * reuse it for their `secondary` trigger so every secondary control in this
72
+ * theme matches (AST-079 FR2).
73
+ */
74
+ const SECONDARY_BUTTON = {
75
+ backgroundColor: 'transparent',
76
+ borderWidth: '1.5px',
77
+ borderStyle: 'solid',
78
+ borderColor: 'var(--color-border-emphasized)',
79
+ ':hover': {
80
+ backgroundColor: 'var(--color-neutral)',
81
+ },
82
+ } as const;
83
+
84
+ /**
85
+ * The secondary Button treatment on Selector and MultiSelector triggers
86
+ * (AST-079 FR2). Inside an InputGroup a trigger keeps the neutral fill and the
87
+ * group's edges instead (FR6). The hover stays off disabled and read-only
88
+ * triggers, which carry `data-disabled` and `data-readonly`.
89
+ */
90
+ const SECONDARY_SELECTOR = {
91
+ ':not(.astryx-input-group *)': {
92
+ backgroundColor: SECONDARY_BUTTON.backgroundColor,
93
+ borderWidth: SECONDARY_BUTTON.borderWidth,
94
+ borderStyle: SECONDARY_BUTTON.borderStyle,
95
+ borderColor: SECONDARY_BUTTON.borderColor,
96
+ },
97
+ ':hover:not([data-disabled], [data-readonly], .astryx-input-group *)':
98
+ SECONDARY_BUTTON[':hover'],
99
+ } as const;
100
+
101
+ /**
102
+ * The secondary Button treatment on ComplexSelector triggers (AST-079 FR2), with
103
+ * the hover kept off a disabled trigger, whose inner button is disabled.
104
+ */
105
+ const SECONDARY_COMPLEX_SELECTOR = {
106
+ backgroundColor: SECONDARY_BUTTON.backgroundColor,
107
+ borderWidth: SECONDARY_BUTTON.borderWidth,
108
+ borderStyle: SECONDARY_BUTTON.borderStyle,
109
+ borderColor: SECONDARY_BUTTON.borderColor,
110
+ ':hover:not(:has(> :disabled))': SECONDARY_BUTTON[':hover'],
111
+ } as const;
112
+
62
113
  export const stoneTheme = defineTheme({
63
114
  name: 'stone',
64
115
 
@@ -262,15 +313,7 @@ export const stoneTheme = defineTheme({
262
313
  base: {
263
314
  borderRadius: 'var(--radius-full)',
264
315
  },
265
- 'variant:secondary': {
266
- backgroundColor: 'transparent',
267
- borderWidth: '1.5px',
268
- borderStyle: 'solid',
269
- borderColor: 'var(--color-border-emphasized)',
270
- ':hover': {
271
- backgroundColor: 'var(--color-neutral)',
272
- },
273
- },
316
+ 'variant:secondary': SECONDARY_BUTTON,
274
317
  'variant:destructive': {
275
318
  backgroundColor: 'var(--color-background-red)',
276
319
  color: 'var(--color-text-red)',
@@ -389,8 +432,12 @@ export const stoneTheme = defineTheme({
389
432
  'number-input': INPUT_STATUS_VARS,
390
433
  'date-input': INPUT_STATUS_VARS,
391
434
  'time-input': INPUT_STATUS_VARS,
392
- selector: INPUT_STATUS_VARS,
393
- 'multi-selector': INPUT_STATUS_VARS,
435
+ selector: {...INPUT_STATUS_VARS, 'variant:secondary': SECONDARY_SELECTOR},
436
+ 'multi-selector': {
437
+ ...INPUT_STATUS_VARS,
438
+ 'variant:secondary': SECONDARY_SELECTOR,
439
+ },
440
+ 'complex-selector': {'variant:secondary': SECONDARY_COMPLEX_SELECTOR},
394
441
  typeahead: INPUT_STATUS_VARS,
395
442
  tokenizer: INPUT_STATUS_VARS,
396
443
 
@@ -29,9 +29,64 @@ const y2kSyntax = defineSyntaxTheme({
29
29
  property: ['#3c6755', '#87b5a1'],
30
30
  punctuation: ['#5e5e5e', '#ababab'],
31
31
  background: ['#FFF6ED', '#190f00'],
32
+ // Diff rows: the muted green/red at 25% in light and 20% in dark keep
33
+ // every token at or above 4.5:1 on the tint. Markers: the tinted text
34
+ // tokens in light (7.3:1 / 7.9:1); in dark those tokens are near-black,
35
+ // so the string and tag colours mark the rows (5.1:1 each).
36
+ added: ['#C5E17A40', '#C5E17A33'],
37
+ removed: ['#FFC5C340', '#FFC5C333'],
38
+ 'added-marker': ['#3a5500', '#a5af8b'],
39
+ 'removed-marker': ['#8b1d24', '#d19f9d'],
32
40
  },
33
41
  });
34
42
 
43
+ /**
44
+ * The secondary Button treatment. Selector, MultiSelector, and ComplexSelector
45
+ * reuse it for their `secondary` trigger so every secondary control in this
46
+ * theme matches (AST-079 FR2).
47
+ */
48
+ const SECONDARY_BUTTON = {
49
+ backgroundColor: 'var(--color-background-green)',
50
+ borderWidth: '1px',
51
+ borderStyle: 'solid',
52
+ borderColor: 'var(--color-text-green)',
53
+ color: 'var(--color-text-green)',
54
+ ':hover': {
55
+ backgroundColor: 'var(--color-border-green)',
56
+ },
57
+ } as const;
58
+
59
+ /**
60
+ * The secondary Button treatment on Selector and MultiSelector triggers
61
+ * (AST-079 FR2). Inside an InputGroup a trigger keeps the neutral fill and the
62
+ * group's edges instead (FR6). The hover stays off disabled and read-only
63
+ * triggers, which carry `data-disabled` and `data-readonly`.
64
+ */
65
+ const SECONDARY_SELECTOR = {
66
+ ':not(.astryx-input-group *)': {
67
+ backgroundColor: SECONDARY_BUTTON.backgroundColor,
68
+ borderWidth: SECONDARY_BUTTON.borderWidth,
69
+ borderStyle: SECONDARY_BUTTON.borderStyle,
70
+ borderColor: SECONDARY_BUTTON.borderColor,
71
+ color: SECONDARY_BUTTON.color,
72
+ },
73
+ ':hover:not([data-disabled], [data-readonly], .astryx-input-group *)':
74
+ SECONDARY_BUTTON[':hover'],
75
+ } as const;
76
+
77
+ /**
78
+ * The secondary Button treatment on ComplexSelector triggers (AST-079 FR2), with
79
+ * the hover kept off a disabled trigger, whose inner button is disabled.
80
+ */
81
+ const SECONDARY_COMPLEX_SELECTOR = {
82
+ backgroundColor: SECONDARY_BUTTON.backgroundColor,
83
+ borderWidth: SECONDARY_BUTTON.borderWidth,
84
+ borderStyle: SECONDARY_BUTTON.borderStyle,
85
+ borderColor: SECONDARY_BUTTON.borderColor,
86
+ color: SECONDARY_BUTTON.color,
87
+ ':hover:not(:has(> :disabled))': SECONDARY_BUTTON[':hover'],
88
+ } as const;
89
+
35
90
  export const y2kTheme = defineTheme({
36
91
  name: 'y2k',
37
92
 
@@ -256,16 +311,7 @@ export const y2kTheme = defineTheme({
256
311
  color: 'var(--color-background-body)',
257
312
  borderColor: 'transparent',
258
313
  },
259
- 'variant:secondary': {
260
- backgroundColor: 'var(--color-background-green)',
261
- borderWidth: '1px',
262
- borderStyle: 'solid',
263
- borderColor: 'var(--color-text-green)',
264
- color: 'var(--color-text-green)',
265
- ':hover': {
266
- backgroundColor: 'var(--color-border-green)',
267
- },
268
- },
314
+ 'variant:secondary': SECONDARY_BUTTON,
269
315
  'variant:ghost': {
270
316
  borderColor: 'transparent',
271
317
  },
@@ -277,6 +323,10 @@ export const y2kTheme = defineTheme({
277
323
  borderColor: 'var(--color-text-red)',
278
324
  },
279
325
  },
326
+ // Secondary selector triggers match the secondary Button (AST-079 FR2).
327
+ selector: {'variant:secondary': SECONDARY_SELECTOR},
328
+ 'multi-selector': {'variant:secondary': SECONDARY_SELECTOR},
329
+ 'complex-selector': {'variant:secondary': SECONDARY_COMPLEX_SELECTOR},
280
330
 
281
331
  badge: {
282
332
  base: {