@designa8/vibedesign-stripe-deepseek-85ea 0.2.2 → 0.2.4

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @designa8/vibedesign-stripe-deepseek-85ea
2
2
 
3
- Complete design system generated by Atoms Design, version **0.2.2**.
3
+ Complete design system generated by Atoms Design, version **0.2.4**.
4
4
  Tokens, atoms, molecules, organisms, and templates — import from the package root;
5
5
  never hardcode visual values in consumer code.
6
6
 
package/dist/index.cjs CHANGED
@@ -239,7 +239,8 @@ var ICON_REGISTRY = {
239
239
  "server": { viewBox: "0 0 24 24", nodes: [{ tag: "rect", width: "20", height: "8", x: "2", y: "2", rx: "2", ry: "2" }, { tag: "rect", width: "20", height: "8", x: "2", y: "14", rx: "2", ry: "2" }, { tag: "line", x1: "6", x2: "6.01", y1: "6", y2: "6" }, { tag: "line", x1: "6", x2: "6.01", y1: "18", y2: "18" }] },
240
240
  "grip-vertical": { viewBox: "0 0 24 24", nodes: [{ tag: "circle", cx: "9", cy: "12", r: "1" }, { tag: "circle", cx: "9", cy: "5", r: "1" }, { tag: "circle", cx: "9", cy: "19", r: "1" }, { tag: "circle", cx: "15", cy: "12", r: "1" }, { tag: "circle", cx: "15", cy: "5", r: "1" }, { tag: "circle", cx: "15", cy: "19", r: "1" }] },
241
241
  "euro": { viewBox: "0 0 24 24", nodes: [{ tag: "path", d: "M4 10h12" }, { tag: "path", d: "M4 14h9" }, { tag: "path", d: "M19 6a7.7 7.7 0 0 0-5.2-2A7.9 7.9 0 0 0 6 12c0 4.4 3.5 8 7.8 8 2 0 3.8-.8 5.2-2" }] },
242
- "square": { viewBox: "0 0 24 24", nodes: [{ tag: "rect", width: "18", height: "18", x: "3", y: "3", rx: "2" }] }
242
+ "square": { viewBox: "0 0 24 24", nodes: [{ tag: "rect", width: "18", height: "18", x: "3", y: "3", rx: "2" }] },
243
+ "eye-off": { viewBox: "0 0 24 24", nodes: [{ tag: "path", d: "M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" }, { tag: "path", d: "M14.084 14.158a3 3 0 0 1-4.242-4.242" }, { tag: "path", d: "M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" }, { tag: "path", d: "m2 2 20 20" }] }
243
244
  };
244
245
 
245
246
  // virtual-pkg:src/atoms/Icon/Icon.tsx
@@ -5031,6 +5032,7 @@ function DataTableDesktopInner(props, ref) {
5031
5032
  onFilterChange,
5032
5033
  filterBy,
5033
5034
  searchPlaceholder,
5035
+ searchable = true,
5034
5036
  query,
5035
5037
  onQueryChange,
5036
5038
  searchBy,
@@ -5194,6 +5196,7 @@ function DataTableDesktopInner(props, ref) {
5194
5196
  const headCell = HEAD_CELL;
5195
5197
  const bodyCell = dense ? "px-sm py-sm align-middle text-body-sm" : "px-sm py-md align-middle";
5196
5198
  const rangeTriggerLabel = activeRange?.detail ?? activeRange?.label ?? "";
5199
+ const showToolRow = searchable || activeFilters.length > 0 || Boolean(activeRange) || Boolean(toolbar);
5197
5200
  return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
5198
5201
  "div",
5199
5202
  {
@@ -5209,8 +5212,8 @@ function DataTableDesktopInner(props, ref) {
5209
5212
  title ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { className: "text-body-lg text-ink", children: title }) : null,
5210
5213
  description ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { className: "text-body-sm text-muted", children: description }) : null
5211
5214
  ] }) : null,
5212
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-wrap items-center gap-sm", children: [
5213
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "w-full shrink-0 sm:w-64", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
5215
+ showToolRow ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-wrap items-center gap-sm", children: [
5216
+ searchable ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "w-full shrink-0 sm:w-64", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
5214
5217
  Input,
5215
5218
  {
5216
5219
  size: "md",
@@ -5221,7 +5224,7 @@ function DataTableDesktopInner(props, ref) {
5221
5224
  placeholder: searchPlaceholder ?? "Search",
5222
5225
  leading: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(Icon, { name: "search", size: 16, strokeWidth: 2, tone: "default" })
5223
5226
  }
5224
- ) }),
5227
+ ) }) : null,
5225
5228
  activeFilters.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "flex flex-wrap items-center gap-xs", children: activeFilters.map((item) => /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
5226
5229
  Chip,
5227
5230
  {
@@ -5277,7 +5280,7 @@ function DataTableDesktopInner(props, ref) {
5277
5280
  }
5278
5281
  ) }) : null,
5279
5282
  toolbar ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "ml-auto flex shrink-0 items-center gap-xs", children: toolbar }) : null
5280
- ] })
5283
+ ] }) : null
5281
5284
  ] }),
5282
5285
  activeRange && typeof document !== "undefined" ? (0, import_react_dom2.createPortal)(
5283
5286
  /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
@@ -5703,6 +5706,7 @@ function DataTableMobileInner(props, ref) {
5703
5706
  onFilterChange,
5704
5707
  filterBy,
5705
5708
  searchPlaceholder,
5709
+ searchable = true,
5706
5710
  query,
5707
5711
  onQueryChange,
5708
5712
  searchBy,
@@ -5789,6 +5793,10 @@ function DataTableMobileInner(props, ref) {
5789
5793
  const subtitleText = activeSpec?.subtitle;
5790
5794
  const trailingText = activeSpec?.trailing;
5791
5795
  const metaText = activeSpec?.meta;
5796
+ const hasRanges = Boolean(ranges && ranges.length > 0);
5797
+ const hasHeader = Boolean(
5798
+ title || description || searchable || activeFilters.length > 0 || hasRanges || toolbar
5799
+ );
5792
5800
  return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
5793
5801
  "div",
5794
5802
  {
@@ -5799,12 +5807,12 @@ function DataTableMobileInner(props, ref) {
5799
5807
  ),
5800
5808
  ...rest,
5801
5809
  children: [
5802
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex w-full flex-col gap-base px-lg pt-lg pb-base", children: [
5810
+ hasHeader ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex w-full flex-col gap-base px-lg pt-lg pb-base", children: [
5803
5811
  title || description ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-col gap-xs", children: [
5804
5812
  title ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "text-body-lg text-ink", children: title }) : null,
5805
5813
  description ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "text-body-sm text-muted", children: description }) : null
5806
5814
  ] }) : null,
5807
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
5815
+ searchable ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
5808
5816
  "div",
5809
5817
  {
5810
5818
  className: cn24(
@@ -5829,7 +5837,7 @@ function DataTableMobileInner(props, ref) {
5829
5837
  }
5830
5838
  )
5831
5839
  }
5832
- ),
5840
+ ) : null,
5833
5841
  activeFilters.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "flex flex-wrap items-center gap-xs", children: activeFilters.map((item) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
5834
5842
  Chip,
5835
5843
  {
@@ -5844,7 +5852,7 @@ function DataTableMobileInner(props, ref) {
5844
5852
  },
5845
5853
  item.value
5846
5854
  )) }) : null,
5847
- ranges && ranges.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "flex flex-wrap items-center gap-base", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
5855
+ hasRanges && ranges ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "flex flex-wrap items-center gap-base", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
5848
5856
  FilterPill,
5849
5857
  {
5850
5858
  menuLabel: "Date range",
@@ -5857,7 +5865,7 @@ function DataTableMobileInner(props, ref) {
5857
5865
  }
5858
5866
  ) }) : null,
5859
5867
  toolbar ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "flex flex-wrap items-center gap-xs", children: toolbar }) : null
5860
- ] }),
5868
+ ] }) : null,
5861
5869
  bulkActions && selected.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-wrap items-center gap-sm border-t border-hairline bg-surface-soft px-lg py-sm", children: [
5862
5870
  /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("span", { className: "text-body-sm text-ink", children: [
5863
5871
  selected.length,
@@ -6012,6 +6020,7 @@ function DataTableInner(props, ref) {
6012
6020
  onFilterChange,
6013
6021
  filterBy,
6014
6022
  searchPlaceholder,
6023
+ searchable,
6015
6024
  query,
6016
6025
  onQueryChange,
6017
6026
  searchBy,
@@ -6073,6 +6082,7 @@ function DataTableInner(props, ref) {
6073
6082
  onFilterChange: handleFilter,
6074
6083
  filterBy,
6075
6084
  searchPlaceholder,
6085
+ searchable,
6076
6086
  query: activeQuery,
6077
6087
  onQueryChange: handleQuery,
6078
6088
  searchBy,
package/dist/index.js CHANGED
@@ -102,7 +102,8 @@ var ICON_REGISTRY = {
102
102
  "server": { viewBox: "0 0 24 24", nodes: [{ tag: "rect", width: "20", height: "8", x: "2", y: "2", rx: "2", ry: "2" }, { tag: "rect", width: "20", height: "8", x: "2", y: "14", rx: "2", ry: "2" }, { tag: "line", x1: "6", x2: "6.01", y1: "6", y2: "6" }, { tag: "line", x1: "6", x2: "6.01", y1: "18", y2: "18" }] },
103
103
  "grip-vertical": { viewBox: "0 0 24 24", nodes: [{ tag: "circle", cx: "9", cy: "12", r: "1" }, { tag: "circle", cx: "9", cy: "5", r: "1" }, { tag: "circle", cx: "9", cy: "19", r: "1" }, { tag: "circle", cx: "15", cy: "12", r: "1" }, { tag: "circle", cx: "15", cy: "5", r: "1" }, { tag: "circle", cx: "15", cy: "19", r: "1" }] },
104
104
  "euro": { viewBox: "0 0 24 24", nodes: [{ tag: "path", d: "M4 10h12" }, { tag: "path", d: "M4 14h9" }, { tag: "path", d: "M19 6a7.7 7.7 0 0 0-5.2-2A7.9 7.9 0 0 0 6 12c0 4.4 3.5 8 7.8 8 2 0 3.8-.8 5.2-2" }] },
105
- "square": { viewBox: "0 0 24 24", nodes: [{ tag: "rect", width: "18", height: "18", x: "3", y: "3", rx: "2" }] }
105
+ "square": { viewBox: "0 0 24 24", nodes: [{ tag: "rect", width: "18", height: "18", x: "3", y: "3", rx: "2" }] },
106
+ "eye-off": { viewBox: "0 0 24 24", nodes: [{ tag: "path", d: "M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" }, { tag: "path", d: "M14.084 14.158a3 3 0 0 1-4.242-4.242" }, { tag: "path", d: "M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" }, { tag: "path", d: "m2 2 20 20" }] }
106
107
  };
107
108
 
108
109
  // virtual-pkg:src/atoms/Icon/Icon.tsx
@@ -4894,6 +4895,7 @@ function DataTableDesktopInner(props, ref) {
4894
4895
  onFilterChange,
4895
4896
  filterBy,
4896
4897
  searchPlaceholder,
4898
+ searchable = true,
4897
4899
  query,
4898
4900
  onQueryChange,
4899
4901
  searchBy,
@@ -5057,6 +5059,7 @@ function DataTableDesktopInner(props, ref) {
5057
5059
  const headCell = HEAD_CELL;
5058
5060
  const bodyCell = dense ? "px-sm py-sm align-middle text-body-sm" : "px-sm py-md align-middle";
5059
5061
  const rangeTriggerLabel = activeRange?.detail ?? activeRange?.label ?? "";
5062
+ const showToolRow = searchable || activeFilters.length > 0 || Boolean(activeRange) || Boolean(toolbar);
5060
5063
  return /* @__PURE__ */ jsxs26(
5061
5064
  "div",
5062
5065
  {
@@ -5072,8 +5075,8 @@ function DataTableDesktopInner(props, ref) {
5072
5075
  title ? /* @__PURE__ */ jsx29("p", { className: "text-body-lg text-ink", children: title }) : null,
5073
5076
  description ? /* @__PURE__ */ jsx29("p", { className: "text-body-sm text-muted", children: description }) : null
5074
5077
  ] }) : null,
5075
- /* @__PURE__ */ jsxs26("div", { className: "flex flex-wrap items-center gap-sm", children: [
5076
- /* @__PURE__ */ jsx29("div", { className: "w-full shrink-0 sm:w-64", children: /* @__PURE__ */ jsx29(
5078
+ showToolRow ? /* @__PURE__ */ jsxs26("div", { className: "flex flex-wrap items-center gap-sm", children: [
5079
+ searchable ? /* @__PURE__ */ jsx29("div", { className: "w-full shrink-0 sm:w-64", children: /* @__PURE__ */ jsx29(
5077
5080
  Input,
5078
5081
  {
5079
5082
  size: "md",
@@ -5084,7 +5087,7 @@ function DataTableDesktopInner(props, ref) {
5084
5087
  placeholder: searchPlaceholder ?? "Search",
5085
5088
  leading: /* @__PURE__ */ jsx29(Icon, { name: "search", size: 16, strokeWidth: 2, tone: "default" })
5086
5089
  }
5087
- ) }),
5090
+ ) }) : null,
5088
5091
  activeFilters.length > 0 ? /* @__PURE__ */ jsx29("div", { className: "flex flex-wrap items-center gap-xs", children: activeFilters.map((item) => /* @__PURE__ */ jsxs26(
5089
5092
  Chip,
5090
5093
  {
@@ -5140,7 +5143,7 @@ function DataTableDesktopInner(props, ref) {
5140
5143
  }
5141
5144
  ) }) : null,
5142
5145
  toolbar ? /* @__PURE__ */ jsx29("div", { className: "ml-auto flex shrink-0 items-center gap-xs", children: toolbar }) : null
5143
- ] })
5146
+ ] }) : null
5144
5147
  ] }),
5145
5148
  activeRange && typeof document !== "undefined" ? createPortal5(
5146
5149
  /* @__PURE__ */ jsx29(
@@ -5566,6 +5569,7 @@ function DataTableMobileInner(props, ref) {
5566
5569
  onFilterChange,
5567
5570
  filterBy,
5568
5571
  searchPlaceholder,
5572
+ searchable = true,
5569
5573
  query,
5570
5574
  onQueryChange,
5571
5575
  searchBy,
@@ -5652,6 +5656,10 @@ function DataTableMobileInner(props, ref) {
5652
5656
  const subtitleText = activeSpec?.subtitle;
5653
5657
  const trailingText = activeSpec?.trailing;
5654
5658
  const metaText = activeSpec?.meta;
5659
+ const hasRanges = Boolean(ranges && ranges.length > 0);
5660
+ const hasHeader = Boolean(
5661
+ title || description || searchable || activeFilters.length > 0 || hasRanges || toolbar
5662
+ );
5655
5663
  return /* @__PURE__ */ jsxs27(
5656
5664
  "div",
5657
5665
  {
@@ -5662,12 +5670,12 @@ function DataTableMobileInner(props, ref) {
5662
5670
  ),
5663
5671
  ...rest,
5664
5672
  children: [
5665
- /* @__PURE__ */ jsxs27("div", { className: "flex w-full flex-col gap-base px-lg pt-lg pb-base", children: [
5673
+ hasHeader ? /* @__PURE__ */ jsxs27("div", { className: "flex w-full flex-col gap-base px-lg pt-lg pb-base", children: [
5666
5674
  title || description ? /* @__PURE__ */ jsxs27("div", { className: "flex flex-col gap-xs", children: [
5667
5675
  title ? /* @__PURE__ */ jsx30("p", { className: "text-body-lg text-ink", children: title }) : null,
5668
5676
  description ? /* @__PURE__ */ jsx30("p", { className: "text-body-sm text-muted", children: description }) : null
5669
5677
  ] }) : null,
5670
- /* @__PURE__ */ jsx30(
5678
+ searchable ? /* @__PURE__ */ jsx30(
5671
5679
  "div",
5672
5680
  {
5673
5681
  className: cn24(
@@ -5692,7 +5700,7 @@ function DataTableMobileInner(props, ref) {
5692
5700
  }
5693
5701
  )
5694
5702
  }
5695
- ),
5703
+ ) : null,
5696
5704
  activeFilters.length > 0 ? /* @__PURE__ */ jsx30("div", { className: "flex flex-wrap items-center gap-xs", children: activeFilters.map((item) => /* @__PURE__ */ jsxs27(
5697
5705
  Chip,
5698
5706
  {
@@ -5707,7 +5715,7 @@ function DataTableMobileInner(props, ref) {
5707
5715
  },
5708
5716
  item.value
5709
5717
  )) }) : null,
5710
- ranges && ranges.length > 0 ? /* @__PURE__ */ jsx30("div", { className: "flex flex-wrap items-center gap-base", children: /* @__PURE__ */ jsx30(
5718
+ hasRanges && ranges ? /* @__PURE__ */ jsx30("div", { className: "flex flex-wrap items-center gap-base", children: /* @__PURE__ */ jsx30(
5711
5719
  FilterPill,
5712
5720
  {
5713
5721
  menuLabel: "Date range",
@@ -5720,7 +5728,7 @@ function DataTableMobileInner(props, ref) {
5720
5728
  }
5721
5729
  ) }) : null,
5722
5730
  toolbar ? /* @__PURE__ */ jsx30("div", { className: "flex flex-wrap items-center gap-xs", children: toolbar }) : null
5723
- ] }),
5731
+ ] }) : null,
5724
5732
  bulkActions && selected.length > 0 ? /* @__PURE__ */ jsxs27("div", { className: "flex flex-wrap items-center gap-sm border-t border-hairline bg-surface-soft px-lg py-sm", children: [
5725
5733
  /* @__PURE__ */ jsxs27("span", { className: "text-body-sm text-ink", children: [
5726
5734
  selected.length,
@@ -5875,6 +5883,7 @@ function DataTableInner(props, ref) {
5875
5883
  onFilterChange,
5876
5884
  filterBy,
5877
5885
  searchPlaceholder,
5886
+ searchable,
5878
5887
  query,
5879
5888
  onQueryChange,
5880
5889
  searchBy,
@@ -5936,6 +5945,7 @@ function DataTableInner(props, ref) {
5936
5945
  onFilterChange: handleFilter,
5937
5946
  filterBy,
5938
5947
  searchPlaceholder,
5948
+ searchable,
5939
5949
  query: activeQuery,
5940
5950
  onQueryChange: handleQuery,
5941
5951
  searchBy,
package/dist/styles.css CHANGED
@@ -3461,6 +3461,11 @@
3461
3461
  width: calc(var(--spacing) * 64);
3462
3462
  }
3463
3463
  }
3464
+ .sm\:w-80 {
3465
+ @media (width >= 640px) {
3466
+ width: calc(var(--spacing) * 80);
3467
+ }
3468
+ }
3464
3469
  .sm\:grid-cols-2 {
3465
3470
  @media (width >= 640px) {
3466
3471
  grid-template-columns: repeat(2, minmax(0, 1fr));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@designa8/vibedesign-stripe-deepseek-85ea",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Complete design system \"My Project stripe-deepseek\" generated by Atoms Design.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -38,6 +38,12 @@ export interface DataTableProps<T extends DataTableBaseRow = DataTableBaseRow>
38
38
  onFilterChange?: (value: string) => void;
39
39
  filterBy?: (row: T, value: string) => boolean;
40
40
  searchPlaceholder?: string;
41
+ /**
42
+ * Renders the built-in search field in both layouts. Defaults to true. Set
43
+ * it to false when the page already owns a global search bar: `query` and
44
+ * `searchBy` keep filtering exactly the same, only the field is left out.
45
+ */
46
+ searchable?: boolean;
41
47
  query?: string;
42
48
  onQueryChange?: (value: string) => void;
43
49
  searchBy?: (row: T) => string;
@@ -96,6 +102,7 @@ function DataTableInner<T extends DataTableBaseRow>(
96
102
  onFilterChange,
97
103
  filterBy,
98
104
  searchPlaceholder,
105
+ searchable,
99
106
  query,
100
107
  onQueryChange,
101
108
  searchBy,
@@ -167,6 +174,7 @@ function DataTableInner<T extends DataTableBaseRow>(
167
174
  onFilterChange: handleFilter,
168
175
  filterBy,
169
176
  searchPlaceholder,
177
+ searchable,
170
178
  query: activeQuery,
171
179
  onQueryChange: handleQuery,
172
180
  searchBy,
@@ -1,6 +1,8 @@
1
1
  import * as React from "react";
2
2
  import { Badge } from "../Badge";
3
3
  import { Button } from "../Button";
4
+ import { Icon } from "../Icon";
5
+ import { Input } from "../Input";
4
6
  import { DataTable } from "./DataTable";
5
7
  import type { DataTableColumn, DataTableFilter, DataTableRange } from "./DataTable";
6
8
 
@@ -148,6 +150,7 @@ export default function Preview() {
148
150
  const [selected, setSelected] = React.useState<string[]>([]);
149
151
  const [filter, setFilter] = React.useState("all");
150
152
  const [query, setQuery] = React.useState("");
153
+ const [pageQuery, setPageQuery] = React.useState("");
151
154
 
152
155
  return (
153
156
  <div className="w-full max-w-5xl mx-auto p-lg space-y-lg bg-canvas">
@@ -221,6 +224,46 @@ export default function Preview() {
221
224
  selectable={false}
222
225
  mobileRow={mobileRow}
223
226
  />
227
+
228
+ <div className="flex w-full flex-col gap-md">
229
+ <div className="flex w-full flex-col gap-xs">
230
+ <p className="text-body-lg text-ink">Page-level search</p>
231
+ <p className="text-body-sm text-muted">
232
+ The table renders no field of its own, but the controlled query
233
+ still filters both layouts.
234
+ </p>
235
+ </div>
236
+
237
+ <div className="w-full sm:w-80">
238
+ <Input
239
+ size="md"
240
+ type="search"
241
+ value={pageQuery}
242
+ onChange={(event) => setPageQuery(event.target.value)}
243
+ ariaLabel="Search all invoices"
244
+ placeholder="Search all invoices"
245
+ leading={<Icon name="search" size={16} strokeWidth={2} tone="default" />}
246
+ />
247
+ </div>
248
+
249
+ <DataTable<Invoice>
250
+ columns={columns}
251
+ rows={invoices}
252
+ searchable={false}
253
+ query={pageQuery}
254
+ onQueryChange={setPageQuery}
255
+ searchBy={(row) => `${row.id} ${row.customer}`}
256
+ filters={filters.slice(0, 1)}
257
+ pageSize={5}
258
+ mobilePageSize={4}
259
+ itemLabel="invoices"
260
+ dense
261
+ selectable={false}
262
+ mobileRow={mobileRow}
263
+ emptyTitle="Nothing matches that search"
264
+ emptyDescription="Try a customer name or an invoice number."
265
+ />
266
+ </div>
224
267
  </div>
225
268
  );
226
269
  }
@@ -226,6 +226,12 @@ export interface DataTableDesktopProps<
226
226
  /** Row predicate for the active filter. `"all"` always shows everything. */
227
227
  filterBy?: (row: T, value: string) => boolean;
228
228
  searchPlaceholder?: string;
229
+ /**
230
+ * Renders the built-in search field. Defaults to true. Set it to false when
231
+ * the page already owns a global search bar: `query` and `searchBy` keep
232
+ * filtering exactly the same, only the field is left out.
233
+ */
234
+ searchable?: boolean;
229
235
  /** Controlled search text. */
230
236
  query?: string;
231
237
  onQueryChange?: (value: string) => void;
@@ -277,6 +283,7 @@ function DataTableDesktopInner<T extends DataTableBaseRow>(
277
283
  onFilterChange,
278
284
  filterBy,
279
285
  searchPlaceholder,
286
+ searchable = true,
280
287
  query,
281
288
  onQueryChange,
282
289
  searchBy,
@@ -505,6 +512,10 @@ function DataTableDesktopInner<T extends DataTableBaseRow>(
505
512
 
506
513
  const rangeTriggerLabel = activeRange?.detail ?? activeRange?.label ?? "";
507
514
 
515
+ /** The tool row is only worth its vertical space when it holds something. */
516
+ const showToolRow =
517
+ searchable || activeFilters.length > 0 || Boolean(activeRange) || Boolean(toolbar);
518
+
508
519
  return (
509
520
  <div
510
521
  ref={ref}
@@ -526,89 +537,93 @@ function DataTableDesktopInner<T extends DataTableBaseRow>(
526
537
  </div>
527
538
  ) : null}
528
539
 
529
- <div className="flex flex-wrap items-center gap-sm">
530
- <div className="w-full shrink-0 sm:w-64">
531
- <Input
532
- size="md"
533
- type="search"
534
- value={activeQuery}
535
- onChange={(event) => applyQuery(event.target.value)}
536
- ariaLabel={searchPlaceholder ?? "Search table"}
537
- placeholder={searchPlaceholder ?? "Search"}
538
- leading={
539
- <Icon name="search" size={16} strokeWidth={2} tone="default" />
540
- }
541
- />
542
- </div>
543
-
544
- {activeFilters.length > 0 ? (
545
- <div className="flex flex-wrap items-center gap-xs">
546
- {activeFilters.map((item) => (
547
- <Chip
548
- key={item.value}
549
- appearance="outline"
540
+ {showToolRow ? (
541
+ <div className="flex flex-wrap items-center gap-sm">
542
+ {searchable ? (
543
+ <div className="w-full shrink-0 sm:w-64">
544
+ <Input
550
545
  size="md"
551
- selected={activeFilter === item.value}
552
- onClick={() => applyFilter(item.value)}
553
- >
554
- {item.label}
555
- {typeof item.count === "number" ? (
556
- <span className="ml-xs text-muted-soft">{item.count}</span>
557
- ) : null}
558
- </Chip>
559
- ))}
560
- </div>
561
- ) : null}
546
+ type="search"
547
+ value={activeQuery}
548
+ onChange={(event) => applyQuery(event.target.value)}
549
+ ariaLabel={searchPlaceholder ?? "Search table"}
550
+ placeholder={searchPlaceholder ?? "Search"}
551
+ leading={
552
+ <Icon name="search" size={16} strokeWidth={2} tone="default" />
553
+ }
554
+ />
555
+ </div>
556
+ ) : null}
562
557
 
563
- {activeRange ? (
564
- <div className="relative shrink-0">
565
- <Button
566
- ref={triggerRef}
567
- variant="field-soft"
568
- size="field"
569
- className="w-auto"
570
- aria-expanded={rangeOpen}
571
- aria-controls={panelId}
572
- aria-haspopup="menu"
573
- aria-label={`Date range: ${rangeTriggerLabel}`}
574
- onClick={() => {
575
- placePanel();
576
- setRangeOpen((value) => !value);
577
- }}
578
- leadingIcon={
579
- <Icon
580
- name="calendar"
581
- size={16}
582
- strokeWidth={1.75}
583
- tone="default"
584
- />
585
- }
586
- trailingIcon={
587
- <Icon
588
- name="chevron-down"
589
- size={16}
590
- strokeWidth={2}
591
- tone="muted"
592
- className={cn(
593
- "transition-transform duration-200 ease-out",
594
- rangeOpen && "rotate-180",
595
- )}
596
- />
597
- }
598
- >
599
- <span className="whitespace-nowrap text-body-sm text-ink">
600
- {rangeTriggerLabel}
601
- </span>
602
- </Button>
603
- </div>
604
- ) : null}
558
+ {activeFilters.length > 0 ? (
559
+ <div className="flex flex-wrap items-center gap-xs">
560
+ {activeFilters.map((item) => (
561
+ <Chip
562
+ key={item.value}
563
+ appearance="outline"
564
+ size="md"
565
+ selected={activeFilter === item.value}
566
+ onClick={() => applyFilter(item.value)}
567
+ >
568
+ {item.label}
569
+ {typeof item.count === "number" ? (
570
+ <span className="ml-xs text-muted-soft">{item.count}</span>
571
+ ) : null}
572
+ </Chip>
573
+ ))}
574
+ </div>
575
+ ) : null}
605
576
 
606
- {toolbar ? (
607
- <div className="ml-auto flex shrink-0 items-center gap-xs">
608
- {toolbar}
609
- </div>
610
- ) : null}
611
- </div>
577
+ {activeRange ? (
578
+ <div className="relative shrink-0">
579
+ <Button
580
+ ref={triggerRef}
581
+ variant="field-soft"
582
+ size="field"
583
+ className="w-auto"
584
+ aria-expanded={rangeOpen}
585
+ aria-controls={panelId}
586
+ aria-haspopup="menu"
587
+ aria-label={`Date range: ${rangeTriggerLabel}`}
588
+ onClick={() => {
589
+ placePanel();
590
+ setRangeOpen((value) => !value);
591
+ }}
592
+ leadingIcon={
593
+ <Icon
594
+ name="calendar"
595
+ size={16}
596
+ strokeWidth={1.75}
597
+ tone="default"
598
+ />
599
+ }
600
+ trailingIcon={
601
+ <Icon
602
+ name="chevron-down"
603
+ size={16}
604
+ strokeWidth={2}
605
+ tone="muted"
606
+ className={cn(
607
+ "transition-transform duration-200 ease-out",
608
+ rangeOpen && "rotate-180",
609
+ )}
610
+ />
611
+ }
612
+ >
613
+ <span className="whitespace-nowrap text-body-sm text-ink">
614
+ {rangeTriggerLabel}
615
+ </span>
616
+ </Button>
617
+ </div>
618
+ ) : null}
619
+
620
+ {toolbar ? (
621
+ <div className="ml-auto flex shrink-0 items-center gap-xs">
622
+ {toolbar}
623
+ </div>
624
+ ) : null}
625
+ </div>
626
+ ) : null}
612
627
  </div>
613
628
 
614
629
  {activeRange && typeof document !== "undefined"
@@ -121,6 +121,12 @@ export interface DataTableMobileProps<
121
121
  onFilterChange?: (value: string) => void;
122
122
  filterBy?: (row: T, value: string) => boolean;
123
123
  searchPlaceholder?: string;
124
+ /**
125
+ * Renders the built-in search field. Defaults to true. Set it to false when
126
+ * the page already owns a global search bar: `query` and `searchBy` keep
127
+ * filtering exactly the same, only the field is left out.
128
+ */
129
+ searchable?: boolean;
124
130
  query?: string;
125
131
  onQueryChange?: (value: string) => void;
126
132
  searchBy?: (row: T) => string;
@@ -310,6 +316,7 @@ function DataTableMobileInner<T extends DataTableMobileBaseRow>(
310
316
  onFilterChange,
311
317
  filterBy,
312
318
  searchPlaceholder,
319
+ searchable = true,
313
320
  query,
314
321
  onQueryChange,
315
322
  searchBy,
@@ -441,6 +448,13 @@ function DataTableMobileInner<T extends DataTableMobileBaseRow>(
441
448
  const trailingText = activeSpec?.trailing;
442
449
  const metaText = activeSpec?.meta;
443
450
 
451
+ const hasRanges = Boolean(ranges && ranges.length > 0);
452
+
453
+ /** Nothing to show above the list means no header block, and no dead space. */
454
+ const hasHeader = Boolean(
455
+ title || description || searchable || activeFilters.length > 0 || hasRanges || toolbar,
456
+ );
457
+
444
458
  return (
445
459
  <div
446
460
  ref={ref}
@@ -450,77 +464,83 @@ function DataTableMobileInner<T extends DataTableMobileBaseRow>(
450
464
  )}
451
465
  {...rest}
452
466
  >
453
- <div className="flex w-full flex-col gap-base px-lg pt-lg pb-base">
454
- {title || description ? (
455
- <div className="flex flex-col gap-xs">
456
- {title ? <p className="text-body-lg text-ink">{title}</p> : null}
457
- {description ? (
458
- <p className="text-body-sm text-muted">{description}</p>
459
- ) : null}
460
- </div>
461
- ) : null}
462
-
463
- <div
464
- className={cn(
465
- "flex min-h-12 w-full items-center gap-sm rounded-xl border px-base",
466
- "border-hairline bg-canvas",
467
- "transition duration-150 ease-out",
468
- "focus-within:border-primary focus-within:ring-1 focus-within:ring-primary",
469
- )}
470
- >
471
- <Input
472
- chrome="embedded"
473
- type="search"
474
- value={activeQuery}
475
- onChange={(event) => applyQuery(event.target.value)}
476
- placeholder={searchPlaceholder ?? "Search"}
477
- ariaLabel={searchPlaceholder ?? "Search table"}
478
- autoComplete="off"
479
- className="flex-1"
480
- inputClassName="[&::-webkit-search-cancel-button]:appearance-none"
481
- leading={
482
- <Icon name="search" size={18} strokeWidth={2} tone="default" />
483
- }
484
- />
467
+ {hasHeader ? (
468
+ <div className="flex w-full flex-col gap-base px-lg pt-lg pb-base">
469
+ {title || description ? (
470
+ <div className="flex flex-col gap-xs">
471
+ {title ? <p className="text-body-lg text-ink">{title}</p> : null}
472
+ {description ? (
473
+ <p className="text-body-sm text-muted">{description}</p>
474
+ ) : null}
475
+ </div>
476
+ ) : null}
477
+
478
+ {searchable ? (
479
+ <div
480
+ className={cn(
481
+ "flex min-h-12 w-full items-center gap-sm rounded-xl border px-base",
482
+ "border-hairline bg-canvas",
483
+ "transition duration-150 ease-out",
484
+ "focus-within:border-primary focus-within:ring-1 focus-within:ring-primary",
485
+ )}
486
+ >
487
+ <Input
488
+ chrome="embedded"
489
+ type="search"
490
+ value={activeQuery}
491
+ onChange={(event) => applyQuery(event.target.value)}
492
+ placeholder={searchPlaceholder ?? "Search"}
493
+ ariaLabel={searchPlaceholder ?? "Search table"}
494
+ autoComplete="off"
495
+ className="flex-1"
496
+ inputClassName="[&::-webkit-search-cancel-button]:appearance-none"
497
+ leading={
498
+ <Icon name="search" size={18} strokeWidth={2} tone="default" />
499
+ }
500
+ />
501
+ </div>
502
+ ) : null}
503
+
504
+ {activeFilters.length > 0 ? (
505
+ <div className="flex flex-wrap items-center gap-xs">
506
+ {activeFilters.map((item) => (
507
+ <Chip
508
+ key={item.value}
509
+ appearance="outline"
510
+ size="md"
511
+ selected={activeFilter === item.value}
512
+ onClick={() => applyFilter(item.value)}
513
+ >
514
+ {item.label}
515
+ {typeof item.count === "number" ? (
516
+ <span className="ml-xs text-muted-soft">{item.count}</span>
517
+ ) : null}
518
+ </Chip>
519
+ ))}
520
+ </div>
521
+ ) : null}
522
+
523
+ {hasRanges && ranges ? (
524
+ <div className="flex flex-wrap items-center gap-base">
525
+ <FilterPill
526
+ menuLabel="Date range"
527
+ options={ranges.map((item) => ({
528
+ value: item.value,
529
+ label: item.detail
530
+ ? `${item.label} · ${item.detail}`
531
+ : item.label,
532
+ }))}
533
+ value={activeRange?.value ?? ranges[0].value}
534
+ onValueChange={applyRange}
535
+ />
536
+ </div>
537
+ ) : null}
538
+
539
+ {toolbar ? (
540
+ <div className="flex flex-wrap items-center gap-xs">{toolbar}</div>
541
+ ) : null}
485
542
  </div>
486
-
487
- {activeFilters.length > 0 ? (
488
- <div className="flex flex-wrap items-center gap-xs">
489
- {activeFilters.map((item) => (
490
- <Chip
491
- key={item.value}
492
- appearance="outline"
493
- size="md"
494
- selected={activeFilter === item.value}
495
- onClick={() => applyFilter(item.value)}
496
- >
497
- {item.label}
498
- {typeof item.count === "number" ? (
499
- <span className="ml-xs text-muted-soft">{item.count}</span>
500
- ) : null}
501
- </Chip>
502
- ))}
503
- </div>
504
- ) : null}
505
-
506
- {ranges && ranges.length > 0 ? (
507
- <div className="flex flex-wrap items-center gap-base">
508
- <FilterPill
509
- menuLabel="Date range"
510
- options={ranges.map((item) => ({
511
- value: item.value,
512
- label: item.detail ? `${item.label} · ${item.detail}` : item.label,
513
- }))}
514
- value={activeRange?.value ?? ranges[0].value}
515
- onValueChange={applyRange}
516
- />
517
- </div>
518
- ) : null}
519
-
520
- {toolbar ? (
521
- <div className="flex flex-wrap items-center gap-xs">{toolbar}</div>
522
- ) : null}
523
- </div>
543
+ ) : null}
524
544
 
525
545
  {bulkActions && selected.length > 0 ? (
526
546
  <div className="flex flex-wrap items-center gap-sm border-t border-hairline bg-surface-soft px-lg py-sm">
@@ -93,6 +93,7 @@ export const ICON_REGISTRY = {
93
93
  "grip-vertical": { viewBox: "0 0 24 24", nodes: [{ tag: "circle", cx: "9", cy: "12", r: "1" }, { tag: "circle", cx: "9", cy: "5", r: "1" }, { tag: "circle", cx: "9", cy: "19", r: "1" }, { tag: "circle", cx: "15", cy: "12", r: "1" }, { tag: "circle", cx: "15", cy: "5", r: "1" }, { tag: "circle", cx: "15", cy: "19", r: "1" }] },
94
94
  "euro": { viewBox: "0 0 24 24", nodes: [{ tag: "path", d: "M4 10h12" }, { tag: "path", d: "M4 14h9" }, { tag: "path", d: "M19 6a7.7 7.7 0 0 0-5.2-2A7.9 7.9 0 0 0 6 12c0 4.4 3.5 8 7.8 8 2 0 3.8-.8 5.2-2" }] },
95
95
  "square": { viewBox: "0 0 24 24", nodes: [{ tag: "rect", width: "18", height: "18", x: "3", y: "3", rx: "2" }] },
96
+ "eye-off": { viewBox: "0 0 24 24", nodes: [{ tag: "path", d: "M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" }, { tag: "path", d: "M14.084 14.158a3 3 0 0 1-4.242-4.242" }, { tag: "path", d: "M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" }, { tag: "path", d: "m2 2 20 20" }] },
96
97
  } as const;
97
98
 
98
99
  export type IconName = keyof typeof ICON_REGISTRY;