@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/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.2.3",
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
- <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">
@@ -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";
@@ -0,0 +1,2 @@
1
+ export { Panel } from "./Panel";
2
+ export type { PanelProps } from "./Panel";