@astryxdesign/core 0.1.7 → 0.1.8-canary.c5fa19b
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 +65 -0
- package/README.md +30 -19
- package/dist/Avatar/Avatar.d.ts +10 -5
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +12 -8
- package/dist/Avatar/AvatarSizeContext.d.ts +1 -1
- package/dist/Avatar/AvatarSizeContext.js +1 -1
- package/dist/Avatar/AvatarStatusDot.d.ts +2 -2
- package/dist/Avatar/AvatarStatusDot.js +2 -2
- package/dist/AvatarGroup/AvatarGroup.d.ts +2 -2
- package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroup.js +2 -2
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +16 -14
- package/dist/Carousel/Carousel.d.ts +3 -2
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +32 -2
- package/dist/Chat/ChatMessage.d.ts +1 -1
- package/dist/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/Chat/ChatMessage.js +1 -1
- package/dist/Citation/Citation.d.ts.map +1 -1
- package/dist/Citation/Citation.js +10 -4
- package/dist/Collapsible/Collapsible.d.ts +11 -1
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +36 -4
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +4 -3
- package/dist/ContextMenu/index.d.ts +3 -0
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/ContextMenu/index.js +7 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +8 -4
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +89 -0
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +170 -0
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +18 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +69 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +70 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +69 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +178 -0
- package/dist/DropdownMenu/index.d.ts +5 -1
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/index.js +10 -1
- package/dist/DropdownMenu/menuItemRoles.d.ts +13 -0
- package/dist/DropdownMenu/menuItemRoles.d.ts.map +1 -0
- package/dist/DropdownMenu/menuItemRoles.js +15 -0
- package/dist/Icon/Icon.d.ts +43 -13
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +62 -27
- package/dist/Outline/Outline.d.ts +69 -3
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +129 -26
- package/dist/Outline/useScrollSpy.d.ts +27 -7
- package/dist/Outline/useScrollSpy.d.ts.map +1 -1
- package/dist/Outline/useScrollSpy.js +202 -70
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +2 -2
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +2 -0
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +1 -1
- package/dist/Table/Table.d.ts +1 -1
- package/dist/Table/Table.js +1 -1
- package/dist/Table/index.d.ts +2 -0
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +1 -0
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +24 -2
- package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortable.js +18 -7
- package/dist/Table/plugins/tree/index.d.ts +5 -0
- package/dist/Table/plugins/tree/index.d.ts.map +1 -0
- package/dist/Table/plugins/tree/index.js +4 -0
- package/dist/Table/plugins/tree/useTableTreeData.d.ts +38 -0
- package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -0
- package/dist/Table/plugins/tree/useTableTreeData.js +358 -0
- package/dist/Table/plugins/tree/useTableTreeState.d.ts +51 -0
- package/dist/Table/plugins/tree/useTableTreeState.d.ts.map +1 -0
- package/dist/Table/plugins/tree/useTableTreeState.js +175 -0
- package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.js +6 -3
- package/dist/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar/Toolbar.js +8 -1
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +5 -14
- package/dist/astryx.css +24 -4
- package/dist/astryx.umd.js +54 -54
- package/dist/astryx.umd.js.map +4 -4
- package/dist/theme/Theme.js +1 -1
- package/dist/theme/defineTheme.d.ts +1 -1
- package/dist/theme/defineTheme.js +1 -1
- package/dist/theme/hct.d.ts +0 -1
- package/dist/theme/hct.d.ts.map +1 -1
- package/dist/theme/hct.js +0 -3
- package/docs.mjs +32 -461
- package/locales/en.json +200 -172
- package/locales/pseudo.json +21 -0
- package/package.json +3 -1
- package/scripts/postinstall.mjs +93 -0
- package/src/Avatar/Avatar.doc.mjs +6 -6
- package/src/Avatar/Avatar.tsx +16 -11
- package/src/Avatar/AvatarSizeContext.ts +1 -1
- package/src/Avatar/AvatarStatusDot.tsx +2 -2
- package/src/AvatarGroup/AvatarGroup.doc.mjs +1 -1
- package/src/AvatarGroup/AvatarGroup.test.tsx +17 -3
- package/src/AvatarGroup/AvatarGroup.tsx +3 -3
- package/src/AvatarGroup/AvatarGroupOverflow.doc.mjs +4 -4
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +21 -5
- package/src/Carousel/Carousel.doc.mjs +1 -0
- package/src/Carousel/Carousel.test.tsx +96 -1
- package/src/Carousel/Carousel.tsx +29 -2
- package/src/Chat/Chat.doc.mjs +1 -1
- package/src/Chat/ChatMessage.tsx +4 -2
- package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
- package/src/Citation/Citation.test.tsx +51 -0
- package/src/Citation/Citation.tsx +8 -2
- package/src/Collapsible/Collapsible.doc.mjs +7 -0
- package/src/Collapsible/Collapsible.test.tsx +80 -0
- package/src/Collapsible/Collapsible.tsx +58 -5
- package/src/ContextMenu/ContextMenu.test.tsx +75 -1
- package/src/ContextMenu/ContextMenu.tsx +10 -5
- package/src/ContextMenu/index.ts +15 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +23 -0
- package/src/DropdownMenu/DropdownMenu.tsx +11 -6
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +95 -0
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +237 -0
- package/src/DropdownMenu/DropdownMenuContext.tsx +29 -1
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +7 -1
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +80 -0
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +119 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +80 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +232 -0
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +163 -0
- package/src/DropdownMenu/index.ts +23 -3
- package/src/DropdownMenu/menuItemRoles.ts +22 -0
- package/src/Field/Field.doc.mjs +1 -1
- package/src/Field/FieldLabel.doc.mjs +1 -1
- package/src/Icon/Icon.doc.mjs +12 -0
- package/src/Icon/Icon.test.tsx +142 -0
- package/src/Icon/Icon.tsx +98 -30
- package/src/Item/Item.doc.mjs +1 -1
- package/src/NumberInput/NumberInput.doc.mjs +2 -2
- package/src/Outline/Outline.doc.mjs +123 -3
- package/src/Outline/Outline.test.tsx +735 -0
- package/src/Outline/Outline.tsx +184 -33
- package/src/Outline/useScrollSpy.ts +248 -72
- package/src/Selector/SelectorOption.doc.mjs +1 -1
- package/src/SideNav/SideNavItem.doc.mjs +2 -2
- package/src/Switch/Switch.doc.mjs +1 -1
- package/src/TabList/Tab.tsx +5 -1
- package/src/TabList/TabList.doc.mjs +3 -3
- package/src/TabList/TabList.test.tsx +66 -0
- package/src/TabList/TabList.tsx +8 -0
- package/src/TabList/TabMenu.tsx +4 -1
- package/src/Table/Table.doc.mjs +2 -0
- package/src/Table/Table.tsx +1 -1
- package/src/Table/index.ts +7 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +90 -1
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +37 -5
- package/src/Table/plugins/sortable/useTableSortable.test.tsx +95 -9
- package/src/Table/plugins/sortable/useTableSortable.tsx +23 -7
- package/src/Table/plugins/tree/index.ts +12 -0
- package/src/Table/plugins/tree/useTableTreeData-perf.test.tsx +163 -0
- package/src/Table/plugins/tree/useTableTreeData.test.tsx +594 -0
- package/src/Table/plugins/tree/useTableTreeData.tsx +495 -0
- package/src/Table/plugins/tree/useTableTreeState.test.tsx +636 -0
- package/src/Table/plugins/tree/useTableTreeState.tsx +253 -0
- package/src/Table/useBaseTablePlugins.ts +6 -2
- package/src/Table/useTableTreeData.doc.mjs +75 -0
- package/src/Table/useTableTreeState.doc.mjs +98 -0
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextInput/TextInput.doc.mjs +1 -1
- package/src/Toolbar/Toolbar.tsx +10 -3
- package/src/TreeList/TreeList.test.tsx +32 -1
- package/src/TreeList/TreeListItem.tsx +19 -10
- package/src/docs-script.test.mjs +18 -13
- package/src/postinstall.test.mjs +71 -0
- package/src/theme/Theme.doc.mjs +4 -4
- package/src/theme/Theme.tsx +1 -1
- package/src/theme/defineTheme.ts +1 -1
- package/src/theme/hct.ts +0 -4
- package/dist/TreeList/treeListItem.markers.stylex.d.ts +0 -16
- package/dist/TreeList/treeListItem.markers.stylex.d.ts.map +0 -1
- package/dist/TreeList/treeListItem.markers.stylex.js +0 -22
- package/src/TreeList/treeListItem.markers.stylex.ts +0 -20
package/locales/pseudo.json
CHANGED
|
@@ -317,6 +317,21 @@
|
|
|
317
317
|
"@astryx.table.sort.clear": {
|
|
318
318
|
"defaultMessage": "⟦Çłéàř šóřţ⟧"
|
|
319
319
|
},
|
|
320
|
+
"@astryx.table.sort.direction.ascending": {
|
|
321
|
+
"defaultMessage": "⟦àšçéñðíñĝ⟧"
|
|
322
|
+
},
|
|
323
|
+
"@astryx.table.sort.direction.descending": {
|
|
324
|
+
"defaultMessage": "⟦ðéšçéñðíñĝ⟧"
|
|
325
|
+
},
|
|
326
|
+
"@astryx.table.sort.sortBy": {
|
|
327
|
+
"defaultMessage": "⟦Šóřţ ƀý {label}⟧"
|
|
328
|
+
},
|
|
329
|
+
"@astryx.table.sort.sortedBy": {
|
|
330
|
+
"defaultMessage": "⟦Šóřţ ƀý {label}, šóřţéð {direction}⟧"
|
|
331
|
+
},
|
|
332
|
+
"@astryx.table.sort.sortedByWithPriority": {
|
|
333
|
+
"defaultMessage": "⟦Šóřţ ƀý {label}, šóřţéð {direction}, þříóříţý {rank, number} óƒ {total, number}⟧"
|
|
334
|
+
},
|
|
320
335
|
"@astryx.toast.dismiss": {
|
|
321
336
|
"defaultMessage": "⟦Ðíšɱíšš ñóţíƒíçàţíóñ⟧"
|
|
322
337
|
},
|
|
@@ -542,6 +557,12 @@
|
|
|
542
557
|
"@astryx.tableRowExpansion.expandAllRows": {
|
|
543
558
|
"defaultMessage": "⟦Éẋþàñð àłł řóŵš⟧"
|
|
544
559
|
},
|
|
560
|
+
"@astryx.tableTree.collapseRow": {
|
|
561
|
+
"defaultMessage": "⟦Çółłàþšé řóŵ⟧"
|
|
562
|
+
},
|
|
563
|
+
"@astryx.tableTree.expandRow": {
|
|
564
|
+
"defaultMessage": "⟦Éẋþàñð řóŵ⟧"
|
|
565
|
+
},
|
|
545
566
|
"@astryx.textInput.clearLabel": {
|
|
546
567
|
"defaultMessage": "⟦Çłéàř {label}⟧"
|
|
547
568
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8-canary.c5fa19b",
|
|
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",
|
|
@@ -636,6 +636,7 @@
|
|
|
636
636
|
"files": [
|
|
637
637
|
"dist",
|
|
638
638
|
"src",
|
|
639
|
+
"scripts/postinstall.mjs",
|
|
639
640
|
"locales",
|
|
640
641
|
"docs.mjs",
|
|
641
642
|
"groups.doc.mjs",
|
|
@@ -662,6 +663,7 @@
|
|
|
662
663
|
"intl-messageformat": "^11.2.9"
|
|
663
664
|
},
|
|
664
665
|
"scripts": {
|
|
666
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
665
667
|
"build": "rimraf dist && pnpm build:i18n && pnpm build:esm && tsc --project tsconfig.build.json && pnpm build:css && pnpm build:umd && node ../../scripts/check-no-dev-jsx.mjs",
|
|
666
668
|
"build:i18n": "node scripts/build-pseudo-locale.mjs",
|
|
667
669
|
"build:esm": "babel src --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.ts,**/*.test.tsx,**/__tests__/**,**/*.perf.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css,**/*.css.d.ts\" --config-file ./babel.config.json",
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file @astryxdesign/core postinstall — nudge to run `npx @astryxdesign/cli init`.
|
|
5
|
+
*
|
|
6
|
+
* Enforcement layer 1 of making `astryx init` foolproof: when the design system
|
|
7
|
+
* is installed and the project hasn't run init yet, print a one-line next-step
|
|
8
|
+
* so agents/humans discover it (this is the most common fresh-install entry).
|
|
9
|
+
*
|
|
10
|
+
* Self-contained: core is a separate package and cannot import the CLI, so the
|
|
11
|
+
* agent-doc locations + marker below intentionally DUPLICATE the CLI's agent-doc
|
|
12
|
+
* discovery in packages/cli/src/commands/agent-docs.mjs — keep them aligned with
|
|
13
|
+
* that file. The .hermes.md / HERMES.md entries are an intentional superset (the
|
|
14
|
+
* CLI writes those via `--agent hermes`). Non-interactive, never fails the
|
|
15
|
+
* install, and quiet in the monorepo build, during npx's fetch, and once set up.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
21
|
+
|
|
22
|
+
const HERE = fileURLToPath(import.meta.url);
|
|
23
|
+
|
|
24
|
+
// Duplicates the CLI's agent-doc locations + markers — keep aligned with
|
|
25
|
+
// packages/cli/src/commands/agent-docs.mjs. Hermes files are an intentional superset.
|
|
26
|
+
const AGENT_DOC_PATHS = [
|
|
27
|
+
'AGENTS.md',
|
|
28
|
+
'CLAUDE.md',
|
|
29
|
+
'.claude/CLAUDE.md',
|
|
30
|
+
'.cursorrules',
|
|
31
|
+
'.hermes.md',
|
|
32
|
+
'HERMES.md',
|
|
33
|
+
];
|
|
34
|
+
const MARKERS = ['<!-- ASTRYX:START -->', '<!-- XDS:START -->'];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* True when the project already has the Astryx agent prompt installed (an Astryx
|
|
38
|
+
* marker is present in any agent-doc file) — the same check the CLI performs.
|
|
39
|
+
* @param {string} root @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
export function isAstryxInitialized(root) {
|
|
42
|
+
for (const rel of AGENT_DOC_PATHS) {
|
|
43
|
+
try {
|
|
44
|
+
const content = fs.readFileSync(path.join(root, rel), 'utf-8');
|
|
45
|
+
if (MARKERS.some(m => content.includes(m))) return true;
|
|
46
|
+
} catch {
|
|
47
|
+
// Unreadable/missing — keep checking the others.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pure decision: should the postinstall print the setup nudge? Unit-testable
|
|
55
|
+
* without an actual npm install.
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
export function shouldNudge({scriptPath, npmCommand, isSetUp} = {}) {
|
|
59
|
+
if (!scriptPath || !scriptPath.includes('node_modules')) return false; // monorepo/source build
|
|
60
|
+
if (scriptPath.includes('_npx') || npmCommand === 'exec') return false; // npx transient
|
|
61
|
+
if (isSetUp) return false; // already set up — stay quiet
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function main() {
|
|
66
|
+
const root = process.env.INIT_CWD || process.cwd();
|
|
67
|
+
if (
|
|
68
|
+
shouldNudge({
|
|
69
|
+
scriptPath: HERE,
|
|
70
|
+
npmCommand: process.env.npm_command,
|
|
71
|
+
isSetUp: isAstryxInitialized(root),
|
|
72
|
+
})
|
|
73
|
+
) {
|
|
74
|
+
// Scoped package form (`@astryxdesign/cli`) — always resolves to us, even
|
|
75
|
+
// before the CLI is installed. Bare `npx astryx` would fetch an unrelated
|
|
76
|
+
// look-alike package (see PR #4151). After that lands, switch this to its
|
|
77
|
+
// getCliInvocation() single source of truth.
|
|
78
|
+
process.stdout.write(
|
|
79
|
+
'\nNext step: run `npx @astryxdesign/cli init` to finish setup and install the Astryx agent prompt.\n\n',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Run only when executed directly (`node scripts/postinstall.mjs`), never when
|
|
85
|
+
// imported by tests.
|
|
86
|
+
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
|
|
87
|
+
try {
|
|
88
|
+
main();
|
|
89
|
+
} catch {
|
|
90
|
+
// never break an install
|
|
91
|
+
}
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
@@ -13,7 +13,7 @@ export const docs = {
|
|
|
13
13
|
'Avatar represents a person or team with a profile photo, initials, or a default icon. Use it in comment headers, contact lists, chat messages, user cards, and anywhere you need to identify someone visually.',
|
|
14
14
|
bestPractices: [
|
|
15
15
|
{guidance: true, description: 'Always pass a name so the avatar can show initials if the photo fails to load, and so screen readers can announce who it represents.'},
|
|
16
|
-
{guidance: true, description: 'Pick a size that matches the context:
|
|
16
|
+
{guidance: true, description: 'Pick a size that matches the context: xsm or sm for inline mentions, md or lg for lists and cards, xl for profile headers.'},
|
|
17
17
|
{guidance: true, description: 'Add a status dot when knowing someone\'s availability matters, like in chat or team views.'},
|
|
18
18
|
{guidance: false, description: 'Use Avatar for logos, product images, or anything that isn\'t a person or team. Use an image or icon instead.'},
|
|
19
19
|
{guidance: false, description: 'Force a square or custom shape. Avatars are always circular to stay consistent across the system.'},
|
|
@@ -55,9 +55,9 @@ export const docs = {
|
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
name: 'size',
|
|
58
|
-
type: "'
|
|
59
|
-
description: "Avatar size. Use a named size ('
|
|
60
|
-
default: "'
|
|
58
|
+
type: "'xsm' | 'sm' | 'md' | 'lg' | 'xl' | number",
|
|
59
|
+
description: "Avatar size. Use a named size ('xsm' 20px, 'sm' 24px, 'md' 36px, 'lg' 48px, 'xl' 128px) or a numeric pixel value. Avatar shares Icon's abbreviated scale, but its tiers are larger because avatars align with media rather than glyphs.",
|
|
60
|
+
default: "'md'",
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
name: 'status',
|
|
@@ -100,7 +100,7 @@ export const docsDense = {
|
|
|
100
100
|
'Avatar represents a person or team with a profile photo, initials, or a default icon. Falls back automatically. Use in comment headers, contact lists, chat, user cards.',
|
|
101
101
|
bestPractices: [
|
|
102
102
|
{guidance: true, description: 'Always pass a name for initials fallback and screen reader alt text.'},
|
|
103
|
-
{guidance: true, description: 'Match size to context:
|
|
103
|
+
{guidance: true, description: 'Match size to context: xsm/sm inline, md/lg in lists, xl for profiles.'},
|
|
104
104
|
{guidance: true, description: 'Add a status dot in chat or team views where availability matters.'},
|
|
105
105
|
{guidance: false, description: 'Use for logos or product images. Use an image or icon instead.'},
|
|
106
106
|
{guidance: false, description: 'Force a square or custom shape. Avatars are always circular.'},
|
|
@@ -111,7 +111,7 @@ export const docsDense = {
|
|
|
111
111
|
fallbackSrc: 'fallback image when primary fails',
|
|
112
112
|
name: 'user name for initials and alt text',
|
|
113
113
|
alt: 'alt text; falls back to name',
|
|
114
|
-
size: "avatar size. Named ('
|
|
114
|
+
size: "avatar size. Named ('xsm' 20px, 'sm' 24px, 'md' 36px, 'lg' 48px, 'xl' 128px) or numeric px.",
|
|
115
115
|
status: 'corner content for status indicators',
|
|
116
116
|
},
|
|
117
117
|
components: [
|
package/src/Avatar/Avatar.tsx
CHANGED
|
@@ -55,9 +55,13 @@ const CIRCLE_EDGE_OFFSET_RATIO = (1 - 1 / Math.SQRT2) / 2;
|
|
|
55
55
|
const INITIALS_FONT_SIZE_RATIO = 0.4;
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
* Named size options
|
|
58
|
+
* Named size options.
|
|
59
|
+
*
|
|
60
|
+
* Avatar uses the same abbreviated scale as Icon (`xsm`/`sm`/`md`/`lg`/`xl`),
|
|
61
|
+
* but the values are larger because avatars align with media rather than
|
|
62
|
+
* glyphs. The tiers match EPS's avatar sizes.
|
|
59
63
|
*/
|
|
60
|
-
type AvatarNamedSize = '
|
|
64
|
+
type AvatarNamedSize = 'xsm' | 'sm' | 'md' | 'lg' | 'xl';
|
|
61
65
|
|
|
62
66
|
/**
|
|
63
67
|
* Numeric size options (in pixels)
|
|
@@ -78,15 +82,15 @@ export function resolveSize(size: AvatarSize): number {
|
|
|
78
82
|
return size;
|
|
79
83
|
}
|
|
80
84
|
switch (size) {
|
|
81
|
-
case '
|
|
85
|
+
case 'xsm':
|
|
82
86
|
return 20;
|
|
83
|
-
case '
|
|
87
|
+
case 'sm':
|
|
84
88
|
return 24;
|
|
85
|
-
case '
|
|
89
|
+
case 'md':
|
|
86
90
|
return 36;
|
|
87
|
-
case '
|
|
91
|
+
case 'lg':
|
|
88
92
|
return 48;
|
|
89
|
-
case '
|
|
93
|
+
case 'xl':
|
|
90
94
|
return 128;
|
|
91
95
|
}
|
|
92
96
|
}
|
|
@@ -198,8 +202,9 @@ export interface AvatarProps extends BaseProps<HTMLDivElement> {
|
|
|
198
202
|
*/
|
|
199
203
|
name?: string;
|
|
200
204
|
/**
|
|
201
|
-
* The size of the avatar.
|
|
202
|
-
*
|
|
205
|
+
* The size of the avatar. A named size (`xsm` 20px, `sm` 24px, `md` 36px,
|
|
206
|
+
* `lg` 48px, `xl` 128px) or a specific pixel value.
|
|
207
|
+
* @default 'md'
|
|
203
208
|
*/
|
|
204
209
|
size?: AvatarSize;
|
|
205
210
|
/**
|
|
@@ -258,7 +263,7 @@ function DefaultIcon({size}: {size: number}) {
|
|
|
258
263
|
* @example
|
|
259
264
|
* ```
|
|
260
265
|
* <Avatar src="/user.jpg" name="John Doe" />
|
|
261
|
-
* <Avatar name="Jane Smith" size="
|
|
266
|
+
* <Avatar name="Jane Smith" size="xl" />
|
|
262
267
|
* <Avatar src="/user.jpg" status={<OnlineIndicator />} />
|
|
263
268
|
* ```
|
|
264
269
|
*/
|
|
@@ -267,7 +272,7 @@ export function Avatar({
|
|
|
267
272
|
'data-testid': testId,
|
|
268
273
|
fallbackSrc,
|
|
269
274
|
name,
|
|
270
|
-
size = '
|
|
275
|
+
size = 'md',
|
|
271
276
|
src,
|
|
272
277
|
status,
|
|
273
278
|
xstyle,
|
|
@@ -18,7 +18,7 @@ import {createContext} from 'react';
|
|
|
18
18
|
* Context that provides the resolved numeric avatar size (in pixels)
|
|
19
19
|
* to child components like AvatarStatusDot.
|
|
20
20
|
*
|
|
21
|
-
* Default value of 36 matches the default '
|
|
21
|
+
* Default value of 36 matches the default 'md' avatar size.
|
|
22
22
|
*/
|
|
23
23
|
export const AvatarSizeContext = createContext<number>(36);
|
|
24
24
|
AvatarSizeContext.displayName = 'AvatarSizeContext';
|
|
@@ -168,12 +168,12 @@ const variantStyleMap: Partial<
|
|
|
168
168
|
* ```
|
|
169
169
|
* <Avatar
|
|
170
170
|
* name="John Doe"
|
|
171
|
-
* size="
|
|
171
|
+
* size="lg"
|
|
172
172
|
* status={<AvatarStatusDot variant="success" label="Online" />}
|
|
173
173
|
* />
|
|
174
174
|
* <Avatar
|
|
175
175
|
* name="Jane Smith"
|
|
176
|
-
* size="
|
|
176
|
+
* size="xl"
|
|
177
177
|
* status={<AvatarStatusDot variant="success" label="Verified" icon={<CheckIcon />} />}
|
|
178
178
|
* />
|
|
179
179
|
* ```
|
|
@@ -56,14 +56,14 @@ describe('AvatarGroup', () => {
|
|
|
56
56
|
|
|
57
57
|
it('applies size class to the group', () => {
|
|
58
58
|
render(
|
|
59
|
-
<AvatarGroup size="
|
|
59
|
+
<AvatarGroup size="lg">
|
|
60
60
|
<Avatar name="Alice" />
|
|
61
61
|
</AvatarGroup>,
|
|
62
62
|
);
|
|
63
63
|
|
|
64
64
|
const group = screen.getByRole('group');
|
|
65
65
|
expect(group.className).toContain('astryx-avatar-group');
|
|
66
|
-
expect(group.className).toContain('
|
|
66
|
+
expect(group.className).toContain('lg');
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
it('renders empty group when no children', () => {
|
|
@@ -134,7 +134,7 @@ describe('AvatarGroupOverflow', () => {
|
|
|
134
134
|
const visibleCount = 3;
|
|
135
135
|
|
|
136
136
|
render(
|
|
137
|
-
<AvatarGroup size="
|
|
137
|
+
<AvatarGroup size="lg">
|
|
138
138
|
{users.slice(0, visibleCount).map(name => (
|
|
139
139
|
<Avatar key={name} name={name} />
|
|
140
140
|
))}
|
|
@@ -211,4 +211,18 @@ describe('AvatarGroupOverflow — hardening', () => {
|
|
|
211
211
|
|
|
212
212
|
expect(screen.getByText('+999')).toBeInTheDocument();
|
|
213
213
|
});
|
|
214
|
+
|
|
215
|
+
it('renders the full "+N" text for wide multi-digit counts', () => {
|
|
216
|
+
// The indicator grows into a pill for long counts, so the entire number
|
|
217
|
+
// must remain present (nothing clipped away in the DOM).
|
|
218
|
+
render(
|
|
219
|
+
<AvatarGroup>
|
|
220
|
+
<Avatar name="Alice" />
|
|
221
|
+
<AvatarGroupOverflow count={4912} />
|
|
222
|
+
</AvatarGroup>,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(screen.getByText('+4912')).toBeInTheDocument();
|
|
226
|
+
expect(screen.getByLabelText('4912 more')).toBeInTheDocument();
|
|
227
|
+
});
|
|
214
228
|
});
|
|
@@ -39,7 +39,7 @@ export interface AvatarGroupProps extends BaseProps<HTMLDivElement> {
|
|
|
39
39
|
children: ReactNode;
|
|
40
40
|
/**
|
|
41
41
|
* Size applied to all avatars via context.
|
|
42
|
-
* @default '
|
|
42
|
+
* @default 'md'
|
|
43
43
|
*/
|
|
44
44
|
size?: AvatarSize;
|
|
45
45
|
/**
|
|
@@ -65,7 +65,7 @@ const styles = stylex.create({
|
|
|
65
65
|
*
|
|
66
66
|
* @example
|
|
67
67
|
* ```
|
|
68
|
-
* <AvatarGroup size="
|
|
68
|
+
* <AvatarGroup size="lg">
|
|
69
69
|
* {users.slice(0, 3).map(u => (
|
|
70
70
|
* <Avatar key={u.id} src={u.src} name={u.name} />
|
|
71
71
|
* ))}
|
|
@@ -75,7 +75,7 @@ const styles = stylex.create({
|
|
|
75
75
|
*/
|
|
76
76
|
export function AvatarGroup({
|
|
77
77
|
children,
|
|
78
|
-
size = '
|
|
78
|
+
size = 'md',
|
|
79
79
|
'data-testid': testId,
|
|
80
80
|
'aria-label': ariaLabelFromProps,
|
|
81
81
|
xstyle,
|
|
@@ -51,7 +51,7 @@ export const docs = {
|
|
|
51
51
|
'AvatarGroupOverflow appears at the end of an AvatarGroup to summarize people who are not shown individually. Use it when a group is sliced to a small number of visible avatars but the hidden count still matters.',
|
|
52
52
|
bestPractices: [
|
|
53
53
|
{guidance: true, description: 'Pass the real hidden count to `count` so the accessible label matches the visible indicator.'},
|
|
54
|
-
{guidance: true, description: 'Use short custom text such as `+12` or `99
|
|
54
|
+
{guidance: true, description: 'Use short custom text such as `+12` or `99+`. The indicator is circular for short counts and grows into a pill for wider counts so the number always fits.'},
|
|
55
55
|
{guidance: true, description: 'Provide `onClick` when the overflow opens a member list, popover, or detail view.'},
|
|
56
56
|
{guidance: false, description: 'Do not use long labels inside the indicator; place longer participant details next to the group instead.'},
|
|
57
57
|
],
|
|
@@ -64,7 +64,7 @@ export const docs = {
|
|
|
64
64
|
{
|
|
65
65
|
label: 'Default overflow count',
|
|
66
66
|
code: `
|
|
67
|
-
<AvatarGroup size="
|
|
67
|
+
<AvatarGroup size="lg">
|
|
68
68
|
{users.slice(0, 3).map(user => (
|
|
69
69
|
<Avatar key={user.id} src={user.src} name={user.name} />
|
|
70
70
|
))}
|
|
@@ -75,7 +75,7 @@ export const docs = {
|
|
|
75
75
|
{
|
|
76
76
|
label: 'Custom count text',
|
|
77
77
|
code: `
|
|
78
|
-
<AvatarGroup size="
|
|
78
|
+
<AvatarGroup size="lg">
|
|
79
79
|
{users.slice(0, 3).map(user => (
|
|
80
80
|
<Avatar key={user.id} src={user.src} name={user.name} />
|
|
81
81
|
))}
|
|
@@ -104,7 +104,7 @@ export const docsZh = {
|
|
|
104
104
|
'AvatarGroupOverflow 显示在 AvatarGroup 末尾,用来汇总未单独展示的成员。适用于只展示少量头像但仍需要显示隐藏数量的场景。',
|
|
105
105
|
bestPractices: [
|
|
106
106
|
{guidance: true, description: '向 `count` 传入真实隐藏数量,确保无障碍标签与可见指示器一致。'},
|
|
107
|
-
{guidance: true, description: '使用 `+12` 或 `99+`
|
|
107
|
+
{guidance: true, description: '使用 `+12` 或 `99+` 等短文本;指示器在计数较短时为圆形,计数较宽时会拉伸为胶囊形,确保数字始终完整显示。'},
|
|
108
108
|
{guidance: true, description: '当溢出项会打开成员列表、弹出层或详情视图时提供 `onClick`。'},
|
|
109
109
|
{guidance: false, description: '不要在指示器内使用长标签;更长的参与者详情应放在头像组旁边。'},
|
|
110
110
|
],
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
typographyVars,
|
|
21
21
|
fontWeightVars,
|
|
22
22
|
radiusVars,
|
|
23
|
+
spacingVars,
|
|
23
24
|
} from '../theme/tokens.stylex';
|
|
24
25
|
import {mergeProps} from '../utils';
|
|
25
26
|
import {useAvatarGroup} from './AvatarGroupContext';
|
|
@@ -67,13 +68,21 @@ const styles = stylex.create({
|
|
|
67
68
|
borderWidth: BORDER_WIDTH,
|
|
68
69
|
borderStyle: 'solid',
|
|
69
70
|
borderColor: colorVars['--color-background-surface'],
|
|
70
|
-
|
|
71
|
+
// border-box so the border and inline padding are included in the box
|
|
72
|
+
// size: a short "+N" stays a circle at exactly the avatar size, while
|
|
73
|
+
// longer content pushes past the min width and grows into a pill.
|
|
74
|
+
boxSizing: 'border-box',
|
|
75
|
+
// Horizontal breathing room so multi-digit "+N" counts don't crowd the
|
|
76
|
+
// edges once the indicator grows into a pill.
|
|
77
|
+
paddingInline: spacingVars['--spacing-2'],
|
|
71
78
|
// Neutral tint layer (preserves opaque base underneath)
|
|
72
79
|
backgroundImage: `linear-gradient(${colorVars['--color-neutral']}, ${colorVars['--color-neutral']})`,
|
|
73
80
|
},
|
|
74
81
|
button: {
|
|
75
82
|
cursor: 'pointer',
|
|
76
|
-
padding
|
|
83
|
+
// Reset the UA button's block padding only; the inline padding from `base`
|
|
84
|
+
// provides the pill's breathing room and must be preserved.
|
|
85
|
+
paddingBlock: 0,
|
|
77
86
|
// Interactive overlay states layered on top via backgroundImage
|
|
78
87
|
backgroundImage: {
|
|
79
88
|
default: `linear-gradient(${colorVars['--color-neutral']}, ${colorVars['--color-neutral']})`,
|
|
@@ -99,8 +108,15 @@ const styles = stylex.create({
|
|
|
99
108
|
|
|
100
109
|
const dynamicStyles = stylex.create({
|
|
101
110
|
size: (s: number) => ({
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
// Pin height to the avatar's rendered size and enforce the same value as a
|
|
112
|
+
// *minimum* width, so short counts (`+5`) render a perfect circle. With
|
|
113
|
+
// border-box, the inline padding lives inside this size; longer content
|
|
114
|
+
// (`+4912`) pushes past the min width and grows into a stadium/pill.
|
|
115
|
+
// The border is added to the declared size (like the avatars' ring, which
|
|
116
|
+
// uses content-box + a 2px border) to keep the indicator the same overall
|
|
117
|
+
// size as its sibling avatars.
|
|
118
|
+
minWidth: s + BORDER_WIDTH * 2,
|
|
119
|
+
height: s + BORDER_WIDTH * 2,
|
|
104
120
|
}),
|
|
105
121
|
fontSize: (s: number) => ({
|
|
106
122
|
fontSize: s * OVERFLOW_FONT_RATIO,
|
|
@@ -116,7 +132,7 @@ const dynamicStyles = stylex.create({
|
|
|
116
132
|
*
|
|
117
133
|
* @example
|
|
118
134
|
* ```
|
|
119
|
-
* <AvatarGroup size="
|
|
135
|
+
* <AvatarGroup size="lg">
|
|
120
136
|
* {users.slice(0, 3).map(u => (
|
|
121
137
|
* <Avatar key={u.id} src={u.src} name={u.name} />
|
|
122
138
|
* ))}
|
|
@@ -14,6 +14,7 @@ export const docs = {
|
|
|
14
14
|
{guidance: true, description: 'Enable scroll-snap when each item should land precisely at the start edge, like a gallery or product list.'},
|
|
15
15
|
{guidance: true, description: 'Always provide an aria-label that describes what the carousel contains, like "Featured products" or "Team members".'},
|
|
16
16
|
{guidance: true, description: 'Use a consistent gap and item width so the carousel looks intentional, not like content overflowing by accident.'},
|
|
17
|
+
{guidance: true, description: 'Trust the built-in navigation: trackpad users can swipe horizontally, and mouse users can hold Shift while scrolling the wheel to move through items.'},
|
|
17
18
|
{guidance: false, description: 'Use a carousel for content every user must see. Not everyone scrolls horizontally, so put critical content above the fold.'},
|
|
18
19
|
{guidance: false, description: 'Auto-advance items. Let the user scroll at their own pace.'},
|
|
19
20
|
{guidance: false, description: 'Nest carousels. A carousel inside a carousel is confusing and breaks keyboard navigation.'},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
3
|
import {describe, it, expect, vi} from 'vitest';
|
|
4
|
-
import {render, screen} from '@testing-library/react';
|
|
4
|
+
import {render, screen, fireEvent} from '@testing-library/react';
|
|
5
5
|
import {Carousel} from './Carousel';
|
|
6
6
|
|
|
7
7
|
// Mock ResizeObserver (not available in jsdom)
|
|
@@ -111,4 +111,99 @@ describe('Carousel', () => {
|
|
|
111
111
|
expect(left).toBeDisabled();
|
|
112
112
|
expect(right).toBeDisabled();
|
|
113
113
|
});
|
|
114
|
+
|
|
115
|
+
describe('Shift + wheel horizontal scroll', () => {
|
|
116
|
+
// jsdom doesn't lay out elements, so we fake an overflowing scroll
|
|
117
|
+
// container and capture scrollBy calls.
|
|
118
|
+
function getScroller() {
|
|
119
|
+
const region = screen.getByRole('region');
|
|
120
|
+
return region.firstElementChild as HTMLElement;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function makeOverflowing(el: HTMLElement) {
|
|
124
|
+
Object.defineProperty(el, 'scrollWidth', {
|
|
125
|
+
value: 500,
|
|
126
|
+
configurable: true,
|
|
127
|
+
});
|
|
128
|
+
Object.defineProperty(el, 'clientWidth', {
|
|
129
|
+
value: 200,
|
|
130
|
+
configurable: true,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
it('maps Shift + vertical wheel to horizontal scroll', () => {
|
|
135
|
+
render(
|
|
136
|
+
<Carousel aria-label="Wheel">
|
|
137
|
+
<div>Item 1</div>
|
|
138
|
+
<div>Item 2</div>
|
|
139
|
+
</Carousel>,
|
|
140
|
+
);
|
|
141
|
+
const scroller = getScroller();
|
|
142
|
+
makeOverflowing(scroller);
|
|
143
|
+
const scrollBy = vi.fn();
|
|
144
|
+
scroller.scrollBy = scrollBy;
|
|
145
|
+
|
|
146
|
+
fireEvent.wheel(scroller, {shiftKey: true, deltaY: 120, deltaX: 0});
|
|
147
|
+
|
|
148
|
+
expect(scrollBy).toHaveBeenCalledWith({left: 120, behavior: 'auto'});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('does not translate wheel without Shift held', () => {
|
|
152
|
+
render(
|
|
153
|
+
<Carousel aria-label="Wheel">
|
|
154
|
+
<div>Item 1</div>
|
|
155
|
+
<div>Item 2</div>
|
|
156
|
+
</Carousel>,
|
|
157
|
+
);
|
|
158
|
+
const scroller = getScroller();
|
|
159
|
+
makeOverflowing(scroller);
|
|
160
|
+
const scrollBy = vi.fn();
|
|
161
|
+
scroller.scrollBy = scrollBy;
|
|
162
|
+
|
|
163
|
+
fireEvent.wheel(scroller, {shiftKey: false, deltaY: 120, deltaX: 0});
|
|
164
|
+
|
|
165
|
+
expect(scrollBy).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('leaves native horizontal wheel deltas alone', () => {
|
|
169
|
+
// Trackpads emit deltaX directly — don't double-handle those.
|
|
170
|
+
render(
|
|
171
|
+
<Carousel aria-label="Wheel">
|
|
172
|
+
<div>Item 1</div>
|
|
173
|
+
<div>Item 2</div>
|
|
174
|
+
</Carousel>,
|
|
175
|
+
);
|
|
176
|
+
const scroller = getScroller();
|
|
177
|
+
makeOverflowing(scroller);
|
|
178
|
+
const scrollBy = vi.fn();
|
|
179
|
+
scroller.scrollBy = scrollBy;
|
|
180
|
+
|
|
181
|
+
fireEvent.wheel(scroller, {shiftKey: true, deltaY: 120, deltaX: 40});
|
|
182
|
+
|
|
183
|
+
expect(scrollBy).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('does not intercept when there is no horizontal overflow', () => {
|
|
187
|
+
render(
|
|
188
|
+
<Carousel aria-label="Wheel">
|
|
189
|
+
<div>Item 1</div>
|
|
190
|
+
</Carousel>,
|
|
191
|
+
);
|
|
192
|
+
const scroller = getScroller();
|
|
193
|
+
Object.defineProperty(scroller, 'scrollWidth', {
|
|
194
|
+
value: 200,
|
|
195
|
+
configurable: true,
|
|
196
|
+
});
|
|
197
|
+
Object.defineProperty(scroller, 'clientWidth', {
|
|
198
|
+
value: 200,
|
|
199
|
+
configurable: true,
|
|
200
|
+
});
|
|
201
|
+
const scrollBy = vi.fn();
|
|
202
|
+
scroller.scrollBy = scrollBy;
|
|
203
|
+
|
|
204
|
+
fireEvent.wheel(scroller, {shiftKey: true, deltaY: 120, deltaX: 0});
|
|
205
|
+
|
|
206
|
+
expect(scrollBy).not.toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
114
209
|
});
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* @input Uses React, StyleX, useScrollOverflow, useLayer, Button, Icon, theme tokens
|
|
8
8
|
* @output Exports Carousel component
|
|
9
9
|
* @position Horizontal scroll container with fade-edge overflow indication,
|
|
10
|
-
* optional prev/next buttons on the top layer, scroll-snap,
|
|
11
|
-
* visual bleed allowance for child selection indicators
|
|
10
|
+
* optional prev/next buttons on the top layer, scroll-snap, a 1px
|
|
11
|
+
* visual bleed allowance for child selection indicators, and Shift + wheel
|
|
12
|
+
* mapping so mouse users can scroll horizontally.
|
|
12
13
|
*
|
|
13
14
|
* SYNC: When modified, update:
|
|
14
15
|
* - /packages/core/src/Carousel/index.ts (exports)
|
|
@@ -290,6 +291,31 @@ export function Carousel({
|
|
|
290
291
|
[scrollRef],
|
|
291
292
|
);
|
|
292
293
|
|
|
294
|
+
// Map Shift + vertical wheel to horizontal scroll. Trackpads emit
|
|
295
|
+
// horizontal deltas natively, but a standard mouse only produces deltaY —
|
|
296
|
+
// so mouse users can't wheel-scroll a horizontal container. Shift + wheel
|
|
297
|
+
// is the long-established convention for horizontal scroll containers; we
|
|
298
|
+
// honor it by translating the vertical delta into a horizontal scroll.
|
|
299
|
+
//
|
|
300
|
+
// Only kicks in when Shift is held and the wheel is purely vertical
|
|
301
|
+
// (deltaX === 0), so native trackpad horizontal scrolling is untouched.
|
|
302
|
+
const handleWheel = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
|
|
303
|
+
if (!event.shiftKey || event.deltaY === 0 || event.deltaX !== 0) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const el = scrollElRef.current;
|
|
307
|
+
if (!el) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
// Nothing to scroll horizontally — let the event fall through so the page
|
|
311
|
+
// can scroll as it normally would.
|
|
312
|
+
if (el.scrollWidth <= el.clientWidth) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
event.preventDefault();
|
|
316
|
+
el.scrollBy({left: event.deltaY, behavior: 'auto'});
|
|
317
|
+
}, []);
|
|
318
|
+
|
|
293
319
|
const scrollBy = useCallback((direction: -1 | 1) => {
|
|
294
320
|
const el = scrollElRef.current;
|
|
295
321
|
if (!el) {
|
|
@@ -346,6 +372,7 @@ export function Carousel({
|
|
|
346
372
|
<div
|
|
347
373
|
ref={composedRef}
|
|
348
374
|
tabIndex={0}
|
|
375
|
+
onWheel={handleWheel}
|
|
349
376
|
{...stylex.props(
|
|
350
377
|
styles.scroller,
|
|
351
378
|
gapStyles[gap],
|
package/src/Chat/Chat.doc.mjs
CHANGED
|
@@ -80,7 +80,7 @@ export const docs = {
|
|
|
80
80
|
{ name: 'Scroll-to-bottom button', required: false, description: 'Appears when the user scrolls up or new messages arrive. Defaults to ChatLayoutScrollButton; pass null to hide or a custom element to override.' },
|
|
81
81
|
{ name: 'Composer', required: true, description: 'The input area for sending messages, typically ChatComposer. Docked at the bottom inside the frosted glass layer.' },
|
|
82
82
|
{ name: 'Empty state', required: false, description: 'Centered placeholder shown when no messages exist. Use EmptyState for a consistent look.' },
|
|
83
|
-
{ name: 'Avatar', required: false, description: 'A sender avatar rendered beside the message. Typically Avatar with size="
|
|
83
|
+
{ name: 'Avatar', required: false, description: 'A sender avatar rendered beside the message. Typically Avatar with size="md". Hidden for system messages.' },
|
|
84
84
|
{ name: 'Name', required: false, description: 'Sender name above the message body. Place on the bubble when using bubbles, or on the message wrapper for raw content.' },
|
|
85
85
|
{ name: 'Content', required: true, description: 'The message body: one or more ChatMessageBubble elements, or any free-form ReactNode like images or tool calls.' },
|
|
86
86
|
{ name: 'Metadata', required: false, description: 'Timestamp, delivery status, and footer actions below the message. Place on the last bubble or on the message wrapper.' },
|