@astryxdesign/core 0.7.2-canary.7462c41 → 0.7.2-canary.7ae39ea
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/dist/DropdownMenu/DropdownMenu.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +36 -7
- 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/package.json +3 -3
- package/scripts/agent-doc-state.mjs +38 -0
- package/scripts/postinstall.mjs +1 -1
- package/src/Code/Code.doc.mjs +1 -0
- package/src/CommandPalette/CommandPalette.doc.mjs +2 -0
- package/src/Drawer/Drawer.doc.mjs +1 -0
- package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +57 -0
- package/src/DropdownMenu/DropdownMenu.test.tsx +14 -1
- package/src/DropdownMenu/DropdownMenu.tsx +36 -8
- package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +126 -0
- package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +43 -0
- package/src/DropdownMenu/lazyMenuBottomSheet.ts +71 -0
- package/src/Icon/Icon.doc.mjs +36 -0
- package/src/Layout/LayoutHeader.doc.mjs +5 -0
- package/src/MetadataList/MetadataList.doc.mjs +1 -1
- package/src/MoreMenu/MoreMenu.test.tsx +5 -1
- package/src/ProgressBar/ProgressBar.doc.mjs +1 -0
- package/src/Resizable/useResizable.doc.mjs +2 -0
- package/src/ScrollableArea/ScrollableArea.doc.mjs +1 -1
- package/src/Spinner/Spinner.doc.mjs +1 -1
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/ToggleButton/ToggleButtonGroup.doc.mjs +4 -0
- package/src/postinstall.test.mjs +46 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file DropdownMenu.tsx
|
|
3
|
-
* @input Uses React, StyleX, usePopover, MenuBottomSheet, Button, List,
|
|
3
|
+
* @input Uses React, StyleX, usePopover, MenuBottomSheet (lazy), Button, List,
|
|
4
4
|
* useListFocus, and the shared viewport-safe menu-width resolver
|
|
5
5
|
* @output Exports DropdownMenu with caller-selected popover or bottom-sheet
|
|
6
6
|
* presentation
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DropdownMenu.d.ts","sourceRoot":"","sources":["../../src/DropdownMenu/DropdownMenu.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,EAOZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,OAAO,EAAS,KAAK,WAAW,EAAC,MAAM,WAAW,CAAC;AAKnD,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,oBAAoB,CAAC;AA0B9D,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"DropdownMenu.d.ts","sourceRoot":"","sources":["../../src/DropdownMenu/DropdownMenu.tsx"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file DropdownMenu.tsx
|
|
7
|
-
* @input Uses React, StyleX, usePopover, MenuBottomSheet, Button, List,
|
|
7
|
+
* @input Uses React, StyleX, usePopover, MenuBottomSheet (lazy), Button, List,
|
|
8
8
|
* useListFocus, and the shared viewport-safe menu-width resolver
|
|
9
9
|
* @output Exports DropdownMenu with caller-selected popover or bottom-sheet
|
|
10
10
|
* presentation
|
|
@@ -58,8 +58,8 @@ import { resolveMenuWidth } from "./menuWidth.js";
|
|
|
58
58
|
import "../Layer/layerViewportInset.stylex.js";
|
|
59
59
|
import { layerViewportInset } from "../Layer/layerViewportInset.stylex.js";
|
|
60
60
|
import { useAdaptivePresentation } from "../hooks/useAdaptivePresentation.js";
|
|
61
|
-
|
|
62
|
-
import { MenuBottomSheetActionList } from "./
|
|
61
|
+
// Loaded on demand: only the bottom-sheet presentation renders the sheet.
|
|
62
|
+
import { MenuBottomSheet, MenuBottomSheetActionList } from "./lazyMenuBottomSheet.js";
|
|
63
63
|
import { layerAnimations } from "../Layer/layerAnimations.stylex.js";
|
|
64
64
|
import "../theme/tokens.stylex.js";
|
|
65
65
|
import { spacingVars, colorVars, radiusVars, durationVars, easeVars } from "../theme/tokens.stylex.js";
|
|
@@ -491,12 +491,41 @@ function DropdownMenuBottomSheet({
|
|
|
491
491
|
if (!isOpen || openModalityRef.current !== 'keyboard') {
|
|
492
492
|
return;
|
|
493
493
|
}
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
494
|
+
const findFirstAction = () => actionListRef.current?.querySelector('button:not(:disabled), a[href]') ?? null;
|
|
495
|
+
let observer = null;
|
|
496
|
+
let frame = requestAnimationFrame(() => {
|
|
497
|
+
const action = findFirstAction();
|
|
498
|
+
if (action != null) {
|
|
499
|
+
action.focus({
|
|
500
|
+
preventScroll: true
|
|
501
|
+
});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
// The sheet loads on demand, so a keyboard open can arrive before its
|
|
505
|
+
// rows exist. Wait for them, then focus a frame later so the sheet's
|
|
506
|
+
// own open-focus has already run.
|
|
507
|
+
observer = new MutationObserver(() => {
|
|
508
|
+
const loaded = findFirstAction();
|
|
509
|
+
if (loaded == null) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
observer?.disconnect();
|
|
513
|
+
observer = null;
|
|
514
|
+
frame = requestAnimationFrame(() => {
|
|
515
|
+
loaded.focus({
|
|
516
|
+
preventScroll: true
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
observer.observe(document.body, {
|
|
521
|
+
childList: true,
|
|
522
|
+
subtree: true
|
|
497
523
|
});
|
|
498
524
|
});
|
|
499
|
-
return () =>
|
|
525
|
+
return () => {
|
|
526
|
+
cancelAnimationFrame(frame);
|
|
527
|
+
observer?.disconnect();
|
|
528
|
+
};
|
|
500
529
|
}, [currentItems, isOpen]);
|
|
501
530
|
useEffect(() => {
|
|
502
531
|
if (!isOpen) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file lazyMenuBottomSheet.ts
|
|
3
|
+
* @input Uses React lazy and Suspense, useIsHydrating, MenuBottomSheet, and
|
|
4
|
+
* MenuBottomSheetActionList
|
|
5
|
+
* @output Exports MenuBottomSheet and MenuBottomSheetActionList wrappers that
|
|
6
|
+
* load the touch sheet on first render
|
|
7
|
+
* @position Internal to DropdownMenu
|
|
8
|
+
*
|
|
9
|
+
* The touch sheet (BottomSheet, its gestures, and the touch action list) only
|
|
10
|
+
* renders for the bottom-sheet presentation. Loading it on demand keeps it out
|
|
11
|
+
* of pages whose dropdowns only open as anchored popovers. Each wrapper keeps
|
|
12
|
+
* the wrapped component's props and renders nothing until its module loads.
|
|
13
|
+
*
|
|
14
|
+
* Neither wrapper renders on the server or in the render that hydrates server
|
|
15
|
+
* HTML (spec:AST-013 FR9): a lazy component that has not loaded yet would
|
|
16
|
+
* leave server markup incomplete and abort hydration. The closed sheet has no
|
|
17
|
+
* visible markup, so it first renders right after hydration.
|
|
18
|
+
*/
|
|
19
|
+
import { type ComponentProps, type ReactElement } from 'react';
|
|
20
|
+
import type { MenuBottomSheet as EagerMenuBottomSheet } from './MenuBottomSheet';
|
|
21
|
+
import type { MenuBottomSheetActionList as EagerMenuBottomSheetActionList } from './MenuBottomSheetActionList';
|
|
22
|
+
export declare function MenuBottomSheet(props: ComponentProps<typeof EagerMenuBottomSheet>): ReactElement | null;
|
|
23
|
+
export declare function MenuBottomSheetActionList(props: ComponentProps<typeof EagerMenuBottomSheetActionList>): ReactElement | null;
|
|
24
|
+
//# sourceMappingURL=lazyMenuBottomSheet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lazyMenuBottomSheet.d.ts","sourceRoot":"","sources":["../../src/DropdownMenu/lazyMenuBottomSheet.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,YAAY,EAClB,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,eAAe,IAAI,oBAAoB,EAAC,MAAM,mBAAmB,CAAC;AAC/E,OAAO,KAAK,EAAC,yBAAyB,IAAI,8BAA8B,EAAC,MAAM,6BAA6B,CAAC;AAc7G,wBAAgB,eAAe,CAC7B,KAAK,EAAE,cAAc,CAAC,OAAO,oBAAoB,CAAC,GACjD,YAAY,GAAG,IAAI,CASrB;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,cAAc,CAAC,OAAO,8BAA8B,CAAC,GAC3D,YAAY,GAAG,IAAI,CASrB"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file lazyMenuBottomSheet.ts
|
|
7
|
+
* @input Uses React lazy and Suspense, useIsHydrating, MenuBottomSheet, and
|
|
8
|
+
* MenuBottomSheetActionList
|
|
9
|
+
* @output Exports MenuBottomSheet and MenuBottomSheetActionList wrappers that
|
|
10
|
+
* load the touch sheet on first render
|
|
11
|
+
* @position Internal to DropdownMenu
|
|
12
|
+
*
|
|
13
|
+
* The touch sheet (BottomSheet, its gestures, and the touch action list) only
|
|
14
|
+
* renders for the bottom-sheet presentation. Loading it on demand keeps it out
|
|
15
|
+
* of pages whose dropdowns only open as anchored popovers. Each wrapper keeps
|
|
16
|
+
* the wrapped component's props and renders nothing until its module loads.
|
|
17
|
+
*
|
|
18
|
+
* Neither wrapper renders on the server or in the render that hydrates server
|
|
19
|
+
* HTML (spec:AST-013 FR9): a lazy component that has not loaded yet would
|
|
20
|
+
* leave server markup incomplete and abort hydration. The closed sheet has no
|
|
21
|
+
* visible markup, so it first renders right after hydration.
|
|
22
|
+
*/
|
|
23
|
+
import { createElement, lazy, Suspense } from 'react';
|
|
24
|
+
import { useIsHydrating } from "../hooks/useIsHydrating.js";
|
|
25
|
+
const LazyMenuBottomSheet = /*#__PURE__*/lazy(async () => import("./MenuBottomSheet.js").then(module => ({
|
|
26
|
+
default: module.MenuBottomSheet
|
|
27
|
+
})));
|
|
28
|
+
const LazyMenuBottomSheetActionList = /*#__PURE__*/lazy(async () => import("./MenuBottomSheetActionList.js").then(module => ({
|
|
29
|
+
default: module.MenuBottomSheetActionList
|
|
30
|
+
})));
|
|
31
|
+
export function MenuBottomSheet(props) {
|
|
32
|
+
if (useIsHydrating()) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return /*#__PURE__*/createElement(Suspense, {
|
|
36
|
+
fallback: null
|
|
37
|
+
}, /*#__PURE__*/createElement(LazyMenuBottomSheet, props));
|
|
38
|
+
}
|
|
39
|
+
export function MenuBottomSheetActionList(props) {
|
|
40
|
+
if (useIsHydrating()) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return /*#__PURE__*/createElement(Suspense, {
|
|
44
|
+
fallback: null
|
|
45
|
+
}, /*#__PURE__*/createElement(LazyMenuBottomSheetActionList, props));
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.2-canary.
|
|
3
|
+
"version": "0.7.2-canary.7ae39ea",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -792,8 +792,8 @@
|
|
|
792
792
|
"react-dom": ">=19.0.0"
|
|
793
793
|
},
|
|
794
794
|
"devDependencies": {
|
|
795
|
-
"@astryxdesign/a11y-spec": "0.7.2-canary.
|
|
796
|
-
"@astryxdesign/cli": "0.7.2-canary.
|
|
795
|
+
"@astryxdesign/a11y-spec": "0.7.2-canary.7ae39ea",
|
|
796
|
+
"@astryxdesign/cli": "0.7.2-canary.7ae39ea",
|
|
797
797
|
"@babel/cli": "^7.29.7",
|
|
798
798
|
"@babel/core": "^7.29.7",
|
|
799
799
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -106,6 +106,44 @@ export function isAstryxInitialized(targetDir = process.cwd()) {
|
|
|
106
106
|
return false;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Does an agent working in `startDir` already have the Astryx prompt in reach?
|
|
111
|
+
*
|
|
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
|
+
|
|
109
147
|
/**
|
|
110
148
|
* The one-line next-step both postinstall nudges print. Kept here so layer 1
|
|
111
149
|
* (core) and layer 2 (cli) can never word it differently.
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -45,7 +45,7 @@ async function main() {
|
|
|
45
45
|
contract.shouldNudge({
|
|
46
46
|
scriptPath: HERE,
|
|
47
47
|
npmCommand: process.env.npm_command,
|
|
48
|
-
isSetUp: contract.
|
|
48
|
+
isSetUp: contract.isAstryxPromptInReach(root),
|
|
49
49
|
})
|
|
50
50
|
) {
|
|
51
51
|
process.stdout.write(contract.SETUP_NUDGE);
|
package/src/Code/Code.doc.mjs
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file DropdownMenu.ssr.test.tsx
|
|
5
|
+
* @input DropdownMenu server markup and React hydration
|
|
6
|
+
* @output Proves a bottom-sheet DropdownMenu hydrates without a recoverable
|
|
7
|
+
* error while its on-demand sheet has not loaded (spec:AST-013 FR9)
|
|
8
|
+
* @position SSR acceptance test for DropdownMenu
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {act} from 'react';
|
|
12
|
+
import {afterEach, describe, expect, it, vi} from 'vitest';
|
|
13
|
+
import {hydrateRoot} from 'react-dom/client';
|
|
14
|
+
import {renderToString} from 'react-dom/server';
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
document.body.replaceChildren();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
async function serverRenderThenHydrate(): Promise<unknown[]> {
|
|
22
|
+
// A fresh module graph is a fresh server process: the sheet has not loaded.
|
|
23
|
+
vi.resetModules();
|
|
24
|
+
const {DropdownMenu} = await import('./DropdownMenu');
|
|
25
|
+
const tree = (
|
|
26
|
+
<DropdownMenu
|
|
27
|
+
button={{label: 'Actions'}}
|
|
28
|
+
presentation="bottom-sheet"
|
|
29
|
+
items={[{label: 'Edit project'}, {label: 'Delete'}]}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
const container = document.createElement('div');
|
|
33
|
+
container.innerHTML = renderToString(tree);
|
|
34
|
+
document.body.appendChild(container);
|
|
35
|
+
|
|
36
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
37
|
+
const recoverableErrors: unknown[] = [];
|
|
38
|
+
let root!: ReturnType<typeof hydrateRoot>;
|
|
39
|
+
await act(async () => {
|
|
40
|
+
root = hydrateRoot(container, tree, {
|
|
41
|
+
onRecoverableError: error => recoverableErrors.push(error),
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
await act(async () => {
|
|
45
|
+
root.unmount();
|
|
46
|
+
});
|
|
47
|
+
container.remove();
|
|
48
|
+
return recoverableErrors;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('DropdownMenu — SSR', () => {
|
|
52
|
+
it('hydrates a bottom-sheet menu cleanly on the first renders of a server process', async () => {
|
|
53
|
+
expect(await serverRenderThenHydrate()).toEqual([]);
|
|
54
|
+
expect(await serverRenderThenHydrate()).toEqual([]);
|
|
55
|
+
expect(await serverRenderThenHydrate()).toEqual([]);
|
|
56
|
+
}, 30000);
|
|
57
|
+
});
|
|
@@ -9,7 +9,15 @@
|
|
|
9
9
|
* SYNC: When DropdownMenu.tsx changes, update tests to match new behavior
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
describe,
|
|
14
|
+
it,
|
|
15
|
+
expect,
|
|
16
|
+
vi,
|
|
17
|
+
beforeAll,
|
|
18
|
+
beforeEach,
|
|
19
|
+
afterEach,
|
|
20
|
+
} from 'vitest';
|
|
13
21
|
import {readFileSync} from 'node:fs';
|
|
14
22
|
import {render, screen, fireEvent, waitFor, act} from '@testing-library/react';
|
|
15
23
|
import userEvent from '@testing-library/user-event';
|
|
@@ -19,6 +27,7 @@ import {DropdownMenu, type DropdownMenuTriggerProps} from './DropdownMenu';
|
|
|
19
27
|
import {DropdownMenuItem} from './DropdownMenuItem';
|
|
20
28
|
import {DropdownMenuDivider} from './DropdownMenuDivider';
|
|
21
29
|
import {DropdownMenuGroup} from './DropdownMenuGroup';
|
|
30
|
+
import {warmMenuBottomSheet} from './__tests__/warmMenuBottomSheet';
|
|
22
31
|
import {DropdownMenuSubMenu} from './DropdownMenuSubMenu';
|
|
23
32
|
import {Divider} from '../Divider';
|
|
24
33
|
import {rtlStyles} from '../utils';
|
|
@@ -74,6 +83,10 @@ beforeEach(() => {
|
|
|
74
83
|
};
|
|
75
84
|
});
|
|
76
85
|
|
|
86
|
+
// The touch sheet is loaded lazily; resolve it once so sheets below render
|
|
87
|
+
// synchronously.
|
|
88
|
+
beforeAll(warmMenuBottomSheet, 20000);
|
|
89
|
+
|
|
77
90
|
describe('DropdownMenu', () => {
|
|
78
91
|
it('renders trigger button with label', () => {
|
|
79
92
|
render(
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file DropdownMenu.tsx
|
|
7
|
-
* @input Uses React, StyleX, usePopover, MenuBottomSheet, Button, List,
|
|
7
|
+
* @input Uses React, StyleX, usePopover, MenuBottomSheet (lazy), Button, List,
|
|
8
8
|
* useListFocus, and the shared viewport-safe menu-width resolver
|
|
9
9
|
* @output Exports DropdownMenu with caller-selected popover or bottom-sheet
|
|
10
10
|
* presentation
|
|
@@ -81,8 +81,11 @@ import {
|
|
|
81
81
|
useAdaptivePresentation,
|
|
82
82
|
type AdaptivePresentation,
|
|
83
83
|
} from '../hooks/useAdaptivePresentation';
|
|
84
|
-
|
|
85
|
-
import {
|
|
84
|
+
// Loaded on demand: only the bottom-sheet presentation renders the sheet.
|
|
85
|
+
import {
|
|
86
|
+
MenuBottomSheet,
|
|
87
|
+
MenuBottomSheetActionList,
|
|
88
|
+
} from './lazyMenuBottomSheet';
|
|
86
89
|
import {layerAnimations} from '../Layer/layerAnimations.stylex';
|
|
87
90
|
import type {LayerAlignment, LayerPlacement} from '../Layer/useLayer';
|
|
88
91
|
import {
|
|
@@ -759,12 +762,37 @@ function DropdownMenuBottomSheet({
|
|
|
759
762
|
if (!isOpen || openModalityRef.current !== 'keyboard') {
|
|
760
763
|
return;
|
|
761
764
|
}
|
|
762
|
-
const
|
|
763
|
-
actionListRef.current
|
|
764
|
-
|
|
765
|
-
|
|
765
|
+
const findFirstAction = () =>
|
|
766
|
+
actionListRef.current?.querySelector<HTMLElement>(
|
|
767
|
+
'button:not(:disabled), a[href]',
|
|
768
|
+
) ?? null;
|
|
769
|
+
let observer: MutationObserver | null = null;
|
|
770
|
+
let frame = requestAnimationFrame(() => {
|
|
771
|
+
const action = findFirstAction();
|
|
772
|
+
if (action != null) {
|
|
773
|
+
action.focus({preventScroll: true});
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
// The sheet loads on demand, so a keyboard open can arrive before its
|
|
777
|
+
// rows exist. Wait for them, then focus a frame later so the sheet's
|
|
778
|
+
// own open-focus has already run.
|
|
779
|
+
observer = new MutationObserver(() => {
|
|
780
|
+
const loaded = findFirstAction();
|
|
781
|
+
if (loaded == null) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
observer?.disconnect();
|
|
785
|
+
observer = null;
|
|
786
|
+
frame = requestAnimationFrame(() => {
|
|
787
|
+
loaded.focus({preventScroll: true});
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
observer.observe(document.body, {childList: true, subtree: true});
|
|
766
791
|
});
|
|
767
|
-
return () =>
|
|
792
|
+
return () => {
|
|
793
|
+
cancelAnimationFrame(frame);
|
|
794
|
+
observer?.disconnect();
|
|
795
|
+
};
|
|
768
796
|
}, [currentItems, isOpen]);
|
|
769
797
|
|
|
770
798
|
useEffect(() => {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file DropdownMenuLazySheet.test.tsx
|
|
5
|
+
* @input Uses DropdownMenu, Testing Library, and a held module load
|
|
6
|
+
* @output Tests DropdownMenu while its on-demand touch sheet is still loading
|
|
7
|
+
* @position Test file for DropdownMenu
|
|
8
|
+
*
|
|
9
|
+
* This file never warms the sheet, and it checks that a popover menu never
|
|
10
|
+
* requests it: each test holds the sheet modules until it
|
|
11
|
+
* releases them, so it exercises the path a real page takes while the sheet's
|
|
12
|
+
* code is still downloading.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {describe, it, expect, vi, beforeAll, beforeEach} from 'vitest';
|
|
16
|
+
import {render, screen, waitFor, act} from '@testing-library/react';
|
|
17
|
+
import userEvent from '@testing-library/user-event';
|
|
18
|
+
import type {DropdownMenu as DropdownMenuComponent} from './DropdownMenu';
|
|
19
|
+
|
|
20
|
+
const sheetLoad = vi.hoisted(() => {
|
|
21
|
+
let release: () => void = () => {};
|
|
22
|
+
const gate = new Promise<void>(resolve => {
|
|
23
|
+
release = resolve;
|
|
24
|
+
});
|
|
25
|
+
return {gate, release: () => release(), requested: [] as string[]};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
vi.mock('./MenuBottomSheet', async importOriginal => {
|
|
29
|
+
sheetLoad.requested.push('MenuBottomSheet');
|
|
30
|
+
await sheetLoad.gate;
|
|
31
|
+
return importOriginal();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
vi.mock('./MenuBottomSheetActionList', async importOriginal => {
|
|
35
|
+
sheetLoad.requested.push('MenuBottomSheetActionList');
|
|
36
|
+
await sheetLoad.gate;
|
|
37
|
+
return importOriginal();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
let DropdownMenu: typeof DropdownMenuComponent;
|
|
41
|
+
|
|
42
|
+
// If any module the dropdown loads imported the sheet eagerly, this import
|
|
43
|
+
// would wait on the held sheet and the hook would time out.
|
|
44
|
+
beforeAll(async () => {
|
|
45
|
+
({DropdownMenu} = await import('./DropdownMenu'));
|
|
46
|
+
}, 10000);
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
HTMLDialogElement.prototype.showModal = vi.fn(function (
|
|
50
|
+
this: HTMLDialogElement,
|
|
51
|
+
) {
|
|
52
|
+
this.setAttribute('open', '');
|
|
53
|
+
});
|
|
54
|
+
HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
|
|
55
|
+
this.removeAttribute('open');
|
|
56
|
+
});
|
|
57
|
+
vi.stubGlobal(
|
|
58
|
+
'matchMedia',
|
|
59
|
+
vi.fn().mockImplementation((query: string) => ({
|
|
60
|
+
matches: false,
|
|
61
|
+
media: query,
|
|
62
|
+
onchange: null,
|
|
63
|
+
addEventListener: vi.fn(),
|
|
64
|
+
removeEventListener: vi.fn(),
|
|
65
|
+
addListener: vi.fn(),
|
|
66
|
+
removeListener: vi.fn(),
|
|
67
|
+
dispatchEvent: vi.fn(),
|
|
68
|
+
})),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('DropdownMenu touch sheet loaded on demand', () => {
|
|
73
|
+
// Runs first: nothing in this file has rendered a sheet yet.
|
|
74
|
+
it('never loads the sheet for a popover menu', async () => {
|
|
75
|
+
const user = userEvent.setup();
|
|
76
|
+
HTMLElement.prototype.showPopover = vi.fn();
|
|
77
|
+
HTMLElement.prototype.hidePopover = vi.fn();
|
|
78
|
+
render(
|
|
79
|
+
<DropdownMenu
|
|
80
|
+
button={{label: 'Actions'}}
|
|
81
|
+
items={[{label: 'Edit project'}, {label: 'Delete'}]}
|
|
82
|
+
/>,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await user.click(screen.getByRole('button', {name: /Actions/}));
|
|
86
|
+
await act(async () => {
|
|
87
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Importing DropdownMenu and opening a popover menu requested neither
|
|
91
|
+
// sheet module, so no other module the dropdown loads imports it eagerly.
|
|
92
|
+
expect(sheetLoad.requested).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('focuses the first action when a keyboard open beats the sheet load', async () => {
|
|
96
|
+
const user = userEvent.setup();
|
|
97
|
+
render(
|
|
98
|
+
<DropdownMenu
|
|
99
|
+
button={{label: 'Actions'}}
|
|
100
|
+
presentation="bottom-sheet"
|
|
101
|
+
items={[{label: 'Edit project'}, {label: 'Delete'}]}
|
|
102
|
+
/>,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const trigger = screen.getByRole('button', {name: /Actions/});
|
|
106
|
+
trigger.focus();
|
|
107
|
+
await user.keyboard('{Enter}');
|
|
108
|
+
// The open has committed, but the sheet's code has not arrived yet.
|
|
109
|
+
await act(async () => {
|
|
110
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
111
|
+
});
|
|
112
|
+
expect(screen.queryByRole('button', {name: 'Edit project'})).toBeNull();
|
|
113
|
+
|
|
114
|
+
await act(async () => {
|
|
115
|
+
sheetLoad.release();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await waitFor(
|
|
119
|
+
() =>
|
|
120
|
+
expect(
|
|
121
|
+
screen.getByRole('button', {name: 'Edit project'}),
|
|
122
|
+
).toHaveFocus(),
|
|
123
|
+
{timeout: 15000},
|
|
124
|
+
);
|
|
125
|
+
}, 20000);
|
|
126
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file warmMenuBottomSheet.tsx
|
|
5
|
+
* @input Uses DropdownMenu and Testing Library
|
|
6
|
+
* @output Exports warmMenuBottomSheet, a test helper that resolves the lazily
|
|
7
|
+
* loaded touch sheet once
|
|
8
|
+
* @position Test-only helper for suites that open a DropdownMenu bottom sheet
|
|
9
|
+
*
|
|
10
|
+
* DropdownMenu loads its touch sheet with React.lazy, so the first sheet
|
|
11
|
+
* renders a tick after it opens. Opening one up front caches the resolved
|
|
12
|
+
* modules on the lazy components, and every later sheet renders
|
|
13
|
+
* synchronously. Call it from `beforeAll`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {cleanup, render, screen} from '@testing-library/react';
|
|
17
|
+
import {DropdownMenu} from '../DropdownMenu';
|
|
18
|
+
|
|
19
|
+
export async function warmMenuBottomSheet(): Promise<void> {
|
|
20
|
+
const {showModal, close} = HTMLDialogElement.prototype;
|
|
21
|
+
HTMLDialogElement.prototype.showModal = function (this: HTMLDialogElement) {
|
|
22
|
+
this.setAttribute('open', '');
|
|
23
|
+
};
|
|
24
|
+
HTMLDialogElement.prototype.close = function (this: HTMLDialogElement) {
|
|
25
|
+
this.removeAttribute('open');
|
|
26
|
+
};
|
|
27
|
+
try {
|
|
28
|
+
render(
|
|
29
|
+
<DropdownMenu
|
|
30
|
+
button={{label: 'Warm'}}
|
|
31
|
+
presentation="bottom-sheet"
|
|
32
|
+
isMenuOpen
|
|
33
|
+
items={[{label: 'Warm item'}]}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
// A cold import transforms the sheet's module graph; allow for it.
|
|
37
|
+
await screen.findByText('Warm item', undefined, {timeout: 15000});
|
|
38
|
+
} finally {
|
|
39
|
+
cleanup();
|
|
40
|
+
HTMLDialogElement.prototype.showModal = showModal;
|
|
41
|
+
HTMLDialogElement.prototype.close = close;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file lazyMenuBottomSheet.ts
|
|
7
|
+
* @input Uses React lazy and Suspense, useIsHydrating, MenuBottomSheet, and
|
|
8
|
+
* MenuBottomSheetActionList
|
|
9
|
+
* @output Exports MenuBottomSheet and MenuBottomSheetActionList wrappers that
|
|
10
|
+
* load the touch sheet on first render
|
|
11
|
+
* @position Internal to DropdownMenu
|
|
12
|
+
*
|
|
13
|
+
* The touch sheet (BottomSheet, its gestures, and the touch action list) only
|
|
14
|
+
* renders for the bottom-sheet presentation. Loading it on demand keeps it out
|
|
15
|
+
* of pages whose dropdowns only open as anchored popovers. Each wrapper keeps
|
|
16
|
+
* the wrapped component's props and renders nothing until its module loads.
|
|
17
|
+
*
|
|
18
|
+
* Neither wrapper renders on the server or in the render that hydrates server
|
|
19
|
+
* HTML (spec:AST-013 FR9): a lazy component that has not loaded yet would
|
|
20
|
+
* leave server markup incomplete and abort hydration. The closed sheet has no
|
|
21
|
+
* visible markup, so it first renders right after hydration.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
createElement,
|
|
26
|
+
lazy,
|
|
27
|
+
Suspense,
|
|
28
|
+
type ComponentProps,
|
|
29
|
+
type ReactElement,
|
|
30
|
+
} from 'react';
|
|
31
|
+
import {useIsHydrating} from '../hooks/useIsHydrating';
|
|
32
|
+
import type {MenuBottomSheet as EagerMenuBottomSheet} from './MenuBottomSheet';
|
|
33
|
+
import type {MenuBottomSheetActionList as EagerMenuBottomSheetActionList} from './MenuBottomSheetActionList';
|
|
34
|
+
|
|
35
|
+
const LazyMenuBottomSheet = lazy(async () =>
|
|
36
|
+
import('./MenuBottomSheet').then(module => ({
|
|
37
|
+
default: module.MenuBottomSheet,
|
|
38
|
+
})),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const LazyMenuBottomSheetActionList = lazy(async () =>
|
|
42
|
+
import('./MenuBottomSheetActionList').then(module => ({
|
|
43
|
+
default: module.MenuBottomSheetActionList,
|
|
44
|
+
})),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export function MenuBottomSheet(
|
|
48
|
+
props: ComponentProps<typeof EagerMenuBottomSheet>,
|
|
49
|
+
): ReactElement | null {
|
|
50
|
+
if (useIsHydrating()) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return createElement(
|
|
54
|
+
Suspense,
|
|
55
|
+
{fallback: null},
|
|
56
|
+
createElement(LazyMenuBottomSheet, props),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function MenuBottomSheetActionList(
|
|
61
|
+
props: ComponentProps<typeof EagerMenuBottomSheetActionList>,
|
|
62
|
+
): ReactElement | null {
|
|
63
|
+
if (useIsHydrating()) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return createElement(
|
|
67
|
+
Suspense,
|
|
68
|
+
{fallback: null},
|
|
69
|
+
createElement(LazyMenuBottomSheetActionList, props),
|
|
70
|
+
);
|
|
71
|
+
}
|
package/src/Icon/Icon.doc.mjs
CHANGED
|
@@ -23,6 +23,42 @@ export const docs = {
|
|
|
23
23
|
'pictogram',
|
|
24
24
|
'graphic',
|
|
25
25
|
'vector',
|
|
26
|
+
'arrow',
|
|
27
|
+
'chevron',
|
|
28
|
+
'caret',
|
|
29
|
+
'checkmark',
|
|
30
|
+
'cross',
|
|
31
|
+
'gear',
|
|
32
|
+
'cog',
|
|
33
|
+
'broom',
|
|
34
|
+
'sparkle',
|
|
35
|
+
'sparkles',
|
|
36
|
+
'star',
|
|
37
|
+
'heart',
|
|
38
|
+
'bell',
|
|
39
|
+
'trash',
|
|
40
|
+
'pencil',
|
|
41
|
+
'magnifyingglass',
|
|
42
|
+
'paperclip',
|
|
43
|
+
'house',
|
|
44
|
+
'flame',
|
|
45
|
+
'rocket',
|
|
46
|
+
'lightning',
|
|
47
|
+
'bolt',
|
|
48
|
+
'globe',
|
|
49
|
+
'sun',
|
|
50
|
+
'moon',
|
|
51
|
+
'bookmark',
|
|
52
|
+
'thumbsup',
|
|
53
|
+
'smiley',
|
|
54
|
+
'emoji',
|
|
55
|
+
'lock',
|
|
56
|
+
'eye',
|
|
57
|
+
'cloud',
|
|
58
|
+
'hourglass',
|
|
59
|
+
'trophy',
|
|
60
|
+
'gift',
|
|
61
|
+
'camera',
|
|
26
62
|
],
|
|
27
63
|
playground: {
|
|
28
64
|
// `icon` is required and its type can't be auto-generated, so the
|
|
@@ -6,6 +6,11 @@ export const docs = {
|
|
|
6
6
|
name: 'LayoutHeader',
|
|
7
7
|
subComponentOf: 'Layout',
|
|
8
8
|
displayName: 'Layout Header',
|
|
9
|
+
keywords: [
|
|
10
|
+
'pageheader',
|
|
11
|
+
'appheader',
|
|
12
|
+
'header',
|
|
13
|
+
],
|
|
9
14
|
isHiddenFromOverview: true,
|
|
10
15
|
description: 'Top bar for page titles, app bars, and toolbars.',
|
|
11
16
|
playground: {
|
|
@@ -7,7 +7,7 @@ export const docs = {
|
|
|
7
7
|
displayName: 'Metadata List',
|
|
8
8
|
group: 'MetadataList',
|
|
9
9
|
category: 'Table & List',
|
|
10
|
-
keywords: ["metadata","description","definition","keyvalue","properties","details","attributes","summary"],
|
|
10
|
+
keywords: ["metadata","description","definition","keyvalue","properties","details","attributes","summary","descriptionlist","descriptions"],
|
|
11
11
|
theming: {
|
|
12
12
|
targets: [
|
|
13
13
|
{
|
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
* SYNC: When MoreMenu.tsx changes, update tests to match new behavior
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
12
|
+
import {describe, it, expect, vi, beforeAll, beforeEach} from 'vitest';
|
|
13
13
|
import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import {MoreMenu} from './MoreMenu';
|
|
16
16
|
import {DropdownMenu} from '../DropdownMenu/DropdownMenu';
|
|
17
|
+
import {warmMenuBottomSheet} from '../DropdownMenu/__tests__/warmMenuBottomSheet';
|
|
18
|
+
|
|
19
|
+
// DropdownMenu's touch sheet is loaded lazily; resolve it once up front.
|
|
20
|
+
beforeAll(warmMenuBottomSheet, 20000);
|
|
17
21
|
|
|
18
22
|
// Mock showPopover and hidePopover methods since they're not implemented in jsdom
|
|
19
23
|
beforeEach(() => {
|
|
@@ -5,7 +5,7 @@ export const docs = {
|
|
|
5
5
|
name: 'ScrollableArea',
|
|
6
6
|
displayName: 'Scrollable Area',
|
|
7
7
|
category: 'Layout',
|
|
8
|
-
keywords: ['scroll', 'overflow', 'viewport', 'logical axis', 'keyboard', 'overscroll', 'sticky', 'scrollbar', 'padding', 'full bleed'],
|
|
8
|
+
keywords: ['scroll', 'overflow', 'viewport', 'logical axis', 'keyboard', 'overscroll', 'sticky', 'scrollbar', 'padding', 'full bleed', 'scrollarea', 'scrollview'],
|
|
9
9
|
usage: {
|
|
10
10
|
description: 'Provides a native scroll viewport and a real observed content box. The viewport enters the tab order only while a requested logical axis is effectively scrollable, and containment applies only to effective axes.',
|
|
11
11
|
bestPractices: [
|
|
@@ -6,7 +6,7 @@ export const docs = {
|
|
|
6
6
|
name: 'Spinner',
|
|
7
7
|
displayName: 'Spinner',
|
|
8
8
|
category: 'Feedback & Status',
|
|
9
|
-
keywords: ["spinner","loader","loading","circular","progress","spin","activity","busy","indeterminate"],
|
|
9
|
+
keywords: ["spinner","loader","loading","circular","progress","spin","activity","busy","indeterminate","circularprogress"],
|
|
10
10
|
props: [
|
|
11
11
|
{
|
|
12
12
|
name: 'size',
|
|
@@ -6,7 +6,7 @@ export const docs = {
|
|
|
6
6
|
name: 'Timestamp',
|
|
7
7
|
displayName: 'Timestamp',
|
|
8
8
|
category: 'Content',
|
|
9
|
-
keywords: ['date', 'time', 'datetime', 'relative', 'ago', 'clock', 'format', 'duration'],
|
|
9
|
+
keywords: ['date', 'time', 'datetime', 'relative', 'ago', 'clock', 'format', 'duration', 'relativetime'],
|
|
10
10
|
playground: {
|
|
11
11
|
// `value` is required but has no semantic default, so the properties-tab
|
|
12
12
|
// preview fell back to the literal string "value" — which
|
|
@@ -6,6 +6,10 @@ export const docs = {
|
|
|
6
6
|
name: 'ToggleButtonGroup',
|
|
7
7
|
subComponentOf: 'ToggleButton',
|
|
8
8
|
displayName: 'Toggle Button Group',
|
|
9
|
+
keywords: [
|
|
10
|
+
'togglegroup',
|
|
11
|
+
'buttontogglegroup',
|
|
12
|
+
],
|
|
9
13
|
description: 'Groups toggle buttons for exclusive (single) or multi-select behavior. Uses discriminated union on type for type-safe value/onChange.',
|
|
10
14
|
props: [
|
|
11
15
|
{
|
package/src/postinstall.test.mjs
CHANGED
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
14
|
+
import {spawnSync} from 'node:child_process';
|
|
14
15
|
import * as fs from 'node:fs';
|
|
15
16
|
import * as os from 'node:os';
|
|
16
17
|
import * as path from 'node:path';
|
|
18
|
+
import {fileURLToPath} from 'node:url';
|
|
17
19
|
// Core's own package-local contract copy — what actually runs at install time.
|
|
18
20
|
import {
|
|
19
21
|
isAstryxInitialized,
|
|
22
|
+
isAstryxPromptInReach,
|
|
20
23
|
shouldNudge,
|
|
21
24
|
AGENT_DOC_PATHS,
|
|
22
25
|
INIT_MARKERS,
|
|
@@ -98,6 +101,49 @@ describe('core postinstall — one authoritative source, no drift', () => {
|
|
|
98
101
|
write('AGENTS.md', `# doc\n${MARKER}\nbody`);
|
|
99
102
|
expect(isAstryxInitialized(tmp)).toBe(cliLeaf.isAstryxInitialized(tmp));
|
|
100
103
|
});
|
|
104
|
+
|
|
105
|
+
it('agrees with the CLI leaf on a monorepo package', () => {
|
|
106
|
+
fs.mkdirSync(path.join(tmp, '.git'));
|
|
107
|
+
write('AGENTS.md', MARKER);
|
|
108
|
+
write('packages/app/package.json', '{"name":"app"}');
|
|
109
|
+
const app = path.join(tmp, 'packages/app');
|
|
110
|
+
expect(isAstryxPromptInReach(app)).toBe(true);
|
|
111
|
+
expect(isAstryxPromptInReach(app)).toBe(cliLeaf.isAstryxPromptInReach(app));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Install time, end to end: core's real script and its contract copy, installed
|
|
116
|
+
// under a monorepo's node_modules, run the way npm runs them for a package.
|
|
117
|
+
describe('core postinstall — inside a monorepo package', () => {
|
|
118
|
+
const CORE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
119
|
+
const install = () => {
|
|
120
|
+
const installed = path.join(tmp, 'node_modules/@astryxdesign/core/scripts');
|
|
121
|
+
fs.mkdirSync(installed, {recursive: true});
|
|
122
|
+
for (const file of ['postinstall.mjs', 'agent-doc-state.mjs']) {
|
|
123
|
+
fs.copyFileSync(path.join(CORE, 'scripts', file), path.join(installed, file));
|
|
124
|
+
}
|
|
125
|
+
return spawnSync(process.execPath, [path.join(installed, 'postinstall.mjs')], {
|
|
126
|
+
env: {...process.env, INIT_CWD: path.join(tmp, 'packages/app'), npm_command: 'install'},
|
|
127
|
+
encoding: 'utf8',
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
fs.mkdirSync(path.join(tmp, '.git'));
|
|
132
|
+
write('packages/app/package.json', '{"name":"app"}');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('stays quiet when init ran at the repository root', () => {
|
|
136
|
+
write('AGENTS.md', MARKER);
|
|
137
|
+
const r = install();
|
|
138
|
+
expect(r.status).toBe(0);
|
|
139
|
+
expect(r.stdout).toBe('');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('still nudges when no folder up to the repository root ran init', () => {
|
|
143
|
+
const r = install();
|
|
144
|
+
expect(r.status).toBe(0);
|
|
145
|
+
expect(r.stdout).toMatch(/finish setup and install the Astryx agent prompt/);
|
|
146
|
+
});
|
|
101
147
|
});
|
|
102
148
|
|
|
103
149
|
describe('core postinstall — shouldNudge', () => {
|