@mohasinac/appkit 4.15.0 → 4.16.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/_internal/shared/actions/action-registry.d.ts +9 -2
- package/dist/_internal/shared/actions/action-registry.js +455 -6
- package/dist/_internal/shared/features/blog/schema.d.ts +4 -4
- package/dist/_internal/shared/features/cart/schema.d.ts +11 -11
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +22 -22
- package/dist/_internal/shared/features/orders/schema.d.ts +4 -4
- package/dist/_internal/shared/features/products/schema.d.ts +25 -25
- package/dist/_internal/shared/features/promotions/schema.d.ts +4 -4
- package/dist/_internal/shared/features/reviews/schema.d.ts +2 -2
- package/dist/contracts/extend.d.ts +29 -0
- package/dist/contracts/extend.js +20 -0
- package/dist/contracts/index.d.ts +2 -1
- package/dist/contracts/index.js +2 -0
- package/dist/features/admin/components/AdminViewCards.js +24 -17
- package/dist/features/admin/components/DataListingView.js +15 -1
- package/dist/features/admin/components/DataTable.js +7 -17
- package/dist/features/auctions/schemas/index.d.ts +14 -14
- package/dist/features/auth/schemas/index.d.ts +2 -2
- package/dist/features/blog/actions/blog-actions.d.ts +7 -7
- package/dist/features/blog/schemas/blog-form.d.ts +6 -6
- package/dist/features/blog/schemas/index.d.ts +10 -10
- package/dist/features/cart/schemas/index.d.ts +3 -3
- package/dist/features/catalogue/schemas/validation.d.ts +12 -12
- package/dist/features/categories/schemas/index.d.ts +16 -16
- package/dist/features/collections/schemas/index.d.ts +8 -8
- package/dist/features/events/schemas/index.d.ts +2 -2
- package/dist/features/faq/actions/faq-actions.d.ts +6 -6
- package/dist/features/faq/schemas/index.d.ts +6 -6
- package/dist/features/grouped/schemas/grouped-listing-form.d.ts +2 -2
- package/dist/features/grouped/schemas/index.d.ts +4 -4
- package/dist/features/homepage/schemas/index.d.ts +40 -40
- package/dist/features/messages/schemas/index.d.ts +2 -2
- package/dist/features/orders/schemas/index.d.ts +24 -24
- package/dist/features/pre-orders/schemas/index.d.ts +4 -4
- package/dist/features/products/schemas/index.d.ts +36 -36
- package/dist/features/products/schemas/product-features.validators.d.ts +2 -2
- package/dist/features/promotions/schemas/index.d.ts +14 -14
- package/dist/features/reviews/schemas/index.d.ts +14 -14
- package/dist/features/scams/schemas/index.d.ts +2 -2
- package/dist/features/search/schemas/index.d.ts +2 -2
- package/dist/features/seller/schemas/index.d.ts +2 -2
- package/dist/features/shipments/schemas/validation.d.ts +22 -22
- package/dist/features/store-extensions/schemas/listing-template-form.d.ts +3 -3
- package/dist/features/store-extensions/schemas/store-category-form.d.ts +2 -2
- package/dist/features/stores/schemas/index.d.ts +4 -4
- package/dist/features/support/schemas/index.d.ts +4 -4
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +24 -0
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/schemas/registry.d.ts +108 -108
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/DataTable.js +4 -2
- package/dist/ui/columns/build-columns.d.ts +6 -1
- package/dist/ui/columns/build-columns.js +53 -0
- package/dist/ui/columns/cell-renderers.d.ts +68 -0
- package/dist/ui/columns/cell-renderers.js +111 -0
- package/dist/ui/columns/column-renderers.d.ts +6 -1
- package/dist/ui/columns/column-renderers.js +6 -1
- package/dist/ui/columns/index.d.ts +2 -1
- package/dist/ui/columns/index.js +5 -1
- package/dist/ui/components/Button.js +27 -2
- package/dist/ui/icons/icon-registry.d.ts +65 -0
- package/dist/ui/icons/icon-registry.js +115 -0
- package/package.json +1 -1
package/dist/ui/DataTable.js
CHANGED
|
@@ -12,6 +12,8 @@ import { GRID_MAP } from "./components/Layout";
|
|
|
12
12
|
import { Row } from "./components/Layout";
|
|
13
13
|
import { Div } from "./components/Div";
|
|
14
14
|
import { mergeTableConfig, DEFAULT_PAGINATION_CONFIG } from "../contracts";
|
|
15
|
+
import { resolveColumnPriority } from "./columns/build-columns";
|
|
16
|
+
import { COLUMN_PRIORITY_CLASS } from "../contracts/extend";
|
|
15
17
|
export function DataTable({ data, columns, keyExtractor: keyExtractorProp, onRowClick, loading = false, emptyMessage, actions, mobileCardRender, emptyState, emptyIcon, emptyTitle, tableConfig, paginationConfig, externalPagination = false,
|
|
16
18
|
// Explicit flat props — override tableConfig when provided
|
|
17
19
|
pageSize: pageSizeProp, stickyHeader: stickyHeaderProp, striped: stripedProp, showViewToggle: showViewToggleProp, showTableView = true, viewMode: controlledViewMode, defaultViewMode: defaultViewModeProp, onViewModeChange, selectable: selectableProp, selectedIds = [], onSelectionChange, gridCols = "cards", labels = {}, }) {
|
|
@@ -135,13 +137,13 @@ pageSize: pageSizeProp, stickyHeader: stickyHeaderProp, striped: stripedProp, sh
|
|
|
135
137
|
? "ascending"
|
|
136
138
|
: "descending"
|
|
137
139
|
: "none"
|
|
138
|
-
: undefined, className: `appkit-data-table__th ${col.sortable ? "appkit-data-table__th--sortable" : ""}`, style: { width: col.width }, onClick: () => col.sortable && handleSort(col.key), children: _jsxs(Row, { gap: "sm", children: [col.header, col.sortable && (_jsx(Span, { "aria-hidden": "true", color: "faint", children: sortKey === col.key ? (sortDirection === "asc" ? ("↑") : ("↓")) : (_jsx(Span, { className: "opacity-30", children: "\u2195" })) }))] }) }, col.key))), actions && (_jsx("th", { scope: "col", className: "appkit-data-table__th appkit-data-table__td--actions", children: labelActions }))] }) }), _jsx("tbody", { className: "appkit-data-table__tbody divide-y divide-zinc-200 divide-[var(--appkit-color-border)]", children: paginatedData.map((item, index) => (_jsxs("tr", { className: [
|
|
140
|
+
: undefined, className: `appkit-data-table__th ${col.sortable ? "appkit-data-table__th--sortable" : ""} ${COLUMN_PRIORITY_CLASS[resolveColumnPriority(col)]}`, style: { width: col.width }, onClick: () => col.sortable && handleSort(col.key), children: _jsxs(Row, { gap: "sm", children: [col.header, col.sortable && (_jsx(Span, { "aria-hidden": "true", color: "faint", children: sortKey === col.key ? (sortDirection === "asc" ? ("↑") : ("↓")) : (_jsx(Span, { className: "opacity-30", children: "\u2195" })) }))] }) }, col.key))), actions && (_jsx("th", { scope: "col", className: "appkit-data-table__th appkit-data-table__td--actions", children: labelActions }))] }) }), _jsx("tbody", { className: "appkit-data-table__tbody divide-y divide-zinc-200 divide-[var(--appkit-color-border)]", children: paginatedData.map((item, index) => (_jsxs("tr", { className: [
|
|
139
141
|
"appkit-data-table__row",
|
|
140
142
|
striped && index % 2 === 1
|
|
141
143
|
? "appkit-data-table__row--striped"
|
|
142
144
|
: "",
|
|
143
145
|
onRowClick ? "appkit-data-table__row--clickable" : "",
|
|
144
|
-
].join(" "), onClick: () => onRowClick?.(item), children: [selectable && (_jsx("td", { className: "px-4 py-4 w-8", onClick: (e) => e.stopPropagation(), children: _jsx(Checkbox, { bare: true, className: CLS_CHECKBOX, "aria-label": "Select row", checked: selectedIds.includes(keyExtractor(item)), onChange: (e) => handleRowSelectionChange(item, e.target.checked) }) })), columns.map((col) => (_jsx("td", { className:
|
|
146
|
+
].join(" "), onClick: () => onRowClick?.(item), children: [selectable && (_jsx("td", { className: "px-4 py-4 w-8", onClick: (e) => e.stopPropagation(), children: _jsx(Checkbox, { bare: true, className: CLS_CHECKBOX, "aria-label": "Select row", checked: selectedIds.includes(keyExtractor(item)), onChange: (e) => handleRowSelectionChange(item, e.target.checked) }) })), columns.map((col) => (_jsx("td", { className: `appkit-data-table__td ${COLUMN_PRIORITY_CLASS[resolveColumnPriority(col)]}`, children: col.render
|
|
145
147
|
? col.render(item)
|
|
146
148
|
: (item[col.key] ?? "-") }, col.key))), actions && (_jsx("td", { className: "appkit-data-table__td appkit-data-table__td--actions", onClick: (e) => e.stopPropagation(), children: actions(item) }))] }, keyExtractor(item)))) })] }) }) })), !externalPagination && totalPages > 1 && (_jsx(Div, { className: "appkit-data-table__pagination", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: setCurrentPage, maxVisible: resolvedPag.maxVisible, showFirstLast: resolvedPag.showFirstLast, showPrevNext: resolvedPag.showPrevNext, size: resolvedPag.size }) }))] }));
|
|
147
149
|
}
|
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
* @example
|
|
6
6
|
* export const buildOrderColumns = createColumnBuilder(orderAdminColumns);
|
|
7
7
|
*/
|
|
8
|
-
import type { TableColumn, ColumnExtensionOpts } from "../../contracts";
|
|
8
|
+
import type { TableColumn, ColumnExtensionOpts, ColumnPriority } from "../../contracts";
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a column's breakpoint. An explicit `priority` always wins; otherwise
|
|
11
|
+
* the field dictionary decides; otherwise the column shows at every width.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveColumnPriority<T>(col: TableColumn<T>): ColumnPriority;
|
|
9
14
|
/**
|
|
10
15
|
* Build a merged column list from base columns + extension opts.
|
|
11
16
|
* Handles omit, overrides, and extras in one pass.
|
|
@@ -5,6 +5,59 @@
|
|
|
5
5
|
* @example
|
|
6
6
|
* export const buildOrderColumns = createColumnBuilder(orderAdminColumns);
|
|
7
7
|
*/
|
|
8
|
+
/*
|
|
9
|
+
* Field-name → breakpoint, from the same vocabulary the form field-dictionary
|
|
10
|
+
* uses. Applied ONLY when a column does not set `priority` itself.
|
|
11
|
+
*
|
|
12
|
+
* 🛑 The default for an unrecognised key is `"always"`, not `"lg"`.
|
|
13
|
+
*
|
|
14
|
+
* A `"lg"` default would have been tidier and would have silently hidden a
|
|
15
|
+
* column on every one of the ~25 existing column sets the moment this shipped
|
|
16
|
+
* — including, on some tables, the only identifying label. Safe-by-default
|
|
17
|
+
* plus an audit that nudges wide tables to declare priorities gets the same
|
|
18
|
+
* end state without a silent regression in between.
|
|
19
|
+
*/
|
|
20
|
+
const PRIORITY_BY_FIELD = {
|
|
21
|
+
// md — one state and one number is enough to triage a row
|
|
22
|
+
status: "md",
|
|
23
|
+
price: "md",
|
|
24
|
+
total: "md",
|
|
25
|
+
totalAmount: "md",
|
|
26
|
+
amount: "md",
|
|
27
|
+
listingType: "md",
|
|
28
|
+
type: "md",
|
|
29
|
+
// lg — relational context
|
|
30
|
+
category: "lg",
|
|
31
|
+
categorySlug: "lg",
|
|
32
|
+
brand: "lg",
|
|
33
|
+
brandSlug: "lg",
|
|
34
|
+
store: "lg",
|
|
35
|
+
storeName: "lg",
|
|
36
|
+
storeId: "lg",
|
|
37
|
+
quantity: "lg",
|
|
38
|
+
stockQuantity: "lg",
|
|
39
|
+
rating: "lg",
|
|
40
|
+
scope: "lg",
|
|
41
|
+
role: "lg",
|
|
42
|
+
// xl — audit metadata: useful, never urgent
|
|
43
|
+
createdAt: "xl",
|
|
44
|
+
updatedAt: "xl",
|
|
45
|
+
publishedAt: "xl",
|
|
46
|
+
id: "xl",
|
|
47
|
+
slug: "xl",
|
|
48
|
+
views: "xl",
|
|
49
|
+
viewCount: "xl",
|
|
50
|
+
productCount: "xl",
|
|
51
|
+
createdBy: "xl",
|
|
52
|
+
updatedBy: "xl",
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a column's breakpoint. An explicit `priority` always wins; otherwise
|
|
56
|
+
* the field dictionary decides; otherwise the column shows at every width.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveColumnPriority(col) {
|
|
59
|
+
return col.priority ?? PRIORITY_BY_FIELD[col.key] ?? "always";
|
|
60
|
+
}
|
|
8
61
|
/**
|
|
9
62
|
* Build a merged column list from base columns + extension opts.
|
|
10
63
|
* Handles omit, overrides, and extras in one pass.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type StatusTone } from "./column-renderers";
|
|
3
|
+
/**
|
|
4
|
+
* The single tone→class map.
|
|
5
|
+
*
|
|
6
|
+
* Previously duplicated verbatim in `features/admin/components/DataTable.tsx`
|
|
7
|
+
* and `AdminViewCards.tsx`. Both are now consumers of this one.
|
|
8
|
+
*/
|
|
9
|
+
export declare const STATUS_TONE_CLASSES: Record<StatusTone, string>;
|
|
10
|
+
/** A status string as a themed chip. Unknown statuses render neutral, not invisible. */
|
|
11
|
+
export declare function renderStatusBadge(status: string | null | undefined, opts?: {
|
|
12
|
+
emptyLabel?: string;
|
|
13
|
+
}): React.ReactNode;
|
|
14
|
+
declare const THUMB_SIZE: {
|
|
15
|
+
readonly sm: "h-8 w-8";
|
|
16
|
+
readonly md: "h-10 w-10";
|
|
17
|
+
readonly lg: "h-14 w-14";
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A row thumbnail in an explicitly-sized box.
|
|
21
|
+
*
|
|
22
|
+
* `fallback` takes the resource emoji from `getResourceIcon`, so a row with no
|
|
23
|
+
* image still has a visual anchor rather than an empty cell.
|
|
24
|
+
*/
|
|
25
|
+
export declare function renderThumbnail(src: string | null | undefined, alt: string, opts?: {
|
|
26
|
+
size?: keyof typeof THUMB_SIZE;
|
|
27
|
+
fallback?: string;
|
|
28
|
+
rounded?: "full" | "md";
|
|
29
|
+
}): React.ReactNode;
|
|
30
|
+
/** A person: round thumbnail + name, with an optional secondary line. */
|
|
31
|
+
export declare function renderAvatar(name: string, opts?: {
|
|
32
|
+
photoURL?: string | null;
|
|
33
|
+
secondary?: string | null;
|
|
34
|
+
fallback?: string;
|
|
35
|
+
}): React.ReactNode;
|
|
36
|
+
/** Money, right-aligned and tabular so columns of figures line up. */
|
|
37
|
+
export declare function renderMoney(amount: number | null | undefined, opts?: {
|
|
38
|
+
currency?: string;
|
|
39
|
+
locale?: string;
|
|
40
|
+
}): React.ReactNode;
|
|
41
|
+
/**
|
|
42
|
+
* "2 days ago", with the full timestamp on hover.
|
|
43
|
+
*
|
|
44
|
+
* Takes `now` so it is testable and so a table full of dates shares one clock
|
|
45
|
+
* rather than drifting cell to cell.
|
|
46
|
+
*/
|
|
47
|
+
export declare function renderRelativeDate(value: Date | string | number | null | undefined, opts?: {
|
|
48
|
+
now?: Date;
|
|
49
|
+
emptyLabel?: string;
|
|
50
|
+
}): React.ReactNode;
|
|
51
|
+
/** ✓ / ✗ instead of the words "true"/"false". */
|
|
52
|
+
export declare function renderBooleanIcon(value: boolean | null | undefined): React.ReactNode;
|
|
53
|
+
/** A count as a pill. Zero renders muted rather than shouting. */
|
|
54
|
+
export declare function renderCountPill(count: number | null | undefined, opts?: {
|
|
55
|
+
label?: string;
|
|
56
|
+
}): React.ReactNode;
|
|
57
|
+
/**
|
|
58
|
+
* A listing type as its registry badge.
|
|
59
|
+
*
|
|
60
|
+
* Goes through `listingBadgeVariant()`, not `LISTING_BADGE_VARIANT[kind]` — the
|
|
61
|
+
* accessor tolerates a non-`ListingType` string (a raw API value, a UI
|
|
62
|
+
* sentinel like `"all"`) while the map itself stays exhaustively typed, so a
|
|
63
|
+
* new listing type is a compile error there rather than a silent fallthrough.
|
|
64
|
+
*/
|
|
65
|
+
export declare function renderTypeChip(listingType: string | null | undefined, opts?: {
|
|
66
|
+
label?: string;
|
|
67
|
+
}): React.ReactNode;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Badge } from "../components/Badge";
|
|
4
|
+
import { Row, Stack } from "../components/Layout";
|
|
5
|
+
import { Div } from "../components/Div";
|
|
6
|
+
import { Span, Text } from "../components/Typography";
|
|
7
|
+
import { MediaImage } from "../../features/media/MediaImage";
|
|
8
|
+
import { getStatusTone, renderCurrency } from "./column-renderers";
|
|
9
|
+
import { listingBadgeVariant } from "../../features/products/utils/listing-badge-variant";
|
|
10
|
+
/**
|
|
11
|
+
* The single tone→class map.
|
|
12
|
+
*
|
|
13
|
+
* Previously duplicated verbatim in `features/admin/components/DataTable.tsx`
|
|
14
|
+
* and `AdminViewCards.tsx`. Both are now consumers of this one.
|
|
15
|
+
*/
|
|
16
|
+
export const STATUS_TONE_CLASSES = {
|
|
17
|
+
success: "bg-success-surface text-success",
|
|
18
|
+
warning: "bg-warning-surface text-warning",
|
|
19
|
+
error: "bg-error-surface text-error",
|
|
20
|
+
info: "bg-info-surface text-info",
|
|
21
|
+
// Not a status colour — see the header.
|
|
22
|
+
neutral: "bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]",
|
|
23
|
+
};
|
|
24
|
+
/** A status string as a themed chip. Unknown statuses render neutral, not invisible. */
|
|
25
|
+
export function renderStatusBadge(status, opts) {
|
|
26
|
+
const label = status ?? opts?.emptyLabel ?? "—";
|
|
27
|
+
const tone = getStatusTone(status);
|
|
28
|
+
return (_jsx(Span, { size: "xs", weight: "medium", rounded: "full", padding: "pill-sm-tall", className: `inline-flex whitespace-nowrap ${STATUS_TONE_CLASSES[tone]}`, children: label }));
|
|
29
|
+
}
|
|
30
|
+
const THUMB_SIZE = {
|
|
31
|
+
sm: "h-8 w-8",
|
|
32
|
+
md: "h-10 w-10",
|
|
33
|
+
lg: "h-14 w-14",
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A row thumbnail in an explicitly-sized box.
|
|
37
|
+
*
|
|
38
|
+
* `fallback` takes the resource emoji from `getResourceIcon`, so a row with no
|
|
39
|
+
* image still has a visual anchor rather than an empty cell.
|
|
40
|
+
*/
|
|
41
|
+
export function renderThumbnail(src, alt, opts) {
|
|
42
|
+
const size = opts?.size ?? "sm";
|
|
43
|
+
return (_jsx(Div, { className: `relative shrink-0 ${THUMB_SIZE[size]}`, overflow: "hidden", rounded: opts?.rounded ?? "md", children: _jsx(MediaImage, { src: src ?? undefined, alt: alt, size: "avatar", fallback: opts?.fallback }) }));
|
|
44
|
+
}
|
|
45
|
+
/** A person: round thumbnail + name, with an optional secondary line. */
|
|
46
|
+
export function renderAvatar(name, opts) {
|
|
47
|
+
return (_jsxs(Row, { gap: "sm", align: "center", children: [renderThumbnail(opts?.photoURL, name, { rounded: "full", fallback: opts?.fallback }), _jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { weight: "medium", color: "primary", className: "truncate", children: name }), opts?.secondary ? (_jsx(Text, { size: "xs", color: "muted", className: "truncate", children: opts.secondary })) : null] })] }));
|
|
48
|
+
}
|
|
49
|
+
/** Money, right-aligned and tabular so columns of figures line up. */
|
|
50
|
+
export function renderMoney(amount, opts) {
|
|
51
|
+
return (_jsx(Span, { size: "sm", numeric: true, className: "whitespace-nowrap", children: renderCurrency(amount, opts?.currency, opts?.locale) }));
|
|
52
|
+
}
|
|
53
|
+
const MINUTE = 60000;
|
|
54
|
+
const HOUR = 60 * MINUTE;
|
|
55
|
+
const DAY = 24 * HOUR;
|
|
56
|
+
/**
|
|
57
|
+
* Thresholds, ordered ascending — a table rather than an if/else chain so a new
|
|
58
|
+
* granularity is one row, and so the ordering is visible at a glance.
|
|
59
|
+
*/
|
|
60
|
+
const RELATIVE_STEPS = [
|
|
61
|
+
{ under: MINUTE, format: () => "just now" },
|
|
62
|
+
{ under: HOUR, format: (abs, suffix) => `${Math.round(abs / MINUTE)}m ${suffix}` },
|
|
63
|
+
{ under: DAY, format: (abs, suffix) => `${Math.round(abs / HOUR)}h ${suffix}` },
|
|
64
|
+
{ under: 30 * DAY, format: (abs, suffix) => `${Math.round(abs / DAY)}d ${suffix}` },
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* "2 days ago", with the full timestamp on hover.
|
|
68
|
+
*
|
|
69
|
+
* Takes `now` so it is testable and so a table full of dates shares one clock
|
|
70
|
+
* rather than drifting cell to cell.
|
|
71
|
+
*/
|
|
72
|
+
export function renderRelativeDate(value, opts) {
|
|
73
|
+
if (value == null || value === "") {
|
|
74
|
+
return (_jsx(Span, { size: "sm", color: "muted", children: opts?.emptyLabel ?? "—" }));
|
|
75
|
+
}
|
|
76
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
77
|
+
if (Number.isNaN(date.getTime())) {
|
|
78
|
+
// A malformed date renders as an em-dash rather than "Invalid Date".
|
|
79
|
+
return (_jsx(Span, { size: "sm", color: "muted", children: opts?.emptyLabel ?? "—" }));
|
|
80
|
+
}
|
|
81
|
+
const diff = (opts?.now ?? new Date()).getTime() - date.getTime();
|
|
82
|
+
const abs = Math.abs(diff);
|
|
83
|
+
const suffix = diff >= 0 ? "ago" : "from now";
|
|
84
|
+
const step = RELATIVE_STEPS.find((s) => abs < s.under);
|
|
85
|
+
// Past ~a month, a relative label stops being useful — show the real date.
|
|
86
|
+
const label = step ? step.format(abs, suffix) : date.toLocaleDateString();
|
|
87
|
+
return (_jsx(Span, { size: "sm", color: "muted", className: "whitespace-nowrap", title: date.toLocaleString(), children: label }));
|
|
88
|
+
}
|
|
89
|
+
/** ✓ / ✗ instead of the words "true"/"false". */
|
|
90
|
+
export function renderBooleanIcon(value) {
|
|
91
|
+
return value ? (_jsx(Span, { size: "sm", className: "text-success", "aria-label": "Yes", children: "\u2713" })) : (_jsx(Span, { size: "sm", color: "muted", "aria-label": "No", children: "\u2717" }));
|
|
92
|
+
}
|
|
93
|
+
/** A count as a pill. Zero renders muted rather than shouting. */
|
|
94
|
+
export function renderCountPill(count, opts) {
|
|
95
|
+
const n = count ?? 0;
|
|
96
|
+
const suffix = opts?.label ? ` ${opts.label}` : "";
|
|
97
|
+
return (_jsxs(Span, { size: "xs", weight: "medium", rounded: "full", padding: "pill-sm-tall", numeric: true, className: `inline-flex whitespace-nowrap ${n > 0 ? STATUS_TONE_CLASSES.info : STATUS_TONE_CLASSES.neutral}`, children: [n.toLocaleString(), suffix] }));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A listing type as its registry badge.
|
|
101
|
+
*
|
|
102
|
+
* Goes through `listingBadgeVariant()`, not `LISTING_BADGE_VARIANT[kind]` — the
|
|
103
|
+
* accessor tolerates a non-`ListingType` string (a raw API value, a UI
|
|
104
|
+
* sentinel like `"all"`) while the map itself stays exhaustively typed, so a
|
|
105
|
+
* new listing type is a compile error there rather than a silent fallthrough.
|
|
106
|
+
*/
|
|
107
|
+
export function renderTypeChip(listingType, opts) {
|
|
108
|
+
if (!listingType)
|
|
109
|
+
return null;
|
|
110
|
+
return (_jsx(Badge, { variant: listingBadgeVariant(listingType), children: opts?.label ?? listingType }));
|
|
111
|
+
}
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* Every renderer returns a `string` (not ReactNode) so it works with
|
|
5
5
|
* `TableColumn<T>.render` in both server and client contexts.
|
|
6
6
|
*
|
|
7
|
-
* For JSX renderers (badge, thumbnail,
|
|
7
|
+
* For JSX renderers (status badge, thumbnail, avatar, money, relative date,
|
|
8
|
+
* boolean icon, count pill, type chip) see `cell-renderers.tsx`.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: that sibling was called `column-renderers.tsx` here for months and was
|
|
11
|
+
* never written, because it cannot be: `.ts` and `.tsx` of the same basename
|
|
12
|
+
* both emit `column-renderers.js` (TS5056). Hence the distinct name.
|
|
8
13
|
*/
|
|
9
14
|
export interface BooleanRenderOpts {
|
|
10
15
|
trueLabel?: string;
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* Every renderer returns a `string` (not ReactNode) so it works with
|
|
5
5
|
* `TableColumn<T>.render` in both server and client contexts.
|
|
6
6
|
*
|
|
7
|
-
* For JSX renderers (badge, thumbnail,
|
|
7
|
+
* For JSX renderers (status badge, thumbnail, avatar, money, relative date,
|
|
8
|
+
* boolean icon, count pill, type chip) see `cell-renderers.tsx`.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: that sibling was called `column-renderers.tsx` here for months and was
|
|
11
|
+
* never written, because it cannot be: `.ts` and `.tsx` of the same basename
|
|
12
|
+
* both emit `column-renderers.js` (TS5056). Hence the distinct name.
|
|
8
13
|
*/
|
|
9
14
|
import { formatCurrency } from "../../utils/number.formatter";
|
|
10
15
|
import { getDefaultCurrency } from "../../core/baseline-resolver";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Barrel for shared column utilities.
|
|
3
3
|
*/
|
|
4
|
-
export { buildColumns, createColumnBuilder } from "./build-columns";
|
|
4
|
+
export { buildColumns, createColumnBuilder, resolveColumnPriority } from "./build-columns";
|
|
5
5
|
export { renderBoolean, renderCurrency, renderCurrencyCompact, renderCount, renderNullable, renderRating, getStatusTone, getResourceIcon, } from "./column-renderers";
|
|
6
6
|
export type { BooleanRenderOpts, RatingMode, RatingRenderOpts, StatusTone, } from "./column-renderers";
|
|
7
|
+
export { STATUS_TONE_CLASSES, renderStatusBadge, renderThumbnail, renderAvatar, renderMoney, renderRelativeDate, renderBooleanIcon, renderCountPill, renderTypeChip, } from "./cell-renderers";
|
package/dist/ui/columns/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Barrel for shared column utilities.
|
|
3
3
|
*/
|
|
4
|
-
export { buildColumns, createColumnBuilder } from "./build-columns";
|
|
4
|
+
export { buildColumns, createColumnBuilder, resolveColumnPriority } from "./build-columns";
|
|
5
5
|
export { renderBoolean, renderCurrency, renderCurrencyCompact, renderCount, renderNullable, renderRating, getStatusTone, getResourceIcon, } from "./column-renderers";
|
|
6
|
+
// JSX renderers. Deliberately a separate module from `column-renderers.ts`:
|
|
7
|
+
// that one is React-free so it can run in a server context, and mixing the two
|
|
8
|
+
// would drag React into every consumer of a plain string formatter.
|
|
9
|
+
export { STATUS_TONE_CLASSES, renderStatusBadge, renderThumbnail, renderAvatar, renderMoney, renderRelativeDate, renderBooleanIcon, renderCountPill, renderTypeChip, } from "./cell-renderers";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React, { useCallback, useState } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { twMerge } from "tailwind-merge";
|
|
@@ -7,6 +7,7 @@ import { Loader2 } from "lucide-react";
|
|
|
7
7
|
import { surfaceError } from "../../client/api/surface-error";
|
|
8
8
|
import { useToastSafe } from "./Toast";
|
|
9
9
|
import { SHADOW_MAP } from "./surface-tokens";
|
|
10
|
+
import { resolveIcon } from "../icons/icon-registry";
|
|
10
11
|
function spawnRipple(host, clientX, clientY) {
|
|
11
12
|
const rect = host.getBoundingClientRect();
|
|
12
13
|
const size = Math.max(rect.width, rect.height);
|
|
@@ -105,6 +106,23 @@ const BUTTON_JUSTIFY_MAP = {
|
|
|
105
106
|
end: "justify-end",
|
|
106
107
|
between: "justify-between",
|
|
107
108
|
};
|
|
109
|
+
/**
|
|
110
|
+
* The action's icon, rendered before its label.
|
|
111
|
+
*
|
|
112
|
+
* This is what makes `ActionDef.iconKey` real. It has existed on the type
|
|
113
|
+
* since the registry shipped, with a comment saying it is "resolved by the
|
|
114
|
+
* consumer's icon set" — there was no icon set, nothing resolved it, and every
|
|
115
|
+
* button in the app was text-only as a result.
|
|
116
|
+
*
|
|
117
|
+
* `aria-hidden` because the label (or `ariaLabel`) already names the action;
|
|
118
|
+
* announcing the glyph as well would read it twice.
|
|
119
|
+
*/
|
|
120
|
+
function ActionIcon({ action }) {
|
|
121
|
+
const Icon = resolveIcon(action?.iconKey);
|
|
122
|
+
if (!Icon)
|
|
123
|
+
return null;
|
|
124
|
+
return _jsx(Icon, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" });
|
|
125
|
+
}
|
|
108
126
|
// Map ActionKind → Button variant (caller can override via explicit variant prop)
|
|
109
127
|
const ACTION_KIND_VARIANT = {
|
|
110
128
|
primary: "primary",
|
|
@@ -128,7 +146,14 @@ export function Button({ variant, size = "md", className = "", isLoading = false
|
|
|
128
146
|
const pendingTargetRef = React.useRef(null);
|
|
129
147
|
// Resolve defaults from action registry
|
|
130
148
|
const resolvedVariant = variant ?? (action ? (ACTION_KIND_VARIANT[action.kind] ?? "primary") : "primary");
|
|
131
|
-
|
|
149
|
+
/*
|
|
150
|
+
* The icon is prepended only when the Button is rendering the action's OWN
|
|
151
|
+
* label. If the caller passed children they have composed their own content,
|
|
152
|
+
* and injecting a glyph into it would be a surprise.
|
|
153
|
+
*/
|
|
154
|
+
const actionIcon = children === undefined && action?.iconKey ? _jsx(ActionIcon, { action: action }) : null;
|
|
155
|
+
const resolvedChildren = children ??
|
|
156
|
+
(action ? (actionIcon ? (_jsxs(_Fragment, { children: [actionIcon, action.label] })) : (action.label)) : undefined);
|
|
132
157
|
const resolvedAriaLabel = props["aria-label"] ?? (action ? (action.ariaLabel ?? action.label) : undefined);
|
|
133
158
|
const classes = twMerge(UI_BUTTON.base, UI_BUTTON.variants[resolvedVariant], UI_BUTTON.sizes[size], gap ? BUTTON_GAP_MAP[gap] : "", textSize ? BUTTON_TEXT_SIZE_MAP[textSize] : "", paddingX ? BUTTON_PADDING_X_MAP[paddingX] : "", paddingY ? BUTTON_PADDING_Y_MAP[paddingY] : "", weight ? BUTTON_WEIGHT_MAP[weight] : "", textColor ? BUTTON_TEXT_COLOR_MAP[textColor] : "", border ? BUTTON_BORDER_MAP[border] : "", rounded ? BUTTON_ROUNDED_MAP[rounded] : "", shadow ? SHADOW_MAP[shadow] : "", justify ? BUTTON_JUSTIFY_MAP[justify] : "", className);
|
|
134
159
|
const userOnClick = props.onClick;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type LucideIcon } from "lucide-react";
|
|
2
|
+
/**
|
|
3
|
+
* Semantic name → component.
|
|
4
|
+
*
|
|
5
|
+
* Keys are INTENT ("approve", "reject"), not appearance ("check", "x"), so a
|
|
6
|
+
* later change of glyph is one edit here rather than a sweep of call sites.
|
|
7
|
+
* The few appearance-named keys exist because the action they serve genuinely
|
|
8
|
+
* has no better verb.
|
|
9
|
+
*/
|
|
10
|
+
export declare const ICONS: {
|
|
11
|
+
readonly view: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
12
|
+
readonly hide: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
13
|
+
readonly edit: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
14
|
+
readonly create: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
15
|
+
readonly save: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
16
|
+
readonly delete: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
17
|
+
readonly archive: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
18
|
+
readonly duplicate: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
19
|
+
readonly approve: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
20
|
+
readonly reject: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
21
|
+
readonly confirm: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
22
|
+
readonly cancel: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
23
|
+
readonly pause: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
24
|
+
readonly resume: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
25
|
+
readonly restore: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
26
|
+
readonly retry: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
27
|
+
readonly ban: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
28
|
+
readonly report: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
29
|
+
readonly verify: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
30
|
+
readonly lock: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
31
|
+
readonly cart: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
32
|
+
readonly order: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
33
|
+
readonly ship: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
34
|
+
readonly bid: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
35
|
+
readonly wishlist: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
36
|
+
readonly review: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
37
|
+
readonly analytics: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
38
|
+
readonly message: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
39
|
+
readonly reply: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
40
|
+
readonly email: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
41
|
+
readonly send: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
42
|
+
readonly share: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
43
|
+
readonly link: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
44
|
+
readonly download: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
45
|
+
readonly export: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
46
|
+
readonly upload: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
47
|
+
readonly back: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
48
|
+
readonly search: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
49
|
+
readonly filter: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
50
|
+
readonly settings: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
51
|
+
readonly expand: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
52
|
+
readonly user: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
53
|
+
};
|
|
54
|
+
/** Every valid `iconKey`. A typo is a compile error, not a silent blank. */
|
|
55
|
+
export type IconKey = keyof typeof ICONS;
|
|
56
|
+
/** Narrow an unknown string — for data that crosses a boundary the type cannot. */
|
|
57
|
+
export declare function isIconKey(value: unknown): value is IconKey;
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a key to its component, or `undefined`.
|
|
60
|
+
*
|
|
61
|
+
* Returns `undefined` rather than a placeholder glyph on purpose: a missing
|
|
62
|
+
* icon should render nothing and leave the label intact, not draw a
|
|
63
|
+
* question-mark box that reads as a broken image.
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveIcon(key: string | null | undefined): LucideIcon | undefined;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: `ActionDef.iconKey` has existed as `iconKey?: string` with the comment
|
|
3
|
+
* *"resolved by the consumer's icon set"* — and there is no consumer icon
|
|
4
|
+
* set. Nothing in either tree resolves it. It is set on **2 of 220**
|
|
5
|
+
* actions (0.9%) and **rendered by nothing**: `Button` is the only
|
|
6
|
+
* component that accepts an `ActionDef` and it reads `kind`, `label` and
|
|
7
|
+
* `ariaLabel` only.
|
|
8
|
+
*
|
|
9
|
+
* Meanwhile 105 files import `lucide-react` directly, so there is no
|
|
10
|
+
* indirection anywhere — an icon is whatever each file happened to pick.
|
|
11
|
+
*
|
|
12
|
+
* WHAT: The name→component registry `iconKey` was always supposed to resolve
|
|
13
|
+
* against, so the field can become real rather than inert.
|
|
14
|
+
*
|
|
15
|
+
* ## Typed, so a bad key cannot ship
|
|
16
|
+
*
|
|
17
|
+
* `ActionDef.iconKey` is retyped from `string` to `IconKey`. A typo was
|
|
18
|
+
* previously a silent no-op that rendered nothing; it is now a compile error.
|
|
19
|
+
* That is the same reasoning as every other registry in this codebase
|
|
20
|
+
* (`Record<Union, true>`), applied to icons.
|
|
21
|
+
*
|
|
22
|
+
* The one working name→icon pattern already in the repo is `TrustBadges.tsx`'s
|
|
23
|
+
* local `BADGE_ICONS` + `TrustBadgeIconKey` union. This generalises that shape
|
|
24
|
+
* rather than inventing a new one.
|
|
25
|
+
*
|
|
26
|
+
* ## Deliberately small
|
|
27
|
+
*
|
|
28
|
+
* Only the verbs the action registry actually uses, plus the handful of
|
|
29
|
+
* resource glyphs row menus need. This is not a re-export of lucide — a
|
|
30
|
+
* registry that mirrors the whole icon set is just an alias with extra steps,
|
|
31
|
+
* and it would pull every icon into the bundle.
|
|
32
|
+
*
|
|
33
|
+
* EXPORTS: ICONS, type IconKey, isIconKey, resolveIcon
|
|
34
|
+
*
|
|
35
|
+
* @tag domain:ui
|
|
36
|
+
* @tag layer:constants
|
|
37
|
+
* @tag pattern:none
|
|
38
|
+
* @tag access:client
|
|
39
|
+
* @tag consumers:Button,RowActionMenu,BulkActionBar,action-registry
|
|
40
|
+
* @tag sideEffects:none
|
|
41
|
+
*/
|
|
42
|
+
import { Archive, ArrowLeft, Ban, Check, CheckCircle, ChevronDown, Copy, Download, Edit, Eye, EyeOff, FileDown, Filter, Flag, Gavel, Heart, Link as LinkIcon, Lock, Mail, MessageSquare, Package, Pause, Play, Plus, RefreshCw, Reply, RotateCcw, Save, Search, Send, Settings, Share2, ShieldCheck, ShoppingCart, Star, Trash2, TrendingUp, Truck, Upload, User, X, XCircle, } from "lucide-react";
|
|
43
|
+
/**
|
|
44
|
+
* Semantic name → component.
|
|
45
|
+
*
|
|
46
|
+
* Keys are INTENT ("approve", "reject"), not appearance ("check", "x"), so a
|
|
47
|
+
* later change of glyph is one edit here rather than a sweep of call sites.
|
|
48
|
+
* The few appearance-named keys exist because the action they serve genuinely
|
|
49
|
+
* has no better verb.
|
|
50
|
+
*/
|
|
51
|
+
export const ICONS = {
|
|
52
|
+
// Core CRUD
|
|
53
|
+
view: Eye,
|
|
54
|
+
hide: EyeOff,
|
|
55
|
+
edit: Edit,
|
|
56
|
+
create: Plus,
|
|
57
|
+
save: Save,
|
|
58
|
+
delete: Trash2,
|
|
59
|
+
archive: Archive,
|
|
60
|
+
duplicate: Copy,
|
|
61
|
+
// Decisions
|
|
62
|
+
approve: CheckCircle,
|
|
63
|
+
reject: XCircle,
|
|
64
|
+
confirm: Check,
|
|
65
|
+
cancel: X,
|
|
66
|
+
pause: Pause,
|
|
67
|
+
resume: Play,
|
|
68
|
+
restore: RotateCcw,
|
|
69
|
+
retry: RefreshCw,
|
|
70
|
+
// Moderation / trust
|
|
71
|
+
ban: Ban,
|
|
72
|
+
report: Flag,
|
|
73
|
+
verify: ShieldCheck,
|
|
74
|
+
lock: Lock,
|
|
75
|
+
// Commerce
|
|
76
|
+
cart: ShoppingCart,
|
|
77
|
+
order: Package,
|
|
78
|
+
ship: Truck,
|
|
79
|
+
bid: Gavel,
|
|
80
|
+
wishlist: Heart,
|
|
81
|
+
review: Star,
|
|
82
|
+
analytics: TrendingUp,
|
|
83
|
+
// Communication
|
|
84
|
+
message: MessageSquare,
|
|
85
|
+
reply: Reply,
|
|
86
|
+
email: Mail,
|
|
87
|
+
send: Send,
|
|
88
|
+
share: Share2,
|
|
89
|
+
link: LinkIcon,
|
|
90
|
+
// Data movement
|
|
91
|
+
download: Download,
|
|
92
|
+
export: FileDown,
|
|
93
|
+
upload: Upload,
|
|
94
|
+
// Navigation / chrome
|
|
95
|
+
back: ArrowLeft,
|
|
96
|
+
search: Search,
|
|
97
|
+
filter: Filter,
|
|
98
|
+
settings: Settings,
|
|
99
|
+
expand: ChevronDown,
|
|
100
|
+
user: User,
|
|
101
|
+
};
|
|
102
|
+
/** Narrow an unknown string — for data that crosses a boundary the type cannot. */
|
|
103
|
+
export function isIconKey(value) {
|
|
104
|
+
return typeof value === "string" && value in ICONS;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Resolve a key to its component, or `undefined`.
|
|
108
|
+
*
|
|
109
|
+
* Returns `undefined` rather than a placeholder glyph on purpose: a missing
|
|
110
|
+
* icon should render nothing and leave the label intact, not draw a
|
|
111
|
+
* question-mark box that reads as a broken image.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveIcon(key) {
|
|
114
|
+
return isIconKey(key) ? ICONS[key] : undefined;
|
|
115
|
+
}
|