@astryxdesign/cli 0.7.12-canary.ccb4a6d → 0.7.12-canary.da3dbdc
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/api/component/__fixtures__/resolutions/modules.txt +5 -5
- package/api/docs/_adapter.d.mts +22 -0
- package/api/docs/_adapter.mjs +81 -3
- package/api/docs/docs.doc.mjs +5 -5
- package/api/docs/docs.type.d.mts +21 -0
- package/api/docs/docs.type.mjs +11 -0
- package/api/docs/list/list.d.mts +5 -1
- package/api/docs/list/list.mjs +72 -6
- package/api/docs/node/node.mjs +2 -40
- package/api/index.d.mts +1 -0
- package/api/index.mjs +1 -0
- package/api/render/_adapter.mjs +9 -0
- package/api/render/render.doc.mjs +2 -2
- package/api/search/_adapter.d.mts +22 -1
- package/api/search/_adapter.mjs +37 -20
- package/api/search/exportSearchIndex.doc.d.mts +10 -0
- package/api/search/exportSearchIndex.doc.mjs +58 -0
- package/api/search/rankSearchIndex.doc.d.mts +10 -0
- package/api/search/rankSearchIndex.doc.mjs +79 -0
- package/api/search/search-index.d.mts +44 -0
- package/api/search/search-index.mjs +270 -0
- package/api/search/search.type.d.mts +28 -0
- package/api/search/search.type.mjs +17 -0
- package/api/swizzle/_moduleGraph.d.mts +7 -0
- package/api/swizzle/_moduleGraph.mjs +191 -83
- package/api/swizzle/copy/copy.mjs +100 -9
- package/api/swizzle/swizzle.doc.mjs +7 -3
- package/api/swizzle/swizzle.type.d.mts +5 -0
- package/api/swizzle/swizzle.type.mjs +1 -0
- package/api/template/template.mjs +1 -0
- package/assets/discover/official-packages.json +16 -0
- package/assets/docs/browser-support.doc.mjs +2 -2
- package/assets/docs/styling-overview.doc.mjs +6 -2
- package/assets/docs/tree/render-pages.doc.mjs +2 -2
- package/assets/docs/tree/tailwind.doc.mjs +41 -1
- package/assets/docs/tree/type-scale.doc.mjs +19 -2
- package/assets/templates/blocks/components/Calendar/useCalendarConstraintsHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Calendar/useCalendarConstraintsHookUsage.tsx +146 -0
- package/assets/templates/blocks/components/Calendar/useCalendarNavigationHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Calendar/useCalendarNavigationHookUsage.tsx +173 -0
- package/assets/templates/blocks/components/Hooks/useInputStatusIconHookUsage.tsx +1 -0
- package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.tsx +56 -0
- package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.doc.mjs +2 -2
- package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.tsx +66 -10
- package/assets/templates/blocks/components/Link/useLinkifyHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Link/useLinkifyHookUsage.tsx +39 -0
- package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.tsx +83 -0
- package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.tsx +35 -0
- package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.tsx +97 -0
- package/assets/templates/pages/table-filter/page.tsx +15 -23
- package/assets/templates/pages/table-inbox/page.tsx +3 -6
- package/assets/templates/themes/butter/butterTheme.ts +2 -2
- package/assets/templates/themes/stone/stoneTheme.ts +1 -1
- package/assets/templates/themes/y2k/y2kTheme.ts +1 -1
- package/clients/cli/commands/docs.doc.mjs +5 -1
- package/clients/cli/commands/docs.mjs +30 -2
- package/clients/cli/commands/swizzle.doc.mjs +4 -0
- package/clients/cli/commands/swizzle.mjs +12 -0
- package/clients/cli/lib/resolve-theme.mjs +16 -7
- package/foundation/discovery/hook-discovery.mjs +31 -8
- package/foundation/doc-compiler/import.d.mts +5 -1
- package/foundation/doc-compiler/import.mjs +40 -6
- package/foundation/fs/import-url.d.mts +20 -0
- package/foundation/fs/import-url.mjs +22 -0
- package/foundation/fs/module-loader.d.mts +24 -3
- package/foundation/fs/module-loader.mjs +69 -47
- package/foundation/render/charts.d.mts +59 -0
- package/foundation/render/charts.mjs +216 -0
- package/foundation/render/embedded-imports.d.mts +7 -7
- package/foundation/render/embedded-imports.mjs +63 -54
- package/foundation/render/page-worker.mjs +339 -52
- package/foundation/render/render-page.mjs +26 -10
- package/foundation/render/ts-paths.d.mts +63 -0
- package/foundation/render/ts-paths.mjs +138 -0
- package/foundation/response/error-codes.doc.mjs +2 -2
- package/foundation/response/error-codes.mjs +3 -2
- package/foundation/text/levenshtein.d.mts +6 -1
- package/foundation/text/levenshtein.mjs +25 -9
- package/foundation/text/suggest-order.d.mts +8 -1
- package/foundation/text/suggest-order.mjs +72 -6
- package/package.json +7 -6
|
@@ -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
|
|
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,
|
|
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',
|
|
@@ -90,7 +90,7 @@ export const docs = {
|
|
|
90
90
|
{
|
|
91
91
|
type: 'code',
|
|
92
92
|
lang: 'bash',
|
|
93
|
-
label: 'Install
|
|
93
|
+
label: 'Install',
|
|
94
94
|
code: '{@pm install @astryxdesign/core @stylexjs/stylex @astryxdesign/theme-neutral}',
|
|
95
95
|
},
|
|
96
96
|
{
|
|
@@ -113,7 +113,7 @@ export const docs = {
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
type: 'prose',
|
|
116
|
-
text: 'For Tailwind v3, see the layer workaround in {@link generic:migration-setup}.',
|
|
116
|
+
text: 'A project with Tailwind already set up needs a smaller change, which {@link generic:tailwind} shows. For Tailwind v3, see the layer workaround in {@link generic:migration-setup}.',
|
|
117
117
|
},
|
|
118
118
|
],
|
|
119
119
|
},
|
|
@@ -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
|
{
|
|
@@ -57,7 +57,7 @@ export const docs = {
|
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
type: 'prose',
|
|
60
|
-
text: 'What it cannot keep: anything that needs a browser while running. State, effects, click handlers and data loaded after the first paint are gone
|
|
60
|
+
text: 'What it cannot keep: anything that needs a browser while running. State, effects, click handlers and data loaded after the first paint are gone. Charts made with recharts are the exception: the render draws each chart and ResponsiveContainer a module of the page (not an installed package) imports from recharts by name (`import {LineChart} from \'recharts\'`), inside the page, so a custom tick, label or shape sees the page\'s React context. A chart sized by its container scales to the width the page gives it, and its legend stays out of the picture: a horizontal legend takes its own row under it (above it when the legend asks for the top), and a vertical legend on one side sits beside it, on that side; in a chart of fixed size, a legend keeps the place its props give it. Charts show the data and props of the page\'s first render, before its effects run: when the page cannot be drawn again without a browser, or its effects change its charts or re-render the component that makes them, each chart is drawn by itself and its parts see React context defaults. A value a chart\'s parts read from a React context that an effect changes may still show its later value. A `chart-size` warning counts the charts the render cannot draw (one used through `import * as R`, say), and says when charts were drawn by themselves. Read browser-only values such as window inside an effect, or the render stops with ERR_RENDER_BROWSER_API.',
|
|
61
61
|
},
|
|
62
62
|
],
|
|
63
63
|
},
|
|
@@ -86,7 +86,7 @@ export const docs = {
|
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
type: 'prose',
|
|
89
|
-
text: 'It writes the page, the modules, stylesheets and JSON files the page reaches through its own imports, and the images, fonts and media the render carried (however the page named them, as long as their bytes really are one): each goes where the page\'s relative imports find it from wherever you put the page, and a file the page imports through a path alias (`@/components/Header`) goes where your project\'s `tsconfig.json` or `jsconfig.json` alias points. Every file but the page goes inside the one new folder the restore creates (the topmost folder on the page\'s path that does not exist yet), never into a folder that already exists: a page that imports other files comes back into a new folder (`--out restored/app/page.tsx`), and a file it imports through a path alias comes back only when that alias points inside the new folder. Nor does it write a file that a test, story or benchmark runner would run by its default patterns (Jest, Vitest, Bun, AVA, Playwright, Storybook, Cypress and `node --test`: names such as `*.test.*`, `*.spec.*`, `*_test.*`, `*-test.*`, `*.bench.*`, `*.stories.*`, `*.cy.*`, and anything under `test/`, `tests/` or `__tests__/`), judged from the repository root (every folder above the file up to it), else the topmost folder with a package.json. Restored files are the sender\'s code: read them before you run or import them. Without `--out`, everything comes back into a new folder beside the HTML file, named after it (`page.html` comes back into `page-restored/`). A received file that carries anything else (a file the page never imports, a dotfile, `package.json`, a tool configuration, a path that leaves the folder) is refused with nothing written. An existing file is never replaced unless you pass `--overwrite`, and the refusal names every one; a generated, vendored or ignored file, and any path through a symbolic link, is not written even then. Add `--json` for the result as data: what was written, how big it is, and every warning.',
|
|
89
|
+
text: 'It writes the page, the modules, stylesheets and JSON files the page reaches through its own imports, and the images, fonts and media the render carried (however the page named them, as long as their bytes really are one): each goes where the page\'s relative imports find it from wherever you put the page, and a file the page imports through a path alias (`@/components/Header`) goes where your project\'s `tsconfig.json` or `jsconfig.json` alias points. Every file but the page goes inside the one new folder the restore creates (the topmost folder on the page\'s path that does not exist yet), never into a folder that already exists: a page that imports other files comes back into a new folder (`--out restored/app/page.tsx`), and a file it imports through a path alias comes back only when that alias points inside the new folder. Nor does it write a file that a test, story or benchmark runner would run by its default patterns (Jest, Vitest, Bun, AVA, Playwright, Storybook, Cypress and `node --test`: names such as `*.test.*`, `*.spec.*`, `*_test.*`, `*-test.*`, `*.bench.*`, `*.stories.*`, `*.cy.*`, and anything under `test/`, `tests/` or `__tests__/`), judged from the repository root (every folder above the file up to it), else the topmost folder with a package.json. Restored files are the sender\'s code: read them before you run or import them. Without `--out`, everything comes back into a new folder beside the HTML file, named after it (`page.html` comes back into `page-restored/`). A received file that carries anything else (a file the page never imports, a dotfile, `package.json`, a tool configuration, a path that leaves the folder) is refused with nothing written. A rendered file that lost its embedded source on the way (a sanitizer or an editor strips `<script>` blocks) and has no Astryx components to rebuild from is refused with `ERR_RENDER_SOURCE_DAMAGED`. An existing file is never replaced unless you pass `--overwrite`, and the refusal names every one; a generated, vendored or ignored file, and any path through a symbolic link, is not written even then. Add `--json` for the result as data: what was written, how big it is, and every warning.',
|
|
90
90
|
},
|
|
91
91
|
],
|
|
92
92
|
},
|
|
@@ -19,6 +19,8 @@ export const docs = {
|
|
|
19
19
|
'tailwind-theme.css',
|
|
20
20
|
'utility classes',
|
|
21
21
|
'preflight',
|
|
22
|
+
'existing Tailwind app',
|
|
23
|
+
'cascade layer order',
|
|
22
24
|
],
|
|
23
25
|
|
|
24
26
|
sections: [
|
|
@@ -69,7 +71,45 @@ import {themes, defaultThemeSlug} from './astryx-themes';`,
|
|
|
69
71
|
},
|
|
70
72
|
{
|
|
71
73
|
type: 'prose',
|
|
72
|
-
text:
|
|
74
|
+
text: "The Tailwind bridge is the concrete example of the general interop pattern: expose another library's semantic API, but point the values at system token variables.",
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
title: 'Add Astryx to an app that already uses Tailwind',
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: 'prose',
|
|
83
|
+
text: 'The recipe in the Tailwind section is a whole `globals.css` for a new app. An app that already uses Tailwind keeps its own file: its `@import "tailwindcss"`, its `@theme` mapping, and its `:root` variables stay as they are. Add one `@layer` line above the Tailwind import and the Astryx stylesheets below it.',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'code',
|
|
87
|
+
lang: 'css',
|
|
88
|
+
label: 'globals.css (existing Tailwind app)',
|
|
89
|
+
code: `@layer reset, theme, base, astryx-base, astryx-theme, components, utilities;
|
|
90
|
+
|
|
91
|
+
@import "tailwindcss"; /* the app's own entry, unchanged */
|
|
92
|
+
@import "@astryxdesign/core/reset.css";
|
|
93
|
+
@import "@astryxdesign/core/astryx.css";
|
|
94
|
+
@import "@astryxdesign/theme-neutral/theme.css";
|
|
95
|
+
|
|
96
|
+
/* The app's own @theme and :root rules stay below, unchanged. */`,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'prose',
|
|
100
|
+
text: "This recipe leaves out `tailwind-theme.css`. The bridge maps Tailwind theme names to system tokens, including Tailwind's own defaults such as `rounded-lg` and `shadow-md`, so in an existing app it changes what the app's own classes paint. Add it only when the app does not use the names it maps.",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: 'prose',
|
|
104
|
+
text: "Astryx's theme defines its own CSS custom properties (color, spacing, radius, shadow, font, motion and more), and {@link namespace:tokens} has the full list. An app token with one of those names collides in one of two ways. An app `@theme` key takes Astryx's value, because `astryx.css` sets the tokens on `:root` above Tailwind's `theme` layer: with `@theme { --spacing-4: 1.5rem }`, the app's `gap-4` stays 16px. Marking the block `@theme inline` keeps the app's value in its utilities. An app `:root` variable replaces Astryx's value, so Astryx's own components change: `:root { --spacing-4: 1.5rem }` widens Astryx's spacing step 4 from 16px to 24px. Where the root Theme component sets a token again, such as `--color-warning`, the app's own rules inside it read Astryx's value instead. Rename any app token that shares a name.",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'prose',
|
|
108
|
+
text: "Put the `@layer` line above the app's `@import \"tailwindcss\"`, never below it. A `@layer` statement orders only the layer names the page has not seen yet, and the Tailwind import names `theme`, `base`, `components`, and `utilities` as it loads. Below that import, the statement can only add `reset`, `astryx-base`, and `astryx-theme` after Tailwind's `utilities`. That inverts the cascade: the Astryx reset then outranks every utility class in the app, and, for example, removes the borders and padding the app's utilities give its controls. Nothing reports it: the build and the browser console stay clean, and the `@layer` line still looks right in the source, so only the rendered page shows the problem.",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
type: 'prose',
|
|
112
|
+
text: 'The load-order rule from the Tailwind section still applies: `globals.css` must be the first stylesheet the page loads, so its `@layer` line names the layers first.',
|
|
73
113
|
},
|
|
74
114
|
],
|
|
75
115
|
},
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file `astryx docs typography/type-scale`: line height, semantic type scale,
|
|
5
|
-
* display text, headings and text components,
|
|
5
|
+
* display text, headings and text components, custom type scales, and the
|
|
6
|
+
* reader's text size on iOS.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
/** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */
|
|
@@ -14,13 +15,15 @@ export const docs = {
|
|
|
14
15
|
placement: {parent: 'namespace:typography', slot: 'guides', order: 20},
|
|
15
16
|
category: 'foundations',
|
|
16
17
|
description:
|
|
17
|
-
|
|
18
|
+
"Line height, semantic type scale tokens, display text, Heading and Text components, custom type scales via defineTheme, and following the reader's text size on iOS.",
|
|
18
19
|
keywords: [
|
|
19
20
|
'line height',
|
|
20
21
|
'leading',
|
|
21
22
|
'defineTheme',
|
|
22
23
|
'base',
|
|
23
24
|
'ratio',
|
|
25
|
+
'dynamic type',
|
|
26
|
+
'text size',
|
|
24
27
|
],
|
|
25
28
|
|
|
26
29
|
sections: [
|
|
@@ -143,5 +146,19 @@ const denseTheme = defineTheme({
|
|
|
143
146
|
},
|
|
144
147
|
],
|
|
145
148
|
},
|
|
149
|
+
{
|
|
150
|
+
id: 'dynamic-type',
|
|
151
|
+
title: "Following the reader's text size on iOS",
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: 'prose',
|
|
155
|
+
text: "On iPhone, readers pick a text size in Settings (Display & Text Size, then Larger Text), and native apps grow or shrink their text with it. Every theme does the same in Safari on iPhone, which exposes that choice through `font: -apple-system-body`: each font size follows the reader's choice on the curve of the iOS text style it plays, so body text scales like Body, `heading-1` like Title 1 and display text like Large Title, and every role keeps its order. A size the theme fixes at a length, such as a 12px supporting size, scales on the curve of the step it reads by default; a size in `em` or `%` follows its parent, and one that reads a variable follows that variable. At the default size every font size is the theme's own. The family, spacing, radius and the root font size never change, line heights keep their ratios, text inputs keep their 16px floor on iOS, and on Android the system font scale grows every font size through `env(preferred-text-scale)` where the browser reports it; browsers with neither see no change. There is nothing to turn on: no theme option, prop or document attribute.",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: 'prose',
|
|
159
|
+
text: 'Fixed-height controls such as the text inputs keep their height, so their text can overflow at the accessibility sizes. A labelled `Button` grows with its label.',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
146
163
|
],
|
|
147
164
|
};
|
|
@@ -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
|
+
}
|
|
@@ -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: 'useInteractiveRole',
|
|
7
|
+
name: 'useInteractiveRole — One Tile, Four Roles',
|
|
8
|
+
displayName: 'useInteractiveRole — One Tile, Four Roles',
|
|
9
|
+
description:
|
|
10
|
+
'A polymorphic Tile used four ways: with an href it renders as a link, with an onClick as a button, with neither as plain content, and placed as a Popover trigger it becomes the trigger button through context — the resolution order in one place.',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 4 / 3,
|
|
13
|
+
componentsUsed: ['Popover', 'Card', 'Text', 'Layout', 'Link'],
|
|
14
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import {useInteractiveRole} from '@astryxdesign/core/hooks';
|
|
6
|
+
import {Link} from '@astryxdesign/core/Link';
|
|
7
|
+
import {Popover} from '@astryxdesign/core/Popover';
|
|
8
|
+
import {Card} from '@astryxdesign/core/Card';
|
|
9
|
+
import {Text} from '@astryxdesign/core/Text';
|
|
10
|
+
import {VStack} from '@astryxdesign/core/Layout';
|
|
11
|
+
|
|
12
|
+
// A polymorphic Tile: the hook alone decides what it renders as — href wins,
|
|
13
|
+
// then onClick, then a trigger context (Popover), then inert.
|
|
14
|
+
function Tile({
|
|
15
|
+
href,
|
|
16
|
+
onClick,
|
|
17
|
+
children,
|
|
18
|
+
}: {
|
|
19
|
+
href?: string;
|
|
20
|
+
onClick?: () => void;
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}) {
|
|
23
|
+
const role = useInteractiveRole({href, onClick});
|
|
24
|
+
const body = (
|
|
25
|
+
<Card padding={3} width={200}>
|
|
26
|
+
<Text type="body">{children}</Text>
|
|
27
|
+
</Card>
|
|
28
|
+
);
|
|
29
|
+
switch (role) {
|
|
30
|
+
case 'link':
|
|
31
|
+
return <Link href={href}>{body}</Link>;
|
|
32
|
+
case 'button':
|
|
33
|
+
return <button onClick={onClick}>{body}</button>;
|
|
34
|
+
default:
|
|
35
|
+
return body;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function UseInteractiveRoleHookUsage() {
|
|
40
|
+
return (
|
|
41
|
+
<VStack gap={3} hAlign="center" style={{width: '100%'}}>
|
|
42
|
+
<Tile href="https://example.com/docs">Renders as a link</Tile>
|
|
43
|
+
<Tile onClick={() => {}}>Renders as a button</Tile>
|
|
44
|
+
<Popover
|
|
45
|
+
label="Tile details"
|
|
46
|
+
content={
|
|
47
|
+
<Card padding={3}>
|
|
48
|
+
<Text type="body">Opened through the tile's context role</Text>
|
|
49
|
+
</Card>
|
|
50
|
+
}>
|
|
51
|
+
<Tile>Trigger via context</Tile>
|
|
52
|
+
</Popover>
|
|
53
|
+
<Tile>Plain content</Tile>
|
|
54
|
+
</VStack>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -7,8 +7,8 @@ export const doc = {
|
|
|
7
7
|
name: 'useStreamingText — Streaming Response',
|
|
8
8
|
displayName: 'useStreamingText — Streaming Response',
|
|
9
9
|
description:
|
|
10
|
-
'
|
|
10
|
+
'A chat-style reply card: while the response streams in bursts, useStreamingText reveals it steadily character by character; when streaming ends the full text snaps into place. Regenerate replays the stream.',
|
|
11
11
|
isReady: true,
|
|
12
12
|
aspectRatio: 4 / 3,
|
|
13
|
-
componentsUsed: ['Card', 'Text', 'Layout'],
|
|
13
|
+
componentsUsed: ['Button', 'Card', 'Text', 'Layout'],
|
|
14
14
|
};
|