@godxjp/ui 27.4.1 → 27.5.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/dist/components/data-entry/upload-file-kind.d.ts +25 -0
- package/dist/components/data-entry/upload-file-kind.js +88 -0
- package/dist/components/data-entry/upload.d.ts +2 -2
- package/dist/components/data-entry/upload.js +10 -6
- package/dist/components/ui/avatar.d.ts +1 -1
- package/dist/components/ui/avatar.js +2 -0
- package/dist/components/ui/segmented.d.ts +7 -2
- package/dist/components/ui/toggle.d.ts +2 -2
- package/dist/components/ui/toggle.js +4 -0
- package/dist/contracts/measurement.json +1 -1
- package/dist/props/components/data-display.prop.d.ts +14 -0
- package/dist/props/components/data-entry.prop.d.ts +25 -0
- package/dist/props/registry.d.ts +6 -1
- package/dist/props/registry.js +6 -1
- package/dist/styles/control.css +12 -0
- package/dist/styles/data-display-layout.css +35 -5
- package/dist/styles/data-entry-layout.css +22 -0
- package/dist/tokens/components/control.css +1 -0
- package/dist/tokens/components/data-display.css +26 -0
- package/dist/tokens/components/segmented.css +4 -0
- package/dist/tokens/components/upload.css +4 -0
- package/docs/CUSTOMER-THEMING.md +26 -0
- package/docs/data-display/avatar.tsx +128 -11
- package/docs/data-display/prose.tsx +64 -0
- package/docs/data-entry/segmented.tsx +55 -5
- package/docs/data-entry/toggle-group.tsx +15 -3
- package/docs/data-entry/toggle.tsx +39 -1
- package/docs/data-entry/upload.tsx +38 -0
- package/package.json +2 -2
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a listed file IS, at the coarseness a row can actually draw (gh#720).
|
|
3
|
+
*
|
|
4
|
+
* A picture list shows a thumbnail for an image and a glyph for everything else, so the buckets
|
|
5
|
+
* are exactly the marks we have: five, not one per MIME type. The kind is also written onto the
|
|
6
|
+
* row as `data-file-kind`, so a service can retint a bucket without forking the component.
|
|
7
|
+
*/
|
|
8
|
+
export type UploadFileKind = "image" | "pdf" | "archive" | "text" | "file";
|
|
9
|
+
/** The kind of one listed file, from its MIME type when it has one and its extension otherwise. */
|
|
10
|
+
export declare function uploadFileKind(item: {
|
|
11
|
+
name?: string;
|
|
12
|
+
mimeType?: string;
|
|
13
|
+
}): UploadFileKind;
|
|
14
|
+
/**
|
|
15
|
+
* The mark per kind. PDF takes the prose-lines document (`FileText`) because lucide ships no PDF
|
|
16
|
+
* glyph, which is also why plain/structured text takes the `</>` mark rather than the same one: a
|
|
17
|
+
* `.pdf` and a `.json` on adjacent rows must not draw identically.
|
|
18
|
+
*/
|
|
19
|
+
export declare const UPLOAD_FILE_KIND_GLYPHS: {
|
|
20
|
+
readonly image: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
21
|
+
readonly pdf: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
22
|
+
readonly archive: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
23
|
+
readonly text: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
24
|
+
readonly file: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
25
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { File as FileGlyph, FileArchive, FileCode, FileImage, FileText } from "lucide-react";
|
|
2
|
+
const MIME_KINDS = {
|
|
3
|
+
"application/pdf": "pdf",
|
|
4
|
+
"application/x-pdf": "pdf",
|
|
5
|
+
"application/zip": "archive",
|
|
6
|
+
"application/x-zip-compressed": "archive",
|
|
7
|
+
"application/x-7z-compressed": "archive",
|
|
8
|
+
"application/vnd.rar": "archive",
|
|
9
|
+
"application/x-rar-compressed": "archive",
|
|
10
|
+
"application/gzip": "archive",
|
|
11
|
+
"application/x-tar": "archive",
|
|
12
|
+
"application/x-bzip": "archive",
|
|
13
|
+
"application/x-bzip2": "archive",
|
|
14
|
+
"application/json": "text",
|
|
15
|
+
"application/ld+json": "text",
|
|
16
|
+
"application/xml": "text",
|
|
17
|
+
"application/yaml": "text",
|
|
18
|
+
"application/x-yaml": "text"
|
|
19
|
+
};
|
|
20
|
+
const EXTENSION_KINDS = {
|
|
21
|
+
png: "image",
|
|
22
|
+
jpg: "image",
|
|
23
|
+
jpeg: "image",
|
|
24
|
+
gif: "image",
|
|
25
|
+
webp: "image",
|
|
26
|
+
avif: "image",
|
|
27
|
+
svg: "image",
|
|
28
|
+
bmp: "image",
|
|
29
|
+
ico: "image",
|
|
30
|
+
heic: "image",
|
|
31
|
+
heif: "image",
|
|
32
|
+
tif: "image",
|
|
33
|
+
tiff: "image",
|
|
34
|
+
pdf: "pdf",
|
|
35
|
+
zip: "archive",
|
|
36
|
+
tar: "archive",
|
|
37
|
+
gz: "archive",
|
|
38
|
+
tgz: "archive",
|
|
39
|
+
rar: "archive",
|
|
40
|
+
"7z": "archive",
|
|
41
|
+
bz2: "archive",
|
|
42
|
+
xz: "archive",
|
|
43
|
+
zst: "archive",
|
|
44
|
+
txt: "text",
|
|
45
|
+
md: "text",
|
|
46
|
+
markdown: "text",
|
|
47
|
+
json: "text",
|
|
48
|
+
jsonl: "text",
|
|
49
|
+
csv: "text",
|
|
50
|
+
tsv: "text",
|
|
51
|
+
log: "text",
|
|
52
|
+
xml: "text",
|
|
53
|
+
yml: "text",
|
|
54
|
+
yaml: "text",
|
|
55
|
+
html: "text",
|
|
56
|
+
htm: "text",
|
|
57
|
+
css: "text",
|
|
58
|
+
js: "text",
|
|
59
|
+
ts: "text"
|
|
60
|
+
};
|
|
61
|
+
function uploadFileKind(item) {
|
|
62
|
+
const mime = item.mimeType?.trim().toLowerCase();
|
|
63
|
+
if (mime) {
|
|
64
|
+
if (mime.startsWith("image/")) return "image";
|
|
65
|
+
if (mime.startsWith("text/")) return "text";
|
|
66
|
+
const known = MIME_KINDS[mime];
|
|
67
|
+
if (known) return known;
|
|
68
|
+
}
|
|
69
|
+
const name = item.name ?? "";
|
|
70
|
+
const dot = name.lastIndexOf(".");
|
|
71
|
+
if (dot > 0) {
|
|
72
|
+
const extension = name.slice(dot + 1).toLowerCase();
|
|
73
|
+
const known = EXTENSION_KINDS[extension];
|
|
74
|
+
if (known) return known;
|
|
75
|
+
}
|
|
76
|
+
return "file";
|
|
77
|
+
}
|
|
78
|
+
const UPLOAD_FILE_KIND_GLYPHS = {
|
|
79
|
+
image: FileImage,
|
|
80
|
+
pdf: FileText,
|
|
81
|
+
archive: FileArchive,
|
|
82
|
+
text: FileCode,
|
|
83
|
+
file: FileGlyph
|
|
84
|
+
};
|
|
85
|
+
export {
|
|
86
|
+
UPLOAD_FILE_KIND_GLYPHS,
|
|
87
|
+
uploadFileKind
|
|
88
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { UploadProp } from "../../props/components/data-entry.prop.js";
|
|
3
|
-
export type { UploadProp, UploadProp as UploadProps, UploadFileItemProp, UploadVariantProp, } from "../../props/components/data-entry.prop.js";
|
|
3
|
+
export type { UploadProp, UploadProp as UploadProps, UploadFileItemProp, UploadVariantProp, UploadListTypeProp, } from "../../props/components/data-entry.prop.js";
|
|
4
4
|
export type { UploadFileItem, UploadVariant, UploadCommitAction } from "./upload-types.js";
|
|
5
5
|
export { collectUploadCommitActions, createUploadItem, UPLOAD_LIST_IGNORE } from "./upload-types.js";
|
|
6
6
|
export { useUploadDraft } from "./use-upload-draft.js";
|
|
7
|
-
export declare function Upload({ variant, triggerSize, triggerVariant, value, defaultValue, onValueChange, accept: acceptProp, multiple: multipleProp, maxCount: maxCountProp, maxSizeBytes, disabled: disabledProp, readOnly, directory, pastable, openFileDialogOnClick, name, action, method, headers, data, withCredentials, beforeUpload, onReject, onRemove, onPreview, onDownload, previewFile, onDrop, showUploadList, itemRender, removable, onUpload, id, className, children, ...ariaProps }: UploadProp): React.JSX.Element;
|
|
7
|
+
export declare function Upload({ variant, listType, triggerSize, triggerVariant, value, defaultValue, onValueChange, accept: acceptProp, multiple: multipleProp, maxCount: maxCountProp, maxSizeBytes, disabled: disabledProp, readOnly, directory, pastable, openFileDialogOnClick, name, action, method, headers, data, withCredentials, beforeUpload, onReject, onRemove, onPreview, onDownload, previewFile, onDrop, showUploadList, itemRender, removable, onUpload, id, className, children, ...ariaProps }: UploadProp): React.JSX.Element;
|
|
@@ -15,9 +15,10 @@ import { useTranslation } from "../../i18n/use-translation.js";
|
|
|
15
15
|
import { formatBytes } from "../../lib/format.js";
|
|
16
16
|
import { cn } from "../../lib/utils.js";
|
|
17
17
|
import { resolveFieldA11y } from "../../lib/field-a11y.js";
|
|
18
|
-
import { controlIconClass } from "../../lib/control-styles.js";
|
|
19
18
|
import { Button } from "../general/button.js";
|
|
19
|
+
import { Icon } from "../general/icon.js";
|
|
20
20
|
import { UploadCropDialog } from "./upload-crop-dialog.js";
|
|
21
|
+
import { UPLOAD_FILE_KIND_GLYPHS, uploadFileKind } from "./upload-file-kind.js";
|
|
21
22
|
import {
|
|
22
23
|
UPLOAD_LIST_IGNORE,
|
|
23
24
|
createUploadItem,
|
|
@@ -66,6 +67,7 @@ function useUploadList(controlled, defaultValue, onValueChange) {
|
|
|
66
67
|
}
|
|
67
68
|
function Upload({
|
|
68
69
|
variant = "dropzone",
|
|
70
|
+
listType,
|
|
69
71
|
triggerSize,
|
|
70
72
|
triggerVariant = "outline",
|
|
71
73
|
value,
|
|
@@ -109,6 +111,7 @@ function Upload({
|
|
|
109
111
|
triggerOwnsAriaLabel ? { ...ariaProps, "aria-label": void 0 } : ariaProps,
|
|
110
112
|
t("dataEntry.upload.inputLabel")
|
|
111
113
|
);
|
|
114
|
+
const resolvedListType = listType ?? (variant === "picture" ? "picture" : "text");
|
|
112
115
|
const accept = acceptProp ?? defaultAcceptForVariant(variant);
|
|
113
116
|
const maxCount = maxCountProp ?? defaultMaxCount(variant);
|
|
114
117
|
const multiple = multipleProp ?? (maxCount === 1 ? false : true);
|
|
@@ -380,7 +383,7 @@ function Upload({
|
|
|
380
383
|
hasCustomPreview: Boolean(onPreview),
|
|
381
384
|
onDownload,
|
|
382
385
|
itemRender,
|
|
383
|
-
|
|
386
|
+
listType: resolvedListType
|
|
384
387
|
}
|
|
385
388
|
) : null;
|
|
386
389
|
const hiddenInput = /* @__PURE__ */ jsx(
|
|
@@ -571,7 +574,7 @@ function Upload({
|
|
|
571
574
|
onClick: openPicker,
|
|
572
575
|
className: "ui-upload-picture-empty",
|
|
573
576
|
children: [
|
|
574
|
-
/* @__PURE__ */ jsx(ImagePlus, { className:
|
|
577
|
+
/* @__PURE__ */ jsx(ImagePlus, { className: "ui-upload-picture-empty-icon", "aria-hidden": "true" }),
|
|
575
578
|
/* @__PURE__ */ jsx("span", { className: "ui-upload-picture-empty-label", children: t("dataEntry.upload.addImage") })
|
|
576
579
|
]
|
|
577
580
|
}
|
|
@@ -621,7 +624,7 @@ function Upload({
|
|
|
621
624
|
className: "ui-upload-avatar ui-focus-ring",
|
|
622
625
|
"aria-label": t("dataEntry.upload.avatarLabel"),
|
|
623
626
|
children: [
|
|
624
|
-
!showPlaceholder && item?.previewUrl ? /* @__PURE__ */ jsx("img", { src: item.previewUrl, alt: "", className: "ui-upload-avatar-image" }) : /* @__PURE__ */ jsx("span", { className: "ui-upload-avatar-placeholder", children: /* @__PURE__ */ jsx(Camera, { className:
|
|
627
|
+
!showPlaceholder && item?.previewUrl ? /* @__PURE__ */ jsx("img", { src: item.previewUrl, alt: "", className: "ui-upload-avatar-image" }) : /* @__PURE__ */ jsx("span", { className: "ui-upload-avatar-placeholder", children: /* @__PURE__ */ jsx(Camera, { className: "ui-upload-avatar-icon", "aria-hidden": "true" }) }),
|
|
625
628
|
!disabled && /* @__PURE__ */ jsx("span", { className: "ui-upload-avatar-change", children: t("dataEntry.upload.change") })
|
|
626
629
|
]
|
|
627
630
|
}
|
|
@@ -750,12 +753,13 @@ function UploadFileList({
|
|
|
750
753
|
hasCustomPreview,
|
|
751
754
|
onDownload,
|
|
752
755
|
itemRender,
|
|
753
|
-
|
|
756
|
+
listType
|
|
754
757
|
}) {
|
|
755
758
|
const { t } = useTranslation();
|
|
756
759
|
return /* @__PURE__ */ jsx("ul", { className: "ui-stack-xs", children: items.map((item) => {
|
|
760
|
+
const kind = uploadFileKind(item);
|
|
757
761
|
const node = /* @__PURE__ */ jsxs("div", { className: "ui-upload-row", children: [
|
|
758
|
-
|
|
762
|
+
listType === "picture" && (item.previewUrl ? /* @__PURE__ */ jsx("img", { src: item.previewUrl, alt: "", className: "ui-upload-list-thumb" }) : /* @__PURE__ */ jsx("span", { className: "ui-upload-list-glyph", "data-file-kind": kind, children: /* @__PURE__ */ jsx(Icon, { as: UPLOAD_FILE_KIND_GLYPHS[kind], size: "lg" }) })),
|
|
759
763
|
/* @__PURE__ */ jsxs("div", { className: "ui-upload-row-main", children: [
|
|
760
764
|
/* @__PURE__ */ jsx("div", { className: "truncate font-medium", children: item.name }),
|
|
761
765
|
/* @__PURE__ */ jsxs("div", { className: "text-muted-foreground text-xs", children: [
|
|
@@ -19,7 +19,7 @@ import type { AvatarProp } from "../../props/components/data-display.prop.js";
|
|
|
19
19
|
type ImageLoadingStatus = "idle" | "loading" | "loaded" | "error";
|
|
20
20
|
export declare const Avatar: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & {
|
|
21
21
|
asChild?: boolean;
|
|
22
|
-
} & Pick<AvatarProp, "appearance" | "presence" | "presenceLabel" | "shape"> & React.RefAttributes<HTMLSpanElement>>;
|
|
22
|
+
} & Pick<AvatarProp, "appearance" | "presence" | "presenceLabel" | "shape" | "size"> & React.RefAttributes<HTMLSpanElement>>;
|
|
23
23
|
export declare const AvatarImage: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, "ref"> & {
|
|
24
24
|
asChild?: boolean;
|
|
25
25
|
onLoadingStatusChange?: (status: ImageLoadingStatus) => void;
|
|
@@ -53,6 +53,7 @@ const Avatar = React.forwardRef(
|
|
|
53
53
|
({
|
|
54
54
|
className,
|
|
55
55
|
shape = "circle",
|
|
56
|
+
size = "md",
|
|
56
57
|
appearance = "default",
|
|
57
58
|
presence,
|
|
58
59
|
presenceLabel,
|
|
@@ -73,6 +74,7 @@ const Avatar = React.forwardRef(
|
|
|
73
74
|
ref,
|
|
74
75
|
"data-slot": "avatar",
|
|
75
76
|
"data-shape": shape === "square" ? "square" : void 0,
|
|
77
|
+
"data-size": size === "md" ? void 0 : size,
|
|
76
78
|
"data-appearance": appearance === "tinted" ? "tinted" : void 0,
|
|
77
79
|
"data-presence": presence,
|
|
78
80
|
className: cn("ui-avatar", className),
|
|
@@ -42,8 +42,13 @@ export type SegmentedProp = {
|
|
|
42
42
|
block?: boolean;
|
|
43
43
|
/** antd `vertical` — stack the choices in a column. Arrow keys follow the axis. */
|
|
44
44
|
vertical?: boolean;
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Control height tier: `md` (default), `xs`, `sm` or `lg` — the same `--control-height-*` tier
|
|
47
|
+
* every other control reads. `xs` is the fourth step (gh#719, the one Toggle/ToggleGroup took in
|
|
48
|
+
* gh#716): a 24px-dense row can carry a real segmented control instead of a hand-rolled row of
|
|
49
|
+
* Buttons.
|
|
50
|
+
*/
|
|
51
|
+
size?: "xs" | "sm" | "md" | "lg";
|
|
47
52
|
/** Disable the whole group. */
|
|
48
53
|
disabled?: boolean;
|
|
49
54
|
/** Form field name — submits the selected value with the form. */
|
|
@@ -2,7 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const toggleVariants: (props?: ({
|
|
4
4
|
variant?: "default" | "outline" | null | undefined;
|
|
5
|
-
size?: "lg" | "md" | "sm" | null | undefined;
|
|
5
|
+
size?: "lg" | "md" | "sm" | "xs" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
/**
|
|
8
8
|
* Not exported on its own — it is folded into `ToggleProp` and `ToggleGroupItemProp` so there is
|
|
@@ -69,7 +69,7 @@ export type ToggleProp = React.ComponentPropsWithoutRef<"button"> & TogglePresse
|
|
|
69
69
|
export type ToggleProps = ToggleProp;
|
|
70
70
|
export declare const Toggle: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> & TogglePressedFields & VariantProps<(props?: ({
|
|
71
71
|
variant?: "default" | "outline" | null | undefined;
|
|
72
|
-
size?: "lg" | "md" | "sm" | null | undefined;
|
|
72
|
+
size?: "lg" | "md" | "sm" | "xs" | null | undefined;
|
|
73
73
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string> & ToggleCountFields & React.RefAttributes<HTMLButtonElement>>;
|
|
74
74
|
export { toggleVariants, useCounterPill, restoreDomProps };
|
|
75
75
|
export type { ToggleCountFields, TogglePressedFields };
|
|
@@ -13,6 +13,10 @@ const toggleVariants = cva("ui-toggle", {
|
|
|
13
13
|
outline: "ui-toggle-outline"
|
|
14
14
|
},
|
|
15
15
|
size: {
|
|
16
|
+
// The fourth step of the shared control ladder (gh#716): 24px, `--control-height-xs`, the
|
|
17
|
+
// same tier `Button size="xs"` reads — so a 24px-dense row can carry a segmented control
|
|
18
|
+
// instead of someone hand-rolling one out of Buttons.
|
|
19
|
+
xs: "ui-toggle-xs",
|
|
16
20
|
sm: "ui-toggle-sm",
|
|
17
21
|
md: "ui-toggle-default-size",
|
|
18
22
|
lg: "ui-toggle-lg"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "27.
|
|
3
|
+
"version": "27.5.0",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -208,6 +208,20 @@ export type DescriptionsItemProp = {
|
|
|
208
208
|
*/
|
|
209
209
|
export type AvatarProp = React.ComponentPropsWithoutRef<"span"> & {
|
|
210
210
|
shape?: AvatarShapeProp;
|
|
211
|
+
/**
|
|
212
|
+
* Box size on the shared control ladder — `md` (default) is `--control-height`, and `xs`/`sm`/
|
|
213
|
+
* `lg` are `--control-height-xs/-sm/-lg`, the SAME tier Button and Input read. State it when the
|
|
214
|
+
* mark has to sit in a row the row's height already decided: inside an `icon-sm` trigger, in a
|
|
215
|
+
* 24px dense table row, beside a `size="sm"` Button. Before gh#716 the box was welded to
|
|
216
|
+
* `--control-height`, so a 32px mark inside a 28px trigger overflowed it and the only legal move
|
|
217
|
+
* left was raising the whole row.
|
|
218
|
+
*
|
|
219
|
+
* The initials' type step and a glyph's box move with the box — one step of the type scale and
|
|
220
|
+
* one step of the `--icon-size-*` scale per step — so a small mark is small, not clipped.
|
|
221
|
+
* `shape="square"` rides the identical ladder. Beyond the ladder (a 96px profile mark) is a
|
|
222
|
+
* `className` size utility, exactly as before; the ladder is for the control row.
|
|
223
|
+
*/
|
|
224
|
+
size?: SizeProp;
|
|
211
225
|
appearance?: AvatarAppearanceProp;
|
|
212
226
|
/**
|
|
213
227
|
* Presence — WHO is reachable right now, drawn as an indicator at the block-end/inline-end
|
|
@@ -1464,9 +1464,34 @@ export type SelectDataProp = SearchSelectBaseProp & {
|
|
|
1464
1464
|
export type UploadFileItemProp = UploadFileItem;
|
|
1465
1465
|
/** @see Upload */
|
|
1466
1466
|
export type UploadVariantProp = "dropzone" | "button" | "picture-card" | "picture" | "avatar" | "avatar-crop";
|
|
1467
|
+
/**
|
|
1468
|
+
* Upload `listType` — HOW THE CHOSEN FILES ARE LISTED, ported from antd (gh#720).
|
|
1469
|
+
*
|
|
1470
|
+
* antd separates two axes and this component had only one: `variant` says how a file is PICKED
|
|
1471
|
+
* (drag area, button, tile grid, avatar), `listType` says how the picked ones are DRAWN. Welding
|
|
1472
|
+
* the second onto the first meant a `variant="dropzone"` could not show a thumbnail even for an
|
|
1473
|
+
* item that already carried a `previewUrl`, and its rows carried no per-type mark at all — a
|
|
1474
|
+
* drawer listing a `.png` beside a `.json` and a `.txt` could draw only three identical
|
|
1475
|
+
* name-and-size rows.
|
|
1476
|
+
*
|
|
1477
|
+
* - `text` — name, size, actions. The row every dropzone/button list has always drawn.
|
|
1478
|
+
* - `picture` — a leading box on every row: the thumbnail when the item has a `previewUrl`, and
|
|
1479
|
+
* otherwise the glyph for its file kind (image / pdf / archive / text / generic). Both boxes are
|
|
1480
|
+
* the same size, so a mixed list keeps one row height.
|
|
1481
|
+
*
|
|
1482
|
+
* antd's third value, `picture-card`, is `variant="picture-card"` here: in that variant the tile
|
|
1483
|
+
* grid IS the picker (the add tile sits inside the grid), so it is not a listing that can be put
|
|
1484
|
+
* under a different trigger. It is deliberately not offered as a `listType`.
|
|
1485
|
+
*/
|
|
1486
|
+
export type UploadListTypeProp = "text" | "picture";
|
|
1467
1487
|
/** @see Upload — presentational; wire `onUpload` to media-service in app api.ts */
|
|
1468
1488
|
export type UploadProp = FieldA11yProps & {
|
|
1469
1489
|
variant?: UploadVariantProp;
|
|
1490
|
+
/**
|
|
1491
|
+
* @see UploadListTypeProp — an axis of its own, independent of `variant`. Defaults to the
|
|
1492
|
+
* listing each variant already drew: `picture` for `variant="picture"`, `text` everywhere else.
|
|
1493
|
+
*/
|
|
1494
|
+
listType?: UploadListTypeProp;
|
|
1470
1495
|
value?: ValueProp<UploadFileItemProp[]>;
|
|
1471
1496
|
defaultValue?: DefaultValueProp<UploadFileItemProp[]>;
|
|
1472
1497
|
onValueChange?: OnValueChangeProp<UploadFileItemProp[]>;
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -1650,6 +1650,11 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
1650
1650
|
readonly file: "components/data-entry.prop.ts";
|
|
1651
1651
|
readonly vocabulary: readonly [];
|
|
1652
1652
|
};
|
|
1653
|
+
readonly UploadListTypeProp: {
|
|
1654
|
+
readonly group: "data-entry";
|
|
1655
|
+
readonly file: "components/data-entry.prop.ts";
|
|
1656
|
+
readonly vocabulary: readonly [];
|
|
1657
|
+
};
|
|
1653
1658
|
readonly TreeOptionProp: {
|
|
1654
1659
|
readonly group: "data-entry";
|
|
1655
1660
|
readonly file: "components/data-entry.prop.ts";
|
|
@@ -1708,7 +1713,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
1708
1713
|
readonly AvatarProp: {
|
|
1709
1714
|
readonly group: "data-display";
|
|
1710
1715
|
readonly file: "components/data-display.prop.ts";
|
|
1711
|
-
readonly vocabulary: readonly ["AvatarShapeProp", "LabelProp", "ChildrenProp", "ClassNameProp"];
|
|
1716
|
+
readonly vocabulary: readonly ["AvatarShapeProp", "SizeProp", "LabelProp", "ChildrenProp", "ClassNameProp"];
|
|
1712
1717
|
};
|
|
1713
1718
|
readonly AvatarAppearanceProp: {
|
|
1714
1719
|
readonly group: "data-display";
|
package/dist/props/registry.js
CHANGED
|
@@ -1935,6 +1935,11 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
1935
1935
|
vocabulary: []
|
|
1936
1936
|
},
|
|
1937
1937
|
UploadVariantProp: { group: "data-entry", file: "components/data-entry.prop.ts", vocabulary: [] },
|
|
1938
|
+
UploadListTypeProp: {
|
|
1939
|
+
group: "data-entry",
|
|
1940
|
+
file: "components/data-entry.prop.ts",
|
|
1941
|
+
vocabulary: []
|
|
1942
|
+
},
|
|
1938
1943
|
TreeOptionProp: {
|
|
1939
1944
|
group: "data-entry",
|
|
1940
1945
|
file: "components/data-entry.prop.ts",
|
|
@@ -2042,7 +2047,7 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
2042
2047
|
AvatarProp: {
|
|
2043
2048
|
group: "data-display",
|
|
2044
2049
|
file: "components/data-display.prop.ts",
|
|
2045
|
-
vocabulary: ["AvatarShapeProp", "LabelProp", "ChildrenProp", "ClassNameProp"]
|
|
2050
|
+
vocabulary: ["AvatarShapeProp", "SizeProp", "LabelProp", "ChildrenProp", "ClassNameProp"]
|
|
2046
2051
|
},
|
|
2047
2052
|
AvatarAppearanceProp: {
|
|
2048
2053
|
group: "data-display",
|
package/dist/styles/control.css
CHANGED
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
padding-inline: var(--space-3);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
.ui-toggle-xs {
|
|
35
|
+
min-height: var(--control-height-xs);
|
|
36
|
+
padding-inline: var(--space-1);
|
|
37
|
+
font-size: var(--toggle-xs-font-size);
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
.ui-toggle-sm {
|
|
35
41
|
min-height: var(--control-height-sm);
|
|
36
42
|
padding-inline: var(--space-2);
|
|
@@ -3135,6 +3141,12 @@
|
|
|
3135
3141
|
justify-content: flex-start;
|
|
3136
3142
|
}
|
|
3137
3143
|
|
|
3144
|
+
.ui-segmented[data-size="xs"] {
|
|
3145
|
+
--control-height: var(--control-height-xs);
|
|
3146
|
+
--segmented-item-padding-inline: var(--segmented-xs-item-padding-inline);
|
|
3147
|
+
font-size: var(--segmented-xs-font-size);
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3138
3150
|
.ui-segmented[data-size="sm"] {
|
|
3139
3151
|
--control-height: var(--control-height-sm);
|
|
3140
3152
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
.ui-avatar {
|
|
3
3
|
position: relative;
|
|
4
4
|
display: inline-flex;
|
|
5
|
-
width: var(--
|
|
6
|
-
height: var(--
|
|
5
|
+
width: var(--avatar-size);
|
|
6
|
+
height: var(--avatar-size);
|
|
7
7
|
flex-shrink: 0;
|
|
8
8
|
align-items: center;
|
|
9
9
|
justify-content: center;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
linear-gradient(var(--avatar-tint), var(--avatar-tint)),
|
|
14
14
|
var(--avatar-background, hsl(var(--muted)));
|
|
15
15
|
color: hsl(var(--muted-foreground));
|
|
16
|
-
font-size: var(--font-size
|
|
16
|
+
font-size: var(--avatar-font-size);
|
|
17
17
|
font-weight: var(--font-weight-medium);
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -36,6 +36,36 @@
|
|
|
36
36
|
flex: none;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
.ui-avatar[data-size="xs"] {
|
|
40
|
+
--avatar-size: var(--avatar-size-xs);
|
|
41
|
+
--avatar-square-size: var(--avatar-size-xs);
|
|
42
|
+
--avatar-font-size: var(--avatar-font-size-xs);
|
|
43
|
+
--avatar-glyph-size: var(--avatar-glyph-size-xs);
|
|
44
|
+
--avatar-tinted-glyph-size: var(--avatar-glyph-size-xs);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.ui-avatar[data-size="sm"] {
|
|
48
|
+
--avatar-size: var(--avatar-size-sm);
|
|
49
|
+
--avatar-square-size: var(--avatar-size-sm);
|
|
50
|
+
--avatar-font-size: var(--avatar-font-size-sm);
|
|
51
|
+
--avatar-glyph-size: var(--avatar-glyph-size-sm);
|
|
52
|
+
--avatar-tinted-glyph-size: var(--avatar-glyph-size-sm);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.ui-avatar[data-size="lg"] {
|
|
56
|
+
--avatar-size: var(--avatar-size-lg);
|
|
57
|
+
--avatar-square-size: var(--avatar-size-lg);
|
|
58
|
+
--avatar-font-size: var(--avatar-font-size-lg);
|
|
59
|
+
--avatar-glyph-size: var(--avatar-glyph-size-lg);
|
|
60
|
+
--avatar-tinted-glyph-size: var(--avatar-glyph-size-lg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ui-avatar[data-size] svg {
|
|
64
|
+
inline-size: var(--avatar-glyph-size);
|
|
65
|
+
block-size: var(--avatar-glyph-size);
|
|
66
|
+
flex: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
39
69
|
.ui-avatar-image {
|
|
40
70
|
width: 100%;
|
|
41
71
|
height: 100%;
|
|
@@ -1066,8 +1096,8 @@
|
|
|
1066
1096
|
}
|
|
1067
1097
|
|
|
1068
1098
|
.ui-prose a {
|
|
1069
|
-
color: hsl(var(--primary));
|
|
1070
|
-
text-decoration:
|
|
1099
|
+
color: hsl(var(--prose-link-color, var(--primary)));
|
|
1100
|
+
text-decoration-line: var(--prose-link-decoration-line);
|
|
1071
1101
|
}
|
|
1072
1102
|
|
|
1073
1103
|
.ui-prose table {
|
|
@@ -199,6 +199,18 @@
|
|
|
199
199
|
flex-shrink: 0;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
.ui-upload-list-glyph {
|
|
203
|
+
display: flex;
|
|
204
|
+
align-items: center;
|
|
205
|
+
justify-content: center;
|
|
206
|
+
inline-size: var(--control-height-lg);
|
|
207
|
+
block-size: var(--control-height-lg);
|
|
208
|
+
border-radius: var(--radius);
|
|
209
|
+
background-color: hsl(var(--muted));
|
|
210
|
+
color: hsl(var(--muted-foreground));
|
|
211
|
+
flex-shrink: 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
202
214
|
.ui-upload-picture-item {
|
|
203
215
|
inline-size: var(--upload-tile-size);
|
|
204
216
|
overflow-wrap: anywhere;
|
|
@@ -336,6 +348,11 @@
|
|
|
336
348
|
border-color: hsl(var(--primary));
|
|
337
349
|
}
|
|
338
350
|
|
|
351
|
+
.ui-upload-picture-empty-icon {
|
|
352
|
+
inline-size: var(--upload-picture-icon-size);
|
|
353
|
+
block-size: var(--upload-picture-icon-size);
|
|
354
|
+
}
|
|
355
|
+
|
|
339
356
|
.ui-upload-picture-empty-label {
|
|
340
357
|
margin-block-start: var(--upload-picture-empty-label-space-block-start);
|
|
341
358
|
font-size: var(--upload-picture-empty-label-font-size);
|
|
@@ -370,6 +387,11 @@
|
|
|
370
387
|
color: hsl(var(--muted-foreground));
|
|
371
388
|
}
|
|
372
389
|
|
|
390
|
+
.ui-upload-avatar-icon {
|
|
391
|
+
inline-size: var(--upload-avatar-icon-size);
|
|
392
|
+
block-size: var(--upload-avatar-icon-size);
|
|
393
|
+
}
|
|
394
|
+
|
|
373
395
|
.ui-upload-avatar-change {
|
|
374
396
|
position: absolute;
|
|
375
397
|
inset-inline: 0;
|
|
@@ -166,6 +166,7 @@
|
|
|
166
166
|
--search-input-label-font-size: var(--font-size-xs);
|
|
167
167
|
--tag-input-chip-font-size: var(--font-size-xs);
|
|
168
168
|
--toggle-sm-font-size: var(--font-size-xs);
|
|
169
|
+
--toggle-xs-font-size: var(--font-size-xs);
|
|
169
170
|
--button-sm-font-size: var(--font-size-xs);
|
|
170
171
|
}
|
|
171
172
|
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
--prose-code-font-size: calc(1em / var(--font-size-ratio));
|
|
31
31
|
--prose-code-space-inline: var(--space-1);
|
|
32
32
|
--prose-code-radius: var(--radius-sm);
|
|
33
|
+
|
|
34
|
+
--prose-link-color: initial;
|
|
35
|
+
|
|
36
|
+
--prose-link-decoration-line: underline;
|
|
33
37
|
--prose-image-radius: var(--radius-md);
|
|
34
38
|
--timeline-note-font-size: var(--font-size-xs);
|
|
35
39
|
|
|
@@ -37,6 +41,28 @@
|
|
|
37
41
|
|
|
38
42
|
--avatar-tint: transparent;
|
|
39
43
|
|
|
44
|
+
--avatar-size: var(--control-height);
|
|
45
|
+
|
|
46
|
+
--avatar-size-xs: var(--control-height-xs);
|
|
47
|
+
|
|
48
|
+
--avatar-size-sm: var(--control-height-sm);
|
|
49
|
+
|
|
50
|
+
--avatar-size-lg: var(--control-height-lg);
|
|
51
|
+
|
|
52
|
+
--avatar-font-size: var(--font-size-base);
|
|
53
|
+
|
|
54
|
+
--avatar-font-size-xs: var(--font-size-2xs);
|
|
55
|
+
|
|
56
|
+
--avatar-font-size-sm: var(--font-size-xs);
|
|
57
|
+
|
|
58
|
+
--avatar-font-size-lg: var(--font-size-lg);
|
|
59
|
+
|
|
60
|
+
--avatar-glyph-size-xs: calc(var(--icon-size-xs) * var(--scaling));
|
|
61
|
+
|
|
62
|
+
--avatar-glyph-size-sm: calc(var(--icon-size-sm) * var(--scaling));
|
|
63
|
+
|
|
64
|
+
--avatar-glyph-size-lg: calc(var(--icon-size-lg) * var(--scaling));
|
|
65
|
+
|
|
40
66
|
--avatar-square-radius: var(--radius-lg);
|
|
41
67
|
--avatar-square-size: var(--control-height);
|
|
42
68
|
--avatar-square-background: initial;
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
--segmented-item-padding-inline: calc(var(--control-padding-x) - 1px);
|
|
13
13
|
|
|
14
|
+
--segmented-xs-font-size: var(--font-size-xs);
|
|
15
|
+
|
|
16
|
+
--segmented-xs-item-padding-inline: calc(var(--control-padding-x-compact) - 1px);
|
|
17
|
+
|
|
14
18
|
--segmented-item-gap: calc(var(--space-3) / 2);
|
|
15
19
|
|
|
16
20
|
--segmented-item-color: var(--muted-foreground);
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
--upload-picture-radius: var(--radius-md);
|
|
31
31
|
--upload-picture-preview-max-height: 12rem;
|
|
32
32
|
--upload-picture-empty-height: 8rem;
|
|
33
|
+
|
|
34
|
+
--upload-picture-icon-size: var(--icon-size-2xl);
|
|
33
35
|
--upload-picture-empty-label-space-block-start: var(--space-2);
|
|
34
36
|
--upload-picture-empty-label-font-size: var(--font-size-sm);
|
|
35
37
|
--upload-picture-overlay-font-size: var(--font-size-sm);
|
|
@@ -41,6 +43,8 @@
|
|
|
41
43
|
--upload-picture-badge-font-size: var(--font-size-xs);
|
|
42
44
|
|
|
43
45
|
--upload-avatar-size: 6rem;
|
|
46
|
+
|
|
47
|
+
--upload-avatar-icon-size: var(--icon-size-xl);
|
|
44
48
|
--upload-avatar-border-width: var(--stroke-md);
|
|
45
49
|
--upload-avatar-change-space-block: var(--space-1);
|
|
46
50
|
|
package/docs/CUSTOMER-THEMING.md
CHANGED
|
@@ -213,6 +213,32 @@ Density (`<PageContainer density>`) scales component sizes, not the grid. - **Pi
|
|
|
213
213
|
|
|
214
214
|
---
|
|
215
215
|
|
|
216
|
+
## Selectors the package promises are YOURS
|
|
217
|
+
|
|
218
|
+
A `data-*` attribute the package never writes is a selector you can hold a stylesheet against — the one place where CSS of your own is the documented route rather than a workaround. Each of these is a promise held by a test, not an accident of the current markup:
|
|
219
|
+
|
|
220
|
+
| You write | The package promises | Why it exists |
|
|
221
|
+
| --- | --- | --- |
|
|
222
|
+
| `.ui-prose a[data-…]` | Prose writes `data-*` on its **own root only** — never on a descendant. Every `data-*` on an `a` inside it is yours, survives the render, and stays selectable (gh#717) | A renderer knows things about a link that the package cannot: in a wiki, a link whose target **does not exist yet** must read differently from one that resolves |
|
|
223
|
+
| `<TableRow data-expanded-row="">` | The striping counts records, not DOM rows — a row you mark is skipped in the count and wears its record's stripe | A detail row under a record is your composition, not a row of data |
|
|
224
|
+
| `[data-tenant]`, `[data-program]` | Every anchor token re-resolves at the scope (below) | One app, many brands |
|
|
225
|
+
|
|
226
|
+
**Style a marked link through the knob, not around it.** `--prose-link-color` is read as `hsl(var(--prose-link-color, var(--primary)))` **at the anchor**, so declaring the custom property inside your own higher-specificity selector is enough — you never have to restate the `color` declaration, and a later package change to how the ink is painted still reaches you:
|
|
227
|
+
|
|
228
|
+
```css
|
|
229
|
+
/* your app's stylesheet */
|
|
230
|
+
.ui-prose a[data-unresolved="true"] {
|
|
231
|
+
--prose-link-color: var(--text-error); /* the TEXT tier — 7.25:1 light, 5.51:1 dark on a card */
|
|
232
|
+
text-decoration-style: dashed;
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Use `--text-error`, not `--destructive`: the fill tier is tuned for a white label on top of it and measures 2.95:1 as ink on the dark card (gh#610). The whole family is `--prose-link-color` (default `hsl(var(--primary))`, resolved at the anchor so a scoped re-tint reaches it) and `--prose-link-decoration-line` (default `underline`). Set the first on `[data-tenant]` to re-tint every wiki link at once.
|
|
237
|
+
|
|
238
|
+
There is **no prop** naming the attribute, deliberately: the attribute IS the API, the same way `data-expanded-row` and `data-axe-open` are. A prop would make the package own a name only your renderer knows, and would let exactly one state be marked.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
216
242
|
## Multi-tenant (one app, many brands)
|
|
217
243
|
|
|
218
244
|
Scope the overrides under a tenant attribute instead of `:root`. The colour utilities are declared with `@theme inline`, so a scoped `--primary` (and the other anchors) **re-resolve at the element** — `bg-primary`, `text-success`, every component surface, the focus ring, the brand glow, gradients and the modal scrim all retint inside the scope:
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
CardTitle,
|
|
10
10
|
ListRow,
|
|
11
11
|
} from "@godxjp/ui/data-display";
|
|
12
|
-
import { Text } from "@godxjp/ui/general";
|
|
12
|
+
import { Button, Text } from "@godxjp/ui/general";
|
|
13
13
|
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
14
14
|
import { Building2, KeyRound, ShieldCheck, Sparkles } from "lucide-react";
|
|
15
15
|
|
|
@@ -34,7 +34,9 @@ import portraitSlate from "../assets/portrait-slate.svg";
|
|
|
34
34
|
/**
|
|
35
35
|
* Avatar — identity image with a readable fallback (users, teams, entities).
|
|
36
36
|
* Always compose AvatarImage + AvatarFallback so broken/missing images degrade
|
|
37
|
-
* gracefully. Size
|
|
37
|
+
* gracefully. Size on the control ladder with `size` (xs/sm/md/lg, the same
|
|
38
|
+
* --control-height tier Button reads); className only for sizes off that ladder.
|
|
39
|
+
* Composed only from real @godxjp/ui components.
|
|
38
40
|
*/
|
|
39
41
|
export default function Demo() {
|
|
40
42
|
return (
|
|
@@ -322,25 +324,140 @@ export default function Demo() {
|
|
|
322
324
|
</CardContent>
|
|
323
325
|
</Card>
|
|
324
326
|
|
|
327
|
+
{/* size — the control ladder (gh#716) */}
|
|
325
328
|
<Card>
|
|
326
329
|
<CardHeader>
|
|
327
|
-
<CardTitle level={2}
|
|
330
|
+
<CardTitle level={2}>サイズ · size</CardTitle>
|
|
328
331
|
<CardDescription>
|
|
329
|
-
|
|
330
|
-
|
|
332
|
+
xs 24px / sm 28px / md 32px(既定) / lg 36px。すべて Button や Input と同じ
|
|
333
|
+
--control-height の段なので、同じ段の Button
|
|
334
|
+
と並べたときに高さがそろう。イニシャルの級数もグリフの箱も同じ段で動くため、小さい
|
|
335
|
+
マークは「小さい」であって「切れている」ではない。shape="square"
|
|
336
|
+
も同じ段に乗る。 ラダーの外側のサイズ(プロフィール用の 96px マークなど)は従来どおり
|
|
337
|
+
className。
|
|
338
|
+
</CardDescription>
|
|
339
|
+
</CardHeader>
|
|
340
|
+
<CardContent>
|
|
341
|
+
<Flex direction="col" gap="md">
|
|
342
|
+
<Flex direction="row" wrap align="center" gap="md">
|
|
343
|
+
<Avatar size="xs">
|
|
344
|
+
<AvatarFallback>XS</AvatarFallback>
|
|
345
|
+
</Avatar>
|
|
346
|
+
<Avatar size="sm">
|
|
347
|
+
<AvatarFallback>SM</AvatarFallback>
|
|
348
|
+
</Avatar>
|
|
349
|
+
<Avatar>
|
|
350
|
+
<AvatarFallback>MD</AvatarFallback>
|
|
351
|
+
</Avatar>
|
|
352
|
+
<Avatar size="lg">
|
|
353
|
+
<AvatarFallback>LG</AvatarFallback>
|
|
354
|
+
</Avatar>
|
|
355
|
+
</Flex>
|
|
356
|
+
{/* square rides the identical ladder */}
|
|
357
|
+
<Flex direction="row" wrap align="center" gap="md">
|
|
358
|
+
<Avatar size="xs" shape="square">
|
|
359
|
+
<AvatarFallback>山</AvatarFallback>
|
|
360
|
+
</Avatar>
|
|
361
|
+
<Avatar size="sm" shape="square">
|
|
362
|
+
<AvatarFallback>山</AvatarFallback>
|
|
363
|
+
</Avatar>
|
|
364
|
+
<Avatar shape="square">
|
|
365
|
+
<AvatarFallback>山</AvatarFallback>
|
|
366
|
+
</Avatar>
|
|
367
|
+
<Avatar size="lg" shape="square">
|
|
368
|
+
<AvatarFallback>山</AvatarFallback>
|
|
369
|
+
</Avatar>
|
|
370
|
+
</Flex>
|
|
371
|
+
{/* the glyph steps with the box, so a medallion is never clipped */}
|
|
372
|
+
<Flex direction="row" wrap align="center" gap="md">
|
|
373
|
+
<Avatar size="xs" shape="square" appearance="tinted">
|
|
374
|
+
<AvatarFallback>
|
|
375
|
+
<ShieldCheck aria-hidden="true" />
|
|
376
|
+
</AvatarFallback>
|
|
377
|
+
</Avatar>
|
|
378
|
+
<Avatar size="sm" shape="square" appearance="tinted">
|
|
379
|
+
<AvatarFallback>
|
|
380
|
+
<ShieldCheck aria-hidden="true" />
|
|
381
|
+
</AvatarFallback>
|
|
382
|
+
</Avatar>
|
|
383
|
+
<Avatar shape="square" appearance="tinted">
|
|
384
|
+
<AvatarFallback>
|
|
385
|
+
<ShieldCheck aria-hidden="true" />
|
|
386
|
+
</AvatarFallback>
|
|
387
|
+
</Avatar>
|
|
388
|
+
<Avatar size="lg" shape="square" appearance="tinted">
|
|
389
|
+
<AvatarFallback>
|
|
390
|
+
<ShieldCheck aria-hidden="true" />
|
|
391
|
+
</AvatarFallback>
|
|
392
|
+
</Avatar>
|
|
393
|
+
</Flex>
|
|
394
|
+
</Flex>
|
|
395
|
+
</CardContent>
|
|
396
|
+
</Card>
|
|
397
|
+
|
|
398
|
+
{/* The row that could not be built before #716 */}
|
|
399
|
+
<Card>
|
|
400
|
+
<CardHeader>
|
|
401
|
+
<CardTitle level={2}>行に入るマーク · size="sm" と icon-sm</CardTitle>
|
|
402
|
+
<CardDescription>
|
|
403
|
+
行の高さが先に決まっている場所(28px の icon-sm トリガ、24/28px
|
|
404
|
+
の密なテーブル行)では、マークの側が行に合わせる。#716 以前は箱が --control-height
|
|
405
|
+
に固定されていたため、28px のトリガに 32px
|
|
406
|
+
のマークが入って外へはみ出し、消費側に残された手は「行全体を 32px
|
|
407
|
+
に上げる」だけだった。 下の 1 段目は同じ段の Button と並べたもの、2 段目は icon-sm
|
|
408
|
+
トリガの中に入れたもの。
|
|
409
|
+
</CardDescription>
|
|
410
|
+
</CardHeader>
|
|
411
|
+
<CardContent>
|
|
412
|
+
<Flex direction="col" gap="md">
|
|
413
|
+
<Flex direction="row" wrap align="center" gap="sm">
|
|
414
|
+
<Avatar size="sm">
|
|
415
|
+
<AvatarImage src={portraitSlate} alt="" />
|
|
416
|
+
<AvatarFallback>田</AvatarFallback>
|
|
417
|
+
</Avatar>
|
|
418
|
+
<Button size="sm" variant="outline">
|
|
419
|
+
担当者を変更
|
|
420
|
+
</Button>
|
|
421
|
+
<Avatar size="xs">
|
|
422
|
+
<AvatarFallback>佐</AvatarFallback>
|
|
423
|
+
</Avatar>
|
|
424
|
+
<Button size="xs" variant="outline">
|
|
425
|
+
履歴
|
|
426
|
+
</Button>
|
|
427
|
+
</Flex>
|
|
428
|
+
<Flex direction="row" wrap align="center" gap="sm">
|
|
429
|
+
<Button size="icon-sm" variant="ghost" aria-label="担当者 田中 未来">
|
|
430
|
+
<Avatar size="sm">
|
|
431
|
+
<AvatarImage src={portraitMoss} alt="" />
|
|
432
|
+
<AvatarFallback>田</AvatarFallback>
|
|
433
|
+
</Avatar>
|
|
434
|
+
</Button>
|
|
435
|
+
<Button size="icon-xs" variant="ghost" aria-label="担当者 佐藤 玲">
|
|
436
|
+
<Avatar size="xs">
|
|
437
|
+
<AvatarFallback>佐</AvatarFallback>
|
|
438
|
+
</Avatar>
|
|
439
|
+
</Button>
|
|
440
|
+
<Text as="p" size="xs" tone="muted">
|
|
441
|
+
マークはトリガの内寸に収まる(28px の中に 28px、24px の中に 24px)。
|
|
442
|
+
</Text>
|
|
443
|
+
</Flex>
|
|
444
|
+
</Flex>
|
|
445
|
+
</CardContent>
|
|
446
|
+
</Card>
|
|
447
|
+
|
|
448
|
+
<Card>
|
|
449
|
+
<CardHeader>
|
|
450
|
+
<CardTitle level={2}>ラダーの外 · className</CardTitle>
|
|
451
|
+
<CardDescription>
|
|
452
|
+
コントロール行の段に無いサイズ(プロフィールヘッダの大きなマークなど)は、従来どおり
|
|
453
|
+
size-* ユーティリティで指定する。
|
|
331
454
|
</CardDescription>
|
|
332
455
|
</CardHeader>
|
|
333
456
|
<CardContent>
|
|
334
457
|
<Flex direction="row" wrap align="center" gap="md">
|
|
335
|
-
<Avatar className="size-8">
|
|
336
|
-
<AvatarFallback>S</AvatarFallback>
|
|
337
|
-
</Avatar>
|
|
338
458
|
<Avatar className="size-10">
|
|
339
459
|
<AvatarFallback>M</AvatarFallback>
|
|
340
460
|
</Avatar>
|
|
341
|
-
<Avatar>
|
|
342
|
-
<AvatarFallback>D</AvatarFallback>
|
|
343
|
-
</Avatar>
|
|
344
461
|
<Avatar className="size-12">
|
|
345
462
|
<AvatarFallback>L</AvatarFallback>
|
|
346
463
|
</Avatar>
|
|
@@ -13,6 +13,34 @@ import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
|
13
13
|
* semantic HTML inside it from the tokens; the HTML can come from react-markdown, a sanitised
|
|
14
14
|
* string or JSX. Composed only from real @godxjp/ui components.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A CONSUMER'S OWN STYLESHEET — the file their app already has, not part of the package. It is
|
|
19
|
+
* inlined here so the third card below is the real thing rather than a description of it.
|
|
20
|
+
*
|
|
21
|
+
* Both halves of the gh#717 contract are exercised:
|
|
22
|
+
* · `--prose-link-*` — the service tunes every link in its wiki once, through the knob;
|
|
23
|
+
* · `a[data-unresolved]` — Prose writes `data-*` on its own ROOT only, so every `data-*` on a
|
|
24
|
+
* descendant `a` is the consumer's and stays selectable. That is a promise, held by
|
|
25
|
+
* src/components/data-display/__tests__/prose-link-717.test.tsx.
|
|
26
|
+
*
|
|
27
|
+
* Measured on the card surface (#fdfdfc light / #21201c dark):
|
|
28
|
+
* resolved --text-link #6400d4 → 8.29:1 light · #dcbcff → 9.85:1 dark
|
|
29
|
+
* unresolved --text-error #aa181f → 7.25:1 light · #eb7076 → 5.51:1 dark
|
|
30
|
+
* The TEXT tier, never `--destructive`: that is the FILL tier, tuned for a white label on top of
|
|
31
|
+
* it, and it measures 2.95:1 as ink on the dark card (gh#610).
|
|
32
|
+
*/
|
|
33
|
+
const WIKI_STYLESHEET = `
|
|
34
|
+
[data-wiki] {
|
|
35
|
+
--prose-link-color: var(--text-link);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
[data-wiki] a[data-unresolved="true"] {
|
|
39
|
+
--prose-link-color: var(--text-error);
|
|
40
|
+
text-decoration-style: dashed;
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
|
|
16
44
|
export default function Demo() {
|
|
17
45
|
return (
|
|
18
46
|
<PageContainer title="Prose" subtitle="レンダリングされた本文のタイポグラフィ">
|
|
@@ -99,6 +127,42 @@ export default function Demo() {
|
|
|
99
127
|
</Prose>
|
|
100
128
|
</CardContent>
|
|
101
129
|
</Card>
|
|
130
|
+
|
|
131
|
+
<Card>
|
|
132
|
+
<CardHeader>
|
|
133
|
+
<CardTitle level={2}>Wiki body · リンクの状態 (--prose-link-*)</CardTitle>
|
|
134
|
+
<CardDescription>
|
|
135
|
+
リンクの色と下線は --prose-link-color / --prose-link-decoration-line で調整する。Prose
|
|
136
|
+
は自身のルート以外に data-* を書かないため、a[data-…]
|
|
137
|
+
は利用側が所有するセレクタとして保証される。ここではまだ存在しないページへのリンクを
|
|
138
|
+
--text-error +破線で示した。カード面でのコントラストは、解決済み 8.29:1 (ライト) /
|
|
139
|
+
9.85:1 (ダーク)、未解決 7.25:1 / 5.51:1。
|
|
140
|
+
</CardDescription>
|
|
141
|
+
</CardHeader>
|
|
142
|
+
<CardContent>
|
|
143
|
+
<style>{WIKI_STYLESHEET}</style>
|
|
144
|
+
<Prose size="md" data-wiki="">
|
|
145
|
+
<h2>出張費の精算</h2>
|
|
146
|
+
<p>
|
|
147
|
+
原則は <a href="#rules">精算規程</a> に従う。宿泊費の上限は地域区分で変わるため、
|
|
148
|
+
<a href="#lodging" data-unresolved="true">
|
|
149
|
+
宿泊費区分表
|
|
150
|
+
</a>{" "}
|
|
151
|
+
を参照すること(このページはまだ書かれていない)。
|
|
152
|
+
</p>
|
|
153
|
+
<ul>
|
|
154
|
+
<li>
|
|
155
|
+
<a href="#receipt">領収書の要件</a>
|
|
156
|
+
</li>
|
|
157
|
+
<li>
|
|
158
|
+
<a href="#foreign" data-unresolved="true">
|
|
159
|
+
海外出張の為替レート
|
|
160
|
+
</a>
|
|
161
|
+
</li>
|
|
162
|
+
</ul>
|
|
163
|
+
</Prose>
|
|
164
|
+
</CardContent>
|
|
165
|
+
</Card>
|
|
102
166
|
</Flex>
|
|
103
167
|
</PageContainer>
|
|
104
168
|
);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
|
|
3
3
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
|
|
4
|
-
import { FormField, Segmented } from "@godxjp/ui/data-entry";
|
|
5
|
-
import { Text, VisuallyHidden } from "@godxjp/ui/general";
|
|
4
|
+
import { FormField, Segmented, ToggleGroup, ToggleGroupItem } from "@godxjp/ui/data-entry";
|
|
5
|
+
import { Button, Text, VisuallyHidden } from "@godxjp/ui/general";
|
|
6
6
|
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
7
7
|
import { CalendarDays, Columns3, LayoutList, Monitor, Moon, Sun } from "lucide-react";
|
|
8
8
|
|
|
@@ -110,14 +110,25 @@ export default function Demo() {
|
|
|
110
110
|
<CardHeader>
|
|
111
111
|
<CardTitle level={2}>size · vertical · block</CardTitle>
|
|
112
112
|
<CardDescription>
|
|
113
|
-
size は
|
|
114
|
-
MobileShell
|
|
115
|
-
vertical では 1 行ぶんが 1
|
|
113
|
+
size は 4 段(xs 24px / sm 28px / md 32px / lg 36px)。トラックの高さは
|
|
114
|
+
--control-height そのもので、MobileShell のようにその変数をスコープする領域では
|
|
115
|
+
自動的にタッチ段(44px)になります。vertical では 1 行ぶんが 1
|
|
116
|
+
コントロールの高さになります。横 1 行のときだけトラック
|
|
116
117
|
内側の余白を引くので、積んだときは引きません。block は幅いっぱいに広げます。
|
|
117
118
|
</CardDescription>
|
|
118
119
|
</CardHeader>
|
|
119
120
|
<CardContent>
|
|
120
121
|
<Flex direction="col" gap="md" align="start">
|
|
122
|
+
<Segmented
|
|
123
|
+
aria-label="サイズ xs"
|
|
124
|
+
size="xs"
|
|
125
|
+
defaultValue="a"
|
|
126
|
+
options={[
|
|
127
|
+
{ value: "a", label: "小" },
|
|
128
|
+
{ value: "b", label: "中" },
|
|
129
|
+
{ value: "c", label: "大" },
|
|
130
|
+
]}
|
|
131
|
+
/>
|
|
121
132
|
<Segmented
|
|
122
133
|
aria-label="サイズ sm"
|
|
123
134
|
size="sm"
|
|
@@ -162,6 +173,45 @@ export default function Demo() {
|
|
|
162
173
|
</CardContent>
|
|
163
174
|
</Card>
|
|
164
175
|
|
|
176
|
+
<Card>
|
|
177
|
+
<CardHeader>
|
|
178
|
+
<CardTitle level={2}>24px の密な行 · size="xs"</CardTitle>
|
|
179
|
+
<CardDescription>
|
|
180
|
+
行の高さが 24px に決まっている監査ログや明細ツールバーでは、xs
|
|
181
|
+
のトラックがそのまま収まる。同じ段の ToggleGroup size="xs" と Button
|
|
182
|
+
size="xs" と高さが一致するので、行が段差にならない。ここで xs
|
|
183
|
+
が無かったころは Button を並べて自作するしかなく、radiogroup
|
|
184
|
+
の読み上げも矢印キーも失われていた。
|
|
185
|
+
</CardDescription>
|
|
186
|
+
</CardHeader>
|
|
187
|
+
<CardContent>
|
|
188
|
+
<Flex direction="col" gap="sm">
|
|
189
|
+
<Flex direction="row" gap="sm" align="center">
|
|
190
|
+
<Segmented
|
|
191
|
+
aria-label="表示期間"
|
|
192
|
+
size="xs"
|
|
193
|
+
defaultValue="week"
|
|
194
|
+
options={[
|
|
195
|
+
{ value: "day", label: "日" },
|
|
196
|
+
{ value: "week", label: "週" },
|
|
197
|
+
{ value: "month", label: "月" },
|
|
198
|
+
]}
|
|
199
|
+
/>
|
|
200
|
+
<ToggleGroup type="single" size="xs" defaultValue="list" aria-label="表示形式">
|
|
201
|
+
<ToggleGroupItem value="list">一覧</ToggleGroupItem>
|
|
202
|
+
<ToggleGroupItem value="board">ボード</ToggleGroupItem>
|
|
203
|
+
</ToggleGroup>
|
|
204
|
+
<Button size="xs" variant="outline">
|
|
205
|
+
絞り込み
|
|
206
|
+
</Button>
|
|
207
|
+
</Flex>
|
|
208
|
+
<Text as="p" size="xs" tone="muted">
|
|
209
|
+
いずれも --control-height-xs(24px)。
|
|
210
|
+
</Text>
|
|
211
|
+
</Flex>
|
|
212
|
+
</CardContent>
|
|
213
|
+
</Card>
|
|
214
|
+
|
|
165
215
|
<Card>
|
|
166
216
|
<CardHeader>
|
|
167
217
|
<CardTitle level={2}>短い記号を出して、長い名前を読ませる</CardTitle>
|
|
@@ -129,14 +129,26 @@ export default function Demo() {
|
|
|
129
129
|
|
|
130
130
|
<Card>
|
|
131
131
|
<CardHeader>
|
|
132
|
-
<CardTitle level={2}>size · sm / md / lg</CardTitle>
|
|
132
|
+
<CardTitle level={2}>size · xs / sm / md / lg</CardTitle>
|
|
133
133
|
<CardDescription>
|
|
134
|
-
size も ToggleGroup にだけ指定すれば全アイテムへ伝播する。md
|
|
135
|
-
|
|
134
|
+
size も ToggleGroup にだけ指定すれば全アイテムへ伝播する。md が既定。xs 24px / sm 28px
|
|
135
|
+
/ md 32px / lg 36px はすべて --control-height
|
|
136
|
+
の同じ段なので、行の高さが先に決まっている場所ではその段を選ぶ。xs は 24px
|
|
137
|
+
の密な行にセグメントを載せるための段。
|
|
136
138
|
</CardDescription>
|
|
137
139
|
</CardHeader>
|
|
138
140
|
<CardContent>
|
|
139
141
|
<Flex direction="col" gap="md">
|
|
142
|
+
<Flex direction="col" gap="sm">
|
|
143
|
+
<Text as="p" size="sm" tone="muted">
|
|
144
|
+
size="xs" · 24px の密な行
|
|
145
|
+
</Text>
|
|
146
|
+
<ToggleGroup type="single" size="xs" defaultValue="day">
|
|
147
|
+
<ToggleGroupItem value="day">日次</ToggleGroupItem>
|
|
148
|
+
<ToggleGroupItem value="month">月次</ToggleGroupItem>
|
|
149
|
+
<ToggleGroupItem value="year">年次</ToggleGroupItem>
|
|
150
|
+
</ToggleGroup>
|
|
151
|
+
</Flex>
|
|
140
152
|
<Flex direction="col" gap="sm">
|
|
141
153
|
<Text as="p" size="sm" tone="muted">
|
|
142
154
|
size="sm"
|
|
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|
|
2
2
|
|
|
3
3
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
|
|
4
4
|
import { Toggle } from "@godxjp/ui/data-entry";
|
|
5
|
+
import { Button, Text } from "@godxjp/ui/general";
|
|
5
6
|
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -73,10 +74,18 @@ export default function Demo() {
|
|
|
73
74
|
<Card>
|
|
74
75
|
<CardHeader>
|
|
75
76
|
<CardTitle level={2}>Sizes</CardTitle>
|
|
76
|
-
<CardDescription>
|
|
77
|
+
<CardDescription>
|
|
78
|
+
xs 24px / sm 28px / md 32px / lg 36px · すべて --control-height
|
|
79
|
+
の同じ段。行の高さが先に決まっている場所では、その段を選ぶ。xs は 24px
|
|
80
|
+
の密な行にセグメント状のチップを載せるための段で、Button
|
|
81
|
+
を並べて自作する代わりになる。
|
|
82
|
+
</CardDescription>
|
|
77
83
|
</CardHeader>
|
|
78
84
|
<CardContent>
|
|
79
85
|
<Flex direction="row" gap="md" align="center">
|
|
86
|
+
<Toggle size="xs" aria-label="極小サイズ">
|
|
87
|
+
極小
|
|
88
|
+
</Toggle>
|
|
80
89
|
<Toggle size="sm" aria-label="小サイズ">
|
|
81
90
|
小
|
|
82
91
|
</Toggle>
|
|
@@ -90,6 +99,35 @@ export default function Demo() {
|
|
|
90
99
|
</CardContent>
|
|
91
100
|
</Card>
|
|
92
101
|
|
|
102
|
+
<Card>
|
|
103
|
+
<CardHeader>
|
|
104
|
+
<CardTitle level={2}>24px の密な行 · size="xs"</CardTitle>
|
|
105
|
+
<CardDescription>
|
|
106
|
+
行の高さが 24px に決まっている監査ログや明細ツールバーでは、xs
|
|
107
|
+
のチップがそのまま収まる。同じ段の Button size="xs"
|
|
108
|
+
と高さが一致するので、行が段差にならない。
|
|
109
|
+
</CardDescription>
|
|
110
|
+
</CardHeader>
|
|
111
|
+
<CardContent>
|
|
112
|
+
<Flex direction="col" gap="sm">
|
|
113
|
+
<Flex direction="row" gap="sm" align="center">
|
|
114
|
+
<Toggle size="xs" defaultPressed aria-label="未読のみ">
|
|
115
|
+
未読のみ
|
|
116
|
+
</Toggle>
|
|
117
|
+
<Toggle size="xs" aria-label="添付あり">
|
|
118
|
+
添付あり
|
|
119
|
+
</Toggle>
|
|
120
|
+
<Button size="xs" variant="outline">
|
|
121
|
+
絞り込み
|
|
122
|
+
</Button>
|
|
123
|
+
</Flex>
|
|
124
|
+
<Text as="p" size="xs" tone="muted">
|
|
125
|
+
いずれも --control-height-xs(24px)。
|
|
126
|
+
</Text>
|
|
127
|
+
</Flex>
|
|
128
|
+
</CardContent>
|
|
129
|
+
</Card>
|
|
130
|
+
|
|
93
131
|
<Card>
|
|
94
132
|
<CardHeader>
|
|
95
133
|
<CardTitle level={2}>Count</CardTitle>
|
|
@@ -36,6 +36,25 @@ export default function Demo() {
|
|
|
36
36
|
const [pictureItem, setPictureItem] = useState<UploadFileItem[]>([]);
|
|
37
37
|
const [avatarItem, setAvatarItem] = useState<UploadFileItem[]>([]);
|
|
38
38
|
const [avatarCropItem, setAvatarCropItem] = useState<UploadFileItem[]>([]);
|
|
39
|
+
/** A test-result drawer: one screenshot, one machine-readable result, one log, one archive. */
|
|
40
|
+
const mixedItems: UploadFileItem[] = [
|
|
41
|
+
{
|
|
42
|
+
uid: "mixed-screenshot",
|
|
43
|
+
name: "screenshot.png",
|
|
44
|
+
size: 184 * 1024,
|
|
45
|
+
mimeType: "image/png",
|
|
46
|
+
status: "done",
|
|
47
|
+
previewUrl:
|
|
48
|
+
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='72' height='72'><rect width='72' height='72' fill='%23c9c6bd'/><circle cx='26' cy='26' r='9' fill='%238a877d'/></svg>",
|
|
49
|
+
},
|
|
50
|
+
// Server media whose thumbnail has not been generated yet: still an image, still marked as one.
|
|
51
|
+
{ uid: "mixed-original", name: "photo-original.png", size: 4 * 1024 * 1024, status: "done" },
|
|
52
|
+
{ uid: "mixed-result", name: "result.json", size: 12 * 1024, status: "done" },
|
|
53
|
+
{ uid: "mixed-log", name: "run.txt", size: 3 * 1024, status: "done" },
|
|
54
|
+
{ uid: "mixed-bundle", name: "evidence.zip", size: 2 * 1024 * 1024, status: "done" },
|
|
55
|
+
{ uid: "mixed-spec", name: "spec.pdf", size: 640 * 1024, status: "done" },
|
|
56
|
+
{ uid: "mixed-dump", name: "device-dump", size: 96 * 1024, status: "done" },
|
|
57
|
+
];
|
|
39
58
|
const failedItems: UploadFileItem[] = [
|
|
40
59
|
{
|
|
41
60
|
uid: "failed-invoice",
|
|
@@ -105,6 +124,25 @@ export default function Demo() {
|
|
|
105
124
|
</CardContent>
|
|
106
125
|
</Card>
|
|
107
126
|
|
|
127
|
+
<Card>
|
|
128
|
+
<CardHeader>
|
|
129
|
+
<CardTitle level={2}>List type · 添付ファイル一覧</CardTitle>
|
|
130
|
+
<CardDescription>
|
|
131
|
+
listType は variant と独立した軸です。dropzone のまま listType="picture"
|
|
132
|
+
にすると、画像はサムネイル、それ以外はファイル種別のアイコンで並びます。
|
|
133
|
+
</CardDescription>
|
|
134
|
+
</CardHeader>
|
|
135
|
+
<CardContent>
|
|
136
|
+
<Upload
|
|
137
|
+
variant="dropzone"
|
|
138
|
+
listType="picture"
|
|
139
|
+
value={mixedItems}
|
|
140
|
+
onValueChange={() => {}}
|
|
141
|
+
onDownload={() => {}}
|
|
142
|
+
/>
|
|
143
|
+
</CardContent>
|
|
144
|
+
</Card>
|
|
145
|
+
|
|
108
146
|
<Card>
|
|
109
147
|
<CardHeader>
|
|
110
148
|
<CardTitle level={2}>Error and recovery</CardTitle>
|