@tristan2828/ui-foundation 4.63.2 → 4.63.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -90,7 +90,7 @@ export function WidgetCard({ id, title, icon: Icon, headingLevel = 2, link, isLo
|
|
|
90
90
|
return;
|
|
91
91
|
toggle();
|
|
92
92
|
};
|
|
93
|
-
return (_jsx("section", { id: id, "aria-labelledby": headingId, "data-slot": "widget-card", "data-state": state, "data-collapsed": collapsed || undefined, className: cn('min-w-0 self-start', className), children: _jsxs(Card, { className:
|
|
93
|
+
return (_jsx("section", { id: id, "aria-labelledby": headingId, "data-slot": "widget-card", "data-state": state, "data-collapsed": collapsed || undefined, className: cn('min-w-0 self-start', className), children: _jsxs(Card, { className: cn('glow-edge density-compact:[--card-spacing:--spacing(3)]', collapsed && 'pb-0'), children: [_jsxs(CardHeader, { onClick: onHeaderClick, className: cn('flex items-center justify-between gap-3', inAccordion && 'cursor-pointer select-none', collapsed && 'border-b-0'), children: [_jsxs(Heading, { id: headingId, className: "flex min-w-0 items-center gap-2 type-section-title text-foreground", children: [Icon && _jsx(Icon, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: title })] }), (link || foldable || summaryShown) && (_jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [summaryShown && (_jsx("span", { id: summaryId, "data-slot": "widget-card-summary", className: "mr-2 flex items-center", children: _jsx(WidgetCardSummary, { state: state, summary: summary, emptySummary: emptySummary }) })), link?.href !== undefined ? (_jsxs("a", { href: link.href, target: "_blank", rel: "noreferrer", "aria-describedby": headingId, className: cn(HEADER_LINK_CLASS, 'inline-flex items-center gap-1'), children: [link.label, _jsx(ExternalLinkIcon, { "aria-hidden": "true", className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "sr-only", children: " (opens in a new tab)" })] })) : (link && (_jsx(Link, { to: link.to, "aria-describedby": headingId, className: HEADER_LINK_CLASS, children: link.label }))), foldable && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-labelledby": headingId, "aria-describedby": summaryShown ? summaryId : undefined, "aria-expanded": !collapsed, "aria-controls": contentId, onClick: toggle,
|
|
94
94
|
// The button's own open look (a filled background) would
|
|
95
95
|
// read as pressed: the chevron says it.
|
|
96
96
|
className: "-my-1 -mr-1.5 aria-expanded:not-hover:bg-transparent", children: _jsx(ChevronDownIcon, { "aria-hidden": "true", className: cn('transition-transform', collapsed && '-rotate-90') }) }))] }))] }), _jsx(CardContent, { id: contentId, hidden: collapsed, "aria-busy": state === 'loading' || undefined, children: error ? (_jsx(ErrorState, { error: error, onRetry: onRetry, size: "compact" })) : state === 'loading' ? (_jsx(WidgetListSkeleton, { rows: skeletonRows })) : state === 'empty' ? (_jsx("p", { "data-state": "empty", className: "py-2 type-body text-muted-foreground", children: emptyMessage })) : (children) })] }) }));
|
package/dist/lib/image-file.d.ts
CHANGED
|
@@ -30,8 +30,11 @@ export declare function formatBytes(bytes: number): string;
|
|
|
30
30
|
export declare function acceptAttribute(accept?: readonly string[]): string;
|
|
31
31
|
/**
|
|
32
32
|
* Why a file can't be uploaded, as a sentence to show under the field, or
|
|
33
|
-
* null when it can: a type not accepted, or over the size limit.
|
|
33
|
+
* null when it can: a type not accepted, or over the size limit. A file
|
|
34
|
+
* with an empty type is judged by its name's extension (`.heic`, `.heif`).
|
|
34
35
|
*/
|
|
35
|
-
export declare function imageRefusal(file: Pick<File, 'type' | 'size'
|
|
36
|
+
export declare function imageRefusal(file: Pick<File, 'type' | 'size'> & {
|
|
37
|
+
name?: string;
|
|
38
|
+
}, rules?: ImageFileRules): string | null;
|
|
36
39
|
/** What a failed upload says: the rejection's own `message` (an `AppError`'s), else a generic sentence. */
|
|
37
40
|
export declare function uploadFailure(error: unknown, noun?: string): string;
|
package/dist/lib/image-file.js
CHANGED
|
@@ -42,22 +42,37 @@ export function formatBytes(bytes) {
|
|
|
42
42
|
function accepts(accept, type) {
|
|
43
43
|
return accept.some((allowed) => allowed === type || (allowed.endsWith('/*') && type.startsWith(allowed.slice(0, -1))));
|
|
44
44
|
}
|
|
45
|
+
// Some pickers (Windows, some Android) hand over a HEIC or HEIF photo with
|
|
46
|
+
// an empty `type`. Then the name's extension stands in for it, so the file
|
|
47
|
+
// is judged as if it had been typed.
|
|
48
|
+
const TYPES_BY_EXTENSION = {
|
|
49
|
+
heic: 'image/heic',
|
|
50
|
+
heif: 'image/heif',
|
|
51
|
+
};
|
|
52
|
+
function fileType(file) {
|
|
53
|
+
if (file.type || !file.name)
|
|
54
|
+
return file.type;
|
|
55
|
+
const extension = file.name.split('.').at(-1)?.toLowerCase();
|
|
56
|
+
return (file.name.includes('.') && extension && TYPES_BY_EXTENSION[extension]) || '';
|
|
57
|
+
}
|
|
45
58
|
/** The `accept` attribute for a file input: `image/jpeg,image/png,image/webp`. */
|
|
46
59
|
export function acceptAttribute(accept = DEFAULT_IMAGE_TYPES) {
|
|
47
60
|
return accept.join(',');
|
|
48
61
|
}
|
|
49
62
|
/**
|
|
50
63
|
* Why a file can't be uploaded, as a sentence to show under the field, or
|
|
51
|
-
* null when it can: a type not accepted, or over the size limit.
|
|
64
|
+
* null when it can: a type not accepted, or over the size limit. A file
|
|
65
|
+
* with an empty type is judged by its name's extension (`.heic`, `.heif`).
|
|
52
66
|
*/
|
|
53
67
|
export function imageRefusal(file, rules = {}) {
|
|
54
68
|
const accept = rules.accept ?? DEFAULT_IMAGE_TYPES;
|
|
55
|
-
|
|
69
|
+
const type = fileType(file);
|
|
70
|
+
if (!accepts(accept, type)) {
|
|
56
71
|
const wanted = listOr(accept.map(typeName));
|
|
57
72
|
const choose = accept.length === 1 && accept[0] === 'image/*' ? 'Choose an image.' : `Choose a ${wanted} image.`;
|
|
58
73
|
// A type with a name people know is named; any other file that isn't
|
|
59
74
|
// an image is just that.
|
|
60
|
-
const named = TYPE_NAMES[
|
|
75
|
+
const named = TYPE_NAMES[type] ?? (type.startsWith('image/') ? typeName(type) : null);
|
|
61
76
|
return named ? `That file is ${article(named)} ${named}. ${choose}` : `That file isn't an image. ${choose}`;
|
|
62
77
|
}
|
|
63
78
|
if (rules.maxSize !== undefined && file.size > rules.maxSize) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "4.63.
|
|
3
|
+
"version": "4.63.3",
|
|
4
4
|
"description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|