@astryxdesign/core 0.4.6 → 0.4.7-canary.0b6762d
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 +35 -0
- package/dist/AspectRatio/AspectRatio.d.ts +24 -0
- package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
- package/dist/AspectRatio/AspectRatio.js +23 -4
- package/dist/BottomSheet/BottomSheet.d.ts +1 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +54 -15
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +6 -0
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetEdgeTint.js +62 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +10 -4
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +23 -5
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +5 -5
- package/dist/Calendar/Calendar.d.ts +3 -1
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +13 -14
- package/dist/Calendar/getInitialFocusDate.d.ts +46 -0
- package/dist/Calendar/getInitialFocusDate.d.ts.map +1 -0
- package/dist/Calendar/getInitialFocusDate.js +62 -0
- package/dist/Chat/ChatMessageList.d.ts +22 -3
- package/dist/Chat/ChatMessageList.d.ts.map +1 -1
- package/dist/Chat/ChatMessageList.js +6 -3
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +34 -1
- package/dist/Dialog/Dialog.d.ts +7 -1
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +48 -22
- package/dist/Heading/Heading.d.ts.map +1 -1
- package/dist/Heading/Heading.js +6 -2
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +10 -3
- package/dist/Markdown/index.d.ts +1 -1
- package/dist/Markdown/index.d.ts.map +1 -1
- package/dist/Markdown/parser.d.ts +38 -1
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +149 -23
- package/dist/MultiSelector/MultiSelector.d.ts +15 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +17 -6
- package/dist/MultiSelector/index.d.ts +1 -1
- package/dist/MultiSelector/index.d.ts.map +1 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +5 -0
- package/dist/Stepper/Step.d.ts +140 -0
- package/dist/Stepper/Step.d.ts.map +1 -0
- package/dist/Stepper/Step.js +1045 -0
- package/dist/Stepper/StepStatus.d.ts +24 -0
- package/dist/Stepper/StepStatus.d.ts.map +1 -0
- package/dist/Stepper/StepStatus.js +1 -0
- package/dist/Stepper/Stepper.d.ts +93 -0
- package/dist/Stepper/Stepper.d.ts.map +1 -0
- package/dist/Stepper/Stepper.js +184 -0
- package/dist/Stepper/StepperContext.d.ts +41 -0
- package/dist/Stepper/StepperContext.d.ts.map +1 -0
- package/dist/Stepper/StepperContext.js +34 -0
- package/dist/Stepper/index.d.ts +8 -0
- package/dist/Stepper/index.d.ts.map +1 -0
- package/dist/Stepper/index.js +7 -0
- package/dist/Stepper/stepper.stylex.d.ts +15 -0
- package/dist/Stepper/stepper.stylex.d.ts.map +1 -0
- package/dist/Stepper/stepper.stylex.js +20 -0
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +5 -1
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +4 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +20 -8
- package/dist/Table/plugins/pagination/useTablePagination.d.ts +3 -0
- package/dist/Table/plugins/pagination/useTablePagination.d.ts.map +1 -1
- package/dist/Table/plugins/pagination/useTablePagination.js +21 -4
- package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -1
- package/dist/Table/plugins/rowStatus/useTableRowStatus.js +10 -3
- package/dist/Table/plugins/selection/useTableSelection.d.ts +16 -0
- package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
- package/dist/Table/plugins/selection/useTableSelection.js +19 -5
- package/dist/Table/types.d.ts +22 -4
- package/dist/Table/types.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.js +5 -0
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +6 -2
- package/dist/astryx.css +43 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useHotkeys.d.ts.map +1 -1
- package/dist/hooks/useHotkeys.js +10 -3
- package/dist/hooks/useListFocus.d.ts +5 -2
- package/dist/hooks/useListFocus.d.ts.map +1 -1
- package/dist/hooks/useListFocus.js +12 -6
- package/dist/hooks/useMergedRefs.d.ts +18 -0
- package/dist/hooks/useMergedRefs.d.ts.map +1 -0
- package/dist/hooks/useMergedRefs.js +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/utils/timeParser.d.ts +1 -1
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +26 -12
- package/locales/en.json +23 -7
- package/locales/pseudo.json +12 -0
- package/package.json +8 -3
- package/src/AspectRatio/AspectRatio.doc.mjs +6 -3
- package/src/AspectRatio/AspectRatio.test.tsx +57 -10
- package/src/AspectRatio/AspectRatio.tsx +40 -1
- package/src/Avatar/Avatar.doc.mjs +2 -1
- package/src/BottomSheet/BottomSheet.test.tsx +123 -0
- package/src/BottomSheet/BottomSheet.tsx +36 -4
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +225 -0
- package/src/BottomSheet/BottomSheetEdgeTint.tsx +82 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +66 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +19 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +13 -0
- package/src/BottomSheet/useSheetGestures.test.ts +27 -0
- package/src/BottomSheet/useSheetGestures.ts +25 -5
- package/src/Breadcrumbs/BreadcrumbItem.tsx +2 -1
- package/src/Button/Button.doc.mjs +22 -0
- package/src/Calendar/Calendar.doc.mjs +4 -3
- package/src/Calendar/Calendar.test.tsx +52 -0
- package/src/Calendar/Calendar.tsx +18 -15
- package/src/Calendar/getInitialFocusDate.test.ts +94 -0
- package/src/Calendar/getInitialFocusDate.ts +93 -0
- package/src/Chat/ChatMessageList.doc.mjs +9 -1
- package/src/Chat/ChatMessageList.test.tsx +46 -0
- package/src/Chat/ChatMessageList.tsx +28 -4
- package/src/CodeBlock/CodeBlock.doc.mjs +6 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +5 -0
- package/src/DateInput/DateInputTouch.test.tsx +36 -0
- package/src/DateInput/TouchDateField.tsx +35 -1
- package/src/Dialog/Dialog.doc.mjs +8 -3
- package/src/Dialog/Dialog.test.tsx +71 -0
- package/src/Dialog/Dialog.tsx +72 -20
- package/src/Field/Field.doc.mjs +11 -0
- package/src/Heading/Heading.test.tsx +23 -1
- package/src/Heading/Heading.tsx +6 -2
- package/src/Kbd/Kbd.test.tsx +17 -0
- package/src/Kbd/Kbd.tsx +10 -3
- package/src/Link/Link.doc.mjs +11 -0
- package/src/Markdown/index.ts +1 -0
- package/src/Markdown/parser.perf.test.ts +71 -1
- package/src/Markdown/parser.test.ts +145 -2
- package/src/Markdown/parser.ts +208 -22
- package/src/MoreMenu/MoreMenu.doc.mjs +7 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +10 -0
- package/src/MultiSelector/MultiSelector.test.tsx +105 -0
- package/src/MultiSelector/MultiSelector.tsx +50 -6
- package/src/MultiSelector/index.ts +1 -0
- package/src/NumberInput/NumberInput.doc.mjs +5 -0
- package/src/Popover/Popover.test.tsx +27 -1
- package/src/PowerSearch/PowerSearch.doc.mjs +10 -0
- package/src/Selector/Selector.doc.mjs +11 -0
- package/src/Selector/Selector.test.tsx +21 -0
- package/src/Selector/Selector.tsx +11 -1
- package/src/Stepper/Step.doc.mjs +98 -0
- package/src/Stepper/Step.tsx +1638 -0
- package/src/Stepper/StepStatus.ts +26 -0
- package/src/Stepper/Stepper.doc.mjs +370 -0
- package/src/Stepper/Stepper.test.tsx +1024 -0
- package/src/Stepper/Stepper.tsx +258 -0
- package/src/Stepper/StepperContext.ts +71 -0
- package/src/Stepper/index.ts +18 -0
- package/src/Stepper/stepper.stylex.ts +19 -0
- package/src/TabList/Tab.tsx +5 -1
- package/src/TabList/TabList.test.tsx +21 -4
- package/src/Table/BaseTable.tsx +6 -3
- package/src/Table/Table.test.tsx +35 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +112 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +100 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +17 -8
- package/src/Table/plugins/pagination/useTablePagination.test.tsx +34 -4
- package/src/Table/plugins/pagination/useTablePagination.tsx +35 -3
- package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +13 -7
- package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +12 -3
- package/src/Table/plugins/selection/useTableSelection.test.tsx +76 -0
- package/src/Table/plugins/selection/useTableSelection.tsx +40 -7
- package/src/Table/types.ts +22 -4
- package/src/Table/useBaseTablePlugins.ts +5 -0
- package/src/Table/useTableGroupedRows.doc.mjs +5 -4
- package/src/Table/useTablePagination.doc.mjs +7 -0
- package/src/Table/useTableRowStatus.doc.mjs +1 -1
- package/src/Table/useTableSelection.doc.mjs +31 -0
- package/src/Text/Text.test.tsx +23 -1
- package/src/Text/Text.tsx +6 -2
- package/src/TextInput/TextInput.doc.mjs +10 -0
- package/src/Toast/Toast.doc.mjs +6 -0
- package/src/Tokenizer/Tokenizer.doc.mjs +10 -0
- package/src/Toolbar/Toolbar.doc.mjs +5 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useHotkeys.test.ts +18 -0
- package/src/hooks/useHotkeys.ts +10 -3
- package/src/hooks/useListFocus.doc.mjs +2 -2
- package/src/hooks/useListFocus.test.tsx +65 -3
- package/src/hooks/useListFocus.ts +15 -7
- package/src/hooks/useMergedRefs.doc.mjs +79 -0
- package/src/hooks/useMergedRefs.test.tsx +62 -0
- package/src/hooks/useMergedRefs.ts +36 -0
- package/src/index.ts +1 -0
- package/src/theme/MediaTheme.doc.mjs +5 -5
- package/src/utils/timeParser.test.ts +6 -1
- package/src/utils/timeParser.ts +35 -13
- package/src/__tests__/inputWidthContract.test.tsx +0 -69
|
@@ -530,6 +530,33 @@ describe('useSheetGestures', () => {
|
|
|
530
530
|
expect(hook.result.current.dragOffset).toBe(40);
|
|
531
531
|
});
|
|
532
532
|
|
|
533
|
+
/**
|
|
534
|
+
* A finger is never still. Tapping a control inside the sheet drifts a
|
|
535
|
+
* pixel or two, and with no slop that drift promoted to a sheet drag —
|
|
536
|
+
* which wrote `transition: none` onto the panel across the release, so the
|
|
537
|
+
* close the tap triggered cut instead of animating. That is the "the sheet
|
|
538
|
+
* closes with no animation" report: only on a tap, and only on a tap
|
|
539
|
+
* inside the sheet (the scrim never arms a drag).
|
|
540
|
+
*/
|
|
541
|
+
it('treats a few pixels of finger drift as a tap, not a drag', () => {
|
|
542
|
+
const {hook} = setup({snapHeights: () => [200]});
|
|
543
|
+
const body = makeBody(0); // at the top, where a pull-down would promote
|
|
544
|
+
bodyDown(hook, 0, 0, body);
|
|
545
|
+
bodyMove(hook, 3, 40, body); // the drift of a finger tapping a button
|
|
546
|
+
expect(hook.result.current.isDragging).toBe(false);
|
|
547
|
+
// and nothing suppressed the panel's transition, so a close that lands
|
|
548
|
+
// now still animates
|
|
549
|
+
expect(hook.result.current.contentProps.style.transition).toBeUndefined();
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('still promotes once the pull passes the tap slop', () => {
|
|
553
|
+
const {hook} = setup({snapHeights: () => [200]});
|
|
554
|
+
const body = makeBody(0);
|
|
555
|
+
bodyDown(hook, 0, 0, body);
|
|
556
|
+
bodyMove(hook, 9, 40, body); // just past the 8px slop
|
|
557
|
+
expect(hook.result.current.isDragging).toBe(true);
|
|
558
|
+
});
|
|
559
|
+
|
|
533
560
|
it('does not hijack scrolling when the body is scrolled down', () => {
|
|
534
561
|
const {hook} = setup({snapHeights: () => [200]});
|
|
535
562
|
const body = makeBody(120); // scrolled, not at the top
|
|
@@ -118,6 +118,20 @@ const OVERSCROLL_MAX = 48;
|
|
|
118
118
|
// the scroll, large enough that a swipe merely coming to rest on the last pixel
|
|
119
119
|
// doesn't start a drag on jitter.
|
|
120
120
|
const CONTENT_END_HANDOFF_SLOP = 4;
|
|
121
|
+
// How far a finger resting on the body must travel before the pull becomes a
|
|
122
|
+
// sheet drag rather than a tap.
|
|
123
|
+
//
|
|
124
|
+
// This used to be zero: any downward movement promoted. A finger is never
|
|
125
|
+
// still, so tapping a control inside the sheet drifted a pixel or two and
|
|
126
|
+
// started a drag -- which suppresses the panel's transition, correctly, for as
|
|
127
|
+
// long as the sheet is tracking the finger. The close that the tap triggered
|
|
128
|
+
// then landed inside that window and cut instead of animating. A deliberate
|
|
129
|
+
// pull was unaffected; only a tap, and only a tap inside the sheet, since the
|
|
130
|
+
// scrim is outside the body and arms nothing.
|
|
131
|
+
//
|
|
132
|
+
// 8px is the conventional tap slop, a shade over iOS's own recognizer, and
|
|
133
|
+
// well under what a real pull covers in its first frames.
|
|
134
|
+
const DRAG_PROMOTION_SLOP = 8;
|
|
121
135
|
|
|
122
136
|
// Short haptic tick on detent settle where supported. iOS Safari doesn't
|
|
123
137
|
// expose navigator.vibrate, so this is a no-op there; skipped under
|
|
@@ -1140,7 +1154,7 @@ export function useSheetGestures({
|
|
|
1140
1154
|
return;
|
|
1141
1155
|
}
|
|
1142
1156
|
const delta = event.clientY - armed.startCoord;
|
|
1143
|
-
if (delta >
|
|
1157
|
+
if (delta > DRAG_PROMOTION_SLOP && armed.scroller.scrollTop <= 0) {
|
|
1144
1158
|
// Downward pull at the top: promote to a sheet drag, anchored at the
|
|
1145
1159
|
// original pointer-down position so the pull distance carries over.
|
|
1146
1160
|
armedBodyRef.current = null;
|
|
@@ -1302,8 +1316,10 @@ export function useSheetGestures({
|
|
|
1302
1316
|
// no longer scroll that way: at the top, a downward pull (delta > 0)
|
|
1303
1317
|
// collapses; at the bottom, an upward pull (delta < 0) expands. The
|
|
1304
1318
|
// opposite direction is a real scroll, so disarm and let it through.
|
|
1305
|
-
const pullDownAtTop =
|
|
1306
|
-
|
|
1319
|
+
const pullDownAtTop =
|
|
1320
|
+
armed.top && delta > DRAG_PROMOTION_SLOP && atTop(scroller);
|
|
1321
|
+
const pullUpAtBottom =
|
|
1322
|
+
armed.bottom && delta < -DRAG_PROMOTION_SLOP && atBottom(scroller);
|
|
1307
1323
|
if (pullDownAtTop || pullUpAtBottom) {
|
|
1308
1324
|
event.preventDefault();
|
|
1309
1325
|
touchDragRef.current = null;
|
|
@@ -1433,15 +1449,19 @@ export function useSheetGestures({
|
|
|
1433
1449
|
style: {
|
|
1434
1450
|
transform:
|
|
1435
1451
|
activeOffset !== 0 ? `translateY(${activeOffset}px)` : undefined,
|
|
1452
|
+
// Gated on the sheet being up: suppression must not outlive the sheet
|
|
1453
|
+
// it was suppressing for. The host swaps the panel to its closing
|
|
1454
|
+
// state in whatever frame the dismissal lands, and a stale `none`
|
|
1455
|
+
// would apply to that transform too, cutting the exit.
|
|
1436
1456
|
transition:
|
|
1437
|
-
isDragging || isScrollAreaReconciling || reducedMotion
|
|
1457
|
+
isOpen && (isDragging || isScrollAreaReconciling || reducedMotion)
|
|
1438
1458
|
? 'none'
|
|
1439
1459
|
: undefined,
|
|
1440
1460
|
touchAction: 'none',
|
|
1441
1461
|
overscrollBehavior: 'contain',
|
|
1442
1462
|
},
|
|
1443
1463
|
}),
|
|
1444
|
-
[activeOffset, isDragging, isScrollAreaReconciling, reducedMotion],
|
|
1464
|
+
[activeOffset, isDragging, isOpen, isScrollAreaReconciling, reducedMotion],
|
|
1445
1465
|
);
|
|
1446
1466
|
|
|
1447
1467
|
const handleProps = useMemo<SheetHandleProps>(
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
spacingVars,
|
|
39
39
|
typeScaleVars,
|
|
40
40
|
radiusVars,
|
|
41
|
+
fontWeightVars,
|
|
41
42
|
} from '../theme/tokens.stylex';
|
|
42
43
|
import {BreadcrumbContext, type BreadcrumbsVariant} from './Breadcrumbs';
|
|
43
44
|
import {useLinkComponent} from '../Link/useLinkComponent';
|
|
@@ -186,7 +187,7 @@ const itemStyles = stylex.create({
|
|
|
186
187
|
color: colorVars['--color-text-secondary'],
|
|
187
188
|
},
|
|
188
189
|
current: {
|
|
189
|
-
fontWeight: '
|
|
190
|
+
fontWeight: fontWeightVars['--font-weight-semibold'],
|
|
190
191
|
},
|
|
191
192
|
defaultCurrent: {
|
|
192
193
|
color: colorVars['--color-text-primary'],
|
|
@@ -150,6 +150,28 @@ export const docs = {
|
|
|
150
150
|
description:
|
|
151
151
|
'Async click handler. Shows loading state while the returned promise is pending.',
|
|
152
152
|
},
|
|
153
|
+
{
|
|
154
|
+
name: 'href',
|
|
155
|
+
type: 'string',
|
|
156
|
+
description:
|
|
157
|
+
'When provided, renders the button as a link element (<a> or custom link component).',
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'as',
|
|
161
|
+
type: 'ComponentType',
|
|
162
|
+
description:
|
|
163
|
+
'Custom link component to use when href is provided (e.g. Next.js Link).',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'target',
|
|
167
|
+
type: 'string',
|
|
168
|
+
description: 'HTML target attribute when rendered as a link (e.g. "_blank").',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'rel',
|
|
172
|
+
type: 'string',
|
|
173
|
+
description: 'HTML rel attribute when rendered as a link (e.g. "noopener noreferrer").',
|
|
174
|
+
},
|
|
153
175
|
],
|
|
154
176
|
playground: {
|
|
155
177
|
defaults: {
|
|
@@ -83,7 +83,8 @@ export const docs = {
|
|
|
83
83
|
{
|
|
84
84
|
name: 'focusDate',
|
|
85
85
|
type: 'ISODateString',
|
|
86
|
-
description:
|
|
86
|
+
description:
|
|
87
|
+
'Controlled visible month. Unset, the calendar opens on the selected date, else on today clamped into the min/max window.',
|
|
87
88
|
},
|
|
88
89
|
{
|
|
89
90
|
name: 'onFocusDateChange',
|
|
@@ -156,7 +157,7 @@ export const docsZh = {
|
|
|
156
157
|
{name: 'dateConstraints', type: 'Array<(date: Date) => boolean>', description: '自定义约束函数。'},
|
|
157
158
|
{name: 'maxRangeSpan', type: 'number', description: '范围模式:范围最多可跨越的天数,含首尾两端(7 = 7 天窗口)。选定起始日后限制窗口大小。'},
|
|
158
159
|
{name: 'minRangeSpan', type: 'number', description: '范围模式:范围最少需跨越的天数,含首尾两端(2 表示禁止单日范围)。默认为 1。'},
|
|
159
|
-
{name: 'focusDate', type: 'ISODateString', description: '
|
|
160
|
+
{name: 'focusDate', type: 'ISODateString', description: '受控可见月份。未设置时,日历打开时显示已选日期所在月份;若无选中值,则显示今天,并将其限制在 min/max 范围内。'},
|
|
160
161
|
{name: 'onFocusDateChange', type: '(focusDate: ISODateString) => void', description: '导航回调函数。'},
|
|
161
162
|
{name: 'handleRef', type: 'React.Ref<CalendarHandle>', description: '日历导航的命令式句柄,包括 navigateTo()。'},
|
|
162
163
|
{name: 'hasOutsideDays', type: 'boolean', description: '显示相邻月份的日期。', default: 'true'},
|
|
@@ -221,7 +222,7 @@ export const docsDense = {
|
|
|
221
222
|
dateConstraints: 'custom constraint fns',
|
|
222
223
|
maxRangeSpan: 'range mode: max days a range may span, both ends counted (7 = 7-day window)',
|
|
223
224
|
minRangeSpan: 'range mode: min days a range must span, both ends counted (default 1)',
|
|
224
|
-
focusDate: 'controlled visible month',
|
|
225
|
+
focusDate: 'controlled visible month (default: selected date, else today clamped into min/max)',
|
|
225
226
|
onFocusDateChange: 'navigation callback',
|
|
226
227
|
handleRef: 'imperative navigation handle',
|
|
227
228
|
hasOutsideDays: 'show days from adjacent months',
|
|
@@ -263,6 +263,58 @@ describe('Calendar', () => {
|
|
|
263
263
|
expect(day15).not.toBeDisabled();
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
+
// ─── Initial Visible Month ───────────────────────────────────
|
|
267
|
+
|
|
268
|
+
describe('opens on a month inside the min/max window', () => {
|
|
269
|
+
beforeEach(() => {
|
|
270
|
+
vi.useFakeTimers({toFake: ['Date']});
|
|
271
|
+
vi.setSystemTime(new Date(2026, 7, 21, 12, 0, 0));
|
|
272
|
+
});
|
|
273
|
+
afterEach(() => {
|
|
274
|
+
vi.useRealTimers();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('opens on today when today is inside the window', () => {
|
|
278
|
+
render(<Calendar min="2026-01-01" max="2026-12-31" />);
|
|
279
|
+
|
|
280
|
+
expect(screen.getByText('August 2026')).toBeInTheDocument();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('opens on min when the window is entirely in the future', () => {
|
|
284
|
+
render(<Calendar min="2027-03-04" max="2027-06-30" />);
|
|
285
|
+
|
|
286
|
+
expect(screen.getByText('March 2027')).toBeInTheDocument();
|
|
287
|
+
expect(getDayButton(4, 'March', 2027)).not.toBeDisabled();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('opens on max when the window is entirely in the past', () => {
|
|
291
|
+
render(<Calendar min="2019-01-01" max="2019-04-30" />);
|
|
292
|
+
|
|
293
|
+
expect(screen.getByText('April 2019')).toBeInTheDocument();
|
|
294
|
+
expect(getDayButton(30, 'April', 2019)).not.toBeDisabled();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('keeps max in the last pane in the two-month layout', () => {
|
|
298
|
+
render(<Calendar numberOfMonths={2} min="2019-01-01" max="2019-04-30" />);
|
|
299
|
+
|
|
300
|
+
expect(screen.getByText('March 2019 – April 2019')).toBeInTheDocument();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('still honors an explicit focusDate outside the window', () => {
|
|
304
|
+
render(
|
|
305
|
+
<Calendar focusDate="2026-01-01" min="2027-03-04" max="2027-06-30" />,
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
expect(screen.getByText('January 2026')).toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('still opens on the selected value outside the window', () => {
|
|
312
|
+
render(<Calendar defaultValue="2031-07-04" min="2019-01-01" />);
|
|
313
|
+
|
|
314
|
+
expect(screen.getByText('July 2031')).toBeInTheDocument();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
266
318
|
it('respects custom dateConstraints', () => {
|
|
267
319
|
// Only allow weekdays
|
|
268
320
|
const isWeekday = (date: Date) => {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* SYNC: When modified, update these files to stay in sync:
|
|
13
13
|
* - /packages/core/src/Calendar/Calendar.doc.mjs (props table, features, implementation notes)
|
|
14
|
+
* - /packages/core/src/Calendar/getInitialFocusDate.ts (which month the calendar opens on)
|
|
14
15
|
* - /packages/core/src/Calendar/Calendar.test.tsx (tests for new/changed behavior)
|
|
15
16
|
* - /packages/core/src/Calendar/index.ts (exports if types change)
|
|
16
17
|
* - /apps/storybook/stories/Calendar.stories.tsx (storybook stories)
|
|
@@ -60,6 +61,7 @@ import {
|
|
|
60
61
|
} from '../utils/plainDate';
|
|
61
62
|
import {mergeProps, composeEventHandlers, rtlStyles} from '../utils';
|
|
62
63
|
import {focusOutlineProps} from '../utils/focusOutline.stylex';
|
|
64
|
+
import {getInitialFocusDate} from './getInitialFocusDate';
|
|
63
65
|
import {
|
|
64
66
|
computeDayCellState,
|
|
65
67
|
computeRangeRounding,
|
|
@@ -142,7 +144,9 @@ interface CalendarBaseProps extends Omit<
|
|
|
142
144
|
|
|
143
145
|
/**
|
|
144
146
|
* Controlled focus date (which month is visible).
|
|
145
|
-
* If not provided, defaults to selected date
|
|
147
|
+
* If not provided, defaults to the selected date, else today clamped into
|
|
148
|
+
* the `min`/`max` window (so a window that excludes today opens on the
|
|
149
|
+
* bound nearest to it, not on an all-disabled month).
|
|
146
150
|
*/
|
|
147
151
|
focusDate?: ISODateString;
|
|
148
152
|
|
|
@@ -277,20 +281,19 @@ export function Calendar({ref, ...props}: CalendarProps) {
|
|
|
277
281
|
// Determine effective value
|
|
278
282
|
const effectiveValue = value !== undefined ? value : internalValue;
|
|
279
283
|
|
|
280
|
-
// Focus date state (which month is visible)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
});
|
|
284
|
+
// Focus date state (which month is visible). Falls back to today, clamped
|
|
285
|
+
// into the min/max window so a window that doesn't contain today doesn't
|
|
286
|
+
// open on an all-disabled month.
|
|
287
|
+
const [internalFocusDate, setInternalFocusDate] = useState<PlainDate>(() =>
|
|
288
|
+
getInitialFocusDate({
|
|
289
|
+
focusDate: focusDateProp,
|
|
290
|
+
value: effectiveValue,
|
|
291
|
+
min,
|
|
292
|
+
max,
|
|
293
|
+
numberOfMonths,
|
|
294
|
+
today,
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
294
297
|
|
|
295
298
|
// Use controlled focusDate if callback is provided, otherwise use internal state
|
|
296
299
|
const isControlledFocus =
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file getInitialFocusDate.test.ts
|
|
5
|
+
* @input Uses vitest
|
|
6
|
+
* @output Test suite for getInitialFocusDate
|
|
7
|
+
* @position Tests for getInitialFocusDate.ts
|
|
8
|
+
*
|
|
9
|
+
* SYNC: When getInitialFocusDate.ts changes, update tests accordingly
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {describe, it, expect} from 'vitest';
|
|
13
|
+
import {getInitialFocusDate} from './getInitialFocusDate';
|
|
14
|
+
import {plainDateFromISO, plainDateToISO} from '../utils/plainDate';
|
|
15
|
+
|
|
16
|
+
const TODAY = plainDateFromISO('2026-08-21');
|
|
17
|
+
|
|
18
|
+
function focusISO(options: Partial<Parameters<typeof getInitialFocusDate>[0]>) {
|
|
19
|
+
return plainDateToISO(
|
|
20
|
+
getInitialFocusDate({numberOfMonths: 1, today: TODAY, ...options}),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('getInitialFocusDate', () => {
|
|
25
|
+
it('opens on today when there are no bounds', () => {
|
|
26
|
+
expect(focusISO({})).toBe('2026-08-21');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('opens on today when today is inside the min/max window', () => {
|
|
30
|
+
expect(focusISO({min: '2026-01-01', max: '2026-12-31'})).toBe('2026-08-21');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('treats the bounds as inclusive', () => {
|
|
34
|
+
expect(focusISO({min: '2026-08-21'})).toBe('2026-08-21');
|
|
35
|
+
expect(focusISO({max: '2026-08-21'})).toBe('2026-08-21');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('opens on min when the whole window is in the future', () => {
|
|
39
|
+
expect(focusISO({min: '2027-03-04', max: '2027-06-30'})).toBe('2027-03-04');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('opens on max when the whole window is in the past', () => {
|
|
43
|
+
expect(focusISO({min: '2019-01-01', max: '2019-04-30'})).toBe('2019-04-01');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('clamps against a one-sided min', () => {
|
|
47
|
+
expect(focusISO({min: '2030-05-17'})).toBe('2030-05-17');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('clamps against a one-sided max', () => {
|
|
51
|
+
expect(focusISO({max: '2020-02-09'})).toBe('2020-02-01');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('lands max in the last pane in the two-month layout', () => {
|
|
55
|
+
// Opening on max's month would spend the second pane entirely
|
|
56
|
+
// out of bounds, so the window's end sits on the right instead.
|
|
57
|
+
expect(focusISO({max: '2020-02-09', numberOfMonths: 2})).toBe('2020-01-01');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('does not shift the two-month layout back past min', () => {
|
|
61
|
+
expect(
|
|
62
|
+
focusISO({min: '2020-02-03', max: '2020-02-09', numberOfMonths: 2}),
|
|
63
|
+
).toBe('2020-02-03');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('prefers the selected value over the bounds', () => {
|
|
67
|
+
expect(focusISO({value: '2031-07-04', min: '2019-01-01'})).toBe(
|
|
68
|
+
'2031-07-04',
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('uses a range value start as the visible month', () => {
|
|
73
|
+
expect(focusISO({value: {start: '2019-03-08', end: '2019-03-19'}})).toBe(
|
|
74
|
+
'2019-03-08',
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('prefers an explicit focusDate over everything else', () => {
|
|
79
|
+
expect(
|
|
80
|
+
focusISO({
|
|
81
|
+
focusDate: '2015-11-02',
|
|
82
|
+
value: '2031-07-04',
|
|
83
|
+
min: '2026-01-01',
|
|
84
|
+
max: '2026-12-31',
|
|
85
|
+
}),
|
|
86
|
+
).toBe('2015-11-02');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('prefers min when the bounds are inverted', () => {
|
|
90
|
+
// Degenerate input (min after max) — resolve it deterministically
|
|
91
|
+
// rather than reading the second bound off a contradiction.
|
|
92
|
+
expect(focusISO({min: '2027-01-01', max: '2020-01-01'})).toBe('2027-01-01');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file getInitialFocusDate.ts
|
|
5
|
+
* @input PlainDate utilities, min/max bounds, today
|
|
6
|
+
* @output Exports getInitialFocusDate — the month Calendar opens on
|
|
7
|
+
* @position Calendar-specific helper; used by Calendar.tsx
|
|
8
|
+
*
|
|
9
|
+
* SYNC: When modified, update:
|
|
10
|
+
* - /packages/core/src/Calendar/getInitialFocusDate.test.ts
|
|
11
|
+
* - /packages/core/src/Calendar/Calendar.tsx (the only caller)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {ISODateString, DateRange} from '../utils/dateTypes';
|
|
15
|
+
import {
|
|
16
|
+
type PlainDate,
|
|
17
|
+
plainDateFromISO,
|
|
18
|
+
plainDateSetFirstOfMonth,
|
|
19
|
+
plainDateAddMonths,
|
|
20
|
+
plainDateIsBefore,
|
|
21
|
+
plainDateIsAfter,
|
|
22
|
+
} from '../utils/plainDate';
|
|
23
|
+
|
|
24
|
+
export interface InitialFocusDateOptions {
|
|
25
|
+
/** Controlled visible month, if the consumer supplied one. */
|
|
26
|
+
focusDate?: ISODateString;
|
|
27
|
+
/** Selected value (single date or range), controlled or uncontrolled. */
|
|
28
|
+
value?: ISODateString | DateRange;
|
|
29
|
+
/** Earliest selectable date. */
|
|
30
|
+
min?: ISODateString;
|
|
31
|
+
/** Latest selectable date. */
|
|
32
|
+
max?: ISODateString;
|
|
33
|
+
/** How many month panes the calendar renders (1 or 2). */
|
|
34
|
+
numberOfMonths: number;
|
|
35
|
+
/** Today, injected so the caller keeps a single memoized source of "now". */
|
|
36
|
+
today: PlainDate;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Picks the date whose month the calendar opens on.
|
|
41
|
+
*
|
|
42
|
+
* An explicit `focusDate` wins, then the selected value — both are the
|
|
43
|
+
* consumer's own instruction about where to look, so neither is second-guessed
|
|
44
|
+
* against `min`/`max`.
|
|
45
|
+
*
|
|
46
|
+
* Otherwise the calendar opens on today, clamped into the `min`/`max` window.
|
|
47
|
+
* Without the clamp a window that doesn't contain today (a 2019 audit range, a
|
|
48
|
+
* booking window opening next spring) opened on today's month with every day
|
|
49
|
+
* disabled, leaving prev/next clicking as the only way in.
|
|
50
|
+
*
|
|
51
|
+
* When clamping forward to `min`, that month leads: the whole selectable window
|
|
52
|
+
* runs ahead of it. When clamping back to `max`, the last pane lands on `max`'s
|
|
53
|
+
* month instead — with `numberOfMonths={2}` opening on `[max, max + 1]` would
|
|
54
|
+
* spend half the calendar on a month that is entirely out of bounds. That
|
|
55
|
+
* shift never crosses `min`'s month.
|
|
56
|
+
*/
|
|
57
|
+
export function getInitialFocusDate(
|
|
58
|
+
options: InitialFocusDateOptions,
|
|
59
|
+
): PlainDate {
|
|
60
|
+
const {focusDate, value, min, max, numberOfMonths, today} = options;
|
|
61
|
+
|
|
62
|
+
if (focusDate) {
|
|
63
|
+
return plainDateFromISO(focusDate);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (value) {
|
|
67
|
+
return plainDateFromISO(typeof value === 'string' ? value : value.start);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const minDate = min ? plainDateFromISO(min) : null;
|
|
71
|
+
const maxDate = max ? plainDateFromISO(max) : null;
|
|
72
|
+
|
|
73
|
+
if (minDate && plainDateIsBefore(today, minDate)) {
|
|
74
|
+
return minDate;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (maxDate && plainDateIsAfter(today, maxDate)) {
|
|
78
|
+
const lastPaneOffset = Math.max(0, numberOfMonths - 1);
|
|
79
|
+
const shifted = plainDateAddMonths(
|
|
80
|
+
plainDateSetFirstOfMonth(maxDate),
|
|
81
|
+
-lastPaneOffset,
|
|
82
|
+
);
|
|
83
|
+
if (
|
|
84
|
+
minDate &&
|
|
85
|
+
plainDateIsBefore(shifted, plainDateSetFirstOfMonth(minDate))
|
|
86
|
+
) {
|
|
87
|
+
return minDate;
|
|
88
|
+
}
|
|
89
|
+
return shifted;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return today;
|
|
93
|
+
}
|
|
@@ -7,7 +7,7 @@ export const docs = {
|
|
|
7
7
|
subComponentOf: 'Chat',
|
|
8
8
|
displayName: 'Chat Message List',
|
|
9
9
|
isHiddenFromOverview: true,
|
|
10
|
-
description: `Presentational message container with density context and infinite scroll support. Provides role="log" with aria-live="polite" for accessibility. A flex spacer pushes messages to the bottom when the list isn't full.`,
|
|
10
|
+
description: `Presentational message container with density context and infinite scroll support. Provides role="log" with aria-live="polite" for accessibility. A flex spacer pushes messages to the bottom when the list isn't full; set align="top" to start messages at the top instead.`,
|
|
11
11
|
props: [
|
|
12
12
|
{
|
|
13
13
|
name: 'children',
|
|
@@ -45,6 +45,12 @@ export const docs = {
|
|
|
45
45
|
type: '0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10',
|
|
46
46
|
description: 'Gap between top-level message rows. Defaults to the selected density; override for LLM event streams or independent rows that need different spacing from density.',
|
|
47
47
|
},
|
|
48
|
+
{
|
|
49
|
+
name: 'align',
|
|
50
|
+
type: "'top' | 'bottom'",
|
|
51
|
+
description: "Vertical alignment when the list is shorter than its container. 'bottom' fills free space with a spacer so short conversations sit above the composer; 'top' omits the spacer so messages start at the top. Only affects a non-full list; overflowing lists scroll identically, preserving auto-scroll-to-bottom.",
|
|
52
|
+
default: "'bottom'",
|
|
53
|
+
},
|
|
48
54
|
{
|
|
49
55
|
name: 'isStreaming',
|
|
50
56
|
type: 'boolean',
|
|
@@ -65,6 +71,7 @@ export const docsZh = {
|
|
|
65
71
|
scrollToTopAction: '用户滚动到顶部时触发的异步操作。用于加载更早的消息。',
|
|
66
72
|
density: '视觉密度,通过上下文传递给子消息。',
|
|
67
73
|
gap: '顶层消息行之间的间距。默认跟随密度;当 LLM 事件流等独立行需要不同间距时可覆盖。',
|
|
74
|
+
align: "列表内容不足一屏时的垂直对齐方式。'bottom' 用占位块填满剩余空间,让简短对话贴近输入框;'top' 省略占位块,让消息从顶部开始。仅影响未填满的列表,溢出后两种模式滚动一致,保持自动滚动到底部。",
|
|
68
75
|
isStreaming: '是否有助手消息正在流式输出。标记日志为 aria-busy,让屏幕阅读器等待并在完成后一次性播报,而不是每个 token 都重复播报。',
|
|
69
76
|
},
|
|
70
77
|
};
|
|
@@ -80,6 +87,7 @@ export const docsDense = {
|
|
|
80
87
|
scrollToTopAction: 'async action at scroll top; load older msgs',
|
|
81
88
|
density: 'visual density; flows to children via context',
|
|
82
89
|
gap: 'top-level row gap; defaults to density spacing; override for independent LLM/tool rows',
|
|
90
|
+
align: "'top'|'bottom' (def bottom); bottom spacer pushes short lists down, top starts at top; overflow scrolls same either way",
|
|
83
91
|
isStreaming: 'assistant streaming? marks log aria-busy so SR announces the completed msg once',
|
|
84
92
|
},
|
|
85
93
|
};
|
|
@@ -83,4 +83,50 @@ describe('ChatMessageList', () => {
|
|
|
83
83
|
);
|
|
84
84
|
expect(screen.getByTestId('chat-list')).toBeTruthy();
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
// With no scrollToTopAction the spacer is the only aria-hidden element in
|
|
88
|
+
// the list, so its presence maps 1:1 to the aria-hidden count.
|
|
89
|
+
it('renders the bottom spacer by default', () => {
|
|
90
|
+
render(
|
|
91
|
+
<ChatMessageList data-testid="list">
|
|
92
|
+
<div>msg</div>
|
|
93
|
+
</ChatMessageList>,
|
|
94
|
+
);
|
|
95
|
+
expect(
|
|
96
|
+
screen.getByTestId('list').querySelectorAll('[aria-hidden]'),
|
|
97
|
+
).toHaveLength(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('renders the bottom spacer when align="bottom"', () => {
|
|
101
|
+
render(
|
|
102
|
+
<ChatMessageList align="bottom" data-testid="list">
|
|
103
|
+
<div>msg</div>
|
|
104
|
+
</ChatMessageList>,
|
|
105
|
+
);
|
|
106
|
+
expect(
|
|
107
|
+
screen.getByTestId('list').querySelectorAll('[aria-hidden]'),
|
|
108
|
+
).toHaveLength(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('omits the spacer when align="top"', () => {
|
|
112
|
+
render(
|
|
113
|
+
<ChatMessageList align="top" data-testid="list">
|
|
114
|
+
<div>msg</div>
|
|
115
|
+
</ChatMessageList>,
|
|
116
|
+
);
|
|
117
|
+
expect(
|
|
118
|
+
screen.getByTestId('list').querySelectorAll('[aria-hidden]'),
|
|
119
|
+
).toHaveLength(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('still renders children when align="top"', () => {
|
|
123
|
+
render(
|
|
124
|
+
<ChatMessageList align="top">
|
|
125
|
+
<ChatMessage sender="assistant">
|
|
126
|
+
<ChatMessageBubble>Hello</ChatMessageBubble>
|
|
127
|
+
</ChatMessage>
|
|
128
|
+
</ChatMessageList>,
|
|
129
|
+
);
|
|
130
|
+
expect(screen.getByText('Hello')).toBeTruthy();
|
|
131
|
+
});
|
|
86
132
|
});
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Renders a container with role="log" for chat message histories.
|
|
12
12
|
* Handles density context, configurable gap, empty state,
|
|
13
|
-
* a spacer that pushes messages to the bottom,
|
|
13
|
+
* a configurable spacer (align) that pushes messages to the bottom,
|
|
14
|
+
* and an infinite scroll sentinel.
|
|
14
15
|
*
|
|
15
16
|
* Auto-scroll and the scroll-to-bottom button are owned by
|
|
16
17
|
* ChatLayout. When used standalone (without a layout), the list
|
|
@@ -73,6 +74,24 @@ export interface ChatMessageListProps extends BaseProps<HTMLDivElement> {
|
|
|
73
74
|
*/
|
|
74
75
|
gap?: SpacingStep;
|
|
75
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Vertical alignment of messages when the list is shorter than its
|
|
79
|
+
* container.
|
|
80
|
+
*
|
|
81
|
+
* - `'bottom'` (default): a spacer fills the free space and pushes
|
|
82
|
+
* messages to the bottom, so a short conversation sits just above the
|
|
83
|
+
* composer — the familiar messaging-app layout.
|
|
84
|
+
* - `'top'`: the spacer is omitted, so messages start at the top and grow
|
|
85
|
+
* downward — better for document-style or log-style lists.
|
|
86
|
+
*
|
|
87
|
+
* This only changes the resting position of a non-full list. Once messages
|
|
88
|
+
* overflow the container the spacer collapses to zero in both modes, so
|
|
89
|
+
* ChatLayout auto-scroll-to-bottom behavior is identical either way.
|
|
90
|
+
*
|
|
91
|
+
* @default 'bottom'
|
|
92
|
+
*/
|
|
93
|
+
align?: 'top' | 'bottom';
|
|
94
|
+
|
|
76
95
|
/**
|
|
77
96
|
* Whether an assistant message is actively streaming into the list.
|
|
78
97
|
*
|
|
@@ -183,7 +202,8 @@ const gapStyles = stylex.create({
|
|
|
183
202
|
*
|
|
184
203
|
* Renders messages in a flex column with density-based spacing.
|
|
185
204
|
* Override gap to tune row spacing separately from density.
|
|
186
|
-
*
|
|
205
|
+
* By default a spacer pushes content to the bottom when the list isn't full;
|
|
206
|
+
* set `align='top'` to start messages at the top instead.
|
|
187
207
|
* Supports loading older messages via `scrollToTopAction`.
|
|
188
208
|
*
|
|
189
209
|
* Auto-scroll and the scroll-to-bottom button are owned by
|
|
@@ -204,6 +224,7 @@ export function ChatMessageList({
|
|
|
204
224
|
scrollToTopAction,
|
|
205
225
|
density = 'balanced',
|
|
206
226
|
gap,
|
|
227
|
+
align = 'bottom',
|
|
207
228
|
isStreaming = false,
|
|
208
229
|
xstyle,
|
|
209
230
|
className,
|
|
@@ -291,8 +312,11 @@ export function ChatMessageList({
|
|
|
291
312
|
</div>
|
|
292
313
|
)}
|
|
293
314
|
|
|
294
|
-
{/* Spacer pushes messages to bottom when list isn't full
|
|
295
|
-
|
|
315
|
+
{/* Spacer pushes messages to bottom when the list isn't full.
|
|
316
|
+
Omitted for top alignment so messages start at the top. */}
|
|
317
|
+
{align === 'bottom' && (
|
|
318
|
+
<div {...stylex.props(styles.spacer)} aria-hidden />
|
|
319
|
+
)}
|
|
296
320
|
|
|
297
321
|
{/* Messages or empty state */}
|
|
298
322
|
{hasChildren ? (
|
|
@@ -95,6 +95,12 @@ export const docs = {
|
|
|
95
95
|
type: 'SyntaxThemeDefinition',
|
|
96
96
|
description: 'Per-instance syntax theme override. Shorthand for wrapping the block in <SyntaxTheme theme={...}>. Accepts a preset from @astryxdesign/core/theme/syntax or a theme created with defineSyntaxTheme(). Defaults to the nearest SyntaxTheme ancestor or the theme-level syntax colors.',
|
|
97
97
|
},
|
|
98
|
+
{
|
|
99
|
+
name: 'highlightMode',
|
|
100
|
+
type: "'auto' | 'ranges' | 'spans'",
|
|
101
|
+
description: 'Syntax highlighting rendering mode.',
|
|
102
|
+
default: "'auto'",
|
|
103
|
+
},
|
|
98
104
|
{
|
|
99
105
|
name: 'isCollapsible',
|
|
100
106
|
type: 'boolean',
|
|
@@ -58,6 +58,11 @@ export const docs = {
|
|
|
58
58
|
description: 'Accessible name for the menu surface, announced when it opens.',
|
|
59
59
|
default: "'Context menu'",
|
|
60
60
|
},
|
|
61
|
+
{
|
|
62
|
+
name: 'onOpenChange',
|
|
63
|
+
type: '(isOpen: boolean) => void',
|
|
64
|
+
description: 'Callback fired when the menu opens or closes.',
|
|
65
|
+
},
|
|
61
66
|
{
|
|
62
67
|
name: 'isDisabled',
|
|
63
68
|
type: 'boolean',
|