@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,50 +1,50 @@
|
|
|
1
|
-
import { forwardRef } from "react";
|
|
2
|
-
import type { AnchorHTMLAttributes, ReactNode } from "react";
|
|
3
|
-
import { cx } from "../utils";
|
|
4
|
-
|
|
5
|
-
export interface LauncherCardProps extends Omit<AnchorHTMLAttributes<HTMLElement>, "title"> {
|
|
6
|
-
/** Tinted icon tile content (inline SVG). */
|
|
7
|
-
icon?: ReactNode;
|
|
8
|
-
title: ReactNode;
|
|
9
|
-
/** Trailing badge inside the title. */
|
|
10
|
-
badge?: ReactNode;
|
|
11
|
-
description?: ReactNode;
|
|
12
|
-
/** Footer text, e.g. why the card is disabled. */
|
|
13
|
-
footer?: ReactNode;
|
|
14
|
-
/** Current card (`aria-current="true"`). */
|
|
15
|
-
active?: boolean;
|
|
16
|
-
/** Not clickable (`aria-disabled`); renders a <span>. */
|
|
17
|
-
disabled?: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** `.oc-launcher`: an `<a>` (with `href`), a `<button>` (with `onClick`), or a `<span>` when disabled. */
|
|
21
|
-
export const LauncherCard = forwardRef<HTMLElement, LauncherCardProps>(function LauncherCard(
|
|
22
|
-
{ icon, title, badge, description, footer, active, disabled, className, href, onClick, ...rest },
|
|
23
|
-
ref,
|
|
24
|
-
) {
|
|
25
|
-
const content = (
|
|
26
|
-
<>
|
|
27
|
-
{icon != null && <span className="oc-launcher-icon" aria-hidden="true">{icon}</span>}
|
|
28
|
-
<h3 className="oc-launcher-title">{title}{badge != null && <> {badge}</>}</h3>
|
|
29
|
-
{description != null && <p className="oc-launcher-description">{description}</p>}
|
|
30
|
-
{footer != null && <span className="oc-launcher-footer">{footer}</span>}
|
|
31
|
-
</>
|
|
32
|
-
);
|
|
33
|
-
const cls = cx("oc-launcher", className);
|
|
34
|
-
const common = { className: cls, "aria-current": active ? ("true" as const) : undefined };
|
|
35
|
-
if (disabled) {
|
|
36
|
-
return <span ref={ref} {...common} aria-disabled="true">{content}</span>;
|
|
37
|
-
}
|
|
38
|
-
if (href === undefined && onClick) {
|
|
39
|
-
return (
|
|
40
|
-
<button ref={ref as React.Ref<HTMLButtonElement>} type="button" {...common} onClick={onClick as never}>
|
|
41
|
-
{content}
|
|
42
|
-
</button>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
return (
|
|
46
|
-
<a ref={ref as React.Ref<HTMLAnchorElement>} href={href} {...common} onClick={onClick} {...rest}>
|
|
47
|
-
{content}
|
|
48
|
-
</a>
|
|
49
|
-
);
|
|
50
|
-
});
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { AnchorHTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface LauncherCardProps extends Omit<AnchorHTMLAttributes<HTMLElement>, "title"> {
|
|
6
|
+
/** Tinted icon tile content (inline SVG). */
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
/** Trailing badge inside the title. */
|
|
10
|
+
badge?: ReactNode;
|
|
11
|
+
description?: ReactNode;
|
|
12
|
+
/** Footer text, e.g. why the card is disabled. */
|
|
13
|
+
footer?: ReactNode;
|
|
14
|
+
/** Current card (`aria-current="true"`). */
|
|
15
|
+
active?: boolean;
|
|
16
|
+
/** Not clickable (`aria-disabled`); renders a <span>. */
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `.oc-launcher`: an `<a>` (with `href`), a `<button>` (with `onClick`), or a `<span>` when disabled. */
|
|
21
|
+
export const LauncherCard = forwardRef<HTMLElement, LauncherCardProps>(function LauncherCard(
|
|
22
|
+
{ icon, title, badge, description, footer, active, disabled, className, href, onClick, ...rest },
|
|
23
|
+
ref,
|
|
24
|
+
) {
|
|
25
|
+
const content = (
|
|
26
|
+
<>
|
|
27
|
+
{icon != null && <span className="oc-launcher-icon" aria-hidden="true">{icon}</span>}
|
|
28
|
+
<h3 className="oc-launcher-title">{title}{badge != null && <> {badge}</>}</h3>
|
|
29
|
+
{description != null && <p className="oc-launcher-description">{description}</p>}
|
|
30
|
+
{footer != null && <span className="oc-launcher-footer">{footer}</span>}
|
|
31
|
+
</>
|
|
32
|
+
);
|
|
33
|
+
const cls = cx("oc-launcher", className);
|
|
34
|
+
const common = { className: cls, "aria-current": active ? ("true" as const) : undefined };
|
|
35
|
+
if (disabled) {
|
|
36
|
+
return <span ref={ref} {...common} aria-disabled="true">{content}</span>;
|
|
37
|
+
}
|
|
38
|
+
if (href === undefined && onClick) {
|
|
39
|
+
return (
|
|
40
|
+
<button ref={ref as React.Ref<HTMLButtonElement>} type="button" {...common} onClick={onClick as never}>
|
|
41
|
+
{content}
|
|
42
|
+
</button>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return (
|
|
46
|
+
<a ref={ref as React.Ref<HTMLAnchorElement>} href={href} {...common} onClick={onClick} {...rest}>
|
|
47
|
+
{content}
|
|
48
|
+
</a>
|
|
49
|
+
);
|
|
50
|
+
});
|
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
// File: packages/react/src/components/Lightbox.tsx
|
|
2
|
-
// Map: types -> Lightbox (thumbnails + <dialog> with a snap track of slides) -> jump-to-slide effect
|
|
3
|
-
// Same markup as packages/ui/snippets/lightbox.html. Prev / next stay plain anchor links (no JS needed);
|
|
4
|
-
// React only opens the dialog and scrolls to the clicked photo.
|
|
5
|
-
"use client";
|
|
6
|
-
|
|
7
|
-
import { forwardRef, useEffect, useId, useRef, useState } from "react";
|
|
8
|
-
import type { DialogHTMLAttributes, ReactNode } from "react";
|
|
9
|
-
import { attrs, cx } from "../utils";
|
|
10
|
-
import { useDialog, useMergedRef } from "../hooks";
|
|
11
|
-
import type { DialogState } from "../hooks";
|
|
12
|
-
|
|
13
|
-
export interface LightboxImage {
|
|
14
|
-
/** Image URL. Leave empty to show placeholder art (`.oc-media-art`). */
|
|
15
|
-
src?: string;
|
|
16
|
-
alt: string;
|
|
17
|
-
caption?: ReactNode;
|
|
18
|
-
/** Smaller image for the thumbnail (defaults to `src`). */
|
|
19
|
-
thumb?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface LightboxProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "title" | "open" | "onClose">, DialogState {
|
|
23
|
-
images: LightboxImage[];
|
|
24
|
-
/** Text in the top bar (`oc-lightbox__title`). */
|
|
25
|
-
title?: ReactNode;
|
|
26
|
-
/** Render the thumbnail grid that opens the viewer. Default true. */
|
|
27
|
-
thumbnails?: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const media = (img: LightboxImage, src?: string) =>
|
|
31
|
-
src ? <img src={src} alt={img.alt} /> : <span className="oc-media-art" role="img" aria-label={img.alt} />;
|
|
32
|
-
|
|
33
|
-
/** `.oc-lightbox-thumbs` + `<dialog class="oc-lightbox">`. Controlled with `open` / `onOpenChange`, or uncontrolled. */
|
|
34
|
-
export const Lightbox = forwardRef<HTMLDialogElement, LightboxProps>(function Lightbox(
|
|
35
|
-
{ images, title, thumbnails = true, open, defaultOpen, onOpenChange, className, "aria-label": label = "Photo viewer", ...rest },
|
|
36
|
-
forwardedRef,
|
|
37
|
-
) {
|
|
38
|
-
const inner = useRef<HTMLDialogElement>(null);
|
|
39
|
-
const ref = useMergedRef(inner, forwardedRef);
|
|
40
|
-
const base = `oc-lightbox-${useId().replace(/:/g, "")}`;
|
|
41
|
-
const [start, setStart] = useState(0);
|
|
42
|
-
const dlg = useDialog(inner, { open, defaultOpen, onOpenChange });
|
|
43
|
-
const single = images.length === 1;
|
|
44
|
-
const slideId = (i: number) => `${base}-${((i % images.length) + images.length) % images.length + 1}`;
|
|
45
|
-
|
|
46
|
-
// After opening: jump (without animation) to the photo that was clicked.
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
if (dlg.open) document.getElementById(slideId(start))?.scrollIntoView?.({ behavior: "instant" as ScrollBehavior, block: "nearest", inline: "center" });
|
|
49
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
50
|
-
}, [dlg.open, start]);
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<>
|
|
54
|
-
{thumbnails && (
|
|
55
|
-
<div className="oc-lightbox-thumbs">
|
|
56
|
-
{images.map((img, i) => (
|
|
57
|
-
<button key={i} type="button" className="oc-lightbox-thumb" aria-label={`Open photo ${i + 1}: ${img.alt}`} onClick={() => { setStart(i); dlg.setOpen(true); }}>
|
|
58
|
-
{media(img, img.thumb ?? img.src)}
|
|
59
|
-
</button>
|
|
60
|
-
))}
|
|
61
|
-
</div>
|
|
62
|
-
)}
|
|
63
|
-
<dialog ref={ref} className={cx("oc-lightbox", single && "oc-lightbox--single", className)} aria-label={label} {...attrs({ closedby: "any" })} {...rest} onClose={dlg.onClose}>
|
|
64
|
-
<div className="oc-lightbox__bar">
|
|
65
|
-
{title != null ? <h2 className="oc-lightbox__title">{title}</h2> : <span />}
|
|
66
|
-
<button type="button" className="oc-lightbox__close" aria-label="Close" onClick={dlg.close}>✕</button>
|
|
67
|
-
</div>
|
|
68
|
-
<div className="oc-lightbox__track" tabIndex={0} aria-label="Photos, use the arrow keys">
|
|
69
|
-
{images.map((img, i) => (
|
|
70
|
-
<figure key={i} className="oc-lightbox__slide" id={slideId(i)}>
|
|
71
|
-
<div className="oc-lightbox__media">{media(img, img.src)}</div>
|
|
72
|
-
{!single && <a className="oc-lightbox__nav oc-lightbox__nav--prev" href={`#${slideId(i - 1)}`} aria-label="Previous photo">←</a>}
|
|
73
|
-
{!single && <a className="oc-lightbox__nav oc-lightbox__nav--next" href={`#${slideId(i + 1)}`} aria-label="Next photo">→</a>}
|
|
74
|
-
<figcaption className="oc-lightbox__caption">
|
|
75
|
-
{!single && <span className="oc-lightbox__counter">{i + 1} / {images.length}</span>} {img.caption ?? img.alt}
|
|
76
|
-
</figcaption>
|
|
77
|
-
</figure>
|
|
78
|
-
))}
|
|
79
|
-
</div>
|
|
80
|
-
</dialog>
|
|
81
|
-
</>
|
|
82
|
-
);
|
|
83
|
-
});
|
|
1
|
+
// File: packages/react/src/components/Lightbox.tsx
|
|
2
|
+
// Map: types -> Lightbox (thumbnails + <dialog> with a snap track of slides) -> jump-to-slide effect
|
|
3
|
+
// Same markup as packages/ui/snippets/lightbox.html. Prev / next stay plain anchor links (no JS needed);
|
|
4
|
+
// React only opens the dialog and scrolls to the clicked photo.
|
|
5
|
+
"use client";
|
|
6
|
+
|
|
7
|
+
import { forwardRef, useEffect, useId, useRef, useState } from "react";
|
|
8
|
+
import type { DialogHTMLAttributes, ReactNode } from "react";
|
|
9
|
+
import { attrs, cx } from "../utils";
|
|
10
|
+
import { useDialog, useMergedRef } from "../hooks";
|
|
11
|
+
import type { DialogState } from "../hooks";
|
|
12
|
+
|
|
13
|
+
export interface LightboxImage {
|
|
14
|
+
/** Image URL. Leave empty to show placeholder art (`.oc-media-art`). */
|
|
15
|
+
src?: string;
|
|
16
|
+
alt: string;
|
|
17
|
+
caption?: ReactNode;
|
|
18
|
+
/** Smaller image for the thumbnail (defaults to `src`). */
|
|
19
|
+
thumb?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LightboxProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "title" | "open" | "onClose">, DialogState {
|
|
23
|
+
images: LightboxImage[];
|
|
24
|
+
/** Text in the top bar (`oc-lightbox__title`). */
|
|
25
|
+
title?: ReactNode;
|
|
26
|
+
/** Render the thumbnail grid that opens the viewer. Default true. */
|
|
27
|
+
thumbnails?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const media = (img: LightboxImage, src?: string) =>
|
|
31
|
+
src ? <img src={src} alt={img.alt} /> : <span className="oc-media-art" role="img" aria-label={img.alt} />;
|
|
32
|
+
|
|
33
|
+
/** `.oc-lightbox-thumbs` + `<dialog class="oc-lightbox">`. Controlled with `open` / `onOpenChange`, or uncontrolled. */
|
|
34
|
+
export const Lightbox = forwardRef<HTMLDialogElement, LightboxProps>(function Lightbox(
|
|
35
|
+
{ images, title, thumbnails = true, open, defaultOpen, onOpenChange, className, "aria-label": label = "Photo viewer", ...rest },
|
|
36
|
+
forwardedRef,
|
|
37
|
+
) {
|
|
38
|
+
const inner = useRef<HTMLDialogElement>(null);
|
|
39
|
+
const ref = useMergedRef(inner, forwardedRef);
|
|
40
|
+
const base = `oc-lightbox-${useId().replace(/:/g, "")}`;
|
|
41
|
+
const [start, setStart] = useState(0);
|
|
42
|
+
const dlg = useDialog(inner, { open, defaultOpen, onOpenChange });
|
|
43
|
+
const single = images.length === 1;
|
|
44
|
+
const slideId = (i: number) => `${base}-${((i % images.length) + images.length) % images.length + 1}`;
|
|
45
|
+
|
|
46
|
+
// After opening: jump (without animation) to the photo that was clicked.
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (dlg.open) document.getElementById(slideId(start))?.scrollIntoView?.({ behavior: "instant" as ScrollBehavior, block: "nearest", inline: "center" });
|
|
49
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
50
|
+
}, [dlg.open, start]);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
{thumbnails && (
|
|
55
|
+
<div className="oc-lightbox-thumbs">
|
|
56
|
+
{images.map((img, i) => (
|
|
57
|
+
<button key={i} type="button" className="oc-lightbox-thumb" aria-label={`Open photo ${i + 1}: ${img.alt}`} onClick={() => { setStart(i); dlg.setOpen(true); }}>
|
|
58
|
+
{media(img, img.thumb ?? img.src)}
|
|
59
|
+
</button>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
<dialog ref={ref} className={cx("oc-lightbox", single && "oc-lightbox--single", className)} aria-label={label} {...attrs({ closedby: "any" })} {...rest} onClose={dlg.onClose}>
|
|
64
|
+
<div className="oc-lightbox__bar">
|
|
65
|
+
{title != null ? <h2 className="oc-lightbox__title">{title}</h2> : <span />}
|
|
66
|
+
<button type="button" className="oc-lightbox__close" aria-label="Close" onClick={dlg.close}>✕</button>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="oc-lightbox__track" tabIndex={0} aria-label="Photos, use the arrow keys">
|
|
69
|
+
{images.map((img, i) => (
|
|
70
|
+
<figure key={i} className="oc-lightbox__slide" id={slideId(i)}>
|
|
71
|
+
<div className="oc-lightbox__media">{media(img, img.src)}</div>
|
|
72
|
+
{!single && <a className="oc-lightbox__nav oc-lightbox__nav--prev" href={`#${slideId(i - 1)}`} aria-label="Previous photo">←</a>}
|
|
73
|
+
{!single && <a className="oc-lightbox__nav oc-lightbox__nav--next" href={`#${slideId(i + 1)}`} aria-label="Next photo">→</a>}
|
|
74
|
+
<figcaption className="oc-lightbox__caption">
|
|
75
|
+
{!single && <span className="oc-lightbox__counter">{i + 1} / {images.length}</span>} {img.caption ?? img.alt}
|
|
76
|
+
</figcaption>
|
|
77
|
+
</figure>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
</dialog>
|
|
81
|
+
</>
|
|
82
|
+
);
|
|
83
|
+
});
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
// File: packages/react/src/components/LinkArrow.tsx | Map: types -> LinkArrow (a.oc-link-arrow + modifiers)
|
|
2
|
-
import { forwardRef } from "react";
|
|
3
|
-
import type { AnchorHTMLAttributes } from "react";
|
|
4
|
-
import { cx } from "../utils";
|
|
5
|
-
|
|
6
|
-
export type LinkArrowDirection = "forward" | "back" | "external";
|
|
7
|
-
|
|
8
|
-
export interface LinkArrowProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
9
|
-
direction?: LinkArrowDirection;
|
|
10
|
-
tone?: "default" | "accent" | "muted";
|
|
11
|
-
size?: "sm" | "default" | "lg";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** `<a class="oc-link-arrow">`: text link with an arrow that moves on hover and focus. */
|
|
15
|
-
export const LinkArrow = forwardRef<HTMLAnchorElement, LinkArrowProps>(function LinkArrow(
|
|
16
|
-
{ direction = "forward", tone = "default", size = "default", className, ...rest },
|
|
17
|
-
ref,
|
|
18
|
-
) {
|
|
19
|
-
const classes = cx(
|
|
20
|
-
"oc-link-arrow",
|
|
21
|
-
direction !== "forward" && `oc-link-arrow--${direction}`,
|
|
22
|
-
tone !== "default" && `oc-link-arrow--${tone}`,
|
|
23
|
-
size !== "default" && `oc-link-arrow--${size}`,
|
|
24
|
-
className,
|
|
25
|
-
);
|
|
26
|
-
return <a ref={ref} className={classes} {...rest} />;
|
|
27
|
-
});
|
|
1
|
+
// File: packages/react/src/components/LinkArrow.tsx | Map: types -> LinkArrow (a.oc-link-arrow + modifiers)
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import type { AnchorHTMLAttributes } from "react";
|
|
4
|
+
import { cx } from "../utils";
|
|
5
|
+
|
|
6
|
+
export type LinkArrowDirection = "forward" | "back" | "external";
|
|
7
|
+
|
|
8
|
+
export interface LinkArrowProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
9
|
+
direction?: LinkArrowDirection;
|
|
10
|
+
tone?: "default" | "accent" | "muted";
|
|
11
|
+
size?: "sm" | "default" | "lg";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** `<a class="oc-link-arrow">`: text link with an arrow that moves on hover and focus. */
|
|
15
|
+
export const LinkArrow = forwardRef<HTMLAnchorElement, LinkArrowProps>(function LinkArrow(
|
|
16
|
+
{ direction = "forward", tone = "default", size = "default", className, ...rest },
|
|
17
|
+
ref,
|
|
18
|
+
) {
|
|
19
|
+
const classes = cx(
|
|
20
|
+
"oc-link-arrow",
|
|
21
|
+
direction !== "forward" && `oc-link-arrow--${direction}`,
|
|
22
|
+
tone !== "default" && `oc-link-arrow--${tone}`,
|
|
23
|
+
size !== "default" && `oc-link-arrow--${size}`,
|
|
24
|
+
className,
|
|
25
|
+
);
|
|
26
|
+
return <a ref={ref} className={classes} {...rest} />;
|
|
27
|
+
});
|
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
// File: packages/react/src/components/ListToolbar.tsx
|
|
2
|
-
// Map: ListToolbar = toolbar row -> search -> count -> spacer -> view radios -> actions
|
|
3
|
-
// The view toggle is a radio group. In React you usually render only the active view yourself
|
|
4
|
-
// (use `view` + `onViewChange`); the CSS-only `.oc-list-toolbar-scope` switch also works.
|
|
5
|
-
"use client";
|
|
6
|
-
|
|
7
|
-
import { forwardRef, useId } from "react";
|
|
8
|
-
import type { HTMLAttributes, ReactNode } from "react";
|
|
9
|
-
import { cx } from "../utils";
|
|
10
|
-
import { useControllable } from "../hooks";
|
|
11
|
-
|
|
12
|
-
export interface ListToolbarView {
|
|
13
|
-
/** "cards", "table" or "list" (matches `.oc-list-toolbar-panel[data-view]`). */
|
|
14
|
-
value: string;
|
|
15
|
-
label: string;
|
|
16
|
-
icon: ReactNode;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ListToolbarProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
20
|
-
/** Search area content, e.g. `<input class="oc-input" type="search">`. */
|
|
21
|
-
search?: ReactNode;
|
|
22
|
-
/** Result count text, e.g. "3 items". */
|
|
23
|
-
count?: ReactNode;
|
|
24
|
-
views?: ListToolbarView[];
|
|
25
|
-
view?: string;
|
|
26
|
-
defaultView?: string;
|
|
27
|
-
onViewChange?: (view: string) => void;
|
|
28
|
-
viewsLabel?: string;
|
|
29
|
-
actions?: ReactNode;
|
|
30
|
-
sticky?: boolean;
|
|
31
|
-
bordered?: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** `.oc-list-toolbar`: search, count, view toggle and actions above a list. */
|
|
35
|
-
export const ListToolbar = forwardRef<HTMLDivElement, ListToolbarProps>(function ListToolbar(
|
|
36
|
-
{ search, count, views, view, defaultView, onViewChange, viewsLabel = "View", actions, sticky, bordered, className, ...rest },
|
|
37
|
-
ref,
|
|
38
|
-
) {
|
|
39
|
-
const name = `oc-list-view-${useId().replace(/:/g, "")}`;
|
|
40
|
-
const [current, setCurrent] = useControllable(view, defaultView ?? views?.[0]?.value ?? "", onViewChange);
|
|
41
|
-
const cls = cx("oc-list-toolbar", sticky && "oc-list-toolbar--sticky", bordered && "oc-list-toolbar--bordered", className);
|
|
42
|
-
return (
|
|
43
|
-
<div ref={ref} className={cls} role="toolbar" {...rest}>
|
|
44
|
-
{search != null && <div className="oc-list-toolbar-search">{search}</div>}
|
|
45
|
-
{count != null && <span className="oc-list-toolbar-count">{count}</span>}
|
|
46
|
-
<span className="oc-list-toolbar-spacer" />
|
|
47
|
-
{views && views.length > 0 && (
|
|
48
|
-
<div className="oc-list-toolbar-views" role="radiogroup" aria-label={viewsLabel}>
|
|
49
|
-
{views.map((v) => (
|
|
50
|
-
<label className="oc-list-toolbar-view" title={v.label} key={v.value}>
|
|
51
|
-
<input
|
|
52
|
-
type="radio"
|
|
53
|
-
name={name}
|
|
54
|
-
value={v.value}
|
|
55
|
-
aria-label={v.label}
|
|
56
|
-
checked={current === v.value}
|
|
57
|
-
onChange={() => setCurrent(v.value)}
|
|
58
|
-
/>
|
|
59
|
-
{v.icon}
|
|
60
|
-
</label>
|
|
61
|
-
))}
|
|
62
|
-
</div>
|
|
63
|
-
)}
|
|
64
|
-
{actions != null && <div className="oc-list-toolbar-actions">{actions}</div>}
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
});
|
|
1
|
+
// File: packages/react/src/components/ListToolbar.tsx
|
|
2
|
+
// Map: ListToolbar = toolbar row -> search -> count -> spacer -> view radios -> actions
|
|
3
|
+
// The view toggle is a radio group. In React you usually render only the active view yourself
|
|
4
|
+
// (use `view` + `onViewChange`); the CSS-only `.oc-list-toolbar-scope` switch also works.
|
|
5
|
+
"use client";
|
|
6
|
+
|
|
7
|
+
import { forwardRef, useId } from "react";
|
|
8
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
9
|
+
import { cx } from "../utils";
|
|
10
|
+
import { useControllable } from "../hooks";
|
|
11
|
+
|
|
12
|
+
export interface ListToolbarView {
|
|
13
|
+
/** "cards", "table" or "list" (matches `.oc-list-toolbar-panel[data-view]`). */
|
|
14
|
+
value: string;
|
|
15
|
+
label: string;
|
|
16
|
+
icon: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ListToolbarProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
20
|
+
/** Search area content, e.g. `<input class="oc-input" type="search">`. */
|
|
21
|
+
search?: ReactNode;
|
|
22
|
+
/** Result count text, e.g. "3 items". */
|
|
23
|
+
count?: ReactNode;
|
|
24
|
+
views?: ListToolbarView[];
|
|
25
|
+
view?: string;
|
|
26
|
+
defaultView?: string;
|
|
27
|
+
onViewChange?: (view: string) => void;
|
|
28
|
+
viewsLabel?: string;
|
|
29
|
+
actions?: ReactNode;
|
|
30
|
+
sticky?: boolean;
|
|
31
|
+
bordered?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** `.oc-list-toolbar`: search, count, view toggle and actions above a list. */
|
|
35
|
+
export const ListToolbar = forwardRef<HTMLDivElement, ListToolbarProps>(function ListToolbar(
|
|
36
|
+
{ search, count, views, view, defaultView, onViewChange, viewsLabel = "View", actions, sticky, bordered, className, ...rest },
|
|
37
|
+
ref,
|
|
38
|
+
) {
|
|
39
|
+
const name = `oc-list-view-${useId().replace(/:/g, "")}`;
|
|
40
|
+
const [current, setCurrent] = useControllable(view, defaultView ?? views?.[0]?.value ?? "", onViewChange);
|
|
41
|
+
const cls = cx("oc-list-toolbar", sticky && "oc-list-toolbar--sticky", bordered && "oc-list-toolbar--bordered", className);
|
|
42
|
+
return (
|
|
43
|
+
<div ref={ref} className={cls} role="toolbar" {...rest}>
|
|
44
|
+
{search != null && <div className="oc-list-toolbar-search">{search}</div>}
|
|
45
|
+
{count != null && <span className="oc-list-toolbar-count">{count}</span>}
|
|
46
|
+
<span className="oc-list-toolbar-spacer" />
|
|
47
|
+
{views && views.length > 0 && (
|
|
48
|
+
<div className="oc-list-toolbar-views" role="radiogroup" aria-label={viewsLabel}>
|
|
49
|
+
{views.map((v) => (
|
|
50
|
+
<label className="oc-list-toolbar-view" title={v.label} key={v.value}>
|
|
51
|
+
<input
|
|
52
|
+
type="radio"
|
|
53
|
+
name={name}
|
|
54
|
+
value={v.value}
|
|
55
|
+
aria-label={v.label}
|
|
56
|
+
checked={current === v.value}
|
|
57
|
+
onChange={() => setCurrent(v.value)}
|
|
58
|
+
/>
|
|
59
|
+
{v.icon}
|
|
60
|
+
</label>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
{actions != null && <div className="oc-list-toolbar-actions">{actions}</div>}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
});
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
// File: packages/react/src/components/Marquee.tsx | Map: props -> Marquee (track with items + aria-hidden copy)
|
|
2
|
-
import { forwardRef } from "react";
|
|
3
|
-
import type { HTMLAttributes, ReactNode } from "react";
|
|
4
|
-
import { cx } from "../utils";
|
|
5
|
-
|
|
6
|
-
export interface MarqueeProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
-
/** Items of the strip. They are rendered twice; the copy is aria-hidden. */
|
|
8
|
-
items: ReactNode[];
|
|
9
|
-
reverse?: boolean;
|
|
10
|
-
speed?: "default" | "slow" | "fast";
|
|
11
|
-
/** Text only, no pills. */
|
|
12
|
-
plain?: boolean;
|
|
13
|
-
/** No soft fade at the edges. */
|
|
14
|
-
noFade?: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** `.oc-marquee`: endless strip that pauses on hover / focus and stands still with reduced motion. */
|
|
18
|
-
export const Marquee = forwardRef<HTMLDivElement, MarqueeProps>(function Marquee(
|
|
19
|
-
{ items, reverse = false, speed = "default", plain = false, noFade = false, className, ...rest },
|
|
20
|
-
ref,
|
|
21
|
-
) {
|
|
22
|
-
const classes = cx(
|
|
23
|
-
"oc-marquee",
|
|
24
|
-
reverse && "oc-marquee--reverse",
|
|
25
|
-
speed !== "default" && `oc-marquee--${speed}`,
|
|
26
|
-
plain && "oc-marquee--plain",
|
|
27
|
-
noFade && "oc-marquee--no-fade",
|
|
28
|
-
className,
|
|
29
|
-
);
|
|
30
|
-
return (
|
|
31
|
-
<div ref={ref} className={classes} {...rest}>
|
|
32
|
-
<div className="oc-marquee__track">
|
|
33
|
-
{items.map((item, i) => <span className="oc-marquee__item" key={i}>{item}</span>)}
|
|
34
|
-
{items.map((item, i) => <span className="oc-marquee__item" aria-hidden="true" key={`copy-${i}`}>{item}</span>)}
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
37
|
-
);
|
|
38
|
-
});
|
|
1
|
+
// File: packages/react/src/components/Marquee.tsx | Map: props -> Marquee (track with items + aria-hidden copy)
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
4
|
+
import { cx } from "../utils";
|
|
5
|
+
|
|
6
|
+
export interface MarqueeProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Items of the strip. They are rendered twice; the copy is aria-hidden. */
|
|
8
|
+
items: ReactNode[];
|
|
9
|
+
reverse?: boolean;
|
|
10
|
+
speed?: "default" | "slow" | "fast";
|
|
11
|
+
/** Text only, no pills. */
|
|
12
|
+
plain?: boolean;
|
|
13
|
+
/** No soft fade at the edges. */
|
|
14
|
+
noFade?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** `.oc-marquee`: endless strip that pauses on hover / focus and stands still with reduced motion. */
|
|
18
|
+
export const Marquee = forwardRef<HTMLDivElement, MarqueeProps>(function Marquee(
|
|
19
|
+
{ items, reverse = false, speed = "default", plain = false, noFade = false, className, ...rest },
|
|
20
|
+
ref,
|
|
21
|
+
) {
|
|
22
|
+
const classes = cx(
|
|
23
|
+
"oc-marquee",
|
|
24
|
+
reverse && "oc-marquee--reverse",
|
|
25
|
+
speed !== "default" && `oc-marquee--${speed}`,
|
|
26
|
+
plain && "oc-marquee--plain",
|
|
27
|
+
noFade && "oc-marquee--no-fade",
|
|
28
|
+
className,
|
|
29
|
+
);
|
|
30
|
+
return (
|
|
31
|
+
<div ref={ref} className={classes} {...rest}>
|
|
32
|
+
<div className="oc-marquee__track">
|
|
33
|
+
{items.map((item, i) => <span className="oc-marquee__item" key={i}>{item}</span>)}
|
|
34
|
+
{items.map((item, i) => <span className="oc-marquee__item" aria-hidden="true" key={`copy-${i}`}>{item}</span>)}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
});
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
// File: packages/react/src/components/MediaCard.tsx
|
|
2
|
-
// Map: props -> MediaCard (MediaThumb -> body: avatar, title link + meta, menu button -> popover menu)
|
|
3
|
-
// Same markup as packages/ui/snippets/media-card.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 MediaCardProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
11
|
-
title: ReactNode;
|
|
12
|
-
/** Link of the title; it covers the whole card. */
|
|
13
|
-
href: string;
|
|
14
|
-
/** "Channel · 24K views · 3 days ago". */
|
|
15
|
-
meta?: ReactNode;
|
|
16
|
-
/** Initials, an <img>, or any node. Leave out for no avatar. */
|
|
17
|
-
avatar?: ReactNode;
|
|
18
|
-
/** Props for the MediaThumb on top (src, duration, progress, …). */
|
|
19
|
-
thumb?: MediaThumbProps;
|
|
20
|
-
/** Items of the ⋮ menu (`.oc-dropdown-item` elements). No menu button when empty. */
|
|
21
|
-
menu?: ReactNode;
|
|
22
|
-
compact?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** `article.oc-media-card`. */
|
|
26
|
-
export const MediaCard = forwardRef<HTMLElement, MediaCardProps>(function MediaCard(
|
|
27
|
-
{ title, href, meta, avatar, thumb, menu, compact, className, ...rest },
|
|
28
|
-
ref,
|
|
29
|
-
) {
|
|
30
|
-
const menuId = `oc-media-card-menu-${useId().replace(/:/g, "")}`;
|
|
31
|
-
return (
|
|
32
|
-
<article ref={ref} className={cx("oc-media-card", compact && "oc-media-card--compact", className)} {...rest}>
|
|
33
|
-
<MediaThumb {...thumb} />
|
|
34
|
-
<div className="oc-media-card__body">
|
|
35
|
-
{avatar != null && <span className="oc-media-card__avatar" aria-hidden="true">{avatar}</span>}
|
|
36
|
-
<div className="oc-media-card__text">
|
|
37
|
-
<h3 className="oc-media-card__title"><a href={href}>{title}</a></h3>
|
|
38
|
-
{meta != null && <p className="oc-media-card__meta">{meta}</p>}
|
|
39
|
-
</div>
|
|
40
|
-
{menu != null && (
|
|
41
|
-
<button type="button" className="oc-media-card__menu" aria-label="More actions" {...attrs({ popovertarget: menuId })}>⋮</button>
|
|
42
|
-
)}
|
|
43
|
-
</div>
|
|
44
|
-
{menu != null && <div className="oc-dropdown-menu" id={menuId} {...attrs({ popover: "" })}>{menu}</div>}
|
|
45
|
-
</article>
|
|
46
|
-
);
|
|
47
|
-
});
|
|
1
|
+
// File: packages/react/src/components/MediaCard.tsx
|
|
2
|
+
// Map: props -> MediaCard (MediaThumb -> body: avatar, title link + meta, menu button -> popover menu)
|
|
3
|
+
// Same markup as packages/ui/snippets/media-card.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 MediaCardProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
11
|
+
title: ReactNode;
|
|
12
|
+
/** Link of the title; it covers the whole card. */
|
|
13
|
+
href: string;
|
|
14
|
+
/** "Channel · 24K views · 3 days ago". */
|
|
15
|
+
meta?: ReactNode;
|
|
16
|
+
/** Initials, an <img>, or any node. Leave out for no avatar. */
|
|
17
|
+
avatar?: ReactNode;
|
|
18
|
+
/** Props for the MediaThumb on top (src, duration, progress, …). */
|
|
19
|
+
thumb?: MediaThumbProps;
|
|
20
|
+
/** Items of the ⋮ menu (`.oc-dropdown-item` elements). No menu button when empty. */
|
|
21
|
+
menu?: ReactNode;
|
|
22
|
+
compact?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `article.oc-media-card`. */
|
|
26
|
+
export const MediaCard = forwardRef<HTMLElement, MediaCardProps>(function MediaCard(
|
|
27
|
+
{ title, href, meta, avatar, thumb, menu, compact, className, ...rest },
|
|
28
|
+
ref,
|
|
29
|
+
) {
|
|
30
|
+
const menuId = `oc-media-card-menu-${useId().replace(/:/g, "")}`;
|
|
31
|
+
return (
|
|
32
|
+
<article ref={ref} className={cx("oc-media-card", compact && "oc-media-card--compact", className)} {...rest}>
|
|
33
|
+
<MediaThumb {...thumb} />
|
|
34
|
+
<div className="oc-media-card__body">
|
|
35
|
+
{avatar != null && <span className="oc-media-card__avatar" aria-hidden="true">{avatar}</span>}
|
|
36
|
+
<div className="oc-media-card__text">
|
|
37
|
+
<h3 className="oc-media-card__title"><a href={href}>{title}</a></h3>
|
|
38
|
+
{meta != null && <p className="oc-media-card__meta">{meta}</p>}
|
|
39
|
+
</div>
|
|
40
|
+
{menu != null && (
|
|
41
|
+
<button type="button" className="oc-media-card__menu" aria-label="More actions" {...attrs({ popovertarget: menuId })}>⋮</button>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
{menu != null && <div className="oc-dropdown-menu" id={menuId} {...attrs({ popover: "" })}>{menu}</div>}
|
|
45
|
+
</article>
|
|
46
|
+
);
|
|
47
|
+
});
|