@birdycom/omniaui-react 0.11.0 → 0.11.2
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 -21
- package/README.md +21 -21
- package/package.json +71 -71
- package/src/blocks/BlockAboutTimeline.tsx +46 -46
- package/src/blocks/BlockAppHeader.tsx +52 -52
- package/src/blocks/BlockAppShell.tsx +56 -56
- package/src/blocks/BlockAuth.tsx +53 -53
- package/src/blocks/BlockAuthSplit.tsx +84 -84
- package/src/blocks/BlockContactGrid.tsx +71 -71
- package/src/blocks/BlockCta.tsx +23 -23
- package/src/blocks/BlockDataTable.tsx +57 -57
- package/src/blocks/BlockFaq.tsx +55 -55
- package/src/blocks/BlockFeatures.tsx +38 -38
- package/src/blocks/BlockFooter.tsx +50 -50
- package/src/blocks/BlockFormPage.tsx +26 -26
- package/src/blocks/BlockHero.tsx +27 -27
- package/src/blocks/BlockHeroCode.tsx +63 -63
- package/src/blocks/BlockHeroOrbit.tsx +73 -73
- package/src/blocks/BlockHeroSplit.tsx +28 -28
- package/src/blocks/BlockModuleHeader.tsx +34 -34
- package/src/blocks/BlockPageHero.tsx +25 -25
- package/src/blocks/BlockPortfolioGrid.tsx +50 -50
- package/src/blocks/BlockPricing.tsx +62 -62
- package/src/blocks/BlockProcess.tsx +50 -50
- package/src/blocks/BlockProductGrid.tsx +54 -54
- package/src/blocks/BlockProfileHeader.tsx +57 -57
- package/src/blocks/BlockShowcase.tsx +58 -58
- package/src/blocks/BlockSiteFooter.tsx +60 -60
- package/src/blocks/BlockSiteHeader.tsx +42 -42
- package/src/blocks/BlockSpotlight.tsx +58 -58
- package/src/blocks/BlockStats.tsx +26 -26
- package/src/blocks/BlockStatsCounter.tsx +31 -31
- package/src/blocks/BlockTeamList.tsx +48 -48
- package/src/blocks/BlockTwoFactor.tsx +57 -57
- package/src/blocks/BlockWizardDialog.tsx +101 -101
- package/src/components/Accordion.tsx +31 -31
- package/src/components/AccordionAccent.tsx +39 -39
- package/src/components/AffixInput.tsx +47 -47
- package/src/components/Alert.tsx +24 -24
- package/src/components/AlertDialog.tsx +60 -60
- package/src/components/AlphaTabs.tsx +63 -63
- package/src/components/Attachment.tsx +140 -140
- package/src/components/Avatar.tsx +32 -32
- package/src/components/BackLink.tsx +11 -11
- package/src/components/Badge.tsx +14 -14
- package/src/components/BarcodeInput.tsx +64 -64
- package/src/components/Breadcrumb.tsx +32 -32
- package/src/components/BrowserFrame.tsx +48 -48
- package/src/components/BusyOverlay.tsx +52 -52
- package/src/components/Button.tsx +49 -49
- package/src/components/Card.tsx +27 -27
- package/src/components/ChartLine.tsx +119 -119
- package/src/components/ChatComposer.tsx +73 -73
- package/src/components/Checkbox.tsx +14 -14
- package/src/components/Checklist.tsx +45 -45
- package/src/components/ChipFilter.tsx +60 -60
- package/src/components/Collapsible.tsx +43 -43
- package/src/components/ColorField.tsx +48 -48
- package/src/components/ColorSelect.tsx +49 -49
- package/src/components/ColumnMenu.tsx +66 -66
- package/src/components/Command.tsx +113 -113
- package/src/components/ConfirmDialog.tsx +95 -95
- package/src/components/CookieConsent.tsx +137 -137
- package/src/components/CopyField.tsx +44 -44
- package/src/components/Counter.tsx +21 -21
- package/src/components/DataTable.tsx +201 -201
- package/src/components/DecimalInput.tsx +87 -87
- package/src/components/DefinitionList.tsx +38 -38
- package/src/components/DocViewer.tsx +110 -110
- package/src/components/Dropdown.tsx +52 -52
- package/src/components/Dropzone.tsx +46 -46
- package/src/components/EmojiPicker.tsx +72 -72
- package/src/components/EmptyState.tsx +27 -27
- package/src/components/EntityCard.tsx +56 -56
- package/src/components/FactList.tsx +43 -43
- package/src/components/FilterCounts.tsx +50 -50
- package/src/components/FlipCard.tsx +55 -55
- package/src/components/FormField.tsx +54 -54
- package/src/components/Greeting.tsx +63 -63
- package/src/components/Heading.tsx +27 -27
- package/src/components/Icon.tsx +25 -25
- package/src/components/ImageZoom.tsx +51 -51
- package/src/components/Input.tsx +21 -21
- package/src/components/InputGroup.tsx +24 -24
- package/src/components/LanguageSelect.tsx +64 -64
- package/src/components/LauncherCard.tsx +50 -50
- package/src/components/Lightbox.tsx +83 -83
- package/src/components/LinkArrow.tsx +27 -27
- package/src/components/ListToolbar.tsx +67 -67
- package/src/components/Marquee.tsx +38 -38
- package/src/components/MediaCard.tsx +47 -47
- package/src/components/MediaRow.tsx +53 -53
- package/src/components/MediaThumb.tsx +49 -49
- package/src/components/Message.tsx +171 -171
- package/src/components/MessageThread.tsx +177 -177
- package/src/components/Modal.tsx +81 -81
- package/src/components/Navmenu.tsx +62 -62
- package/src/components/NextTeaser.tsx +29 -29
- package/src/components/NumberInput.tsx +64 -64
- package/src/components/OtpInput.tsx +48 -48
- package/src/components/PageHeader.tsx +43 -43
- package/src/components/Pagination.tsx +42 -42
- package/src/components/PasswordInput.tsx +36 -36
- package/src/components/PasswordRequirements.tsx +46 -46
- package/src/components/PhoneInput.tsx +93 -93
- package/src/components/Popover.tsx +34 -34
- package/src/components/PriceCard.tsx +83 -83
- package/src/components/ProductMark.tsx +35 -35
- package/src/components/Progress.tsx +32 -32
- package/src/components/ProjectCard.tsx +46 -46
- package/src/components/Prose.tsx +79 -79
- package/src/components/RecoveryCodes.tsx +65 -65
- package/src/components/Segmented.tsx +47 -47
- package/src/components/Select.tsx +27 -27
- package/src/components/SeverityCard.tsx +53 -53
- package/src/components/Sheet.tsx +62 -62
- package/src/components/Skeleton.tsx +12 -12
- package/src/components/SortableRow.tsx +46 -46
- package/src/components/StackCard.tsx +40 -40
- package/src/components/StackList.tsx +42 -42
- package/src/components/StatCard.tsx +49 -49
- package/src/components/Stepper.tsx +54 -54
- package/src/components/Switch.tsx +18 -18
- package/src/components/Table.tsx +75 -75
- package/src/components/TableActions.tsx +54 -54
- package/src/components/TableCompact.tsx +61 -61
- package/src/components/Tabs.tsx +45 -45
- package/src/components/TagInput.tsx +73 -73
- package/src/components/TerminalLog.tsx +63 -63
- package/src/components/Textarea.tsx +19 -19
- package/src/components/ThemeSwitch.tsx +45 -45
- package/src/components/TimeInput.tsx +76 -76
- package/src/components/Timeline.tsx +45 -45
- package/src/components/Toast.tsx +138 -138
- package/src/components/ToggleField.tsx +39 -39
- package/src/components/Tooltip.tsx +14 -14
- package/src/components/TopProgress.tsx +54 -54
- package/src/components/Tree.tsx +97 -97
- package/src/components/UpgradeBanner.tsx +62 -62
- package/src/components/User.tsx +31 -31
- package/src/components/VersionBanner.tsx +41 -41
- package/src/hooks.tsx +141 -141
- package/src/utils.ts +25 -25
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
// File: packages/react/src/components/MediaRow.tsx
|
|
2
|
-
// Map: MediaRowList (ul / ol, numbered) -> MediaRow (MediaThumb -> title link + meta -> menu button + popover)
|
|
3
|
-
// Same markup as packages/ui/snippets/media-row.html.
|
|
4
|
-
import { forwardRef, useId } from "react";
|
|
5
|
-
import type { HTMLAttributes, ReactNode } from "react";
|
|
6
|
-
import { attrs, cx } from "../utils";
|
|
7
|
-
import { MediaThumb } from "./MediaThumb";
|
|
8
|
-
import type { MediaThumbProps } from "./MediaThumb";
|
|
9
|
-
|
|
10
|
-
export interface MediaRowListProps extends HTMLAttributes<HTMLElement> {
|
|
11
|
-
/** Numbered playlist: renders an <ol> with a counter column. */
|
|
12
|
-
numbered?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** `ul.oc-media-row-list` or `ol.oc-media-row-list--numbered`. */
|
|
16
|
-
export const MediaRowList = forwardRef<HTMLElement, MediaRowListProps>(function MediaRowList({ numbered, className, ...rest }, ref) {
|
|
17
|
-
const Tag = (numbered ? "ol" : "ul") as "ul";
|
|
18
|
-
return <Tag ref={ref as React.Ref<HTMLUListElement>} className={cx("oc-media-row-list", numbered && "oc-media-row-list--numbered", className)} {...(rest as HTMLAttributes<HTMLUListElement>)} />;
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
export interface MediaRowProps extends Omit<HTMLAttributes<HTMLLIElement>, "title"> {
|
|
22
|
-
title: ReactNode;
|
|
23
|
-
href: string;
|
|
24
|
-
meta?: ReactNode;
|
|
25
|
-
thumb?: MediaThumbProps;
|
|
26
|
-
/** Now playing (`aria-current="true"`). */
|
|
27
|
-
current?: boolean;
|
|
28
|
-
/** Items of the ⋮ menu. No menu button when empty. */
|
|
29
|
-
menu?: ReactNode;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** `li.oc-media-row`: thumb | title + meta | menu. */
|
|
33
|
-
export const MediaRow = forwardRef<HTMLLIElement, MediaRowProps>(function MediaRow(
|
|
34
|
-
{ title, href, meta, thumb, current, menu, className, ...rest },
|
|
35
|
-
ref,
|
|
36
|
-
) {
|
|
37
|
-
const menuId = `oc-media-row-menu-${useId().replace(/:/g, "")}`;
|
|
38
|
-
return (
|
|
39
|
-
<li ref={ref} className={cx("oc-media-row", className)} aria-current={current ? "true" : undefined} {...rest}>
|
|
40
|
-
<MediaThumb {...thumb} />
|
|
41
|
-
<div className="oc-media-row__text">
|
|
42
|
-
<h3 className="oc-media-row__title"><a href={href}>{title}</a></h3>
|
|
43
|
-
{meta != null && <p className="oc-media-row__meta">{meta}</p>}
|
|
44
|
-
</div>
|
|
45
|
-
{menu != null && (
|
|
46
|
-
<>
|
|
47
|
-
<button type="button" className="oc-media-row__menu" aria-label="More actions" {...attrs({ popovertarget: menuId })}>⋮</button>
|
|
48
|
-
<div className="oc-dropdown-menu" id={menuId} {...attrs({ popover: "" })}>{menu}</div>
|
|
49
|
-
</>
|
|
50
|
-
)}
|
|
51
|
-
</li>
|
|
52
|
-
);
|
|
53
|
-
});
|
|
1
|
+
// File: packages/react/src/components/MediaRow.tsx
|
|
2
|
+
// Map: MediaRowList (ul / ol, numbered) -> MediaRow (MediaThumb -> title link + meta -> menu button + popover)
|
|
3
|
+
// Same markup as packages/ui/snippets/media-row.html.
|
|
4
|
+
import { forwardRef, useId } from "react";
|
|
5
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
6
|
+
import { attrs, cx } from "../utils";
|
|
7
|
+
import { MediaThumb } from "./MediaThumb";
|
|
8
|
+
import type { MediaThumbProps } from "./MediaThumb";
|
|
9
|
+
|
|
10
|
+
export interface MediaRowListProps extends HTMLAttributes<HTMLElement> {
|
|
11
|
+
/** Numbered playlist: renders an <ol> with a counter column. */
|
|
12
|
+
numbered?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** `ul.oc-media-row-list` or `ol.oc-media-row-list--numbered`. */
|
|
16
|
+
export const MediaRowList = forwardRef<HTMLElement, MediaRowListProps>(function MediaRowList({ numbered, className, ...rest }, ref) {
|
|
17
|
+
const Tag = (numbered ? "ol" : "ul") as "ul";
|
|
18
|
+
return <Tag ref={ref as React.Ref<HTMLUListElement>} className={cx("oc-media-row-list", numbered && "oc-media-row-list--numbered", className)} {...(rest as HTMLAttributes<HTMLUListElement>)} />;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export interface MediaRowProps extends Omit<HTMLAttributes<HTMLLIElement>, "title"> {
|
|
22
|
+
title: ReactNode;
|
|
23
|
+
href: string;
|
|
24
|
+
meta?: ReactNode;
|
|
25
|
+
thumb?: MediaThumbProps;
|
|
26
|
+
/** Now playing (`aria-current="true"`). */
|
|
27
|
+
current?: boolean;
|
|
28
|
+
/** Items of the ⋮ menu. No menu button when empty. */
|
|
29
|
+
menu?: ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `li.oc-media-row`: thumb | title + meta | menu. */
|
|
33
|
+
export const MediaRow = forwardRef<HTMLLIElement, MediaRowProps>(function MediaRow(
|
|
34
|
+
{ title, href, meta, thumb, current, menu, className, ...rest },
|
|
35
|
+
ref,
|
|
36
|
+
) {
|
|
37
|
+
const menuId = `oc-media-row-menu-${useId().replace(/:/g, "")}`;
|
|
38
|
+
return (
|
|
39
|
+
<li ref={ref} className={cx("oc-media-row", className)} aria-current={current ? "true" : undefined} {...rest}>
|
|
40
|
+
<MediaThumb {...thumb} />
|
|
41
|
+
<div className="oc-media-row__text">
|
|
42
|
+
<h3 className="oc-media-row__title"><a href={href}>{title}</a></h3>
|
|
43
|
+
{meta != null && <p className="oc-media-row__meta">{meta}</p>}
|
|
44
|
+
</div>
|
|
45
|
+
{menu != null && (
|
|
46
|
+
<>
|
|
47
|
+
<button type="button" className="oc-media-row__menu" aria-label="More actions" {...attrs({ popovertarget: menuId })}>⋮</button>
|
|
48
|
+
<div className="oc-dropdown-menu" id={menuId} {...attrs({ popover: "" })}>{menu}</div>
|
|
49
|
+
</>
|
|
50
|
+
)}
|
|
51
|
+
</li>
|
|
52
|
+
);
|
|
53
|
+
});
|
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
// File: packages/react/src/components/MediaThumb.tsx
|
|
2
|
-
// Map: props -> MediaThumb (<a> or <div> -> picture -> badge -> duration -> progress)
|
|
3
|
-
// Same markup as packages/ui/snippets/media-thumb.html.
|
|
4
|
-
import { forwardRef } from "react";
|
|
5
|
-
import type { HTMLAttributes, ReactNode } from "react";
|
|
6
|
-
import { cx } from "../utils";
|
|
7
|
-
|
|
8
|
-
export interface MediaThumbProps extends HTMLAttributes<HTMLElement> {
|
|
9
|
-
/** Picture URL. Empty = placeholder art (`.oc-media-art`, pick a variant with `art`). */
|
|
10
|
-
src?: string;
|
|
11
|
-
alt?: string;
|
|
12
|
-
/** Placeholder art variant 1-6 (only without `src`). */
|
|
13
|
-
art?: number;
|
|
14
|
-
/** Renders an <a> when set. */
|
|
15
|
-
href?: string;
|
|
16
|
-
/** "12:04" in the bottom-right corner. */
|
|
17
|
-
duration?: ReactNode;
|
|
18
|
-
/** Top-left label ("New", "LIVE"). */
|
|
19
|
-
badge?: ReactNode;
|
|
20
|
-
live?: boolean;
|
|
21
|
-
/** Watched part, 0-100. Shows the progress bar when set. */
|
|
22
|
-
progress?: number;
|
|
23
|
-
watched?: boolean;
|
|
24
|
-
ratio?: "video" | "square" | "portrait";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** `.oc-media-thumb`: 16:9 video thumbnail. */
|
|
28
|
-
export const MediaThumb = forwardRef<HTMLElement, MediaThumbProps>(function MediaThumb(
|
|
29
|
-
{ src, alt = "", art, href, duration, badge, live, progress, watched, ratio = "video", className, children, ...rest },
|
|
30
|
-
ref,
|
|
31
|
-
) {
|
|
32
|
-
const Tag = (href ? "a" : "div") as "a";
|
|
33
|
-
return (
|
|
34
|
-
<Tag
|
|
35
|
-
ref={ref as React.Ref<HTMLAnchorElement>}
|
|
36
|
-
href={href}
|
|
37
|
-
className={cx("oc-media-thumb", ratio !== "video" && `oc-media-thumb--${ratio}`, watched && "is-watched", className)}
|
|
38
|
-
{...(rest as HTMLAttributes<HTMLAnchorElement>)}
|
|
39
|
-
>
|
|
40
|
-
{src ? <img src={src} alt={alt} /> : <div className={cx("oc-media-art", art != null && art > 1 && `oc-media-art--${art}`)} />}
|
|
41
|
-
{badge != null && <span className={cx("oc-media-thumb__badge", live && "oc-media-thumb__badge--live")}>{badge}</span>}
|
|
42
|
-
{duration != null && <span className="oc-media-thumb__duration">{duration}</span>}
|
|
43
|
-
{progress != null && (
|
|
44
|
-
<progress className="oc-media-thumb__progress" value={progress} max={100} aria-label={`Watched ${progress}%`}>{progress}%</progress>
|
|
45
|
-
)}
|
|
46
|
-
{children}
|
|
47
|
-
</Tag>
|
|
48
|
-
);
|
|
49
|
-
});
|
|
1
|
+
// File: packages/react/src/components/MediaThumb.tsx
|
|
2
|
+
// Map: props -> MediaThumb (<a> or <div> -> picture -> badge -> duration -> progress)
|
|
3
|
+
// Same markup as packages/ui/snippets/media-thumb.html.
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
6
|
+
import { cx } from "../utils";
|
|
7
|
+
|
|
8
|
+
export interface MediaThumbProps extends HTMLAttributes<HTMLElement> {
|
|
9
|
+
/** Picture URL. Empty = placeholder art (`.oc-media-art`, pick a variant with `art`). */
|
|
10
|
+
src?: string;
|
|
11
|
+
alt?: string;
|
|
12
|
+
/** Placeholder art variant 1-6 (only without `src`). */
|
|
13
|
+
art?: number;
|
|
14
|
+
/** Renders an <a> when set. */
|
|
15
|
+
href?: string;
|
|
16
|
+
/** "12:04" in the bottom-right corner. */
|
|
17
|
+
duration?: ReactNode;
|
|
18
|
+
/** Top-left label ("New", "LIVE"). */
|
|
19
|
+
badge?: ReactNode;
|
|
20
|
+
live?: boolean;
|
|
21
|
+
/** Watched part, 0-100. Shows the progress bar when set. */
|
|
22
|
+
progress?: number;
|
|
23
|
+
watched?: boolean;
|
|
24
|
+
ratio?: "video" | "square" | "portrait";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `.oc-media-thumb`: 16:9 video thumbnail. */
|
|
28
|
+
export const MediaThumb = forwardRef<HTMLElement, MediaThumbProps>(function MediaThumb(
|
|
29
|
+
{ src, alt = "", art, href, duration, badge, live, progress, watched, ratio = "video", className, children, ...rest },
|
|
30
|
+
ref,
|
|
31
|
+
) {
|
|
32
|
+
const Tag = (href ? "a" : "div") as "a";
|
|
33
|
+
return (
|
|
34
|
+
<Tag
|
|
35
|
+
ref={ref as React.Ref<HTMLAnchorElement>}
|
|
36
|
+
href={href}
|
|
37
|
+
className={cx("oc-media-thumb", ratio !== "video" && `oc-media-thumb--${ratio}`, watched && "is-watched", className)}
|
|
38
|
+
{...(rest as HTMLAttributes<HTMLAnchorElement>)}
|
|
39
|
+
>
|
|
40
|
+
{src ? <img src={src} alt={alt} /> : <div className={cx("oc-media-art", art != null && art > 1 && `oc-media-art--${art}`)} />}
|
|
41
|
+
{badge != null && <span className={cx("oc-media-thumb__badge", live && "oc-media-thumb__badge--live")}>{badge}</span>}
|
|
42
|
+
{duration != null && <span className="oc-media-thumb__duration">{duration}</span>}
|
|
43
|
+
{progress != null && (
|
|
44
|
+
<progress className="oc-media-thumb__progress" value={progress} max={100} aria-label={`Watched ${progress}%`}>{progress}%</progress>
|
|
45
|
+
)}
|
|
46
|
+
{children}
|
|
47
|
+
</Tag>
|
|
48
|
+
);
|
|
49
|
+
});
|
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
// File: packages/react/src/components/Message.tsx
|
|
2
|
-
// Map: types + icons -> MessageAttachments -> Message (avatar, bubble: sender, quote, text, attachments, meta; actions)
|
|
3
|
-
// -> MessageDivider -> MessageList (demo conversation)
|
|
4
|
-
// Same markup as packages/ui/snippets/message.html. No JavaScript: grouping (--continued) and hover actions are CSS.
|
|
5
|
-
import { forwardRef } from "react";
|
|
6
|
-
import type { HTMLAttributes, ReactNode } from "react";
|
|
7
|
-
import { cx } from "../utils";
|
|
8
|
-
import { Attachment, AttachmentImage, AttachmentThumb } from "./Attachment";
|
|
9
|
-
import type { AttachmentFile } from "./Attachment";
|
|
10
|
-
|
|
11
|
-
/** File chip (default), image preview or square thumbnail inside a message. */
|
|
12
|
-
export type MessageAttachment =
|
|
13
|
-
| (AttachmentFile & { kind?: "file" })
|
|
14
|
-
| { kind: "image"; href?: string; src?: string; alt: string; art?: number; caption?: ReactNode }
|
|
15
|
-
| { kind: "thumb"; src?: string; alt: string; art?: number };
|
|
16
|
-
|
|
17
|
-
export interface MessageAction {
|
|
18
|
-
/** Passed to onAction, e.g. "reply". */
|
|
19
|
-
name: string;
|
|
20
|
-
/** aria-label, e.g. "Reply to this message". */
|
|
21
|
-
label: string;
|
|
22
|
-
/** Built-in "reply" / "delete" icon, or your own svg. */
|
|
23
|
-
icon?: "reply" | "delete" | ReactNode;
|
|
24
|
-
destructive?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface MessageData {
|
|
28
|
-
direction?: "in" | "out";
|
|
29
|
-
sender?: ReactNode;
|
|
30
|
-
/** Initials in the avatar. */
|
|
31
|
-
avatar?: ReactNode;
|
|
32
|
-
time?: ReactNode;
|
|
33
|
-
datetime?: string;
|
|
34
|
-
status?: "sent" | "read" | "failed";
|
|
35
|
-
/** Default "Sent" / "Read" / "Failed". */
|
|
36
|
-
statusLabel?: ReactNode;
|
|
37
|
-
/** Double check icon before the status. */
|
|
38
|
-
statusIcon?: boolean;
|
|
39
|
-
/** Follows a message of the same sender (no name; avatar on the last one). */
|
|
40
|
-
continued?: boolean;
|
|
41
|
-
quote?: { href?: string; sender: ReactNode; text: ReactNode };
|
|
42
|
-
/** A string becomes one paragraph; pass elements for lists, links and more paragraphs. */
|
|
43
|
-
text?: ReactNode;
|
|
44
|
-
attachments?: MessageAttachment[];
|
|
45
|
-
actions?: MessageAction[];
|
|
46
|
-
/** aria-label of the row, e.g. "Lisa Verbeke, 09:12". */
|
|
47
|
-
label?: string;
|
|
48
|
-
id?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const svg = (d: string, width = "2") => (
|
|
52
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={width} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d={d} /></svg>
|
|
53
|
-
);
|
|
54
|
-
const ICONS: Record<string, string> = { reply: "M9 14 4 9l5-5M4 9h10a6 6 0 0 1 6 6v4", delete: "M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6" };
|
|
55
|
-
const STATUS = { sent: "Sent", read: "Read", failed: "Failed" };
|
|
56
|
-
|
|
57
|
-
/** `.oc-message__attachments`: thumbnails share one `.oc-attachment-list`, files and images stand alone. */
|
|
58
|
-
export function MessageAttachments({ items }: { items: MessageAttachment[] }) {
|
|
59
|
-
const thumbs = items.every((a) => a.kind === "thumb");
|
|
60
|
-
return (
|
|
61
|
-
<div className="oc-message__attachments">
|
|
62
|
-
{thumbs ? (
|
|
63
|
-
<div className="oc-attachment-list">{items.map((a, i) => <AttachmentThumb key={i} as="span" {...(a as Extract<MessageAttachment, { kind: "thumb" }>)} />)}</div>
|
|
64
|
-
) : items.map((a, i) => {
|
|
65
|
-
if (a.kind === "image") { const { kind: _k, ...img } = a; return <AttachmentImage key={i} {...img} />; }
|
|
66
|
-
if (a.kind === "thumb") { const { kind: _k, ...t } = a; return <AttachmentThumb key={i} as="span" {...t} />; }
|
|
67
|
-
const { kind: _k, ...file } = a;
|
|
68
|
-
return <Attachment key={i} {...file} />;
|
|
69
|
-
})}
|
|
70
|
-
</div>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface MessageProps extends Omit<HTMLAttributes<HTMLElement>, "id">, MessageData {
|
|
75
|
-
onAction?: (name: string) => void;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** `article.oc-message`: one chat bubble with avatar, meta and hover actions. */
|
|
79
|
-
export const Message = forwardRef<HTMLElement, MessageProps>(function Message(
|
|
80
|
-
{ direction = "in", sender, avatar, time, datetime, status, statusLabel, statusIcon, continued, quote, text, attachments, actions, label, onAction, className, children, ...rest },
|
|
81
|
-
ref,
|
|
82
|
-
) {
|
|
83
|
-
return (
|
|
84
|
-
<article ref={ref} className={cx("oc-message", direction === "out" && "oc-message--out", continued && "oc-message--continued", className)} aria-label={label} {...rest}>
|
|
85
|
-
<span className="oc-avatar oc-message__avatar">{avatar}</span>
|
|
86
|
-
<div className="oc-message__bubble">
|
|
87
|
-
{sender != null && !continued && <p className="oc-message__sender">{sender}</p>}
|
|
88
|
-
{quote && <a className="oc-message__quote" href={quote.href}><strong>{quote.sender}</strong><span>{quote.text}</span></a>}
|
|
89
|
-
{(text != null || children != null) && <div className="oc-message__text">{typeof text === "string" ? <p>{text}</p> : text}{children}</div>}
|
|
90
|
-
{attachments?.length ? <MessageAttachments items={attachments} /> : null}
|
|
91
|
-
{(time != null || status) && (
|
|
92
|
-
<p className="oc-message__meta">
|
|
93
|
-
{time != null && <time dateTime={datetime}>{time}</time>}
|
|
94
|
-
{status && (
|
|
95
|
-
<span className={cx("oc-message__status", status !== "sent" && `oc-message__status--${status}`)}>
|
|
96
|
-
{statusIcon && svg("M2 12.5 7 17.5 18 6.5M13 16.5l1 1L22.5 9", "2.2")}{statusLabel ?? STATUS[status]}
|
|
97
|
-
</span>
|
|
98
|
-
)}
|
|
99
|
-
</p>
|
|
100
|
-
)}
|
|
101
|
-
</div>
|
|
102
|
-
{actions?.length ? (
|
|
103
|
-
<div className="oc-message__actions">
|
|
104
|
-
{actions.map((a) => (
|
|
105
|
-
<button key={a.name} type="button" className={cx("oc-message__action", a.destructive && "oc-message__action--destructive")} aria-label={a.label} onClick={() => onAction?.(a.name)}>
|
|
106
|
-
{typeof a.icon === "string" || a.icon == null ? svg(ICONS[(a.icon as string) ?? a.name] ?? ICONS.reply) : a.icon}
|
|
107
|
-
</button>
|
|
108
|
-
))}
|
|
109
|
-
</div>
|
|
110
|
-
) : null}
|
|
111
|
-
</article>
|
|
112
|
-
);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
export interface MessageDividerProps extends HTMLAttributes<HTMLDivElement> {
|
|
116
|
-
/** Centred notice without lines. */
|
|
117
|
-
system?: boolean;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** `.oc-message-divider`: date divider, or a system notice. */
|
|
121
|
-
export function MessageDivider({ system, className, children, ...rest }: MessageDividerProps) {
|
|
122
|
-
return <div className={cx("oc-message-divider", system && "oc-message-divider--system", className)} role={system ? "note" : undefined} {...rest}><span>{children}</span></div>;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export type MessageListItem = MessageData | { divider: ReactNode; system?: boolean };
|
|
126
|
-
|
|
127
|
-
export interface MessageListProps extends HTMLAttributes<HTMLDivElement> {
|
|
128
|
-
/** Messages and dividers in order; `children` replace them. */
|
|
129
|
-
items?: MessageListItem[];
|
|
130
|
-
/** role="log" for a list that updates live (default true). */
|
|
131
|
-
log?: boolean;
|
|
132
|
-
onAction?: (name: string, message: MessageData, index: number) => void;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const DEMO: MessageListItem[] = [
|
|
136
|
-
{ divider: "Tuesday 14 October" },
|
|
137
|
-
{ sender: "Lisa Verbeke", avatar: "LV", time: "09:12", datetime: "2026-10-14T09:12", label: "Lisa Verbeke, 09:12", text: "Morning Tom! The worktop supplier sent two samples for Villa Dewitte." },
|
|
138
|
-
{
|
|
139
|
-
sender: "Lisa Verbeke", avatar: "LV", time: "09:13", datetime: "2026-10-14T09:13", label: "Lisa Verbeke, 09:13", continued: true, id: "oc-message-demo-question",
|
|
140
|
-
text: (
|
|
141
|
-
<>
|
|
142
|
-
<p>Which finish should we order?</p>
|
|
143
|
-
<ul><li><strong>Calacatta</strong> matt, 20 mm</li><li><strong>Nero Marquina</strong> polished, 12 mm</li></ul>
|
|
144
|
-
<p>Details are on the <a href="#supplier-sheet">supplier sheet</a>.</p>
|
|
145
|
-
</>
|
|
146
|
-
),
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
direction: "out", avatar: "TP", time: "09:20", datetime: "2026-10-14T09:20", label: "You, 09:20", status: "read", statusIcon: true,
|
|
150
|
-
quote: { href: "#oc-message-demo-question", sender: "Lisa Verbeke", text: "Which finish should we order? Calacatta matt, 20 mm or Nero Marquina polished, 12 mm" },
|
|
151
|
-
text: "Calacatta matt. The client wants the lighter look next to the oak fronts.",
|
|
152
|
-
actions: [{ name: "reply", label: "Reply to this message" }, { name: "delete", label: "Delete this message", destructive: true }],
|
|
153
|
-
},
|
|
154
|
-
{ divider: "Lisa Verbeke marked this question as answered", system: true },
|
|
155
|
-
];
|
|
156
|
-
|
|
157
|
-
/** `.oc-message-list`: column of messages and dividers. */
|
|
158
|
-
export const MessageList = forwardRef<HTMLDivElement, MessageListProps>(function MessageList(
|
|
159
|
-
{ items = DEMO, log = true, onAction, className, children, "aria-label": label = log ? "Conversation with Lisa Verbeke" : undefined, ...rest },
|
|
160
|
-
ref,
|
|
161
|
-
) {
|
|
162
|
-
return (
|
|
163
|
-
<div ref={ref} className={cx("oc-message-list", className)} role={log ? "log" : undefined} aria-label={label} {...rest}>
|
|
164
|
-
{children ?? items.map((item, i) =>
|
|
165
|
-
"divider" in item
|
|
166
|
-
? <MessageDivider key={i} system={item.system}>{item.divider}</MessageDivider>
|
|
167
|
-
: <Message key={i} {...item} onAction={(name) => onAction?.(name, item, i)} />,
|
|
168
|
-
)}
|
|
169
|
-
</div>
|
|
170
|
-
);
|
|
171
|
-
});
|
|
1
|
+
// File: packages/react/src/components/Message.tsx
|
|
2
|
+
// Map: types + icons -> MessageAttachments -> Message (avatar, bubble: sender, quote, text, attachments, meta; actions)
|
|
3
|
+
// -> MessageDivider -> MessageList (demo conversation)
|
|
4
|
+
// Same markup as packages/ui/snippets/message.html. No JavaScript: grouping (--continued) and hover actions are CSS.
|
|
5
|
+
import { forwardRef } from "react";
|
|
6
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
7
|
+
import { cx } from "../utils";
|
|
8
|
+
import { Attachment, AttachmentImage, AttachmentThumb } from "./Attachment";
|
|
9
|
+
import type { AttachmentFile } from "./Attachment";
|
|
10
|
+
|
|
11
|
+
/** File chip (default), image preview or square thumbnail inside a message. */
|
|
12
|
+
export type MessageAttachment =
|
|
13
|
+
| (AttachmentFile & { kind?: "file" })
|
|
14
|
+
| { kind: "image"; href?: string; src?: string; alt: string; art?: number; caption?: ReactNode }
|
|
15
|
+
| { kind: "thumb"; src?: string; alt: string; art?: number };
|
|
16
|
+
|
|
17
|
+
export interface MessageAction {
|
|
18
|
+
/** Passed to onAction, e.g. "reply". */
|
|
19
|
+
name: string;
|
|
20
|
+
/** aria-label, e.g. "Reply to this message". */
|
|
21
|
+
label: string;
|
|
22
|
+
/** Built-in "reply" / "delete" icon, or your own svg. */
|
|
23
|
+
icon?: "reply" | "delete" | ReactNode;
|
|
24
|
+
destructive?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MessageData {
|
|
28
|
+
direction?: "in" | "out";
|
|
29
|
+
sender?: ReactNode;
|
|
30
|
+
/** Initials in the avatar. */
|
|
31
|
+
avatar?: ReactNode;
|
|
32
|
+
time?: ReactNode;
|
|
33
|
+
datetime?: string;
|
|
34
|
+
status?: "sent" | "read" | "failed";
|
|
35
|
+
/** Default "Sent" / "Read" / "Failed". */
|
|
36
|
+
statusLabel?: ReactNode;
|
|
37
|
+
/** Double check icon before the status. */
|
|
38
|
+
statusIcon?: boolean;
|
|
39
|
+
/** Follows a message of the same sender (no name; avatar on the last one). */
|
|
40
|
+
continued?: boolean;
|
|
41
|
+
quote?: { href?: string; sender: ReactNode; text: ReactNode };
|
|
42
|
+
/** A string becomes one paragraph; pass elements for lists, links and more paragraphs. */
|
|
43
|
+
text?: ReactNode;
|
|
44
|
+
attachments?: MessageAttachment[];
|
|
45
|
+
actions?: MessageAction[];
|
|
46
|
+
/** aria-label of the row, e.g. "Lisa Verbeke, 09:12". */
|
|
47
|
+
label?: string;
|
|
48
|
+
id?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const svg = (d: string, width = "2") => (
|
|
52
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={width} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d={d} /></svg>
|
|
53
|
+
);
|
|
54
|
+
const ICONS: Record<string, string> = { reply: "M9 14 4 9l5-5M4 9h10a6 6 0 0 1 6 6v4", delete: "M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6" };
|
|
55
|
+
const STATUS = { sent: "Sent", read: "Read", failed: "Failed" };
|
|
56
|
+
|
|
57
|
+
/** `.oc-message__attachments`: thumbnails share one `.oc-attachment-list`, files and images stand alone. */
|
|
58
|
+
export function MessageAttachments({ items }: { items: MessageAttachment[] }) {
|
|
59
|
+
const thumbs = items.every((a) => a.kind === "thumb");
|
|
60
|
+
return (
|
|
61
|
+
<div className="oc-message__attachments">
|
|
62
|
+
{thumbs ? (
|
|
63
|
+
<div className="oc-attachment-list">{items.map((a, i) => <AttachmentThumb key={i} as="span" {...(a as Extract<MessageAttachment, { kind: "thumb" }>)} />)}</div>
|
|
64
|
+
) : items.map((a, i) => {
|
|
65
|
+
if (a.kind === "image") { const { kind: _k, ...img } = a; return <AttachmentImage key={i} {...img} />; }
|
|
66
|
+
if (a.kind === "thumb") { const { kind: _k, ...t } = a; return <AttachmentThumb key={i} as="span" {...t} />; }
|
|
67
|
+
const { kind: _k, ...file } = a;
|
|
68
|
+
return <Attachment key={i} {...file} />;
|
|
69
|
+
})}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface MessageProps extends Omit<HTMLAttributes<HTMLElement>, "id">, MessageData {
|
|
75
|
+
onAction?: (name: string) => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `article.oc-message`: one chat bubble with avatar, meta and hover actions. */
|
|
79
|
+
export const Message = forwardRef<HTMLElement, MessageProps>(function Message(
|
|
80
|
+
{ direction = "in", sender, avatar, time, datetime, status, statusLabel, statusIcon, continued, quote, text, attachments, actions, label, onAction, className, children, ...rest },
|
|
81
|
+
ref,
|
|
82
|
+
) {
|
|
83
|
+
return (
|
|
84
|
+
<article ref={ref} className={cx("oc-message", direction === "out" && "oc-message--out", continued && "oc-message--continued", className)} aria-label={label} {...rest}>
|
|
85
|
+
<span className="oc-avatar oc-message__avatar">{avatar}</span>
|
|
86
|
+
<div className="oc-message__bubble">
|
|
87
|
+
{sender != null && !continued && <p className="oc-message__sender">{sender}</p>}
|
|
88
|
+
{quote && <a className="oc-message__quote" href={quote.href}><strong>{quote.sender}</strong><span>{quote.text}</span></a>}
|
|
89
|
+
{(text != null || children != null) && <div className="oc-message__text">{typeof text === "string" ? <p>{text}</p> : text}{children}</div>}
|
|
90
|
+
{attachments?.length ? <MessageAttachments items={attachments} /> : null}
|
|
91
|
+
{(time != null || status) && (
|
|
92
|
+
<p className="oc-message__meta">
|
|
93
|
+
{time != null && <time dateTime={datetime}>{time}</time>}
|
|
94
|
+
{status && (
|
|
95
|
+
<span className={cx("oc-message__status", status !== "sent" && `oc-message__status--${status}`)}>
|
|
96
|
+
{statusIcon && svg("M2 12.5 7 17.5 18 6.5M13 16.5l1 1L22.5 9", "2.2")}{statusLabel ?? STATUS[status]}
|
|
97
|
+
</span>
|
|
98
|
+
)}
|
|
99
|
+
</p>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
{actions?.length ? (
|
|
103
|
+
<div className="oc-message__actions">
|
|
104
|
+
{actions.map((a) => (
|
|
105
|
+
<button key={a.name} type="button" className={cx("oc-message__action", a.destructive && "oc-message__action--destructive")} aria-label={a.label} onClick={() => onAction?.(a.name)}>
|
|
106
|
+
{typeof a.icon === "string" || a.icon == null ? svg(ICONS[(a.icon as string) ?? a.name] ?? ICONS.reply) : a.icon}
|
|
107
|
+
</button>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
) : null}
|
|
111
|
+
</article>
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export interface MessageDividerProps extends HTMLAttributes<HTMLDivElement> {
|
|
116
|
+
/** Centred notice without lines. */
|
|
117
|
+
system?: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** `.oc-message-divider`: date divider, or a system notice. */
|
|
121
|
+
export function MessageDivider({ system, className, children, ...rest }: MessageDividerProps) {
|
|
122
|
+
return <div className={cx("oc-message-divider", system && "oc-message-divider--system", className)} role={system ? "note" : undefined} {...rest}><span>{children}</span></div>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type MessageListItem = MessageData | { divider: ReactNode; system?: boolean };
|
|
126
|
+
|
|
127
|
+
export interface MessageListProps extends HTMLAttributes<HTMLDivElement> {
|
|
128
|
+
/** Messages and dividers in order; `children` replace them. */
|
|
129
|
+
items?: MessageListItem[];
|
|
130
|
+
/** role="log" for a list that updates live (default true). */
|
|
131
|
+
log?: boolean;
|
|
132
|
+
onAction?: (name: string, message: MessageData, index: number) => void;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const DEMO: MessageListItem[] = [
|
|
136
|
+
{ divider: "Tuesday 14 October" },
|
|
137
|
+
{ sender: "Lisa Verbeke", avatar: "LV", time: "09:12", datetime: "2026-10-14T09:12", label: "Lisa Verbeke, 09:12", text: "Morning Tom! The worktop supplier sent two samples for Villa Dewitte." },
|
|
138
|
+
{
|
|
139
|
+
sender: "Lisa Verbeke", avatar: "LV", time: "09:13", datetime: "2026-10-14T09:13", label: "Lisa Verbeke, 09:13", continued: true, id: "oc-message-demo-question",
|
|
140
|
+
text: (
|
|
141
|
+
<>
|
|
142
|
+
<p>Which finish should we order?</p>
|
|
143
|
+
<ul><li><strong>Calacatta</strong> matt, 20 mm</li><li><strong>Nero Marquina</strong> polished, 12 mm</li></ul>
|
|
144
|
+
<p>Details are on the <a href="#supplier-sheet">supplier sheet</a>.</p>
|
|
145
|
+
</>
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
direction: "out", avatar: "TP", time: "09:20", datetime: "2026-10-14T09:20", label: "You, 09:20", status: "read", statusIcon: true,
|
|
150
|
+
quote: { href: "#oc-message-demo-question", sender: "Lisa Verbeke", text: "Which finish should we order? Calacatta matt, 20 mm or Nero Marquina polished, 12 mm" },
|
|
151
|
+
text: "Calacatta matt. The client wants the lighter look next to the oak fronts.",
|
|
152
|
+
actions: [{ name: "reply", label: "Reply to this message" }, { name: "delete", label: "Delete this message", destructive: true }],
|
|
153
|
+
},
|
|
154
|
+
{ divider: "Lisa Verbeke marked this question as answered", system: true },
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
/** `.oc-message-list`: column of messages and dividers. */
|
|
158
|
+
export const MessageList = forwardRef<HTMLDivElement, MessageListProps>(function MessageList(
|
|
159
|
+
{ items = DEMO, log = true, onAction, className, children, "aria-label": label = log ? "Conversation with Lisa Verbeke" : undefined, ...rest },
|
|
160
|
+
ref,
|
|
161
|
+
) {
|
|
162
|
+
return (
|
|
163
|
+
<div ref={ref} className={cx("oc-message-list", className)} role={log ? "log" : undefined} aria-label={label} {...rest}>
|
|
164
|
+
{children ?? items.map((item, i) =>
|
|
165
|
+
"divider" in item
|
|
166
|
+
? <MessageDivider key={i} system={item.system}>{item.divider}</MessageDivider>
|
|
167
|
+
: <Message key={i} {...item} onAction={(name) => onAction?.(name, item, i)} />,
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
});
|