@idyeah/diya 0.0.0-stage → 1.0.0
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 +317 -0
- package/LICENSE +21 -0
- package/NOTICE +29 -0
- package/README.md +599 -2
- package/dist/DiyaProvider.d.ts +79 -0
- package/dist/DiyaProvider.js +77 -0
- package/dist/Icon.d.ts +10 -0
- package/dist/Icon.js +27 -0
- package/dist/accordion/Accordion.d.ts +34 -0
- package/dist/accordion/Accordion.js +15 -0
- package/dist/alert/Alert.d.ts +49 -0
- package/dist/alert/Alert.js +11 -0
- package/dist/alert/messages.d.ts +9 -0
- package/dist/alert/messages.js +2 -0
- package/dist/api.json +7277 -0
- package/dist/api.schema.json +214 -0
- package/dist/application-patterns/Chatbot.d.ts +114 -0
- package/dist/application-patterns/Chatbot.js +110 -0
- package/dist/application-patterns/SideNavigation.d.ts +74 -0
- package/dist/application-patterns/SideNavigation.js +31 -0
- package/dist/application-patterns/chatbot-messages.d.ts +20 -0
- package/dist/application-patterns/chatbot-messages.js +2 -0
- package/dist/application-patterns/side-navigation-messages.d.ts +5 -0
- package/dist/application-patterns/side-navigation-messages.js +2 -0
- package/dist/avatar/Avatar.d.ts +32 -0
- package/dist/avatar/Avatar.js +19 -0
- package/dist/avatar/messages.d.ts +7 -0
- package/dist/avatar/messages.js +2 -0
- package/dist/badge/Badge.d.ts +42 -0
- package/dist/badge/Badge.js +16 -0
- package/dist/badge/badge-utils.d.ts +6 -0
- package/dist/badge/badge-utils.js +24 -0
- package/dist/badge/messages.d.ts +4 -0
- package/dist/badge/messages.js +2 -0
- package/dist/breadcrumbs/Breadcrumbs.d.ts +34 -0
- package/dist/breadcrumbs/Breadcrumbs.js +19 -0
- package/dist/breadcrumbs/messages.d.ts +5 -0
- package/dist/breadcrumbs/messages.js +2 -0
- package/dist/button/Button.d.ts +46 -0
- package/dist/button/Button.js +44 -0
- package/dist/calendar/Calendar.d.ts +55 -0
- package/dist/calendar/Calendar.js +61 -0
- package/dist/calendar/messages.d.ts +8 -0
- package/dist/calendar/messages.js +2 -0
- package/dist/calendar/month.d.ts +61 -0
- package/dist/calendar/month.js +134 -0
- package/dist/card/Card.d.ts +46 -0
- package/dist/card/Card.js +8 -0
- package/dist/choice/Choice.d.ts +72 -0
- package/dist/choice/Choice.js +68 -0
- package/dist/choice/messages.d.ts +4 -0
- package/dist/choice/messages.js +2 -0
- package/dist/code-block/CodeBlock.d.ts +20 -0
- package/dist/code-block/CodeBlock.js +49 -0
- package/dist/code-block/messages.d.ts +8 -0
- package/dist/code-block/messages.js +2 -0
- package/dist/combobox/Combobox.d.ts +60 -0
- package/dist/combobox/Combobox.js +160 -0
- package/dist/combobox/messages.d.ts +9 -0
- package/dist/combobox/messages.js +2 -0
- package/dist/combobox/useAnchoredPopover.d.ts +5 -0
- package/dist/combobox/useAnchoredPopover.js +100 -0
- package/dist/data-viz/DataViz.d.ts +140 -0
- package/dist/data-viz/DataViz.js +469 -0
- package/dist/data-viz/index.d.ts +2 -0
- package/dist/data-viz/index.js +1 -0
- package/dist/data-viz/messages.d.ts +11 -0
- package/dist/data-viz/messages.js +2 -0
- package/dist/date-picker/DatePicker.d.ts +57 -0
- package/dist/date-picker/DatePicker.js +146 -0
- package/dist/date-picker/messages.d.ts +13 -0
- package/dist/date-picker/messages.js +2 -0
- package/dist/dialog/Dialog.d.ts +84 -0
- package/dist/dialog/Dialog.js +107 -0
- package/dist/dialog/messages.d.ts +9 -0
- package/dist/dialog/messages.js +2 -0
- package/dist/fonts/DIYASans-Italic.woff2 +0 -0
- package/dist/fonts/DIYASans.woff2 +0 -0
- package/dist/fonts/OFL.txt +94 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +46 -0
- package/dist/internal/brand.d.ts +33 -0
- package/dist/internal/brand.js +57 -0
- package/dist/internal/cx.d.ts +2 -0
- package/dist/internal/cx.js +4 -0
- package/dist/internal/link.d.ts +14 -0
- package/dist/internal/link.js +7 -0
- package/dist/internal/slot.d.ts +15 -0
- package/dist/internal/slot.js +67 -0
- package/dist/layout/Inline.d.ts +4 -0
- package/dist/layout/Inline.js +7 -0
- package/dist/layout/Stack.d.ts +4 -0
- package/dist/layout/Stack.js +7 -0
- package/dist/layout/layout.d.ts +32 -0
- package/dist/layout/layout.js +12 -0
- package/dist/link/TextLink.d.ts +23 -0
- package/dist/link/TextLink.js +17 -0
- package/dist/list/List.d.ts +48 -0
- package/dist/list/List.js +17 -0
- package/dist/list/messages.d.ts +4 -0
- package/dist/list/messages.js +2 -0
- package/dist/menu/Menu.d.ts +49 -0
- package/dist/menu/Menu.js +54 -0
- package/dist/messages-defaults.d.ts +201 -0
- package/dist/messages-defaults.js +61 -0
- package/dist/messages.d.ts +12 -0
- package/dist/messages.js +1 -0
- package/dist/metric/Metric.d.ts +42 -0
- package/dist/metric/Metric.js +11 -0
- package/dist/metric/messages.d.ts +9 -0
- package/dist/metric/messages.js +2 -0
- package/dist/overlay/FloatingSurface.d.ts +29 -0
- package/dist/overlay/FloatingSurface.js +254 -0
- package/dist/page-header/PageHeader.d.ts +38 -0
- package/dist/page-header/PageHeader.js +10 -0
- package/dist/pagination/Pagination.d.ts +44 -0
- package/dist/pagination/Pagination.js +26 -0
- package/dist/pagination/messages.d.ts +11 -0
- package/dist/pagination/messages.js +2 -0
- package/dist/popover/Popover.d.ts +23 -0
- package/dist/popover/Popover.js +8 -0
- package/dist/progress/Progress.d.ts +33 -0
- package/dist/progress/Progress.js +14 -0
- package/dist/progress/messages.d.ts +4 -0
- package/dist/progress/messages.js +2 -0
- package/dist/prose/Prose.d.ts +14 -0
- package/dist/prose/Prose.js +7 -0
- package/dist/rating/Rating.d.ts +32 -0
- package/dist/rating/Rating.js +27 -0
- package/dist/rating/messages.d.ts +6 -0
- package/dist/rating/messages.js +2 -0
- package/dist/search/Search.d.ts +11 -0
- package/dist/search/Search.js +9 -0
- package/dist/select/Select.d.ts +65 -0
- package/dist/select/Select.js +19 -0
- package/dist/select/messages.d.ts +4 -0
- package/dist/select/messages.js +2 -0
- package/dist/site-header/SiteHeader.d.ts +88 -0
- package/dist/site-header/SiteHeader.js +211 -0
- package/dist/site-header/messages.d.ts +8 -0
- package/dist/site-header/messages.js +2 -0
- package/dist/slider/Slider.d.ts +54 -0
- package/dist/slider/Slider.js +89 -0
- package/dist/slider/messages.d.ts +5 -0
- package/dist/slider/messages.js +2 -0
- package/dist/styles.css +3379 -0
- package/dist/switch/Switch.d.ts +47 -0
- package/dist/switch/Switch.js +18 -0
- package/dist/table/Table.d.ts +99 -0
- package/dist/table/Table.js +158 -0
- package/dist/table/messages.d.ts +23 -0
- package/dist/table/messages.js +2 -0
- package/dist/tabs/Tabs.d.ts +49 -0
- package/dist/tabs/Tabs.js +26 -0
- package/dist/tag/Tag.d.ts +37 -0
- package/dist/tag/Tag.js +20 -0
- package/dist/tag/messages.d.ts +4 -0
- package/dist/tag/messages.js +2 -0
- package/dist/text-area/TextArea.d.ts +47 -0
- package/dist/text-area/TextArea.js +19 -0
- package/dist/text-input/TextInput.d.ts +53 -0
- package/dist/text-input/TextInput.js +35 -0
- package/dist/text-input/messages.d.ts +4 -0
- package/dist/text-input/messages.js +3 -0
- package/dist/text-input/native-field.d.ts +15 -0
- package/dist/text-input/native-field.js +56 -0
- package/dist/toast/Toast.d.ts +24 -0
- package/dist/toast/Toast.js +79 -0
- package/dist/toast/messages.d.ts +4 -0
- package/dist/toast/messages.js +2 -0
- package/dist/tooltip/Tooltip.d.ts +22 -0
- package/dist/tooltip/Tooltip.js +92 -0
- package/dist/workflow/ContentState.d.ts +44 -0
- package/dist/workflow/ContentState.js +11 -0
- package/dist/workflow/Drawer.d.ts +48 -0
- package/dist/workflow/Drawer.js +51 -0
- package/dist/workflow/FileUpload.d.ts +67 -0
- package/dist/workflow/FileUpload.js +47 -0
- package/dist/workflow/Form.d.ts +52 -0
- package/dist/workflow/Form.js +16 -0
- package/dist/workflow/Wizard.d.ts +76 -0
- package/dist/workflow/Wizard.js +31 -0
- package/dist/workflow/drawer-messages.d.ts +4 -0
- package/dist/workflow/drawer-messages.js +2 -0
- package/dist/workflow/file-upload-messages.d.ts +11 -0
- package/dist/workflow/file-upload-messages.js +2 -0
- package/dist/workflow/form-messages.d.ts +5 -0
- package/dist/workflow/form-messages.js +2 -0
- package/dist/workflow/wizard-messages.d.ts +9 -0
- package/dist/workflow/wizard-messages.js +2 -0
- package/package.json +84 -4
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useDiyaMessages } from '../DiyaProvider.js';
|
|
5
|
+
import { cx } from '../internal/cx.js';
|
|
6
|
+
import { avatarMessages } from './messages.js';
|
|
7
|
+
/** A person's picture or initials, with an optional presence dot. */
|
|
8
|
+
export const Avatar = forwardRef(function Avatar({ name, src, size = 'medium', shape = 'circle', status, className, ...props }, ref) {
|
|
9
|
+
const t = useDiyaMessages('avatar', avatarMessages);
|
|
10
|
+
const [failed, setFailed] = useState();
|
|
11
|
+
const image = useRef(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
// A cached failure may finish before hydration can attach the error handler.
|
|
14
|
+
if (src && image.current?.complete && image.current.naturalWidth === 0)
|
|
15
|
+
setFailed(src);
|
|
16
|
+
}, [src]);
|
|
17
|
+
const initials = name.trim().split(/\s+/).filter(Boolean).slice(0, 2).map((word) => Array.from(word)[0]).join('').toUpperCase();
|
|
18
|
+
return _jsxs("span", { ...props, ref: ref, className: cx('diya-avatar', className), "data-size": size, "data-shape": shape, role: "img", "aria-label": `${name || t.user}${status ? `, ${t[status]}` : ''}`, children: [src && failed !== src ? _jsx("img", { ref: image, src: src, alt: "", onError: () => setFailed(src) }) : initials ? _jsx("span", { "aria-hidden": "true", children: initials }) : _jsxs("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [_jsx("circle", { cx: "12", cy: "8", r: "4" }), _jsx("path", { d: "M4 21v-2a8 8 0 0 1 16 0v2" })] }), status && _jsx("span", { className: "diya-avatar-status", "data-status": status, "aria-hidden": "true" })] });
|
|
19
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
import { badgeMessages } from './messages.js';
|
|
3
|
+
declare module '../messages.js' {
|
|
4
|
+
interface DiyaMessages {
|
|
5
|
+
badge: typeof badgeMessages;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** Status vocabulary shared by Badge, Tag, Alert and Toast: 'neutral' plain, 'brand' your brand color, 'success' done, 'warning' needs care, 'error' failed, 'info' for information. */
|
|
9
|
+
export type Tone = 'neutral' | 'brand' | 'success' | 'warning' | 'error' | 'info';
|
|
10
|
+
export type BadgeProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
|
|
11
|
+
/** Required, so a Badge never guesses: 'count' shows `count` as a number; 'dot' is a small dot with no text. */
|
|
12
|
+
variant: 'count' | 'dot';
|
|
13
|
+
/** Badge size. Defaults to 'medium'. */
|
|
14
|
+
size?: 'small' | 'medium' | 'large';
|
|
15
|
+
/** Color meaning; see `Tone`. 'brand' follows the provider's brand color. Defaults to 'neutral'. */
|
|
16
|
+
tone?: Tone;
|
|
17
|
+
/** Whole number shown by a 'count' badge; a count of 0 hides the badge unless `showZero` is set. */
|
|
18
|
+
count?: number;
|
|
19
|
+
/** Highest number shown; larger counts show as '{max}+', such as '99+', while the accessible name keeps the real count. Defaults to 99. */
|
|
20
|
+
max?: number;
|
|
21
|
+
/** Keeps a 'count' badge visible at 0. */
|
|
22
|
+
showZero?: boolean;
|
|
23
|
+
/** Accessible name, never shown: what the dot means, or what is counted, read after the number ("3 unread messages"). For visible text, use Tag. */
|
|
24
|
+
label: string;
|
|
25
|
+
};
|
|
26
|
+
/** A small count (`variant="count"`) or dot (`variant="dot"`) that flags status or new items. Its `label` is announced, never shown. */
|
|
27
|
+
export declare const Badge: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLSpanElement>, "children"> & {
|
|
28
|
+
/** Required, so a Badge never guesses: 'count' shows `count` as a number; 'dot' is a small dot with no text. */
|
|
29
|
+
variant: "count" | "dot";
|
|
30
|
+
/** Badge size. Defaults to 'medium'. */
|
|
31
|
+
size?: "small" | "medium" | "large";
|
|
32
|
+
/** Color meaning; see `Tone`. 'brand' follows the provider's brand color. Defaults to 'neutral'. */
|
|
33
|
+
tone?: Tone;
|
|
34
|
+
/** Whole number shown by a 'count' badge; a count of 0 hides the badge unless `showZero` is set. */
|
|
35
|
+
count?: number;
|
|
36
|
+
/** Highest number shown; larger counts show as '{max}+', such as '99+', while the accessible name keeps the real count. Defaults to 99. */
|
|
37
|
+
max?: number;
|
|
38
|
+
/** Keeps a 'count' badge visible at 0. */
|
|
39
|
+
showZero?: boolean;
|
|
40
|
+
/** Accessible name, never shown: what the dot means, or what is counted, read after the number ("3 unread messages"). For visible text, use Tag. */
|
|
41
|
+
label: string;
|
|
42
|
+
} & import("react").RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import { useDiyaMessages } from '../DiyaProvider.js';
|
|
5
|
+
import { cx } from '../internal/cx.js';
|
|
6
|
+
import { countLabel } from './badge-utils.js';
|
|
7
|
+
import { badgeMessages } from './messages.js';
|
|
8
|
+
/** A small count (`variant="count"`) or dot (`variant="dot"`) that flags status or new items. Its `label` is announced, never shown. */
|
|
9
|
+
export const Badge = forwardRef(function Badge({ variant, size = 'medium', tone = 'neutral', count = 0, max = 99, showZero = false, label, className, ...props }, ref) {
|
|
10
|
+
const t = useDiyaMessages('badge', badgeMessages);
|
|
11
|
+
const dot = variant === 'dot';
|
|
12
|
+
const value = countLabel(count, max);
|
|
13
|
+
if (!dot && value.count === 0 && !showZero)
|
|
14
|
+
return null;
|
|
15
|
+
return (_jsx("span", { role: "img", ...props, ref: ref, className: cx('diya-badge', className), "data-variant": dot ? 'dot' : 'count', "data-size": size, "data-color": tone, "data-multiple": !dot && value.text.length > 1 ? true : undefined, "aria-label": dot ? label : t.count(value.count, label), children: !dot && _jsx("span", { "aria-hidden": "true", children: value.text }) }));
|
|
16
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
export function normalizeHex(value) {
|
|
3
|
+
const hex = value.trim().replace(/^#/, '');
|
|
4
|
+
if (/^[\da-f]{3}$/i.test(hex))
|
|
5
|
+
return `#${[...hex]
|
|
6
|
+
.map((x) => x + x)
|
|
7
|
+
.join('')
|
|
8
|
+
.toUpperCase()}`;
|
|
9
|
+
return /^[\da-f]{6}$/i.test(hex) ? `#${hex.toUpperCase()}` : null;
|
|
10
|
+
}
|
|
11
|
+
export function badgeForeground(hex) {
|
|
12
|
+
const color = normalizeHex(hex) ?? '#808080';
|
|
13
|
+
const channels = [1, 3, 5].map((index) => {
|
|
14
|
+
const value = parseInt(color.slice(index, index + 2), 16) / 255;
|
|
15
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
16
|
+
});
|
|
17
|
+
const luminance = channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
|
|
18
|
+
return (luminance + 0.05) / 0.05 >= 1.05 / (luminance + 0.05) ? '#000000' : '#FFFFFF';
|
|
19
|
+
}
|
|
20
|
+
export function countLabel(count, maximum) {
|
|
21
|
+
const safeCount = Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0;
|
|
22
|
+
const safeMax = Number.isFinite(maximum) ? Math.max(1, Math.floor(maximum)) : 99;
|
|
23
|
+
return { count: safeCount, text: safeCount > safeMax ? `${safeMax}+` : String(safeCount) };
|
|
24
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
import { breadcrumbsMessages } from './messages.js';
|
|
3
|
+
declare module '../messages.js' {
|
|
4
|
+
interface DiyaMessages {
|
|
5
|
+
breadcrumbs: typeof breadcrumbsMessages;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export type BreadcrumbItem = {
|
|
9
|
+
/** Visible page name. */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Link target; also the item's key, so keep it unique. */
|
|
12
|
+
href: string;
|
|
13
|
+
};
|
|
14
|
+
export type BreadcrumbsProps = HTMLAttributes<HTMLElement> & {
|
|
15
|
+
/** The trail from the top level to the current page; the last item is the current page and is not a link. */
|
|
16
|
+
items: BreadcrumbItem[];
|
|
17
|
+
/** Replaces the first item's text with a home icon; its `label` stays as the accessible name. */
|
|
18
|
+
showHomeIcon?: boolean;
|
|
19
|
+
/** With more than three items, folds the middle ones into a "More pages" menu. */
|
|
20
|
+
collapsed?: boolean;
|
|
21
|
+
/** Handles plain left-clicks and menu picks in the app instead of following `href`; modified clicks still open the link. Without it, items are links through the provider's `linkComponent`. */
|
|
22
|
+
onNavigate?: (item: BreadcrumbItem) => void;
|
|
23
|
+
};
|
|
24
|
+
/** A trail of links from the top level to the current page; the ref, `className`, `style` and other attributes go to the `nav`. */
|
|
25
|
+
export declare const Breadcrumbs: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLElement> & {
|
|
26
|
+
/** The trail from the top level to the current page; the last item is the current page and is not a link. */
|
|
27
|
+
items: BreadcrumbItem[];
|
|
28
|
+
/** Replaces the first item's text with a home icon; its `label` stays as the accessible name. */
|
|
29
|
+
showHomeIcon?: boolean;
|
|
30
|
+
/** With more than three items, folds the middle ones into a "More pages" menu. */
|
|
31
|
+
collapsed?: boolean;
|
|
32
|
+
/** Handles plain left-clicks and menu picks in the app instead of following `href`; modified clicks still open the link. Without it, items are links through the provider's `linkComponent`. */
|
|
33
|
+
onNavigate?: (item: BreadcrumbItem) => void;
|
|
34
|
+
} & import("react").RefAttributes<HTMLElement>>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import { Menu } from '../menu/Menu.js';
|
|
5
|
+
import { useDiyaMessages } from '../DiyaProvider.js';
|
|
6
|
+
import { cx } from '../internal/cx.js';
|
|
7
|
+
import { useDiyaLinkComponent } from '../internal/link.js';
|
|
8
|
+
import { breadcrumbsMessages } from './messages.js';
|
|
9
|
+
/** A trail of links from the top level to the current page; the ref, `className`, `style` and other attributes go to the `nav`. */
|
|
10
|
+
export const Breadcrumbs = forwardRef(function Breadcrumbs({ items, showHomeIcon = false, collapsed = false, onNavigate, className, ...props }, ref) {
|
|
11
|
+
const t = useDiyaMessages('breadcrumbs', breadcrumbsMessages);
|
|
12
|
+
const Link = useDiyaLinkComponent();
|
|
13
|
+
if (!items.length)
|
|
14
|
+
return null;
|
|
15
|
+
const middle = items.slice(1, -1);
|
|
16
|
+
const shown = collapsed && items.length > 3 ? [items[0], null, items[items.length - 1]] : items;
|
|
17
|
+
return _jsx("nav", { "aria-label": t.label, ...props, ref: ref, className: cx('diya-breadcrumbs', className), children: _jsx("ol", { children: shown.map((item, index) => _jsxs("li", { children: [index > 0 && _jsx("svg", { className: "diya-breadcrumb-separator", width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: _jsx("path", { d: "m9 5 7 7-7 7" }) }), !item ? _jsx("span", { className: "diya-breadcrumb-collapse", children: _jsx(Menu, { label: t.morePages, items: middle.map((entry) => ({ id: entry.href, label: entry.label, href: entry.href, onSelect: onNavigate && (() => onNavigate(entry)) })) }) }) : index === shown.length - 1 ? _jsx("span", { "aria-current": "page", children: item.label }) : _jsx(Link, { href: item.href, "aria-label": showHomeIcon && index === 0 ? item.label : undefined, onClick: onNavigate ? (event) => { if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
18
|
+
return; event.preventDefault(); onNavigate(item); } : undefined, children: showHomeIcon && index === 0 ? _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: _jsx("path", { d: "m3 10 9-7 9 7v11H3Z M9 21v-8h6v8" }) }) : item.label })] }, `${item?.href ?? 'collapsed'}-${index}`)) }) });
|
|
19
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type AnchorHTMLAttributes, type ButtonHTMLAttributes } from 'react';
|
|
2
|
+
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
3
|
+
/** 'solid' for the main action, 'outline' for secondary ones, 'quiet' for low-emphasis ones, 'destructive' for an action that deletes or can't be undone. Defaults to 'solid'. */
|
|
4
|
+
variant?: 'solid' | 'outline' | 'quiet' | 'destructive';
|
|
5
|
+
/** Button height and text size. Defaults to 'medium'. */
|
|
6
|
+
size?: 'small' | 'medium' | 'large';
|
|
7
|
+
/** 'pill' is fully round; 'rounded' uses the shared control corner, like fields. Defaults to 'pill'. */
|
|
8
|
+
shape?: 'pill' | 'rounded';
|
|
9
|
+
/** Square button for a single icon child; give it an `aria-label`, since the icon is its only content. */
|
|
10
|
+
iconOnly?: boolean;
|
|
11
|
+
/** Shows a working indicator and sets `aria-busy`; the label stays unless `iconOnly`. The button stays focusable, so focus isn't lost while it works, but it is inert: `aria-disabled="true"`, and clicks, Enter and Space do nothing, so it can't submit twice. */
|
|
12
|
+
loading?: boolean;
|
|
13
|
+
/** Renders a link to this URL that looks like a Button, through the provider's `linkComponent` (a plain anchor by default). */
|
|
14
|
+
href?: string;
|
|
15
|
+
/** With `href`, where the link opens, such as '_blank'. */
|
|
16
|
+
target?: AnchorHTMLAttributes<HTMLAnchorElement>['target'];
|
|
17
|
+
/** With `href`, the link's relationship to this page, such as 'noopener'. */
|
|
18
|
+
rel?: string;
|
|
19
|
+
/** With `href`, downloads the target instead of opening it; a string names the saved file. */
|
|
20
|
+
download?: AnchorHTMLAttributes<HTMLAnchorElement>['download'];
|
|
21
|
+
/** Styles your single child element as the Button instead of rendering one, such as a router `Link`; DIYA's classes, attributes and handlers merge into it. */
|
|
22
|
+
asChild?: boolean;
|
|
23
|
+
};
|
|
24
|
+
/** A native button in DIYA's styles, or a link that looks like one with `href` or `asChild`; other button attributes pass through, and `type` defaults to 'button'. */
|
|
25
|
+
export declare const Button: import("react").ForwardRefExoticComponent<ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
26
|
+
/** 'solid' for the main action, 'outline' for secondary ones, 'quiet' for low-emphasis ones, 'destructive' for an action that deletes or can't be undone. Defaults to 'solid'. */
|
|
27
|
+
variant?: "solid" | "outline" | "quiet" | "destructive";
|
|
28
|
+
/** Button height and text size. Defaults to 'medium'. */
|
|
29
|
+
size?: "small" | "medium" | "large";
|
|
30
|
+
/** 'pill' is fully round; 'rounded' uses the shared control corner, like fields. Defaults to 'pill'. */
|
|
31
|
+
shape?: "pill" | "rounded";
|
|
32
|
+
/** Square button for a single icon child; give it an `aria-label`, since the icon is its only content. */
|
|
33
|
+
iconOnly?: boolean;
|
|
34
|
+
/** Shows a working indicator and sets `aria-busy`; the label stays unless `iconOnly`. The button stays focusable, so focus isn't lost while it works, but it is inert: `aria-disabled="true"`, and clicks, Enter and Space do nothing, so it can't submit twice. */
|
|
35
|
+
loading?: boolean;
|
|
36
|
+
/** Renders a link to this URL that looks like a Button, through the provider's `linkComponent` (a plain anchor by default). */
|
|
37
|
+
href?: string;
|
|
38
|
+
/** With `href`, where the link opens, such as '_blank'. */
|
|
39
|
+
target?: AnchorHTMLAttributes<HTMLAnchorElement>["target"];
|
|
40
|
+
/** With `href`, the link's relationship to this page, such as 'noopener'. */
|
|
41
|
+
rel?: string;
|
|
42
|
+
/** With `href`, downloads the target instead of opening it; a string names the saved file. */
|
|
43
|
+
download?: AnchorHTMLAttributes<HTMLAnchorElement>["download"];
|
|
44
|
+
/** Styles your single child element as the Button instead of rendering one, such as a router `Link`; DIYA's classes, attributes and handlers merge into it. */
|
|
45
|
+
asChild?: boolean;
|
|
46
|
+
} & import("react").RefAttributes<HTMLAnchorElement | HTMLButtonElement>>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cloneElement, forwardRef, isValidElement } from 'react';
|
|
4
|
+
import { cx } from '../internal/cx.js';
|
|
5
|
+
import { useDiyaLinkComponent } from '../internal/link.js';
|
|
6
|
+
import { onlyChild, renderSlot } from '../internal/slot.js';
|
|
7
|
+
// A link can't be disabled natively, so its clicks stop here, before the link or a router sees them.
|
|
8
|
+
function stopClick(event) {
|
|
9
|
+
event.preventDefault();
|
|
10
|
+
event.stopPropagation();
|
|
11
|
+
}
|
|
12
|
+
// A loading button keeps focus but must not act: Enter and Space would activate it, so they stop here too.
|
|
13
|
+
function stopActivation(event) {
|
|
14
|
+
if (event.key === 'Enter' || event.key === ' ')
|
|
15
|
+
event.preventDefault();
|
|
16
|
+
}
|
|
17
|
+
/** A native button in DIYA's styles, or a link that looks like one with `href` or `asChild`; other button attributes pass through, and `type` defaults to 'button'. */
|
|
18
|
+
export const Button = forwardRef(function Button({ variant = 'solid', size = 'medium', shape = 'pill', iconOnly = false, loading = false, disabled, children, className, type = 'button', href, target, rel, download, asChild = false, ...props }, ref) {
|
|
19
|
+
const Link = useDiyaLinkComponent();
|
|
20
|
+
const inactive = !!disabled || loading;
|
|
21
|
+
const shared = { className: cx('diya-button', className), 'data-variant': variant, 'data-size': size, 'data-shape': shape, 'data-icon-only': iconOnly || undefined, 'aria-busy': loading || undefined };
|
|
22
|
+
const content = (inner) => _jsxs(_Fragment, { children: [loading && _jsx("span", { className: "diya-button-progress", "aria-hidden": "true", children: "\u00B7\u00B7\u00B7" }), !(iconOnly && loading) && inner] });
|
|
23
|
+
if (asChild) {
|
|
24
|
+
const child = onlyChild(children);
|
|
25
|
+
if (!isValidElement(child))
|
|
26
|
+
return null;
|
|
27
|
+
const own = child;
|
|
28
|
+
const filled = loading ? cloneElement(own, undefined, content(own.props.children)) : own;
|
|
29
|
+
return renderSlot(filled, { ...props, ...shared, 'aria-disabled': inactive || undefined, onClickCapture: inactive ? stopClick : undefined }, ref);
|
|
30
|
+
}
|
|
31
|
+
if (href !== undefined) {
|
|
32
|
+
const anchor = props;
|
|
33
|
+
const anchorRef = ref;
|
|
34
|
+
// Without its href the anchor leaves the tab order and can't be followed, like a disabled button.
|
|
35
|
+
if (inactive)
|
|
36
|
+
return _jsx("a", { ...anchor, ...shared, ref: anchorRef, role: "link", "aria-disabled": "true", onClickCapture: stopClick, children: content(children) });
|
|
37
|
+
return _jsx(Link, { ...anchor, ...shared, ref: anchorRef, href: href, target: target, rel: rel, download: download, children: content(children) });
|
|
38
|
+
}
|
|
39
|
+
// Loading keeps the button in the tab order with focus on it, unlike disabled; aria-disabled and the
|
|
40
|
+
// blocked click (which Enter, Space and a form's implicit submission all go through) make it inert.
|
|
41
|
+
const busy = loading && !disabled;
|
|
42
|
+
const button = props;
|
|
43
|
+
return _jsx("button", { ...button, ...shared, ref: ref, type: type, disabled: !!disabled, "aria-disabled": busy ? true : button['aria-disabled'], onClickCapture: busy ? stopClick : button.onClickCapture, onKeyDown: busy ? stopActivation : button.onKeyDown, children: content(children) });
|
|
44
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
import { calendarMessages } from './messages.js';
|
|
3
|
+
import { type DateRange } from './month.js';
|
|
4
|
+
declare module '../messages.js' {
|
|
5
|
+
interface DiyaMessages {
|
|
6
|
+
calendar: typeof calendarMessages;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
type CalendarBase = Omit<HTMLAttributes<HTMLDivElement>, 'defaultValue' | 'onChange' | 'children'> & {
|
|
10
|
+
/** Visible label above the calendar; also names it for assistive tech. Hide it with hideLabel. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Hides the label visually but keeps it as the calendar's accessible name. */
|
|
13
|
+
hideLabel?: boolean;
|
|
14
|
+
/** Day height and type size, matching fields of the same size; defaults to 'medium'. */
|
|
15
|
+
size?: 'small' | 'medium' | 'large';
|
|
16
|
+
/** Earliest date that can be chosen, as ISO YYYY-MM-DD; earlier days show as unavailable. */
|
|
17
|
+
min?: string;
|
|
18
|
+
/** Latest date that can be chosen, as ISO YYYY-MM-DD; later days show as unavailable. */
|
|
19
|
+
max?: string;
|
|
20
|
+
/** Called with each ISO YYYY-MM-DD day on show; return true to mark the day unavailable (announced, and it can't be chosen), as with `min` and `max`. */
|
|
21
|
+
isDateUnavailable?: (date: string) => boolean;
|
|
22
|
+
/** Dims the calendar and stops every day and month button. */
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
/** The month on show as ISO YYYY-MM, when controlled; pair it with `onMonthChange`. Left out, the calendar starts on the chosen date's month, or today's. */
|
|
25
|
+
month?: string;
|
|
26
|
+
/** Called with the ISO YYYY-MM month to show: from the month buttons, from the keyboard moving into another month, or when `value` changes to a date in another month. */
|
|
27
|
+
onMonthChange?: (month: string) => void;
|
|
28
|
+
};
|
|
29
|
+
/** One ISO date (YYYY-MM-DD), '' for none. */
|
|
30
|
+
export type CalendarSingleProps = CalendarBase & {
|
|
31
|
+
/** Single-date mode (the default): leave unset or false. */
|
|
32
|
+
range?: false;
|
|
33
|
+
/** The chosen date as ISO YYYY-MM-DD, or '' for none, when controlled; pair it with `onValueChange`. */
|
|
34
|
+
value?: string;
|
|
35
|
+
/** The initially chosen ISO date when uncontrolled. */
|
|
36
|
+
defaultValue?: string;
|
|
37
|
+
/** Called with the chosen ISO YYYY-MM-DD date. */
|
|
38
|
+
onValueChange?: (value: string) => void;
|
|
39
|
+
};
|
|
40
|
+
/** A start and end date. */
|
|
41
|
+
export type CalendarRangeProps = CalendarBase & {
|
|
42
|
+
/** Range mode: the first click sets the start, the second the end (swapped if earlier); the next click starts a new range. */
|
|
43
|
+
range: true;
|
|
44
|
+
/** The chosen range as ISO YYYY-MM-DD dates, a bound '' while unset, when controlled; pair it with `onValueChange`. */
|
|
45
|
+
value?: DateRange;
|
|
46
|
+
/** The initially chosen range when uncontrolled. */
|
|
47
|
+
defaultValue?: DateRange;
|
|
48
|
+
/** Called with the new range after each click; `end` is '' until the second date is chosen. */
|
|
49
|
+
onValueChange?: (value: DateRange) => void;
|
|
50
|
+
};
|
|
51
|
+
/** One ISO date ('' for none) by default; `range` takes and returns a DateRange. Other attributes and the ref go on the outer element. */
|
|
52
|
+
export type CalendarProps = CalendarSingleProps | CalendarRangeProps;
|
|
53
|
+
/** A labeled month calendar that stays on the page; picks one ISO date, or a start and end date with `range`. */
|
|
54
|
+
export declare const Calendar: import("react").ForwardRefExoticComponent<CalendarProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useId, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { useDiyaLocale, useDiyaMessages } from '../DiyaProvider.js';
|
|
5
|
+
import { cx } from '../internal/cx.js';
|
|
6
|
+
import { calendarMessages } from './messages.js';
|
|
7
|
+
import { iso, monthOf, useClientLayoutEffect, useMonthGrid, useToday } from './month.js';
|
|
8
|
+
const toMonth = (date) => iso(date).slice(0, 7);
|
|
9
|
+
const fromMonth = (text) => text && /^\d{4}-(0[1-9]|1[0-2])$/.test(text) ? new Date(Number(text.slice(0, 4)), Number(text.slice(5)) - 1, 1) : undefined;
|
|
10
|
+
/** A labeled month calendar that stays on the page; picks one ISO date, or a start and end date with `range`. */
|
|
11
|
+
export const Calendar = forwardRef(function Calendar(props, ref) {
|
|
12
|
+
const { label, hideLabel = false, size = 'medium', min, max, isDateUnavailable, disabled = false, month: monthProp, onMonthChange, className, range: _range, value: _value, defaultValue: _defaultValue, onValueChange: _onValueChange, ...rest } = props;
|
|
13
|
+
const t = useDiyaMessages('calendar', calendarMessages);
|
|
14
|
+
const locale = useDiyaLocale();
|
|
15
|
+
const id = useId();
|
|
16
|
+
const range = props.range === true;
|
|
17
|
+
const controlled = props.value !== undefined;
|
|
18
|
+
const [uncontrolled, setUncontrolled] = useState(() => props.range ? props.defaultValue ?? { start: '', end: '' } : { start: props.defaultValue ?? '', end: '' });
|
|
19
|
+
// Internally a value is always a range; a single date is { start: date, end: '' }.
|
|
20
|
+
const value = !controlled ? uncontrolled : props.range ? props.value ?? { start: '', end: '' } : { start: props.value ?? '', end: '' };
|
|
21
|
+
// A start without an end means the next day chosen ends the range.
|
|
22
|
+
const choosingEnd = range && !!value.start && !value.end;
|
|
23
|
+
// With no date to show, the server and the first browser render both open on the UTC month, so
|
|
24
|
+
// they agree whatever the reader's time zone; the reader's own month follows once the page is live.
|
|
25
|
+
const today = useToday();
|
|
26
|
+
const [ownMonth, setOwnMonth] = useState(() => value.start ? monthOf(value.start, min, max) : null);
|
|
27
|
+
const shownKey = fromMonth(monthProp) ? monthProp : toMonth(ownMonth ?? monthOf(today || new Date().toISOString().slice(0, 10), min, max));
|
|
28
|
+
const month = useMemo(() => fromMonth(shownKey), [shownKey]);
|
|
29
|
+
function changeMonth(next) {
|
|
30
|
+
if (monthProp === undefined)
|
|
31
|
+
setOwnMonth(next);
|
|
32
|
+
onMonthChange?.(toMonth(next));
|
|
33
|
+
}
|
|
34
|
+
// A new value in another month (a "Next day" button, say) turns the page to it.
|
|
35
|
+
const lastStart = useRef(value.start);
|
|
36
|
+
useClientLayoutEffect(() => {
|
|
37
|
+
if (value.start === lastStart.current)
|
|
38
|
+
return;
|
|
39
|
+
lastStart.current = value.start;
|
|
40
|
+
if (!value.start)
|
|
41
|
+
return;
|
|
42
|
+
const target = monthOf(value.start, min, max);
|
|
43
|
+
if (toMonth(target) !== shownKey)
|
|
44
|
+
changeMonth(target);
|
|
45
|
+
}, [value.start]);
|
|
46
|
+
function choose(day) {
|
|
47
|
+
if (disabled || monthGrid.isBlocked(day))
|
|
48
|
+
return;
|
|
49
|
+
const next = !range ? { start: day, end: '' }
|
|
50
|
+
: !choosingEnd ? { start: day, end: '' }
|
|
51
|
+
: { start: day < value.start ? day : value.start, end: day < value.start ? value.start : day };
|
|
52
|
+
if (!controlled)
|
|
53
|
+
setUncontrolled(next);
|
|
54
|
+
if (props.range)
|
|
55
|
+
props.onValueChange?.(next);
|
|
56
|
+
else
|
|
57
|
+
props.onValueChange?.(next.start);
|
|
58
|
+
}
|
|
59
|
+
const monthGrid = useMonthGrid({ locale, month, onMonthChange: changeMonth, value, range, choosingEnd, min, max, isDateUnavailable, disabled, active: true, strings: t, onChoose: choose });
|
|
60
|
+
return _jsxs("div", { ...rest, ref: ref, role: "group", "aria-labelledby": `${id}-label`, "aria-disabled": disabled || undefined, className: cx('diya-inline-calendar', className), "data-size": size, "data-disabled": disabled || undefined, children: [_jsx("span", { id: `${id}-label`, className: hideLabel ? 'diya-sr-only' : 'diya-calendar-label', children: label }), monthGrid.render()] });
|
|
61
|
+
});
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** calendar: English defaults, overridable through DiyaProvider `messages`. */
|
|
2
|
+
export const calendarMessages = { previousMonth: 'Previous month', nextMonth: 'Next month', chooseStart: 'Choose start date', chooseEnd: 'Choose end date', unavailableDay: (date) => `${date}, unavailable` };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
/** A date range as ISO dates (YYYY-MM-DD); '' while a bound is unset. */
|
|
3
|
+
export type DateRange = {
|
|
4
|
+
start: string;
|
|
5
|
+
end: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const iso: (date: Date) => string;
|
|
8
|
+
export declare const parse: (value: string) => Date;
|
|
9
|
+
export declare const display: (value: string, locale: string) => string;
|
|
10
|
+
export declare const useClientLayoutEffect: typeof useEffect;
|
|
11
|
+
/** Whether `day` falls before `min` or after `max`. */
|
|
12
|
+
export declare const outsideLimits: (day: string, min?: string, max?: string) => boolean;
|
|
13
|
+
/** `day` moved inside `min` and `max`. */
|
|
14
|
+
export declare const clampToLimits: (day: string, min?: string, max?: string) => string;
|
|
15
|
+
/** The first of the month that shows `day` (today when it is '' or invalid), kept inside `min` and `max`. */
|
|
16
|
+
export declare function monthOf(day: string, min?: string, max?: string): Date;
|
|
17
|
+
/** Today as an ISO date; '' while hydrating server HTML, so a server in another time zone never marks the wrong day. */
|
|
18
|
+
export declare const useToday: () => string;
|
|
19
|
+
export type MonthGridStrings = {
|
|
20
|
+
previousMonth: string;
|
|
21
|
+
nextMonth: string;
|
|
22
|
+
chooseStart: string;
|
|
23
|
+
chooseEnd: string;
|
|
24
|
+
unavailableDay: (date: string) => string;
|
|
25
|
+
};
|
|
26
|
+
type MonthGridOptions = {
|
|
27
|
+
locale: string;
|
|
28
|
+
/** The first of the month on show. */
|
|
29
|
+
month: Date;
|
|
30
|
+
/** Asks to show another month: the month buttons, or the keyboard crossing into it. */
|
|
31
|
+
onMonthChange: (month: Date) => void;
|
|
32
|
+
value: DateRange;
|
|
33
|
+
range: boolean;
|
|
34
|
+
/** In range mode, whether the next choice sets the end. */
|
|
35
|
+
choosingEnd: boolean;
|
|
36
|
+
min?: string;
|
|
37
|
+
max?: string;
|
|
38
|
+
isDateUnavailable?: (date: string) => boolean;
|
|
39
|
+
/** Disables every button in the grid (Calendar's `disabled`). */
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
/** Whether the grid is on screen; focus follows the keyboard into a new month only then. */
|
|
42
|
+
active: boolean;
|
|
43
|
+
strings: MonthGridStrings;
|
|
44
|
+
onChoose: (day: string) => void;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The month grid DatePicker shows in its popup and Calendar shows on the page: the
|
|
48
|
+
* month heading with its buttons, the range hint, then the ARIA date grid of the APG
|
|
49
|
+
* date picker example. The grid is named by the month; a header row of weekday column
|
|
50
|
+
* headers, then a row per week of gridcells. Each day is a native button with the
|
|
51
|
+
* gridcell role, named with its weekday ("Thursday, November 12, 2026"): chosen days
|
|
52
|
+
* carry aria-selected, today aria-current="date". One day is a Tab stop; arrows,
|
|
53
|
+
* Home/End and PageUp/PageDown move it, kept inside min and max. Days outside them,
|
|
54
|
+
* or unavailable, say so in their name and aria-disabled, and can't be chosen.
|
|
55
|
+
*/
|
|
56
|
+
export declare function useMonthGrid(options: MonthGridOptions): {
|
|
57
|
+
render: () => import("react/jsx-runtime").JSX.Element;
|
|
58
|
+
resetCursor: () => void;
|
|
59
|
+
isBlocked: (day: string) => boolean;
|
|
60
|
+
};
|
|
61
|
+
export {};
|