@astryxdesign/cli 0.7.12-canary.ccb4a6d → 0.7.12-canary.f543ec7

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.
@@ -247,9 +247,9 @@ useAutoMediaMode useAutoMediaMode hooks/useAnimatedLayout.doc.mjs hooks/useAutoM
247
247
  useBackdropPress useBackdropPress Layer/Layer.doc.mjs Layer/useBackdropPress.ts
248
248
  useBaseTablePlugins useBaseTablePlugins Table/Table.doc.mjs Table/useBaseTablePlugins.ts
249
249
  useBusyAnnouncement useBusyAnnouncement hooks/useAnimatedLayout.doc.mjs hooks/useBusyAnnouncement.ts
250
- useCalendarConstraints useCalendarConstraints Calendar/Calendar.doc.mjs Calendar/hooks/useCalendarConstraints.ts
250
+ useCalendarConstraints useCalendarConstraints Calendar/useCalendarConstraints.doc.mjs Calendar/hooks/useCalendarConstraints.ts
251
251
  useCalendarDays useCalendarDays Calendar/useCalendarDays.doc.mjs Calendar/hooks/useCalendarDays.ts
252
- useCalendarNavigation useCalendarNavigation Calendar/Calendar.doc.mjs Calendar/hooks/useCalendarNavigation.ts
252
+ useCalendarNavigation useCalendarNavigation Calendar/useCalendarNavigation.doc.mjs Calendar/hooks/useCalendarNavigation.ts
253
253
  useChatComposerTokens useChatComposerTokens Chat/Chat.doc.mjs Chat/useChatComposerTokens.ts
254
254
  useChatDictation useChatDictation Chat/Chat.doc.mjs Chat/useChatDictation.ts
255
255
  useChatNewMessages useChatNewMessages Chat/Chat.doc.mjs Chat/useChatNewMessages.ts
@@ -473,16 +473,51 @@ export async function swizzleCopy(component, options = {}) {
473
473
  );
474
474
  }
475
475
 
476
+ // The marker tells @astryxdesign/build that the output folder holds Astryx
477
+ // source, so the copy compiles below the theme as the package does.
478
+ const markerDest = confineWrite(path.join(outputBase, SWIZZLE_MARKER), cwd, {
479
+ allowAbsolute: true,
480
+ label: 'output file',
481
+ });
482
+ const markerExisting = readExisting(markerDest);
483
+ const markerContent = swizzleMarkerContent(
484
+ markerExisting,
485
+ owner.ownerPackage,
486
+ ownerVersion,
487
+ );
488
+ // Every module under the marker compiles as Astryx source, so swizzle marks
489
+ // only a folder it owns: one the copy creates, an empty one, or one it
490
+ // marked before. Any other folder is reported with what it holds.
491
+ const foundInOutputBase =
492
+ markerExisting == null &&
493
+ fs.existsSync(outputBase) &&
494
+ fs.statSync(outputBase).isDirectory()
495
+ ? fs.readdirSync(outputBase).sort()
496
+ : [];
497
+ const marksOutputBase = foundInOutputBase.length === 0;
498
+ const writes = marksOutputBase
499
+ ? [
500
+ ...outputs,
501
+ {
502
+ dest: markerDest,
503
+ content: markerContent,
504
+ marker: true,
505
+ unchanged: markerExisting === markerContent,
506
+ },
507
+ ]
508
+ : outputs;
509
+
476
510
  const outputDirExisted = fs.existsSync(outputDir);
477
511
  /** @type {string[]} folders this run created, outermost first */
478
512
  const createdDirs = [];
479
513
  /** @type {Array<{dest: string, original: Buffer|null}>} */
480
514
  const written = [];
481
515
  let copied = 0;
482
- for (const output of outputs) {
516
+ for (const output of writes) {
517
+ const counts = !('marker' in output);
483
518
  // An identical file is already in place: it is part of this copy, unwritten.
484
519
  if (output.unchanged) {
485
- copied++;
520
+ if (counts) copied++;
486
521
  continue;
487
522
  }
488
523
  try {
@@ -491,7 +526,7 @@ export async function swizzleCopy(component, options = {}) {
491
526
  if (firstCreated != null) createdDirs.push(firstCreated);
492
527
  written.push({dest: output.dest, original});
493
528
  fs.writeFileSync(output.dest, output.content);
494
- copied++;
529
+ if (counts) copied++;
495
530
  } catch (err) {
496
531
  const unrestored = undoCopy(written);
497
532
  for (const dir of [...createdDirs].reverse()) {
@@ -516,10 +551,44 @@ export async function swizzleCopy(component, options = {}) {
516
551
  usesStyleX: outputs.some(o => o.content.includes('@stylexjs/stylex')),
517
552
  dependencies: plan.dependencies,
518
553
  };
554
+ data.marker = marksOutputBase
555
+ ? {path: toPosix(path.relative(cwd, markerDest)), written: true}
556
+ : {
557
+ path: toPosix(path.relative(cwd, markerDest)),
558
+ written: false,
559
+ found: foundInOutputBase.slice(0, 5),
560
+ };
519
561
  if (feedback) data.feedback = feedback;
520
562
  return {type: 'swizzle.copy', package: data.package, data};
521
563
  }
522
564
 
565
+ /** The file that marks a swizzle output folder as Astryx source. */
566
+ const SWIZZLE_MARKER = 'astryx-swizzle.json';
567
+
568
+ /**
569
+ * The marker's content after a copy from `ownerPackage`: the packages the
570
+ * folder's copies came from, each with its version. Unreadable content is
571
+ * replaced.
572
+ * @param {string|null} existing
573
+ * @param {string} ownerPackage
574
+ * @param {string} version
575
+ * @returns {string}
576
+ */
577
+ function swizzleMarkerContent(existing, ownerPackage, version) {
578
+ /** @type {Record<string, string>} */
579
+ let packages = {};
580
+ try {
581
+ const parsed = existing == null ? null : JSON.parse(existing);
582
+ if (parsed?.packages && typeof parsed.packages === 'object') {
583
+ packages = {...parsed.packages};
584
+ }
585
+ } catch {
586
+ // Unreadable content is rewritten below.
587
+ }
588
+ packages[ownerPackage] = version;
589
+ return `${JSON.stringify({packages}, null, 2)}\n`;
590
+ }
591
+
523
592
  /**
524
593
  * Undo the writes of a copy that failed part-way, newest first: delete the
525
594
  * files the copy created and put back the bytes of the files it replaced. A
@@ -21,8 +21,12 @@ export const doc = {
21
21
  'the owner package exports: names the package exports are imported from its ' +
22
22
  'public paths, and every internal module the component needs is ' +
23
23
  "copied beside it in the package's folder layout. Tests, stories and docs are not " +
24
- 'copied. The receipt flags whether any copied file uses StyleX and lists each npm ' +
25
- 'package the copy imports with the range the owner package declares. With no name ' +
24
+ 'copied. An output folder swizzle owns (one the copy creates, an empty one, or one it ' +
25
+ 'marked before) gets an astryx-swizzle.json marker naming each package its copies came from, ' +
26
+ "with its version, so @astryxdesign/build's Babel, PostCSS and Vite integrations, and " +
27
+ 'withAstryx() under webpack, compile the copy below the theme. The receipt flags whether ' +
28
+ 'any copied file uses StyleX and lists each npm package the copy imports with the range ' +
29
+ 'the owner package declares. With no name ' +
26
30
  '(or list) it returns the swizzlable component names instead. An integration ' +
27
31
  'component that replaces a Core component is what that Core name copies; ' +
28
32
  "options.package '@astryxdesign/core' copies the original. The list names Core " +
@@ -81,7 +85,7 @@ export const doc = {
81
85
  {
82
86
  type: 'swizzle.copy',
83
87
  description:
84
- 'A receipt after copying the component into the project: the component name, owning package, output directory, files-copied count, the copied file names (relative to the component folder; the internal modules copied beside it start with `../`, e.g. `../InputGroup/groupStyles.ts`), whether any file uses StyleX, and, when the owner has an issues URL, feedback ({issuesUrl, ghCommand?}): where to report the gap that led to swizzling.',
88
+ 'A receipt after copying the component into the project: the component name, owning package, output directory, files-copied count, the copied file names (relative to the component folder; the internal modules copied beside it start with `../`, e.g. `../InputGroup/groupStyles.ts`), whether any file uses StyleX, the npm packages the copy imports with the ranges the owner declares (dependencies), the output folder\'s astryx-swizzle.json (marker: {path, written, found?}; written is false for a folder swizzle does not own, and found names up to five entries it already holds), and, when the owner has an issues URL, feedback ({issuesUrl, ghCommand?}): where to report the gap that led to swizzling.',
85
89
  },
86
90
  ],
87
91
  throws: [
@@ -42,6 +42,11 @@ export type SwizzleCopyResponse = {
42
42
  files: string[];
43
43
  usesStyleX: boolean;
44
44
  dependencies: Record<string, string>;
45
+ marker?: {
46
+ path: string;
47
+ written: boolean;
48
+ found?: string[];
49
+ } | undefined;
45
50
  feedback?: SwizzleFeedback | undefined;
46
51
  };
47
52
  };
@@ -37,6 +37,7 @@
37
37
  * @property {string[]} data.files Copied file names, relative to the component folder; the internal modules copied beside it start with `../` (spec:AST-110 FR2).
38
38
  * @property {boolean} data.usesStyleX Whether any copied file uses StyleX (requires build-time setup).
39
39
  * @property {Record<string, string>} data.dependencies Each npm package the copied files import, other than the owner package, with the version range the owner package declares (`*` when it declares none). The copy builds once the product installs them (spec:AST-110 FR1).
40
+ * @property {{path: string, written: boolean, found?: string[]}} [data.marker] The output folder's astryx-swizzle.json, relative to the project (spec:AST-110 FR5). Swizzle writes it only into a folder it owns: one the copy creates, an empty one, or one it marked before. When it does not, `written` is false and `found` names up to five entries already in the folder.
40
41
  * @property {SwizzleFeedback} [data.feedback]
41
42
  */
42
43
 
@@ -148,11 +148,11 @@ export const docs = {
148
148
  content: [
149
149
  {
150
150
  type: 'prose',
151
- text: "On a phone, a fully expanded `height='tall'` BottomSheet responds to the on-screen keyboard: it measures the keyboard as the layout viewport's height minus the visual viewport's height times its scale. Where the visual viewport shrinks for the keyboard, as in Safari on iOS in the browser and as a home-screen app, in Firefox for Android and in Chrome on Android, it keeps the focused field above the keyboard and leaves the page where it was. If Firefox for Android pans to the field, the sheet puts the page back within a couple of frames.",
151
+ text: "On a phone, the components that respond to the on-screen keyboard, a fully expanded `height='tall'` BottomSheet and ChatLayout's composer, measure it the same way: the layout viewport's height minus the visual viewport's height times its scale. Where the visual viewport shrinks for the keyboard, as in Safari on iOS in the browser and as a home-screen app, in Firefox for Android and in Chrome on Android, they keep the focused field and the surface around it above the keyboard and leave the page where it was. If Firefox for Android pans to the field, they put the page back within a couple of frames.",
152
152
  },
153
153
  {
154
154
  type: 'prose',
155
- text: 'In Chrome on Android, a Tall BottomSheet\u2019s focused field rides the keyboard as Gboard slides it up, and as Gboard slides it down after the field loses focus. The scroll room stays until the keyboard is down. A hide Chrome reports late, such as Back, which keeps the focus, rides down from Chrome\u2019s report. When the keyboard is reported, the sheet places the field just above it in that same frame, so Chrome has no covered field to pan to; if Chrome pans anyway, the sheet puts the page back when that pan ends. Chrome follows your viewport setting, which Astryx documents and never sets: your app owns its viewport `<meta>` element. By default (`interactive-widget=resizes-visual`) Chrome lays the keyboard over the page and pans to a focused field elsewhere on the page. With `interactive-widget=resizes-content` it shrinks the layout viewport instead, so content anchored to the bottom sits above the keyboard, and the sheet measures no keyboard and changes nothing: the browser has already made the room.',
155
+ text: 'In Chrome on Android, ChatLayout\u2019s composer rides the keyboard as Gboard slides it up and down. A Tall BottomSheet\u2019s focused field rides the keyboard as Gboard slides it up, and as Gboard slides it down after the field loses focus. The scroll room stays until the keyboard is down. A hide Chrome reports late, such as Back, which keeps the focus, rides down from Chrome\u2019s report. When the keyboard is reported, the sheet places the field just above it in that same frame, so Chrome has no covered field to pan to; if Chrome pans anyway, the sheet puts the page back when that pan ends. Chrome follows your viewport setting, which Astryx documents and never sets: your app owns its viewport `<meta>` element. By default (`interactive-widget=resizes-visual`) Chrome lays the keyboard over the page and pans to a focused field elsewhere on the page. With `interactive-widget=resizes-content` it shrinks the layout viewport instead, so content anchored to the bottom sits above the keyboard, and those components measure no keyboard and change nothing: the browser has already made the room.',
156
156
  },
157
157
  {
158
158
  type: 'code',
@@ -160,6 +160,10 @@ export const docs = {
160
160
  type: 'prose',
161
161
  text: 'If a swizzled component fails at runtime with a `stylex.create` error, the app is missing a StyleX compiler: see the StyleX Build Setup section of {@link generic:tokens-and-setup}.',
162
162
  },
163
+ {
164
+ type: 'prose',
165
+ text: '`{@command swizzle <Component>}` writes an `astryx-swizzle.json` marker into an output folder it owns (one the copy creates, an empty one, or one it marked before); every module under that folder then compiles as Astryx source, so keep copies in a folder of their own, such as the default `components/astryx`. For a folder that already holds other files it writes no marker and says how to add one by hand. With `@astryxdesign/build`\'s Babel, PostCSS and Vite integrations, or `withAstryx()` under webpack, a copy under that folder compiles into `astryx-base`, below `astryx-theme`, so theme component overrides reach it as they reach the package component. Under Turbopack, and with the stock StyleX plugins above, a copy\'s rules land in your own styles, above `astryx-theme`, and theme component overrides do not reach it.',
166
+ },
163
167
  ],
164
168
  },
165
169
  {
@@ -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: 'useCalendarConstraints',
7
+ name: 'useCalendarConstraints — Hotel Booking Range',
8
+ displayName: 'useCalendarConstraints — Hotel Booking Range',
9
+ description:
10
+ 'A hotel booking range picker enforcing season min/max dates, a 7-night maximum stay once check-in is picked, and a custom rule disabling closed Mondays.',
11
+ isReady: true,
12
+ aspectRatio: 4 / 3,
13
+ componentsUsed: ['Button', 'Layout', 'Text'],
14
+ };
@@ -0,0 +1,146 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {useState} from 'react';
6
+ import {
7
+ useCalendarConstraints,
8
+ useCalendarDays,
9
+ type CalendarDay,
10
+ } from '@astryxdesign/core/Calendar';
11
+ import {Button} from '@astryxdesign/core/Button';
12
+ import {HStack, VStack} from '@astryxdesign/core/Layout';
13
+ import {Text} from '@astryxdesign/core/Text';
14
+
15
+ // Hotel closed on Mondays (day === 1)
16
+ const isNotMonday = (date: Date) => date.getDay() !== 1;
17
+
18
+ export default function UseCalendarConstraintsHookUsage() {
19
+ const [checkIn, setCheckIn] = useState<CalendarDay['date'] | null>(null);
20
+ const [checkOut, setCheckOut] = useState<CalendarDay['date'] | null>(null);
21
+
22
+ // June 2026 season: open Jun 5 – Jun 25, max 7 nights (8-day span), closed Mondays.
23
+ // When checkOut is already chosen, clear rangeAnchor so full season remains viewable without lingering span restrictions.
24
+ const {isDateDisabled} = useCalendarConstraints({
25
+ min: '2026-06-05',
26
+ max: '2026-06-25',
27
+ maxRangeSpan: 8,
28
+ rangeAnchor: checkOut ? null : checkIn,
29
+ dateConstraints: [isNotMonday],
30
+ });
31
+
32
+ const {weeks, dayNames} = useCalendarDays({
33
+ year: 2026,
34
+ month: 6,
35
+ weekStartsOn: 0,
36
+ hasVariableRowCount: true,
37
+ });
38
+
39
+ const handleSelect = (day: CalendarDay) => {
40
+ if (day.isOutside || isDateDisabled(day.date)) {
41
+ return;
42
+ }
43
+ // If no check-in yet, or already booked, or clicking on/before current check-in: start new check-in
44
+ if (!checkIn || checkOut || day.date.day <= checkIn.day) {
45
+ setCheckIn(day.date);
46
+ setCheckOut(null);
47
+ } else {
48
+ // Pick is after checkIn: set as check-out
49
+ setCheckOut(day.date);
50
+ }
51
+ };
52
+
53
+ const handleReset = () => {
54
+ setCheckIn(null);
55
+ setCheckOut(null);
56
+ };
57
+
58
+ return (
59
+ <VStack gap={3}>
60
+ <VStack gap={1}>
61
+ <Text type="body" weight="bold">
62
+ Hotel Booking — June 2026
63
+ </Text>
64
+ <Text type="supporting" color="secondary">
65
+ Season: Jun 5 – Jun 25 · Max stay: 7 nights · Closed Mondays
66
+ </Text>
67
+ </VStack>
68
+ <HStack gap={2} vAlign="center">
69
+ <Text type="supporting">
70
+ {checkIn
71
+ ? checkOut
72
+ ? `Booked: Jun ${checkIn.day} – Jun ${checkOut.day}`
73
+ : `Check-in: Jun ${checkIn.day} (pick check-out)`
74
+ : 'Pick a check-in date'}
75
+ </Text>
76
+ {checkIn && (
77
+ <Button
78
+ label="Reset"
79
+ size="sm"
80
+ variant="secondary"
81
+ onClick={handleReset}
82
+ />
83
+ )}
84
+ </HStack>
85
+ <table style={{tableLayout: 'fixed', width: 280}}>
86
+ <thead>
87
+ <tr>
88
+ {dayNames.map((name, i) => (
89
+ <th key={i} scope="col">
90
+ <Text type="supporting" color="secondary">
91
+ {name}
92
+ </Text>
93
+ </th>
94
+ ))}
95
+ </tr>
96
+ </thead>
97
+ <tbody>
98
+ {weeks.map(week => (
99
+ <tr key={week[0].iso}>
100
+ {week.map(day => {
101
+ if (day.isOutside) {
102
+ return <td key={day.iso} align="center" />;
103
+ }
104
+
105
+ const disabled = isDateDisabled(day.date);
106
+ const isSelected =
107
+ (checkIn &&
108
+ checkIn.day === day.date.day &&
109
+ checkIn.month === day.date.month) ||
110
+ (checkOut &&
111
+ checkOut.day === day.date.day &&
112
+ checkOut.month === day.date.month);
113
+
114
+ return (
115
+ <td key={day.iso} align="center">
116
+ <button
117
+ type="button"
118
+ disabled={disabled}
119
+ onClick={() => handleSelect(day)}
120
+ aria-pressed={isSelected ? 'true' : 'false'}
121
+ aria-label={`June ${day.dayNumber}`}
122
+ style={{
123
+ width: 32,
124
+ height: 32,
125
+ borderRadius: 6,
126
+ border: isSelected ? '1px solid currentColor' : 'none',
127
+ background: 'transparent',
128
+ opacity: disabled ? 0.35 : 1,
129
+ cursor: disabled ? 'not-allowed' : 'pointer',
130
+ }}>
131
+ <Text
132
+ type="body"
133
+ color={disabled ? 'secondary' : 'primary'}>
134
+ {day.dayNumber}
135
+ </Text>
136
+ </button>
137
+ </td>
138
+ );
139
+ })}
140
+ </tr>
141
+ ))}
142
+ </tbody>
143
+ </table>
144
+ </VStack>
145
+ );
146
+ }
@@ -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: 'useCalendarNavigation',
7
+ name: 'useCalendarNavigation — Month Pager',
8
+ displayName: 'useCalendarNavigation — Month Pager',
9
+ description:
10
+ 'A calendar month pager with previous/next navigation buttons, localized month-year label, and cross-month arrow key navigation using pendingFocus.',
11
+ isReady: true,
12
+ aspectRatio: 4 / 3,
13
+ componentsUsed: ['Button', 'Layout', 'Text'],
14
+ };
@@ -0,0 +1,173 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {useEffect, useRef, useState} from 'react';
6
+ import {
7
+ useCalendarDays,
8
+ useCalendarNavigation,
9
+ type CalendarDay,
10
+ type ISODateString,
11
+ } from '@astryxdesign/core/Calendar';
12
+ import {Button} from '@astryxdesign/core/Button';
13
+ import {HStack, VStack} from '@astryxdesign/core/Layout';
14
+ import {Text} from '@astryxdesign/core/Text';
15
+
16
+ export default function UseCalendarNavigationHookUsage() {
17
+ const [focusDate, setFocusDate] = useState<ISODateString>(
18
+ '2026-06-01' as ISODateString,
19
+ );
20
+
21
+ const {
22
+ monthYearLabel,
23
+ visibleCalendarMonths,
24
+ navigateMonth,
25
+ pendingFocus,
26
+ clearPendingFocus,
27
+ } = useCalendarNavigation({
28
+ focusDate,
29
+ onFocusDateChange: setFocusDate,
30
+ numberOfMonths: 1,
31
+ });
32
+
33
+ const {weeks, dayNames} = useCalendarDays({
34
+ ...visibleCalendarMonths[0],
35
+ weekStartsOn: 0,
36
+ hasVariableRowCount: true,
37
+ });
38
+
39
+ const dayButtonRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
40
+
41
+ // Move focus to target date when navigating across month boundaries
42
+ useEffect(() => {
43
+ if (pendingFocus) {
44
+ const el = dayButtonRefs.current.get(pendingFocus);
45
+ if (el) {
46
+ el.focus();
47
+ clearPendingFocus();
48
+ }
49
+ }
50
+ }, [pendingFocus, clearPendingFocus, weeks]);
51
+
52
+ // Arrow key navigation moving across days and month boundaries with RTL awareness (DR6, DR11)
53
+ const handleKeyDown = (
54
+ e: React.KeyboardEvent<HTMLButtonElement>,
55
+ day: CalendarDay,
56
+ ) => {
57
+ if (day.isOutside) {
58
+ return;
59
+ }
60
+
61
+ const isRtl =
62
+ typeof window !== 'undefined' &&
63
+ getComputedStyle(e.currentTarget).direction === 'rtl';
64
+
65
+ const isNext = isRtl ? e.key === 'ArrowLeft' : e.key === 'ArrowRight';
66
+ const isPrev = isRtl ? e.key === 'ArrowRight' : e.key === 'ArrowLeft';
67
+
68
+ if (isNext) {
69
+ e.preventDefault();
70
+ const inMonthDays = weeks.flatMap(w => w.filter(d => !d.isOutside));
71
+ const currentIndex = inMonthDays.findIndex(d => d.iso === day.iso);
72
+ if (currentIndex === inMonthDays.length - 1) {
73
+ // Last day of month: advance month and focus next day across boundary
74
+ navigateMonth(1, day.iso, 1);
75
+ } else {
76
+ const nextDay = inMonthDays[currentIndex + 1];
77
+ dayButtonRefs.current.get(nextDay.iso)?.focus();
78
+ }
79
+ } else if (isPrev) {
80
+ e.preventDefault();
81
+ const inMonthDays = weeks.flatMap(w => w.filter(d => !d.isOutside));
82
+ const currentIndex = inMonthDays.findIndex(d => d.iso === day.iso);
83
+ if (currentIndex === 0) {
84
+ // First day of month: step back month and focus previous day across boundary
85
+ navigateMonth(-1, day.iso, 1);
86
+ } else {
87
+ const prevDay = inMonthDays[currentIndex - 1];
88
+ dayButtonRefs.current.get(prevDay.iso)?.focus();
89
+ }
90
+ }
91
+ };
92
+
93
+ return (
94
+ <VStack gap={3} style={{width: 280}}>
95
+ <HStack gap={2} vAlign="center" hAlign="between">
96
+ <Button
97
+ label="Previous"
98
+ size="sm"
99
+ variant="secondary"
100
+ onClick={() => navigateMonth(-1)}
101
+ aria-label="Previous month"
102
+ />
103
+ <Text type="body" weight="bold">
104
+ {monthYearLabel}
105
+ </Text>
106
+ <Button
107
+ label="Next"
108
+ size="sm"
109
+ variant="secondary"
110
+ onClick={() => navigateMonth(1)}
111
+ aria-label="Next month"
112
+ />
113
+ </HStack>
114
+
115
+ <table style={{tableLayout: 'fixed', width: 280}}>
116
+ <thead>
117
+ <tr>
118
+ {dayNames.map((name, i) => (
119
+ <th key={i} scope="col">
120
+ <Text type="supporting" color="secondary">
121
+ {name}
122
+ </Text>
123
+ </th>
124
+ ))}
125
+ </tr>
126
+ </thead>
127
+ <tbody>
128
+ {weeks.map(week => (
129
+ <tr key={week[0].iso}>
130
+ {week.map(day => (
131
+ <td key={day.iso} align="center">
132
+ <button
133
+ type="button"
134
+ ref={el => {
135
+ if (el) {
136
+ dayButtonRefs.current.set(day.iso, el);
137
+ } else {
138
+ dayButtonRefs.current.delete(day.iso);
139
+ }
140
+ }}
141
+ disabled={day.isOutside}
142
+ aria-hidden={day.isOutside || undefined}
143
+ onKeyDown={e => handleKeyDown(e, day)}
144
+ aria-label={
145
+ day.isOutside
146
+ ? undefined
147
+ : `${monthYearLabel} ${day.dayNumber}`
148
+ }
149
+ tabIndex={day.isOutside ? -1 : 0}
150
+ style={{
151
+ width: 32,
152
+ height: 32,
153
+ borderRadius: 6,
154
+ border: 'none',
155
+ background: 'transparent',
156
+ opacity: day.isOutside ? 0.35 : 1,
157
+ cursor: day.isOutside ? 'not-allowed' : 'pointer',
158
+ }}>
159
+ <Text
160
+ type="body"
161
+ color={day.isOutside ? 'secondary' : 'primary'}>
162
+ {day.dayNumber}
163
+ </Text>
164
+ </button>
165
+ </td>
166
+ ))}
167
+ </tr>
168
+ ))}
169
+ </tbody>
170
+ </table>
171
+ </VStack>
172
+ );
173
+ }
@@ -260,7 +260,7 @@ export const butterTheme = defineTheme({
260
260
  '--color-tint-hover': [neutral.light[0], neutral.dark[100]],
261
261
 
262
262
  // Typography override
263
- '--text-supporting-size': '12px',
263
+ '--text-supporting-size': '0.75rem', // 12px at the default root size
264
264
 
265
265
  // Element sizes — slightly taller defaults so the new input padding
266
266
  // (--spacing-2 block) has room to breathe.
@@ -240,7 +240,7 @@ export const stoneTheme = defineTheme({
240
240
  '--color-tint-hover': [neutral.light[0], neutral.dark[100]],
241
241
 
242
242
  // Typography override
243
- '--text-supporting-size': '12px',
243
+ '--text-supporting-size': '0.75rem', // 12px at the default root size
244
244
 
245
245
  // Categorical
246
246
  // Light mode pairs pastel stop 90 surfaces with stop 30 foregrounds.
@@ -236,7 +236,7 @@ export const y2kTheme = defineTheme({
236
236
  '--color-tint-hover': [neutral.light[15], neutral.dark[95]],
237
237
 
238
238
  // Typography override
239
- '--text-supporting-size': '12px',
239
+ '--text-supporting-size': '0.75rem', // 12px at the default root size
240
240
 
241
241
  // Categorical colors
242
242
  '--color-background-green': [green.light[80], green.dark[80]],
@@ -22,6 +22,10 @@ export const doc = {
22
22
  "is copied beside it in the package's folder layout. " +
23
23
  'Tests, stories and docs are not copied. It lists the npm packages the copy imports, ' +
24
24
  'with the ranges the owner package declares, to install if your project lacks them. ' +
25
+ 'An output folder swizzle owns (one the copy ' +
26
+ 'creates, an empty one, or one it marked before) gets an astryx-swizzle.json marker naming ' +
27
+ "each package its copies came from, so @astryxdesign/build's Babel, PostCSS and Vite " +
28
+ 'integrations, and withAstryx() under webpack, compile the copy below your theme. ' +
25
29
  'With no name it lists the swizzlable components. ' +
26
30
  'An integration component that replaces a Core component is what that Core name ' +
27
31
  'copies; --package @astryxdesign/core copies the original. --list lists Core ' +
@@ -113,6 +113,18 @@ export function registerSwizzle(program) {
113
113
  );
114
114
  }
115
115
 
116
+ const {marker} = result.data;
117
+ if (marker) {
118
+ const folder = marker.path.replace(/\/?astryx-swizzle\.json$/, '') || '.';
119
+ out.push(
120
+ text(
121
+ marker.written
122
+ ? `Marked ${folder}/ with astryx-swizzle.json: with @astryxdesign/build's Babel, PostCSS and Vite integrations, or withAstryx() under webpack, every module under it compiles below your theme, so theme overrides reach the copy. Keep only swizzled copies there. Turbopack does not read the marker yet.`
123
+ : `Did not mark ${folder}/ with astryx-swizzle.json: it already holds ${(marker.found ?? []).join(', ')} and no astryx-swizzle.json. If it holds only swizzled copies, add ${marker.path} to mark it, so theme overrides reach them; keep the product's own components out of a marked folder.`,
124
+ ),
125
+ );
126
+ }
127
+
116
128
  // StyleX build requirement — swizzled StyleX source throws on import
117
129
  // ("Unexpected 'stylex.create' call at runtime") unless the consumer's
118
130
  // build runs a StyleX compiler, so the page does not render at all.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.7.12-canary.ccb4a6d",
3
+ "version": "0.7.12-canary.f543ec7",
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",
@@ -124,7 +124,7 @@
124
124
  "zod": "^4.4.3"
125
125
  },
126
126
  "peerDependencies": {
127
- "@astryxdesign/core": "0.7.12-canary.ccb4a6d"
127
+ "@astryxdesign/core": "0.7.12-canary.f543ec7"
128
128
  },
129
129
  "peerDependenciesMeta": {
130
130
  "@astryxdesign/core": {
@@ -132,10 +132,10 @@
132
132
  }
133
133
  },
134
134
  "devDependencies": {
135
- "@astryxdesign/charts": "0.7.12-canary.ccb4a6d",
136
- "@astryxdesign/core": "0.7.12-canary.ccb4a6d",
137
- "@astryxdesign/lab": "0.7.12-canary.ccb4a6d",
138
- "@astryxdesign/theme-neutral": "0.7.12-canary.ccb4a6d",
135
+ "@astryxdesign/charts": "0.7.12-canary.f543ec7",
136
+ "@astryxdesign/core": "0.7.12-canary.f543ec7",
137
+ "@astryxdesign/lab": "0.7.12-canary.f543ec7",
138
+ "@astryxdesign/theme-neutral": "0.7.12-canary.f543ec7",
139
139
  "@heroicons/react": "^2.2.0",
140
140
  "@stylexjs/stylex": "^0.19.0",
141
141
  "@types/babel__core": "^7.20.5",