@designa8/vibedesign-stripe-deepseek-85ea 0.2.3 → 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.3**.
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
@@ -5032,6 +5032,7 @@ function DataTableDesktopInner(props, ref) {
5032
5032
  onFilterChange,
5033
5033
  filterBy,
5034
5034
  searchPlaceholder,
5035
+ searchable = true,
5035
5036
  query,
5036
5037
  onQueryChange,
5037
5038
  searchBy,
@@ -5195,6 +5196,7 @@ function DataTableDesktopInner(props, ref) {
5195
5196
  const headCell = HEAD_CELL;
5196
5197
  const bodyCell = dense ? "px-sm py-sm align-middle text-body-sm" : "px-sm py-md align-middle";
5197
5198
  const rangeTriggerLabel = activeRange?.detail ?? activeRange?.label ?? "";
5199
+ const showToolRow = searchable || activeFilters.length > 0 || Boolean(activeRange) || Boolean(toolbar);
5198
5200
  return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
5199
5201
  "div",
5200
5202
  {
@@ -5210,8 +5212,8 @@ function DataTableDesktopInner(props, ref) {
5210
5212
  title ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { className: "text-body-lg text-ink", children: title }) : null,
5211
5213
  description ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { className: "text-body-sm text-muted", children: description }) : null
5212
5214
  ] }) : null,
5213
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-wrap items-center gap-sm", children: [
5214
- /* @__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)(
5215
5217
  Input,
5216
5218
  {
5217
5219
  size: "md",
@@ -5222,7 +5224,7 @@ function DataTableDesktopInner(props, ref) {
5222
5224
  placeholder: searchPlaceholder ?? "Search",
5223
5225
  leading: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(Icon, { name: "search", size: 16, strokeWidth: 2, tone: "default" })
5224
5226
  }
5225
- ) }),
5227
+ ) }) : null,
5226
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)(
5227
5229
  Chip,
5228
5230
  {
@@ -5278,7 +5280,7 @@ function DataTableDesktopInner(props, ref) {
5278
5280
  }
5279
5281
  ) }) : null,
5280
5282
  toolbar ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "ml-auto flex shrink-0 items-center gap-xs", children: toolbar }) : null
5281
- ] })
5283
+ ] }) : null
5282
5284
  ] }),
5283
5285
  activeRange && typeof document !== "undefined" ? (0, import_react_dom2.createPortal)(
5284
5286
  /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
@@ -5704,6 +5706,7 @@ function DataTableMobileInner(props, ref) {
5704
5706
  onFilterChange,
5705
5707
  filterBy,
5706
5708
  searchPlaceholder,
5709
+ searchable = true,
5707
5710
  query,
5708
5711
  onQueryChange,
5709
5712
  searchBy,
@@ -5790,6 +5793,10 @@ function DataTableMobileInner(props, ref) {
5790
5793
  const subtitleText = activeSpec?.subtitle;
5791
5794
  const trailingText = activeSpec?.trailing;
5792
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
+ );
5793
5800
  return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
5794
5801
  "div",
5795
5802
  {
@@ -5800,12 +5807,12 @@ function DataTableMobileInner(props, ref) {
5800
5807
  ),
5801
5808
  ...rest,
5802
5809
  children: [
5803
- /* @__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: [
5804
5811
  title || description ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-col gap-xs", children: [
5805
5812
  title ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "text-body-lg text-ink", children: title }) : null,
5806
5813
  description ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "text-body-sm text-muted", children: description }) : null
5807
5814
  ] }) : null,
5808
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
5815
+ searchable ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
5809
5816
  "div",
5810
5817
  {
5811
5818
  className: cn24(
@@ -5830,7 +5837,7 @@ function DataTableMobileInner(props, ref) {
5830
5837
  }
5831
5838
  )
5832
5839
  }
5833
- ),
5840
+ ) : null,
5834
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)(
5835
5842
  Chip,
5836
5843
  {
@@ -5845,7 +5852,7 @@ function DataTableMobileInner(props, ref) {
5845
5852
  },
5846
5853
  item.value
5847
5854
  )) }) : null,
5848
- 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)(
5849
5856
  FilterPill,
5850
5857
  {
5851
5858
  menuLabel: "Date range",
@@ -5858,7 +5865,7 @@ function DataTableMobileInner(props, ref) {
5858
5865
  }
5859
5866
  ) }) : null,
5860
5867
  toolbar ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "flex flex-wrap items-center gap-xs", children: toolbar }) : null
5861
- ] }),
5868
+ ] }) : null,
5862
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: [
5863
5870
  /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("span", { className: "text-body-sm text-ink", children: [
5864
5871
  selected.length,
@@ -6013,6 +6020,7 @@ function DataTableInner(props, ref) {
6013
6020
  onFilterChange,
6014
6021
  filterBy,
6015
6022
  searchPlaceholder,
6023
+ searchable,
6016
6024
  query,
6017
6025
  onQueryChange,
6018
6026
  searchBy,
@@ -6074,6 +6082,7 @@ function DataTableInner(props, ref) {
6074
6082
  onFilterChange: handleFilter,
6075
6083
  filterBy,
6076
6084
  searchPlaceholder,
6085
+ searchable,
6077
6086
  query: activeQuery,
6078
6087
  onQueryChange: handleQuery,
6079
6088
  searchBy,
package/dist/index.js CHANGED
@@ -4895,6 +4895,7 @@ function DataTableDesktopInner(props, ref) {
4895
4895
  onFilterChange,
4896
4896
  filterBy,
4897
4897
  searchPlaceholder,
4898
+ searchable = true,
4898
4899
  query,
4899
4900
  onQueryChange,
4900
4901
  searchBy,
@@ -5058,6 +5059,7 @@ function DataTableDesktopInner(props, ref) {
5058
5059
  const headCell = HEAD_CELL;
5059
5060
  const bodyCell = dense ? "px-sm py-sm align-middle text-body-sm" : "px-sm py-md align-middle";
5060
5061
  const rangeTriggerLabel = activeRange?.detail ?? activeRange?.label ?? "";
5062
+ const showToolRow = searchable || activeFilters.length > 0 || Boolean(activeRange) || Boolean(toolbar);
5061
5063
  return /* @__PURE__ */ jsxs26(
5062
5064
  "div",
5063
5065
  {
@@ -5073,8 +5075,8 @@ function DataTableDesktopInner(props, ref) {
5073
5075
  title ? /* @__PURE__ */ jsx29("p", { className: "text-body-lg text-ink", children: title }) : null,
5074
5076
  description ? /* @__PURE__ */ jsx29("p", { className: "text-body-sm text-muted", children: description }) : null
5075
5077
  ] }) : null,
5076
- /* @__PURE__ */ jsxs26("div", { className: "flex flex-wrap items-center gap-sm", children: [
5077
- /* @__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(
5078
5080
  Input,
5079
5081
  {
5080
5082
  size: "md",
@@ -5085,7 +5087,7 @@ function DataTableDesktopInner(props, ref) {
5085
5087
  placeholder: searchPlaceholder ?? "Search",
5086
5088
  leading: /* @__PURE__ */ jsx29(Icon, { name: "search", size: 16, strokeWidth: 2, tone: "default" })
5087
5089
  }
5088
- ) }),
5090
+ ) }) : null,
5089
5091
  activeFilters.length > 0 ? /* @__PURE__ */ jsx29("div", { className: "flex flex-wrap items-center gap-xs", children: activeFilters.map((item) => /* @__PURE__ */ jsxs26(
5090
5092
  Chip,
5091
5093
  {
@@ -5141,7 +5143,7 @@ function DataTableDesktopInner(props, ref) {
5141
5143
  }
5142
5144
  ) }) : null,
5143
5145
  toolbar ? /* @__PURE__ */ jsx29("div", { className: "ml-auto flex shrink-0 items-center gap-xs", children: toolbar }) : null
5144
- ] })
5146
+ ] }) : null
5145
5147
  ] }),
5146
5148
  activeRange && typeof document !== "undefined" ? createPortal5(
5147
5149
  /* @__PURE__ */ jsx29(
@@ -5567,6 +5569,7 @@ function DataTableMobileInner(props, ref) {
5567
5569
  onFilterChange,
5568
5570
  filterBy,
5569
5571
  searchPlaceholder,
5572
+ searchable = true,
5570
5573
  query,
5571
5574
  onQueryChange,
5572
5575
  searchBy,
@@ -5653,6 +5656,10 @@ function DataTableMobileInner(props, ref) {
5653
5656
  const subtitleText = activeSpec?.subtitle;
5654
5657
  const trailingText = activeSpec?.trailing;
5655
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
+ );
5656
5663
  return /* @__PURE__ */ jsxs27(
5657
5664
  "div",
5658
5665
  {
@@ -5663,12 +5670,12 @@ function DataTableMobileInner(props, ref) {
5663
5670
  ),
5664
5671
  ...rest,
5665
5672
  children: [
5666
- /* @__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: [
5667
5674
  title || description ? /* @__PURE__ */ jsxs27("div", { className: "flex flex-col gap-xs", children: [
5668
5675
  title ? /* @__PURE__ */ jsx30("p", { className: "text-body-lg text-ink", children: title }) : null,
5669
5676
  description ? /* @__PURE__ */ jsx30("p", { className: "text-body-sm text-muted", children: description }) : null
5670
5677
  ] }) : null,
5671
- /* @__PURE__ */ jsx30(
5678
+ searchable ? /* @__PURE__ */ jsx30(
5672
5679
  "div",
5673
5680
  {
5674
5681
  className: cn24(
@@ -5693,7 +5700,7 @@ function DataTableMobileInner(props, ref) {
5693
5700
  }
5694
5701
  )
5695
5702
  }
5696
- ),
5703
+ ) : null,
5697
5704
  activeFilters.length > 0 ? /* @__PURE__ */ jsx30("div", { className: "flex flex-wrap items-center gap-xs", children: activeFilters.map((item) => /* @__PURE__ */ jsxs27(
5698
5705
  Chip,
5699
5706
  {
@@ -5708,7 +5715,7 @@ function DataTableMobileInner(props, ref) {
5708
5715
  },
5709
5716
  item.value
5710
5717
  )) }) : null,
5711
- 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(
5712
5719
  FilterPill,
5713
5720
  {
5714
5721
  menuLabel: "Date range",
@@ -5721,7 +5728,7 @@ function DataTableMobileInner(props, ref) {
5721
5728
  }
5722
5729
  ) }) : null,
5723
5730
  toolbar ? /* @__PURE__ */ jsx30("div", { className: "flex flex-wrap items-center gap-xs", children: toolbar }) : null
5724
- ] }),
5731
+ ] }) : null,
5725
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: [
5726
5733
  /* @__PURE__ */ jsxs27("span", { className: "text-body-sm text-ink", children: [
5727
5734
  selected.length,
@@ -5876,6 +5883,7 @@ function DataTableInner(props, ref) {
5876
5883
  onFilterChange,
5877
5884
  filterBy,
5878
5885
  searchPlaceholder,
5886
+ searchable,
5879
5887
  query,
5880
5888
  onQueryChange,
5881
5889
  searchBy,
@@ -5937,6 +5945,7 @@ function DataTableInner(props, ref) {
5937
5945
  onFilterChange: handleFilter,
5938
5946
  filterBy,
5939
5947
  searchPlaceholder,
5948
+ searchable,
5940
5949
  query: activeQuery,
5941
5950
  onQueryChange: handleQuery,
5942
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.3",
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">