@cratis/components 4.19.2 → 4.21.0
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/README.md +2 -2
- package/compat-manifest.json +11 -11
- package/dist/esm/Chat/ChatConversation.d.ts +8 -1
- package/dist/esm/Chat/ChatConversation.d.ts.map +1 -1
- package/dist/esm/Chat/ChatConversation.js +2 -2
- package/dist/esm/Chat/ChatConversation.js.map +1 -1
- package/dist/esm/Chat/ChatSidebar.d.ts +12 -2
- package/dist/esm/Chat/ChatSidebar.d.ts.map +1 -1
- package/dist/esm/Chat/ChatSidebar.js +2 -2
- package/dist/esm/Chat/ChatSidebar.js.map +1 -1
- package/dist/esm/Chat/ChatTopicAction.d.ts +23 -0
- package/dist/esm/Chat/ChatTopicAction.d.ts.map +1 -0
- package/dist/esm/Chat/ChatTopicAction.js +4 -0
- package/dist/esm/Chat/ChatTopicAction.js.map +1 -0
- package/dist/esm/Chat/ChatTopicActions.d.ts +25 -0
- package/dist/esm/Chat/ChatTopicActions.d.ts.map +1 -0
- package/dist/esm/Chat/ChatTopicActions.js +6 -0
- package/dist/esm/Chat/ChatTopicActions.js.map +1 -0
- package/dist/esm/Chat/ChatTopicList.d.ts +5 -2
- package/dist/esm/Chat/ChatTopicList.d.ts.map +1 -1
- package/dist/esm/Chat/ChatTopicList.js +17 -9
- package/dist/esm/Chat/ChatTopicList.js.map +1 -1
- package/dist/esm/Chat/index.d.ts +1 -0
- package/dist/esm/Chat/index.d.ts.map +1 -1
- package/dist/esm/Chat/useTopicActionFocusRecovery.d.ts +19 -0
- package/dist/esm/Chat/useTopicActionFocusRecovery.d.ts.map +1 -0
- package/dist/esm/Chat/useTopicActionFocusRecovery.js +41 -0
- package/dist/esm/Chat/useTopicActionFocusRecovery.js.map +1 -0
- package/dist/esm/Common/ActionMenubar.d.ts.map +1 -1
- package/dist/esm/Common/ActionMenubar.js +11 -6
- package/dist/esm/Common/ActionMenubar.js.map +1 -1
- package/dist/esm/Common/ToolbarFocusMode.d.ts +9 -1
- package/dist/esm/Common/ToolbarFocusMode.d.ts.map +1 -1
- package/dist/esm/Common/ToolbarFocusMode.js +1 -0
- package/dist/esm/Common/ToolbarFocusMode.js.map +1 -1
- package/dist/esm/Common/ToolbarRovingContext.d.ts +39 -0
- package/dist/esm/Common/ToolbarRovingContext.d.ts.map +1 -0
- package/dist/esm/Common/ToolbarRovingContext.js +40 -0
- package/dist/esm/Common/ToolbarRovingContext.js.map +1 -0
- package/dist/esm/Common/useToolbarKeyboardNavigation.d.ts +2 -0
- package/dist/esm/Common/useToolbarKeyboardNavigation.d.ts.map +1 -1
- package/dist/esm/Common/useToolbarKeyboardNavigation.js +164 -15
- package/dist/esm/Common/useToolbarKeyboardNavigation.js.map +1 -1
- package/dist/esm/DataTables/DataTableCore.d.ts +19 -1
- package/dist/esm/DataTables/DataTableCore.d.ts.map +1 -1
- package/dist/esm/DataTables/DataTableCore.js +19 -14
- package/dist/esm/DataTables/DataTableCore.js.map +1 -1
- package/dist/esm/DataTables/DataTableRowProcessing.d.ts +11 -0
- package/dist/esm/DataTables/DataTableRowProcessing.d.ts.map +1 -0
- package/dist/esm/DataTables/DataTableRowProcessing.js +8 -0
- package/dist/esm/DataTables/DataTableRowProcessing.js.map +1 -0
- package/dist/esm/DataTables/DataTableSort.d.ts +9 -0
- package/dist/esm/DataTables/DataTableSort.d.ts.map +1 -0
- package/dist/esm/DataTables/DataTableSort.js +4 -0
- package/dist/esm/DataTables/DataTableSort.js.map +1 -0
- package/dist/esm/DataTables/DataTableSortDirection.d.ts +8 -0
- package/dist/esm/DataTables/DataTableSortDirection.d.ts.map +1 -0
- package/dist/esm/DataTables/DataTableSortDirection.js +8 -0
- package/dist/esm/DataTables/DataTableSortDirection.js.map +1 -0
- package/dist/esm/DataTables/index.d.ts +3 -0
- package/dist/esm/DataTables/index.d.ts.map +1 -1
- package/dist/esm/DataTables/index.js +2 -0
- package/dist/esm/DataTables/index.js.map +1 -1
- package/dist/esm/DataTables/useControllableState.d.ts +10 -0
- package/dist/esm/DataTables/useControllableState.d.ts.map +1 -0
- package/dist/esm/DataTables/useControllableState.js +15 -0
- package/dist/esm/DataTables/useControllableState.js.map +1 -0
- package/dist/esm/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/esm/Toolbar/Toolbar.js +3 -2
- package/dist/esm/Toolbar/Toolbar.js.map +1 -1
- package/dist/esm/Toolbar/ToolbarButton.d.ts.map +1 -1
- package/dist/esm/Toolbar/ToolbarButton.js +4 -2
- package/dist/esm/Toolbar/ToolbarButton.js.map +1 -1
- package/dist/esm/Toolbar/ToolbarFanOutItem.d.ts.map +1 -1
- package/dist/esm/Toolbar/ToolbarFanOutItem.js +10 -2
- package/dist/esm/Toolbar/ToolbarFanOutItem.js.map +1 -1
- package/dist/esm/Toolbar/ToolbarFolder.d.ts.map +1 -1
- package/dist/esm/Toolbar/ToolbarFolder.js +10 -2
- package/dist/esm/Toolbar/ToolbarFolder.js.map +1 -1
- package/dist/esm/styles.Canvas.css +17 -1
- package/dist/esm/styles.Chat.css +17 -1
- package/dist/esm/styles.css +17 -1
- package/package.json +8 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatTopicActions.d.ts","sourceRoot":"","sources":["../../../Chat/ChatTopicActions.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAmB,CAAC;AAEzD;;;GAGG;AACH,MAAM,WAAW,qBAAqB,CAAC,MAAM,SAAS,SAAS;IAC3D,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,2FAA2F;IAC3F,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,4CAA4C;IAC5C,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC;IACnC,gDAAgD;IAChD,aAAa,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IACpD,gDAAgD;IAChD,YAAY,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;CACtD;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,SAAS,SAAS,+DAMtD,qBAAqB,CAAC,MAAM,CAAC,gCAoB/B,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx } from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
const ChatTopicActions = ({ topic, topicName, actions, onActionFocus, onActionBlur, }) => (jsx("div", { className: 'cratis-chat-message__actions', children: actions.map((action) => (jsx("button", { type: 'button', className: 'cratis-chat-message__action', style: action.icon == null ? { width: 'auto', padding: '0 0.375rem' } : undefined, title: action.label, "aria-label": `${action.label} ${topicName}`, onFocus: onActionFocus, onBlur: onActionBlur, onClick: () => action.onInvoke(topic), children: typeof action.icon === 'string' ? (jsx("i", { className: action.icon, "aria-hidden": 'true' })) : (action.icon ?? action.label) }, action.id))) }));
|
|
4
|
+
|
|
5
|
+
export { ChatTopicActions };
|
|
6
|
+
//# sourceMappingURL=ChatTopicActions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatTopicActions.js","sources":["../../../Chat/ChatTopicActions.tsx"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport type { FocusEventHandler } from 'react';\nimport type { ChatTopic } from './ChatTopic';\nimport type { ChatTopicAction } from './ChatTopicAction';\n\n/**\n * Props for {@link ChatTopicActions}.\n * @typeParam TTopic The topic type the list renders.\n */\nexport interface ChatTopicActionsProps<TTopic extends ChatTopic> {\n /** The topic the actions act on. */\n topic: TTopic;\n /** The topic's visible name, used to give each action an accessible name for its topic. */\n topicName: string | undefined;\n /** The actions available for this topic. */\n actions: ChatTopicAction<TTopic>[];\n /** Called when an action button gains focus. */\n onActionFocus: FocusEventHandler<HTMLButtonElement>;\n /** Called when an action button loses focus. */\n onActionBlur: FocusEventHandler<HTMLButtonElement>;\n}\n\n/**\n * The action buttons shown beside a topic's opening button. Reuses the message action overlay,\n * which keyboard focus reveals.\n */\nexport const ChatTopicActions = <TTopic extends ChatTopic>({\n topic,\n topicName,\n actions,\n onActionFocus,\n onActionBlur,\n}: ChatTopicActionsProps<TTopic>) => (\n <div className='cratis-chat-message__actions'>\n {actions.map((action) => (\n <button\n key={action.id}\n type='button'\n className='cratis-chat-message__action'\n style={action.icon == null ? { width: 'auto', padding: '0 0.375rem' } : undefined}\n title={action.label}\n aria-label={`${action.label} ${topicName}`}\n onFocus={onActionFocus}\n onBlur={onActionBlur}\n onClick={() => action.onInvoke(topic)}\n >\n {typeof action.icon === 'string' ? (\n <i className={action.icon} aria-hidden='true' />\n ) : (action.icon ?? action.label)}\n </button>\n ))}\n </div>\n);\n"],"names":["_jsx"],"mappings":";;AA4BO,MAAM,gBAAgB,GAAG,CAA2B,EACvD,KAAK,EACL,SAAS,EACT,OAAO,EACP,aAAa,EACb,YAAY,GACgB,MAC5BA,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,8BAA8B,EAAA,QAAA,EACxC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,MAChBA,GAAA,CAAA,QAAA,EAAA,EAEI,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,6BAA6B,EACvC,KAAK,EAAE,MAAM,CAAC,IAAI,IAAI,IAAI,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,SAAS,EACjF,KAAK,EAAE,MAAM,CAAC,KAAK,EAAA,YAAA,EACP,CAAA,EAAG,MAAM,CAAC,KAAK,CAAA,CAAA,EAAI,SAAS,CAAA,CAAE,EAC1C,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,MAAM,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAA,QAAA,EAEpC,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,IAC5BA,GAAA,CAAA,GAAA,EAAA,EAAG,SAAS,EAAE,MAAM,CAAC,IAAI,EAAA,aAAA,EAAc,MAAM,EAAA,CAAG,KAC/C,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,EAAA,EAZ5B,MAAM,CAAC,EAAE,CAaT,CACZ,CAAC,EAAA,CACA;;"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
2
|
import { type BuildAvatarUrlParams } from './Kit/Avatar/index.js';
|
|
3
3
|
import type { ChatAuthor } from './ChatAuthor.js';
|
|
4
4
|
import { type ChatIdentifier } from './ChatIdentifier.js';
|
|
5
5
|
import type { ChatTopic } from './ChatTopic.js';
|
|
6
|
+
import type { ChatTopicAction } from './ChatTopicAction.js';
|
|
6
7
|
import { ChatStatus } from './ChatStatus.js';
|
|
7
8
|
import { type RelativeTimestampLabels } from './relativeTimestamp.js';
|
|
8
9
|
/** Overrides for every label the {@link ChatTopicList} renders. Any field left unset falls back
|
|
@@ -73,6 +74,8 @@ export interface ChatTopicListProps<TTopic extends ChatTopic = ChatTopic> {
|
|
|
73
74
|
buildAvatarUrl?: (params: BuildAvatarUrlParams) => string;
|
|
74
75
|
/** Overrides for every label rendered. Unset fields fall back to literal English defaults. */
|
|
75
76
|
labels?: ChatTopicListLabels;
|
|
77
|
+
/** The host's actions to offer on each topic where they are available. */
|
|
78
|
+
topicActions?: ChatTopicAction<TTopic>[];
|
|
76
79
|
/** Additional class name for the list's root element. */
|
|
77
80
|
className?: string;
|
|
78
81
|
}
|
|
@@ -82,5 +85,5 @@ export interface ChatTopicListProps<TTopic extends ChatTopic = ChatTopic> {
|
|
|
82
85
|
* host-side auto-naming, the name arrives through the data once the host has produced it, and
|
|
83
86
|
* the placeholder simply stops being needed.
|
|
84
87
|
*/
|
|
85
|
-
export declare const ChatTopicList: <TTopic extends ChatTopic = ChatTopic>({ topics, status, onOpen, onStart, authorOf, renderAvatar, isTopicUnnamed, buildAvatarUrl, labels, className, }: ChatTopicListProps<TTopic>) => import("react").JSX.Element;
|
|
88
|
+
export declare const ChatTopicList: <TTopic extends ChatTopic = ChatTopic>({ topics, status, onOpen, onStart, authorOf, renderAvatar, isTopicUnnamed, buildAvatarUrl, labels, topicActions, className, }: ChatTopicListProps<TTopic>) => import("react").JSX.Element;
|
|
86
89
|
//# sourceMappingURL=ChatTopicList.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatTopicList.d.ts","sourceRoot":"","sources":["../../../Chat/ChatTopicList.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"ChatTopicList.d.ts","sourceRoot":"","sources":["../../../Chat/ChatTopicList.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC/C,OAAO,EAAsB,KAAK,oBAAoB,EAAE,MAAM,uBAAc,CAAC;AAE7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAc,CAAC;AAC/C,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,qBAAkB,CAAC;AAC7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAmB,CAAC;AAEzD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAc,CAAC;AAE1C,OAAO,EAAqB,KAAK,uBAAuB,EAAE,MAAM,wBAAqB,CAAC;AAmBtF;oCACoC;AACpC,MAAM,WAAW,mBAAmB;IAChC,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,2FAA2F;IAC3F,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,4GAA4G;IAC5G,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,8GAA8G;IAC9G,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,+HAA+H;IAC/H,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,+FAA+F;IAC/F,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,2DAA2D;IAC3D,UAAU,CAAC,EAAE,uBAAuB,CAAC;CACxC;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB,CAAC,MAAM,SAAS,SAAS,GAAG,SAAS;IACpE;;;OAGG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;IAEjB,mKAAmK;IACnK,MAAM,CAAC,EAAE,UAAU,CAAC;IAEpB;;;OAGG;IACH,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,UAAU,CAAC;IAEpD;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,EAAE,MAAM,EAAE,UAAU,KAAK,SAAS,CAAC;IAE3E;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IAE5C,oFAAoF;IACpF,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,MAAM,CAAC;IAE1D,8FAA8F;IAC9F,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAE7B,0EAA0E;IAC1E,YAAY,CAAC,EAAE,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC;IAEzC,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,SAAS,SAAS,GAAG,SAAS,iIAY/D,kBAAkB,CAAC,MAAM,CAAC,gCAgI5B,CAAC"}
|
|
@@ -1,32 +1,40 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { useRef } from 'react';
|
|
2
3
|
import { PersonAvatarCircle } from './Kit/Avatar/PersonAvatarCircle.js';
|
|
3
4
|
import { ChatAuthorKind } from './Kit/ChatAuthorKind.js';
|
|
4
5
|
import { chatIdentifierString } from './ChatIdentifier.js';
|
|
6
|
+
import { ChatTopicActions } from './ChatTopicActions.js';
|
|
5
7
|
import { ChatStatus } from './ChatStatus.js';
|
|
6
8
|
import { isTopicUnnamed } from './isTopicUnnamed.js';
|
|
7
9
|
import { relativeTimestamp } from './relativeTimestamp.js';
|
|
10
|
+
import { useTopicActionFocusRecovery } from './useTopicActionFocusRecovery.js';
|
|
8
11
|
import { topicsByActivity } from './topicsByActivity.js';
|
|
9
12
|
|
|
10
13
|
const PlusIcon = () => (jsxs("svg", { width: '14', height: '14', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2.5', "aria-hidden": 'true', children: [jsx("line", { x1: '12', y1: '5', x2: '12', y2: '19' }), jsx("line", { x1: '5', y1: '12', x2: '19', y2: '12' })] }));
|
|
11
|
-
const ChatTopicList = ({ topics, status = ChatStatus.Ready, onOpen, onStart, authorOf, renderAvatar, isTopicUnnamed: isTopicUnnamed$1, buildAvatarUrl, labels, className, }) => {
|
|
14
|
+
const ChatTopicList = ({ topics, status = ChatStatus.Ready, onOpen, onStart, authorOf, renderAvatar, isTopicUnnamed: isTopicUnnamed$1, buildAvatarUrl, labels, topicActions, className, }) => {
|
|
12
15
|
const unnamed = isTopicUnnamed$1 ?? isTopicUnnamed;
|
|
16
|
+
const listRef = useRef(null);
|
|
17
|
+
const orderedTopics = status === ChatStatus.Unauthorized ? [] : topicsByActivity(topics);
|
|
18
|
+
const { onActionFocus, onActionBlur } = useTopicActionFocusRecovery(listRef, orderedTopics.map((topic) => chatIdentifierString(topic.id)));
|
|
13
19
|
const authorFor = (authorId) => authorOf?.(authorId) ?? {
|
|
14
20
|
name: chatIdentifierString(authorId),
|
|
15
21
|
kind: ChatAuthorKind.User,
|
|
16
22
|
};
|
|
17
|
-
return (jsxs("div", { className: `cratis-chat-topics${className ? ` ${className}` : ''}`, "aria-busy": status === ChatStatus.Loading && topics.length > 0 || undefined, children: [onStart && (jsxs("button", { type: 'button', className: 'cratis-chat-topics__start', disabled: status === ChatStatus.Unauthorized, onClick: onStart, children: [jsx(PlusIcon, {}), jsx("span", { children: labels?.newTopic ?? 'New topic' })] })), (topics.length === 0 || status === ChatStatus.Failed || status === ChatStatus.Unauthorized) && (jsx("p", { className: 'cratis-chat-topics__empty', children: status === ChatStatus.Loading ? (jsx("span", { role: 'status', children: labels?.loading ?? 'Loading topics…' }, status)) : status === ChatStatus.Failed ? (jsx("span", { role: 'alert', children: labels?.failed ?? 'Could not load topics.' }, status)) : status === ChatStatus.Unauthorized ? (jsx("span", { role: 'alert', children: labels?.unauthorized ?? 'You are not authorized to view these topics.' }, status)) : (labels?.empty ?? 'No topics yet. Start the first one!') })), jsx("ul", { className: 'cratis-chat-topics__list', children: status !== ChatStatus.Unauthorized &&
|
|
23
|
+
return (jsxs("div", { className: `cratis-chat-topics${className ? ` ${className}` : ''}`, "aria-busy": status === ChatStatus.Loading && topics.length > 0 || undefined, children: [onStart && (jsxs("button", { type: 'button', className: 'cratis-chat-topics__start', disabled: status === ChatStatus.Unauthorized, onClick: onStart, children: [jsx(PlusIcon, {}), jsx("span", { children: labels?.newTopic ?? 'New topic' })] })), (topics.length === 0 || status === ChatStatus.Failed || status === ChatStatus.Unauthorized) && (jsx("p", { className: 'cratis-chat-topics__empty', children: status === ChatStatus.Loading ? (jsx("span", { role: 'status', children: labels?.loading ?? 'Loading topics…' }, status)) : status === ChatStatus.Failed ? (jsx("span", { role: 'alert', children: labels?.failed ?? 'Could not load topics.' }, status)) : status === ChatStatus.Unauthorized ? (jsx("span", { role: 'alert', children: labels?.unauthorized ?? 'You are not authorized to view these topics.' }, status)) : (labels?.empty ?? 'No topics yet. Start the first one!') })), jsx("ul", { ref: listRef, className: 'cratis-chat-topics__list', children: status !== ChatStatus.Unauthorized && orderedTopics.map((topic) => {
|
|
18
24
|
const starter = topic.startedBy === undefined
|
|
19
25
|
? undefined
|
|
20
26
|
: authorFor(topic.startedBy);
|
|
21
27
|
const isPending = unnamed(topic);
|
|
22
28
|
const activity = topic.lastActivity ?? topic.started;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
const topicName = isPending ? (labels?.unnamedTopic ?? 'New topic') : topic.name;
|
|
30
|
+
const availableActions = (topicActions ?? []).filter((action) => action.isAvailable?.(topic) ?? true);
|
|
31
|
+
const topicButton = (jsxs("button", { type: 'button', className: 'cratis-chat-topics__topic', onClick: () => onOpen(topic), children: [topic.startedBy !== undefined &&
|
|
32
|
+
starter &&
|
|
33
|
+
(renderAvatar ? (renderAvatar(topic.startedBy, starter)) : (jsx(PersonAvatarCircle, { userId: chatIdentifierString(topic.startedBy), name: starter.name, hasAvatar: starter.hasAvatar ?? false, size: 28, ownerType: starter.kind === ChatAuthorKind.Agent
|
|
34
|
+
? 'Agents'
|
|
35
|
+
: 'Users', version: starter.avatarVersion, buildAvatarUrl: buildAvatarUrl }))), jsxs("span", { className: 'cratis-chat-topics__details', children: [jsx("span", { className: `cratis-chat-topics__name${isPending ? ' cratis-chat-topics__name--pending' : ''}`, children: topicName }), starter && (jsx("span", { className: 'cratis-chat-topics__started-by', children: (labels?.startedBy ?? 'Started by {name}').replace('{name}', starter.name) }))] }), activity !== undefined && (jsx("span", { className: 'cratis-chat-topics__activity', children: relativeTimestamp(activity, new Date(), labels?.timestamps) }))] }));
|
|
36
|
+
const topicKey = chatIdentifierString(topic.id);
|
|
37
|
+
return (jsxs("li", { className: availableActions.length > 0 ? 'cratis-chat-topics__row' : undefined, children: [topicButton, availableActions.length > 0 && (jsx(ChatTopicActions, { topic: topic, topicName: topicName, actions: availableActions, onActionFocus: onActionFocus(topicKey), onActionBlur: onActionBlur }))] }, topicKey));
|
|
30
38
|
}) })] }));
|
|
31
39
|
};
|
|
32
40
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatTopicList.js","sources":["../../../Chat/ChatTopicList.tsx"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport type { ReactNode } from 'react';\nimport { PersonAvatarCircle, type BuildAvatarUrlParams } from './Kit/Avatar';\nimport { ChatAuthorKind } from './Kit/ChatAuthorKind';\nimport type { ChatAuthor } from './ChatAuthor';\nimport { chatIdentifierString, type ChatIdentifier } from './ChatIdentifier';\nimport type { ChatTopic } from './ChatTopic';\nimport { ChatStatus } from './ChatStatus';\nimport { isTopicUnnamed as defaultIsTopicUnnamed } from './isTopicUnnamed';\nimport { relativeTimestamp, type RelativeTimestampLabels } from './relativeTimestamp';\nimport { topicsByActivity } from './topicsByActivity';\n\nconst PlusIcon = () => (\n <svg\n width='14'\n height='14'\n viewBox='0 0 24 24'\n fill='none'\n stroke='currentColor'\n strokeWidth='2.5'\n aria-hidden='true'\n >\n <line x1='12' y1='5' x2='12' y2='19' />\n <line x1='5' y1='12' x2='19' y2='12' />\n </svg>\n);\n\n/** Overrides for every label the {@link ChatTopicList} renders. Any field left unset falls back\n * to a literal English default. */\nexport interface ChatTopicListLabels {\n /** The new-topic button. Defaults to `'New topic'`. */\n newTopic?: string;\n\n /** Placeholder name for a topic that has not been named yet. Defaults to `'New topic'`. */\n unnamedTopic?: string;\n\n /** The started-by line under a topic's name. `{name}` is substituted. Defaults to `'Started by {name}'`. */\n startedBy?: string;\n\n /** Shown while loading without topics. Defaults to `'Loading topics…'`. */\n loading?: string;\n\n /** Shown when loading topics fails, alongside any existing topics. Defaults to `'Could not load topics.'`. */\n failed?: string;\n\n /** Shown when access is denied; replaces any existing topics. Defaults to `'You are not authorized to view these topics.'`. */\n unauthorized?: string;\n\n /** Shown when there are no topics yet. Defaults to `'No topics yet. Start the first one!'`. */\n empty?: string;\n\n /** Overrides for the relative last-activity timestamps. */\n timestamps?: RelativeTimestampLabels;\n}\n\n/**\n * Props for {@link ChatTopicList}.\n * @typeParam TTopic The host's topic type — anything extending {@link ChatTopic}.\n */\nexport interface ChatTopicListProps<TTopic extends ChatTopic = ChatTopic> {\n /**\n * The topics, in any order — the list orders them by most recent activity itself. Hand this\n * the array a live (observable) query delivers and the list re-renders as it changes.\n */\n topics: TTopic[];\n\n /** Query display state. Defaults to {@link ChatStatus.Ready}; existing topics remain visible on loading or failure, and unauthorized disables starting a topic. */\n status?: ChatStatus;\n\n /**\n * Invoked when a topic is picked from the list.\n * @param topic The topic that was picked.\n */\n onOpen: (topic: TTopic) => void;\n\n /**\n * Invoked when a new topic is asked for. Creating the topic is the host's business — the\n * component only raises the intent. Omit to leave the new-topic affordance off.\n */\n onStart?: () => void;\n\n /**\n * Resolves an author id into what it takes to render whoever started a topic. Omit to fall\n * back to rendering the id itself as the name.\n * @param authorId The identifier of the author to resolve.\n * @returns The author.\n */\n authorOf?: (authorId: ChatIdentifier) => ChatAuthor;\n\n /**\n * Renders the avatar for whoever started a topic, replacing the built-in initials circle.\n * @param authorId The identifier of the author.\n * @param author The resolved author, as {@link authorOf} answered.\n * @returns What to render.\n */\n renderAvatar?: (authorId: ChatIdentifier, author: ChatAuthor) => ReactNode;\n\n /**\n * Decides whether a topic counts as unnamed and renders the pending placeholder. Defaults to\n * \"no name, or only whitespace\" — override when the host's backend stores a literal\n * placeholder for unnamed topics instead.\n * @param topic The topic to check.\n * @returns True when the topic has no usable name yet.\n */\n isTopicUnnamed?: (topic: TTopic) => boolean;\n\n /** Builds the avatar image URL for topic starters. Omit to always show initials. */\n buildAvatarUrl?: (params: BuildAvatarUrlParams) => string;\n\n /** Overrides for every label rendered. Unset fields fall back to literal English defaults. */\n labels?: ChatTopicListLabels;\n\n /** Additional class name for the list's root element. */\n className?: string;\n}\n\n/**\n * The topics of a chat, most recently active first — pick one to open its conversation, or start\n * a new one. A topic that has not been named yet renders a placeholder in a pending style: with\n * host-side auto-naming, the name arrives through the data once the host has produced it, and\n * the placeholder simply stops being needed.\n */\nexport const ChatTopicList = <TTopic extends ChatTopic = ChatTopic>({\n topics,\n status = ChatStatus.Ready,\n onOpen,\n onStart,\n authorOf,\n renderAvatar,\n isTopicUnnamed,\n buildAvatarUrl,\n labels,\n className,\n}: ChatTopicListProps<TTopic>) => {\n const unnamed = isTopicUnnamed ?? defaultIsTopicUnnamed;\n\n const authorFor = (authorId: ChatIdentifier): ChatAuthor =>\n authorOf?.(authorId) ?? {\n name: chatIdentifierString(authorId),\n kind: ChatAuthorKind.User,\n };\n\n return (\n <div className={`cratis-chat-topics${className ? ` ${className}` : ''}`} aria-busy={status === ChatStatus.Loading && topics.length > 0 || undefined}>\n {onStart && (\n <button\n type='button'\n className='cratis-chat-topics__start'\n disabled={status === ChatStatus.Unauthorized}\n onClick={onStart}\n >\n <PlusIcon />\n <span>{labels?.newTopic ?? 'New topic'}</span>\n </button>\n )}\n {(topics.length === 0 || status === ChatStatus.Failed || status === ChatStatus.Unauthorized) && (\n <p className='cratis-chat-topics__empty'>\n {status === ChatStatus.Loading ? (\n <span key={status} role='status'>{labels?.loading ?? 'Loading topics…'}</span>\n ) : status === ChatStatus.Failed ? (\n <span key={status} role='alert'>{labels?.failed ?? 'Could not load topics.'}</span>\n ) : status === ChatStatus.Unauthorized ? (\n <span key={status} role='alert'>{labels?.unauthorized ?? 'You are not authorized to view these topics.'}</span>\n ) : (\n labels?.empty ?? 'No topics yet. Start the first one!'\n )}\n </p>\n )}\n <ul className='cratis-chat-topics__list'>\n {status !== ChatStatus.Unauthorized && topicsByActivity(topics).map((topic) => {\n const starter =\n topic.startedBy === undefined\n ? undefined\n : authorFor(topic.startedBy);\n const isPending = unnamed(topic);\n const activity = topic.lastActivity ?? topic.started;\n\n return (\n <li key={chatIdentifierString(topic.id)}>\n <button\n type='button'\n className='cratis-chat-topics__topic'\n onClick={() => onOpen(topic)}\n >\n {topic.startedBy !== undefined &&\n starter &&\n (renderAvatar ? (\n renderAvatar(topic.startedBy, starter)\n ) : (\n <PersonAvatarCircle\n userId={chatIdentifierString(topic.startedBy)}\n name={starter.name}\n hasAvatar={starter.hasAvatar ?? false}\n size={28}\n ownerType={\n starter.kind === ChatAuthorKind.Agent\n ? 'Agents'\n : 'Users'\n }\n version={starter.avatarVersion}\n buildAvatarUrl={buildAvatarUrl}\n />\n ))}\n <span className='cratis-chat-topics__details'>\n <span\n className={`cratis-chat-topics__name${isPending ? ' cratis-chat-topics__name--pending' : ''}`}\n >\n {isPending\n ? (labels?.unnamedTopic ?? 'New topic')\n : topic.name}\n </span>\n {starter && (\n <span className='cratis-chat-topics__started-by'>\n {(\n labels?.startedBy ?? 'Started by {name}'\n ).replace('{name}', starter.name)}\n </span>\n )}\n </span>\n {activity !== undefined && (\n <span className='cratis-chat-topics__activity'>\n {relativeTimestamp(\n activity,\n new Date(),\n labels?.timestamps,\n )}\n </span>\n )}\n </button>\n </li>\n );\n })}\n </ul>\n </div>\n );\n};\n"],"names":["_jsxs","_jsx","isTopicUnnamed","defaultIsTopicUnnamed"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChatTopicList.js","sources":["../../../Chat/ChatTopicList.tsx"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { useRef, type ReactNode } from 'react';\nimport { PersonAvatarCircle, type BuildAvatarUrlParams } from './Kit/Avatar';\nimport { ChatAuthorKind } from './Kit/ChatAuthorKind';\nimport type { ChatAuthor } from './ChatAuthor';\nimport { chatIdentifierString, type ChatIdentifier } from './ChatIdentifier';\nimport type { ChatTopic } from './ChatTopic';\nimport type { ChatTopicAction } from './ChatTopicAction';\nimport { ChatTopicActions } from './ChatTopicActions';\nimport { ChatStatus } from './ChatStatus';\nimport { isTopicUnnamed as defaultIsTopicUnnamed } from './isTopicUnnamed';\nimport { relativeTimestamp, type RelativeTimestampLabels } from './relativeTimestamp';\nimport { useTopicActionFocusRecovery } from './useTopicActionFocusRecovery';\nimport { topicsByActivity } from './topicsByActivity';\n\nconst PlusIcon = () => (\n <svg\n width='14'\n height='14'\n viewBox='0 0 24 24'\n fill='none'\n stroke='currentColor'\n strokeWidth='2.5'\n aria-hidden='true'\n >\n <line x1='12' y1='5' x2='12' y2='19' />\n <line x1='5' y1='12' x2='19' y2='12' />\n </svg>\n);\n\n/** Overrides for every label the {@link ChatTopicList} renders. Any field left unset falls back\n * to a literal English default. */\nexport interface ChatTopicListLabels {\n /** The new-topic button. Defaults to `'New topic'`. */\n newTopic?: string;\n\n /** Placeholder name for a topic that has not been named yet. Defaults to `'New topic'`. */\n unnamedTopic?: string;\n\n /** The started-by line under a topic's name. `{name}` is substituted. Defaults to `'Started by {name}'`. */\n startedBy?: string;\n\n /** Shown while loading without topics. Defaults to `'Loading topics…'`. */\n loading?: string;\n\n /** Shown when loading topics fails, alongside any existing topics. Defaults to `'Could not load topics.'`. */\n failed?: string;\n\n /** Shown when access is denied; replaces any existing topics. Defaults to `'You are not authorized to view these topics.'`. */\n unauthorized?: string;\n\n /** Shown when there are no topics yet. Defaults to `'No topics yet. Start the first one!'`. */\n empty?: string;\n\n /** Overrides for the relative last-activity timestamps. */\n timestamps?: RelativeTimestampLabels;\n}\n\n/**\n * Props for {@link ChatTopicList}.\n * @typeParam TTopic The host's topic type — anything extending {@link ChatTopic}.\n */\nexport interface ChatTopicListProps<TTopic extends ChatTopic = ChatTopic> {\n /**\n * The topics, in any order — the list orders them by most recent activity itself. Hand this\n * the array a live (observable) query delivers and the list re-renders as it changes.\n */\n topics: TTopic[];\n\n /** Query display state. Defaults to {@link ChatStatus.Ready}; existing topics remain visible on loading or failure, and unauthorized disables starting a topic. */\n status?: ChatStatus;\n\n /**\n * Invoked when a topic is picked from the list.\n * @param topic The topic that was picked.\n */\n onOpen: (topic: TTopic) => void;\n\n /**\n * Invoked when a new topic is asked for. Creating the topic is the host's business — the\n * component only raises the intent. Omit to leave the new-topic affordance off.\n */\n onStart?: () => void;\n\n /**\n * Resolves an author id into what it takes to render whoever started a topic. Omit to fall\n * back to rendering the id itself as the name.\n * @param authorId The identifier of the author to resolve.\n * @returns The author.\n */\n authorOf?: (authorId: ChatIdentifier) => ChatAuthor;\n\n /**\n * Renders the avatar for whoever started a topic, replacing the built-in initials circle.\n * @param authorId The identifier of the author.\n * @param author The resolved author, as {@link authorOf} answered.\n * @returns What to render.\n */\n renderAvatar?: (authorId: ChatIdentifier, author: ChatAuthor) => ReactNode;\n\n /**\n * Decides whether a topic counts as unnamed and renders the pending placeholder. Defaults to\n * \"no name, or only whitespace\" — override when the host's backend stores a literal\n * placeholder for unnamed topics instead.\n * @param topic The topic to check.\n * @returns True when the topic has no usable name yet.\n */\n isTopicUnnamed?: (topic: TTopic) => boolean;\n\n /** Builds the avatar image URL for topic starters. Omit to always show initials. */\n buildAvatarUrl?: (params: BuildAvatarUrlParams) => string;\n\n /** Overrides for every label rendered. Unset fields fall back to literal English defaults. */\n labels?: ChatTopicListLabels;\n\n /** The host's actions to offer on each topic where they are available. */\n topicActions?: ChatTopicAction<TTopic>[];\n\n /** Additional class name for the list's root element. */\n className?: string;\n}\n\n/**\n * The topics of a chat, most recently active first — pick one to open its conversation, or start\n * a new one. A topic that has not been named yet renders a placeholder in a pending style: with\n * host-side auto-naming, the name arrives through the data once the host has produced it, and\n * the placeholder simply stops being needed.\n */\nexport const ChatTopicList = <TTopic extends ChatTopic = ChatTopic>({\n topics,\n status = ChatStatus.Ready,\n onOpen,\n onStart,\n authorOf,\n renderAvatar,\n isTopicUnnamed,\n buildAvatarUrl,\n labels,\n topicActions,\n className,\n}: ChatTopicListProps<TTopic>) => {\n const unnamed = isTopicUnnamed ?? defaultIsTopicUnnamed;\n const listRef = useRef<HTMLUListElement>(null);\n const orderedTopics = status === ChatStatus.Unauthorized ? [] : topicsByActivity(topics);\n const { onActionFocus, onActionBlur } = useTopicActionFocusRecovery(\n listRef,\n orderedTopics.map((topic) => chatIdentifierString(topic.id)),\n );\n\n const authorFor = (authorId: ChatIdentifier): ChatAuthor =>\n authorOf?.(authorId) ?? {\n name: chatIdentifierString(authorId),\n kind: ChatAuthorKind.User,\n };\n\n return (\n <div className={`cratis-chat-topics${className ? ` ${className}` : ''}`} aria-busy={status === ChatStatus.Loading && topics.length > 0 || undefined}>\n {onStart && (\n <button\n type='button'\n className='cratis-chat-topics__start'\n disabled={status === ChatStatus.Unauthorized}\n onClick={onStart}\n >\n <PlusIcon />\n <span>{labels?.newTopic ?? 'New topic'}</span>\n </button>\n )}\n {(topics.length === 0 || status === ChatStatus.Failed || status === ChatStatus.Unauthorized) && (\n <p className='cratis-chat-topics__empty'>\n {status === ChatStatus.Loading ? (\n <span key={status} role='status'>{labels?.loading ?? 'Loading topics…'}</span>\n ) : status === ChatStatus.Failed ? (\n <span key={status} role='alert'>{labels?.failed ?? 'Could not load topics.'}</span>\n ) : status === ChatStatus.Unauthorized ? (\n <span key={status} role='alert'>{labels?.unauthorized ?? 'You are not authorized to view these topics.'}</span>\n ) : (\n labels?.empty ?? 'No topics yet. Start the first one!'\n )}\n </p>\n )}\n <ul ref={listRef} className='cratis-chat-topics__list'>\n {status !== ChatStatus.Unauthorized && orderedTopics.map((topic) => {\n const starter =\n topic.startedBy === undefined\n ? undefined\n : authorFor(topic.startedBy);\n const isPending = unnamed(topic);\n const activity = topic.lastActivity ?? topic.started;\n const topicName = isPending ? (labels?.unnamedTopic ?? 'New topic') : topic.name;\n const availableActions = (topicActions ?? []).filter(\n (action) => action.isAvailable?.(topic) ?? true,\n );\n\n const topicButton = (\n <button\n type='button'\n className='cratis-chat-topics__topic'\n onClick={() => onOpen(topic)}\n >\n {topic.startedBy !== undefined &&\n starter &&\n (renderAvatar ? (\n renderAvatar(topic.startedBy, starter)\n ) : (\n <PersonAvatarCircle\n userId={chatIdentifierString(topic.startedBy)}\n name={starter.name}\n hasAvatar={starter.hasAvatar ?? false}\n size={28}\n ownerType={\n starter.kind === ChatAuthorKind.Agent\n ? 'Agents'\n : 'Users'\n }\n version={starter.avatarVersion}\n buildAvatarUrl={buildAvatarUrl}\n />\n ))}\n <span className='cratis-chat-topics__details'>\n <span\n className={`cratis-chat-topics__name${isPending ? ' cratis-chat-topics__name--pending' : ''}`}\n >\n {topicName}\n </span>\n {starter && (\n <span className='cratis-chat-topics__started-by'>\n {(\n labels?.startedBy ?? 'Started by {name}'\n ).replace('{name}', starter.name)}\n </span>\n )}\n </span>\n {activity !== undefined && (\n <span className='cratis-chat-topics__activity'>\n {relativeTimestamp(\n activity,\n new Date(),\n labels?.timestamps,\n )}\n </span>\n )}\n </button>\n );\n\n const topicKey = chatIdentifierString(topic.id);\n // Reuses the message action overlay styling; keyboard focus reveals the controls.\n return (\n <li\n key={topicKey}\n className={availableActions.length > 0 ? 'cratis-chat-topics__row' : undefined}\n >\n {topicButton}\n {availableActions.length > 0 && (\n <ChatTopicActions<TTopic>\n topic={topic}\n topicName={topicName}\n actions={availableActions}\n onActionFocus={onActionFocus(topicKey)}\n onActionBlur={onActionBlur}\n />\n )}\n </li>\n );\n })}\n </ul>\n </div>\n );\n};\n"],"names":["_jsxs","_jsx","isTopicUnnamed","defaultIsTopicUnnamed"],"mappings":"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"}
|
package/dist/esm/Chat/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type { ChatSidebarProps, ChatSidebarLabels, ChatSidebarParts, } from './C
|
|
|
14
14
|
export { ChatSidebarForObservableQueries } from './ChatSidebarForObservableQueries.js';
|
|
15
15
|
export type { ChatSidebarForObservableQueriesProps } from './ChatSidebarForObservableQueries.js';
|
|
16
16
|
export type { ChatTopic } from './ChatTopic.js';
|
|
17
|
+
export type { ChatTopicAction } from './ChatTopicAction.js';
|
|
17
18
|
export { ChatTopicList } from './ChatTopicList.js';
|
|
18
19
|
export type { ChatTopicListProps, ChatTopicListLabels } from './ChatTopicList.js';
|
|
19
20
|
export { isTopicUnnamed } from './isTopicUnnamed.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../Chat/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAc,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAoB,CAAC;AACtD,YAAY,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,uBAAoB,CAAC;AACxF,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,qBAAkB,CAAC;AAC5E,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAkB,CAAC;AACvD,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAe,CAAC;AACjD,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAe,CAAC;AACjD,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAqB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAc,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAmB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAmB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAe,CAAC;AAC5C,YAAY,EACR,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GACnB,MAAM,kBAAe,CAAC;AACvB,OAAO,EAAE,+BAA+B,EAAE,MAAM,sCAAmC,CAAC;AACpF,YAAY,EAAE,oCAAoC,EAAE,MAAM,sCAAmC,CAAC;AAC9F,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAa,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAiB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,oBAAiB,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAkB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAqB,CAAC;AACxD,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAqB,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA0B,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAoB,CAAC;AAItD,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAsB,CAAC;AACtD,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAAwB,CAAC;AAC/D,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAc,CAAC;AACzD,YAAY,EAAE,kBAAkB,EAAE,MAAM,uBAAoB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,yBAAgB,CAAC;AACrF,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,yBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../Chat/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAc,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAoB,CAAC;AACtD,YAAY,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,uBAAoB,CAAC;AACxF,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,qBAAkB,CAAC;AAC5E,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAkB,CAAC;AACvD,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAe,CAAC;AACjD,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAe,CAAC;AACjD,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAqB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAc,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAmB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAmB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAe,CAAC;AAC5C,YAAY,EACR,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GACnB,MAAM,kBAAe,CAAC;AACvB,OAAO,EAAE,+BAA+B,EAAE,MAAM,sCAAmC,CAAC;AACpF,YAAY,EAAE,oCAAoC,EAAE,MAAM,sCAAmC,CAAC;AAC9F,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAa,CAAC;AAC7C,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAmB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAiB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,oBAAiB,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAkB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAqB,CAAC;AACxD,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAqB,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA0B,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAoB,CAAC;AAItD,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAsB,CAAC;AACtD,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAAwB,CAAC;AAC/D,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAc,CAAC;AACzD,YAAY,EAAE,kBAAkB,EAAE,MAAM,uBAAoB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,yBAAgB,CAAC;AACrF,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,yBAAgB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type FocusEvent, type RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Keeps keyboard focus in a topic list when a focused topic action disappears, because
|
|
4
|
+
* invoking it made the action unavailable or removed its topic. Without this, the unmounted
|
|
5
|
+
* button drops focus to the document body.
|
|
6
|
+
*
|
|
7
|
+
* Focus moves to the same topic's opening button, or, when the topic is gone, to the topic that
|
|
8
|
+
* took its place in the order last rendered, then to the list's New topic button. Focus that the
|
|
9
|
+
* host already moved elsewhere, such as into a rename dialog, is left alone, and an action that
|
|
10
|
+
* lost focus while still mounted is forgotten.
|
|
11
|
+
* @param listRef The list whose rows each hold one topic.
|
|
12
|
+
* @param orderedTopicKeys The topic keys in rendered row order.
|
|
13
|
+
* @returns Focus and blur handlers to attach to each topic action button.
|
|
14
|
+
*/
|
|
15
|
+
export declare const useTopicActionFocusRecovery: (listRef: RefObject<HTMLUListElement | null>, orderedTopicKeys: readonly string[]) => {
|
|
16
|
+
onActionFocus: (topicKey: string) => (event: FocusEvent<HTMLElement>) => void;
|
|
17
|
+
onActionBlur: (event: FocusEvent<HTMLElement>) => void;
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=useTopicActionFocusRecovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTopicActionFocusRecovery.d.ts","sourceRoot":"","sources":["../../../Chat/useTopicActionFocusRecovery.ts"],"names":[],"mappings":"AAGA,OAAO,EAA2B,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEjF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,2BAA2B,YAC3B,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,oBACzB,SAAS,MAAM,EAAE;IA0B/B,aAAa,aAAa,MAAM,aAAa,UAAU,CAAC,WAAW,CAAC;IAGpE,YAAY,UAAU,UAAU,CAAC,WAAW,CAAC;CAWpD,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useRef, useLayoutEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
const useTopicActionFocusRecovery = (listRef, orderedTopicKeys) => {
|
|
4
|
+
const focusedActionRef = useRef(null);
|
|
5
|
+
const committedTopicKeysRef = useRef([]);
|
|
6
|
+
useLayoutEffect(() => {
|
|
7
|
+
const previousTopicKeys = committedTopicKeysRef.current;
|
|
8
|
+
committedTopicKeysRef.current = orderedTopicKeys;
|
|
9
|
+
const focused = focusedActionRef.current;
|
|
10
|
+
if (!focused || focused.element.isConnected)
|
|
11
|
+
return;
|
|
12
|
+
focusedActionRef.current = null;
|
|
13
|
+
const active = document.activeElement;
|
|
14
|
+
if (active && active !== document.body)
|
|
15
|
+
return;
|
|
16
|
+
const rows = Array.from(listRef.current?.children ?? []);
|
|
17
|
+
const sameTopic = orderedTopicKeys.indexOf(focused.topicKey);
|
|
18
|
+
const row = sameTopic >= 0
|
|
19
|
+
? rows[sameTopic]
|
|
20
|
+
: rows[Math.min(Math.max(previousTopicKeys.indexOf(focused.topicKey), 0), rows.length - 1)];
|
|
21
|
+
const target = row?.querySelector('.cratis-chat-topics__topic') ??
|
|
22
|
+
listRef.current?.parentElement?.querySelector('.cratis-chat-topics__start');
|
|
23
|
+
target?.focus();
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
onActionFocus: (topicKey) => (event) => {
|
|
27
|
+
focusedActionRef.current = { element: event.currentTarget, topicKey };
|
|
28
|
+
},
|
|
29
|
+
onActionBlur: (event) => {
|
|
30
|
+
const element = event.currentTarget;
|
|
31
|
+
queueMicrotask(() => {
|
|
32
|
+
if (focusedActionRef.current?.element === element && element.isConnected) {
|
|
33
|
+
focusedActionRef.current = null;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export { useTopicActionFocusRecovery };
|
|
41
|
+
//# sourceMappingURL=useTopicActionFocusRecovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTopicActionFocusRecovery.js","sources":["../../../Chat/useTopicActionFocusRecovery.ts"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { useLayoutEffect, useRef, type FocusEvent, type RefObject } from 'react';\n\n/**\n * Keeps keyboard focus in a topic list when a focused topic action disappears, because\n * invoking it made the action unavailable or removed its topic. Without this, the unmounted\n * button drops focus to the document body.\n *\n * Focus moves to the same topic's opening button, or, when the topic is gone, to the topic that\n * took its place in the order last rendered, then to the list's New topic button. Focus that the\n * host already moved elsewhere, such as into a rename dialog, is left alone, and an action that\n * lost focus while still mounted is forgotten.\n * @param listRef The list whose rows each hold one topic.\n * @param orderedTopicKeys The topic keys in rendered row order.\n * @returns Focus and blur handlers to attach to each topic action button.\n */\nexport const useTopicActionFocusRecovery = (\n listRef: RefObject<HTMLUListElement | null>,\n orderedTopicKeys: readonly string[],\n) => {\n const focusedActionRef = useRef<{ element: HTMLElement; topicKey: string } | null>(null);\n // The row order from the last commit, so a removed topic's position reflects any re-sorting\n // that happened after its action gained focus.\n const committedTopicKeysRef = useRef<readonly string[]>([]);\n\n useLayoutEffect(() => {\n const previousTopicKeys = committedTopicKeysRef.current;\n committedTopicKeysRef.current = orderedTopicKeys;\n const focused = focusedActionRef.current;\n if (!focused || focused.element.isConnected) return;\n focusedActionRef.current = null;\n const active = document.activeElement;\n if (active && active !== document.body) return;\n const rows = Array.from(listRef.current?.children ?? []);\n const sameTopic = orderedTopicKeys.indexOf(focused.topicKey);\n const row = sameTopic >= 0\n ? rows[sameTopic]\n : rows[Math.min(Math.max(previousTopicKeys.indexOf(focused.topicKey), 0), rows.length - 1)];\n const target = row?.querySelector<HTMLElement>('.cratis-chat-topics__topic') ??\n listRef.current?.parentElement?.querySelector<HTMLElement>('.cratis-chat-topics__start');\n target?.focus();\n });\n\n return {\n onActionFocus: (topicKey: string) => (event: FocusEvent<HTMLElement>) => {\n focusedActionRef.current = { element: event.currentTarget, topicKey };\n },\n onActionBlur: (event: FocusEvent<HTMLElement>) => {\n // A button removed while focused is already disconnected here, so only a deliberate\n // focus change on a still-mounted action clears the tracking.\n const element = event.currentTarget;\n queueMicrotask(() => {\n if (focusedActionRef.current?.element === element && element.isConnected) {\n focusedActionRef.current = null;\n }\n });\n },\n };\n};\n"],"names":[],"mappings":";;MAkBa,2BAA2B,GAAG,CACvC,OAA2C,EAC3C,gBAAmC,KACnC;AACA,IAAA,MAAM,gBAAgB,GAAG,MAAM,CAAoD,IAAI,CAAC;AAGxF,IAAA,MAAM,qBAAqB,GAAG,MAAM,CAAoB,EAAE,CAAC;IAE3D,eAAe,CAAC,MAAK;AACjB,QAAA,MAAM,iBAAiB,GAAG,qBAAqB,CAAC,OAAO;AACvD,QAAA,qBAAqB,CAAC,OAAO,GAAG,gBAAgB;AAChD,QAAA,MAAM,OAAO,GAAG,gBAAgB,CAAC,OAAO;AACxC,QAAA,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW;YAAE;AAC7C,QAAA,gBAAgB,CAAC,OAAO,GAAG,IAAI;AAC/B,QAAA,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa;AACrC,QAAA,IAAI,MAAM,IAAI,MAAM,KAAK,QAAQ,CAAC,IAAI;YAAE;AACxC,QAAA,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,IAAI,EAAE,CAAC;QACxD,MAAM,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;AAC5D,QAAA,MAAM,GAAG,GAAG,SAAS,IAAI;AACrB,cAAE,IAAI,CAAC,SAAS;AAChB,cAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC/F,QAAA,MAAM,MAAM,GAAG,GAAG,EAAE,aAAa,CAAc,4BAA4B,CAAC;YACxE,OAAO,CAAC,OAAO,EAAE,aAAa,EAAE,aAAa,CAAc,4BAA4B,CAAC;QAC5F,MAAM,EAAE,KAAK,EAAE;AACnB,IAAA,CAAC,CAAC;IAEF,OAAO;QACH,aAAa,EAAE,CAAC,QAAgB,KAAK,CAAC,KAA8B,KAAI;AACpE,YAAA,gBAAgB,CAAC,OAAO,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,aAAa,EAAE,QAAQ,EAAE;QACzE,CAAC;AACD,QAAA,YAAY,EAAE,CAAC,KAA8B,KAAI;AAG7C,YAAA,MAAM,OAAO,GAAG,KAAK,CAAC,aAAa;YACnC,cAAc,CAAC,MAAK;AAChB,gBAAA,IAAI,gBAAgB,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,IAAI,OAAO,CAAC,WAAW,EAAE;AACtE,oBAAA,gBAAgB,CAAC,OAAO,GAAG,IAAI;gBACnC;AACJ,YAAA,CAAC,CAAC;QACN,CAAC;KACJ;AACL;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionMenubar.d.ts","sourceRoot":"","sources":["../../../Common/ActionMenubar.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,EAAU,KAAK,WAAW,EAAE,KAAK,cAAc,EAAmB,MAAM,aAAU,CAAC;AAC1F,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"ActionMenubar.d.ts","sourceRoot":"","sources":["../../../Common/ActionMenubar.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,EAAU,KAAK,WAAW,EAAE,KAAK,cAAc,EAAmB,MAAM,aAAU,CAAC;AAC1F,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAoB,CAAC;AAItD,mDAAmD;AACnD,MAAM,WAAW,cAAc;IAC3B,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,yCAAyC;IACzC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,SAAS,CAAC;CAClD;AAED,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IAC/B,0EAA0E;IAC1E,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,4CAA4C;IAC5C,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,EAAE,CAAC,EAAE,WAAW,CAAC;IACjB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAkCD,2DAA2D;AAC3D,eAAO,MAAM,aAAa,kEAMvB,kBAAkB,gCAmBpB,CAAC"}
|
|
@@ -3,6 +3,7 @@ import { Fragment } from 'react';
|
|
|
3
3
|
import { Button } from './Button.js';
|
|
4
4
|
import { ToolbarFocusMode } from './ToolbarFocusMode.js';
|
|
5
5
|
import { useToolbarKeyboardNavigation } from './useToolbarKeyboardNavigation.js';
|
|
6
|
+
import { ToolbarRovingContext, useRovingTool } from './ToolbarRovingContext.js';
|
|
6
7
|
|
|
7
8
|
const buttonToneForSeverity = {
|
|
8
9
|
secondary: 'neutral',
|
|
@@ -13,13 +14,17 @@ const buttonToneForSeverity = {
|
|
|
13
14
|
danger: 'critical',
|
|
14
15
|
contrast: 'neutral',
|
|
15
16
|
};
|
|
17
|
+
const ActionMenubarButton = ({ item, pt }) => {
|
|
18
|
+
const rovingTool = useRovingTool(pt?.root?.tabIndex, pt?.root?.onFocus);
|
|
19
|
+
return (jsx(Button, { variant: 'ghost', tone: item.severity ? buttonToneForSeverity[item.severity] : undefined, onClick: item.command, disabled: item.disabled, className: item.className, icon: item.icon, label: item.label, pt: pt, ref: rovingTool.ref, tabIndex: rovingTool.tabIndex, onFocus: rovingTool.onFocus }));
|
|
20
|
+
};
|
|
16
21
|
const ActionMenubar = ({ model, focusMode = ToolbarFocusMode.Arrows, className, pt, 'aria-label': ariaLabel, }) => {
|
|
17
|
-
const { rootRef, onKeyDown } = useToolbarKeyboardNavigation('horizontal', focusMode);
|
|
18
|
-
return jsx("div", { ref: rootRef, onKeyDown: onKeyDown, role: 'toolbar', className: ['cratis-action-menubar', className].filter(Boolean).join(' '), "data-cratis-part": 'root', "aria-label": ariaLabel, children: model.map((item, index) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
const { rootRef, onKeyDown, roving } = useToolbarKeyboardNavigation('horizontal', focusMode);
|
|
23
|
+
return jsx("div", { ref: rootRef, onKeyDown: onKeyDown, role: 'toolbar', className: ['cratis-action-menubar', className].filter(Boolean).join(' '), "data-cratis-part": 'root', "aria-label": ariaLabel, children: jsx(ToolbarRovingContext.Provider, { value: roving, children: model.map((item, index) => {
|
|
24
|
+
if (item.template)
|
|
25
|
+
return jsx(Fragment, { children: item.template(item) }, index);
|
|
26
|
+
return jsx(ActionMenubarButton, { item: item, pt: pt }, index);
|
|
27
|
+
}) }) });
|
|
23
28
|
};
|
|
24
29
|
|
|
25
30
|
export { ActionMenubar };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionMenubar.js","sources":["../../../Common/ActionMenubar.tsx"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { Fragment, type ReactNode } from 'react';\nimport { Button, type ButtonParts, type ButtonSeverity, type ButtonTone } from './Button';\nimport { ToolbarFocusMode } from './ToolbarFocusMode';\nimport { useToolbarKeyboardNavigation } from './useToolbarKeyboardNavigation';\n\n/** A single action in an {@link ActionMenubar}. */\nexport interface ActionMenuItem {\n /** The visible label. */\n label?: string;\n /** An icon element rendered before the label. */\n icon?: ReactNode;\n /** Invoked when the item is activated. */\n command?: () => void;\n /** When true, the item is greyed out and not clickable. */\n disabled?: boolean;\n /** Extra class name for the item. */\n className?: string;\n /** Severity styling for the action. */\n severity?: ButtonSeverity;\n /** Fully custom render for this item. */\n template?: (item: ActionMenuItem) => ReactNode;\n}\n\n/** Props for {@link ActionMenubar}. */\nexport interface ActionMenubarProps {\n /** Keyboard focus behavior (default: {@link ToolbarFocusMode.Arrows}). */\n focusMode?: ToolbarFocusMode;\n /** Actions to render from left to right. */\n model: ActionMenuItem[];\n /** Extra class name for the toolbar container. */\n className?: string;\n /** Accessible label for the toolbar. */\n 'aria-label'?: string;\n /** Cratis-owned per-part attributes applied to each action button. */\n pt?: ButtonParts;\n /**\n * @deprecated Cratis parts always merge. Remove this renderer-era option.\n */\n ptOptions?: object;\n /**\n * @deprecated Components always uses consumer-owned CSS. Customize through `pt` and CSS instead.\n */\n unstyled?: boolean;\n}\n\nconst buttonToneForSeverity: Record<ButtonSeverity, ButtonTone> = {\n secondary: 'neutral',\n info: 'accent',\n help: 'accent',\n success: 'positive',\n warn: 'caution',\n danger: 'critical',\n contrast: 'neutral',\n};\n\n/** A horizontal, accessible toolbar of command actions. */\nexport const ActionMenubar = ({\n model,\n focusMode = ToolbarFocusMode.Arrows,\n className,\n pt,\n 'aria-label': ariaLabel,\n}: ActionMenubarProps) => {\n const { rootRef, onKeyDown } = useToolbarKeyboardNavigation('horizontal', focusMode);\n return <div\n ref={rootRef}\n onKeyDown={onKeyDown}\n role='toolbar'\n className={['cratis-action-menubar', className].filter(Boolean).join(' ')}\n data-cratis-part='root'\n aria-label={ariaLabel}\n >\n {model.map((item, index) => {\n if (item.template)\n return <Fragment key={index}>{item.template(item)}</Fragment>;\n\n return
|
|
1
|
+
{"version":3,"file":"ActionMenubar.js","sources":["../../../Common/ActionMenubar.tsx"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { Fragment, type ReactNode } from 'react';\nimport { Button, type ButtonParts, type ButtonSeverity, type ButtonTone } from './Button';\nimport { ToolbarFocusMode } from './ToolbarFocusMode';\nimport { useToolbarKeyboardNavigation } from './useToolbarKeyboardNavigation';\nimport { ToolbarRovingContext, useRovingTool } from './ToolbarRovingContext';\n\n/** A single action in an {@link ActionMenubar}. */\nexport interface ActionMenuItem {\n /** The visible label. */\n label?: string;\n /** An icon element rendered before the label. */\n icon?: ReactNode;\n /** Invoked when the item is activated. */\n command?: () => void;\n /** When true, the item is greyed out and not clickable. */\n disabled?: boolean;\n /** Extra class name for the item. */\n className?: string;\n /** Severity styling for the action. */\n severity?: ButtonSeverity;\n /** Fully custom render for this item. */\n template?: (item: ActionMenuItem) => ReactNode;\n}\n\n/** Props for {@link ActionMenubar}. */\nexport interface ActionMenubarProps {\n /** Keyboard focus behavior (default: {@link ToolbarFocusMode.Arrows}). */\n focusMode?: ToolbarFocusMode;\n /** Actions to render from left to right. */\n model: ActionMenuItem[];\n /** Extra class name for the toolbar container. */\n className?: string;\n /** Accessible label for the toolbar. */\n 'aria-label'?: string;\n /** Cratis-owned per-part attributes applied to each action button. */\n pt?: ButtonParts;\n /**\n * @deprecated Cratis parts always merge. Remove this renderer-era option.\n */\n ptOptions?: object;\n /**\n * @deprecated Components always uses consumer-owned CSS. Customize through `pt` and CSS instead.\n */\n unstyled?: boolean;\n}\n\nconst buttonToneForSeverity: Record<ButtonSeverity, ButtonTone> = {\n secondary: 'neutral',\n info: 'accent',\n help: 'accent',\n success: 'positive',\n warn: 'caution',\n danger: 'critical',\n contrast: 'neutral',\n};\n\n/** One Components-owned action button, which takes part in the menubar's single Tab stop. */\nconst ActionMenubarButton = ({ item, pt }: { item: ActionMenuItem; pt: ActionMenubarProps['pt'] }) => {\n // The consumer's pt.root tab index and focus handler are composed, because props passed\n // directly to Button take precedence over pt.root.\n const rovingTool = useRovingTool<HTMLButtonElement>(pt?.root?.tabIndex, pt?.root?.onFocus);\n return (\n <Button\n variant='ghost'\n tone={item.severity ? buttonToneForSeverity[item.severity] : undefined}\n onClick={item.command}\n disabled={item.disabled}\n className={item.className}\n icon={item.icon}\n label={item.label}\n pt={pt}\n ref={rovingTool.ref}\n tabIndex={rovingTool.tabIndex}\n onFocus={rovingTool.onFocus}\n />\n );\n};\n\n/** A horizontal, accessible toolbar of command actions. */\nexport const ActionMenubar = ({\n model,\n focusMode = ToolbarFocusMode.Arrows,\n className,\n pt,\n 'aria-label': ariaLabel,\n}: ActionMenubarProps) => {\n const { rootRef, onKeyDown, roving } = useToolbarKeyboardNavigation('horizontal', focusMode);\n return <div\n ref={rootRef}\n onKeyDown={onKeyDown}\n role='toolbar'\n className={['cratis-action-menubar', className].filter(Boolean).join(' ')}\n data-cratis-part='root'\n aria-label={ariaLabel}\n >\n <ToolbarRovingContext.Provider value={roving}>\n {model.map((item, index) => {\n if (item.template)\n return <Fragment key={index}>{item.template(item)}</Fragment>;\n\n return <ActionMenubarButton key={index} item={item} pt={pt} />;\n })}\n </ToolbarRovingContext.Provider>\n </div>;\n};\n"],"names":["_jsx"],"mappings":";;;;;;;AAiDA,MAAM,qBAAqB,GAAuC;AAC9D,IAAA,SAAS,EAAE,SAAS;AACpB,IAAA,IAAI,EAAE,QAAQ;AACd,IAAA,IAAI,EAAE,QAAQ;AACd,IAAA,OAAO,EAAE,UAAU;AACnB,IAAA,IAAI,EAAE,SAAS;AACf,IAAA,MAAM,EAAE,UAAU;AAClB,IAAA,QAAQ,EAAE,SAAS;CACtB;AAGD,MAAM,mBAAmB,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,EAA0D,KAAI;AAGjG,IAAA,MAAM,UAAU,GAAG,aAAa,CAAoB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC;AAC1F,IAAA,QACIA,GAAA,CAAC,MAAM,EAAA,EACH,OAAO,EAAC,OAAO,EACf,IAAI,EAAE,IAAI,CAAC,QAAQ,GAAG,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,SAAS,EACtE,OAAO,EAAE,IAAI,CAAC,OAAO,EACrB,QAAQ,EAAE,IAAI,CAAC,QAAQ,EACvB,SAAS,EAAE,IAAI,CAAC,SAAS,EACzB,IAAI,EAAE,IAAI,CAAC,IAAI,EACf,KAAK,EAAE,IAAI,CAAC,KAAK,EACjB,EAAE,EAAE,EAAE,EACN,GAAG,EAAE,UAAU,CAAC,GAAG,EACnB,QAAQ,EAAE,UAAU,CAAC,QAAQ,EAC7B,OAAO,EAAE,UAAU,CAAC,OAAO,EAAA,CAC7B;AAEV,CAAC;MAGY,aAAa,GAAG,CAAC,EAC1B,KAAK,EACL,SAAS,GAAG,gBAAgB,CAAC,MAAM,EACnC,SAAS,EACT,EAAE,EACF,YAAY,EAAE,SAAS,GACN,KAAI;AACrB,IAAA,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,4BAA4B,CAAC,YAAY,EAAE,SAAS,CAAC;IAC5F,OAAOA,GAAA,CAAA,KAAA,EAAA,EACH,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,SAAS,EACpB,IAAI,EAAC,SAAS,EACd,SAAS,EAAE,CAAC,uBAAuB,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAA,kBAAA,EACxD,MAAM,EAAA,YAAA,EACX,SAAS,EAAA,QAAA,EAErBA,GAAA,CAAC,oBAAoB,CAAC,QAAQ,EAAA,EAAC,KAAK,EAAE,MAAM,EAAA,QAAA,EAC3C,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,KAAI;gBACvB,IAAI,IAAI,CAAC,QAAQ;AACb,oBAAA,OAAOA,GAAA,CAAC,QAAQ,EAAA,EAAA,QAAA,EAAc,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAA,EAA3B,KAAK,CAAkC;AAEjE,gBAAA,OAAOA,GAAA,CAAC,mBAAmB,EAAA,EAAa,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAA,EAAzB,KAAK,CAAwB;YAClE,CAAC,CAAC,EAAA,CAC8B,EAAA,CAC9B;AACV;;"}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
/** Keyboard focus behavior for Toolbar and ActionMenubar.
|
|
1
|
+
/** Keyboard focus behavior for Toolbar and ActionMenubar. */
|
|
2
2
|
export declare enum ToolbarFocusMode {
|
|
3
3
|
/** Navigate with arrows and Home/End while retaining every tool's Tab stop (default). */
|
|
4
4
|
Arrows = "arrows",
|
|
5
|
+
/**
|
|
6
|
+
* Navigate with arrows and Home/End, with one Tab stop for the toolbar's own tools that
|
|
7
|
+
* returns to the last focused tool (the WAI-ARIA toolbar pattern). Controls you add yourself,
|
|
8
|
+
* widgets such as inputs and sliders, tools rendered outside the toolbar's own markup (for example
|
|
9
|
+
* through a portal), and tools with an explicit tab index keep their own Tab stop. An explicit
|
|
10
|
+
* tab index of -1 removes the tool from both Tab and arrow-key navigation.
|
|
11
|
+
*/
|
|
12
|
+
SingleTabStop = "singleTabStop",
|
|
5
13
|
/** Keep native focus behavior with no toolbar keyboard handling. */
|
|
6
14
|
None = "none"
|
|
7
15
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToolbarFocusMode.d.ts","sourceRoot":"","sources":["../../../Common/ToolbarFocusMode.ts"],"names":[],"mappings":"AAGA,
|
|
1
|
+
{"version":3,"file":"ToolbarFocusMode.d.ts","sourceRoot":"","sources":["../../../Common/ToolbarFocusMode.ts"],"names":[],"mappings":"AAGA,6DAA6D;AAC7D,oBAAY,gBAAgB;IACxB,yFAAyF;IACzF,MAAM,WAAW;IACjB;;;;;;OAMG;IACH,aAAa,kBAAkB;IAC/B,oEAAoE;IACpE,IAAI,SAAS;CAChB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToolbarFocusMode.js","sources":["../../../Common/ToolbarFocusMode.ts"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\n/** Keyboard focus behavior for Toolbar and ActionMenubar.
|
|
1
|
+
{"version":3,"file":"ToolbarFocusMode.js","sources":["../../../Common/ToolbarFocusMode.ts"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\n/** Keyboard focus behavior for Toolbar and ActionMenubar. */\nexport enum ToolbarFocusMode {\n /** Navigate with arrows and Home/End while retaining every tool's Tab stop (default). */\n Arrows = 'arrows',\n /**\n * Navigate with arrows and Home/End, with one Tab stop for the toolbar's own tools that\n * returns to the last focused tool (the WAI-ARIA toolbar pattern). Controls you add yourself,\n * widgets such as inputs and sliders, tools rendered outside the toolbar's own markup (for example\n * through a portal), and tools with an explicit tab index keep their own Tab stop. An explicit\n * tab index of -1 removes the tool from both Tab and arrow-key navigation.\n */\n SingleTabStop = 'singleTabStop',\n /** Keep native focus behavior with no toolbar keyboard handling. */\n None = 'none',\n}\n"],"names":[],"mappings":"IAIY;AAAZ,CAAA,UAAY,gBAAgB,EAAA;AAExB,IAAA,gBAAA,CAAA,QAAA,CAAA,GAAA,QAAiB;AAQjB,IAAA,gBAAA,CAAA,eAAA,CAAA,GAAA,eAA+B;AAE/B,IAAA,gBAAA,CAAA,MAAA,CAAA,GAAA,MAAa;AACjB,CAAC,EAbW,gBAAgB,KAAhB,gBAAgB,GAAA,EAAA,CAAA,CAAA;;"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type FocusEvent, type Ref } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The single-Tab-stop state a toolbar shares with the tools it owns. Its identity is stable for
|
|
4
|
+
* the toolbar's lifetime in that mode; tools subscribe to the active tool instead of re-rendering
|
|
5
|
+
* with every focus change.
|
|
6
|
+
*/
|
|
7
|
+
export interface ToolbarRoving {
|
|
8
|
+
/** The active tool's key, or null until the toolbar has chosen one. */
|
|
9
|
+
getActiveKey(): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Whether a tool lies in the toolbar's own DOM scope. A tool rendered elsewhere, for example
|
|
12
|
+
* through a portal or under a nested consumer toolbar, keeps its native Tab stop.
|
|
13
|
+
*/
|
|
14
|
+
isInScope(key: string): boolean;
|
|
15
|
+
/** Subscribes to changes of the active tool. */
|
|
16
|
+
subscribe(listener: () => void): () => void;
|
|
17
|
+
/** Records the element for a tool, or removes it when the element is null. */
|
|
18
|
+
register(key: string, element: HTMLElement | null): void;
|
|
19
|
+
/** Makes a tool the toolbar's single Tab stop. */
|
|
20
|
+
activate(key: string): void;
|
|
21
|
+
}
|
|
22
|
+
/** Provided by a toolbar in single-Tab-stop mode; null in every other mode. */
|
|
23
|
+
export declare const ToolbarRovingContext: import("react").Context<ToolbarRoving | null>;
|
|
24
|
+
/** The props a Components-owned tool spreads onto its focusable element. */
|
|
25
|
+
export interface RovingToolProps<TElement extends HTMLElement> {
|
|
26
|
+
ref?: Ref<TElement>;
|
|
27
|
+
tabIndex?: number;
|
|
28
|
+
onFocus?: (event: FocusEvent<TElement>) => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Makes a Components-owned tool take part in its toolbar's single Tab stop. React renders the
|
|
32
|
+
* tab index, so nothing rewrites DOM attributes that React also owns. A tool with an explicit
|
|
33
|
+
* tab index keeps it and stays outside the roving set.
|
|
34
|
+
* @param explicitTabIndex The tab index the consumer set on the tool, if any.
|
|
35
|
+
* @param onFocus The consumer's focus handler, called before the tool becomes active.
|
|
36
|
+
* @returns The ref, tab index, and focus handler to spread onto the tool's element.
|
|
37
|
+
*/
|
|
38
|
+
export declare const useRovingTool: <TElement extends HTMLElement>(explicitTabIndex: number | undefined, onFocus: ((event: FocusEvent<TElement>) => void) | undefined) => RovingToolProps<TElement>;
|
|
39
|
+
//# sourceMappingURL=ToolbarRovingContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToolbarRovingContext.d.ts","sourceRoot":"","sources":["../../../Common/ToolbarRovingContext.ts"],"names":[],"mappings":"AAGA,OAAO,EAAuE,KAAK,UAAU,EAAE,KAAK,GAAG,EAAE,MAAM,OAAO,CAAC;AAEvH;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC1B,uEAAuE;IACvE,YAAY,IAAI,MAAM,GAAG,IAAI,CAAC;IAC9B;;;OAGG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,gDAAgD;IAChD,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAAC;IACzD,kDAAkD;IAClD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,+CAA4C,CAAC;AAE9E,4EAA4E;AAC5E,MAAM,WAAW,eAAe,CAAC,QAAQ,SAAS,WAAW;IACzD,GAAG,CAAC,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;CACnD;AAQD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,QAAQ,SAAS,WAAW,oBACpC,MAAM,GAAG,SAAS,WAC3B,CAAC,CAAC,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC,GAAG,SAAS,KAC7D,eAAe,CAAC,QAAQ,CA+B1B,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createContext, useContext, useId, useSyncExternalStore, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
const ToolbarRovingContext = createContext(null);
|
|
4
|
+
const subscribeToNothing = () => () => undefined;
|
|
5
|
+
const unmanaged = 'unmanaged';
|
|
6
|
+
const undecided = 'undecided';
|
|
7
|
+
const active = 'active';
|
|
8
|
+
const inactive = 'inactive';
|
|
9
|
+
const useRovingTool = (explicitTabIndex, onFocus) => {
|
|
10
|
+
const roving = useContext(ToolbarRovingContext);
|
|
11
|
+
const key = useId();
|
|
12
|
+
const store = explicitTabIndex === undefined ? roving : null;
|
|
13
|
+
const state = useSyncExternalStore(store ? store.subscribe : subscribeToNothing, () => {
|
|
14
|
+
if (!store)
|
|
15
|
+
return unmanaged;
|
|
16
|
+
const activeKey = store.getActiveKey();
|
|
17
|
+
if (activeKey === null)
|
|
18
|
+
return undecided;
|
|
19
|
+
if (!store.isInScope(key))
|
|
20
|
+
return unmanaged;
|
|
21
|
+
return activeKey === key ? active : inactive;
|
|
22
|
+
}, () => (store ? undecided : unmanaged));
|
|
23
|
+
const ref = useCallback((element) => {
|
|
24
|
+
store?.register(key, element);
|
|
25
|
+
}, [store, key]);
|
|
26
|
+
const handleFocus = useCallback((event) => {
|
|
27
|
+
onFocus?.(event);
|
|
28
|
+
store?.activate(key);
|
|
29
|
+
}, [onFocus, store, key]);
|
|
30
|
+
if (!store)
|
|
31
|
+
return { tabIndex: explicitTabIndex, onFocus };
|
|
32
|
+
return {
|
|
33
|
+
ref,
|
|
34
|
+
tabIndex: state === active ? 0 : state === inactive ? -1 : undefined,
|
|
35
|
+
onFocus: handleFocus,
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export { ToolbarRovingContext, useRovingTool };
|
|
40
|
+
//# sourceMappingURL=ToolbarRovingContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToolbarRovingContext.js","sources":["../../../Common/ToolbarRovingContext.ts"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { createContext, useCallback, useContext, useId, useSyncExternalStore, type FocusEvent, type Ref } from 'react';\n\n/**\n * The single-Tab-stop state a toolbar shares with the tools it owns. Its identity is stable for\n * the toolbar's lifetime in that mode; tools subscribe to the active tool instead of re-rendering\n * with every focus change.\n */\nexport interface ToolbarRoving {\n /** The active tool's key, or null until the toolbar has chosen one. */\n getActiveKey(): string | null;\n /**\n * Whether a tool lies in the toolbar's own DOM scope. A tool rendered elsewhere, for example\n * through a portal or under a nested consumer toolbar, keeps its native Tab stop.\n */\n isInScope(key: string): boolean;\n /** Subscribes to changes of the active tool. */\n subscribe(listener: () => void): () => void;\n /** Records the element for a tool, or removes it when the element is null. */\n register(key: string, element: HTMLElement | null): void;\n /** Makes a tool the toolbar's single Tab stop. */\n activate(key: string): void;\n}\n\n/** Provided by a toolbar in single-Tab-stop mode; null in every other mode. */\nexport const ToolbarRovingContext = createContext<ToolbarRoving | null>(null);\n\n/** The props a Components-owned tool spreads onto its focusable element. */\nexport interface RovingToolProps<TElement extends HTMLElement> {\n ref?: Ref<TElement>;\n tabIndex?: number;\n onFocus?: (event: FocusEvent<TElement>) => void;\n}\n\nconst subscribeToNothing = () => () => undefined;\nconst unmanaged = 'unmanaged';\nconst undecided = 'undecided';\nconst active = 'active';\nconst inactive = 'inactive';\n\n/**\n * Makes a Components-owned tool take part in its toolbar's single Tab stop. React renders the\n * tab index, so nothing rewrites DOM attributes that React also owns. A tool with an explicit\n * tab index keeps it and stays outside the roving set.\n * @param explicitTabIndex The tab index the consumer set on the tool, if any.\n * @param onFocus The consumer's focus handler, called before the tool becomes active.\n * @returns The ref, tab index, and focus handler to spread onto the tool's element.\n */\nexport const useRovingTool = <TElement extends HTMLElement>(\n explicitTabIndex: number | undefined,\n onFocus: ((event: FocusEvent<TElement>) => void) | undefined,\n): RovingToolProps<TElement> => {\n const roving = useContext(ToolbarRovingContext);\n const key = useId();\n // Only a tool without an explicit tab index is managed by the toolbar.\n const store = explicitTabIndex === undefined ? roving : null;\n const state = useSyncExternalStore(\n store ? store.subscribe : subscribeToNothing,\n () => {\n if (!store) return unmanaged;\n const activeKey = store.getActiveKey();\n if (activeKey === null) return undecided;\n if (!store.isInScope(key)) return unmanaged;\n return activeKey === key ? active : inactive;\n },\n // Server-rendered markup keeps native Tab stops until the toolbar chooses its active tool.\n () => (store ? undecided : unmanaged),\n );\n const ref = useCallback((element: TElement | null) => {\n store?.register(key, element);\n }, [store, key]);\n const handleFocus = useCallback((event: FocusEvent<TElement>) => {\n onFocus?.(event);\n store?.activate(key);\n }, [onFocus, store, key]);\n if (!store) return { tabIndex: explicitTabIndex, onFocus };\n // A tool outside the toolbar's scope still registers, so the toolbar can notice when it enters.\n return {\n ref,\n tabIndex: state === active ? 0 : state === inactive ? -1 : undefined,\n onFocus: handleFocus,\n };\n};\n"],"names":[],"mappings":";;MA2Ba,oBAAoB,GAAG,aAAa,CAAuB,IAAI;AAS5E,MAAM,kBAAkB,GAAG,MAAM,MAAM,SAAS;AAChD,MAAM,SAAS,GAAG,WAAW;AAC7B,MAAM,SAAS,GAAG,WAAW;AAC7B,MAAM,MAAM,GAAG,QAAQ;AACvB,MAAM,QAAQ,GAAG,UAAU;MAUd,aAAa,GAAG,CACzB,gBAAoC,EACpC,OAA4D,KACjC;AAC3B,IAAA,MAAM,MAAM,GAAG,UAAU,CAAC,oBAAoB,CAAC;AAC/C,IAAA,MAAM,GAAG,GAAG,KAAK,EAAE;AAEnB,IAAA,MAAM,KAAK,GAAG,gBAAgB,KAAK,SAAS,GAAG,MAAM,GAAG,IAAI;AAC5D,IAAA,MAAM,KAAK,GAAG,oBAAoB,CAC9B,KAAK,GAAG,KAAK,CAAC,SAAS,GAAG,kBAAkB,EAC5C,MAAK;AACD,QAAA,IAAI,CAAC,KAAK;AAAE,YAAA,OAAO,SAAS;AAC5B,QAAA,MAAM,SAAS,GAAG,KAAK,CAAC,YAAY,EAAE;QACtC,IAAI,SAAS,KAAK,IAAI;AAAE,YAAA,OAAO,SAAS;AACxC,QAAA,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC;AAAE,YAAA,OAAO,SAAS;QAC3C,OAAO,SAAS,KAAK,GAAG,GAAG,MAAM,GAAG,QAAQ;AAChD,IAAA,CAAC,EAED,OAAO,KAAK,GAAG,SAAS,GAAG,SAAS,CAAC,CACxC;AACD,IAAA,MAAM,GAAG,GAAG,WAAW,CAAC,CAAC,OAAwB,KAAI;AACjD,QAAA,KAAK,EAAE,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;AACjC,IAAA,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAChB,IAAA,MAAM,WAAW,GAAG,WAAW,CAAC,CAAC,KAA2B,KAAI;AAC5D,QAAA,OAAO,GAAG,KAAK,CAAC;AAChB,QAAA,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC;IACxB,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;AACzB,IAAA,IAAI,CAAC,KAAK;AAAE,QAAA,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE;IAE1D,OAAO;QACH,GAAG;QACH,QAAQ,EAAE,KAAK,KAAK,MAAM,GAAG,CAAC,GAAG,KAAK,KAAK,QAAQ,GAAG,EAAE,GAAG,SAAS;AACpE,QAAA,OAAO,EAAE,WAAW;KACvB;AACL;;"}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type KeyboardEvent } from 'react';
|
|
2
2
|
import { ToolbarFocusMode } from './ToolbarFocusMode.js';
|
|
3
|
+
import type { ToolbarRoving } from './ToolbarRovingContext.js';
|
|
3
4
|
type Orientation = 'horizontal' | 'vertical';
|
|
4
5
|
/** Components-owned toolbar focus behavior; never captures keys from child widgets. */
|
|
5
6
|
export declare const useToolbarKeyboardNavigation: (orientation: Orientation, focusMode: ToolbarFocusMode) => {
|
|
6
7
|
rootRef: import("react").RefObject<HTMLDivElement | null>;
|
|
7
8
|
onKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;
|
|
9
|
+
roving: ToolbarRoving | null;
|
|
8
10
|
};
|
|
9
11
|
export {};
|
|
10
12
|
//# sourceMappingURL=useToolbarKeyboardNavigation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useToolbarKeyboardNavigation.d.ts","sourceRoot":"","sources":["../../../Common/useToolbarKeyboardNavigation.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"useToolbarKeyboardNavigation.d.ts","sourceRoot":"","sources":["../../../Common/useToolbarKeyboardNavigation.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiD,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAC1F,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAAwB,CAAC;AAK5D,KAAK,WAAW,GAAG,YAAY,GAAG,UAAU,CAAC;AAE7C,uFAAuF;AACvF,eAAO,MAAM,4BAA4B,gBAAiB,WAAW,aAAa,gBAAgB;;uBAkLpE,aAAa,CAAC,cAAc,CAAC;;CAwC1D,CAAC"}
|