@birdycom/omniaui-react 0.11.0 → 0.11.1
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,63 +1,63 @@
|
|
|
1
|
-
// File: packages/react/src/components/AlphaTabs.tsx
|
|
2
|
-
// Map: AlphaTabs = nav -> "All" item -> one item per letter (current / disabled)
|
|
3
|
-
// With `href` the items are links (server filtering); without it they are buttons that call onChange.
|
|
4
|
-
"use client";
|
|
5
|
-
|
|
6
|
-
import { forwardRef } from "react";
|
|
7
|
-
import type { HTMLAttributes } from "react";
|
|
8
|
-
import { cx } from "../utils";
|
|
9
|
-
import { useControllable } from "../hooks";
|
|
10
|
-
|
|
11
|
-
const AZ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
|
12
|
-
|
|
13
|
-
export interface AlphaTabsProps extends Omit<HTMLAttributes<HTMLElement>, "onChange"> {
|
|
14
|
-
/** Letters to show (default A-Z). */
|
|
15
|
-
letters?: string[];
|
|
16
|
-
/** Letters that have entries; the others are disabled. Default: all letters. */
|
|
17
|
-
available?: string[];
|
|
18
|
-
/** Current letter, or "all". */
|
|
19
|
-
value?: string;
|
|
20
|
-
defaultValue?: string;
|
|
21
|
-
onChange?: (letter: string) => void;
|
|
22
|
-
/** Build a link per letter, e.g. `(l) => "?letter=" + l`. */
|
|
23
|
-
href?: (letter: string) => string;
|
|
24
|
-
allLabel?: string;
|
|
25
|
-
wrap?: boolean;
|
|
26
|
-
size?: "sm";
|
|
27
|
-
plain?: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** `nav.oc-alpha-tabs`: All + A-Z letter filter. */
|
|
31
|
-
export const AlphaTabs = forwardRef<HTMLElement, AlphaTabsProps>(function AlphaTabs(
|
|
32
|
-
{ letters = AZ, available, value, defaultValue = "all", onChange, href, allLabel = "All", wrap, size, plain, className, ...rest },
|
|
33
|
-
ref,
|
|
34
|
-
) {
|
|
35
|
-
const [current, setCurrent] = useControllable(value, defaultValue, onChange);
|
|
36
|
-
const cls = cx("oc-alpha-tabs", wrap && "oc-alpha-tabs--wrap", size && `oc-alpha-tabs--${size}`, plain && "oc-alpha-tabs--plain", className);
|
|
37
|
-
|
|
38
|
-
const item = (key: string, label: string, extra?: string) => {
|
|
39
|
-
const isCurrent = current === key;
|
|
40
|
-
const disabled = key !== "all" && available !== undefined && !available.includes(key);
|
|
41
|
-
const itemCls = cx("oc-alpha-tabs-item", extra);
|
|
42
|
-
if (href) {
|
|
43
|
-
// A disabled link has no href, so it cannot be followed.
|
|
44
|
-
return (
|
|
45
|
-
<a key={key} className={itemCls} href={disabled ? undefined : href(key)} aria-current={isCurrent ? "true" : undefined} aria-disabled={disabled ? "true" : undefined}>
|
|
46
|
-
{label}
|
|
47
|
-
</a>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
return (
|
|
51
|
-
<button key={key} type="button" className={itemCls} aria-current={isCurrent ? "true" : undefined} disabled={disabled} onClick={() => setCurrent(key)}>
|
|
52
|
-
{label}
|
|
53
|
-
</button>
|
|
54
|
-
);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<nav ref={ref} className={cls} aria-label="Filter by letter" {...rest}>
|
|
59
|
-
{item("all", allLabel, "oc-alpha-tabs-item--all")}
|
|
60
|
-
{letters.map((l) => item(l, l))}
|
|
61
|
-
</nav>
|
|
62
|
-
);
|
|
63
|
-
});
|
|
1
|
+
// File: packages/react/src/components/AlphaTabs.tsx
|
|
2
|
+
// Map: AlphaTabs = nav -> "All" item -> one item per letter (current / disabled)
|
|
3
|
+
// With `href` the items are links (server filtering); without it they are buttons that call onChange.
|
|
4
|
+
"use client";
|
|
5
|
+
|
|
6
|
+
import { forwardRef } from "react";
|
|
7
|
+
import type { HTMLAttributes } from "react";
|
|
8
|
+
import { cx } from "../utils";
|
|
9
|
+
import { useControllable } from "../hooks";
|
|
10
|
+
|
|
11
|
+
const AZ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
|
12
|
+
|
|
13
|
+
export interface AlphaTabsProps extends Omit<HTMLAttributes<HTMLElement>, "onChange"> {
|
|
14
|
+
/** Letters to show (default A-Z). */
|
|
15
|
+
letters?: string[];
|
|
16
|
+
/** Letters that have entries; the others are disabled. Default: all letters. */
|
|
17
|
+
available?: string[];
|
|
18
|
+
/** Current letter, or "all". */
|
|
19
|
+
value?: string;
|
|
20
|
+
defaultValue?: string;
|
|
21
|
+
onChange?: (letter: string) => void;
|
|
22
|
+
/** Build a link per letter, e.g. `(l) => "?letter=" + l`. */
|
|
23
|
+
href?: (letter: string) => string;
|
|
24
|
+
allLabel?: string;
|
|
25
|
+
wrap?: boolean;
|
|
26
|
+
size?: "sm";
|
|
27
|
+
plain?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `nav.oc-alpha-tabs`: All + A-Z letter filter. */
|
|
31
|
+
export const AlphaTabs = forwardRef<HTMLElement, AlphaTabsProps>(function AlphaTabs(
|
|
32
|
+
{ letters = AZ, available, value, defaultValue = "all", onChange, href, allLabel = "All", wrap, size, plain, className, ...rest },
|
|
33
|
+
ref,
|
|
34
|
+
) {
|
|
35
|
+
const [current, setCurrent] = useControllable(value, defaultValue, onChange);
|
|
36
|
+
const cls = cx("oc-alpha-tabs", wrap && "oc-alpha-tabs--wrap", size && `oc-alpha-tabs--${size}`, plain && "oc-alpha-tabs--plain", className);
|
|
37
|
+
|
|
38
|
+
const item = (key: string, label: string, extra?: string) => {
|
|
39
|
+
const isCurrent = current === key;
|
|
40
|
+
const disabled = key !== "all" && available !== undefined && !available.includes(key);
|
|
41
|
+
const itemCls = cx("oc-alpha-tabs-item", extra);
|
|
42
|
+
if (href) {
|
|
43
|
+
// A disabled link has no href, so it cannot be followed.
|
|
44
|
+
return (
|
|
45
|
+
<a key={key} className={itemCls} href={disabled ? undefined : href(key)} aria-current={isCurrent ? "true" : undefined} aria-disabled={disabled ? "true" : undefined}>
|
|
46
|
+
{label}
|
|
47
|
+
</a>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return (
|
|
51
|
+
<button key={key} type="button" className={itemCls} aria-current={isCurrent ? "true" : undefined} disabled={disabled} onClick={() => setCurrent(key)}>
|
|
52
|
+
{label}
|
|
53
|
+
</button>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<nav ref={ref} className={cls} aria-label="Filter by letter" {...rest}>
|
|
59
|
+
{item("all", allLabel, "oc-alpha-tabs-item--all")}
|
|
60
|
+
{letters.map((l) => item(l, l))}
|
|
61
|
+
</nav>
|
|
62
|
+
);
|
|
63
|
+
});
|
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
// File: packages/react/src/components/Attachment.tsx
|
|
2
|
-
// Map: types + type / extension helpers -> Attachment (file chip) -> AttachmentImage -> AttachmentThumb -> AttachmentList (demo files)
|
|
3
|
-
// Same markup as packages/ui/snippets/attachment.html. No JavaScript: onRemove is yours, the upload progress is a custom property.
|
|
4
|
-
import { forwardRef } from "react";
|
|
5
|
-
import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
|
|
6
|
-
import { cx } from "../utils";
|
|
7
|
-
|
|
8
|
-
export type AttachmentType = "pdf" | "doc" | "sheet" | "zip" | "image";
|
|
9
|
-
|
|
10
|
-
export interface AttachmentFile {
|
|
11
|
-
name: string;
|
|
12
|
-
/** "1.2 MB", or the error text when `state` is "error". */
|
|
13
|
-
size?: ReactNode;
|
|
14
|
-
/** Icon tint. Derived from the extension when empty. */
|
|
15
|
-
type?: AttachmentType;
|
|
16
|
-
/** Text in the icon tile (default: the extension, e.g. "PDF", "XLS"). */
|
|
17
|
-
ext?: string;
|
|
18
|
-
/** Download link (the chip becomes an `<a download>` unless it is removable). */
|
|
19
|
-
href?: string;
|
|
20
|
-
/** Small preview image in the icon tile. */
|
|
21
|
-
image?: string;
|
|
22
|
-
state?: "uploading" | "error";
|
|
23
|
-
/** Upload progress in % (shown while uploading). */
|
|
24
|
-
progress?: number;
|
|
25
|
-
/** Show the remove button (composer). */
|
|
26
|
-
removable?: boolean;
|
|
27
|
-
removeLabel?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const TYPES: Record<string, AttachmentType> = {
|
|
31
|
-
pdf: "pdf", doc: "doc", docx: "doc", odt: "doc", rtf: "doc", txt: "doc", xls: "sheet", xlsx: "sheet", csv: "sheet", ods: "sheet",
|
|
32
|
-
zip: "zip", rar: "zip", "7z": "zip", gz: "zip", jpg: "image", jpeg: "image", png: "image", gif: "image", webp: "image", heic: "image", avif: "image",
|
|
33
|
-
};
|
|
34
|
-
const extOf = (name: string) => (name.includes(".") ? name.split(".").pop()!.toLowerCase() : "");
|
|
35
|
-
/** Icon text and tint of a file name (`Plan.xlsx` -> XLS, sheet). */
|
|
36
|
-
export const attachmentMeta = (f: AttachmentFile) => ({
|
|
37
|
-
ext: f.ext ?? extOf(f.name).toUpperCase().replace(/^(DOC|XLS|PPT)X$/, "$1"),
|
|
38
|
-
type: f.type ?? TYPES[extOf(f.name)],
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
export interface AttachmentProps extends Omit<HTMLAttributes<HTMLElement>, "type">, AttachmentFile {
|
|
42
|
-
onRemove?: () => void;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** `.oc-attachment` file chip: icon tile, name, size and an optional remove button. */
|
|
46
|
-
export const Attachment = forwardRef<HTMLElement, AttachmentProps>(function Attachment(
|
|
47
|
-
{ name, size, type, ext, href, image, state, progress = 0, removable, removeLabel, onRemove, className, style, ...rest },
|
|
48
|
-
ref,
|
|
49
|
-
) {
|
|
50
|
-
const meta = attachmentMeta({ name, type, ext });
|
|
51
|
-
const uploading = state === "uploading";
|
|
52
|
-
const props = {
|
|
53
|
-
className: cx("oc-attachment", meta.type && `oc-attachment--${meta.type}`, state && `oc-attachment--${state}`, className),
|
|
54
|
-
style: uploading ? ({ "--oc-attachment-progress": `${progress}%`, ...style } as CSSProperties) : style,
|
|
55
|
-
...rest,
|
|
56
|
-
};
|
|
57
|
-
const inner = (
|
|
58
|
-
<>
|
|
59
|
-
<span className="oc-attachment__icon" aria-hidden="true">{image ? <img src={image} alt="" /> : meta.ext}</span>
|
|
60
|
-
<span className="oc-attachment__body">
|
|
61
|
-
<span className="oc-attachment__name">{name}</span>
|
|
62
|
-
{uploading ? <span className="oc-attachment__size" role="status">{size ?? `Uploading… ${progress}%`}</span> : size != null && <span className="oc-attachment__size">{size}</span>}
|
|
63
|
-
</span>
|
|
64
|
-
{removable && (
|
|
65
|
-
<button type="button" className="oc-attachment__remove" aria-label={removeLabel ?? (uploading ? `Cancel upload of ${name}` : `Remove ${name}`)} onClick={onRemove}>✕</button>
|
|
66
|
-
)}
|
|
67
|
-
</>
|
|
68
|
-
);
|
|
69
|
-
return href && !removable
|
|
70
|
-
? <a ref={ref as React.Ref<HTMLAnchorElement>} href={href} download {...props}>{inner}</a>
|
|
71
|
-
: <div ref={ref as React.Ref<HTMLDivElement>} {...props}>{inner}</div>;
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
export interface AttachmentImageProps extends HTMLAttributes<HTMLAnchorElement> {
|
|
75
|
-
href?: string;
|
|
76
|
-
/** Image URL. Leave empty to show placeholder art. */
|
|
77
|
-
src?: string;
|
|
78
|
-
alt: string;
|
|
79
|
-
art?: number;
|
|
80
|
-
/** e.g. "worktop-sample.jpg · 2.1 MB". */
|
|
81
|
-
caption?: ReactNode;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const art = (n?: number) => cx("oc-media-art", n != null && `oc-media-art--${n}`);
|
|
85
|
-
|
|
86
|
-
/** `a.oc-attachment-image`: 4:3 image preview inside a message. */
|
|
87
|
-
export function AttachmentImage({ href, src, alt, art: n, caption, className, ...rest }: AttachmentImageProps) {
|
|
88
|
-
return (
|
|
89
|
-
<a className={cx("oc-attachment-image", className)} href={href} {...rest}>
|
|
90
|
-
{src ? <img src={src} alt={alt} /> : <span className={art(n)} role="img" aria-label={alt} />}
|
|
91
|
-
{caption != null && <span className="oc-attachment-image__caption">{caption}</span>}
|
|
92
|
-
</a>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface AttachmentThumbProps extends HTMLAttributes<HTMLElement> {
|
|
97
|
-
src?: string;
|
|
98
|
-
alt: string;
|
|
99
|
-
art?: number;
|
|
100
|
-
/** Remove button label, e.g. "Remove screenshot.png". No label = no button. */
|
|
101
|
-
removeLabel?: string;
|
|
102
|
-
onRemove?: () => void;
|
|
103
|
-
as?: "div" | "span";
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** `.oc-attachment-thumb`: square thumbnail of a pasted screenshot. */
|
|
107
|
-
export function AttachmentThumb({ src, alt, art: n, removeLabel, onRemove, as: Tag = "div", className, ...rest }: AttachmentThumbProps) {
|
|
108
|
-
return (
|
|
109
|
-
<Tag className={cx("oc-attachment-thumb", className)} {...rest}>
|
|
110
|
-
{src ? <img src={src} alt={alt} /> : <span className={art(n)} role="img" aria-label={alt} />}
|
|
111
|
-
{removeLabel && <button type="button" className="oc-attachment__remove" aria-label={removeLabel} onClick={onRemove}>✕</button>}
|
|
112
|
-
</Tag>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export interface AttachmentListProps extends HTMLAttributes<HTMLElement> {
|
|
117
|
-
/** File chips; `children` replace them. */
|
|
118
|
-
items?: AttachmentFile[];
|
|
119
|
-
/** One per line, full width. */
|
|
120
|
-
stack?: boolean;
|
|
121
|
-
/** `ul` (default, items in `li`) or `div`. */
|
|
122
|
-
as?: "ul" | "div";
|
|
123
|
-
onRemove?: (file: AttachmentFile, index: number) => void;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const DEMO: AttachmentFile[] = [
|
|
127
|
-
{ name: "Quote kitchen Dewitte v2.pdf", size: "1.2 MB", href: "#quote.pdf" },
|
|
128
|
-
{ name: "Measurements ground floor.xlsx", size: "86 KB", href: "#measurements.xlsx" },
|
|
129
|
-
{ name: "Contract addendum.docx", size: "240 KB", href: "#contract.docx" },
|
|
130
|
-
{ name: "Plans architect.zip", size: "14.8 MB", href: "#plans.zip" },
|
|
131
|
-
];
|
|
132
|
-
|
|
133
|
-
/** `.oc-attachment-list`: wrapping row of chips. */
|
|
134
|
-
export function AttachmentList({ items = DEMO, stack, as: Tag = "ul", onRemove, className, children, "aria-label": label = "Files in this message", ...rest }: AttachmentListProps) {
|
|
135
|
-
const chips = children ?? items.map((f, i) => {
|
|
136
|
-
const chip = <Attachment key={i} {...f} onRemove={() => onRemove?.(f, i)} />;
|
|
137
|
-
return Tag === "ul" ? <li key={i}>{chip}</li> : chip;
|
|
138
|
-
});
|
|
139
|
-
return <Tag className={cx("oc-attachment-list", stack && "oc-attachment-list--stack", className)} aria-label={label} {...rest}>{chips}</Tag>;
|
|
140
|
-
}
|
|
1
|
+
// File: packages/react/src/components/Attachment.tsx
|
|
2
|
+
// Map: types + type / extension helpers -> Attachment (file chip) -> AttachmentImage -> AttachmentThumb -> AttachmentList (demo files)
|
|
3
|
+
// Same markup as packages/ui/snippets/attachment.html. No JavaScript: onRemove is yours, the upload progress is a custom property.
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
|
|
6
|
+
import { cx } from "../utils";
|
|
7
|
+
|
|
8
|
+
export type AttachmentType = "pdf" | "doc" | "sheet" | "zip" | "image";
|
|
9
|
+
|
|
10
|
+
export interface AttachmentFile {
|
|
11
|
+
name: string;
|
|
12
|
+
/** "1.2 MB", or the error text when `state` is "error". */
|
|
13
|
+
size?: ReactNode;
|
|
14
|
+
/** Icon tint. Derived from the extension when empty. */
|
|
15
|
+
type?: AttachmentType;
|
|
16
|
+
/** Text in the icon tile (default: the extension, e.g. "PDF", "XLS"). */
|
|
17
|
+
ext?: string;
|
|
18
|
+
/** Download link (the chip becomes an `<a download>` unless it is removable). */
|
|
19
|
+
href?: string;
|
|
20
|
+
/** Small preview image in the icon tile. */
|
|
21
|
+
image?: string;
|
|
22
|
+
state?: "uploading" | "error";
|
|
23
|
+
/** Upload progress in % (shown while uploading). */
|
|
24
|
+
progress?: number;
|
|
25
|
+
/** Show the remove button (composer). */
|
|
26
|
+
removable?: boolean;
|
|
27
|
+
removeLabel?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TYPES: Record<string, AttachmentType> = {
|
|
31
|
+
pdf: "pdf", doc: "doc", docx: "doc", odt: "doc", rtf: "doc", txt: "doc", xls: "sheet", xlsx: "sheet", csv: "sheet", ods: "sheet",
|
|
32
|
+
zip: "zip", rar: "zip", "7z": "zip", gz: "zip", jpg: "image", jpeg: "image", png: "image", gif: "image", webp: "image", heic: "image", avif: "image",
|
|
33
|
+
};
|
|
34
|
+
const extOf = (name: string) => (name.includes(".") ? name.split(".").pop()!.toLowerCase() : "");
|
|
35
|
+
/** Icon text and tint of a file name (`Plan.xlsx` -> XLS, sheet). */
|
|
36
|
+
export const attachmentMeta = (f: AttachmentFile) => ({
|
|
37
|
+
ext: f.ext ?? extOf(f.name).toUpperCase().replace(/^(DOC|XLS|PPT)X$/, "$1"),
|
|
38
|
+
type: f.type ?? TYPES[extOf(f.name)],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export interface AttachmentProps extends Omit<HTMLAttributes<HTMLElement>, "type">, AttachmentFile {
|
|
42
|
+
onRemove?: () => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `.oc-attachment` file chip: icon tile, name, size and an optional remove button. */
|
|
46
|
+
export const Attachment = forwardRef<HTMLElement, AttachmentProps>(function Attachment(
|
|
47
|
+
{ name, size, type, ext, href, image, state, progress = 0, removable, removeLabel, onRemove, className, style, ...rest },
|
|
48
|
+
ref,
|
|
49
|
+
) {
|
|
50
|
+
const meta = attachmentMeta({ name, type, ext });
|
|
51
|
+
const uploading = state === "uploading";
|
|
52
|
+
const props = {
|
|
53
|
+
className: cx("oc-attachment", meta.type && `oc-attachment--${meta.type}`, state && `oc-attachment--${state}`, className),
|
|
54
|
+
style: uploading ? ({ "--oc-attachment-progress": `${progress}%`, ...style } as CSSProperties) : style,
|
|
55
|
+
...rest,
|
|
56
|
+
};
|
|
57
|
+
const inner = (
|
|
58
|
+
<>
|
|
59
|
+
<span className="oc-attachment__icon" aria-hidden="true">{image ? <img src={image} alt="" /> : meta.ext}</span>
|
|
60
|
+
<span className="oc-attachment__body">
|
|
61
|
+
<span className="oc-attachment__name">{name}</span>
|
|
62
|
+
{uploading ? <span className="oc-attachment__size" role="status">{size ?? `Uploading… ${progress}%`}</span> : size != null && <span className="oc-attachment__size">{size}</span>}
|
|
63
|
+
</span>
|
|
64
|
+
{removable && (
|
|
65
|
+
<button type="button" className="oc-attachment__remove" aria-label={removeLabel ?? (uploading ? `Cancel upload of ${name}` : `Remove ${name}`)} onClick={onRemove}>✕</button>
|
|
66
|
+
)}
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
return href && !removable
|
|
70
|
+
? <a ref={ref as React.Ref<HTMLAnchorElement>} href={href} download {...props}>{inner}</a>
|
|
71
|
+
: <div ref={ref as React.Ref<HTMLDivElement>} {...props}>{inner}</div>;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export interface AttachmentImageProps extends HTMLAttributes<HTMLAnchorElement> {
|
|
75
|
+
href?: string;
|
|
76
|
+
/** Image URL. Leave empty to show placeholder art. */
|
|
77
|
+
src?: string;
|
|
78
|
+
alt: string;
|
|
79
|
+
art?: number;
|
|
80
|
+
/** e.g. "worktop-sample.jpg · 2.1 MB". */
|
|
81
|
+
caption?: ReactNode;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const art = (n?: number) => cx("oc-media-art", n != null && `oc-media-art--${n}`);
|
|
85
|
+
|
|
86
|
+
/** `a.oc-attachment-image`: 4:3 image preview inside a message. */
|
|
87
|
+
export function AttachmentImage({ href, src, alt, art: n, caption, className, ...rest }: AttachmentImageProps) {
|
|
88
|
+
return (
|
|
89
|
+
<a className={cx("oc-attachment-image", className)} href={href} {...rest}>
|
|
90
|
+
{src ? <img src={src} alt={alt} /> : <span className={art(n)} role="img" aria-label={alt} />}
|
|
91
|
+
{caption != null && <span className="oc-attachment-image__caption">{caption}</span>}
|
|
92
|
+
</a>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface AttachmentThumbProps extends HTMLAttributes<HTMLElement> {
|
|
97
|
+
src?: string;
|
|
98
|
+
alt: string;
|
|
99
|
+
art?: number;
|
|
100
|
+
/** Remove button label, e.g. "Remove screenshot.png". No label = no button. */
|
|
101
|
+
removeLabel?: string;
|
|
102
|
+
onRemove?: () => void;
|
|
103
|
+
as?: "div" | "span";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `.oc-attachment-thumb`: square thumbnail of a pasted screenshot. */
|
|
107
|
+
export function AttachmentThumb({ src, alt, art: n, removeLabel, onRemove, as: Tag = "div", className, ...rest }: AttachmentThumbProps) {
|
|
108
|
+
return (
|
|
109
|
+
<Tag className={cx("oc-attachment-thumb", className)} {...rest}>
|
|
110
|
+
{src ? <img src={src} alt={alt} /> : <span className={art(n)} role="img" aria-label={alt} />}
|
|
111
|
+
{removeLabel && <button type="button" className="oc-attachment__remove" aria-label={removeLabel} onClick={onRemove}>✕</button>}
|
|
112
|
+
</Tag>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface AttachmentListProps extends HTMLAttributes<HTMLElement> {
|
|
117
|
+
/** File chips; `children` replace them. */
|
|
118
|
+
items?: AttachmentFile[];
|
|
119
|
+
/** One per line, full width. */
|
|
120
|
+
stack?: boolean;
|
|
121
|
+
/** `ul` (default, items in `li`) or `div`. */
|
|
122
|
+
as?: "ul" | "div";
|
|
123
|
+
onRemove?: (file: AttachmentFile, index: number) => void;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const DEMO: AttachmentFile[] = [
|
|
127
|
+
{ name: "Quote kitchen Dewitte v2.pdf", size: "1.2 MB", href: "#quote.pdf" },
|
|
128
|
+
{ name: "Measurements ground floor.xlsx", size: "86 KB", href: "#measurements.xlsx" },
|
|
129
|
+
{ name: "Contract addendum.docx", size: "240 KB", href: "#contract.docx" },
|
|
130
|
+
{ name: "Plans architect.zip", size: "14.8 MB", href: "#plans.zip" },
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
/** `.oc-attachment-list`: wrapping row of chips. */
|
|
134
|
+
export function AttachmentList({ items = DEMO, stack, as: Tag = "ul", onRemove, className, children, "aria-label": label = "Files in this message", ...rest }: AttachmentListProps) {
|
|
135
|
+
const chips = children ?? items.map((f, i) => {
|
|
136
|
+
const chip = <Attachment key={i} {...f} onRemove={() => onRemove?.(f, i)} />;
|
|
137
|
+
return Tag === "ul" ? <li key={i}>{chip}</li> : chip;
|
|
138
|
+
});
|
|
139
|
+
return <Tag className={cx("oc-attachment-list", stack && "oc-attachment-list--stack", className)} aria-label={label} {...rest}>{chips}</Tag>;
|
|
140
|
+
}
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import { forwardRef } from "react";
|
|
2
|
-
import type { HTMLAttributes } from "react";
|
|
3
|
-
import { cx } from "../utils";
|
|
4
|
-
|
|
5
|
-
export type AvatarSize = "sm" | "md" | "lg" | "xl";
|
|
6
|
-
|
|
7
|
-
export interface AvatarProps extends HTMLAttributes<HTMLSpanElement> {
|
|
8
|
-
size?: AvatarSize;
|
|
9
|
-
/** Image URL; when set the avatar renders an <img>. Otherwise `children` (initials) are shown. */
|
|
10
|
-
src?: string;
|
|
11
|
-
alt?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** `<span class="oc-avatar oc-avatar--{size}">SC</span>` */
|
|
15
|
-
export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>(function Avatar(
|
|
16
|
-
{ size = "md", src, alt = "", className, children, ...rest },
|
|
17
|
-
ref,
|
|
18
|
-
) {
|
|
19
|
-
return (
|
|
20
|
-
<span ref={ref} className={cx("oc-avatar", size !== "md" && `oc-avatar--${size}`, className)} {...rest}>
|
|
21
|
-
{src ? <img src={src} alt={alt} /> : children}
|
|
22
|
-
</span>
|
|
23
|
-
);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
/** `<div class="oc-avatar-group">` */
|
|
27
|
-
export const AvatarGroup = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function AvatarGroup(
|
|
28
|
-
{ className, ...rest },
|
|
29
|
-
ref,
|
|
30
|
-
) {
|
|
31
|
-
return <div ref={ref} className={cx("oc-avatar-group", className)} {...rest} />;
|
|
32
|
-
});
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { HTMLAttributes } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export type AvatarSize = "sm" | "md" | "lg" | "xl";
|
|
6
|
+
|
|
7
|
+
export interface AvatarProps extends HTMLAttributes<HTMLSpanElement> {
|
|
8
|
+
size?: AvatarSize;
|
|
9
|
+
/** Image URL; when set the avatar renders an <img>. Otherwise `children` (initials) are shown. */
|
|
10
|
+
src?: string;
|
|
11
|
+
alt?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** `<span class="oc-avatar oc-avatar--{size}">SC</span>` */
|
|
15
|
+
export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>(function Avatar(
|
|
16
|
+
{ size = "md", src, alt = "", className, children, ...rest },
|
|
17
|
+
ref,
|
|
18
|
+
) {
|
|
19
|
+
return (
|
|
20
|
+
<span ref={ref} className={cx("oc-avatar", size !== "md" && `oc-avatar--${size}`, className)} {...rest}>
|
|
21
|
+
{src ? <img src={src} alt={alt} /> : children}
|
|
22
|
+
</span>
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** `<div class="oc-avatar-group">` */
|
|
27
|
+
export const AvatarGroup = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function AvatarGroup(
|
|
28
|
+
{ className, ...rest },
|
|
29
|
+
ref,
|
|
30
|
+
) {
|
|
31
|
+
return <div ref={ref} className={cx("oc-avatar-group", className)} {...rest} />;
|
|
32
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { forwardRef } from "react";
|
|
2
|
-
import type { AnchorHTMLAttributes } from "react";
|
|
3
|
-
import { cx } from "../utils";
|
|
4
|
-
|
|
5
|
-
/** `<a class="oc-back-link">Back to Products</a>` */
|
|
6
|
-
export const BackLink = forwardRef<HTMLAnchorElement, AnchorHTMLAttributes<HTMLAnchorElement>>(function BackLink(
|
|
7
|
-
{ className, ...rest },
|
|
8
|
-
ref,
|
|
9
|
-
) {
|
|
10
|
-
return <a ref={ref} className={cx("oc-back-link", className)} {...rest} />;
|
|
11
|
-
});
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { AnchorHTMLAttributes } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
/** `<a class="oc-back-link">Back to Products</a>` */
|
|
6
|
+
export const BackLink = forwardRef<HTMLAnchorElement, AnchorHTMLAttributes<HTMLAnchorElement>>(function BackLink(
|
|
7
|
+
{ className, ...rest },
|
|
8
|
+
ref,
|
|
9
|
+
) {
|
|
10
|
+
return <a ref={ref} className={cx("oc-back-link", className)} {...rest} />;
|
|
11
|
+
});
|
package/src/components/Badge.tsx
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { forwardRef } from "react";
|
|
2
|
-
import type { HTMLAttributes } from "react";
|
|
3
|
-
import { cx } from "../utils";
|
|
4
|
-
|
|
5
|
-
export type BadgeVariant = "default" | "secondary" | "outline" | "destructive" | "success" | "warning" | "info";
|
|
6
|
-
|
|
7
|
-
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
|
8
|
-
variant?: BadgeVariant;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** `<span class="oc-badge oc-badge--{variant}">` */
|
|
12
|
-
export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge({ variant = "default", className, ...rest }, ref) {
|
|
13
|
-
return <span ref={ref} className={cx("oc-badge", variant !== "default" && `oc-badge--${variant}`, className)} {...rest} />;
|
|
14
|
-
});
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { HTMLAttributes } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export type BadgeVariant = "default" | "secondary" | "outline" | "destructive" | "success" | "warning" | "info";
|
|
6
|
+
|
|
7
|
+
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
|
8
|
+
variant?: BadgeVariant;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** `<span class="oc-badge oc-badge--{variant}">` */
|
|
12
|
+
export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge({ variant = "default", className, ...rest }, ref) {
|
|
13
|
+
return <span ref={ref} className={cx("oc-badge", variant !== "default" && `oc-badge--${variant}`, className)} {...rest} />;
|
|
14
|
+
});
|