@open-mercato/ui 0.6.5-develop.4788.1.a3e2520ec0 → 0.6.5-develop.4790.1.bffb4fd44b

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.
@@ -3,16 +3,24 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { SearchX, Undo2 } from "lucide-react";
4
4
  import { Button } from "../../primitives/button.js";
5
5
  import { useT } from "@open-mercato/shared/lib/i18n/context";
6
- function FilteredEmptyResults({ entityNamePlural, canRemoveLast, onClearAll, onRemoveLast }) {
6
+ function FilteredEmptyResults({ entityNamePlural, canRemoveLast, onClearAll, onRemoveLast, onClearSearch }) {
7
7
  const t = useT();
8
+ const searchActive = typeof onClearSearch === "function";
9
+ const title = searchActive ? t("ui.advancedFilter.empty.noMatchesSearchFilters", "No {entity} match your search and filters", { entity: entityNamePlural }) : t("ui.advancedFilter.empty.noMatches", "No {entity} match these filters", { entity: entityNamePlural });
10
+ const description = searchActive ? t("ui.advancedFilter.empty.tryRemovingSearchFilters", "Try a different search, remove a filter, or clear everything to see everyone.") : t("ui.advancedFilter.empty.tryRemoving", "Try removing the most restrictive filter, or clear all filters to see everyone.");
11
+ const primaryLabel = searchActive ? t("ui.advancedFilter.empty.clearSearchAndFilters", "Clear search and filters") : t("ui.advancedFilter.empty.clearAll", "Clear all filters");
12
+ const handlePrimary = searchActive ? () => {
13
+ onClearSearch?.();
14
+ onClearAll();
15
+ } : onClearAll;
8
16
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center gap-4 py-12", "data-testid": "filtered-empty-results", children: [
9
17
  /* @__PURE__ */ jsx("div", { className: "flex size-14 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(SearchX, { className: "size-6 text-muted-foreground" }) }),
10
18
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1", children: [
11
- /* @__PURE__ */ jsx("div", { className: "text-base font-semibold", children: t("ui.advancedFilter.empty.noMatches", "No {entity} match these filters", { entity: entityNamePlural }) }),
12
- /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground max-w-md text-center", children: t("ui.advancedFilter.empty.tryRemoving", "Try removing the most restrictive filter, or clear all filters to see everyone.") })
19
+ /* @__PURE__ */ jsx("div", { className: "text-base font-semibold", children: title }),
20
+ /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground max-w-md text-center", children: description })
13
21
  ] }),
14
22
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
15
- /* @__PURE__ */ jsx(Button, { type: "button", onClick: onClearAll, children: t("ui.advancedFilter.empty.clearAll", "Clear all filters") }),
23
+ /* @__PURE__ */ jsx(Button, { type: "button", onClick: handlePrimary, children: primaryLabel }),
16
24
  /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", disabled: !canRemoveLast, onClick: onRemoveLast, children: [
17
25
  /* @__PURE__ */ jsx(Undo2, { className: "size-4" }),
18
26
  t("ui.advancedFilter.empty.removeLast", "Remove last filter")
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/filters/FilteredEmptyResults.tsx"],
4
- "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { SearchX, Undo2 } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type FilteredEmptyResultsProps = {\n entityNamePlural: string\n canRemoveLast: boolean\n onClearAll: () => void\n onRemoveLast: () => void\n}\n\nexport function FilteredEmptyResults({ entityNamePlural, canRemoveLast, onClearAll, onRemoveLast }: FilteredEmptyResultsProps) {\n const t = useT()\n return (\n <div className=\"flex flex-col items-center justify-center gap-4 py-12\" data-testid=\"filtered-empty-results\">\n <div className=\"flex size-14 items-center justify-center rounded-full bg-muted\">\n <SearchX className=\"size-6 text-muted-foreground\" />\n </div>\n <div className=\"flex flex-col items-center gap-1\">\n <div className=\"text-base font-semibold\">\n {t('ui.advancedFilter.empty.noMatches', 'No {entity} match these filters', { entity: entityNamePlural })}\n </div>\n <div className=\"text-sm text-muted-foreground max-w-md text-center\">\n {t('ui.advancedFilter.empty.tryRemoving', 'Try removing the most restrictive filter, or clear all filters to see everyone.')}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <Button type=\"button\" onClick={onClearAll}>\n {t('ui.advancedFilter.empty.clearAll', 'Clear all filters')}\n </Button>\n <Button type=\"button\" variant=\"outline\" disabled={!canRemoveLast} onClick={onRemoveLast}>\n <Undo2 className=\"size-4\" />\n {t('ui.advancedFilter.empty.removeLast', 'Remove last filter')}\n </Button>\n </div>\n </div>\n )\n}\n"],
5
- "mappings": ";AAkBQ,cAEF,YAFE;AAhBR,SAAS,SAAS,aAAa;AAC/B,SAAS,cAAc;AACvB,SAAS,YAAY;AASd,SAAS,qBAAqB,EAAE,kBAAkB,eAAe,YAAY,aAAa,GAA8B;AAC7H,QAAM,IAAI,KAAK;AACf,SACE,qBAAC,SAAI,WAAU,yDAAwD,eAAY,0BACjF;AAAA,wBAAC,SAAI,WAAU,kEACb,8BAAC,WAAQ,WAAU,gCAA+B,GACpD;AAAA,IACA,qBAAC,SAAI,WAAU,oCACb;AAAA,0BAAC,SAAI,WAAU,2BACZ,YAAE,qCAAqC,mCAAmC,EAAE,QAAQ,iBAAiB,CAAC,GACzG;AAAA,MACA,oBAAC,SAAI,WAAU,sDACZ,YAAE,uCAAuC,iFAAiF,GAC7H;AAAA,OACF;AAAA,IACA,qBAAC,SAAI,WAAU,2BACb;AAAA,0BAAC,UAAO,MAAK,UAAS,SAAS,YAC5B,YAAE,oCAAoC,mBAAmB,GAC5D;AAAA,MACA,qBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,UAAU,CAAC,eAAe,SAAS,cACzE;AAAA,4BAAC,SAAM,WAAU,UAAS;AAAA,QACzB,EAAE,sCAAsC,oBAAoB;AAAA,SAC/D;AAAA,OACF;AAAA,KACF;AAEJ;",
4
+ "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { SearchX, Undo2 } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type FilteredEmptyResultsProps = {\n entityNamePlural: string\n canRemoveLast: boolean\n onClearAll: () => void\n onRemoveLast: () => void\n /**\n * When provided, a search is ALSO active alongside the filters. Switches the\n * copy to a combined search+filter message and makes the primary action clear\n * both the search and the filters (so a single click shows everyone).\n */\n onClearSearch?: () => void\n}\n\nexport function FilteredEmptyResults({ entityNamePlural, canRemoveLast, onClearAll, onRemoveLast, onClearSearch }: FilteredEmptyResultsProps) {\n const t = useT()\n const searchActive = typeof onClearSearch === 'function'\n const title = searchActive\n ? t('ui.advancedFilter.empty.noMatchesSearchFilters', 'No {entity} match your search and filters', { entity: entityNamePlural })\n : t('ui.advancedFilter.empty.noMatches', 'No {entity} match these filters', { entity: entityNamePlural })\n const description = searchActive\n ? t('ui.advancedFilter.empty.tryRemovingSearchFilters', 'Try a different search, remove a filter, or clear everything to see everyone.')\n : t('ui.advancedFilter.empty.tryRemoving', 'Try removing the most restrictive filter, or clear all filters to see everyone.')\n const primaryLabel = searchActive\n ? t('ui.advancedFilter.empty.clearSearchAndFilters', 'Clear search and filters')\n : t('ui.advancedFilter.empty.clearAll', 'Clear all filters')\n const handlePrimary = searchActive\n ? () => { onClearSearch?.(); onClearAll() }\n : onClearAll\n return (\n <div className=\"flex flex-col items-center justify-center gap-4 py-12\" data-testid=\"filtered-empty-results\">\n <div className=\"flex size-14 items-center justify-center rounded-full bg-muted\">\n <SearchX className=\"size-6 text-muted-foreground\" />\n </div>\n <div className=\"flex flex-col items-center gap-1\">\n <div className=\"text-base font-semibold\">{title}</div>\n <div className=\"text-sm text-muted-foreground max-w-md text-center\">{description}</div>\n </div>\n <div className=\"flex items-center gap-2\">\n <Button type=\"button\" onClick={handlePrimary}>{primaryLabel}</Button>\n <Button type=\"button\" variant=\"outline\" disabled={!canRemoveLast} onClick={onRemoveLast}>\n <Undo2 className=\"size-4\" />\n {t('ui.advancedFilter.empty.removeLast', 'Remove last filter')}\n </Button>\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": ";AAqCQ,cAEF,YAFE;AAnCR,SAAS,SAAS,aAAa;AAC/B,SAAS,cAAc;AACvB,SAAS,YAAY;AAed,SAAS,qBAAqB,EAAE,kBAAkB,eAAe,YAAY,cAAc,cAAc,GAA8B;AAC5I,QAAM,IAAI,KAAK;AACf,QAAM,eAAe,OAAO,kBAAkB;AAC9C,QAAM,QAAQ,eACV,EAAE,kDAAkD,6CAA6C,EAAE,QAAQ,iBAAiB,CAAC,IAC7H,EAAE,qCAAqC,mCAAmC,EAAE,QAAQ,iBAAiB,CAAC;AAC1G,QAAM,cAAc,eAChB,EAAE,oDAAoD,+EAA+E,IACrI,EAAE,uCAAuC,iFAAiF;AAC9H,QAAM,eAAe,eACjB,EAAE,iDAAiD,0BAA0B,IAC7E,EAAE,oCAAoC,mBAAmB;AAC7D,QAAM,gBAAgB,eAClB,MAAM;AAAE,oBAAgB;AAAG,eAAW;AAAA,EAAE,IACxC;AACJ,SACE,qBAAC,SAAI,WAAU,yDAAwD,eAAY,0BACjF;AAAA,wBAAC,SAAI,WAAU,kEACb,8BAAC,WAAQ,WAAU,gCAA+B,GACpD;AAAA,IACA,qBAAC,SAAI,WAAU,oCACb;AAAA,0BAAC,SAAI,WAAU,2BAA2B,iBAAM;AAAA,MAChD,oBAAC,SAAI,WAAU,sDAAsD,uBAAY;AAAA,OACnF;AAAA,IACA,qBAAC,SAAI,WAAU,2BACb;AAAA,0BAAC,UAAO,MAAK,UAAS,SAAS,eAAgB,wBAAa;AAAA,MAC5D,qBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,UAAU,CAAC,eAAe,SAAS,cACzE;AAAA,4BAAC,SAAM,WAAU,UAAS;AAAA,QACzB,EAAE,sCAAsC,oBAAoB;AAAA,SAC/D;AAAA,OACF;AAAA,KACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import Link from "next/link";
4
+ import { Inbox, Plus } from "lucide-react";
5
+ import { Button } from "../../primitives/button.js";
6
+ import { EmptyState } from "../../primitives/empty-state.js";
7
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
8
+ function ListEmptyState({
9
+ entityName,
10
+ title,
11
+ description,
12
+ createHref,
13
+ onCreate,
14
+ createLabel,
15
+ icon
16
+ }) {
17
+ const t = useT();
18
+ const entity = entityName ?? t("ui.dataTable.empty.genericEntity", "records");
19
+ const resolvedTitle = title ?? t("ui.dataTable.empty.title", "No {entity} yet", { entity });
20
+ const resolvedDescription = description ?? t("ui.dataTable.empty.description", "Items you add will show up here.");
21
+ const resolvedCreateLabel = createLabel ?? t("ui.dataTable.empty.create", "Create");
22
+ const action = createHref ? /* @__PURE__ */ jsx(Button, { asChild: true, children: /* @__PURE__ */ jsxs(Link, { href: createHref, children: [
23
+ /* @__PURE__ */ jsx(Plus, { className: "size-4", "aria-hidden": true }),
24
+ resolvedCreateLabel
25
+ ] }) }) : onCreate ? /* @__PURE__ */ jsxs(Button, { type: "button", onClick: onCreate, children: [
26
+ /* @__PURE__ */ jsx(Plus, { className: "size-4", "aria-hidden": true }),
27
+ resolvedCreateLabel
28
+ ] }) : null;
29
+ return /* @__PURE__ */ jsx(
30
+ EmptyState,
31
+ {
32
+ variant: "subtle",
33
+ size: "lg",
34
+ icon: icon ?? /* @__PURE__ */ jsx(Inbox, { className: "size-7", "aria-hidden": true }),
35
+ title: resolvedTitle,
36
+ description: resolvedDescription,
37
+ actions: action
38
+ }
39
+ );
40
+ }
41
+ export {
42
+ ListEmptyState
43
+ };
44
+ //# sourceMappingURL=ListEmptyState.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/filters/ListEmptyState.tsx"],
4
+ "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Inbox, Plus } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { EmptyState } from '../../primitives/empty-state'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type ListEmptyStateProps = {\n /** Plural entity label, e.g. \"companies\". Used to build the default title. */\n entityName?: string\n /** Overrides the generated title. */\n title?: string\n /** Overrides the generated description. */\n description?: string\n /** Create link target; renders the primary \"create\" action. */\n createHref?: string\n /** Create click handler (alternative to createHref). */\n onCreate?: () => void\n /** Label for the create action (defaults to a generic \"Create\"). */\n createLabel?: string\n /** Optional leading icon (defaults to a neutral inbox glyph). */\n icon?: React.ReactNode\n}\n\n/**\n * Standardized zero-records empty state for list views (DataTable `emptyState`).\n * Use this when a list has no records yet (no active search/filter). For\n * no-results-after-search the DataTable renders SearchEmptyResults, and for\n * no-results-after-filter it renders FilteredEmptyResults.\n */\nexport function ListEmptyState({\n entityName,\n title,\n description,\n createHref,\n onCreate,\n createLabel,\n icon,\n}: ListEmptyStateProps) {\n const t = useT()\n const entity = entityName ?? t('ui.dataTable.empty.genericEntity', 'records')\n const resolvedTitle = title ?? t('ui.dataTable.empty.title', 'No {entity} yet', { entity })\n const resolvedDescription = description ?? t('ui.dataTable.empty.description', 'Items you add will show up here.')\n const resolvedCreateLabel = createLabel ?? t('ui.dataTable.empty.create', 'Create')\n const action = createHref ? (\n <Button asChild>\n <Link href={createHref}>\n <Plus className=\"size-4\" aria-hidden />\n {resolvedCreateLabel}\n </Link>\n </Button>\n ) : onCreate ? (\n <Button type=\"button\" onClick={onCreate}>\n <Plus className=\"size-4\" aria-hidden />\n {resolvedCreateLabel}\n </Button>\n ) : null\n return (\n <EmptyState\n variant=\"subtle\"\n size=\"lg\"\n icon={icon ?? <Inbox className=\"size-7\" aria-hidden />}\n title={resolvedTitle}\n description={resolvedDescription}\n actions={action}\n />\n )\n}\n"],
5
+ "mappings": ";AA+CM,SACE,KADF;AA7CN,OAAO,UAAU;AACjB,SAAS,OAAO,YAAY;AAC5B,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AAyBd,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,cAAc,EAAE,oCAAoC,SAAS;AAC5E,QAAM,gBAAgB,SAAS,EAAE,4BAA4B,mBAAmB,EAAE,OAAO,CAAC;AAC1F,QAAM,sBAAsB,eAAe,EAAE,kCAAkC,kCAAkC;AACjH,QAAM,sBAAsB,eAAe,EAAE,6BAA6B,QAAQ;AAClF,QAAM,SAAS,aACb,oBAAC,UAAO,SAAO,MACb,+BAAC,QAAK,MAAM,YACV;AAAA,wBAAC,QAAK,WAAU,UAAS,eAAW,MAAC;AAAA,IACpC;AAAA,KACH,GACF,IACE,WACF,qBAAC,UAAO,MAAK,UAAS,SAAS,UAC7B;AAAA,wBAAC,QAAK,WAAU,UAAS,eAAW,MAAC;AAAA,IACpC;AAAA,KACH,IACE;AACJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,MAAM,QAAQ,oBAAC,SAAM,WAAU,UAAS,eAAW,MAAC;AAAA,MACpD,OAAO;AAAA,MACP,aAAa;AAAA,MACb,SAAS;AAAA;AAAA,EACX;AAEJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,28 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { SearchX, X } from "lucide-react";
4
+ import { Button } from "../../primitives/button.js";
5
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
6
+ function SearchEmptyResults({ query, entityNamePlural, onClearSearch }) {
7
+ const t = useT();
8
+ const entity = entityNamePlural ?? t("ui.dataTable.search.empty.genericEntity", "results");
9
+ return /* @__PURE__ */ jsxs("div", { className: "flex w-full max-w-md flex-col items-center gap-4 py-10 text-center", "data-testid": "search-empty-results", children: [
10
+ /* @__PURE__ */ jsx("div", { className: "flex size-16 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(SearchX, { className: "size-7 text-muted-foreground", "aria-hidden": true }) }),
11
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1", children: [
12
+ /* @__PURE__ */ jsx("div", { className: "text-base font-semibold text-foreground", children: t("ui.dataTable.search.empty.title", "No results found") }),
13
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t(
14
+ "ui.dataTable.search.empty.description",
15
+ "We couldn\u2019t find any {entity} matching \u201C{query}\u201D. Try adjusting your search or filters.",
16
+ { entity, query }
17
+ ) })
18
+ ] }),
19
+ /* @__PURE__ */ jsxs(Button, { type: "button", onClick: onClearSearch, children: [
20
+ /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": true }),
21
+ t("ui.dataTable.search.empty.clear", "Clear search")
22
+ ] })
23
+ ] });
24
+ }
25
+ export {
26
+ SearchEmptyResults
27
+ };
28
+ //# sourceMappingURL=SearchEmptyResults.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/filters/SearchEmptyResults.tsx"],
4
+ "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { SearchX, X } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type SearchEmptyResultsProps = {\n /** The active search query, shown back to the user. */\n query: string\n /** Plural entity label (e.g. \"companies\"); falls back to a generic word. */\n entityNamePlural?: string\n /** Clears the active search. */\n onClearSearch: () => void\n}\n\nexport function SearchEmptyResults({ query, entityNamePlural, onClearSearch }: SearchEmptyResultsProps) {\n const t = useT()\n const entity = entityNamePlural ?? t('ui.dataTable.search.empty.genericEntity', 'results')\n return (\n <div className=\"flex w-full max-w-md flex-col items-center gap-4 py-10 text-center\" data-testid=\"search-empty-results\">\n <div className=\"flex size-16 items-center justify-center rounded-full bg-muted\">\n <SearchX className=\"size-7 text-muted-foreground\" aria-hidden />\n </div>\n <div className=\"flex flex-col items-center gap-1\">\n <div className=\"text-base font-semibold text-foreground\">\n {t('ui.dataTable.search.empty.title', 'No results found')}\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'ui.dataTable.search.empty.description',\n 'We couldn\u2019t find any {entity} matching \u201C{query}\u201D. Try adjusting your search or filters.',\n { entity, query },\n )}\n </p>\n </div>\n <Button type=\"button\" onClick={onClearSearch}>\n <X className=\"size-4\" aria-hidden />\n {t('ui.dataTable.search.empty.clear', 'Clear search')}\n </Button>\n </div>\n )\n}\n"],
5
+ "mappings": ";AAqBQ,cAEF,YAFE;AAnBR,SAAS,SAAS,SAAS;AAC3B,SAAS,cAAc;AACvB,SAAS,YAAY;AAWd,SAAS,mBAAmB,EAAE,OAAO,kBAAkB,cAAc,GAA4B;AACtG,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,oBAAoB,EAAE,2CAA2C,SAAS;AACzF,SACE,qBAAC,SAAI,WAAU,sEAAqE,eAAY,wBAC9F;AAAA,wBAAC,SAAI,WAAU,kEACb,8BAAC,WAAQ,WAAU,gCAA+B,eAAW,MAAC,GAChE;AAAA,IACA,qBAAC,SAAI,WAAU,oCACb;AAAA,0BAAC,SAAI,WAAU,2CACZ,YAAE,mCAAmC,kBAAkB,GAC1D;AAAA,MACA,oBAAC,OAAE,WAAU,iCACV;AAAA,QACC;AAAA,QACA;AAAA,QACA,EAAE,QAAQ,MAAM;AAAA,MAClB,GACF;AAAA,OACF;AAAA,IACA,qBAAC,UAAO,MAAK,UAAS,SAAS,eAC7B;AAAA,0BAAC,KAAE,WAAU,UAAS,eAAW,MAAC;AAAA,MACjC,EAAE,mCAAmC,cAAc;AAAA,OACtD;AAAA,KACF;AAEJ;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.6.5-develop.4788.1.a3e2520ec0",
3
+ "version": "0.6.5-develop.4790.1.bffb4fd44b",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -149,13 +149,13 @@
149
149
  "remark-gfm": "^4.0.1"
150
150
  },
151
151
  "peerDependencies": {
152
- "@open-mercato/shared": "0.6.5-develop.4788.1.a3e2520ec0",
152
+ "@open-mercato/shared": "0.6.5-develop.4790.1.bffb4fd44b",
153
153
  "react": ">=18.0.0",
154
154
  "react-dom": ">=18.0.0",
155
155
  "react-is": ">=18.0.0"
156
156
  },
157
157
  "devDependencies": {
158
- "@open-mercato/shared": "0.6.5-develop.4788.1.a3e2520ec0",
158
+ "@open-mercato/shared": "0.6.5-develop.4790.1.bffb4fd44b",
159
159
  "@testing-library/dom": "^10.4.1",
160
160
  "@testing-library/jest-dom": "^6.9.1",
161
161
  "@testing-library/react": "^16.3.1",
@@ -3,7 +3,7 @@ import * as React from 'react'
3
3
  import { useRouter } from 'next/navigation'
4
4
  import { useReactTable, getCoreRowModel, getSortedRowModel, flexRender, type ColumnDef, type SortingState, type Column as TableColumn, type VisibilityState, type RowSelectionState } from '@tanstack/react-table'
5
5
  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
6
- import { RefreshCw, Loader2, SlidersHorizontal, MoreHorizontal, Circle, Filter, Columns3, ChevronUp, ChevronDown, ChevronsUpDown, Check } from 'lucide-react'
6
+ import { RefreshCw, Loader2, SlidersHorizontal, MoreHorizontal, Circle, Filter, Columns3, ChevronUp, ChevronDown, ChevronsUpDown, Check, Inbox } from 'lucide-react'
7
7
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../primitives/table'
8
8
  import { Button } from '../primitives/button'
9
9
  import { Checkbox } from '../primitives/checkbox'
@@ -22,6 +22,7 @@ import { TooltipProvider } from '../primitives/tooltip'
22
22
  import { TruncatedCell } from './TruncatedCell'
23
23
  import { FilterBar, type FilterDef, type FilterValues } from './FilterBar'
24
24
  import { FilteredEmptyResults } from './filters/FilteredEmptyResults'
25
+ import { SearchEmptyResults } from './filters/SearchEmptyResults'
25
26
  import { useCustomFieldFilterDefs } from './utils/customFieldFilters'
26
27
  import { fetchCustomFieldDefinitionsPayload, type CustomFieldsetDto } from './utils/customFieldDefs'
27
28
  import { RowActions, type RowActionItem } from './RowActions'
@@ -2552,6 +2553,22 @@ export function DataTable<T>({
2552
2553
  const tableScrollWrapperClassName = embedded ? '' : 'overflow-auto'
2553
2554
 
2554
2555
  const virtualScrollRef = React.useRef<HTMLDivElement>(null)
2556
+ // Measure the horizontal scroll viewport so the empty state can center within
2557
+ // the visible area instead of within the (often wider, overflowing) table.
2558
+ const [tableScrollEl, setTableScrollEl] = React.useState<HTMLDivElement | null>(null)
2559
+ const [emptyStateViewportWidth, setEmptyStateViewportWidth] = React.useState<number | null>(null)
2560
+ const setTableScrollWrapperRef = React.useCallback((node: HTMLDivElement | null) => {
2561
+ setTableScrollEl(node)
2562
+ virtualScrollRef.current = node
2563
+ }, [])
2564
+ React.useEffect(() => {
2565
+ if (!tableScrollEl || typeof ResizeObserver === 'undefined') return
2566
+ const update = () => setEmptyStateViewportWidth(tableScrollEl.clientWidth)
2567
+ update()
2568
+ const observer = new ResizeObserver(update)
2569
+ observer.observe(tableScrollEl)
2570
+ return () => observer.disconnect()
2571
+ }, [tableScrollEl])
2555
2572
  const allRows = table.getRowModel().rows
2556
2573
  const rowVirtualizer = virtualized
2557
2574
  ? useVirtualizer({
@@ -2668,7 +2685,7 @@ export function DataTable<T>({
2668
2685
  columnIds={headerColumnIds}
2669
2686
  onDragEnd={handleHeaderDragEnd}
2670
2687
  >
2671
- <div ref={virtualized ? virtualScrollRef : undefined} className={tableScrollWrapperClassName} style={virtualMaxHeightStyle}>
2688
+ <div ref={setTableScrollWrapperRef} className={tableScrollWrapperClassName} style={virtualMaxHeightStyle}>
2672
2689
  <Table className="min-w-[640px] md:min-w-0">
2673
2690
  <TableHeader>
2674
2691
  {table.getHeaderGroups().map((hg) => (
@@ -2871,26 +2888,44 @@ export function DataTable<T>({
2871
2888
  </>
2872
2889
  ) : (
2873
2890
  <TableRow>
2874
- <TableCell colSpan={mergedColumns.length + (rowActions || injectedRowActions.length > 0 ? 1 : 0) + (hasInjectedBulkActions ? 1 : 0)} className="py-6">
2875
- {filterAwareEmptyState?.active ? (
2876
- <FilteredEmptyResults
2877
- entityNamePlural={filterAwareEmptyState.entityNamePlural}
2878
- canRemoveLast={filterAwareEmptyState.canRemoveLast}
2879
- onClearAll={filterAwareEmptyState.onClearAll}
2880
- onRemoveLast={filterAwareEmptyState.onRemoveLast}
2881
- />
2882
- ) : emptyState && typeof emptyState !== 'string' ? (
2883
- emptyState
2884
- ) : (
2885
- <EmptyState
2886
- size="sm"
2887
- title={
2888
- typeof emptyState === 'string'
2889
- ? emptyState
2890
- : t('ui.dataTable.emptyState.default', 'No results.')
2891
- }
2892
- />
2893
- )}
2891
+ <TableCell colSpan={mergedColumns.length + (rowActions || injectedRowActions.length > 0 ? 1 : 0) + (hasInjectedBulkActions ? 1 : 0)} className="p-0">
2892
+ <div
2893
+ className={cn('sticky left-0 flex justify-center py-6', emptyStateViewportWidth ? '' : 'w-fit')}
2894
+ style={emptyStateViewportWidth ? { width: emptyStateViewportWidth } : undefined}
2895
+ >
2896
+ {filterAwareEmptyState?.active ? (
2897
+ <FilteredEmptyResults
2898
+ entityNamePlural={filterAwareEmptyState.entityNamePlural}
2899
+ canRemoveLast={filterAwareEmptyState.canRemoveLast}
2900
+ onClearAll={filterAwareEmptyState.onClearAll}
2901
+ onRemoveLast={filterAwareEmptyState.onRemoveLast}
2902
+ onClearSearch={searchValue && searchValue.trim().length > 0 && onSearchChange ? () => onSearchChange('') : undefined}
2903
+ />
2904
+ ) : searchValue && searchValue.trim().length > 0 && onSearchChange ? (
2905
+ <SearchEmptyResults
2906
+ query={searchValue.trim()}
2907
+ entityNamePlural={filterAwareEmptyState?.entityNamePlural}
2908
+ onClearSearch={() => onSearchChange('')}
2909
+ />
2910
+ ) : emptyState && typeof emptyState !== 'string' ? (
2911
+ emptyState
2912
+ ) : (
2913
+ <EmptyState
2914
+ variant="subtle"
2915
+ icon={<Inbox className="size-6" aria-hidden />}
2916
+ title={
2917
+ typeof emptyState === 'string'
2918
+ ? emptyState
2919
+ : t('ui.dataTable.emptyState.default', 'No results.')
2920
+ }
2921
+ description={
2922
+ typeof emptyState === 'string'
2923
+ ? undefined
2924
+ : t('ui.dataTable.emptyState.defaultDescription', 'Items will appear here once added.')
2925
+ }
2926
+ />
2927
+ )}
2928
+ </div>
2894
2929
  </TableCell>
2895
2930
  </TableRow>
2896
2931
  )}
@@ -9,27 +9,40 @@ export type FilteredEmptyResultsProps = {
9
9
  canRemoveLast: boolean
10
10
  onClearAll: () => void
11
11
  onRemoveLast: () => void
12
+ /**
13
+ * When provided, a search is ALSO active alongside the filters. Switches the
14
+ * copy to a combined search+filter message and makes the primary action clear
15
+ * both the search and the filters (so a single click shows everyone).
16
+ */
17
+ onClearSearch?: () => void
12
18
  }
13
19
 
14
- export function FilteredEmptyResults({ entityNamePlural, canRemoveLast, onClearAll, onRemoveLast }: FilteredEmptyResultsProps) {
20
+ export function FilteredEmptyResults({ entityNamePlural, canRemoveLast, onClearAll, onRemoveLast, onClearSearch }: FilteredEmptyResultsProps) {
15
21
  const t = useT()
22
+ const searchActive = typeof onClearSearch === 'function'
23
+ const title = searchActive
24
+ ? t('ui.advancedFilter.empty.noMatchesSearchFilters', 'No {entity} match your search and filters', { entity: entityNamePlural })
25
+ : t('ui.advancedFilter.empty.noMatches', 'No {entity} match these filters', { entity: entityNamePlural })
26
+ const description = searchActive
27
+ ? t('ui.advancedFilter.empty.tryRemovingSearchFilters', 'Try a different search, remove a filter, or clear everything to see everyone.')
28
+ : t('ui.advancedFilter.empty.tryRemoving', 'Try removing the most restrictive filter, or clear all filters to see everyone.')
29
+ const primaryLabel = searchActive
30
+ ? t('ui.advancedFilter.empty.clearSearchAndFilters', 'Clear search and filters')
31
+ : t('ui.advancedFilter.empty.clearAll', 'Clear all filters')
32
+ const handlePrimary = searchActive
33
+ ? () => { onClearSearch?.(); onClearAll() }
34
+ : onClearAll
16
35
  return (
17
36
  <div className="flex flex-col items-center justify-center gap-4 py-12" data-testid="filtered-empty-results">
18
37
  <div className="flex size-14 items-center justify-center rounded-full bg-muted">
19
38
  <SearchX className="size-6 text-muted-foreground" />
20
39
  </div>
21
40
  <div className="flex flex-col items-center gap-1">
22
- <div className="text-base font-semibold">
23
- {t('ui.advancedFilter.empty.noMatches', 'No {entity} match these filters', { entity: entityNamePlural })}
24
- </div>
25
- <div className="text-sm text-muted-foreground max-w-md text-center">
26
- {t('ui.advancedFilter.empty.tryRemoving', 'Try removing the most restrictive filter, or clear all filters to see everyone.')}
27
- </div>
41
+ <div className="text-base font-semibold">{title}</div>
42
+ <div className="text-sm text-muted-foreground max-w-md text-center">{description}</div>
28
43
  </div>
29
44
  <div className="flex items-center gap-2">
30
- <Button type="button" onClick={onClearAll}>
31
- {t('ui.advancedFilter.empty.clearAll', 'Clear all filters')}
32
- </Button>
45
+ <Button type="button" onClick={handlePrimary}>{primaryLabel}</Button>
33
46
  <Button type="button" variant="outline" disabled={!canRemoveLast} onClick={onRemoveLast}>
34
47
  <Undo2 className="size-4" />
35
48
  {t('ui.advancedFilter.empty.removeLast', 'Remove last filter')}
@@ -0,0 +1,69 @@
1
+ 'use client'
2
+ import * as React from 'react'
3
+ import Link from 'next/link'
4
+ import { Inbox, Plus } from 'lucide-react'
5
+ import { Button } from '../../primitives/button'
6
+ import { EmptyState } from '../../primitives/empty-state'
7
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
8
+
9
+ export type ListEmptyStateProps = {
10
+ /** Plural entity label, e.g. "companies". Used to build the default title. */
11
+ entityName?: string
12
+ /** Overrides the generated title. */
13
+ title?: string
14
+ /** Overrides the generated description. */
15
+ description?: string
16
+ /** Create link target; renders the primary "create" action. */
17
+ createHref?: string
18
+ /** Create click handler (alternative to createHref). */
19
+ onCreate?: () => void
20
+ /** Label for the create action (defaults to a generic "Create"). */
21
+ createLabel?: string
22
+ /** Optional leading icon (defaults to a neutral inbox glyph). */
23
+ icon?: React.ReactNode
24
+ }
25
+
26
+ /**
27
+ * Standardized zero-records empty state for list views (DataTable `emptyState`).
28
+ * Use this when a list has no records yet (no active search/filter). For
29
+ * no-results-after-search the DataTable renders SearchEmptyResults, and for
30
+ * no-results-after-filter it renders FilteredEmptyResults.
31
+ */
32
+ export function ListEmptyState({
33
+ entityName,
34
+ title,
35
+ description,
36
+ createHref,
37
+ onCreate,
38
+ createLabel,
39
+ icon,
40
+ }: ListEmptyStateProps) {
41
+ const t = useT()
42
+ const entity = entityName ?? t('ui.dataTable.empty.genericEntity', 'records')
43
+ const resolvedTitle = title ?? t('ui.dataTable.empty.title', 'No {entity} yet', { entity })
44
+ const resolvedDescription = description ?? t('ui.dataTable.empty.description', 'Items you add will show up here.')
45
+ const resolvedCreateLabel = createLabel ?? t('ui.dataTable.empty.create', 'Create')
46
+ const action = createHref ? (
47
+ <Button asChild>
48
+ <Link href={createHref}>
49
+ <Plus className="size-4" aria-hidden />
50
+ {resolvedCreateLabel}
51
+ </Link>
52
+ </Button>
53
+ ) : onCreate ? (
54
+ <Button type="button" onClick={onCreate}>
55
+ <Plus className="size-4" aria-hidden />
56
+ {resolvedCreateLabel}
57
+ </Button>
58
+ ) : null
59
+ return (
60
+ <EmptyState
61
+ variant="subtle"
62
+ size="lg"
63
+ icon={icon ?? <Inbox className="size-7" aria-hidden />}
64
+ title={resolvedTitle}
65
+ description={resolvedDescription}
66
+ actions={action}
67
+ />
68
+ )
69
+ }
@@ -0,0 +1,42 @@
1
+ 'use client'
2
+ import * as React from 'react'
3
+ import { SearchX, X } from 'lucide-react'
4
+ import { Button } from '../../primitives/button'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+
7
+ export type SearchEmptyResultsProps = {
8
+ /** The active search query, shown back to the user. */
9
+ query: string
10
+ /** Plural entity label (e.g. "companies"); falls back to a generic word. */
11
+ entityNamePlural?: string
12
+ /** Clears the active search. */
13
+ onClearSearch: () => void
14
+ }
15
+
16
+ export function SearchEmptyResults({ query, entityNamePlural, onClearSearch }: SearchEmptyResultsProps) {
17
+ const t = useT()
18
+ const entity = entityNamePlural ?? t('ui.dataTable.search.empty.genericEntity', 'results')
19
+ return (
20
+ <div className="flex w-full max-w-md flex-col items-center gap-4 py-10 text-center" data-testid="search-empty-results">
21
+ <div className="flex size-16 items-center justify-center rounded-full bg-muted">
22
+ <SearchX className="size-7 text-muted-foreground" aria-hidden />
23
+ </div>
24
+ <div className="flex flex-col items-center gap-1">
25
+ <div className="text-base font-semibold text-foreground">
26
+ {t('ui.dataTable.search.empty.title', 'No results found')}
27
+ </div>
28
+ <p className="text-sm text-muted-foreground">
29
+ {t(
30
+ 'ui.dataTable.search.empty.description',
31
+ 'We couldn’t find any {entity} matching “{query}”. Try adjusting your search or filters.',
32
+ { entity, query },
33
+ )}
34
+ </p>
35
+ </div>
36
+ <Button type="button" onClick={onClearSearch}>
37
+ <X className="size-4" aria-hidden />
38
+ {t('ui.dataTable.search.empty.clear', 'Clear search')}
39
+ </Button>
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import * as React from 'react'
6
+ import { render, screen } from '@testing-library/react'
7
+ import { ListEmptyState } from '../ListEmptyState'
8
+
9
+ jest.mock('@open-mercato/shared/lib/i18n/context', () => ({
10
+ useT: () => (_key: string, fallback?: string, params?: Record<string, unknown>) => {
11
+ let out = fallback ?? _key
12
+ if (params) for (const k of Object.keys(params)) out = out.replace(`{${k}}`, String(params[k]))
13
+ return out
14
+ },
15
+ }))
16
+
17
+ jest.mock('next/link', () => {
18
+ const React = require('react')
19
+ return React.forwardRef(
20
+ ({ children, href, ...rest }: any, ref: React.ForwardedRef<HTMLAnchorElement>) => (
21
+ <a href={typeof href === 'string' ? href : href?.toString?.()} ref={ref} {...rest}>
22
+ {children}
23
+ </a>
24
+ ),
25
+ )
26
+ })
27
+
28
+ describe('ListEmptyState', () => {
29
+ it('builds the title from entityName', () => {
30
+ render(<ListEmptyState entityName="companies" />)
31
+ expect(screen.getByText('No companies yet')).toBeInTheDocument()
32
+ })
33
+
34
+ it('renders a create link (role=link) with href and label when createHref is set', () => {
35
+ render(<ListEmptyState entityName="companies" createHref="/backend/customers/companies/create" createLabel="New company" />)
36
+ const link = screen.getByRole('link', { name: /new company/i })
37
+ expect(link).toBeInTheDocument()
38
+ expect(link).toHaveAttribute('href', '/backend/customers/companies/create')
39
+ })
40
+
41
+ it('renders a create button when onCreate is provided', () => {
42
+ const onCreate = jest.fn()
43
+ render(<ListEmptyState entityName="users" onCreate={onCreate} createLabel="Add user" />)
44
+ expect(screen.getByRole('button', { name: /add user/i })).toBeInTheDocument()
45
+ expect(screen.queryByRole('link')).not.toBeInTheDocument()
46
+ })
47
+
48
+ it('renders no action when neither createHref nor onCreate is provided', () => {
49
+ render(<ListEmptyState entityName="records" />)
50
+ expect(screen.queryByRole('link')).not.toBeInTheDocument()
51
+ expect(screen.queryByRole('button')).not.toBeInTheDocument()
52
+ })
53
+ })