This commit is contained in:
Armaan Jain
2026-07-19 02:08:18 -07:00
committed by GitHub
6 changed files with 856 additions and 646 deletions
+8
View File
@@ -520,14 +520,22 @@ Use for:
Props:
- `items` (typed union: `text`, `boolean`, `dropdown`, `custom-dropdown`, `custom-button`, `custom`)
- shared item metadata: `itemKey`, `icon`, `name`, optional `description` and `tooltip`
- `columns`: `1 | 2`
- `size`: `"sm" | "md"`
- `deferredSave`, `hasChanges`, `onSave`, `onDiscard`
- `externalModifiedKeys`
Rules:
- prefer this for config forms that are row-based and editable inline
- layout is a flat label/value grid (no per-field cards); editable values use white controls in light mode
- use deferred save mode when many fields should be committed together
- text values enter an explicit edit state; Enter saves and Escape cancels
- omit `onUpdate` or set `readOnly` for a non-editable value
- use `description` for persistent context and `tooltip` only for secondary details
- `custom-dropdown` owns its popover; provide both `triggerContent` and `popoverContent`
- for page-local editable triggers outside the typed union, reuse `designEditableGridControlClassName` / `designEditableGridPopoverClassName`
### 4.12 `DataGrid` + `useDataSource` + `createDefaultDataGridState`
@@ -536,6 +536,7 @@ export default function PageClient() {
type: "text",
icon: <FileText className="h-4 w-4" />,
name: "Display Name",
description: "The customer-facing name for this product.",
value: "Widget Pro",
readOnly: false,
onUpdate: async () => {
@@ -548,6 +549,7 @@ export default function PageClient() {
type: "boolean",
icon: <StackSimple className="h-4 w-4" />,
name: "Active",
description: "Controls whether this product can be purchased.",
value: true,
readOnly: false,
trueLabel: "Yes",
@@ -562,6 +564,7 @@ export default function PageClient() {
type: "dropdown",
icon: <Sliders className="h-4 w-4" />,
name: "Category",
description: "Used to organize this product in the catalog.",
value: "hardware",
options: [
{ value: "hardware", label: "Hardware" },
@@ -579,6 +582,7 @@ export default function PageClient() {
type: "custom",
icon: <Tag className="h-4 w-4" />,
name: "Price",
description: "Current base price before discounts.",
children: <span className="text-sm text-foreground">$29.99</span>,
},
];
@@ -591,6 +595,7 @@ export default function PageClient() {
type: "custom-dropdown",
icon: <Sparkle className="h-4 w-4" />,
name: "Custom Dropdown",
description: "A composed control with custom popover content.",
triggerContent: <span>Open custom panel</span>,
popoverContent: <div>Custom content</div>,
disabled: false,
@@ -600,6 +605,7 @@ export default function PageClient() {
type: "custom-button",
icon: <Cube className="h-4 w-4" />,
name: "Custom Button",
description: "Runs a custom action with built-in loading behavior.",
onClick: () => setGridActionLog("Clicked custom button"),
children: <span>Run action</span>,
disabled: false,
@@ -1082,28 +1088,28 @@ export default function PageClient() {
if (selected === "editable-grid") {
return (
<div className="w-full max-w-3xl">
<div className="rounded-2xl overflow-hidden bg-white/90 dark:bg-[hsl(240,10%,5.5%)] border border-black/[0.12] dark:border-foreground/[0.12] shadow-sm">
<div className="overflow-hidden rounded-2xl border border-black/[0.1] bg-white shadow-sm dark:border-white/[0.1] dark:bg-zinc-950">
<div className="p-4 sm:p-5">
<DesignEditableGrid
items={editableItems}
columns={gridCols}
size={gridSize}
editMode={gridEditMode}
deferredSave={gridDeferredSave}
editMode={gridEditMode}
externalModifiedKeys={gridShowModified ? new Set(["display-name", "category"]) : undefined}
hasChanges={gridHasChanges}
onSave={gridDeferredSave ? async () => {
await new Promise((r) => setTimeout(r, 400));
setGridActionLog("Saved deferred changes");
setGridHasChanges(false);
} : undefined}
items={editableItems}
onDiscard={gridDeferredSave ? () => {
setGridActionLog("Discarded deferred changes");
setGridHasChanges(false);
} : undefined}
externalModifiedKeys={gridShowModified ? new Set(["display-name", "category"]) : undefined}
onSave={gridDeferredSave ? async () => {
await new Promise((resolve) => setTimeout(resolve, 400));
setGridActionLog("Saved deferred changes");
setGridHasChanges(false);
} : undefined}
size={gridSize}
/>
{gridActionLog && (
<Typography variant="secondary" className="text-xs mt-2">
<Typography className="mt-3 text-xs" variant="secondary">
Last action: {gridActionLog}
</Typography>
)}
@@ -1,7 +1,15 @@
"use client";
import { DesignEditableGrid, type DesignEditableGridItem } from "@/components/design-components";
import { EditableInput } from "@/components/editable-input";
import {
DesignBadge,
DesignEditableGrid,
DesignButton,
DesignInput,
DesignMenu,
DesignSelectorDropdown,
type DesignBadgeColor,
type DesignEditableGridItem,
} from "@/components/design-components";
import { Link, StyledLink } from "@/components/link";
import { ItemDialog } from "@/components/payments/item-dialog";
import { useRouter } from "@/components/router";
@@ -20,15 +28,8 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Input,
Label,
Popover,
PopoverContent,
PopoverTrigger,
Select,
SelectContent,
SelectItem,
@@ -42,7 +43,8 @@ import {
} from "@/components/ui";
import { createDefaultDataGridState, DataGrid, useDataGridUrlState, useDataSource, type DataGridColumnDef } from "@hexclave/dashboard-ui-components";
import { useUpdateConfig } from "@/components/config-update";
import { ArrowLeftIcon, ClockIcon, CopyIcon, CurrencyDollarIcon, DotsThreeIcon, FolderOpenIcon, GiftIcon, HardDriveIcon, PackageIcon, PencilSimpleIcon, PlusIcon, PuzzlePieceIcon, ShoppingCartIcon, StackIcon, TagIcon, TrashIcon, UsersIcon } from "@phosphor-icons/react";
import { ArrowLeftIcon, Check, ClockIcon, CopyIcon, CurrencyDollarIcon, FolderOpenIcon, GiftIcon, HardDriveIcon, PackageIcon, PencilSimpleIcon, PlusIcon, PuzzlePieceIcon, ShoppingCartIcon, StackIcon, TagIcon, TrashIcon, UsersIcon, X } from "@phosphor-icons/react";
import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
import { CreateCheckoutDialog } from "@/components/payments/create-checkout-dialog";
import type { CustomerType } from "@/components/payments/customer-selector";
import type { CompleteConfig } from "@hexclave/shared/dist/config/schema";
@@ -51,8 +53,7 @@ import type { DayInterval } from "@hexclave/shared/dist/utils/dates";
import { fromNow } from "@hexclave/shared/dist/utils/dates";
import { prettyPrintWithMagnitudes } from "@hexclave/shared/dist/utils/numbers";
import { typedEntries } from "@hexclave/shared/dist/utils/objects";
import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
import { Suspense, useMemo, useState } from "react";
import { Suspense, useMemo, useRef, useState } from "react";
import { PageLayout } from "../../../page-layout";
import { useAdminApp, useProjectId } from "../../../use-admin-app";
import { CreateProductLineDialog } from "../create-product-line-dialog";
@@ -71,6 +72,16 @@ const CUSTOMER_TYPE_COLORS = {
custom: 'bg-amber-500/15 text-amber-600 dark:bg-amber-500/20 dark:text-amber-400 ring-amber-500/30',
} as const;
const CUSTOMER_TYPE_BADGE_COLORS = new Map<string, DesignBadgeColor>([
["user", "blue"],
["team", "green"],
["custom", "orange"],
]);
function getCustomerTypeBadgeColor(customerType: string): DesignBadgeColor {
return CUSTOMER_TYPE_BADGE_COLORS.get(customerType) ?? "blue";
}
export default function PageClient({ productId }: { productId: string }) {
const adminApp = useAdminApp();
const project = adminApp.useProject();
@@ -113,15 +124,15 @@ function ProductPage({ productId, product, config }: ProductPageProps) {
<PageLayout>
<div className="flex flex-col gap-6">
{canGoBack && (
<Button
<DesignButton
variant="ghost"
size="sm"
className="w-fit -ml-2 text-muted-foreground hover:text-foreground"
className="w-fit -ml-2 h-8 gap-1.5 rounded-lg px-2 text-muted-foreground hover:text-foreground"
onClick={() => router.back()}
>
<ArrowLeftIcon className="h-4 w-4 mr-1" />
<ArrowLeftIcon className="h-4 w-4" />
Back
</Button>
</DesignButton>
)}
<ProductHeader productId={productId} product={product} productLineName={productLineName} />
<Separator />
@@ -141,119 +152,248 @@ type ProductHeaderProps = {
productLineName: string | null,
};
function ProductTitleEditor({
productId,
displayName,
storedDisplayName,
}: {
productId: string,
displayName: string,
storedDisplayName: string,
}) {
const adminApp = useAdminApp();
const updateConfig = useUpdateConfig();
const [editing, setEditing] = useState(false);
const [draftValue, setDraftValue] = useState(storedDisplayName);
const [isSaving, setIsSaving] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const beginEditing = () => {
setDraftValue(storedDisplayName);
setEditing(true);
};
const cancelEditing = () => {
setDraftValue(storedDisplayName);
setEditing(false);
};
const save = async () => {
setIsSaving(true);
try {
const success = await updateConfig({
adminApp,
configUpdate: {
[`payments.products.${productId}.displayName`]: draftValue || null,
},
pushable: true,
});
if (success) {
toast({ title: "Product name updated" });
setEditing(false);
}
} finally {
setIsSaving(false);
}
};
return (
<div
role={editing ? undefined : "button"}
tabIndex={editing ? undefined : 0}
aria-label={editing ? undefined : "Edit display name"}
className={cn(
"group flex h-9 min-w-0 flex-1 items-center gap-1.5 rounded-lg border px-2.5",
"border-black/[0.1] bg-white cursor-text",
"hover:border-black/[0.16] hover:bg-zinc-50",
"dark:border-white/[0.1] dark:bg-zinc-950 dark:hover:border-white/[0.18] dark:hover:bg-zinc-900",
"transition-colors duration-150 hover:transition-none",
editing && "border-black/[0.28] dark:border-white/[0.32]",
)}
onClick={() => {
if (!editing) {
beginEditing();
} else {
inputRef.current?.focus();
}
}}
onKeyDown={(event) => {
if (editing) {
return;
}
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
beginEditing();
}
}}
>
{editing ? (
<input
ref={inputRef}
autoFocus
aria-label="Display name"
autoComplete="off"
disabled={isSaving}
value={draftValue}
placeholder={productId}
onClick={(event) => event.stopPropagation()}
onChange={(event) => setDraftValue(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.nativeEvent.isComposing) {
event.preventDefault();
if (draftValue !== storedDisplayName && !isSaving) {
runAsynchronouslyWithAlert(save());
}
}
if (event.key === "Escape") {
cancelEditing();
}
}}
className="min-w-0 flex-1 bg-transparent text-lg font-semibold leading-none tracking-tight text-foreground outline-none placeholder:text-muted-foreground/60 disabled:opacity-50"
/>
) : (
<h1 className="min-w-0 flex-1 truncate text-lg font-semibold leading-none tracking-tight text-foreground">
{displayName}
</h1>
)}
{editing ? (
<div className="flex shrink-0 items-center gap-0.5" onClick={(event) => event.stopPropagation()}>
<DesignButton
aria-label="Save display name"
className="h-6 w-6 rounded-md p-0 text-emerald-600 hover:bg-emerald-500/10 hover:text-emerald-700 dark:text-emerald-400"
disabled={draftValue === storedDisplayName || isSaving}
loading={isSaving}
onClick={save}
size="icon"
variant="ghost"
>
<Check aria-hidden className="h-3.5 w-3.5" weight="bold" />
</DesignButton>
<DesignButton
aria-label="Cancel editing display name"
className="h-6 w-6 rounded-md p-0 text-red-600 hover:bg-red-500/10 hover:text-red-700 dark:text-red-400"
disabled={isSaving}
onClick={cancelEditing}
size="icon"
variant="ghost"
>
<X aria-hidden className="h-3.5 w-3.5" weight="bold" />
</DesignButton>
</div>
) : (
<PencilSimpleIcon
aria-hidden
className="h-3.5 w-3.5 shrink-0 text-muted-foreground transition-colors duration-150 group-hover:text-foreground group-hover:transition-none"
/>
)}
</div>
);
}
function ProductHeader({ productId, product, productLineName }: ProductHeaderProps) {
const projectId = useProjectId();
const adminApp = useAdminApp();
const project = adminApp.useProject();
const config = project.useConfig();
const updateConfig = useUpdateConfig();
const router = useRouter();
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
const displayName = product.displayName || productId;
const isAddOn = product.isAddOnTo !== false && typeof product.isAddOnTo === 'object';
const isAddOn = product.isAddOnTo !== false && typeof product.isAddOnTo === "object";
// Find add-on parent products
const addOnParents = useMemo(() => {
if (product.isAddOnTo === false || typeof product.isAddOnTo !== 'object') return [];
return Object.keys(product.isAddOnTo).map((parentId: string) => ({
if (product.isAddOnTo === false || typeof product.isAddOnTo !== "object") {
return [];
}
return Object.keys(product.isAddOnTo).map((parentId) => ({
id: parentId,
displayName: config.payments.products[parentId].displayName || parentId,
}));
}, [product.isAddOnTo, config.payments.products]);
return (
<div className="flex gap-4 items-start">
<div className={cn(
"flex h-16 w-16 items-center justify-center rounded-xl shrink-0",
"bg-gradient-to-br from-primary/20 to-primary/5",
"border border-primary/20"
)}>
<div className="flex items-start gap-4">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-xl border border-black/[0.1] bg-white text-muted-foreground dark:border-white/[0.1] dark:bg-zinc-950">
{isAddOn ? (
<PuzzlePieceIcon className="h-8 w-8 text-primary" />
<PuzzlePieceIcon className="h-7 w-7" />
) : (
<PackageIcon className="h-8 w-8 text-primary" />
<PackageIcon className="h-7 w-7" />
)}
</div>
<div className="flex-grow min-w-0 flex flex-col">
<div className="flex min-w-0 flex-1 flex-col gap-2">
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0">
<EditableInput
value={displayName}
initialEditValue={product.displayName || ""}
placeholder={productId}
shiftTextToLeft
inputClassName="text-2xl font-semibold tracking-tight w-full"
onUpdate={async (newName) => {
const success = await updateConfig({ adminApp, configUpdate: {
[`payments.products.${productId}.displayName`]: newName || null,
}, pushable: true });
if (success) {
toast({ title: "Product name updated" });
}
}}
<ProductTitleEditor
productId={productId}
displayName={displayName}
storedDisplayName={product.displayName || ""}
/>
<div className="flex shrink-0 items-center">
<CreateCheckoutDialog
open={isCheckoutOpen}
onOpenChange={setIsCheckoutOpen}
customerType={product.customerType as CustomerType}
productId={productId}
lockProduct
/>
<DesignMenu
variant="actions"
trigger="icon"
triggerLabel="Product actions"
align="end"
withIcons
items={[
{
id: "checkout",
label: "Create checkout",
icon: <ShoppingCartIcon className="h-4 w-4" />,
onClick: () => setIsCheckoutOpen(true),
},
{
id: "product-lines",
label: "View in Product Lines",
icon: <FolderOpenIcon className="h-4 w-4" />,
onClick: () => router.push(`/projects/${projectId}/payments/product-lines#product-${productId}`),
},
{
id: "edit",
label: "Edit full details",
icon: <PencilSimpleIcon className="h-4 w-4" />,
onClick: () => router.push(`/projects/${projectId}/payments/products/${productId}/edit`),
},
]}
/>
</div>
<Button
variant="outline"
size="sm"
className="shrink-0 gap-1.5"
onClick={() => router.push(`/projects/${projectId}/payments/products/${productId}/edit`)}
>
<PencilSimpleIcon className="h-4 w-4" />
Edit
</Button>
<CreateCheckoutDialog
open={isCheckoutOpen}
onOpenChange={setIsCheckoutOpen}
customerType={product.customerType as CustomerType}
productId={productId}
lockProduct
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="shrink-0">
<DotsThreeIcon className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem icon={<ShoppingCartIcon className="h-4 w-4" />} onClick={() => setIsCheckoutOpen(true)}>
Create checkout
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push(`/projects/${projectId}/payments/product-lines#product-${productId}`)}>
View in Product Lines
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center gap-2 mt-1 text-sm text-muted-foreground flex-wrap">
<span className={cn(
"inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold uppercase tracking-wide ring-1",
CUSTOMER_TYPE_COLORS[product.customerType as keyof typeof CUSTOMER_TYPE_COLORS]
)}>
{product.customerType}
<div className="flex flex-wrap items-center gap-2">
<DesignBadge
label={product.customerType.toUpperCase()}
color={getCustomerTypeBadgeColor(product.customerType)}
size="sm"
contentMode="text"
/>
<span className="inline-flex items-center rounded-md border border-black/[0.08] bg-white px-2 py-0.5 font-mono text-[11px] text-muted-foreground dark:border-white/[0.1] dark:bg-zinc-950">
ID: {productId}
</span>
<span className="font-mono text-xs bg-muted px-2 py-0.5 rounded">ID: {productId}</span>
{productLineName && (
<>
<span></span>
<span>Product Line: {productLineName}</span>
</>
{productLineName != null && (
<span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
<span className="text-muted-foreground/40">·</span>
Product Line: <span className="font-medium text-foreground">{productLineName}</span>
</span>
)}
{addOnParents.length > 0 && (
<>
<span></span>
<span className="flex items-center gap-1">
Add-on to{' '}
{addOnParents.map((p: { id: string, displayName: string }, i: number) => (
<span key={p.id}>
{i > 0 && ", "}
<StyledLink href={`/projects/${adminApp.projectId}/payments/products/${p.id}`}>
{p.displayName}
</StyledLink>
</span>
))}
</span>
</>
<span className="inline-flex flex-wrap items-center gap-1 text-xs text-muted-foreground">
<span className="text-muted-foreground/40">·</span>
Add-on to{" "}
{addOnParents.map((parent, index) => (
<span key={parent.id} className="inline-flex items-center">
{index > 0 && <span className="mr-1 text-muted-foreground/50">,</span>}
<StyledLink href={`/projects/${adminApp.projectId}/payments/products/${parent.id}`}>
{parent.displayName}
</StyledLink>
</span>
))}
</span>
)}
</div>
</div>
@@ -558,6 +698,14 @@ function ProductDetailsSection({ productId, product, config }: ProductDetailsSec
setFreeTrialPopoverOpen(false);
};
const handleFreeTrialPopoverOpenChange = (open: boolean) => {
setFreeTrialPopoverOpen(open);
if (open) {
setFreeTrialCount(localFreeTrial ? localFreeTrial[0] : 7);
setFreeTrialUnit(localFreeTrial ? localFreeTrial[1] : 'day');
}
};
// ===== PRICES HANDLERS (for deferred save) =====
const handlePricesChange = (newPrices: Product['prices']) => {
// Clear the "needs at least one price" error as soon as the user adds one back.
@@ -647,69 +795,54 @@ function ProductDetailsSection({ productId, product, config }: ProductDetailsSec
) : 'No',
},
{
type: 'custom',
type: 'custom-dropdown',
itemKey: 'freeTrial',
icon: <ClockIcon size={16} />,
name: "Free Trial",
tooltip: "Free trial period before billing starts. Customers won't be charged during this period.",
children: (
<Popover open={freeTrialPopoverOpen} onOpenChange={setFreeTrialPopoverOpen}>
<PopoverTrigger asChild>
<button
className={cn(
"w-full px-1 py-0 h-[unset] border-transparent rounded text-left text-foreground",
"hover:ring-1 hover:ring-slate-300 dark:hover:ring-gray-500 hover:bg-slate-50 dark:hover:bg-gray-800 hover:cursor-pointer",
"focus:outline-none focus-visible:ring-1 focus-visible:ring-slate-500 dark:focus-visible:ring-gray-50",
"transition-colors duration-150 hover:transition-none"
)}
open: freeTrialPopoverOpen,
onOpenChange: handleFreeTrialPopoverOpenChange,
triggerContent: localFreeTrialDisplayText,
popoverContent: (
<div className="space-y-3">
<div className="flex items-center gap-2">
<DesignInput
className="w-20 bg-white dark:bg-zinc-950"
type="number"
min={1}
value={freeTrialCount}
onChange={(e) => setFreeTrialCount(Math.max(1, parseInt(e.target.value) || 1))}
/>
<DesignSelectorDropdown
className="w-28"
triggerClassName="bg-white dark:bg-zinc-950 border-black/[0.1] dark:border-white/[0.1]"
value={freeTrialUnit}
onValueChange={(v) => setFreeTrialUnit(v as DayInterval[1])}
options={DEFAULT_INTERVAL_UNITS.map((unit) => ({
value: unit,
label: `${unit}${freeTrialCount !== 1 ? 's' : ''}`,
}))}
/>
</div>
<div className="flex gap-2">
<DesignButton
size="sm"
className="flex-1"
onClick={() => handleFreeTrialSave(freeTrialCount, freeTrialUnit)}
>
{localFreeTrialDisplayText}
</button>
</PopoverTrigger>
<PopoverContent className="w-64 p-3">
<div className="space-y-3">
<div className="flex items-center gap-2">
<Input
className="w-20"
type="number"
min={1}
value={freeTrialCount}
onChange={(e) => setFreeTrialCount(parseInt(e.target.value) || 1)}
/>
<Select value={freeTrialUnit} onValueChange={(v) => setFreeTrialUnit(v as DayInterval[1])}>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
{DEFAULT_INTERVAL_UNITS.map((unit) => (
<SelectItem key={unit} value={unit}>
{unit}{freeTrialCount !== 1 ? 's' : ''}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex gap-2">
<Button
size="sm"
className="flex-1"
onClick={() => handleFreeTrialSave(freeTrialCount, freeTrialUnit)}
>
Apply
</Button>
{localFreeTrial && (
<Button
size="sm"
variant="outline"
onClick={handleRemoveFreeTrial}
>
Remove
</Button>
)}
</div>
</div>
</PopoverContent>
</Popover>
Apply
</DesignButton>
{localFreeTrial && (
<DesignButton
size="sm"
variant="outline"
onClick={handleRemoveFreeTrial}
>
Remove
</DesignButton>
)}
</div>
</div>
),
},
{
@@ -614,7 +614,7 @@ function SidebarContent({
</div>
<div className={cn(
"sticky bottom-0 border-t border-black/[0.06] dark:border-foreground/10 py-3 transition-all duration-200 dark:backdrop-blur-xl",
"sticky bottom-0 border-t border-black/[0.06] dark:border-foreground/10 py-3 transition-all duration-200 bg-black/[0.03] dark:bg-foreground/[0.06] dark:backdrop-blur-xl",
!isDrawer && "dark:rounded-b-2xl",
isCollapsed ? "px-2" : "px-3",
)}>
File diff suppressed because it is too large Load Diff
@@ -14,7 +14,17 @@ export type {
} from "../../../../../packages/dashboard-ui-components/src/components/dialog";
export * from "./analytics-card";
export * from "./design-tokens";
export * from "./editable-grid";
export {
DesignEditableGrid,
designEditableGridControlClassName,
designEditableGridPopoverClassName,
} from "./editable-grid";
export type {
DesignEditableGridDropdownOption,
DesignEditableGridItem,
DesignEditableGridProps,
DesignEditableGridSize,
} from "./editable-grid";
export * from "./list";
export * from "./menu";
export * from "./select";