@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@360crewing/ui` are documented here.
|
|
4
|
+
Format roughly follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## 0.1.0 — 2026-05-27
|
|
7
|
+
|
|
8
|
+
Initial public release. Extracted from `tenant-frontend/src/components`.
|
|
9
|
+
|
|
10
|
+
### Primitives
|
|
11
|
+
|
|
12
|
+
- Layout: `Page`, `Card`, `ShadowedBlock`, `LayoutBlock`, `Delimeter`
|
|
13
|
+
- Form fields (legacy + DS): `TextField`, `TextareaField`, `CheckboxField`,
|
|
14
|
+
`ToggleField`, `Checkbox`, `Toggle`
|
|
15
|
+
- Buttons: `Button`, `IconButton`, `DropdownButton`, `FormActionButtons`
|
|
16
|
+
- Feedback: `Badge`, `BadgeWithDot`, `StatusBadge`, `Tag`, `Tooltip`, `Popover`,
|
|
17
|
+
`ContentLoader`, `SkeletonLoader`
|
|
18
|
+
- Navigation: `Tabs`, `Breadcrumbs`, `SidebarMenu`, `Pagination`
|
|
19
|
+
- Other: `Avatar`, `Icon`, `SearchInput`, `FileUpload`, `DetailItem`,
|
|
20
|
+
`InformationPanel`, `CollapsibleFields`
|
|
21
|
+
|
|
22
|
+
### Foundations
|
|
23
|
+
|
|
24
|
+
- `styles/tokens.css` — colour, spacing, radius, shadow tokens
|
|
25
|
+
- `styles/typography.css` — Montserrat-based type scale
|
|
26
|
+
|
|
27
|
+
### Engineering
|
|
28
|
+
|
|
29
|
+
- Pure ESM, `type: "module"`, exports map with `./styles.css` entry
|
|
30
|
+
- `prepublishOnly` runs `tsc -p tsconfig.build.json` + copies CSS
|
|
31
|
+
- Peer-deps: react 18 || 19, react-dom 18 || 19, mobx-react-lite 4, react-icons 5
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 360crewing
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @360crewing/ui
|
|
2
|
+
|
|
3
|
+
Pure visual UI primitives for the 360crewing platform. Used by the host
|
|
4
|
+
(`tenant-frontend`) and by marketplace extensions (via
|
|
5
|
+
`@360crewing/marketplace-sdk`, which re-exports this package).
|
|
6
|
+
|
|
7
|
+
## Principles
|
|
8
|
+
|
|
9
|
+
- **Presentational components only.** No global state, managers, axios, or
|
|
10
|
+
MobX stores. Everything is driven through props.
|
|
11
|
+
- **No import-time side effects.** Importing CSS is the only exception.
|
|
12
|
+
- **Imperative platform services (Modal, Toast, Confirmation, Form) are NOT
|
|
13
|
+
here.** They live in the host as services; extensions reach them only through
|
|
14
|
+
SDK hooks (`useModal`, `useNotify`, `useConfirm`, `useForm`).
|
|
15
|
+
|
|
16
|
+
## What's included
|
|
17
|
+
|
|
18
|
+
**Forms & inputs:** `Button`, `TextField`, `TextareaField`, `CheckboxField`,
|
|
19
|
+
`ToggleField`, `DropdownButton`, `FormActionButtons`.
|
|
20
|
+
|
|
21
|
+
**Layout & presentation:** `Card`, `Page`, `LayoutBlock`, `ShadowedBlock`,
|
|
22
|
+
`Delimeter`, `InformationPanel`, `DetailItem`, `Badge` (+ `BadgeWithDot`),
|
|
23
|
+
`Icon`.
|
|
24
|
+
|
|
25
|
+
**State & navigation:** `ContentLoader`, `SkeletonLoader`, `CollapsibleFields`,
|
|
26
|
+
`Pagination`.
|
|
27
|
+
|
|
28
|
+
## i18n-aware components
|
|
29
|
+
|
|
30
|
+
`Pagination`, `CollapsibleFields`, and `FormActionButtons` accept their text
|
|
31
|
+
labels as props with English defaults. The host wraps them in thin i18n
|
|
32
|
+
adapters that call `useTranslation()` and pass localized strings down.
|
|
33
|
+
Extensions inject their own `useI18n()` from the SDK when using these
|
|
34
|
+
primitives directly.
|
|
35
|
+
|
|
36
|
+
## Styles
|
|
37
|
+
|
|
38
|
+
Import the stylesheet once at the host entry point:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import "@360crewing/ui/styles.css";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Peer dependencies
|
|
45
|
+
|
|
46
|
+
`react`, `react-dom`, `mobx-react-lite`, `react-icons` — provided by the host.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/Avatar.css";
|
|
3
|
+
export type AvatarSize = "sm" | "md" | "lg";
|
|
4
|
+
export interface AvatarProps {
|
|
5
|
+
size?: AvatarSize;
|
|
6
|
+
/** Image URL. When provided, overrides `initials`. */
|
|
7
|
+
src?: string;
|
|
8
|
+
/** Up to 2 characters shown when `src` is missing or fails. */
|
|
9
|
+
initials?: string;
|
|
10
|
+
/** Accessible label (alt text or aria-label). */
|
|
11
|
+
name?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
style?: React.CSSProperties;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Avatar — circular user/entity indicator. Mirrors Figma `Avatar` set
|
|
17
|
+
* (sm 32 / md 40 / lg 48). Falls back to initials on `Surface/Brand-Minimal`.
|
|
18
|
+
*/
|
|
19
|
+
declare const Avatar: React.FC<AvatarProps>;
|
|
20
|
+
export default Avatar;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import "../styles/Avatar.css";
|
|
4
|
+
/**
|
|
5
|
+
* Avatar — circular user/entity indicator. Mirrors Figma `Avatar` set
|
|
6
|
+
* (sm 32 / md 40 / lg 48). Falls back to initials on `Surface/Brand-Minimal`.
|
|
7
|
+
*/
|
|
8
|
+
const Avatar = ({ size = "md", src, initials, name, className, style, }) => {
|
|
9
|
+
const shortInitials = (initials ?? name ?? "")
|
|
10
|
+
.split(/\s+/)
|
|
11
|
+
.map((s) => s[0] ?? "")
|
|
12
|
+
.join("")
|
|
13
|
+
.slice(0, 2)
|
|
14
|
+
.toUpperCase();
|
|
15
|
+
return (_jsx("span", { className: classNames("ds-avatar", `ds-avatar--${size}`, className), style: style, role: "img", "aria-label": name, children: src ? (_jsx("img", { className: "ds-avatar__img", src: src, alt: name ?? "" })) : (_jsx("span", { className: "ds-avatar__initials", children: shortInitials })) }));
|
|
16
|
+
};
|
|
17
|
+
export default Avatar;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import "../styles/Badge.css";
|
|
3
|
+
export type BadgeColor = "gray" | "brand" | "error" | "warning" | "success" | "slate" | "sky" | "blue" | "indigo" | "purple" | "pink" | "orange";
|
|
4
|
+
export type BadgeSize = "sm" | "md" | "lg";
|
|
5
|
+
export interface BadgeProps {
|
|
6
|
+
color?: BadgeColor;
|
|
7
|
+
size?: BadgeSize;
|
|
8
|
+
className?: string;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
declare const Badge: ({ color, size, className, children }: BadgeProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export default Badge;
|
|
13
|
+
export interface BadgeWithDotProps {
|
|
14
|
+
color?: BadgeColor;
|
|
15
|
+
size?: BadgeSize;
|
|
16
|
+
className?: string;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
export declare const BadgeWithDot: ({ color, size, className, children }: BadgeWithDotProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import "../styles/Badge.css";
|
|
4
|
+
const Badge = ({ color = "gray", size = "md", className, children }) => {
|
|
5
|
+
return _jsx("span", { className: classNames("badge", `badge--${size}`, `badge--${color}`, className), children: children });
|
|
6
|
+
};
|
|
7
|
+
export default Badge;
|
|
8
|
+
export const BadgeWithDot = ({ color = "gray", size = "md", className, children }) => {
|
|
9
|
+
return _jsxs("span", { className: classNames("badge", "badge--with-dot", `badge--${size}`, `badge--${color}`, className), children: [_jsx("span", { className: "badge__dot", "aria-hidden": true }), children] });
|
|
10
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/Breadcrumbs.css";
|
|
3
|
+
export interface BreadcrumbItem {
|
|
4
|
+
label: React.ReactNode;
|
|
5
|
+
href?: string;
|
|
6
|
+
onClick?: (e: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => void;
|
|
7
|
+
/** Marks the current page item — rendered non-interactive. */
|
|
8
|
+
isCurrent?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface BreadcrumbsProps {
|
|
11
|
+
items: BreadcrumbItem[];
|
|
12
|
+
/** Custom separator. Defaults to `›`. */
|
|
13
|
+
separator?: React.ReactNode;
|
|
14
|
+
className?: string;
|
|
15
|
+
style?: React.CSSProperties;
|
|
16
|
+
}
|
|
17
|
+
/** Breadcrumbs — page-location trail (Figma `Breadcrumbs`). Last item is current/non-interactive; preceding are links. */
|
|
18
|
+
declare const Breadcrumbs: React.FC<BreadcrumbsProps>;
|
|
19
|
+
export default Breadcrumbs;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import "../styles/Breadcrumbs.css";
|
|
4
|
+
/** Breadcrumbs — page-location trail (Figma `Breadcrumbs`). Last item is current/non-interactive; preceding are links. */
|
|
5
|
+
const Breadcrumbs = ({ items, separator = "›", className, style, }) => {
|
|
6
|
+
return (_jsx("nav", { className: classNames("ds-breadcrumbs", className), style: style, "aria-label": "Breadcrumb", children: _jsx("ol", { className: "ds-breadcrumbs__list", children: items.map((it, i) => {
|
|
7
|
+
const isLast = i === items.length - 1;
|
|
8
|
+
const isCurrent = it.isCurrent ?? isLast;
|
|
9
|
+
return (_jsxs("li", { className: classNames("ds-breadcrumbs__item", isCurrent && "ds-breadcrumbs__item--current"), children: [isCurrent ? (_jsx("span", { className: "ds-breadcrumbs__current", "aria-current": "page", children: it.label })) : it.href ? (_jsx("a", { className: "ds-breadcrumbs__link", href: it.href, onClick: it.onClick, children: it.label })) : (_jsx("button", { type: "button", className: "ds-breadcrumbs__link", onClick: it.onClick, children: it.label })), !isLast && (_jsx("span", { className: "ds-breadcrumbs__separator", "aria-hidden": "true", children: separator }))] }, i));
|
|
10
|
+
}) }) }));
|
|
11
|
+
};
|
|
12
|
+
export default Breadcrumbs;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/Button.css";
|
|
3
|
+
export type ButtonVariant = "primary" | "secondary" | "tertiary" | "edit" | "additional" | "danger" | "ghost";
|
|
4
|
+
export type ButtonSize = "sm" | "md" | "lg";
|
|
5
|
+
interface IDefaultButtonProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>;
|
|
8
|
+
isDisabled?: boolean;
|
|
9
|
+
style?: React.CSSProperties;
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
ref?: React.RefObject<HTMLButtonElement>;
|
|
12
|
+
type?: "button" | "submit" | "reset";
|
|
13
|
+
preventDoubleClick?: boolean;
|
|
14
|
+
cooldownMs?: number;
|
|
15
|
+
lockWhilePending?: boolean;
|
|
16
|
+
/** DS variant → `.ds-button .ds-button--<variant> .ds-button--<size>`.
|
|
17
|
+
* Omitted → legacy `.button` class, so existing `className` callers keep working. */
|
|
18
|
+
variant?: ButtonVariant;
|
|
19
|
+
size?: ButtonSize;
|
|
20
|
+
fullWidth?: boolean;
|
|
21
|
+
iconBefore?: React.ReactNode;
|
|
22
|
+
iconAfter?: React.ReactNode;
|
|
23
|
+
isLoading?: boolean;
|
|
24
|
+
/** Accessibility — required if the button only contains an icon. */
|
|
25
|
+
"aria-label"?: string;
|
|
26
|
+
}
|
|
27
|
+
interface IButtonWithTitle extends IDefaultButtonProps {
|
|
28
|
+
title: string;
|
|
29
|
+
}
|
|
30
|
+
interface IButtonWithChildren extends IDefaultButtonProps {
|
|
31
|
+
title?: string;
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
}
|
|
34
|
+
export type ButtonProps = IButtonWithTitle | IButtonWithChildren;
|
|
35
|
+
declare const Button: (({ title, className, onClick, isDisabled, style, children, ref, type, preventDoubleClick, cooldownMs, lockWhilePending, variant, size, fullWidth, iconBefore, iconAfter, isLoading, "aria-label": ariaLabel, }: ButtonProps) => import("react/jsx-runtime").JSX.Element) & {
|
|
36
|
+
displayName: string;
|
|
37
|
+
};
|
|
38
|
+
export default Button;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import { observer } from "mobx-react-lite";
|
|
4
|
+
import { useRef } from "react";
|
|
5
|
+
import { ClipLoader } from "react-spinners";
|
|
6
|
+
import "../styles/Button.css";
|
|
7
|
+
const Button = observer(({ title, className, onClick, isDisabled, style, children, ref, type, preventDoubleClick = false, cooldownMs = 600, lockWhilePending = true, variant, size = "md", fullWidth, iconBefore, iconAfter, isLoading, "aria-label": ariaLabel, }) => {
|
|
8
|
+
const lockedRef = useRef(false);
|
|
9
|
+
const timerRef = useRef(null);
|
|
10
|
+
const releaseLockAfterCooldown = () => {
|
|
11
|
+
if (timerRef.current) {
|
|
12
|
+
window.clearTimeout(timerRef.current);
|
|
13
|
+
timerRef.current = null;
|
|
14
|
+
}
|
|
15
|
+
timerRef.current = window.setTimeout(() => {
|
|
16
|
+
lockedRef.current = false;
|
|
17
|
+
timerRef.current = null;
|
|
18
|
+
}, Math.max(0, cooldownMs || 0));
|
|
19
|
+
};
|
|
20
|
+
const onClickHandler = (e) => {
|
|
21
|
+
if (isDisabled || isLoading)
|
|
22
|
+
return;
|
|
23
|
+
if (preventDoubleClick) {
|
|
24
|
+
if (lockedRef.current)
|
|
25
|
+
return;
|
|
26
|
+
lockedRef.current = true;
|
|
27
|
+
}
|
|
28
|
+
const maybePromise = onClick?.(e);
|
|
29
|
+
if (preventDoubleClick) {
|
|
30
|
+
const isPromise = maybePromise && typeof maybePromise.then === "function";
|
|
31
|
+
if (isPromise && lockWhilePending !== false) {
|
|
32
|
+
maybePromise
|
|
33
|
+
.finally(() => {
|
|
34
|
+
releaseLockAfterCooldown();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
releaseLockAfterCooldown();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const effectiveDisabled = !!isDisabled || (!!preventDoubleClick && lockedRef.current);
|
|
43
|
+
const useDs = !!variant;
|
|
44
|
+
const composedClassName = classNames(
|
|
45
|
+
// Legacy `.button` only off the DS API; mixing both re-applies the old
|
|
46
|
+
// `min-width:100px; height:20px` reset and breaks DS sizing.
|
|
47
|
+
!useDs && "button", useDs && "ds-button", useDs && `ds-button--${variant}`, useDs && `ds-button--${size}`, useDs && fullWidth && "ds-button--full-width", useDs && isLoading && "is-loading", { disabled: effectiveDisabled, "is-disabled": useDs && effectiveDisabled }, className);
|
|
48
|
+
return (_jsxs("button", { ref: ref, className: composedClassName, onClick: onClickHandler, style: style, type: type, disabled: effectiveDisabled, "aria-label": ariaLabel, "aria-busy": isLoading || undefined, children: [useDs && iconBefore && (_jsx("span", { className: "ds-button__icon", "aria-hidden": "true", children: iconBefore })), children ?? title, useDs && iconAfter && (_jsx("span", { className: "ds-button__icon", "aria-hidden": "true", children: iconAfter })), useDs && isLoading && (_jsx(ClipLoader, { size: 14, color: "currentColor", cssOverride: { position: "absolute" } }))] }));
|
|
49
|
+
});
|
|
50
|
+
export default Button;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/Card.css";
|
|
3
|
+
export interface CardProps {
|
|
4
|
+
title?: React.ReactNode;
|
|
5
|
+
actions?: React.ReactNode;
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: React.CSSProperties;
|
|
9
|
+
padding?: "none" | "sm" | "md" | "lg";
|
|
10
|
+
}
|
|
11
|
+
declare const Card: React.FC<CardProps>;
|
|
12
|
+
export default Card;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "../styles/Card.css";
|
|
3
|
+
const Card = ({ title, actions, children, className, style, padding = "md", }) => {
|
|
4
|
+
return (_jsxs("div", { className: `crewing-card crewing-card--pad-${padding} ${className || ""}`.trim(), style: style, children: [(title || actions) && (_jsxs("div", { className: "crewing-card__header", children: [title && _jsx("div", { className: "crewing-card__title", children: title }), actions && _jsx("div", { className: "crewing-card__actions", children: actions })] })), _jsx("div", { className: "crewing-card__body", children: children })] }));
|
|
5
|
+
};
|
|
6
|
+
export default Card;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/Checkbox.css";
|
|
3
|
+
export type CheckboxSize = "sm" | "md";
|
|
4
|
+
export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "type"> {
|
|
5
|
+
size?: CheckboxSize;
|
|
6
|
+
/** Renders the field in an error state (red border, optional `errorText`). */
|
|
7
|
+
isError?: boolean;
|
|
8
|
+
/** Label rendered next to the checkbox. */
|
|
9
|
+
label?: React.ReactNode;
|
|
10
|
+
/** Wrapper class applied to the outer <label>. */
|
|
11
|
+
wrapperClassName?: string;
|
|
12
|
+
}
|
|
13
|
+
declare const Checkbox: React.ForwardRefExoticComponent<CheckboxProps & React.RefAttributes<HTMLInputElement>>;
|
|
14
|
+
export default Checkbox;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import "../styles/Checkbox.css";
|
|
5
|
+
const Checkbox = forwardRef(({ size = "md", isError, label, className, wrapperClassName, disabled, ...inputProps }, ref) => {
|
|
6
|
+
return (_jsxs("label", { className: classNames("ds-checkbox", `ds-checkbox--${size}`, isError && "ds-checkbox--error", disabled && "ds-checkbox--disabled", wrapperClassName), children: [_jsx("input", { ...inputProps, ref: ref, type: "checkbox", disabled: disabled, "aria-invalid": isError || undefined, className: classNames("ds-checkbox__input", className) }), label !== undefined && (_jsx("span", { className: "ds-checkbox__label", children: label }))] }));
|
|
7
|
+
});
|
|
8
|
+
Checkbox.displayName = "Checkbox";
|
|
9
|
+
export default Checkbox;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/CheckboxField.css";
|
|
3
|
+
import "../styles/Error.css";
|
|
4
|
+
export interface CheckboxFieldProps {
|
|
5
|
+
label?: string | React.ReactNode;
|
|
6
|
+
value?: boolean;
|
|
7
|
+
onChange?: (checked: boolean) => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
isDisabled?: boolean;
|
|
10
|
+
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
|
11
|
+
error?: string | {
|
|
12
|
+
message: string;
|
|
13
|
+
} | null;
|
|
14
|
+
}
|
|
15
|
+
declare const CheckboxField: React.FC<CheckboxFieldProps>;
|
|
16
|
+
export default CheckboxField;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { observer } from "mobx-react-lite";
|
|
3
|
+
import "../styles/CheckboxField.css";
|
|
4
|
+
import "../styles/Error.css";
|
|
5
|
+
const CheckboxField = observer(({ label, value = false, onChange, className, isDisabled = false, inputProps, error = null, }) => {
|
|
6
|
+
const withStop = (handler) => (e) => {
|
|
7
|
+
e.stopPropagation();
|
|
8
|
+
if (handler)
|
|
9
|
+
handler(e);
|
|
10
|
+
};
|
|
11
|
+
const handleChange = (e) => {
|
|
12
|
+
e.stopPropagation();
|
|
13
|
+
onChange?.(e.target.checked);
|
|
14
|
+
};
|
|
15
|
+
return (_jsxs("div", { className: `checkbox-field ${className || ""} ${error ? "error" : ""}`, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onDoubleClick: (e) => e.stopPropagation(), children: [_jsx("input", { type: "checkbox", checked: value, disabled: isDisabled, ...inputProps, onChange: handleChange, onClick: withStop(inputProps?.onClick), onMouseDown: withStop(inputProps?.onMouseDown), onMouseUp: withStop(inputProps?.onMouseUp), onDoubleClick: withStop(inputProps?.onDoubleClick), onKeyDown: withStop(inputProps?.onKeyDown) }), label && _jsx("span", { className: "label", children: label }), error && (_jsx("div", { className: "error-text", children: typeof error === "string" ? error : error.message }))] }));
|
|
16
|
+
});
|
|
17
|
+
export default CheckboxField;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/CollapsibleFields.css";
|
|
3
|
+
export interface CollapsibleFieldsProps {
|
|
4
|
+
label?: string;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
defaultExpanded?: boolean;
|
|
7
|
+
}
|
|
8
|
+
declare const CollapsibleFields: React.FC<CollapsibleFieldsProps>;
|
|
9
|
+
export default CollapsibleFields;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import "../styles/CollapsibleFields.css";
|
|
4
|
+
const CollapsibleFields = ({ label = "Show full details", children, defaultExpanded = false, }) => {
|
|
5
|
+
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
|
6
|
+
return (_jsxs("div", { className: "collapsible-fields", children: [_jsxs("button", { type: "button", className: "collapsible-toggle", onClick: () => setIsExpanded(!isExpanded), children: [_jsx("span", { className: `arrow ${isExpanded ? "expanded" : ""}`, children: "\u25B6" }), label] }), isExpanded && (_jsx("div", { className: "collapsible-content", children: children }))] }));
|
|
7
|
+
};
|
|
8
|
+
export default CollapsibleFields;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ClipLoader } from "react-spinners";
|
|
3
|
+
const ContentLoader = ({ color = "#36d7b7", size = 50, minHeight = "60vh", }) => {
|
|
4
|
+
return (_jsx("div", { style: {
|
|
5
|
+
display: "flex",
|
|
6
|
+
justifyContent: "center",
|
|
7
|
+
alignItems: "center",
|
|
8
|
+
width: "100%",
|
|
9
|
+
height: "100%",
|
|
10
|
+
minHeight,
|
|
11
|
+
flex: 1,
|
|
12
|
+
}, children: _jsx(ClipLoader, { color: color, size: size }) }));
|
|
13
|
+
};
|
|
14
|
+
export default ContentLoader;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "../styles/DetailItem.css";
|
|
3
|
+
export const DetailItem = ({ title, value }) => {
|
|
4
|
+
return (_jsxs("div", { className: "detail-item", children: [_jsx("span", { children: title }), _jsx("span", { className: "dots" }), typeof value === "string" ? _jsx("span", { children: value }) : value] }));
|
|
5
|
+
};
|
|
6
|
+
export default DetailItem;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/DropdownButton.css";
|
|
3
|
+
export interface DropdownOption {
|
|
4
|
+
label: string;
|
|
5
|
+
onClick: () => void;
|
|
6
|
+
}
|
|
7
|
+
export interface DropdownButtonProps {
|
|
8
|
+
options: DropdownOption[];
|
|
9
|
+
className?: string;
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
onClick?: () => void;
|
|
12
|
+
isDisabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
declare const DropdownButton: React.FC<DropdownButtonProps>;
|
|
15
|
+
export default DropdownButton;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import classNames from "classnames";
|
|
4
|
+
import { LuChevronDown } from "react-icons/lu";
|
|
5
|
+
import Button from "./Button";
|
|
6
|
+
import "../styles/DropdownButton.css";
|
|
7
|
+
const DropdownButton = ({ options, className, children, onClick, isDisabled, }) => {
|
|
8
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
9
|
+
const containerRef = useRef(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const handleClickOutside = (event) => {
|
|
12
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
13
|
+
setIsOpen(false);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
17
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
18
|
+
}, []);
|
|
19
|
+
if (options.length <= 1) {
|
|
20
|
+
const singleOnClick = options.length === 1 ? options[0].onClick : onClick;
|
|
21
|
+
return (_jsx(Button, { className: className, onClick: singleOnClick, isDisabled: isDisabled, children: children }));
|
|
22
|
+
}
|
|
23
|
+
return (_jsxs("div", { className: classNames("dropdown-button-container", className, { disabled: isDisabled }), ref: containerRef, onClick: () => !isDisabled && setIsOpen(!isOpen), children: [_jsxs("div", { className: "button-group", children: [_jsx("div", { className: "main-button-content", children: children }), _jsx("div", { className: "dropdown-toggle", children: _jsx(LuChevronDown, {}) })] }), isOpen && (_jsx("div", { className: "dropdown-menu", children: options.map((option, index) => (_jsx("div", { className: "dropdown-item", onClick: (e) => {
|
|
24
|
+
e.stopPropagation();
|
|
25
|
+
option.onClick();
|
|
26
|
+
setIsOpen(false);
|
|
27
|
+
}, children: option.label }, index))) }))] }));
|
|
28
|
+
};
|
|
29
|
+
export default DropdownButton;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "../styles/FileUpload.css";
|
|
3
|
+
export type FileUploadSize = "md" | "lg";
|
|
4
|
+
export type FileUploadState = "default" | "error" | "success";
|
|
5
|
+
export interface FileUploadProps {
|
|
6
|
+
/** Native MIME / extension list. Defaults to images. */
|
|
7
|
+
accept?: string;
|
|
8
|
+
/** Allow selecting multiple files. */
|
|
9
|
+
multiple?: boolean;
|
|
10
|
+
/** Disabled state. */
|
|
11
|
+
isDisabled?: boolean;
|
|
12
|
+
/** "lg" matches Figma "Image upload / Big"; "md" matches "Medium". */
|
|
13
|
+
size?: FileUploadSize;
|
|
14
|
+
state?: FileUploadState;
|
|
15
|
+
/** Heading text inside the drop zone. */
|
|
16
|
+
label?: React.ReactNode;
|
|
17
|
+
/** Sub-line beneath the label (file-size hint, allowed formats, …). */
|
|
18
|
+
helpText?: React.ReactNode;
|
|
19
|
+
/** Custom icon (defaults to a paperclip glyph). */
|
|
20
|
+
icon?: React.ReactNode;
|
|
21
|
+
/** Fired for accepted files (drag-drop OR click). */
|
|
22
|
+
onFilesSelected?: (files: File[]) => void;
|
|
23
|
+
/** Max size per file in bytes — files exceeding are rejected and reported via onReject. */
|
|
24
|
+
maxSizeBytes?: number;
|
|
25
|
+
onReject?: (reason: "size" | "type", file: File) => void;
|
|
26
|
+
className?: string;
|
|
27
|
+
style?: React.CSSProperties;
|
|
28
|
+
id?: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
}
|
|
31
|
+
declare const FileUpload: React.FC<FileUploadProps>;
|
|
32
|
+
export default FileUpload;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import { useCallback, useId, useRef, useState } from "react";
|
|
4
|
+
import "../styles/FileUpload.css";
|
|
5
|
+
const DefaultUploadIcon = () => (_jsxs("svg", { viewBox: "0 0 24 24", width: "32", height: "32", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M12 16V4" }), _jsx("path", { d: "M7 9l5-5 5 5" }), _jsx("path", { d: "M5 20h14" })] }));
|
|
6
|
+
const matchesAccept = (accept, file) => {
|
|
7
|
+
if (!accept)
|
|
8
|
+
return true;
|
|
9
|
+
const tokens = accept.split(",").map(t => t.trim().toLowerCase()).filter(Boolean);
|
|
10
|
+
if (tokens.length === 0)
|
|
11
|
+
return true;
|
|
12
|
+
const fname = file.name.toLowerCase();
|
|
13
|
+
const ftype = file.type.toLowerCase();
|
|
14
|
+
return tokens.some(tok => {
|
|
15
|
+
if (tok.startsWith("."))
|
|
16
|
+
return fname.endsWith(tok);
|
|
17
|
+
if (tok.endsWith("/*"))
|
|
18
|
+
return ftype.startsWith(tok.slice(0, -1));
|
|
19
|
+
return ftype === tok;
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
const FileUpload = ({ accept = "image/*", multiple, isDisabled, size = "lg", state = "default", label = "Click or drop files to upload", helpText, icon, onFilesSelected, maxSizeBytes, onReject, className, style, id, name, }) => {
|
|
23
|
+
const localId = useId();
|
|
24
|
+
const inputId = id ?? `ds-fileupload-${localId}`;
|
|
25
|
+
const inputRef = useRef(null);
|
|
26
|
+
const [isDragging, setDragging] = useState(false);
|
|
27
|
+
const handleFiles = useCallback((list) => {
|
|
28
|
+
if (!list || list.length === 0)
|
|
29
|
+
return;
|
|
30
|
+
const accepted = [];
|
|
31
|
+
for (let i = 0; i < list.length; i++) {
|
|
32
|
+
const f = list.item(i);
|
|
33
|
+
if (!f)
|
|
34
|
+
continue;
|
|
35
|
+
if (!matchesAccept(accept, f)) {
|
|
36
|
+
onReject?.("type", f);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (maxSizeBytes !== undefined && f.size > maxSizeBytes) {
|
|
40
|
+
onReject?.("size", f);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
accepted.push(f);
|
|
44
|
+
}
|
|
45
|
+
if (accepted.length)
|
|
46
|
+
onFilesSelected?.(accepted);
|
|
47
|
+
}, [accept, maxSizeBytes, onFilesSelected, onReject]);
|
|
48
|
+
const onDrop = (e) => {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
e.stopPropagation();
|
|
51
|
+
setDragging(false);
|
|
52
|
+
if (isDisabled)
|
|
53
|
+
return;
|
|
54
|
+
handleFiles(e.dataTransfer?.files ?? null);
|
|
55
|
+
};
|
|
56
|
+
const onDragOver = (e) => {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
e.stopPropagation();
|
|
59
|
+
if (isDisabled)
|
|
60
|
+
return;
|
|
61
|
+
setDragging(true);
|
|
62
|
+
};
|
|
63
|
+
const onDragLeave = (e) => {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
e.stopPropagation();
|
|
66
|
+
setDragging(false);
|
|
67
|
+
};
|
|
68
|
+
const onChange = (e) => {
|
|
69
|
+
handleFiles(e.target.files);
|
|
70
|
+
// Reset so picking the same file twice still fires onChange
|
|
71
|
+
e.target.value = "";
|
|
72
|
+
};
|
|
73
|
+
return (_jsxs("label", { htmlFor: inputId, className: classNames("ds-file-upload", `ds-file-upload--${size}`, `ds-file-upload--${state}`, isDragging && "is-dragging", isDisabled && "is-disabled", className), style: style, onDrop: onDrop, onDragOver: onDragOver, onDragLeave: onDragLeave, children: [_jsx("input", { ref: inputRef, id: inputId, name: name, type: "file", accept: accept, multiple: multiple, disabled: isDisabled, onChange: onChange, className: "ds-file-upload__input" }), _jsx("span", { className: "ds-file-upload__icon", "aria-hidden": "true", children: icon ?? _jsx(DefaultUploadIcon, {}) }), _jsx("span", { className: "ds-file-upload__label", children: label }), helpText && (_jsx("span", { className: "ds-file-upload__help", children: helpText }))] }));
|
|
74
|
+
};
|
|
75
|
+
export default FileUpload;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface FormActionButtonsProps {
|
|
3
|
+
isEditing: boolean;
|
|
4
|
+
onSaveEdit: () => Promise<void> | void;
|
|
5
|
+
onDelete?: () => Promise<void> | void;
|
|
6
|
+
isDisabled?: boolean;
|
|
7
|
+
addButtonText?: string;
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
variant?: "default" | "save-only" | "none";
|
|
10
|
+
/** Label of the save button when editing, default "Save". */
|
|
11
|
+
saveLabel?: string;
|
|
12
|
+
/** Label of the delete button when editing, default "Delete". */
|
|
13
|
+
deleteLabel?: string;
|
|
14
|
+
/** Label of the add button when creating, default "Add" (overridden by `addButtonText`). */
|
|
15
|
+
addLabel?: string;
|
|
16
|
+
}
|
|
17
|
+
declare const FormActionButtons: React.FC<FormActionButtonsProps>;
|
|
18
|
+
export default FormActionButtons;
|