@intentface/chat 0.1.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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/ask-user/ask-user.d.ts +104 -0
- package/dist/ask-user/ask-user.js +226 -0
- package/dist/ask-user/index.d.ts +3 -0
- package/dist/ask-user/index.js +3 -0
- package/dist/ask-user/index.parts.d.ts +1 -0
- package/dist/ask-user/index.parts.js +5 -0
- package/dist/attachments/attachments.d.ts +41 -0
- package/dist/attachments/attachments.js +91 -0
- package/dist/attachments/index.d.ts +3 -0
- package/dist/attachments/index.js +3 -0
- package/dist/attachments/index.parts.d.ts +1 -0
- package/dist/attachments/index.parts.js +5 -0
- package/dist/chip/chip.d.ts +25 -0
- package/dist/chip/chip.js +25 -0
- package/dist/chip/index.d.ts +2 -0
- package/dist/chip/index.js +2 -0
- package/dist/chip/index.parts.d.ts +1 -0
- package/dist/chip/index.parts.js +5 -0
- package/dist/chip-markdown.d.ts +25 -0
- package/dist/chip-markdown.js +52 -0
- package/dist/composer/actions.d.ts +35 -0
- package/dist/composer/actions.js +75 -0
- package/dist/composer/ask-user-machine.d.ts +52 -0
- package/dist/composer/ask-user-machine.js +164 -0
- package/dist/composer/attachments-machine.d.ts +34 -0
- package/dist/composer/attachments-machine.js +91 -0
- package/dist/composer/attachments.d.ts +16 -0
- package/dist/composer/attachments.js +32 -0
- package/dist/composer/command-list.d.ts +49 -0
- package/dist/composer/command-list.js +357 -0
- package/dist/composer/commands.d.ts +28 -0
- package/dist/composer/commands.js +56 -0
- package/dist/composer/container.d.ts +3 -0
- package/dist/composer/container.js +32 -0
- package/dist/composer/controller.d.ts +3 -0
- package/dist/composer/controller.js +6 -0
- package/dist/composer/editor-dom.d.ts +55 -0
- package/dist/composer/editor-dom.js +431 -0
- package/dist/composer/fuzzy.d.ts +4 -0
- package/dist/composer/fuzzy.js +49 -0
- package/dist/composer/index.d.ts +25 -0
- package/dist/composer/index.js +13 -0
- package/dist/composer/index.parts.d.ts +11 -0
- package/dist/composer/index.parts.js +15 -0
- package/dist/composer/internals.d.ts +29 -0
- package/dist/composer/internals.js +102 -0
- package/dist/composer/keyboard.d.ts +61 -0
- package/dist/composer/keyboard.js +75 -0
- package/dist/composer/panel.d.ts +28 -0
- package/dist/composer/panel.js +80 -0
- package/dist/composer/placeholder.d.ts +10 -0
- package/dist/composer/placeholder.js +8 -0
- package/dist/composer/popover.d.ts +12 -0
- package/dist/composer/popover.js +63 -0
- package/dist/composer/prefix-detection.d.ts +23 -0
- package/dist/composer/prefix-detection.js +58 -0
- package/dist/composer/root.d.ts +21 -0
- package/dist/composer/root.js +107 -0
- package/dist/composer/segments.d.ts +108 -0
- package/dist/composer/segments.js +366 -0
- package/dist/composer/store.d.ts +97 -0
- package/dist/composer/store.js +451 -0
- package/dist/composer/textarea.d.ts +39 -0
- package/dist/composer/textarea.js +64 -0
- package/dist/composer/trigger-tracker.d.ts +12 -0
- package/dist/composer/trigger-tracker.js +90 -0
- package/dist/composer/types.d.ts +126 -0
- package/dist/composer/types.js +4 -0
- package/dist/composer/use-composer-editor.d.ts +43 -0
- package/dist/composer/use-composer-editor.js +649 -0
- package/dist/internal/collapsible.d.ts +22 -0
- package/dist/internal/collapsible.js +90 -0
- package/dist/internal/primitive-props.d.ts +4 -0
- package/dist/internal/primitive-props.js +4 -0
- package/dist/internal/render/baseTypes.d.ts +18 -0
- package/dist/internal/render/baseTypes.js +4 -0
- package/dist/internal/render/empty.d.ts +3 -0
- package/dist/internal/render/empty.js +5 -0
- package/dist/internal/render/getReactElementRef.d.ts +5 -0
- package/dist/internal/render/getReactElementRef.js +15 -0
- package/dist/internal/render/getStateAttributesProps.d.ts +4 -0
- package/dist/internal/render/getStateAttributesProps.js +23 -0
- package/dist/internal/render/mergeObjects.d.ts +1 -0
- package/dist/internal/render/mergeObjects.js +14 -0
- package/dist/internal/render/mergeProps.d.ts +55 -0
- package/dist/internal/render/mergeProps.js +183 -0
- package/dist/internal/render/positioning.d.ts +22 -0
- package/dist/internal/render/positioning.js +78 -0
- package/dist/internal/render/reactVersion.d.ts +3 -0
- package/dist/internal/render/reactVersion.js +7 -0
- package/dist/internal/render/resolveClassName.d.ts +8 -0
- package/dist/internal/render/resolveClassName.js +12 -0
- package/dist/internal/render/resolveStyle.d.ts +8 -0
- package/dist/internal/render/resolveStyle.js +12 -0
- package/dist/internal/render/transition.d.ts +48 -0
- package/dist/internal/render/transition.js +203 -0
- package/dist/internal/render/types.d.ts +74 -0
- package/dist/internal/render/types.js +3 -0
- package/dist/internal/render/useMergedRefs.d.ts +21 -0
- package/dist/internal/render/useMergedRefs.js +102 -0
- package/dist/internal/render/useRefWithInit.d.ts +10 -0
- package/dist/internal/render/useRefWithInit.js +12 -0
- package/dist/internal/render/useRender.d.ts +71 -0
- package/dist/internal/render/useRender.js +12 -0
- package/dist/internal/render/useRenderElement.d.ts +62 -0
- package/dist/internal/render/useRenderElement.js +157 -0
- package/dist/internal/render/warn.d.ts +1 -0
- package/dist/internal/render/warn.js +15 -0
- package/dist/internal/state-mappings.d.ts +8 -0
- package/dist/internal/state-mappings.js +17 -0
- package/dist/message/index.d.ts +3 -0
- package/dist/message/index.js +3 -0
- package/dist/message/index.parts.d.ts +1 -0
- package/dist/message/index.parts.js +5 -0
- package/dist/message/message.d.ts +43 -0
- package/dist/message/message.js +78 -0
- package/dist/message-utils.d.ts +55 -0
- package/dist/message-utils.js +79 -0
- package/dist/reasoning/index.d.ts +3 -0
- package/dist/reasoning/index.js +3 -0
- package/dist/reasoning/index.parts.d.ts +1 -0
- package/dist/reasoning/index.parts.js +5 -0
- package/dist/reasoning/reasoning.d.ts +20 -0
- package/dist/reasoning/reasoning.js +49 -0
- package/dist/steps/index.d.ts +2 -0
- package/dist/steps/index.js +2 -0
- package/dist/steps/index.parts.d.ts +1 -0
- package/dist/steps/index.parts.js +5 -0
- package/dist/steps/steps.d.ts +53 -0
- package/dist/steps/steps.js +79 -0
- package/dist/thread/geometry.d.ts +18 -0
- package/dist/thread/geometry.js +59 -0
- package/dist/thread/index.d.ts +4 -0
- package/dist/thread/index.js +3 -0
- package/dist/thread/index.parts.d.ts +1 -0
- package/dist/thread/index.parts.js +5 -0
- package/dist/thread/stores.d.ts +21 -0
- package/dist/thread/stores.js +63 -0
- package/dist/thread/thread.d.ts +66 -0
- package/dist/thread/thread.js +639 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +17 -0
- package/package.json +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Intentface
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @intentface/chat
|
|
2
|
+
|
|
3
|
+
Headless chat UI primitives for React — the behavior, state, and wire formats
|
|
4
|
+
for building AI chat interfaces, with no styling of their own. Pair them with
|
|
5
|
+
the shadcn-style styled layer from the [docs](https://intentface.dev/docs), or
|
|
6
|
+
bring your own.
|
|
7
|
+
|
|
8
|
+
This is the Base UI model applied to chat: **install the logic from npm, copy
|
|
9
|
+
the look from the docs.**
|
|
10
|
+
|
|
11
|
+
## Status
|
|
12
|
+
|
|
13
|
+
Pre-1.0. The primitives are in real use, but the API is still settling and
|
|
14
|
+
minor releases may contain breaking changes — the usual `0.x` semver contract.
|
|
15
|
+
Pin an exact version if that matters to you, and check the
|
|
16
|
+
[changelog](https://github.com/Intentface/intentface-chat/blob/main/packages/chat/CHANGELOG.md)
|
|
17
|
+
when upgrading.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun add @intentface/chat
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`react` and `react-dom` (v19+) are peer dependencies. Nothing else ships with
|
|
26
|
+
it beyond two small runtime deps (`@floating-ui/dom` for anchored positioning,
|
|
27
|
+
`nanoid` for attachment ids) — no editor framework, no animation library.
|
|
28
|
+
|
|
29
|
+
## What's inside
|
|
30
|
+
|
|
31
|
+
Each primitive is a separate entry point, exporting an unstyled compound
|
|
32
|
+
component that renders semantic DOM with `data-*` state attributes, context
|
|
33
|
+
hooks, and a `render` prop for swapping the underlying element:
|
|
34
|
+
|
|
35
|
+
| Import | What it is |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `@intentface/chat/composer` | Rich-text input over a purpose-built contenteditable engine: `/` and `@` command palette, inline chips, attachments, ask-user flow. One store per `<Composer>`, or bring your own via `Composer.createStore()`. |
|
|
38
|
+
| `@intentface/chat/thread` | Scroll container with at-bottom detection and auto-follow. |
|
|
39
|
+
| `@intentface/chat/message` | Message parts, turns, chip-segmented text, sources, actions. |
|
|
40
|
+
| `@intentface/chat/steps`, `/reasoning` | Tool-call timelines and reasoning disclosure. |
|
|
41
|
+
| `@intentface/chat/chip`, `/attachments`, `/ask-user` | The remaining building blocks. |
|
|
42
|
+
| `@intentface/chat/types` | The structural message contract + part type guards. |
|
|
43
|
+
| `@intentface/chat/message-utils` | Part segmentation, turn grouping, reasoning/source derivation. |
|
|
44
|
+
| `@intentface/chat/chip-markdown` | The self-describing chip wire format. |
|
|
45
|
+
|
|
46
|
+
## AI SDK, structurally
|
|
47
|
+
|
|
48
|
+
The types are framework-agnostic. A Vercel AI SDK `UIMessage` satisfies
|
|
49
|
+
`ChatMessage` structurally, so there's no adapter — pass SDK messages straight
|
|
50
|
+
into the utilities and components:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { groupTurns } from "@intentface/chat/message-utils";
|
|
54
|
+
import type { UIMessage } from "ai";
|
|
55
|
+
|
|
56
|
+
const turns = groupTurns(messages); // messages: UIMessage[]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## React Server Components
|
|
60
|
+
|
|
61
|
+
Each primitive is a namespace, and the root is explicit — `Message.Root`, not
|
|
62
|
+
`Message`. Server components can reach every part:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
// A server component — no "use client" needed.
|
|
66
|
+
import { Message } from "@intentface/chat/message";
|
|
67
|
+
|
|
68
|
+
export const Transcript = ({ messages }: { messages: ChatMessage[] }) => (
|
|
69
|
+
<>
|
|
70
|
+
{messages.map((message) => (
|
|
71
|
+
<Message.Root key={message.id} role={message.role}>
|
|
72
|
+
<Message.Text>{text(message)}</Message.Text>
|
|
73
|
+
</Message.Root>
|
|
74
|
+
))}
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The parts are still client components — they carry their own `"use client"` — so
|
|
80
|
+
interactive props behave the way React requires. Pass an event handler from a
|
|
81
|
+
server component and you'll get React's usual error; move that piece into a
|
|
82
|
+
client component:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
"use client";
|
|
86
|
+
import { Composer } from "@intentface/chat/composer";
|
|
87
|
+
|
|
88
|
+
export const Chat = () => (
|
|
89
|
+
<Composer.Root onSubmit={(data) => send(data)}>
|
|
90
|
+
<Composer.Container>
|
|
91
|
+
<Composer.Textarea />
|
|
92
|
+
</Composer.Container>
|
|
93
|
+
</Composer.Root>
|
|
94
|
+
);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Hooks (`useComposer`, `useThread`, `useReasoning`, …) are client-only, as usual.
|
|
98
|
+
The pure modules — `/types`, `/message-utils`, `/chip-markdown` — carry no
|
|
99
|
+
directive and import fine anywhere.
|
|
100
|
+
|
|
101
|
+
## Docs
|
|
102
|
+
|
|
103
|
+
Full guides, live previews, and copy-paste styled source at
|
|
104
|
+
[intentface.dev/docs](https://intentface.dev/docs).
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type ReactNode, type RefObject } from "react";
|
|
2
|
+
import type { PrimitiveProps } from "../internal/primitive-props.js";
|
|
3
|
+
/** AskUser root container. Stateless — consumers manage all state externally. */
|
|
4
|
+
export type AskUserRootProps = PrimitiveProps<"div">;
|
|
5
|
+
export declare const AskUserRoot: ({ className, render, style, ...elementProps }: AskUserRootProps) => import("react").JSX.Element;
|
|
6
|
+
/** Question heading text. Carries the id `Options` uses as its accessible name. */
|
|
7
|
+
export type AskUserLabelProps = PrimitiveProps<"p">;
|
|
8
|
+
export declare const AskUserLabel: ({ className, render, style, ...elementProps }: AskUserLabelProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
9
|
+
/** Row container for `Label` and optional `Navigation`. */
|
|
10
|
+
export type AskUserHeaderProps = PrimitiveProps<"div">;
|
|
11
|
+
export declare const AskUserHeader: ({ className, render, style, ...elementProps }: AskUserHeaderProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
12
|
+
/** Row container for `Previous`, `StepLabel`, and `Next`. */
|
|
13
|
+
export type AskUserNavigationProps = PrimitiveProps<"div">;
|
|
14
|
+
export declare const AskUserNavigation: ({ className, render, style, ...elementProps }: AskUserNavigationProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
15
|
+
/** Navigate to the previous step. Default accessible name; override via aria-label. */
|
|
16
|
+
export type AskUserPreviousProps = PrimitiveProps<"button">;
|
|
17
|
+
export declare const AskUserPrevious: ({ className, render, style, ...elementProps }: AskUserPreviousProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
18
|
+
/** Navigate to the next step. Default accessible name; override via aria-label. */
|
|
19
|
+
export type AskUserNextProps = PrimitiveProps<"button">;
|
|
20
|
+
export declare const AskUserNext: ({ className, render, style, ...elementProps }: AskUserNextProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
21
|
+
/** Step indicator. Reads the current step + total from the composer store; the
|
|
22
|
+
* consumer supplies the text via `children` — either a node, or a callback
|
|
23
|
+
* receiving `{ current, total }` (1-based current). No default format. */
|
|
24
|
+
export type AskUserStepLabelState = {
|
|
25
|
+
current: number;
|
|
26
|
+
total: number;
|
|
27
|
+
};
|
|
28
|
+
export type AskUserStepLabelProps = Omit<PrimitiveProps<"span">, "children"> & {
|
|
29
|
+
children?: ReactNode | ((state: AskUserStepLabelState) => ReactNode);
|
|
30
|
+
};
|
|
31
|
+
export declare const AskUserStepLabel: ({ children, className, render, style, ...elementProps }: AskUserStepLabelProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
32
|
+
/** Fieldset wrapper for `Option` items. Provides `multiSelect`, `groupName`, and highlight state to child options via context. Items self-register on mount (cmdk pattern). */
|
|
33
|
+
type OptionsContextValue = {
|
|
34
|
+
multiSelect: boolean;
|
|
35
|
+
groupName: string;
|
|
36
|
+
highlightedValue: string | null;
|
|
37
|
+
items: RefObject<string[]>;
|
|
38
|
+
register: (value: string, getElement: () => HTMLElement | null) => () => void;
|
|
39
|
+
onItemHover: (value: string) => void;
|
|
40
|
+
};
|
|
41
|
+
export declare const useAskUserOptions: () => OptionsContextValue;
|
|
42
|
+
/** Imperative handle exposed by `AskUser.Options` for keyboard navigation. */
|
|
43
|
+
export type AskUserOptionsHandle = {
|
|
44
|
+
/** Move highlight by direction, focusing the newly highlighted option (roving tabindex). Returns the new highlighted value (null = past the list boundary). */
|
|
45
|
+
navigate: (direction: number) => string | null;
|
|
46
|
+
select: () => {
|
|
47
|
+
value: string;
|
|
48
|
+
} | null;
|
|
49
|
+
clearHighlight: () => void;
|
|
50
|
+
resetHighlight: () => void;
|
|
51
|
+
/** Move DOM focus onto the highlighted option (or the first option when none is highlighted). */
|
|
52
|
+
focusHighlighted: () => void;
|
|
53
|
+
/** The options container element — the composer attaches its question-mode keydown handling here. */
|
|
54
|
+
getElement: () => HTMLElement | null;
|
|
55
|
+
highlightedValue: string | null;
|
|
56
|
+
};
|
|
57
|
+
export type AskUserOptionsProps = Omit<PrimitiveProps<"fieldset">, "value" | "ref"> & {
|
|
58
|
+
/** When true, options render as multi-select (checkbox semantics in the styled layer). */
|
|
59
|
+
multiSelect?: boolean;
|
|
60
|
+
/** Shared `name` attribute for all option inputs in this group. */
|
|
61
|
+
groupName?: string;
|
|
62
|
+
/** Imperative ref for keyboard navigation (navigate, select, clearHighlight, resetHighlight). */
|
|
63
|
+
ref?: RefObject<AskUserOptionsHandle | null>;
|
|
64
|
+
};
|
|
65
|
+
export declare const AskUserOptions: ({ multiSelect, groupName, ref, className, render, style, children, ...elementProps }: AskUserOptionsProps) => import("react").JSX.Element;
|
|
66
|
+
/** Selectable card. Self-registers with parent `Options` on mount (cmdk pattern). Derives highlight state from context. */
|
|
67
|
+
type OptionContextValue = {
|
|
68
|
+
id: string;
|
|
69
|
+
value: string;
|
|
70
|
+
selected: boolean;
|
|
71
|
+
onSelect?: () => void;
|
|
72
|
+
};
|
|
73
|
+
export declare const useAskUserOption: () => OptionContextValue;
|
|
74
|
+
export type AskUserOptionState = {
|
|
75
|
+
/** Present as data-highlighted while keyboard/hover highlighted. */
|
|
76
|
+
highlighted: boolean;
|
|
77
|
+
/** Present as data-selected while the option is selected. */
|
|
78
|
+
selected: boolean;
|
|
79
|
+
};
|
|
80
|
+
export type AskUserOptionProps = PrimitiveProps<"label", AskUserOptionState> & {
|
|
81
|
+
value?: string;
|
|
82
|
+
selected?: boolean;
|
|
83
|
+
onSelect?: () => void;
|
|
84
|
+
};
|
|
85
|
+
export declare const AskUserOption: ({ value, selected, onSelect, className, render, style, children, ...elementProps }: AskUserOptionProps) => import("react").JSX.Element;
|
|
86
|
+
/** Flex column wrapper for `OptionLabel` and `OptionDescription`. */
|
|
87
|
+
export type AskUserOptionContentProps = PrimitiveProps<"span">;
|
|
88
|
+
export declare const AskUserOptionContent: ({ className, render, style, ...elementProps }: AskUserOptionContentProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
89
|
+
/** Option title text. */
|
|
90
|
+
export type AskUserOptionLabelProps = PrimitiveProps<"span">;
|
|
91
|
+
export declare const AskUserOptionLabel: ({ className, render, style, ...elementProps }: AskUserOptionLabelProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
92
|
+
/** Option subtitle/description text. */
|
|
93
|
+
export type AskUserOptionDescriptionProps = PrimitiveProps<"span">;
|
|
94
|
+
export declare const AskUserOptionDescription: ({ className, render, style, ...elementProps }: AskUserOptionDescriptionProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
95
|
+
/** Keyboard shortcut hints displayed below the ask-user options. */
|
|
96
|
+
export type AskUserHintsProps = PrimitiveProps<"div">;
|
|
97
|
+
export declare const AskUserHints: ({ className, render, style, ...elementProps }: AskUserHintsProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
98
|
+
/** Dismiss/skip button. Stateless — wire `onClick` to your dismiss handler. */
|
|
99
|
+
export type AskUserDismissProps = PrimitiveProps<"button">;
|
|
100
|
+
export declare const AskUserDismiss: ({ className, render, style, ...elementProps }: AskUserDismissProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
101
|
+
/** Continue/submit button — `type=submit` so the enclosing form drives it. */
|
|
102
|
+
export type AskUserContinueProps = PrimitiveProps<"button">;
|
|
103
|
+
export declare const AskUserContinue: ({ className, render, style, ...elementProps }: AskUserContinueProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
104
|
+
export {};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
// Headless ask-user questionnaire parts. Owns the option registration system
|
|
4
|
+
// (cmdk pattern), highlight state, and the imperative keyboard-navigation
|
|
5
|
+
// handle. Input widgets (checkbox/radio), icons, and the answered summary
|
|
6
|
+
// belong to the styled layer, which reads state via useAskUserOptions() /
|
|
7
|
+
// useAskUserOption(). Every part supports the Base UI render prop.
|
|
8
|
+
import { createContext, use, useCallback, useId, useImperativeHandle, useLayoutEffect, useRef, useState, } from "react";
|
|
9
|
+
import { useComposer } from "../composer/store.js";
|
|
10
|
+
import { useRenderElement } from "../internal/render/useRenderElement.js";
|
|
11
|
+
const AskUserIdsContext = createContext({ labelId: "", stepLabelId: "" });
|
|
12
|
+
export const AskUserRoot = ({ className, render, style, ...elementProps }) => {
|
|
13
|
+
const labelId = useId();
|
|
14
|
+
const stepLabelId = useId();
|
|
15
|
+
const element = useRenderElement("div", { className, render, style }, { props: [{ "data-ask-user": "" }, elementProps] });
|
|
16
|
+
return _jsx(AskUserIdsContext, { value: { labelId, stepLabelId }, children: element });
|
|
17
|
+
};
|
|
18
|
+
export const AskUserLabel = ({ className, render, style, ...elementProps }) => {
|
|
19
|
+
const { labelId } = use(AskUserIdsContext);
|
|
20
|
+
return useRenderElement("p", { className, render, style }, { props: [{ id: labelId || undefined, "data-ask-user-label": "" }, elementProps] });
|
|
21
|
+
};
|
|
22
|
+
export const AskUserHeader = ({ className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, { props: [{ "data-ask-user-header": "" }, elementProps] });
|
|
23
|
+
export const AskUserNavigation = ({ className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, { props: [{ "data-ask-user-navigation": "" }, elementProps] });
|
|
24
|
+
export const AskUserPrevious = ({ className, render, style, ...elementProps }) => useRenderElement("button", { className, render, style }, { props: [{ "aria-label": "Previous question", "data-ask-user-previous": "" }, elementProps] });
|
|
25
|
+
export const AskUserNext = ({ className, render, style, ...elementProps }) => useRenderElement("button", { className, render, style }, { props: [{ "aria-label": "Next question", "data-ask-user-next": "" }, elementProps] });
|
|
26
|
+
export const AskUserStepLabel = ({ children, className, render, style, ...elementProps }) => {
|
|
27
|
+
const current = useComposer((composer) => composer.askUser.step) + 1;
|
|
28
|
+
const total = useComposer((composer) => composer.askUser.questions?.length ?? 0);
|
|
29
|
+
const { stepLabelId } = use(AskUserIdsContext);
|
|
30
|
+
const content = typeof children === "function" ? children({ current, total }) : children;
|
|
31
|
+
return useRenderElement("span", { className, render, style }, {
|
|
32
|
+
props: [
|
|
33
|
+
{ id: stepLabelId || undefined, "data-ask-user-step-label": "", children: content },
|
|
34
|
+
elementProps,
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
const OptionsContext = createContext({
|
|
39
|
+
multiSelect: false,
|
|
40
|
+
groupName: "",
|
|
41
|
+
highlightedValue: null,
|
|
42
|
+
items: { current: [] },
|
|
43
|
+
register: () => () => { },
|
|
44
|
+
onItemHover: () => { },
|
|
45
|
+
});
|
|
46
|
+
export const useAskUserOptions = () => use(OptionsContext);
|
|
47
|
+
export const AskUserOptions = ({ multiSelect = false, groupName = "", ref, className, render, style, children, ...elementProps }) => {
|
|
48
|
+
const registeredItems = useRef([]);
|
|
49
|
+
const itemElements = useRef(new Map());
|
|
50
|
+
const containerRef = useRef(null);
|
|
51
|
+
const { labelId, stepLabelId } = use(AskUserIdsContext);
|
|
52
|
+
const [highlightedValue, setHighlightedValue] = useState(null);
|
|
53
|
+
// Track highlight in a ref so imperative methods see latest value without re-binding
|
|
54
|
+
const highlightedValueRef = useRef(highlightedValue);
|
|
55
|
+
highlightedValueRef.current = highlightedValue;
|
|
56
|
+
const register = useCallback((itemValue, getElement) => {
|
|
57
|
+
registeredItems.current = [...registeredItems.current, itemValue];
|
|
58
|
+
itemElements.current.set(itemValue, getElement);
|
|
59
|
+
// Auto-highlight whenever an item registers while nothing is highlighted.
|
|
60
|
+
// Using length === 1 here is fragile: during a step transition, React can
|
|
61
|
+
// interleave old-item cleanups with new-item setups, so the new first
|
|
62
|
+
// option may arrive when the list is not exactly length 1.
|
|
63
|
+
if (highlightedValueRef.current === null) {
|
|
64
|
+
highlightedValueRef.current = itemValue;
|
|
65
|
+
setHighlightedValue(itemValue);
|
|
66
|
+
}
|
|
67
|
+
return () => {
|
|
68
|
+
registeredItems.current = registeredItems.current.filter((v) => v !== itemValue);
|
|
69
|
+
itemElements.current.delete(itemValue);
|
|
70
|
+
// Clear highlight when the highlighted item deregisters — allows auto-highlight
|
|
71
|
+
// to fire for the next set of items (e.g. on step change)
|
|
72
|
+
if (highlightedValueRef.current === itemValue) {
|
|
73
|
+
highlightedValueRef.current = null;
|
|
74
|
+
setHighlightedValue(null);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}, []);
|
|
78
|
+
const onItemHover = useCallback((itemValue) => {
|
|
79
|
+
setHighlightedValue(itemValue);
|
|
80
|
+
}, []);
|
|
81
|
+
// Roving tabindex: DOM focus follows the highlight for keyboard navigation.
|
|
82
|
+
const focusValue = useCallback((itemValue) => {
|
|
83
|
+
if (itemValue === null)
|
|
84
|
+
return;
|
|
85
|
+
itemElements.current.get(itemValue)?.()?.focus();
|
|
86
|
+
}, []);
|
|
87
|
+
useImperativeHandle(ref, () => ({
|
|
88
|
+
navigate: (direction) => {
|
|
89
|
+
const items = registeredItems.current;
|
|
90
|
+
if (items.length === 0)
|
|
91
|
+
return null;
|
|
92
|
+
const previous = highlightedValueRef.current;
|
|
93
|
+
let next;
|
|
94
|
+
if (previous === null) {
|
|
95
|
+
next = (direction > 0 ? items[0] : items[items.length - 1]) ?? null;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const currentIndex = items.indexOf(previous);
|
|
99
|
+
const nextIndex = currentIndex + direction;
|
|
100
|
+
if (nextIndex >= items.length || nextIndex < 0)
|
|
101
|
+
next = null;
|
|
102
|
+
else
|
|
103
|
+
next = items[nextIndex] ?? null;
|
|
104
|
+
}
|
|
105
|
+
highlightedValueRef.current = next;
|
|
106
|
+
setHighlightedValue(next);
|
|
107
|
+
focusValue(next);
|
|
108
|
+
return next;
|
|
109
|
+
},
|
|
110
|
+
select: () => {
|
|
111
|
+
const current = highlightedValueRef.current;
|
|
112
|
+
if (current === null)
|
|
113
|
+
return null;
|
|
114
|
+
return { value: current };
|
|
115
|
+
},
|
|
116
|
+
clearHighlight: () => {
|
|
117
|
+
highlightedValueRef.current = null;
|
|
118
|
+
setHighlightedValue(null);
|
|
119
|
+
},
|
|
120
|
+
resetHighlight: () => {
|
|
121
|
+
// Clear synchronously — the auto-highlight path in `register` will pick
|
|
122
|
+
// up the first new item when it mounts after a step change.
|
|
123
|
+
highlightedValueRef.current = null;
|
|
124
|
+
setHighlightedValue(null);
|
|
125
|
+
},
|
|
126
|
+
focusHighlighted: () => {
|
|
127
|
+
const target = highlightedValueRef.current ?? registeredItems.current[0] ?? null;
|
|
128
|
+
if (target !== null && highlightedValueRef.current === null) {
|
|
129
|
+
highlightedValueRef.current = target;
|
|
130
|
+
setHighlightedValue(target);
|
|
131
|
+
}
|
|
132
|
+
focusValue(target);
|
|
133
|
+
},
|
|
134
|
+
getElement: () => containerRef.current,
|
|
135
|
+
get highlightedValue() {
|
|
136
|
+
return highlightedValueRef.current;
|
|
137
|
+
},
|
|
138
|
+
}), [focusValue]);
|
|
139
|
+
const element = useRenderElement("fieldset", { className, render, style }, {
|
|
140
|
+
ref: containerRef,
|
|
141
|
+
props: [
|
|
142
|
+
{
|
|
143
|
+
// The options are the real selectable controls (role radio/checkbox
|
|
144
|
+
// on each option), so the container is their labelled group.
|
|
145
|
+
role: multiSelect ? "group" : "radiogroup",
|
|
146
|
+
"aria-labelledby": labelId || undefined,
|
|
147
|
+
"aria-describedby": stepLabelId || undefined,
|
|
148
|
+
"data-ask-user-options": "",
|
|
149
|
+
children,
|
|
150
|
+
},
|
|
151
|
+
elementProps,
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
return (_jsx(OptionsContext, { value: {
|
|
155
|
+
multiSelect,
|
|
156
|
+
groupName,
|
|
157
|
+
highlightedValue,
|
|
158
|
+
items: registeredItems,
|
|
159
|
+
register,
|
|
160
|
+
onItemHover,
|
|
161
|
+
}, children: element }));
|
|
162
|
+
};
|
|
163
|
+
const OptionContext = createContext({
|
|
164
|
+
id: "",
|
|
165
|
+
value: "",
|
|
166
|
+
selected: false,
|
|
167
|
+
onSelect: () => { },
|
|
168
|
+
});
|
|
169
|
+
export const useAskUserOption = () => use(OptionContext);
|
|
170
|
+
export const AskUserOption = ({ value = "", selected = false, onSelect, className, render, style, children, ...elementProps }) => {
|
|
171
|
+
const id = useId();
|
|
172
|
+
const { multiSelect, highlightedValue, register, onItemHover } = use(OptionsContext);
|
|
173
|
+
const optionRef = useRef(null);
|
|
174
|
+
const isHighlighted = value === highlightedValue;
|
|
175
|
+
// Self-register on mount, deregister on unmount (true subscription side effect)
|
|
176
|
+
const registerRef = useRef(register);
|
|
177
|
+
registerRef.current = register;
|
|
178
|
+
useItemRegistration(value, registerRef, optionRef);
|
|
179
|
+
const element = useRenderElement("label", { className, render, style }, {
|
|
180
|
+
ref: optionRef,
|
|
181
|
+
state: { highlighted: isHighlighted, selected },
|
|
182
|
+
props: [
|
|
183
|
+
{
|
|
184
|
+
// The option IS the selectable control: role + checked state live
|
|
185
|
+
// here, and the roving tabindex makes the highlighted option the
|
|
186
|
+
// group's single tab stop. Never a native input — the composer's
|
|
187
|
+
// question-mode key scoping treats inputs as foreign editables.
|
|
188
|
+
id,
|
|
189
|
+
role: multiSelect ? "checkbox" : "radio",
|
|
190
|
+
"aria-checked": selected,
|
|
191
|
+
tabIndex: isHighlighted ? 0 : -1,
|
|
192
|
+
"data-ask-user-option": "",
|
|
193
|
+
onMouseMove: () => onItemHover(value),
|
|
194
|
+
// Keep highlight and DOM focus unified when focus arrives by other
|
|
195
|
+
// means (Tab into the group, SR virtual-cursor activation).
|
|
196
|
+
onFocus: () => onItemHover(value),
|
|
197
|
+
// Self-contained click selection. The option itself is the control
|
|
198
|
+
// now, so only a *different* nested interactive element defers —
|
|
199
|
+
// a bare option stays fully clickable.
|
|
200
|
+
onClick: (event) => {
|
|
201
|
+
const nested = event.target.closest("input,button,[role=checkbox],[role=radio]");
|
|
202
|
+
if (nested && nested !== event.currentTarget)
|
|
203
|
+
return;
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
onSelect?.();
|
|
206
|
+
},
|
|
207
|
+
children,
|
|
208
|
+
},
|
|
209
|
+
elementProps,
|
|
210
|
+
],
|
|
211
|
+
});
|
|
212
|
+
return _jsx(OptionContext, { value: { id, value, selected, onSelect }, children: element });
|
|
213
|
+
};
|
|
214
|
+
/** Registers an item value + element with the parent Options container synchronously before paint and deregisters on unmount. */
|
|
215
|
+
const useItemRegistration = (value, registerRef, elementRef) => {
|
|
216
|
+
// useLayoutEffect ensures items are registered before paint so the initial highlight resolves immediately
|
|
217
|
+
useLayoutEffect(() => {
|
|
218
|
+
return registerRef.current(value, () => elementRef.current);
|
|
219
|
+
}, [value, registerRef, elementRef]);
|
|
220
|
+
};
|
|
221
|
+
export const AskUserOptionContent = ({ className, render, style, ...elementProps }) => useRenderElement("span", { className, render, style }, { props: [{ "data-ask-user-option-content": "" }, elementProps] });
|
|
222
|
+
export const AskUserOptionLabel = ({ className, render, style, ...elementProps }) => useRenderElement("span", { className, render, style }, { props: [{ "data-ask-user-option-label": "" }, elementProps] });
|
|
223
|
+
export const AskUserOptionDescription = ({ className, render, style, ...elementProps }) => useRenderElement("span", { className, render, style }, { props: [{ "data-ask-user-option-description": "" }, elementProps] });
|
|
224
|
+
export const AskUserHints = ({ className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, { props: [{ "data-ask-user-hints": "" }, elementProps] });
|
|
225
|
+
export const AskUserDismiss = ({ className, render, style, ...elementProps }) => useRenderElement("button", { className, render, style }, { props: [{ type: "button", "data-ask-user-dismiss": "" }, elementProps] });
|
|
226
|
+
export const AskUserContinue = ({ className, render, style, ...elementProps }) => useRenderElement("button", { className, render, style }, { props: [{ type: "submit", "data-ask-user-continue": "" }, elementProps] });
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { AskUserContinueProps, AskUserDismissProps, AskUserHeaderProps, AskUserHintsProps, AskUserLabelProps, AskUserNavigationProps, AskUserNextProps, AskUserOptionContentProps, AskUserOptionDescriptionProps, AskUserOptionLabelProps, AskUserOptionProps, AskUserOptionState, AskUserOptionsHandle, AskUserOptionsProps, AskUserPreviousProps, AskUserRootProps, AskUserStepLabelProps, AskUserStepLabelState, } from "./ask-user.js";
|
|
2
|
+
export { useAskUserOption, useAskUserOptions } from "./ask-user.js";
|
|
3
|
+
export * as AskUser from "./index.parts.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AskUserContinue as Continue, AskUserDismiss as Dismiss, AskUserHeader as Header, AskUserHints as Hints, AskUserLabel as Label, AskUserNavigation as Navigation, AskUserNext as Next, AskUserOption as Option, AskUserOptionContent as OptionContent, AskUserOptionDescription as OptionDescription, AskUserOptionLabel as OptionLabel, AskUserOptions as Options, AskUserPrevious as Previous, AskUserRoot as Root, AskUserStepLabel as StepLabel, } from "./ask-user.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Part names for the `AskUser` namespace. Deliberately carries no
|
|
2
|
+
// "use client" directive: this module and index.ts must stay server-resolvable
|
|
3
|
+
// so a React Server Component can reach AskUser.Root and its siblings
|
|
4
|
+
// through them. The directive lives one level down, on ask-user.tsx.
|
|
5
|
+
export { AskUserContinue as Continue, AskUserDismiss as Dismiss, AskUserHeader as Header, AskUserHints as Hints, AskUserLabel as Label, AskUserNavigation as Navigation, AskUserNext as Next, AskUserOption as Option, AskUserOptionContent as OptionContent, AskUserOptionDescription as OptionDescription, AskUserOptionLabel as OptionLabel, AskUserOptions as Options, AskUserPrevious as Previous, AskUserRoot as Root, AskUserStepLabel as StepLabel, } from "./ask-user.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { PrimitiveProps } from "../internal/primitive-props.js";
|
|
3
|
+
export type AttachmentItem = {
|
|
4
|
+
id: string;
|
|
5
|
+
url: string;
|
|
6
|
+
filename?: string;
|
|
7
|
+
mediaType?: string;
|
|
8
|
+
fileSize?: number;
|
|
9
|
+
};
|
|
10
|
+
export type AttachmentErrorCode = "accept" | "max_file_size" | "max_files";
|
|
11
|
+
export declare const toAttachmentItem: (file: File) => AttachmentItem;
|
|
12
|
+
export declare const revokeAttachmentUrl: (item: Pick<AttachmentItem, "url">) => void;
|
|
13
|
+
export declare const revokeAllAttachmentUrls: (items: Pick<AttachmentItem, "url">[]) => void;
|
|
14
|
+
export declare const matchesAccept: (file: File, accept: string) => boolean;
|
|
15
|
+
export type AttachmentsRootProps = PrimitiveProps<"div">;
|
|
16
|
+
export declare const AttachmentsRoot: ({ className, render, style, ...elementProps }: AttachmentsRootProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
17
|
+
export type AttachmentsItemProps = PrimitiveProps<"div">;
|
|
18
|
+
export declare const AttachmentsItem: ({ className, render, style, ...elementProps }: AttachmentsItemProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
19
|
+
export type AttachmentsRemoveProps = PrimitiveProps<"button"> & {
|
|
20
|
+
onRemove: () => void;
|
|
21
|
+
/** Names the control per item ("Remove report.pdf") — without it, N remove buttons announce identically. */
|
|
22
|
+
filename?: string;
|
|
23
|
+
};
|
|
24
|
+
export declare const AttachmentsRemove: ({ onRemove, filename, className, render, style, ...elementProps }: AttachmentsRemoveProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
25
|
+
export type AttachmentsDropzoneState = {
|
|
26
|
+
/** Present as data-visible while files are dragged over the scope. */
|
|
27
|
+
visible: boolean;
|
|
28
|
+
};
|
|
29
|
+
export type AttachmentsDropzoneProps = PrimitiveProps<"div", AttachmentsDropzoneState> & {
|
|
30
|
+
visible?: boolean;
|
|
31
|
+
/** Keep the dropzone mounted (hidden) when not visible. */
|
|
32
|
+
keepMounted?: boolean;
|
|
33
|
+
/** CSS selector to portal the dropzone into (e.g. an app shell region). */
|
|
34
|
+
portalSelector?: string;
|
|
35
|
+
children?: ReactNode;
|
|
36
|
+
};
|
|
37
|
+
export declare const AttachmentsDropzone: ({ visible, keepMounted, portalSelector, children, className, render, style, ...elementProps }: AttachmentsDropzoneProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | null;
|
|
38
|
+
export type AttachmentsErrorProps = PrimitiveProps<"span">;
|
|
39
|
+
export declare const AttachmentsError: ({ className, render, style, ...elementProps }: AttachmentsErrorProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
40
|
+
export type AttachmentsTriggerProps = PrimitiveProps<"button">;
|
|
41
|
+
export declare const AttachmentsTrigger: ({ className, render, style, ...elementProps }: AttachmentsTriggerProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// Headless attachments: generic structural parts, accept matching, and the
|
|
3
|
+
// platform-default ingestion (blob URL + generated id — the browser-native way
|
|
4
|
+
// to reference a picked File; override via Composer.Attachments' create/destroy
|
|
5
|
+
// for upload-to-storage etc.). Policy (accepted types, counts, sizes), media
|
|
6
|
+
// taxonomy, send serialization, and copy are the consumer's. Card visuals
|
|
7
|
+
// belong to the styled layer.
|
|
8
|
+
import { nanoid } from "nanoid";
|
|
9
|
+
import { useEffect, useState } from "react";
|
|
10
|
+
import { createPortal } from "react-dom";
|
|
11
|
+
import { useRenderElement } from "../internal/render/useRenderElement.js";
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Default ingestion — platform-native, no policy: a picked File becomes an
|
|
14
|
+
// item referencing it by blob URL. Overridable per composer (create/destroy).
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
export const toAttachmentItem = (file) => ({
|
|
17
|
+
id: nanoid(),
|
|
18
|
+
url: URL.createObjectURL(file),
|
|
19
|
+
filename: file.name,
|
|
20
|
+
mediaType: file.type,
|
|
21
|
+
fileSize: file.size,
|
|
22
|
+
});
|
|
23
|
+
export const revokeAttachmentUrl = (item) => {
|
|
24
|
+
if (item.url?.startsWith("blob:")) {
|
|
25
|
+
URL.revokeObjectURL(item.url);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
export const revokeAllAttachmentUrls = (items) => {
|
|
29
|
+
for (const item of items) {
|
|
30
|
+
revokeAttachmentUrl(item);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Accept matching — standard HTML `accept` semantics.
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
export const matchesAccept = (file, accept) => {
|
|
37
|
+
if (!accept || accept.trim() === "") {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
const patterns = accept
|
|
41
|
+
.split(",")
|
|
42
|
+
.map((pattern) => pattern.trim())
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
return patterns.some((pattern) => {
|
|
45
|
+
if (pattern.endsWith("/*")) {
|
|
46
|
+
const prefix = pattern.slice(0, -1);
|
|
47
|
+
return file.type.startsWith(prefix);
|
|
48
|
+
}
|
|
49
|
+
return file.type === pattern;
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
export const AttachmentsRoot = ({ className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, { props: [{ "data-attachments": "" }, elementProps] });
|
|
53
|
+
// Generic structural slot for one attachment. It carries no media taxonomy —
|
|
54
|
+
// categorization (image/pdf/file, icons, thumbnails) belongs to the styled
|
|
55
|
+
// layer, which reads the item's raw mediaType however it wants.
|
|
56
|
+
export const AttachmentsItem = ({ className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, { props: [{ "data-attachments-item": "" }, elementProps] });
|
|
57
|
+
export const AttachmentsRemove = ({ onRemove, filename, className, render, style, ...elementProps }) => useRenderElement("button", { className, render, style }, {
|
|
58
|
+
props: [
|
|
59
|
+
{
|
|
60
|
+
"aria-label": filename ? `Remove ${filename}` : "Remove attachment",
|
|
61
|
+
"data-attachments-remove": "",
|
|
62
|
+
onClick: onRemove,
|
|
63
|
+
},
|
|
64
|
+
elementProps,
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
export const AttachmentsDropzone = ({ visible = false, keepMounted = false, portalSelector, children, className, render, style, ...elementProps }) => {
|
|
68
|
+
const [portalTarget, setPortalTarget] = useState(null);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!portalSelector)
|
|
71
|
+
return;
|
|
72
|
+
setPortalTarget(document.querySelector(portalSelector));
|
|
73
|
+
}, [portalSelector]);
|
|
74
|
+
const content = useRenderElement("div", { className, render, style }, {
|
|
75
|
+
enabled: visible || keepMounted,
|
|
76
|
+
state: { visible },
|
|
77
|
+
props: [{ "data-attachments-dropzone": "", children }, elementProps],
|
|
78
|
+
});
|
|
79
|
+
if (!content)
|
|
80
|
+
return null;
|
|
81
|
+
if (portalTarget)
|
|
82
|
+
return createPortal(content, portalTarget);
|
|
83
|
+
return content;
|
|
84
|
+
};
|
|
85
|
+
// role="alert" (assertive): a rejected pick/drop is a user-action failure that
|
|
86
|
+
// should announce immediately. The copy inside stays the consumer's.
|
|
87
|
+
export const AttachmentsError = ({ className, render, style, ...elementProps }) => useRenderElement("span", { className, render, style }, { props: [{ role: "alert", "data-attachments-error": "" }, elementProps] });
|
|
88
|
+
export const AttachmentsTrigger = ({ className, render, style, ...elementProps }) => useRenderElement("button", { className, render, style }, {
|
|
89
|
+
// Default overridable name — the trigger is typically icon-only.
|
|
90
|
+
props: [{ "aria-label": "Add attachment", "data-attachments-trigger": "" }, elementProps],
|
|
91
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { AttachmentErrorCode, AttachmentItem, AttachmentsDropzoneProps, AttachmentsDropzoneState, AttachmentsErrorProps, AttachmentsItemProps, AttachmentsRemoveProps, AttachmentsRootProps, AttachmentsTriggerProps, } from "./attachments.js";
|
|
2
|
+
export { matchesAccept, revokeAllAttachmentUrls, revokeAttachmentUrl, toAttachmentItem, } from "./attachments.js";
|
|
3
|
+
export * as Attachments from "./index.parts.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AttachmentsDropzone as Dropzone, AttachmentsError as Error, AttachmentsItem as Item, AttachmentsRemove as Remove, AttachmentsRoot as Root, AttachmentsTrigger as Trigger, } from "./attachments.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Part names for the `Attachments` namespace. Deliberately carries no
|
|
2
|
+
// "use client" directive: this module and index.ts must stay server-resolvable
|
|
3
|
+
// so a React Server Component can reach Attachments.Root and its siblings
|
|
4
|
+
// through them. The directive lives one level down, on attachments.tsx.
|
|
5
|
+
export { AttachmentsDropzone as Dropzone, AttachmentsError as Error, AttachmentsItem as Item, AttachmentsRemove as Remove, AttachmentsRoot as Root, AttachmentsTrigger as Trigger, } from "./attachments.js";
|