@astryxdesign/core 0.7.2-canary.fe2c655 → 0.7.2
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.
- package/CHANGELOG.md +156 -2
- package/README.md +4 -4
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +40 -1
- package/dist/AspectRatio/AspectRatio.d.ts +6 -8
- package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
- package/dist/AspectRatio/AspectRatio.js +44 -3
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +8 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +4 -1
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
- package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.js +1 -0
- package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +3 -1
- package/dist/CheckboxList/CheckboxList.d.ts +1 -1
- package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxList.js +6 -2
- package/dist/CheckboxList/CheckboxListItem.d.ts +9 -1
- package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.js +4 -2
- package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +37 -5
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +5 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +20 -8
- package/dist/DropdownMenu/DropdownMenu.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +84 -113
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -0
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +21 -0
- package/dist/DropdownMenu/DropdownMenuDivider.d.ts +5 -3
- package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuDivider.js +7 -3
- package/dist/DropdownMenu/DropdownMenuGroup.d.ts +6 -4
- package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuGroup.js +4 -3
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +15 -4
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +24 -0
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -0
- package/dist/DropdownMenu/lazyMenuBottomSheet.js +46 -0
- package/dist/DropdownMenu/menuRegionStops.d.ts +38 -0
- package/dist/DropdownMenu/menuRegionStops.d.ts.map +1 -0
- package/dist/DropdownMenu/menuRegionStops.js +131 -0
- package/dist/EmptyState/EmptyState.d.ts.map +1 -1
- package/dist/EmptyState/EmptyState.js +4 -10
- package/dist/Field/Field.d.ts +1 -1
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +8 -4
- package/dist/FormLayout/FormLayout.d.ts +5 -4
- package/dist/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/FormLayout/FormLayout.js +1 -1
- package/dist/FormLayout/FormLayoutContext.d.ts +3 -3
- package/dist/FormLayout/FormLayoutContext.js +3 -3
- package/dist/Grid/Grid.d.ts +8 -7
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +5 -4
- package/dist/Grid/GridSpan.d.ts +1 -1
- package/dist/Grid/GridSpan.d.ts.map +1 -1
- package/dist/Grid/GridSpan.js +1 -0
- package/dist/Heading/Heading.d.ts.map +1 -1
- package/dist/Heading/Heading.js +4 -2
- package/dist/Item/Item.d.ts +6 -2
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +19 -4
- package/dist/List/List.d.ts +46 -19
- package/dist/List/List.d.ts.map +1 -1
- package/dist/List/List.js +28 -10
- package/dist/List/ListContext.d.ts +8 -3
- package/dist/List/ListContext.d.ts.map +1 -1
- package/dist/List/ListContext.js +7 -2
- package/dist/List/ListItem.d.ts +13 -5
- package/dist/List/ListItem.d.ts.map +1 -1
- package/dist/List/ListItem.js +21 -8
- package/dist/List/index.d.ts +1 -1
- package/dist/List/index.d.ts.map +1 -1
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +8 -1
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +8 -2
- package/dist/Markdown/plugins/headingLinks.d.ts.map +1 -1
- package/dist/Markdown/plugins/headingLinks.js +9 -1
- package/dist/MetadataList/MetadataList.d.ts.map +1 -1
- package/dist/MetadataList/MetadataList.js +5 -1
- package/dist/MetadataList/MetadataListContext.d.ts +1 -1
- package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.d.ts +9 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +30 -3
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +5 -0
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchValueEditor.js +45 -11
- package/dist/RadioList/RadioList.d.ts +1 -1
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +6 -2
- package/dist/RadioList/RadioListItem.d.ts +9 -1
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +2 -0
- package/dist/SideNav/SideNavItem.d.ts +8 -1
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +76 -10
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +4 -2
- package/dist/ToggleButton/ToggleButton.d.ts +3 -2
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +12 -1
- package/dist/Token/Token.js +1 -1
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +5 -3
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +5 -3
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +1 -0
- package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
- package/dist/TreeList/TreeListBranches.js +2 -2
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +57 -9
- package/dist/astryx.css +27 -4
- package/dist/hooks/useMenuHover.d.ts.map +1 -1
- package/dist/hooks/useMenuHover.js +18 -13
- package/dist/reset.a11y.chromium.spec.js +50 -1
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +15 -9
- package/docs.mjs +4 -4
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +66 -6
- package/scripts/postinstall.mjs +44 -3
- package/src/AppShell/AppShell.tsx +45 -1
- package/src/AspectRatio/AspectRatio.test.tsx +44 -43
- package/src/AspectRatio/AspectRatio.tsx +38 -18
- package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
- package/src/Avatar/Avatar.doc.mjs +5 -3
- package/src/Avatar/Avatar.tsx +11 -2
- package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
- package/src/AvatarGroup/AvatarGroupOverflow.test.tsx +24 -1
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -0
- package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
- package/src/Card/Card.doc.mjs +1 -1
- package/src/Chat/ChatLayoutScrollButton.test.tsx +13 -0
- package/src/Chat/ChatLayoutScrollButton.tsx +4 -2
- package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +64 -19
- package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +3 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
- package/src/CheckboxInput/CheckboxInput.tsx +3 -1
- package/src/CheckboxList/CheckboxList.fr10.test.tsx +29 -2
- package/src/CheckboxList/CheckboxList.test.tsx +22 -0
- package/src/CheckboxList/CheckboxList.tsx +4 -1
- package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
- package/src/CheckboxList/CheckboxListItem.tsx +10 -0
- package/src/Code/Code.doc.mjs +1 -0
- package/src/CommandPalette/CommandPalette.doc.mjs +2 -0
- package/src/CommandPalette/CommandPaletteInput.test.tsx +53 -4
- package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
- package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
- package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +292 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +2 -3
- package/src/ContextMenu/ContextMenu.test.tsx +12 -2
- package/src/ContextMenu/ContextMenu.tsx +4 -1
- package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +438 -0
- package/src/Dialog/Dialog.test.tsx +46 -0
- package/src/Dialog/Dialog.tsx +20 -7
- package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
- package/src/Drawer/Drawer.doc.mjs +1 -0
- package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +57 -0
- package/src/DropdownMenu/DropdownMenu.test.tsx +79 -6
- package/src/DropdownMenu/DropdownMenu.tsx +88 -144
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuContext.tsx +24 -0
- package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
- package/src/DropdownMenu/DropdownMenuDivider.spec.md +192 -0
- package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
- package/src/DropdownMenu/DropdownMenuGroup.doc.mjs +169 -10
- package/src/DropdownMenu/DropdownMenuGroup.tsx +6 -4
- package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +126 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +17 -3
- package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
- package/src/DropdownMenu/__tests__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +334 -0
- package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +43 -0
- package/src/DropdownMenu/lazyMenuBottomSheet.ts +71 -0
- package/src/DropdownMenu/menuRegionStops.ts +167 -0
- package/src/EmptyState/EmptyState.doc.mjs +3 -2
- package/src/EmptyState/EmptyState.spec.md +189 -0
- package/src/EmptyState/EmptyState.test.tsx +74 -3
- package/src/EmptyState/EmptyState.tsx +4 -8
- package/src/Field/Field.doc.mjs +14 -8
- package/src/Field/Field.spec.md +217 -0
- package/src/Field/Field.test.tsx +60 -2
- package/src/Field/Field.tsx +17 -8
- package/src/Field/FieldLabel.doc.mjs +133 -26
- package/src/Field/FieldLabel.spec.md +172 -0
- package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +296 -0
- package/src/FormLayout/FormLayout.doc.mjs +6 -3
- package/src/FormLayout/FormLayout.spec.md +192 -0
- package/src/FormLayout/FormLayout.test.tsx +15 -0
- package/src/FormLayout/FormLayout.tsx +7 -6
- package/src/FormLayout/FormLayoutContext.ts +3 -3
- package/src/Grid/Grid.doc.mjs +7 -1
- package/src/Grid/Grid.spec.md +93 -113
- package/src/Grid/Grid.test.tsx +22 -0
- package/src/Grid/Grid.tsx +10 -9
- package/src/Grid/GridSpan.doc.mjs +3 -3
- package/src/Grid/GridSpan.tsx +4 -1
- package/src/Heading/Heading.test.tsx +57 -0
- package/src/Heading/Heading.tsx +6 -2
- package/src/Icon/Icon.doc.mjs +36 -0
- package/src/Indicator/Indicator.doc.mjs +3 -3
- package/src/Item/Item.doc.mjs +12 -8
- package/src/Item/Item.test.tsx +61 -0
- package/src/Item/Item.tsx +40 -4
- package/src/Layout/LayoutHeader.doc.mjs +5 -0
- package/src/List/List.doc.mjs +14 -1
- package/src/List/List.spec.md +129 -77
- package/src/List/List.test.tsx +163 -94
- package/src/List/List.tsx +74 -18
- package/src/List/ListContext.tsx +9 -3
- package/src/List/ListItem.doc.mjs +16 -0
- package/src/List/ListItem.tsx +56 -27
- package/src/List/index.ts +1 -1
- package/src/Markdown/Markdown.tsx +14 -1
- package/src/Markdown/parser.ts +8 -2
- package/src/Markdown/plugins/headingLinks.ts +9 -1
- package/src/MetadataList/MetadataList.doc.mjs +3 -3
- package/src/MetadataList/MetadataList.test.tsx +38 -0
- package/src/MetadataList/MetadataList.tsx +8 -1
- package/src/MetadataList/MetadataListContext.tsx +1 -1
- package/src/MobileNav/MobileNav.tsx +30 -3
- package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +173 -15
- package/src/MoreMenu/MoreMenu.test.tsx +5 -1
- package/src/PowerSearch/PowerSearch.test.tsx +62 -0
- package/src/PowerSearch/PowerSearch.tsx +9 -0
- package/src/PowerSearch/PowerSearchValueEditor.test.tsx +71 -0
- package/src/PowerSearch/PowerSearchValueEditor.tsx +45 -0
- package/src/ProgressBar/ProgressBar.doc.mjs +1 -0
- package/src/RadioList/RadioList.fr10.test.tsx +45 -2
- package/src/RadioList/RadioList.test.tsx +22 -0
- package/src/RadioList/RadioList.tsx +4 -1
- package/src/RadioList/RadioListItem.doc.mjs +16 -0
- package/src/RadioList/RadioListItem.tsx +10 -1
- package/src/Resizable/useResizable.doc.mjs +2 -0
- package/src/ScrollableArea/ScrollableArea.doc.mjs +1 -1
- package/src/SideNav/SideNav.spec.md +227 -0
- package/src/SideNav/SideNav.test.tsx +220 -0
- package/src/SideNav/SideNavItem.doc.mjs +2 -2
- package/src/SideNav/SideNavItem.tsx +99 -17
- package/src/Slider/Slider.spec.md +39 -18
- package/src/Spinner/Spinner.doc.mjs +1 -1
- package/src/TabList/TabList.spec.md +57 -44
- package/src/Table/Table.spec.md +6 -1
- package/src/Table/plugins/activation/useTableActivation.spec.md +254 -0
- package/src/Text/Text.test.tsx +57 -0
- package/src/Text/Text.tsx +6 -2
- package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/ToggleButton/ToggleButton.test.tsx +95 -0
- package/src/ToggleButton/ToggleButton.tsx +12 -1
- package/src/ToggleButton/ToggleButtonGroup.doc.mjs +4 -0
- package/src/Token/Token.tsx +1 -1
- package/src/Token/__tests__/TokenRemoveTarget.a11y.chromium.spec.ts +86 -0
- package/src/Toolbar/Toolbar.test.tsx +0 -8
- package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +163 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +63 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +142 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +126 -0
- package/src/TopNav/TopNavMegaMenu.test.tsx +47 -0
- package/src/TopNav/TopNavMegaMenu.tsx +9 -3
- package/src/TopNav/TopNavMenu.test.tsx +46 -1
- package/src/TopNav/TopNavMenu.tsx +13 -3
- package/src/TreeList/TreeList.doc.mjs +1 -0
- package/src/TreeList/TreeList.test.tsx +327 -6
- package/src/TreeList/TreeList.tsx +5 -1
- package/src/TreeList/TreeListBranches.tsx +6 -0
- package/src/TreeList/TreeListItem.tsx +88 -13
- package/src/docPropLiterals.test.ts +36 -10
- package/src/hooks/useMenuHover.ts +18 -13
- package/src/postinstall.test.mjs +79 -0
- package/src/reset.a11y.chromium.spec.ts +52 -1
- package/src/reset.css +9 -29
- package/src/reset.test.ts +11 -0
- package/src/theme/derivedVarRegistry.test.ts +7 -0
- package/src/unicodePropertyEscapes.source-build.test.mjs +69 -0
- package/src/utils/plainDate.test.ts +37 -1
- package/src/utils/plainDate.ts +15 -5
|
@@ -107,15 +107,75 @@ export function isAstryxInitialized(targetDir = process.cwd()) {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
*
|
|
111
|
-
* (core) and layer 2 (cli) can never word it differently.
|
|
110
|
+
* Does an agent working in `startDir` already have the Astryx prompt in reach?
|
|
112
111
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
112
|
+
* Coding agents read the agent docs between the repository root and the folder
|
|
113
|
+
* they work in. So inside a git repository, init's block in any folder from
|
|
114
|
+
* `startDir` up to the repository root counts: a workspace package of a
|
|
115
|
+
* monorepo initialized at its root is set up. Outside a repository only
|
|
116
|
+
* `startDir` itself is checked, as {@link isAstryxInitialized} does. Both the
|
|
117
|
+
* per-command nudge and the postinstall nudges ask this.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} [startDir=process.cwd()]
|
|
120
|
+
* @returns {boolean}
|
|
121
|
+
*/
|
|
122
|
+
export function isAstryxPromptInReach(startDir = process.cwd()) {
|
|
123
|
+
const start = path.resolve(startDir);
|
|
124
|
+
const repositoryRoot = findRepositoryRoot(start);
|
|
125
|
+
if (!repositoryRoot) return isAstryxInitialized(start);
|
|
126
|
+
for (let dir = start; ; dir = path.dirname(dir)) {
|
|
127
|
+
if (isAstryxInitialized(dir)) return true;
|
|
128
|
+
if (dir === repositoryRoot) return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The nearest folder at or above `start` that holds `.git` (a folder, or the
|
|
134
|
+
* file a worktree uses), or null outside a repository.
|
|
135
|
+
* @param {string} start
|
|
136
|
+
* @returns {string | null}
|
|
137
|
+
*/
|
|
138
|
+
function findRepositoryRoot(start) {
|
|
139
|
+
for (let dir = start; ; ) {
|
|
140
|
+
if (fs.existsSync(path.join(dir, '.git'))) return dir;
|
|
141
|
+
const parent = path.dirname(dir);
|
|
142
|
+
if (parent === dir) return null;
|
|
143
|
+
dir = parent;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The invocation the setup nudge falls back to when the CLI's picker
|
|
149
|
+
* (`getCliInvocation`) cannot be loaded. Scoped package form
|
|
150
|
+
* (`@astryxdesign/cli`) — always resolves to us, even before the CLI is
|
|
151
|
+
* installed. npx with the unscoped name would fetch an unrelated look-alike
|
|
115
152
|
* package (see PR #4151).
|
|
116
153
|
*/
|
|
117
|
-
export const
|
|
118
|
-
|
|
154
|
+
export const SETUP_INVOCATION = 'npx @astryxdesign/cli';
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The one-line next-step every setup layer prints. Kept here so layer 1
|
|
158
|
+
* (core), layer 2 (cli postinstall) and layer 3 (each command) can never word
|
|
159
|
+
* it differently. Each layer passes the stem from the CLI's picker when it can
|
|
160
|
+
* load it.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} [invocation] the CLI invocation stem, e.g. `pnpm exec astryx`
|
|
163
|
+
* @param {{before?: string | null}} [options] `before`: a command to run
|
|
164
|
+
* first, such as the one that adds the CLI to a project that has none
|
|
165
|
+
* @returns {string}
|
|
166
|
+
*/
|
|
167
|
+
export function setupNudge(
|
|
168
|
+
invocation = SETUP_INVOCATION,
|
|
169
|
+
{before = null} = {},
|
|
170
|
+
) {
|
|
171
|
+
const run = before
|
|
172
|
+
? `\`${before}\`, then \`${invocation} init\``
|
|
173
|
+
: `\`${invocation} init\``;
|
|
174
|
+
return `\nNext step: run ${run} to finish setup and install the Astryx agent prompt.\n\n`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The nudge with the fallback invocation. */
|
|
178
|
+
export const SETUP_NUDGE = setupNudge();
|
|
119
179
|
|
|
120
180
|
/**
|
|
121
181
|
* Pure decision: should a postinstall print the setup nudge? Shared by BOTH
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @file @astryxdesign/core postinstall — nudge to run `
|
|
4
|
+
* @file @astryxdesign/core postinstall — nudge to run `astryx init`, prefixed
|
|
5
|
+
* the way the project's installed CLI prints commands.
|
|
5
6
|
*
|
|
6
7
|
* Enforcement layer 1 of making `astryx init` foolproof: when the design system
|
|
7
8
|
* is installed and the project hasn't run init yet, print a one-line next-step
|
|
@@ -19,6 +20,8 @@
|
|
|
19
20
|
* during npx's fetch, and once set up.
|
|
20
21
|
*/
|
|
21
22
|
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as path from 'node:path';
|
|
22
25
|
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
23
26
|
|
|
24
27
|
const HERE = fileURLToPath(import.meta.url);
|
|
@@ -37,6 +40,39 @@ async function loadContract() {
|
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The stem the project's installed CLI would print, from its picker
|
|
45
|
+
* (`getCliInvocation`). Core cannot depend on the CLI, so this looks for an
|
|
46
|
+
* installed copy from the project upward and imports the picker by path; with
|
|
47
|
+
* no CLI there, or one too old to have the picker, it returns undefined and
|
|
48
|
+
* the contract's scoped fallback is printed. At install time no integration is
|
|
49
|
+
* loaded, so this is the package-manager guess.
|
|
50
|
+
* @param {string} root
|
|
51
|
+
* @returns {Promise<string | undefined>}
|
|
52
|
+
*/
|
|
53
|
+
export async function cliInvocation(root) {
|
|
54
|
+
try {
|
|
55
|
+
let dir = path.resolve(root);
|
|
56
|
+
for (;;) {
|
|
57
|
+
const picker = path.join(
|
|
58
|
+
dir,
|
|
59
|
+
'node_modules/@astryxdesign/cli/foundation/env/package-manager.mjs',
|
|
60
|
+
);
|
|
61
|
+
if (fs.existsSync(picker)) {
|
|
62
|
+
const {getCliInvocation} = await import(pathToFileURL(picker).href);
|
|
63
|
+
return typeof getCliInvocation === 'function'
|
|
64
|
+
? getCliInvocation(root)
|
|
65
|
+
: undefined;
|
|
66
|
+
}
|
|
67
|
+
const parent = path.dirname(dir);
|
|
68
|
+
if (parent === dir) return undefined;
|
|
69
|
+
dir = parent;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
40
76
|
async function main() {
|
|
41
77
|
const contract = await loadContract();
|
|
42
78
|
if (!contract) return;
|
|
@@ -45,10 +81,15 @@ async function main() {
|
|
|
45
81
|
contract.shouldNudge({
|
|
46
82
|
scriptPath: HERE,
|
|
47
83
|
npmCommand: process.env.npm_command,
|
|
48
|
-
isSetUp: contract.
|
|
84
|
+
isSetUp: contract.isAstryxPromptInReach(root),
|
|
49
85
|
})
|
|
50
86
|
) {
|
|
51
|
-
|
|
87
|
+
const invocation = await cliInvocation(root);
|
|
88
|
+
process.stdout.write(
|
|
89
|
+
typeof contract.setupNudge === 'function'
|
|
90
|
+
? contract.setupNudge(invocation)
|
|
91
|
+
: contract.SETUP_NUDGE,
|
|
92
|
+
);
|
|
52
93
|
}
|
|
53
94
|
}
|
|
54
95
|
|
|
@@ -43,6 +43,7 @@ import {LayoutHeader} from '../Layout/LayoutHeader';
|
|
|
43
43
|
import {LayoutPanel} from '../Layout/LayoutPanel';
|
|
44
44
|
import {LayoutContent} from '../Layout/LayoutContent';
|
|
45
45
|
import {MobileNavToggle} from '../MobileNav/MobileNavToggle';
|
|
46
|
+
import {MAX_CLOSE_DELAY_MS as MOBILE_NAV_MAX_CLOSE_DELAY_MS} from '../MobileNav/MobileNav';
|
|
46
47
|
import {SideNavRenderContext} from '../SideNav/SideNavRenderContext';
|
|
47
48
|
import {TopNavRenderContext} from '../TopNav/TopNavRenderContext';
|
|
48
49
|
import {TopNavMobileContentContext} from '../TopNav/TopNavMobileContentContext';
|
|
@@ -81,6 +82,20 @@ const ActivityWrapper = HasActivity
|
|
|
81
82
|
|
|
82
83
|
const MAIN_CONTENT_ID = 'astryx-app-shell-main';
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Extra time, on top of MobileNav's own worst-case close delay, that
|
|
87
|
+
* AppShell waits before flipping its `Activity` boundary to `'hidden'`.
|
|
88
|
+
*
|
|
89
|
+
* React's `Activity` hides its subtree with `display: none !important`
|
|
90
|
+
* synchronously in the same commit as the mode flip, and re-runs the
|
|
91
|
+
* cleanup of every effect inside it — which would otherwise clear
|
|
92
|
+
* MobileNav's own scheduled `dialog.close()` before it fires, or cut off
|
|
93
|
+
* its slide-out CSS transition before it plays (#5701). Waiting strictly
|
|
94
|
+
* longer than MobileNav's own cap, rather than the same duration, means
|
|
95
|
+
* this never turns into a race decided by browser timer-firing order.
|
|
96
|
+
*/
|
|
97
|
+
const ACTIVITY_HIDE_BUFFER_MS = 50;
|
|
98
|
+
|
|
84
99
|
// =============================================================================
|
|
85
100
|
// Types
|
|
86
101
|
// =============================================================================
|
|
@@ -525,6 +540,34 @@ export function AppShell({
|
|
|
525
540
|
const [uncontrolledMobileOpen, setUncontrolledMobileOpen] = useState(false);
|
|
526
541
|
const isMobileNavOpen = mobileNavConfig?.isOpen ?? uncontrolledMobileOpen;
|
|
527
542
|
|
|
543
|
+
// Holds the mobile nav's `Activity` boundary visible for a beat after a
|
|
544
|
+
// close, so MobileNav's own close effect gets a real chance to run its
|
|
545
|
+
// slide-out transition and schedule its delayed `dialog.close()` before
|
|
546
|
+
// Activity hides the subtree (#5701). Only the CLOSE is deferred: opening
|
|
547
|
+
// reads `isMobileNavOpen` directly below, so a controlled open is visible in
|
|
548
|
+
// the commit that carries it instead of one effect later.
|
|
549
|
+
const [isMobileNavCloseHeld, setIsMobileNavCloseHeld] =
|
|
550
|
+
useState(isMobileNavOpen);
|
|
551
|
+
// Arm the hold during render, from the open state itself, so no Effect has
|
|
552
|
+
// to copy props into state; the Effect below only owns the release timer.
|
|
553
|
+
if (isMobileNavOpen && !isMobileNavCloseHeld) {
|
|
554
|
+
setIsMobileNavCloseHeld(true);
|
|
555
|
+
}
|
|
556
|
+
useEffect(() => {
|
|
557
|
+
if (!HasActivity || isMobileNavOpen) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const delayMs = window.matchMedia('(prefers-reduced-motion: reduce)')
|
|
561
|
+
.matches
|
|
562
|
+
? 0
|
|
563
|
+
: MOBILE_NAV_MAX_CLOSE_DELAY_MS + ACTIVITY_HIDE_BUFFER_MS;
|
|
564
|
+
const timeout = setTimeout(() => {
|
|
565
|
+
setIsMobileNavCloseHeld(false);
|
|
566
|
+
}, delayMs);
|
|
567
|
+
return () => clearTimeout(timeout);
|
|
568
|
+
}, [isMobileNavOpen]);
|
|
569
|
+
const isMobileNavActivityVisible = isMobileNavOpen || isMobileNavCloseHeld;
|
|
570
|
+
|
|
528
571
|
const mobileNavOnOpenChange = mobileNavConfig?.onOpenChange;
|
|
529
572
|
const setMobileNavOpen = useCallback(
|
|
530
573
|
(open: boolean) => {
|
|
@@ -864,7 +907,8 @@ export function AppShell({
|
|
|
864
907
|
!mobileNavDisabled &&
|
|
865
908
|
mobileNavReactNode == null &&
|
|
866
909
|
!mobileNavConfigContent && (
|
|
867
|
-
<ActivityWrapper
|
|
910
|
+
<ActivityWrapper
|
|
911
|
+
mode={isMobileNavActivityVisible ? 'visible' : 'hidden'}>
|
|
868
912
|
{/* SideNav drawer — always mounted so presence detection works.
|
|
869
913
|
Hidden when TopNav owns the drawer (combined mode passes
|
|
870
914
|
sideNav via TopNav's mobile content context instead). */}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* SYNC: When AspectRatio.tsx changes, update tests to match new behavior
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {describe, it, expect, vi
|
|
12
|
+
import {describe, it, expect, vi} from 'vitest';
|
|
13
13
|
import {render, screen} from '@testing-library/react';
|
|
14
14
|
import {AspectRatio} from './AspectRatio';
|
|
15
15
|
|
|
@@ -230,9 +230,8 @@ describe('AspectRatio', () => {
|
|
|
230
230
|
<img src="test.jpg" alt="Test" data-testid="image" />
|
|
231
231
|
</AspectRatio>,
|
|
232
232
|
);
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
// AspectRatio's internal structure.
|
|
233
|
+
// Earlier releases documented the marker, so it stays on the child's
|
|
234
|
+
// direct parent for product CSS that targets it.
|
|
236
235
|
const wrapper = screen.getByTestId('image').parentElement;
|
|
237
236
|
expect(wrapper).toHaveAttribute(
|
|
238
237
|
'data-astryx-aspect-ratio-override',
|
|
@@ -274,8 +273,11 @@ describe('AspectRatio', () => {
|
|
|
274
273
|
);
|
|
275
274
|
const wrapper = screen.getByTestId('image').parentElement;
|
|
276
275
|
expect(wrapper).not.toHaveAttribute('data-astryx-aspect-ratio-override');
|
|
277
|
-
// Without
|
|
278
|
-
//
|
|
276
|
+
// Without fit the wrapper carries no child-sizing styles, so existing
|
|
277
|
+
// self-styled children render exactly as before.
|
|
278
|
+
expect(wrapper?.className).not.toContain('childFill');
|
|
279
|
+
expect(wrapper?.className).not.toContain('childCover');
|
|
280
|
+
expect(wrapper?.className).not.toContain('childContain');
|
|
279
281
|
expect(screen.getByTestId('image')).not.toHaveAttribute('class');
|
|
280
282
|
});
|
|
281
283
|
|
|
@@ -292,13 +294,37 @@ describe('AspectRatio', () => {
|
|
|
292
294
|
</AspectRatio>,
|
|
293
295
|
);
|
|
294
296
|
const image = screen.getByTestId('image');
|
|
295
|
-
// Fit styling is
|
|
296
|
-
//
|
|
297
|
-
//
|
|
297
|
+
// Fit styling is a direct-child rule on the wrapper; the child's own
|
|
298
|
+
// className/style pass through untouched, and an inline style still
|
|
299
|
+
// wins over it.
|
|
298
300
|
expect(image.className).toBe('consumer-class');
|
|
299
301
|
expect(image.style.objectFit).toBe('contain');
|
|
300
302
|
});
|
|
301
303
|
|
|
304
|
+
it('sizes the child from the wrapper with fit="cover"', () => {
|
|
305
|
+
render(
|
|
306
|
+
<AspectRatio ratio={16 / 9} fit="cover">
|
|
307
|
+
<img src="test.jpg" alt="Test" data-testid="image" />
|
|
308
|
+
</AspectRatio>,
|
|
309
|
+
);
|
|
310
|
+
const wrapper = screen.getByTestId('image').parentElement;
|
|
311
|
+
expect(wrapper?.className).toContain('childFill');
|
|
312
|
+
expect(wrapper?.className).toContain('childCover');
|
|
313
|
+
expect(wrapper?.className).not.toContain('childContain');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('sizes the child from the wrapper with fit="contain"', () => {
|
|
317
|
+
render(
|
|
318
|
+
<AspectRatio ratio={16 / 9} fit="contain">
|
|
319
|
+
<img src="test.jpg" alt="Test" data-testid="image" />
|
|
320
|
+
</AspectRatio>,
|
|
321
|
+
);
|
|
322
|
+
const wrapper = screen.getByTestId('image').parentElement;
|
|
323
|
+
expect(wrapper?.className).toContain('childFill');
|
|
324
|
+
expect(wrapper?.className).toContain('childContain');
|
|
325
|
+
expect(wrapper?.className).not.toContain('childCover');
|
|
326
|
+
});
|
|
327
|
+
|
|
302
328
|
it('centers the child via the wrapper with fit="center"', () => {
|
|
303
329
|
render(
|
|
304
330
|
<AspectRatio ratio={16 / 9} fit="center">
|
|
@@ -309,6 +335,7 @@ describe('AspectRatio', () => {
|
|
|
309
335
|
expect(image).not.toHaveAttribute('class');
|
|
310
336
|
const wrapper = image.parentElement;
|
|
311
337
|
expect(wrapper?.className).toContain('childCenter');
|
|
338
|
+
expect(wrapper?.className).not.toContain('childFill');
|
|
312
339
|
});
|
|
313
340
|
|
|
314
341
|
it('does not center the wrapper for other fit values', () => {
|
|
@@ -353,45 +380,19 @@ describe('AspectRatio', () => {
|
|
|
353
380
|
});
|
|
354
381
|
});
|
|
355
382
|
|
|
356
|
-
describe('reset.css
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
beforeAll(async () => {
|
|
383
|
+
describe('reset.css', () => {
|
|
384
|
+
it('carries no AspectRatio rules', async () => {
|
|
385
|
+
// fit ships with the component's StyleX styles in `astryx-base`. A
|
|
386
|
+
// rule in reset.css sits below the layer the documented Tailwind
|
|
387
|
+
// setups give preflight, whose `img, video { height: auto }` would
|
|
388
|
+
// undo the fill.
|
|
364
389
|
const fs = await import('fs');
|
|
365
390
|
const path = await import('path');
|
|
366
|
-
resetCSS = fs.readFileSync(
|
|
391
|
+
const resetCSS = fs.readFileSync(
|
|
367
392
|
path.resolve(__dirname, '../reset.css'),
|
|
368
393
|
'utf-8',
|
|
369
394
|
);
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
it('sizes the child to fill the box for cover and contain', () => {
|
|
373
|
-
const fillMatch = resetCSS.match(
|
|
374
|
-
/:where\(\[data-astryx-aspect-ratio-override="cover"\], \[data-astryx-aspect-ratio-override="contain"\]\)\s*>\s*:where\(\*\)\s*\{([^}]+)\}/,
|
|
375
|
-
);
|
|
376
|
-
expect(fillMatch).not.toBeNull();
|
|
377
|
-
expect(fillMatch![1]).toContain('width: 100%');
|
|
378
|
-
expect(fillMatch![1]).toContain('height: 100%');
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it('crops media with object-fit: cover for fit="cover"', () => {
|
|
382
|
-
const coverMatch = resetCSS.match(
|
|
383
|
-
/:where\(\[data-astryx-aspect-ratio-override="cover"\]\)\s*>\s*:where\(img, video\)\s*\{([^}]+)\}/,
|
|
384
|
-
);
|
|
385
|
-
expect(coverMatch).not.toBeNull();
|
|
386
|
-
expect(coverMatch![1]).toContain('object-fit: cover');
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it('letterboxes media with object-fit: contain for fit="contain"', () => {
|
|
390
|
-
const containMatch = resetCSS.match(
|
|
391
|
-
/:where\(\[data-astryx-aspect-ratio-override="contain"\]\)\s*>\s*:where\(img, video\)\s*\{([^}]+)\}/,
|
|
392
|
-
);
|
|
393
|
-
expect(containMatch).not.toBeNull();
|
|
394
|
-
expect(containMatch![1]).toContain('object-fit: contain');
|
|
395
|
+
expect(resetCSS).not.toContain('aspect-ratio-override');
|
|
395
396
|
});
|
|
396
397
|
});
|
|
397
398
|
});
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* SYNC: When modified, update these files to stay in sync:
|
|
10
10
|
* - /packages/core/src/AspectRatio/AspectRatio.doc.mjs
|
|
11
11
|
* - /packages/core/src/AspectRatio/AspectRatio.test.tsx
|
|
12
|
-
* - /packages/core/src/reset.css (AspectRatio fit baseline rules)
|
|
13
12
|
* - /apps/storybook/stories/AspectRatio.stories.tsx
|
|
14
13
|
* - /packages/cli/assets/templates/blocks/components/AspectRatio/ (showcase blocks)
|
|
15
14
|
*/
|
|
@@ -91,13 +90,12 @@ export interface AspectRatioProps extends BaseProps<HTMLDivElement> {
|
|
|
91
90
|
* (`object-fit: contain`).
|
|
92
91
|
* - `center`: child keeps its natural size, centered in the box.
|
|
93
92
|
*
|
|
94
|
-
* `cover`/`contain` child
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* so it is not exposed on the theming surface.
|
|
93
|
+
* `cover`/`contain` size the child from the component's own styles, in
|
|
94
|
+
* the `astryx-base` cascade layer. Styles in a later layer (Tailwind
|
|
95
|
+
* utilities, `astryx-theme`), unlayered CSS, and inline styles on the
|
|
96
|
+
* child still win. When omitted, the child is left unstyled (the
|
|
97
|
+
* pre-existing contract). `fit` is structural, not visual, so it is not
|
|
98
|
+
* exposed on the theming surface.
|
|
101
99
|
*
|
|
102
100
|
* @example
|
|
103
101
|
* ```
|
|
@@ -140,15 +138,33 @@ const styles = stylex.create({
|
|
|
140
138
|
height: '100%',
|
|
141
139
|
},
|
|
142
140
|
// fit="center" centers the child at its natural size from the wrapper —
|
|
143
|
-
// no styles on the child itself.
|
|
144
|
-
// live here (StyleX has no descendant selectors); it ships as baseline
|
|
145
|
-
// rules in reset.css keyed on the `data-astryx-aspect-ratio-override`
|
|
146
|
-
// attribute the wrapper carries (see below).
|
|
141
|
+
// no styles on the child itself.
|
|
147
142
|
childCenter: {
|
|
148
143
|
display: 'flex',
|
|
149
144
|
alignItems: 'center',
|
|
150
145
|
justifyContent: 'center',
|
|
151
146
|
},
|
|
147
|
+
// `cover`/`contain` must style the child itself (`object-fit` only applies
|
|
148
|
+
// on the replaced element), and StyleX has no child selector. The
|
|
149
|
+
// `:not([stylex-hack])` pseudo-class always matches, so StyleX accepts the
|
|
150
|
+
// key and emits `.x:not([stylex-hack]) > *`, a direct-child rule in the
|
|
151
|
+
// `astryx-base` layer like every other component style.
|
|
152
|
+
childFill: {
|
|
153
|
+
':not([stylex-hack]) > *': {
|
|
154
|
+
width: '100%',
|
|
155
|
+
height: '100%',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
childCover: {
|
|
159
|
+
':not([stylex-hack]) > :is(img, video)': {
|
|
160
|
+
objectFit: 'cover',
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
childContain: {
|
|
164
|
+
':not([stylex-hack]) > :is(img, video)': {
|
|
165
|
+
objectFit: 'contain',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
152
168
|
});
|
|
153
169
|
|
|
154
170
|
// The ratio compiles to a CSS variable + a class-level declaration
|
|
@@ -234,14 +250,18 @@ export function AspectRatio({
|
|
|
234
250
|
style,
|
|
235
251
|
)}
|
|
236
252
|
{...props}>
|
|
237
|
-
{/* The marker attribute carries the fit value
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
structure or on the theming surface (fit is structural, not
|
|
241
|
-
themeable). The name is namespaced to avoid collisions. */}
|
|
253
|
+
{/* The marker attribute carries the fit value. No Astryx style keys on
|
|
254
|
+
it any more, but earlier releases documented it, so product CSS may
|
|
255
|
+
still target it. */}
|
|
242
256
|
<div
|
|
243
257
|
data-astryx-aspect-ratio-override={fit}
|
|
244
|
-
{...stylex.props(
|
|
258
|
+
{...stylex.props(
|
|
259
|
+
styles.child,
|
|
260
|
+
fit === 'center' && styles.childCenter,
|
|
261
|
+
(fit === 'cover' || fit === 'contain') && styles.childFill,
|
|
262
|
+
fit === 'cover' && styles.childCover,
|
|
263
|
+
fit === 'contain' && styles.childContain,
|
|
264
|
+
)}>
|
|
245
265
|
{children}
|
|
246
266
|
</div>
|
|
247
267
|
</div>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file AspectRatioFitLayers.a11y.browser.spec.ts
|
|
5
|
+
* @input The built FitModes story (its rendered markup and compiled CSS) and
|
|
6
|
+
* Tailwind's preflight.css
|
|
7
|
+
* @output Browser evidence that `fit` fills the box on the documented setups
|
|
8
|
+
* @position AspectRatio integration proof. Which rule sizes the child is a
|
|
9
|
+
* cascade-layer question, and only a shipping engine resolves the cascade.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import {fileURLToPath} from 'node:url';
|
|
14
|
+
import {expect, test, type Page} from '@playwright/test';
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_STORYBOOK_DIR,
|
|
17
|
+
serveStorybook,
|
|
18
|
+
type StaticServer,
|
|
19
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
20
|
+
|
|
21
|
+
const PREFLIGHT_CSS = fs.readFileSync(
|
|
22
|
+
fileURLToPath(import.meta.resolve('tailwindcss/preflight.css')),
|
|
23
|
+
'utf8',
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// The layer order each setup guide declares, and the layer preflight goes in.
|
|
27
|
+
const TAILWIND_SETUPS = [
|
|
28
|
+
{
|
|
29
|
+
name: 'Tailwind v4',
|
|
30
|
+
order:
|
|
31
|
+
'@layer reset, theme, base, astryx-base, astryx-theme, components, utilities;',
|
|
32
|
+
preflightLayer: 'base',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'Tailwind v3',
|
|
36
|
+
order: '@layer reset, tw-preflight, astryx-base, astryx-theme;',
|
|
37
|
+
preflightLayer: 'tw-preflight',
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// A 100×100 image in a 320px-wide 16:9 box: filling the box makes it 320×180.
|
|
42
|
+
const IMAGE =
|
|
43
|
+
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'/%3E";
|
|
44
|
+
|
|
45
|
+
type Fit = 'cover' | 'contain';
|
|
46
|
+
|
|
47
|
+
let storybook: StaticServer;
|
|
48
|
+
test.beforeAll(async () => {
|
|
49
|
+
storybook = await serveStorybook(
|
|
50
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
test.afterAll(async () => {
|
|
54
|
+
await storybook?.close();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The markup AspectRatio renders for `fit`, and every CSS rule the story
|
|
59
|
+
* page loaded, taken from the built story so the classes and rules are the
|
|
60
|
+
* component's real compiled output. The child image gets a fixed id, a
|
|
61
|
+
* local source and any extra class.
|
|
62
|
+
*/
|
|
63
|
+
async function captureStory(page: Page, fit: Fit, mediaClass: string) {
|
|
64
|
+
await page.goto(
|
|
65
|
+
`${storybook.origin}/iframe.html?id=core-aspectratio--fit-modes&viewMode=story`,
|
|
66
|
+
);
|
|
67
|
+
const wrapper = page.locator(`[data-astryx-aspect-ratio-override="${fit}"]`);
|
|
68
|
+
await expect(wrapper).toHaveCount(1);
|
|
69
|
+
return wrapper.evaluate(
|
|
70
|
+
(element, {image, extraClass}) => {
|
|
71
|
+
const root = element.parentElement?.cloneNode(true);
|
|
72
|
+
const media =
|
|
73
|
+
root instanceof HTMLElement ? root.querySelector('img') : null;
|
|
74
|
+
if (!(root instanceof HTMLElement) || media == null) {
|
|
75
|
+
throw new Error('FitModes no longer renders an img inside AspectRatio');
|
|
76
|
+
}
|
|
77
|
+
media.id = 'media';
|
|
78
|
+
media.src = image;
|
|
79
|
+
if (extraClass) {
|
|
80
|
+
media.classList.add(extraClass);
|
|
81
|
+
}
|
|
82
|
+
const css = [...document.styleSheets]
|
|
83
|
+
.flatMap(sheet => [...sheet.cssRules].map(rule => rule.cssText))
|
|
84
|
+
.join('\n');
|
|
85
|
+
return {html: root.outerHTML, css};
|
|
86
|
+
},
|
|
87
|
+
{image: IMAGE, extraClass: mediaClass},
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Re-host the captured story with `before` CSS ahead of it and `after` CSS behind it. */
|
|
92
|
+
async function measureMedia(
|
|
93
|
+
page: Page,
|
|
94
|
+
fit: Fit,
|
|
95
|
+
{
|
|
96
|
+
before = [],
|
|
97
|
+
after = [],
|
|
98
|
+
mediaClass = '',
|
|
99
|
+
}: {
|
|
100
|
+
before?: string[];
|
|
101
|
+
after?: string[];
|
|
102
|
+
mediaClass?: string;
|
|
103
|
+
},
|
|
104
|
+
) {
|
|
105
|
+
const {html, css} = await captureStory(page, fit, mediaClass);
|
|
106
|
+
await page.setContent(
|
|
107
|
+
`<!doctype html><div style="width: 320px">${html}</div>`,
|
|
108
|
+
);
|
|
109
|
+
for (const content of [...before, css, ...after]) {
|
|
110
|
+
await page.addStyleTag({content});
|
|
111
|
+
}
|
|
112
|
+
await page.waitForFunction(
|
|
113
|
+
() => (document.getElementById('media') as HTMLImageElement).complete,
|
|
114
|
+
);
|
|
115
|
+
return page.evaluate(() => {
|
|
116
|
+
const media = document.getElementById('media');
|
|
117
|
+
if (!media) {
|
|
118
|
+
throw new Error('missing media');
|
|
119
|
+
}
|
|
120
|
+
const box = media.getBoundingClientRect();
|
|
121
|
+
return {
|
|
122
|
+
width: box.width,
|
|
123
|
+
height: box.height,
|
|
124
|
+
objectFit: getComputedStyle(media).objectFit,
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const setup of TAILWIND_SETUPS) {
|
|
130
|
+
for (const fit of ['cover', 'contain'] as const) {
|
|
131
|
+
test(`fit="${fit}" fills the box on the ${setup.name} setup`, async ({
|
|
132
|
+
page,
|
|
133
|
+
}) => {
|
|
134
|
+
const media = await measureMedia(page, fit, {
|
|
135
|
+
before: [
|
|
136
|
+
setup.order,
|
|
137
|
+
`@layer ${setup.preflightLayer} { ${PREFLIGHT_CSS} }`,
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
expect(media).toEqual({width: 320, height: 180, objectFit: fit});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
test('fit fills the box with only Astryx CSS loaded', async ({page}) => {
|
|
146
|
+
const media = await measureMedia(page, 'cover', {});
|
|
147
|
+
expect(media).toEqual({width: 320, height: 180, objectFit: 'cover'});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('a utility class on the child still wins over fit', async ({page}) => {
|
|
151
|
+
const media = await measureMedia(page, 'cover', {
|
|
152
|
+
before: [TAILWIND_SETUPS[0].order, `@layer base { ${PREFLIGHT_CSS} }`],
|
|
153
|
+
after: ['@layer utilities { .h-10 { height: 40px; } }'],
|
|
154
|
+
mediaClass: 'h-10',
|
|
155
|
+
});
|
|
156
|
+
expect(media).toEqual({width: 320, height: 40, objectFit: 'cover'});
|
|
157
|
+
});
|
|
@@ -67,8 +67,10 @@ export const docs = {
|
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
name: 'size',
|
|
70
|
-
type:
|
|
71
|
-
|
|
70
|
+
type:
|
|
71
|
+
"'xsm' | 'sm' | 'md' | 'lg' | 'xl' | 16 | 20 | 24 | 32 | 36 | 40 | 48 | 60 | 64 | 72 | 96 | 128 | 144 | 180",
|
|
72
|
+
description:
|
|
73
|
+
"Avatar size: named sizes are 'xsm' (20px), 'sm' (24px), 'md' (36px), 'lg' (48px), and 'xl' (128px); numeric sizes must be one of the pixel values listed in the type. Inside an AvatarGroup, the group's size always overrides this prop, including when the group uses its default size.",
|
|
72
74
|
default: "'md'",
|
|
73
75
|
},
|
|
74
76
|
{
|
|
@@ -169,7 +171,7 @@ export const docsDense = {
|
|
|
169
171
|
fallbackSrc: 'fallback image when primary fails',
|
|
170
172
|
name: 'user name for initials and alt text',
|
|
171
173
|
alt: 'alt text; falls back to name',
|
|
172
|
-
size: "
|
|
174
|
+
size: "named sizes: 'xsm' 20px, 'sm' 24px, 'md' 36px, 'lg' 48px, 'xl' 128px; numeric px restricted to values listed in the type. AvatarGroup's size always wins, default included.",
|
|
173
175
|
status:
|
|
174
176
|
'corner content for status indicators; AvatarStatusDot reports its `label`, composed into the avatar accessible name ("Jane Doe, Online"), at any nesting depth',
|
|
175
177
|
tooltip:
|
package/src/Avatar/Avatar.tsx
CHANGED
|
@@ -387,9 +387,18 @@ export interface AvatarProps extends BaseProps<HTMLDivElement> {
|
|
|
387
387
|
/**
|
|
388
388
|
* A character that can stand for a word in initials: a letter, a digit, or an
|
|
389
389
|
* emoji. Punctuation and other symbols, such as `(`, `"` or `-`, cannot.
|
|
390
|
+
*
|
|
391
|
+
* Built at runtime, not written as a literal: for targets without property
|
|
392
|
+
* escapes, or when no browserslist is set, Babel's
|
|
393
|
+
* transform-unicode-property-regex rewrites a `\p{…}` literal, and the copy
|
|
394
|
+
* bundled with Next.js lacks the Unicode data it needs, so a source build fails
|
|
395
|
+
* with "Unknown property". A literal is safe once Next's bundled Babel ships
|
|
396
|
+
* that data.
|
|
390
397
|
*/
|
|
391
|
-
const INITIAL_CHARACTER =
|
|
392
|
-
|
|
398
|
+
const INITIAL_CHARACTER = new RegExp(
|
|
399
|
+
'[\\p{L}\\p{N}\\p{Extended_Pictographic}\\p{Regional_Indicator}]',
|
|
400
|
+
'u',
|
|
401
|
+
);
|
|
393
402
|
|
|
394
403
|
/**
|
|
395
404
|
* The first character of a word that can stand for it in initials, skipping
|