@360crewing/ui 0.1.3
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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/components/Avatar.d.ts +20 -0
- package/dist/components/Avatar.js +17 -0
- package/dist/components/Badge.d.ts +19 -0
- package/dist/components/Badge.js +10 -0
- package/dist/components/Breadcrumbs.d.ts +19 -0
- package/dist/components/Breadcrumbs.js +12 -0
- package/dist/components/Button.d.ts +38 -0
- package/dist/components/Button.js +50 -0
- package/dist/components/Card.d.ts +12 -0
- package/dist/components/Card.js +6 -0
- package/dist/components/Checkbox.d.ts +14 -0
- package/dist/components/Checkbox.js +9 -0
- package/dist/components/CheckboxField.d.ts +16 -0
- package/dist/components/CheckboxField.js +17 -0
- package/dist/components/CollapsibleFields.d.ts +9 -0
- package/dist/components/CollapsibleFields.js +8 -0
- package/dist/components/ContentLoader.d.ts +8 -0
- package/dist/components/ContentLoader.js +14 -0
- package/dist/components/Delimeter.d.ts +3 -0
- package/dist/components/Delimeter.js +6 -0
- package/dist/components/DetailItem.d.ts +8 -0
- package/dist/components/DetailItem.js +6 -0
- package/dist/components/DropdownButton.d.ts +15 -0
- package/dist/components/DropdownButton.js +29 -0
- package/dist/components/FileUpload.d.ts +32 -0
- package/dist/components/FileUpload.js +75 -0
- package/dist/components/FormActionButtons.d.ts +18 -0
- package/dist/components/FormActionButtons.js +10 -0
- package/dist/components/Icon.d.ts +20 -0
- package/dist/components/Icon.js +11 -0
- package/dist/components/IconButton.d.ts +14 -0
- package/dist/components/IconButton.js +9 -0
- package/dist/components/InformationPanel.d.ts +14 -0
- package/dist/components/InformationPanel.js +6 -0
- package/dist/components/LayoutBlock.d.ts +6 -0
- package/dist/components/LayoutBlock.js +5 -0
- package/dist/components/Page.d.ts +12 -0
- package/dist/components/Page.js +6 -0
- package/dist/components/Pagination.d.ts +19 -0
- package/dist/components/Pagination.js +35 -0
- package/dist/components/Popover.d.ts +27 -0
- package/dist/components/Popover.js +130 -0
- package/dist/components/SearchInput.d.ts +27 -0
- package/dist/components/SearchInput.js +44 -0
- package/dist/components/ShadowedBlock.d.ts +9 -0
- package/dist/components/ShadowedBlock.js +6 -0
- package/dist/components/SidebarMenu.d.ts +27 -0
- package/dist/components/SidebarMenu.js +16 -0
- package/dist/components/SkeletonLoader.d.ts +4 -0
- package/dist/components/SkeletonLoader.js +7 -0
- package/dist/components/StatusBadge.d.ts +20 -0
- package/dist/components/StatusBadge.js +11 -0
- package/dist/components/Table.d.ts +39 -0
- package/dist/components/Table.js +24 -0
- package/dist/components/Tabs.d.ts +34 -0
- package/dist/components/Tabs.js +95 -0
- package/dist/components/Tag.d.ts +20 -0
- package/dist/components/Tag.js +11 -0
- package/dist/components/TextField.d.ts +45 -0
- package/dist/components/TextField.js +53 -0
- package/dist/components/TextareaField.d.ts +18 -0
- package/dist/components/TextareaField.js +11 -0
- package/dist/components/Toggle.d.ts +10 -0
- package/dist/components/Toggle.js +9 -0
- package/dist/components/ToggleField.d.ts +16 -0
- package/dist/components/ToggleField.js +17 -0
- package/dist/components/Tooltip.d.ts +25 -0
- package/dist/components/Tooltip.js +128 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +35 -0
- package/dist/styles/Avatar.css +47 -0
- package/dist/styles/Badge.css +172 -0
- package/dist/styles/Breadcrumbs.css +54 -0
- package/dist/styles/Button.css +416 -0
- package/dist/styles/Card.css +34 -0
- package/dist/styles/Checkbox.css +102 -0
- package/dist/styles/CheckboxField.css +75 -0
- package/dist/styles/CollapsibleFields.css +53 -0
- package/dist/styles/Delimeter.css +7 -0
- package/dist/styles/DetailItem.css +18 -0
- package/dist/styles/DropdownButton.css +82 -0
- package/dist/styles/Error.css +14 -0
- package/dist/styles/FileUpload.css +113 -0
- package/dist/styles/Icon.css +12 -0
- package/dist/styles/IconButton.css +68 -0
- package/dist/styles/InformationPanel.css +84 -0
- package/dist/styles/Page.css +46 -0
- package/dist/styles/Pagination.css +150 -0
- package/dist/styles/Popover.css +28 -0
- package/dist/styles/ShadowedBlock.css +13 -0
- package/dist/styles/SidebarMenu.css +151 -0
- package/dist/styles/StatusBadge.css +63 -0
- package/dist/styles/Table.css +126 -0
- package/dist/styles/Tabs.css +193 -0
- package/dist/styles/Tag.css +110 -0
- package/dist/styles/TextField.css +276 -0
- package/dist/styles/Toggle.css +105 -0
- package/dist/styles/ToggleField.css +73 -0
- package/dist/styles/Tooltip.css +30 -0
- package/dist/styles/tokens.css +361 -0
- package/dist/styles/typography.css +169 -0
- package/dist/styles.css +33 -0
- package/package.json +50 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { observer } from "mobx-react-lite";
|
|
3
|
+
import Button from "./Button";
|
|
4
|
+
const FormActionButtons = observer(({ isEditing, onSaveEdit, onDelete, isDisabled = false, addButtonText, children, variant = "default", saveLabel = "Save", deleteLabel = "Delete", addLabel = "Add", }) => {
|
|
5
|
+
if (variant === "none") {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return (_jsxs("div", { className: "buttons-container", children: [variant === "save-only" ? (_jsx(Button, { className: "default-button", onClick: onSaveEdit, isDisabled: isDisabled, children: saveLabel })) : (isEditing ? _jsxs(_Fragment, { children: [_jsx(Button, { className: "default-button", onClick: onSaveEdit, isDisabled: isDisabled, children: saveLabel }), _jsx(Button, { className: "inactive-button", onClick: onDelete, isDisabled: isDisabled, children: deleteLabel })] }) : _jsx(Button, { className: "default-button", onClick: onSaveEdit, children: addButtonText ?? addLabel })), children] }));
|
|
9
|
+
});
|
|
10
|
+
export default FormActionButtons;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/Icon.css";
|
|
3
|
+
export interface IconProps {
|
|
4
|
+
/**
|
|
5
|
+
* The icon element to render (typically a `react-icons` component).
|
|
6
|
+
* Pass the icon as a child or via the `as` prop.
|
|
7
|
+
*/
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
/** Pixel size — applied to width/height of the inner svg. */
|
|
10
|
+
size?: number;
|
|
11
|
+
/** Color — applied via `color` (text) and inherited by svg `fill: currentColor`. */
|
|
12
|
+
color?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: React.CSSProperties;
|
|
15
|
+
onClick?: (e: React.MouseEvent<HTMLSpanElement>) => void;
|
|
16
|
+
/** Visually hidden label for screen readers. */
|
|
17
|
+
"aria-label"?: string;
|
|
18
|
+
}
|
|
19
|
+
declare const Icon: React.FC<IconProps>;
|
|
20
|
+
export default Icon;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import "../styles/Icon.css";
|
|
3
|
+
const Icon = ({ children, size = 16, color, className, style, onClick, "aria-label": ariaLabel, }) => {
|
|
4
|
+
return (_jsx("span", { className: `crewing-icon ${className || ""}`.trim(), style: {
|
|
5
|
+
width: size,
|
|
6
|
+
height: size,
|
|
7
|
+
color,
|
|
8
|
+
...style,
|
|
9
|
+
}, onClick: onClick, role: ariaLabel ? "img" : undefined, "aria-label": ariaLabel, children: children }));
|
|
10
|
+
};
|
|
11
|
+
export default Icon;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/IconButton.css";
|
|
3
|
+
export type IconButtonSize = "sm" | "md" | "lg";
|
|
4
|
+
export type IconButtonStyle = "ghost" | "filled";
|
|
5
|
+
export interface IconButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "style" | "type"> {
|
|
6
|
+
size?: IconButtonSize;
|
|
7
|
+
iconStyle?: IconButtonStyle;
|
|
8
|
+
/** Required — purpose of the button for screen readers. */
|
|
9
|
+
"aria-label": string;
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
declare const IconButton: React.ForwardRefExoticComponent<IconButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
14
|
+
export default IconButton;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import "../styles/IconButton.css";
|
|
5
|
+
const IconButton = forwardRef(({ size = "md", iconStyle = "ghost", children, className, disabled, ...rest }, ref) => {
|
|
6
|
+
return (_jsx("button", { ...rest, ref: ref, type: "button", disabled: disabled, className: classNames("ds-icon-button", `ds-icon-button--${size}`, `ds-icon-button--${iconStyle}`, className), children: children }));
|
|
7
|
+
});
|
|
8
|
+
IconButton.displayName = "IconButton";
|
|
9
|
+
export default IconButton;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/InformationPanel.css";
|
|
3
|
+
export interface InformationItem {
|
|
4
|
+
label: string;
|
|
5
|
+
value: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export interface InformationPanelProps {
|
|
8
|
+
items: InformationItem[];
|
|
9
|
+
actions?: React.ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
declare const InformationPanel: React.FC<InformationPanelProps>;
|
|
14
|
+
export default InformationPanel;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import "../styles/InformationPanel.css";
|
|
3
|
+
const InformationPanel = ({ items, actions, className = "", style }) => {
|
|
4
|
+
return (_jsxs("div", { className: `information-panel ${className}`.trim(), style: style, children: [_jsx("div", { className: "information-items", children: items.map((it) => (_jsxs("div", { className: "information-item", children: [_jsxs("span", { className: "information-item-label", children: [it.label, ":"] }), _jsx("span", { className: "information-item-value", children: it.value || "-" })] }, it.label))) }), actions && (_jsx("div", { className: "information-actions", children: actions }))] }));
|
|
5
|
+
};
|
|
6
|
+
export default InformationPanel;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/Page.css";
|
|
3
|
+
export interface PageProps {
|
|
4
|
+
title?: React.ReactNode;
|
|
5
|
+
subtitle?: React.ReactNode;
|
|
6
|
+
actions?: React.ReactNode;
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
style?: React.CSSProperties;
|
|
10
|
+
}
|
|
11
|
+
declare const Page: React.FC<PageProps>;
|
|
12
|
+
export default Page;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "../styles/Page.css";
|
|
3
|
+
const Page = ({ title, subtitle, actions, children, className, style, }) => {
|
|
4
|
+
return (_jsxs("div", { className: `crewing-page ${className || ""}`.trim(), style: style, children: [(title || subtitle || actions) && (_jsxs("header", { className: "crewing-page__header", children: [_jsxs("div", { className: "crewing-page__heading", children: [title && _jsx("h1", { className: "crewing-page__title", children: title }), subtitle && _jsx("div", { className: "crewing-page__subtitle", children: subtitle })] }), actions && _jsx("div", { className: "crewing-page__actions", children: actions })] })), _jsx("div", { className: "crewing-page__content", children: children })] }));
|
|
5
|
+
};
|
|
6
|
+
export default Page;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "../styles/Pagination.css";
|
|
2
|
+
export interface PaginationProps {
|
|
3
|
+
currentPage: number;
|
|
4
|
+
totalPages: number;
|
|
5
|
+
totalResults: number;
|
|
6
|
+
onPageChange: (page: number) => void;
|
|
7
|
+
pageSize: number;
|
|
8
|
+
onPageSizeChange: (size: number) => void;
|
|
9
|
+
isDisabled?: boolean;
|
|
10
|
+
hidePageSizeSelector?: boolean;
|
|
11
|
+
/** Label rendered next to the page-size selector, default "Per page". */
|
|
12
|
+
perPageLabel?: string;
|
|
13
|
+
/** Label of the previous-page button, default "Back". */
|
|
14
|
+
backLabel?: string;
|
|
15
|
+
/** Label of the next-page button, default "Next". */
|
|
16
|
+
nextLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
declare function Pagination({ currentPage, totalPages, totalResults, onPageChange, pageSize, onPageSizeChange, isDisabled, hidePageSizeSelector, perPageLabel, backLabel, nextLabel, }: PaginationProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export default Pagination;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import "../styles/Pagination.css";
|
|
3
|
+
function Pagination({ currentPage, totalPages, totalResults, onPageChange, pageSize, onPageSizeChange, isDisabled = false, hidePageSizeSelector = false, perPageLabel = "Per page", backLabel = "Back", nextLabel = "Next", }) {
|
|
4
|
+
const getVisiblePages = () => {
|
|
5
|
+
const maxVisiblePages = 4;
|
|
6
|
+
const pages = [];
|
|
7
|
+
if (totalPages <= maxVisiblePages) {
|
|
8
|
+
for (let i = 1; i <= totalPages; i++) {
|
|
9
|
+
pages.push(i);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
const start = 1;
|
|
14
|
+
const end = totalPages;
|
|
15
|
+
const range = Math.floor(maxVisiblePages / 2);
|
|
16
|
+
const rangeStart = Math.max(currentPage - range, start + 1);
|
|
17
|
+
const rangeEnd = Math.min(currentPage + range, end - 1);
|
|
18
|
+
pages.push(start);
|
|
19
|
+
if (rangeStart > start + 1) {
|
|
20
|
+
pages.push("...");
|
|
21
|
+
}
|
|
22
|
+
for (let i = rangeStart; i <= rangeEnd; i++) {
|
|
23
|
+
pages.push(i);
|
|
24
|
+
}
|
|
25
|
+
if (rangeEnd < end - 1) {
|
|
26
|
+
pages.push("...");
|
|
27
|
+
}
|
|
28
|
+
pages.push(end);
|
|
29
|
+
}
|
|
30
|
+
return pages;
|
|
31
|
+
};
|
|
32
|
+
const visiblePages = getVisiblePages();
|
|
33
|
+
return (_jsx("div", { className: "pagination-container", children: _jsxs("div", { className: "pages-wrapper", children: [!hidePageSizeSelector && (_jsxs("div", { className: "pagination-page-size", children: [_jsxs("label", { htmlFor: "pageSize", children: [perPageLabel, ": "] }), _jsx("select", { id: "pageSize", value: pageSize, onChange: (e) => onPageSizeChange(parseInt(e.target.value, 10)), disabled: isDisabled, children: [10, 25, 50, 100].map((size) => (_jsx("option", { value: size, children: size }, size))) })] })), _jsxs("div", { className: "pagination-pages", children: [_jsx("button", { type: "button", onClick: () => onPageChange(currentPage - 1), disabled: currentPage === 1 || isDisabled, className: "pagination-button pagination-button--nav pagination-button--prev", children: backLabel }), visiblePages.map((page, index) => page === "..." ? (_jsx("span", { className: "pagination-ellipsis", children: "..." }, `ellipsis-${index}`)) : (_jsx("button", { type: "button", onClick: () => onPageChange(page), className: `pagination-button pagination-button--page ${page === currentPage ? "active" : ""}`, disabled: isDisabled, children: page }, page))), _jsx("button", { type: "button", onClick: () => onPageChange(currentPage + 1), disabled: currentPage === totalPages || isDisabled, className: "pagination-button pagination-button--nav pagination-button--next", children: nextLabel })] })] }) }));
|
|
34
|
+
}
|
|
35
|
+
export default Pagination;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/Popover.css";
|
|
3
|
+
export type PopoverSide = "top" | "right" | "bottom" | "left";
|
|
4
|
+
export type PopoverAlign = "start" | "center" | "end";
|
|
5
|
+
export interface PopoverProps {
|
|
6
|
+
/**
|
|
7
|
+
* Trigger element. Receives an onClick + aria-expanded + aria-controls handler.
|
|
8
|
+
* Must be a single React element that forwards refs.
|
|
9
|
+
*/
|
|
10
|
+
trigger: React.ReactElement;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
side?: PopoverSide;
|
|
13
|
+
align?: PopoverAlign;
|
|
14
|
+
/** Controlled open state. Omit for uncontrolled. */
|
|
15
|
+
isOpen?: boolean;
|
|
16
|
+
defaultOpen?: boolean;
|
|
17
|
+
onOpenChange?: (open: boolean) => void;
|
|
18
|
+
/** Close when clicking outside the trigger / panel. Default true. */
|
|
19
|
+
closeOnOutsideClick?: boolean;
|
|
20
|
+
/** Close on Escape. Default true. */
|
|
21
|
+
closeOnEscape?: boolean;
|
|
22
|
+
className?: string;
|
|
23
|
+
/** Width override for the panel. */
|
|
24
|
+
panelWidth?: number | string;
|
|
25
|
+
}
|
|
26
|
+
declare const Popover: React.FC<PopoverProps>;
|
|
27
|
+
export default Popover;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import { cloneElement, isValidElement, useCallback, useEffect, useId, useLayoutEffect, useRef, useState, } from "react";
|
|
4
|
+
import "../styles/Popover.css";
|
|
5
|
+
const computePosition = (anchor, panel, side, align, gap) => {
|
|
6
|
+
let top = 0;
|
|
7
|
+
let left = 0;
|
|
8
|
+
if (side === "top")
|
|
9
|
+
top = anchor.top - panel.height - gap;
|
|
10
|
+
else if (side === "bottom")
|
|
11
|
+
top = anchor.bottom + gap;
|
|
12
|
+
else if (side === "left")
|
|
13
|
+
left = anchor.left - panel.width - gap;
|
|
14
|
+
else
|
|
15
|
+
left = anchor.right + gap;
|
|
16
|
+
if (side === "top" || side === "bottom") {
|
|
17
|
+
if (align === "start")
|
|
18
|
+
left = anchor.left;
|
|
19
|
+
else if (align === "end")
|
|
20
|
+
left = anchor.right - panel.width;
|
|
21
|
+
else
|
|
22
|
+
left = anchor.left + (anchor.width - panel.width) / 2;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
if (align === "start")
|
|
26
|
+
top = anchor.top;
|
|
27
|
+
else if (align === "end")
|
|
28
|
+
top = anchor.bottom - panel.height;
|
|
29
|
+
else
|
|
30
|
+
top = anchor.top + (anchor.height - panel.height) / 2;
|
|
31
|
+
}
|
|
32
|
+
const margin = 8;
|
|
33
|
+
left = Math.min(Math.max(margin, left), window.innerWidth - panel.width - margin);
|
|
34
|
+
top = Math.min(Math.max(margin, top), window.innerHeight - panel.height - margin);
|
|
35
|
+
return { top: top + window.scrollY, left: left + window.scrollX };
|
|
36
|
+
};
|
|
37
|
+
const Popover = ({ trigger, children, side = "bottom", align = "start", isOpen, defaultOpen, onOpenChange, closeOnOutsideClick = true, closeOnEscape = true, className, panelWidth, }) => {
|
|
38
|
+
const id = useId();
|
|
39
|
+
const triggerRef = useRef(null);
|
|
40
|
+
const panelRef = useRef(null);
|
|
41
|
+
const isControlled = isOpen !== undefined;
|
|
42
|
+
const [internal, setInternal] = useState(defaultOpen ?? false);
|
|
43
|
+
const open = isControlled ? !!isOpen : internal;
|
|
44
|
+
const [pos, setPos] = useState(null);
|
|
45
|
+
const setOpen = useCallback((next) => {
|
|
46
|
+
if (!isControlled)
|
|
47
|
+
setInternal(next);
|
|
48
|
+
onOpenChange?.(next);
|
|
49
|
+
}, [isControlled, onOpenChange]);
|
|
50
|
+
useLayoutEffect(() => {
|
|
51
|
+
if (!open || !triggerRef.current || !panelRef.current)
|
|
52
|
+
return;
|
|
53
|
+
const a = triggerRef.current.getBoundingClientRect();
|
|
54
|
+
const p = panelRef.current.getBoundingClientRect();
|
|
55
|
+
setPos(computePosition(a, p, side, align, 8));
|
|
56
|
+
}, [open, side, align, children]);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!open)
|
|
59
|
+
return;
|
|
60
|
+
const onScrollOrResize = () => {
|
|
61
|
+
if (!triggerRef.current || !panelRef.current)
|
|
62
|
+
return;
|
|
63
|
+
const a = triggerRef.current.getBoundingClientRect();
|
|
64
|
+
const p = panelRef.current.getBoundingClientRect();
|
|
65
|
+
setPos(computePosition(a, p, side, align, 8));
|
|
66
|
+
};
|
|
67
|
+
window.addEventListener("scroll", onScrollOrResize, true);
|
|
68
|
+
window.addEventListener("resize", onScrollOrResize);
|
|
69
|
+
return () => {
|
|
70
|
+
window.removeEventListener("scroll", onScrollOrResize, true);
|
|
71
|
+
window.removeEventListener("resize", onScrollOrResize);
|
|
72
|
+
};
|
|
73
|
+
}, [open, side, align]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!open)
|
|
76
|
+
return;
|
|
77
|
+
const onDocClick = (e) => {
|
|
78
|
+
if (!closeOnOutsideClick)
|
|
79
|
+
return;
|
|
80
|
+
const t = e.target;
|
|
81
|
+
if (!t)
|
|
82
|
+
return;
|
|
83
|
+
if (panelRef.current?.contains(t))
|
|
84
|
+
return;
|
|
85
|
+
if (triggerRef.current?.contains(t))
|
|
86
|
+
return;
|
|
87
|
+
setOpen(false);
|
|
88
|
+
};
|
|
89
|
+
const onKey = (e) => {
|
|
90
|
+
if (e.key === "Escape" && closeOnEscape) {
|
|
91
|
+
e.stopPropagation();
|
|
92
|
+
setOpen(false);
|
|
93
|
+
triggerRef.current?.focus();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
document.addEventListener("mousedown", onDocClick);
|
|
97
|
+
document.addEventListener("keydown", onKey);
|
|
98
|
+
return () => {
|
|
99
|
+
document.removeEventListener("mousedown", onDocClick);
|
|
100
|
+
document.removeEventListener("keydown", onKey);
|
|
101
|
+
};
|
|
102
|
+
}, [open, closeOnOutsideClick, closeOnEscape, setOpen]);
|
|
103
|
+
if (!isValidElement(trigger)) {
|
|
104
|
+
return trigger;
|
|
105
|
+
}
|
|
106
|
+
const triggerProps = {
|
|
107
|
+
ref: (el) => {
|
|
108
|
+
triggerRef.current = el;
|
|
109
|
+
const r = trigger.ref;
|
|
110
|
+
if (typeof r === "function")
|
|
111
|
+
r(el);
|
|
112
|
+
else if (r && typeof r === "object")
|
|
113
|
+
r.current = el;
|
|
114
|
+
},
|
|
115
|
+
onClick: (e) => {
|
|
116
|
+
trigger.props.onClick?.(e);
|
|
117
|
+
if (e.defaultPrevented)
|
|
118
|
+
return;
|
|
119
|
+
setOpen(!open);
|
|
120
|
+
},
|
|
121
|
+
"aria-expanded": open,
|
|
122
|
+
"aria-controls": id,
|
|
123
|
+
"aria-haspopup": "dialog",
|
|
124
|
+
};
|
|
125
|
+
return (_jsxs(_Fragment, { children: [cloneElement(trigger, triggerProps), open && (_jsx("div", { ref: panelRef, id: id, role: "dialog", className: classNames("ds-popover", `ds-popover--${side}`, className), style: {
|
|
126
|
+
...(pos ? { top: pos.top, left: pos.left } : { visibility: "hidden" }),
|
|
127
|
+
...(panelWidth !== undefined ? { width: panelWidth } : {}),
|
|
128
|
+
}, children: children }))] }));
|
|
129
|
+
};
|
|
130
|
+
export default Popover;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { TextFieldSize, TextFieldState } from "./TextField";
|
|
3
|
+
export interface SearchInputProps {
|
|
4
|
+
value?: string;
|
|
5
|
+
defaultValue?: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
isDisabled?: boolean;
|
|
9
|
+
size?: TextFieldSize;
|
|
10
|
+
state?: TextFieldState;
|
|
11
|
+
helpText?: string;
|
|
12
|
+
/** Fires for every keystroke after `debounceMs`. Set to 0 for instant. Default 200. */
|
|
13
|
+
debounceMs?: number;
|
|
14
|
+
onChange?: (value: string) => void;
|
|
15
|
+
onSubmit?: (value: string) => void;
|
|
16
|
+
onClear?: () => void;
|
|
17
|
+
/** Override the leading icon (defaults to a search glyph). */
|
|
18
|
+
leadingIcon?: React.ReactNode;
|
|
19
|
+
/** Hide the clear ✕ button when the field has text. Default false. */
|
|
20
|
+
hideClearAction?: boolean;
|
|
21
|
+
className?: string;
|
|
22
|
+
style?: React.CSSProperties;
|
|
23
|
+
autoFocus?: boolean;
|
|
24
|
+
id?: string;
|
|
25
|
+
}
|
|
26
|
+
declare const SearchInput: React.FC<SearchInputProps>;
|
|
27
|
+
export default SearchInput;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import TextField from "./TextField";
|
|
5
|
+
const DefaultSearchIcon = () => (_jsxs("svg", { viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("circle", { cx: "11", cy: "11", r: "7" }), _jsx("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })] }));
|
|
6
|
+
const SearchInput = ({ value, defaultValue, placeholder = "Search…", name, isDisabled, size = "md", state = "default", helpText, debounceMs = 200, onChange, onSubmit, onClear, leadingIcon, hideClearAction, className, style, autoFocus, id, }) => {
|
|
7
|
+
const isControlled = value !== undefined;
|
|
8
|
+
const [internal, setInternal] = useState(defaultValue ?? "");
|
|
9
|
+
const current = isControlled ? (value ?? "") : internal;
|
|
10
|
+
const debounceTimer = useRef(null);
|
|
11
|
+
useEffect(() => () => {
|
|
12
|
+
if (debounceTimer.current)
|
|
13
|
+
window.clearTimeout(debounceTimer.current);
|
|
14
|
+
}, []);
|
|
15
|
+
const fire = (next) => {
|
|
16
|
+
if (debounceMs <= 0) {
|
|
17
|
+
onChange?.(next);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (debounceTimer.current)
|
|
21
|
+
window.clearTimeout(debounceTimer.current);
|
|
22
|
+
debounceTimer.current = window.setTimeout(() => onChange?.(next), debounceMs);
|
|
23
|
+
};
|
|
24
|
+
const setValue = (next) => {
|
|
25
|
+
if (!isControlled)
|
|
26
|
+
setInternal(next);
|
|
27
|
+
fire(next);
|
|
28
|
+
};
|
|
29
|
+
const onKeyDown = (e) => {
|
|
30
|
+
if (e.key === "Enter")
|
|
31
|
+
onSubmit?.(current);
|
|
32
|
+
if (e.key === "Escape" && current) {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
setValue("");
|
|
35
|
+
onClear?.();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const showClear = !hideClearAction && current.length > 0 && !isDisabled;
|
|
39
|
+
return (_jsx(TextField, { id: id, name: name, type: "search", value: current, onChange: setValue, onKeyDown: onKeyDown, placeholder: placeholder, isDisabled: isDisabled, size: size, state: state, helpText: helpText, leadingIcon: leadingIcon ?? _jsx(DefaultSearchIcon, {}), trailingAction: showClear ? (_jsx("button", { type: "button", className: "ds-input__clear", "aria-label": "Clear search", onClick: () => {
|
|
40
|
+
setValue("");
|
|
41
|
+
onClear?.();
|
|
42
|
+
}, children: "\u2715" })) : undefined, inputMode: "search", autoFocus: autoFocus, className: classNames("ds-search-input", className), style: style }));
|
|
43
|
+
};
|
|
44
|
+
export default SearchInput;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/ShadowedBlock.css";
|
|
3
|
+
export interface ShadowedBlockProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
style?: React.CSSProperties;
|
|
7
|
+
}
|
|
8
|
+
declare const ShadowedBlock: React.FC<ShadowedBlockProps>;
|
|
9
|
+
export default ShadowedBlock;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import "../styles/ShadowedBlock.css";
|
|
3
|
+
const ShadowedBlock = ({ children, className, style }) => {
|
|
4
|
+
return _jsx("div", { className: `shadowed-block ${className || ""}`, style: style, children: children });
|
|
5
|
+
};
|
|
6
|
+
export default ShadowedBlock;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/SidebarMenu.css";
|
|
3
|
+
export type SidebarMenuItemState = "default" | "selected" | "disabled" | "danger" | "locked";
|
|
4
|
+
export interface SidebarMenuItem {
|
|
5
|
+
key: string;
|
|
6
|
+
label: React.ReactNode;
|
|
7
|
+
state?: SidebarMenuItemState;
|
|
8
|
+
icon?: React.ReactNode;
|
|
9
|
+
trailing?: React.ReactNode;
|
|
10
|
+
onClick?: () => void;
|
|
11
|
+
href?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SidebarMenuProps {
|
|
14
|
+
items: SidebarMenuItem[];
|
|
15
|
+
/** Optional header title (e.g. "Menu"). */
|
|
16
|
+
title?: React.ReactNode;
|
|
17
|
+
/** Optional toggle handler for the leading hamburger icon. */
|
|
18
|
+
onToggle?: () => void;
|
|
19
|
+
className?: string;
|
|
20
|
+
style?: React.CSSProperties;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* SidebarMenu — vertical admin-layout menu (Figma `SidebarMenu/Item`).
|
|
24
|
+
* Item states: default/selected/disabled/danger/locked.
|
|
25
|
+
*/
|
|
26
|
+
declare const SidebarMenu: React.FC<SidebarMenuProps>;
|
|
27
|
+
export default SidebarMenu;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import "../styles/SidebarMenu.css";
|
|
4
|
+
/**
|
|
5
|
+
* SidebarMenu — vertical admin-layout menu (Figma `SidebarMenu/Item`).
|
|
6
|
+
* Item states: default/selected/disabled/danger/locked.
|
|
7
|
+
*/
|
|
8
|
+
const SidebarMenu = ({ items, title, onToggle, className, style, }) => {
|
|
9
|
+
return (_jsxs("nav", { className: classNames("ds-sidebar-menu", className), style: style, children: [(title || onToggle) && (_jsxs("div", { className: "ds-sidebar-menu__header", children: [onToggle && (_jsxs("button", { type: "button", className: "ds-sidebar-menu__toggle", onClick: onToggle, "aria-label": "Toggle menu", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] })), title && (_jsx("span", { className: "ds-sidebar-menu__title", children: title }))] })), _jsx("ul", { className: "ds-sidebar-menu__list", children: items.map((it) => {
|
|
10
|
+
const state = it.state ?? "default";
|
|
11
|
+
const interactive = state !== "disabled" && state !== "locked";
|
|
12
|
+
const content = (_jsxs(_Fragment, { children: [_jsx("span", { className: "ds-sidebar-menu__lead", "aria-hidden": "true", children: it.icon ?? _jsx("span", { className: "ds-sidebar-menu__dot" }) }), _jsx("span", { className: "ds-sidebar-menu__label", children: it.label }), it.trailing && (_jsx("span", { className: "ds-sidebar-menu__trailing", children: it.trailing })), state === "locked" && !it.trailing && (_jsx("span", { className: "ds-sidebar-menu__lock", "aria-hidden": "true" }))] }));
|
|
13
|
+
return (_jsx("li", { className: classNames("ds-sidebar-menu__item", `ds-sidebar-menu__item--${state}`), children: interactive && it.href ? (_jsx("a", { className: "ds-sidebar-menu__row", href: it.href, onClick: it.onClick, children: content })) : interactive ? (_jsx("button", { type: "button", className: "ds-sidebar-menu__row", onClick: it.onClick, children: content })) : (_jsx("span", { className: "ds-sidebar-menu__row", "aria-disabled": "true", children: content })) }, it.key));
|
|
14
|
+
}) })] }));
|
|
15
|
+
};
|
|
16
|
+
export default SidebarMenu;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Skeleton from "react-loading-skeleton";
|
|
3
|
+
import "react-loading-skeleton/dist/skeleton.css";
|
|
4
|
+
export const SkeletonLoader = () => {
|
|
5
|
+
return (_jsxs("div", { style: { margin: "20px" }, children: [_jsx(Skeleton, { height: 35, width: 200, style: { marginBottom: 15 } }), _jsx(Skeleton, { height: 20, width: "90%" }), _jsx(Skeleton, { height: 20, width: "80%" }), _jsx(Skeleton, { height: 20, width: "70%", style: { marginBottom: 15 } }), _jsx(Skeleton, { height: 150, width: "100%" })] }));
|
|
6
|
+
};
|
|
7
|
+
export default SkeletonLoader;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/StatusBadge.css";
|
|
3
|
+
export type StatusBadgeStatus = "valid" | "expiring" | "expired" | "unlimited" | "neutral";
|
|
4
|
+
export interface StatusBadgeProps {
|
|
5
|
+
status: StatusBadgeStatus;
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
/** Renders a small leading dot in `currentColor`. */
|
|
8
|
+
withDot?: boolean;
|
|
9
|
+
/** Drops the border for a flatter look. */
|
|
10
|
+
soft?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
style?: React.CSSProperties;
|
|
13
|
+
title?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* StatusBadge — lifecycle indicator for certificate/document/assignment states.
|
|
17
|
+
* Maps `status` to `--status-*` tokens; re-theme in tokens.css.
|
|
18
|
+
*/
|
|
19
|
+
declare const StatusBadge: React.FC<StatusBadgeProps>;
|
|
20
|
+
export default StatusBadge;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import "../styles/StatusBadge.css";
|
|
4
|
+
/**
|
|
5
|
+
* StatusBadge — lifecycle indicator for certificate/document/assignment states.
|
|
6
|
+
* Maps `status` to `--status-*` tokens; re-theme in tokens.css.
|
|
7
|
+
*/
|
|
8
|
+
const StatusBadge = ({ status, children, withDot, soft, className, style, title, }) => {
|
|
9
|
+
return (_jsxs("span", { className: classNames("ds-status-badge", `ds-status-badge--${status}`, soft && "ds-status-badge--soft", className), style: style, title: title, children: [withDot && _jsx("span", { className: "ds-status-badge__dot", "aria-hidden": "true" }), children] }));
|
|
10
|
+
};
|
|
11
|
+
export default StatusBadge;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/Table.css";
|
|
3
|
+
export type TableDensity = "comfortable" | "compact";
|
|
4
|
+
export type SortDirection = "asc" | "desc" | null;
|
|
5
|
+
export interface TableProps extends React.TableHTMLAttributes<HTMLTableElement> {
|
|
6
|
+
density?: TableDensity;
|
|
7
|
+
/** Striped zebra rows. */
|
|
8
|
+
isStriped?: boolean;
|
|
9
|
+
/** Hover highlight on rows. */
|
|
10
|
+
isHoverable?: boolean;
|
|
11
|
+
/** Sticky header — relies on a scroll container set on the parent. */
|
|
12
|
+
hasStickyHeader?: boolean;
|
|
13
|
+
/** Borders between rows. Default true. */
|
|
14
|
+
hasRowBorders?: boolean;
|
|
15
|
+
}
|
|
16
|
+
declare const Table: React.FC<TableProps>;
|
|
17
|
+
export declare const TableHeader: React.FC<React.HTMLAttributes<HTMLTableSectionElement>>;
|
|
18
|
+
export declare const TableBody: React.FC<React.HTMLAttributes<HTMLTableSectionElement>>;
|
|
19
|
+
export interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
|
20
|
+
isSelected?: boolean;
|
|
21
|
+
/** When true, applies a clickable affordance (pointer + focus styles). */
|
|
22
|
+
isClickable?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare const TableRow: React.FC<TableRowProps>;
|
|
25
|
+
export interface TableCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
|
|
26
|
+
/** Numeric / monetary cell — right-aligns content. */
|
|
27
|
+
isNumeric?: boolean;
|
|
28
|
+
/** Truncate long text with ellipsis. */
|
|
29
|
+
truncate?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare const TableCell: React.FC<TableCellProps>;
|
|
32
|
+
export interface TableHeaderCellProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
|
|
33
|
+
isNumeric?: boolean;
|
|
34
|
+
/** When provided, renders a sortable column header. */
|
|
35
|
+
sortDirection?: SortDirection;
|
|
36
|
+
onSortChange?: (next: SortDirection) => void;
|
|
37
|
+
}
|
|
38
|
+
export declare const TableHeaderCell: React.FC<TableHeaderCellProps>;
|
|
39
|
+
export default Table;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import "../styles/Table.css";
|
|
4
|
+
const Table = ({ density = "comfortable", isStriped, isHoverable, hasStickyHeader, hasRowBorders = true, className, children, ...rest }) => (_jsx("table", { className: classNames("ds-table", `ds-table--${density}`, isStriped && "ds-table--striped", isHoverable && "ds-table--hoverable", hasStickyHeader && "ds-table--sticky-header", !hasRowBorders && "ds-table--no-row-borders", className), ...rest, children: children }));
|
|
5
|
+
export const TableHeader = ({ className, children, ...rest }) => (_jsx("thead", { className: classNames("ds-table__header", className), ...rest, children: children }));
|
|
6
|
+
export const TableBody = ({ className, children, ...rest }) => (_jsx("tbody", { className: classNames("ds-table__body", className), ...rest, children: children }));
|
|
7
|
+
export const TableRow = ({ isSelected, isClickable, className, children, ...rest }) => (_jsx("tr", { className: classNames("ds-table__row", isSelected && "is-selected", isClickable && "is-clickable", className), tabIndex: isClickable ? 0 : undefined, ...rest, children: children }));
|
|
8
|
+
export const TableCell = ({ isNumeric, truncate, className, children, ...rest }) => (_jsx("td", { className: classNames("ds-table__cell", isNumeric && "ds-table__cell--numeric", truncate && "ds-table__cell--truncate", className), ...rest, children: children }));
|
|
9
|
+
export const TableHeaderCell = ({ isNumeric, sortDirection, onSortChange, className, children, ...rest }) => {
|
|
10
|
+
const isSortable = onSortChange !== undefined;
|
|
11
|
+
const ariaSort = sortDirection === "asc" ? "ascending"
|
|
12
|
+
: sortDirection === "desc" ? "descending"
|
|
13
|
+
: isSortable ? "none" : undefined;
|
|
14
|
+
const cycleSort = () => {
|
|
15
|
+
if (!onSortChange)
|
|
16
|
+
return;
|
|
17
|
+
const next = sortDirection === "asc" ? "desc"
|
|
18
|
+
: sortDirection === "desc" ? null
|
|
19
|
+
: "asc";
|
|
20
|
+
onSortChange(next);
|
|
21
|
+
};
|
|
22
|
+
return (_jsx("th", { scope: "col", "aria-sort": ariaSort, className: classNames("ds-table__header-cell", isNumeric && "ds-table__cell--numeric", isSortable && "ds-table__header-cell--sortable", className), ...rest, children: isSortable ? (_jsxs("button", { type: "button", className: "ds-table__sort-trigger", onClick: cycleSort, children: [_jsx("span", { className: "ds-table__sort-label", children: children }), _jsx("span", { className: classNames("ds-table__sort-icon", sortDirection && `ds-table__sort-icon--${sortDirection}`), "aria-hidden": "true", children: sortDirection === "asc" ? "▲" : sortDirection === "desc" ? "▼" : "⇅" })] })) : children }));
|
|
23
|
+
};
|
|
24
|
+
export default Table;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/Tabs.css";
|
|
3
|
+
export type TabsVariant = "underline" | "segmented" | "pill";
|
|
4
|
+
export type TabsSize = "sm" | "md" | "lg";
|
|
5
|
+
export interface TabItem {
|
|
6
|
+
/** Stable identifier — used as the active value. */
|
|
7
|
+
id: string;
|
|
8
|
+
label: React.ReactNode;
|
|
9
|
+
/** Optional content panel rendered when this tab is active. */
|
|
10
|
+
content?: React.ReactNode;
|
|
11
|
+
/** Optional leading icon (rendered inside the trigger). */
|
|
12
|
+
icon?: React.ReactNode;
|
|
13
|
+
/** Optional trailing badge (count, dot, etc.). */
|
|
14
|
+
badge?: React.ReactNode;
|
|
15
|
+
isDisabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface TabsProps {
|
|
18
|
+
items: TabItem[];
|
|
19
|
+
/** Controlled active tab id. Omit to render uncontrolled. */
|
|
20
|
+
value?: string;
|
|
21
|
+
/** Default active tab id for uncontrolled mode. Falls back to first item. */
|
|
22
|
+
defaultValue?: string;
|
|
23
|
+
onChange?: (id: string) => void;
|
|
24
|
+
variant?: TabsVariant;
|
|
25
|
+
size?: TabsSize;
|
|
26
|
+
/** Render tab list full-width (triggers stretch). */
|
|
27
|
+
isFullWidth?: boolean;
|
|
28
|
+
className?: string;
|
|
29
|
+
style?: React.CSSProperties;
|
|
30
|
+
/** Accessible label for the tablist. */
|
|
31
|
+
"aria-label"?: string;
|
|
32
|
+
}
|
|
33
|
+
declare const Tabs: React.FC<TabsProps>;
|
|
34
|
+
export default Tabs;
|