@designa8/vibedesign-stripe-deepseek-85ea 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +927 -861
- package/dist/index.js +929 -863
- package/dist/styles.css +10 -0
- package/package.json +6 -1
- package/src/atoms/DataTable/DataTable.tsx +8 -0
- package/src/atoms/DataTable/preview.tsx +43 -0
- package/src/atoms/DataTableDesktop/DataTableDesktop.tsx +95 -80
- package/src/atoms/DataTableMobile/DataTableMobile.tsx +90 -70
- package/src/atoms/Icon/icons.data.ts +1 -0
- package/src/atoms/Panel/Panel.tsx +102 -0
- package/src/atoms/Panel/index.ts +2 -0
- package/src/atoms/Panel/preview.tsx +196 -0
- package/src/index.ts +1 -0
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));
|
|
@@ -3799,6 +3804,11 @@
|
|
|
3799
3804
|
width: calc(var(--spacing) * 64);
|
|
3800
3805
|
}
|
|
3801
3806
|
}
|
|
3807
|
+
.lg\:grid-cols-2 {
|
|
3808
|
+
@media (width >= 1024px) {
|
|
3809
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3802
3812
|
.lg\:grid-cols-4 {
|
|
3803
3813
|
@media (width >= 1024px) {
|
|
3804
3814
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@designa8/vibedesign-stripe-deepseek-85ea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -324,6 +324,11 @@
|
|
|
324
324
|
"kind": "Molecule",
|
|
325
325
|
"description": null
|
|
326
326
|
},
|
|
327
|
+
{
|
|
328
|
+
"name": "Panel",
|
|
329
|
+
"kind": "Molecule",
|
|
330
|
+
"description": null
|
|
331
|
+
},
|
|
327
332
|
{
|
|
328
333
|
"name": "PaymentRoutingCanvas",
|
|
329
334
|
"kind": "Organism",
|
|
@@ -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
|
-
|
|
530
|
-
<div className="
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
{
|
|
555
|
-
{
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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">
|
|
@@ -94,6 +94,7 @@ export const ICON_REGISTRY = {
|
|
|
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
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" }] },
|
|
97
|
+
"chart-column": { viewBox: "0 0 24 24", nodes: [{ tag: "path", d: "M3 3v16a2 2 0 0 0 2 2h16" }, { tag: "path", d: "M18 17V9" }, { tag: "path", d: "M13 17V5" }, { tag: "path", d: "M8 17v-3" }] },
|
|
97
98
|
} as const;
|
|
98
99
|
|
|
99
100
|
export type IconName = keyof typeof ICON_REGISTRY;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
const cn = (...values: Array<string | false | null | undefined>) =>
|
|
4
|
+
values.filter(Boolean).join(" ");
|
|
5
|
+
|
|
6
|
+
export interface PanelProps
|
|
7
|
+
extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
|
|
8
|
+
/** The panel's heading. Rendered as an `<h2>` by default. */
|
|
9
|
+
title: React.ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* Right-aligned caption in the header — a quiet count or timestamp such as
|
|
12
|
+
* "3 accounts" or "Updated 9:41". Never emphasised, never a badge.
|
|
13
|
+
*/
|
|
14
|
+
meta?: React.ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Controls pinned to the right of the header, after the meta caption — a
|
|
17
|
+
* segmented range toggle, an icon button, a link. The panel adds no chrome
|
|
18
|
+
* around them.
|
|
19
|
+
*/
|
|
20
|
+
actions?: React.ReactNode;
|
|
21
|
+
/**
|
|
22
|
+
* Heading rank, so the panel nests correctly under the page's `<h1>`.
|
|
23
|
+
* `2` renders `<h2>` (default), `3` renders `<h3>`.
|
|
24
|
+
*/
|
|
25
|
+
headingLevel?: 2 | 3;
|
|
26
|
+
/** Panel body. Laid out as a column with a `md` gap between children. */
|
|
27
|
+
children?: React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A titled section surface: one calm card that carries a heading, an optional
|
|
32
|
+
* right-aligned caption, an optional actions slot, and a body the caller fills
|
|
33
|
+
* with rows, charts, or tasks.
|
|
34
|
+
*
|
|
35
|
+
* The surface owns the only stroke (one hairline), the radius, and the soft card
|
|
36
|
+
* shadow, and it clips whatever the caller drops inside so a full-bleed child
|
|
37
|
+
* cannot poke through the corners. It is a container, not a control: no hover
|
|
38
|
+
* lift, no press scale, no focus ring, no cursor change — every interactive
|
|
39
|
+
* affordance belongs to a child that can actually be pressed.
|
|
40
|
+
*
|
|
41
|
+
* Every colour, radius, spacing step, and type role is a token utility, so the
|
|
42
|
+
* whole surface repaints under `[data-theme="dark"]`.
|
|
43
|
+
*/
|
|
44
|
+
export const Panel = React.forwardRef<HTMLElement, PanelProps>(
|
|
45
|
+
(
|
|
46
|
+
{
|
|
47
|
+
title,
|
|
48
|
+
meta,
|
|
49
|
+
actions,
|
|
50
|
+
headingLevel = 2,
|
|
51
|
+
id,
|
|
52
|
+
children,
|
|
53
|
+
className,
|
|
54
|
+
...rest
|
|
55
|
+
},
|
|
56
|
+
ref,
|
|
57
|
+
) => {
|
|
58
|
+
const generatedId = React.useId();
|
|
59
|
+
const baseId = id ?? generatedId;
|
|
60
|
+
const titleId = `${baseId}-title`;
|
|
61
|
+
const Heading = (headingLevel === 3 ? "h3" : "h2") as "h2" | "h3";
|
|
62
|
+
const hasTrailing = Boolean(meta) || Boolean(actions);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<section
|
|
66
|
+
ref={ref}
|
|
67
|
+
id={id}
|
|
68
|
+
aria-labelledby={titleId}
|
|
69
|
+
className={cn(
|
|
70
|
+
"w-full min-w-0 overflow-hidden rounded-xl border border-hairline",
|
|
71
|
+
"bg-surface p-xl shadow-card",
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
{...rest}
|
|
75
|
+
>
|
|
76
|
+
<div className="mb-lg flex min-w-0 flex-wrap items-center justify-between gap-x-md gap-y-sm">
|
|
77
|
+
<Heading
|
|
78
|
+
id={titleId}
|
|
79
|
+
className="min-w-0 truncate text-heading-sm text-ink"
|
|
80
|
+
>
|
|
81
|
+
{title}
|
|
82
|
+
</Heading>
|
|
83
|
+
|
|
84
|
+
{hasTrailing ? (
|
|
85
|
+
<div className="ml-auto flex shrink-0 items-center gap-md">
|
|
86
|
+
{meta ? (
|
|
87
|
+
<span className="whitespace-nowrap text-caption text-muted">
|
|
88
|
+
{meta}
|
|
89
|
+
</span>
|
|
90
|
+
) : null}
|
|
91
|
+
{actions}
|
|
92
|
+
</div>
|
|
93
|
+
) : null}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className="flex min-w-0 flex-col gap-md">{children}</div>
|
|
97
|
+
</section>
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
Panel.displayName = "Panel";
|