Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions .github/workflows/javascript-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -701,8 +701,9 @@ jobs:
/tmp/cratis-components.tgz \
"@cratis/arc@${{ matrix.arc-version }}" \
"@cratis/arc.react@${{ matrix.arc-version }}" \
@cratis/fundamentals react@19 react-dom@19 \
reflect-metadata tsyringe
@cratis/fundamentals react@19 react-dom@19 reflect-metadata
cp "$GITHUB_WORKSPACE/Source/scripts/lib/peer-absent-consumer-probe.mjs" verify-peers.mjs
node verify-peers.mjs
node --input-type=module - <<'NODE'
const commandDialog = await import('@cratis/components/CommandDialog');
const dataPage = await import('@cratis/components/DataPage');
Expand Down Expand Up @@ -777,6 +778,8 @@ jobs:
JSON
cp Source/scripts/lib/package-manager-consumer-probe.mjs \
/tmp/components-consumer/verify.mjs
cp Source/scripts/lib/peer-absent-consumer-probe.mjs \
/tmp/components-consumer/verify-peers.mjs

- name: Verify npm consumer
if: matrix.manager == 'npm'
Expand All @@ -788,9 +791,9 @@ jobs:
/tmp/cratis-components.tgz \
@cratis/arc@22.16.0 @cratis/arc.react@22.16.0 \
@cratis/fundamentals@7.19.2 @types/react@19.2.18 \
react@19.2.8 react-dom@19.2.8 \
reflect-metadata@0.2.2 tsyringe@4.10.0 "${PIXI[@]}"
react@19.2.8 react-dom@19.2.8 reflect-metadata@0.2.2 "${PIXI[@]}"
node verify.mjs "${{ matrix.pixi }}"
node verify-peers.mjs

- name: Verify pnpm consumer
if: matrix.manager == 'pnpm'
Expand All @@ -803,9 +806,9 @@ jobs:
/tmp/cratis-components.tgz \
@cratis/arc@22.16.0 @cratis/arc.react@22.16.0 \
@cratis/fundamentals@7.19.2 @types/react@19.2.18 \
react@19.2.8 react-dom@19.2.8 \
reflect-metadata@0.2.2 tsyringe@4.10.0 "${PIXI[@]}"
react@19.2.8 react-dom@19.2.8 reflect-metadata@0.2.2 "${PIXI[@]}"
pnpm exec node verify.mjs "${{ matrix.pixi }}"
pnpm exec node verify-peers.mjs

- name: Verify Yarn PnP consumer
if: matrix.manager == 'yarn-pnp'
Expand All @@ -822,8 +825,9 @@ jobs:
@cratis/arc@22.16.0 @cratis/arc.react@22.16.0 \
@cratis/fundamentals@7.19.2 @types/react@19.2.18 \
react@19.2.8 react-dom@19.2.8 \
reflect-metadata@0.2.2 tsyringe@4.10.0 rxjs@7.8.2 "${PIXI[@]}"
reflect-metadata@0.2.2 rxjs@7.8.2 "${PIXI[@]}"
yarn node verify.mjs "${{ matrix.pixi }}"
yarn node verify-peers.mjs

verify-renderer-adapters:
name: Renderer adapter (${{ matrix.adapter }}, ${{ matrix.boundary }}, ${{ matrix.manager }})
Expand Down
6 changes: 4 additions & 2 deletions Documentation/Chat/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Everything about _data_ stays with your application:
- `@`-mentions from a list you hold or a provider callback you resolve, rendered distinctly in message bodies
- Emoji picker in the composer, with a quick row of recently used emoji
- Host-supplied render callbacks for avatars and display names — messages carry only the author id
- Extensible per-message actions shown on hover or keyboard focus
- Extensible per-message and per-topic actions shown on hover or keyboard focus
- Every color from the `--cratis-*` token seam, so the chat follows your theme

## Components
Expand Down Expand Up @@ -97,7 +97,8 @@ export const Workspace = () => {
| `onRequestTopicName` / `isTopicUnnamed` | callbacks | — | The host-side naming contract. |
| `selectedTopicId` / `onTopicSelected` | `ChatIdentifier \| null`, callback | Internal | Owns or observes the open topic. |
| `authorOf`, `renderAvatar`, `renderAuthorName`, `buildAvatarUrl` | callbacks | — | Author resolution and rendering. Without `authorOf`, the id is shown as the name. |
| `actions`, `quickReply` | `ChatMessageAction[]`, `boolean` | —, `true` | See [Message actions](./message-actions.md). |
| `actions`, `quickReply` | `ChatMessageAction[]`, `boolean` | —, `true` | Message actions and quick reply; see [Message actions](./message-actions.md). |
| `topicActions` | `ChatTopicAction<TTopic>[]` | — | Actions beside available topics; see [Topic actions](./topic-actions.md). |
| `mentionCandidates` / `resolveMentionCandidates` | array or callback | — | See [Mentions and emoji](./mentions-and-emoji.md). Omit both to turn mentions off. |
| `typingAuthors` | `ChatTypingAuthor[]` | `[]` | Who the conversation is waiting on. |
| `autoFocus` | `boolean` | `false` | Focuses the composer when a conversation mounts. |
Expand Down Expand Up @@ -218,4 +219,5 @@ Import `@cratis/components/styles` (or, with per-area stylesheets, `@cratis/comp
- [Topics and naming](./topics-and-naming.md) — the topic lifecycle and the host-side auto-naming contract
- [Mentions and emoji](./mentions-and-emoji.md) — candidate providers, how mentions travel and render
- [Message actions](./message-actions.md) — offering your own actions on messages
- [Topic actions](./topic-actions.md) — offering your own actions on topics
- [Observable queries](./observable-queries.md) — the optional Arc-aware wrapper
2 changes: 2 additions & 0 deletions Documentation/Chat/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
href: mentions-and-emoji.md
- name: Message actions
href: message-actions.md
- name: Topic actions
href: topic-actions.md
- name: Observable queries
href: observable-queries.md
41 changes: 41 additions & 0 deletions Documentation/Chat/topic-actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: Topic actions
description: Offer host-owned actions next to each topic without opening its conversation.
---

Pass `topicActions` to `ChatSidebar` or the standalone `ChatTopicList` when a person needs to rename, archive, or otherwise act on a topic without opening it. Unlike the sidebar's `actions` prop, which applies to **messages**, `topicActions` applies to topics only.

## Descriptor

| Field | Type | Required | Behavior |
| --- | --- | --- | --- |
| `id` | `string` | Yes | Unique rendering key among the actions. |
| `label` | `string` | Yes | Tooltip and accessible name (combined with the topic's name). Shown as button text if no icon is supplied. |
| `icon` | `string \| ReactNode` | No | An icon-font CSS class name or a ready element. |
| `isAvailable` | `(topic: TTopic) => boolean` | No | Return `false` to omit the action for a topic; omitted means available on every topic. |
| `onInvoke` | `(topic: TTopic) => void` | Yes | Receives the full host topic when the control is activated. |

The buttons sit beside, not inside, the button that opens the conversation. They appear on hover or keyboard focus. Tab reaches each available control, and activating one does not open the topic.

## Offer an action

This excerpt assumes `sidebarProps` supplies `open`, `onClose`, `topics`, `messages`, and `onSendMessage` as in [basic usage](./index.md#basic-usage). The handlers belong to your application.

```tsx
import { ChatSidebar } from '@cratis/components/Chat';

<ChatSidebar
{...sidebarProps}
topicActions={[
{
id: 'rename',
label: 'Rename',
icon: 'my-icon-font-edit',
isAvailable: topic => Boolean(topic.name?.trim()),
onInvoke: topic => renameTopic(topic.id),
},
]}
/>
```

The same `topicActions` prop works on `ChatTopicList`. Omit it to leave the existing topic markup and opening behavior unchanged.
12 changes: 12 additions & 0 deletions Documentation/Chat/topics-and-naming.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ interface ChatTopic {

The list orders topics by `lastActivity` (falling back to `started`) itself — hand it topics in any order.

## ChatTopicList props

| Prop | Type | Default | Behavior |
| --- | --- | --- | --- |
| `topics`, `onOpen` | `TTopic[]`, `(topic: TTopic) => void` | Required | Topics and the callback for opening one. |
| `onStart` | `() => void` | — | Shows the new-topic button when supplied. |
| `topicActions` | `ChatTopicAction<TTopic>[]` | — | Host actions beside topics where they are available. See [Topic actions](./topic-actions.md). |
| `status` | `ChatStatus` | `Ready` | List query display state. |
| `authorOf`, `renderAvatar`, `buildAvatarUrl` | callbacks | — | Resolve and render the topic starter. |
| `isTopicUnnamed` | `(topic: TTopic) => boolean` | Blank name | Decides when to show the pending placeholder. |
| `labels`, `className` | `ChatTopicListLabels`, `string` | — | Label overrides and root class name. |

## Starting a topic

`onStartTopic` raises the intent; creating the topic is the application's business. Answer with the new topic's id — directly or through a promise — and the sidebar opens it, ready for the first message:
Expand Down
6 changes: 3 additions & 3 deletions Documentation/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ The examples use the proxies from the Arc tutorial: `RegisterAuthor` has a requi

React Aria, the internationalized date implementation, and React Icons are internal dependencies. The package does not depend on PrimeReact or any other UI kit or theme runtime, and you don't configure an icon package. The optional MUI and PrimeReact integrations are separate [renderer adapter](/components/renderers/) packages.

The package declares React, Arc, Fundamentals, `reflect-metadata`, and `tsyringe` as peers. Keep `@cratis/arc` and `@cratis/arc.react` on the same version your generated proxies were produced with. A strict installer can install every peer explicitly (replace the Arc version with the one your backend uses):
The package requires React, Arc, Fundamentals, and `reflect-metadata` as peers. `tsyringe` is an optional peer: Components never imports it, and Arc React installs it as its own dependency. Keep `@cratis/arc` and `@cratis/arc.react` on the same version your generated proxies were produced with. A strict installer can install the required peers explicitly (replace the Arc version with the one your backend uses):

```bash title="Install explicit peers"
ARC_VERSION=22.16.0
npm install @cratis/components@^4 \
"@cratis/arc@$ARC_VERSION" "@cratis/arc.react@$ARC_VERSION" \
@cratis/fundamentals@^7.19.2 react@^19 react-dom@^19 \
reflect-metadata@0.2.2 tsyringe@4.10.0
reflect-metadata@0.2.2
```

Use Arc 22.16.0 or newer if you plan to use `AutoCommandForm`'s explicit field binding.

`pixi.js@^8.20.0` is an additional **optional** peer, needed only if you use `Canvas` or `PivotViewer`. Every other component needs nothing beyond the peers above; install Pixi later, when you reach a spatial workspace or card-grid screen. See [Choosing a component](/components/choosing-a-component/#spatial-workspaces).
`pixi.js@^8.20.0` is another **optional** peer, needed only if you use `Canvas` or `PivotViewer`. Every other component needs nothing beyond the required peers above; install Pixi later, when you reach a spatial workspace or card-grid screen. See [Choosing a component](/components/choosing-a-component/#spatial-workspaces).

2. **Import the stylesheets once**, at your application entry point, after `reflect-metadata`:

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ npm install @cratis/components@^4

Components 4 needs React 19 and Arc. Its peers are `react` and `react-dom` `^19.0.0`,
`@cratis/arc` and `@cratis/arc.react` `>=20.3.1 <23`, `@cratis/fundamentals` `^7.10.3`,
`reflect-metadata` `0.2.2`, and `tsyringe` `4.10.0`; `pixi.js` `^8.20.0` is an optional peer
needed only by `Canvas` and `PivotViewer`. This repository builds and tests against Arc 22.16.0.
and `reflect-metadata` `0.2.2`. `tsyringe` `4.10.0` is an optional peer that Arc React already
installs, and `pixi.js` `^8.20.0` is an optional peer needed only by `Canvas` and `PivotViewer`. This repository builds and tests against Arc 22.16.0.
The package does not depend on PrimeReact; the optional MUI and PrimeReact renderer adapters are
separate packages.

Expand Down
3 changes: 2 additions & 1 deletion Source/Chat/ChatSidebar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from 'storybook/test';
import { useRef, useState } from 'react';
import { FaCopy } from 'react-icons/fa6';
import { FaCopy, FaPen } from 'react-icons/fa6';
import { ChatAuthorKind } from './Kit/ChatAuthorKind';
import type { ChatAuthor } from './ChatAuthor';
import type { ChatIdentifier } from './ChatIdentifier';
Expand Down Expand Up @@ -72,6 +72,7 @@ export const Playground: Story = {
onStartTopic: fn(),
onRequestTopicName: fn(),
onTopicSelected: fn(),
topicActions: [{ id: 'rename', label: 'Rename', icon: <FaPen />, onInvoke: fn() }],
authorOf,
mentionCandidates,
},
Expand Down
6 changes: 6 additions & 0 deletions Source/Chat/ChatSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { sameChatIdentifier, type ChatIdentifier } from './ChatIdentifier';
import type { ChatMention } from './ChatMention';
import type { ChatMessage } from './ChatMessage';
import type { ChatTopic } from './ChatTopic';
import type { ChatTopicAction } from './ChatTopicAction';
import type { ChatStatus } from './ChatStatus';
import type { ChatTopicListLabels } from './ChatTopicList';
import { ChatTopicList } from './ChatTopicList';
Expand Down Expand Up @@ -107,6 +108,9 @@ export interface ChatSidebarProps<
*/
messages: TMessage[];

/** The host's actions to offer on each topic where they are available. The inherited `actions` prop applies only to messages. */
topicActions?: ChatTopicAction<TTopic>[];

/** Topic-list query display state. Defaults to ready when omitted. */
topicsStatus?: ChatStatus;

Expand Down Expand Up @@ -225,6 +229,7 @@ export const ChatSidebar = <
onClose,
topics,
messages,
topicActions,
topicsStatus,
messagesStatus,
selectedTopicId,
Expand Down Expand Up @@ -399,6 +404,7 @@ export const ChatSidebar = <
{openTopicId === undefined ? (
<ChatTopicList<TTopic>
topics={topics}
topicActions={topicActions}
status={topicsStatus}
onOpen={(topic) => select(topic.id, topic)}
onStart={
Expand Down
30 changes: 30 additions & 0 deletions Source/Chat/ChatTopicAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import type { ReactNode } from 'react';
import type { ChatTopic } from './ChatTopic';

/** An action a host offers on topics. The library ships no topic actions of its own.
* @typeParam TTopic The host's topic type, so {@link onInvoke} receives the full topic back.
*/
export interface ChatTopicAction<TTopic extends ChatTopic = ChatTopic> {
/** Identifies the action among its siblings — used as the rendering key. */
id: string;

/** The label, used in the tooltip and accessible name alongside the topic's name. */
label: string;

/** An icon element or a CSS class name for the host's icon font. Omit to show the label. */
icon?: string | ReactNode;

/** Decides which topics the action is offered on. Omit to offer it on all topics.
* @param topic The topic the action would apply to.
* @returns True when the action should be offered.
*/
isAvailable?: (topic: TTopic) => boolean;

/** Invoked when the action is picked on a topic.
* @param topic The topic it was picked on.
*/
onInvoke: (topic: TTopic) => void;
}
55 changes: 55 additions & 0 deletions Source/Chat/ChatTopicActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import type { FocusEventHandler } from 'react';
import type { ChatTopic } from './ChatTopic';
import type { ChatTopicAction } from './ChatTopicAction';

/**
* Props for {@link ChatTopicActions}.
* @typeParam TTopic The topic type the list renders.
*/
export interface ChatTopicActionsProps<TTopic extends ChatTopic> {
/** The topic the actions act on. */
topic: TTopic;
/** The topic's visible name, used to give each action an accessible name for its topic. */
topicName: string | undefined;
/** The actions available for this topic. */
actions: ChatTopicAction<TTopic>[];
/** Called when an action button gains focus. */
onActionFocus: FocusEventHandler<HTMLButtonElement>;
/** Called when an action button loses focus. */
onActionBlur: FocusEventHandler<HTMLButtonElement>;
}

/**
* The action buttons shown beside a topic's opening button. Reuses the message action overlay,
* which keyboard focus reveals.
*/
export const ChatTopicActions = <TTopic extends ChatTopic>({
topic,
topicName,
actions,
onActionFocus,
onActionBlur,
}: ChatTopicActionsProps<TTopic>) => (
<div className='cratis-chat-message__actions'>
{actions.map((action) => (
<button
key={action.id}
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)}
>
{typeof action.icon === 'string' ? (
<i className={action.icon} aria-hidden='true' />
) : (action.icon ?? action.label)}
</button>
))}
</div>
);
18 changes: 17 additions & 1 deletion Source/Chat/ChatTopicList.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,26 @@
transition: background 0.12s;
}

.cratis-chat-topics__topic:hover {
.cratis-chat-topics__topic:hover,
.cratis-chat-topics__row:hover .cratis-chat-topics__topic {
background: var(--cratis-surface-hover);
}

/* Topic actions reuse the message action overlay, centered on the row instead of hanging above it. */
.cratis-chat-topics__row {
position: relative;
}

.cratis-chat-topics__row .cratis-chat-message__actions {
top: 50%;
transform: translateY(-50%);
}

.cratis-chat-topics__row:hover .cratis-chat-message__actions {
opacity: 1;
pointer-events: auto;
}

.cratis-chat-topics__details {
flex: 1;
min-width: 0;
Expand Down
21 changes: 21 additions & 0 deletions Source/Chat/ChatTopicList.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import type { Meta, StoryObj } from '@storybook/react';
import { fn } from 'storybook/test';
import { FaPen, FaBoxArchive } from 'react-icons/fa6';
import { ChatAuthorKind } from './Kit/ChatAuthorKind';
import type { ChatAuthor } from './ChatAuthor';
import type { ChatIdentifier } from './ChatIdentifier';
Expand Down Expand Up @@ -74,6 +75,26 @@ export const Playground: Story = {
),
};

/** Hover a topic, or Tab from its open button, to see the host's available actions. */
export const WithActions: Story = {
args: {
topics: [
{ id: 'topic-1', name: 'Example topic' },
{ id: 'topic-2', name: 'Pinned topic', metadata: { pinned: true } },
],
onOpen: fn(),
topicActions: [
{ id: 'rename', label: 'Rename', icon: <FaPen />, onInvoke: fn() },
{ id: 'archive', label: 'Archive', icon: <FaBoxArchive />, isAvailable: (topic) => topic.metadata?.pinned !== true, onInvoke: fn() },
],
},
render: (args) => (
<div style={{ width: 360, border: '1px solid var(--cratis-surface-border)', borderRadius: 8 }}>
<ChatTopicList {...args} />
</div>
),
};

/** Nothing to pick from yet — only the empty message and the way to start the first topic. */
export const Empty: Story = {
args: {
Expand Down
Loading
Loading