@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.
Files changed (64) hide show
  1. package/dist/_internal/shared/actions/action-registry.d.ts +9 -2
  2. package/dist/_internal/shared/actions/action-registry.js +455 -6
  3. package/dist/_internal/shared/features/blog/schema.d.ts +4 -4
  4. package/dist/_internal/shared/features/cart/schema.d.ts +11 -11
  5. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +22 -22
  6. package/dist/_internal/shared/features/orders/schema.d.ts +4 -4
  7. package/dist/_internal/shared/features/products/schema.d.ts +25 -25
  8. package/dist/_internal/shared/features/promotions/schema.d.ts +4 -4
  9. package/dist/_internal/shared/features/reviews/schema.d.ts +2 -2
  10. package/dist/contracts/extend.d.ts +29 -0
  11. package/dist/contracts/extend.js +20 -0
  12. package/dist/contracts/index.d.ts +2 -1
  13. package/dist/contracts/index.js +2 -0
  14. package/dist/features/admin/components/AdminViewCards.js +24 -17
  15. package/dist/features/admin/components/DataListingView.js +15 -1
  16. package/dist/features/admin/components/DataTable.js +7 -17
  17. package/dist/features/auctions/schemas/index.d.ts +14 -14
  18. package/dist/features/auth/schemas/index.d.ts +2 -2
  19. package/dist/features/blog/actions/blog-actions.d.ts +7 -7
  20. package/dist/features/blog/schemas/blog-form.d.ts +6 -6
  21. package/dist/features/blog/schemas/index.d.ts +10 -10
  22. package/dist/features/cart/schemas/index.d.ts +3 -3
  23. package/dist/features/catalogue/schemas/validation.d.ts +12 -12
  24. package/dist/features/categories/schemas/index.d.ts +16 -16
  25. package/dist/features/collections/schemas/index.d.ts +8 -8
  26. package/dist/features/events/schemas/index.d.ts +2 -2
  27. package/dist/features/faq/actions/faq-actions.d.ts +6 -6
  28. package/dist/features/faq/schemas/index.d.ts +6 -6
  29. package/dist/features/grouped/schemas/grouped-listing-form.d.ts +2 -2
  30. package/dist/features/grouped/schemas/index.d.ts +4 -4
  31. package/dist/features/homepage/schemas/index.d.ts +40 -40
  32. package/dist/features/messages/schemas/index.d.ts +2 -2
  33. package/dist/features/orders/schemas/index.d.ts +24 -24
  34. package/dist/features/pre-orders/schemas/index.d.ts +4 -4
  35. package/dist/features/products/schemas/index.d.ts +36 -36
  36. package/dist/features/products/schemas/product-features.validators.d.ts +2 -2
  37. package/dist/features/promotions/schemas/index.d.ts +14 -14
  38. package/dist/features/reviews/schemas/index.d.ts +14 -14
  39. package/dist/features/scams/schemas/index.d.ts +2 -2
  40. package/dist/features/search/schemas/index.d.ts +2 -2
  41. package/dist/features/seller/schemas/index.d.ts +2 -2
  42. package/dist/features/shipments/schemas/validation.d.ts +22 -22
  43. package/dist/features/store-extensions/schemas/listing-template-form.d.ts +3 -3
  44. package/dist/features/store-extensions/schemas/store-category-form.d.ts +2 -2
  45. package/dist/features/stores/schemas/index.d.ts +4 -4
  46. package/dist/features/support/schemas/index.d.ts +4 -4
  47. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +24 -0
  48. package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
  49. package/dist/schemas/registry.d.ts +108 -108
  50. package/dist/styles.css +1 -1
  51. package/dist/tailwind-utilities.css +1 -1
  52. package/dist/ui/DataTable.js +4 -2
  53. package/dist/ui/columns/build-columns.d.ts +6 -1
  54. package/dist/ui/columns/build-columns.js +53 -0
  55. package/dist/ui/columns/cell-renderers.d.ts +68 -0
  56. package/dist/ui/columns/cell-renderers.js +111 -0
  57. package/dist/ui/columns/column-renderers.d.ts +6 -1
  58. package/dist/ui/columns/column-renderers.js +6 -1
  59. package/dist/ui/columns/index.d.ts +2 -1
  60. package/dist/ui/columns/index.js +5 -1
  61. package/dist/ui/components/Button.js +27 -2
  62. package/dist/ui/icons/icon-registry.d.ts +65 -0
  63. package/dist/ui/icons/icon-registry.js +115 -0
  64. package/package.json +1 -1
@@ -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: "appkit-data-table__td", children: col.render
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, action buttons) see `column-renderers.tsx`.
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, action buttons) see `column-renderers.tsx`.
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";
@@ -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, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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
- const resolvedChildren = children ?? (action ? action.label : undefined);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.15.0",
3
+ "version": "4.16.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"