@colineapp/ui 0.4.0 → 0.4.1

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/AGENTS.md CHANGED
@@ -100,3 +100,15 @@ Keep state/conflict/sync logic in the SDK; React owns only subscriptions and
100
100
  lifecycle. Inline serializable query objects must not restart the view on each
101
101
  render. React Strict Mode cleanup/start must remain supported. Never infer record
102
102
  permission from optimistic state or broadcast private fields to an install room.
103
+
104
+ ## UI 0.4.1 defaults
105
+
106
+ Use `Tabs` / `TabsList` / `TabsTrigger` / `TabsContent` for app views. The raised
107
+ sliding selection and content transition are automatic. Do not recreate tab
108
+ buttons or choose the opt-in line variant for standard Coline navigation.
109
+ Use `FilterMenu` for value filters, and `DropdownMenu` for actions. Both use the
110
+ canonical Coline menu surface and highlight. `pnpm check:parity` enforces the
111
+ vendored dropdown implementation; `pnpm sync:platform` refreshes it.
112
+ Keep animation CSS in the package, respect reduced motion, and verify controls
113
+ in a portalled dialog as well as on the page. A form submit button needs explicit
114
+ `type="submit"`. Do not restyle shared controls with app-specific CSS.
package/README.md CHANGED
@@ -111,3 +111,40 @@ routing, drag-and-drop, or persistence. `RecordTable` provides a first-cell butt
111
111
  when `onOpen` is set (don't nest a second control there); secondary columns hide
112
112
  at narrow widths. Split details stack on mobile; the app owns focus management.
113
113
  See the scaffold's `examples/` for complete interactive references.
114
+
115
+ ## Shared controls and motion (0.4.1)
116
+
117
+ `Tabs` uses the same raised selection as Coline and Upstream. Selection springs
118
+ and content transitions are automatic; no indicator or animation wrapper is
119
+ required. Each tabs group owns its indicator so multiple groups never collide.
120
+ `TabsIndicator` remains available for existing custom compositions.
121
+
122
+ ```tsx
123
+ <Tabs defaultValue="inbox">
124
+ <TabsList aria-label="Views">
125
+ <TabsTrigger value="inbox">Inbox</TabsTrigger>
126
+ <TabsTrigger value="roadmap">Roadmap</TabsTrigger>
127
+ </TabsList>
128
+ <TabsContent value="inbox">{/* Inbox */}</TabsContent>
129
+ <TabsContent value="roadmap">{/* Roadmap */}</TabsContent>
130
+ </Tabs>
131
+ <FilterMenu
132
+ aria-label="Filter by status"
133
+ value={status}
134
+ onValueChange={setStatus}
135
+ options={[{ value: "all", label: "All statuses" }, { value: "planned", label: "Planned" }]}
136
+ />
137
+ ```
138
+
139
+ `FilterMenu` composes the exact Coline `DropdownMenu` radio group, surface, and
140
+ sliding highlight. Use `DropdownMenu` directly for action menus. `PageToolbar`
141
+ is borderless. `RecordTable` uses separated, rounded selection/hover rows.
142
+ Dialogs and menus include their animation CSS; the provider respects reduced
143
+ motion, including portals. Buttons default to `type="button"`; use
144
+ `type="submit"` for a form's submit action.
145
+
146
+ Use component defaults rather than copying their CSS into the app. Custom CSS
147
+ variables scoped to `AppPage` do not reach portalled dialogs/menus. Keep decorative
148
+ app variables at `:root` when they must cross portals, and retain host theme tokens.
149
+ App bundles include their dependencies: upgrade, rebuild and push to adopt a UI
150
+ release. Updating npm does not rewrite already-published app bundles.
package/dist/index.d.ts CHANGED
@@ -168,21 +168,21 @@ declare function TableHead({ className, ...props }: React$1.ComponentProps<"th">
168
168
  declare function TableCell({ className, ...props }: React$1.ComponentProps<"td">): React$1.JSX.Element;
169
169
  declare function TableCaption({ className, ...props }: React$1.ComponentProps<"caption">): React$1.JSX.Element;
170
170
 
171
- /** Shared spring for tab pill + trigger layout keep in sync wherever `layout` runs alongside `TabsIndicator`. */
171
+ /** Same raised selection and spring as Coline and Upstream navigation. */
172
172
  declare const TABS_LAYOUT_SPRING: {
173
173
  type: "spring";
174
174
  stiffness: number;
175
175
  damping: number;
176
176
  mass: number;
177
177
  };
178
- declare function Tabs({ className, orientation, ...props }: React$1.ComponentProps<typeof Tabs$1.Root>): React$1.JSX.Element;
178
+ declare function Tabs({ className, orientation, value, defaultValue, onValueChange, ...props }: React$1.ComponentProps<typeof Tabs$1.Root>): React$1.JSX.Element;
179
179
  declare const tabsListVariants: (props?: ({
180
180
  variant?: "default" | "line" | null | undefined;
181
181
  } & class_variance_authority_types.ClassProp) | undefined) => string;
182
182
  declare function TabsList({ className, variant, ...props }: React$1.ComponentProps<typeof Tabs$1.List> & VariantProps<typeof tabsListVariants>): React$1.JSX.Element;
183
- declare function TabsTrigger({ className, ...props }: React$1.ComponentProps<typeof Tabs$1.Trigger>): React$1.JSX.Element;
184
- /** Animated sliding pill indicator for TabsList (Apple-style). */
185
- declare function TabsIndicator({ className, layoutId, }: {
183
+ declare function TabsTrigger({ className, children, value, asChild, ...props }: React$1.ComponentProps<typeof Tabs$1.Trigger>): React$1.JSX.Element;
184
+ /** Optional customization; the raised indicator is included automatically. */
185
+ declare function TabsIndicator({ className, layoutId }: {
186
186
  className?: string;
187
187
  layoutId?: string;
188
188
  }): React$1.JSX.Element;
@@ -296,4 +296,18 @@ declare function SettingsSection({ title, description, children, }: {
296
296
  children: React$1.ReactNode;
297
297
  }): React$1.JSX.Element;
298
298
 
299
- export { AppErrorState, AppLoadingState, AppPage, Badge, Board, BoardCard, BoardColumn, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, ColineAppProvider, type ColineQueryState, DetailPanel, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet, FieldTitle, Input, Label, PageToolbar, Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, Progress, type RecordColumn, RecordTable, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, SettingsSection, Skeleton, Slider, SplitView, StatusBadge, Switch, TABS_LAYOUT_SPRING, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsIndicator, TabsList, TabsTrigger, Textarea, Toggle, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, buttonVariants, cn, tabsListVariants, toggleVariants, useColine, useColineContext, useColineQuery, useColorScheme, useLiveCollection };
299
+ /** A value picker using the same dropdown and sliding highlight as Coline. */
300
+ declare const FilterMenu: React$1.ForwardRefExoticComponent<{
301
+ value: string;
302
+ options: readonly {
303
+ value: string;
304
+ label: string;
305
+ disabled?: boolean;
306
+ }[];
307
+ onValueChange: (value: string) => void;
308
+ "aria-label": string;
309
+ disabled?: boolean;
310
+ className?: string;
311
+ } & React$1.RefAttributes<HTMLButtonElement>>;
312
+
313
+ export { AppErrorState, AppLoadingState, AppPage, Badge, Board, BoardCard, BoardColumn, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, ColineAppProvider, type ColineQueryState, DetailPanel, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet, FieldTitle, FilterMenu, Input, Label, PageToolbar, Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, Progress, type RecordColumn, RecordTable, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, SettingsSection, Skeleton, Slider, SplitView, StatusBadge, Switch, TABS_LAYOUT_SPRING, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsIndicator, TabsList, TabsTrigger, Textarea, Toggle, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, buttonVariants, cn, tabsListVariants, toggleVariants, useColine, useColineContext, useColineQuery, useColorScheme, useLiveCollection };
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ function cn(...inputs) {
8
8
  }
9
9
 
10
10
  // src/provider.tsx
11
+ import { MotionConfig } from "framer-motion";
11
12
  import * as React from "react";
12
13
  import {
13
14
  connectGuestBridge
@@ -67,7 +68,7 @@ function ColineAppProvider({ children }) {
67
68
  if (!runtime) {
68
69
  return /* @__PURE__ */ jsx("p", { role: connectionError ? "alert" : "status", children: connectionError ?? "Connecting to Coline\u2026" });
69
70
  }
70
- return /* @__PURE__ */ jsx(ColineRuntimeContext.Provider, { value: runtime, children });
71
+ return /* @__PURE__ */ jsx(ColineRuntimeContext.Provider, { value: runtime, children: /* @__PURE__ */ jsx(MotionConfig, { reducedMotion: "user", children }) });
71
72
  }
72
73
  function useColine() {
73
74
  const runtime = React.useContext(ColineRuntimeContext);
@@ -165,7 +166,7 @@ import { cva as cva2 } from "class-variance-authority";
165
166
  import { Slot as Slot2 } from "radix-ui";
166
167
  import { jsx as jsx3 } from "react/jsx-runtime";
167
168
  var buttonVariants = cva2(
168
- "group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-white/20 text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color] duration-200 ease-out outline-none select-none focus-visible:border-ring disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:border-white/[0.06] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
169
+ "coline-button group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-white/20 text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform,box-shadow] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] outline-none select-none focus-visible:border-ring disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:border-white/[0.06] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
169
170
  {
170
171
  variants: {
171
172
  variant: {
@@ -198,6 +199,7 @@ var Button = React3.forwardRef(function Button2({
198
199
  variant = "default",
199
200
  size = "default",
200
201
  asChild = false,
202
+ type,
201
203
  ...props
202
204
  }, ref) {
203
205
  const Comp = asChild ? Slot2.Root : "button";
@@ -205,6 +207,7 @@ var Button = React3.forwardRef(function Button2({
205
207
  Comp,
206
208
  {
207
209
  ref,
210
+ ...!asChild ? { type: type ?? "button" } : type ? { type } : {},
208
211
  "data-slot": "button",
209
212
  "data-variant": variant,
210
213
  "data-size": size,
@@ -339,6 +342,11 @@ function Checkbox({
339
342
  import "react";
340
343
  import { Dialog as DialogPrimitive } from "radix-ui";
341
344
  import { IconX } from "@tabler/icons-react";
345
+
346
+ // src/components/liquid-menu-surface.ts
347
+ var liquidMenuSurfaceClasses = "rounded-2xl bg-popover/75 dark:bg-zinc-900/75 backdrop-blur-[3px] border border-black/[0.08] text-popover-foreground shadow-[0_4px_16px_-4px_rgba(0,0,0,0.08),0_16px_48px_-12px_rgba(0,0,0,0.28)] dark:border-white/[0.09] dark:bg-[#141417] dark:shadow-[0_4px_16px_-4px_rgba(0,0,0,0.4),0_16px_48px_-12px_rgba(0,0,0,0.7)]";
348
+
349
+ // src/components/dialog.tsx
342
350
  import { jsx as jsx6, jsxs } from "react/jsx-runtime";
343
351
  function Dialog({
344
352
  ...props
@@ -369,7 +377,7 @@ function DialogOverlay({
369
377
  {
370
378
  "data-slot": "dialog-overlay",
371
379
  className: cn(
372
- "fixed inset-0 isolate z-50 bg-black/30 supports-backdrop-filter:backdrop-blur-sm",
380
+ "coline-dialog-overlay fixed inset-0 isolate z-50 bg-black/30 supports-backdrop-filter:backdrop-blur-sm",
373
381
  className
374
382
  ),
375
383
  ...props
@@ -390,15 +398,16 @@ function DialogContent({
390
398
  "data-slot": "dialog-content",
391
399
  className: cn(
392
400
  // Mobile: bottom-sheet style, pinned to bottom with slide-up
393
- "fixed z-50 grid w-full gap-4 bg-background/75 dark:bg-zinc-900/75 backdrop-blur-sm p-4 text-sm shadow-xl shadow-black/10 dark:shadow-black/30 ring-1 ring-white/15 dark:ring-white/10 inset-ring inset-ring-white/10 dark:inset-ring-white/5 outline-none",
394
- "inset-x-0 bottom-0 max-h-[85dvh] overflow-y-auto rounded-t-3xl pb-[max(1rem,env(safe-area-inset-bottom))]",
401
+ "coline-dialog-content fixed z-50 grid w-full gap-4 p-4 text-sm outline-none",
402
+ liquidMenuSurfaceClasses,
403
+ "inset-x-0 bottom-0 max-h-[85dvh] overflow-y-auto rounded-b-none pb-[max(1rem,env(safe-area-inset-bottom))]",
395
404
  // Desktop: centered modal
396
- "sm:inset-x-auto sm:bottom-auto sm:top-1/2 sm:left-1/2 sm:max-w-md sm:max-h-[calc(100dvh-4rem)] sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-4xl sm:p-6 sm:gap-6 sm:pb-6",
405
+ "sm:inset-x-auto sm:bottom-auto sm:top-1/2 sm:left-1/2 sm:max-w-md sm:max-h-[calc(100dvh-4rem)] sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-2xl sm:p-6 sm:gap-6 sm:pb-6",
397
406
  className
398
407
  ),
399
408
  ...props,
400
409
  children: [
401
- /* @__PURE__ */ jsx6("div", { className: "mx-auto h-1 w-10 shrink-0 rounded-full bg-muted-foreground/30 sm:hidden" }),
410
+ /* @__PURE__ */ jsx6("div", { className: "mx-auto h-1 w-10 shrink-0 rounded-full bg-muted-foreground/30 sm:hidden", "aria-hidden": "true" }),
402
411
  children,
403
412
  showCloseButton && /* @__PURE__ */ jsx6(DialogPrimitive.Close, { "data-slot": "dialog-close", asChild: true, children: /* @__PURE__ */ jsxs(
404
413
  Button,
@@ -422,7 +431,7 @@ function DialogHeader({ className, ...props }) {
422
431
  "div",
423
432
  {
424
433
  "data-slot": "dialog-header",
425
- className: cn("flex flex-col gap-2", className),
434
+ className: cn("flex flex-col gap-2 pr-8", className),
426
435
  ...props
427
436
  }
428
437
  );
@@ -484,9 +493,6 @@ import * as React8 from "react";
484
493
  import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
485
494
  import { IconCheck as IconCheck2, IconChevronRight } from "@tabler/icons-react";
486
495
 
487
- // src/components/liquid-menu-surface.ts
488
- var liquidMenuSurfaceClasses = "rounded-2xl bg-popover/75 dark:bg-zinc-900/75 backdrop-blur-[3px] border border-black/[0.08] text-popover-foreground shadow-[0_4px_16px_-4px_rgba(0,0,0,0.08),0_16px_48px_-12px_rgba(0,0,0,0.28)] dark:border-white/[0.09] dark:bg-[#141417] dark:shadow-[0_4px_16px_-4px_rgba(0,0,0,0.4),0_16px_48px_-12px_rgba(0,0,0,0.7)]";
489
-
490
496
  // src/components/menu-highlight.tsx
491
497
  import * as React7 from "react";
492
498
  import { motion } from "framer-motion";
@@ -1646,115 +1652,73 @@ function TableCaption({
1646
1652
  }
1647
1653
 
1648
1654
  // src/components/tabs.tsx
1649
- import "react";
1655
+ import * as React18 from "react";
1650
1656
  import { cva as cva4 } from "class-variance-authority";
1651
1657
  import { Tabs as TabsPrimitive } from "radix-ui";
1652
- import { motion as motion2 } from "framer-motion";
1653
- import { jsx as jsx20 } from "react/jsx-runtime";
1654
- var TABS_LAYOUT_SPRING = {
1655
- type: "spring",
1656
- stiffness: 720,
1657
- damping: 46,
1658
- mass: 0.48
1659
- };
1660
- function Tabs({
1661
- className,
1662
- orientation = "horizontal",
1663
- ...props
1664
- }) {
1665
- return /* @__PURE__ */ jsx20(
1658
+ import { motion as motion2, useReducedMotion } from "framer-motion";
1659
+ import { Fragment, jsx as jsx20, jsxs as jsxs6 } from "react/jsx-runtime";
1660
+ var TABS_LAYOUT_SPRING = { type: "spring", stiffness: 300, damping: 26, mass: 1 };
1661
+ var TabsContext = React18.createContext(null);
1662
+ var ListVariantContext = React18.createContext("default");
1663
+ function Tabs({ className, orientation = "horizontal", value, defaultValue, onValueChange, ...props }) {
1664
+ const [localValue, setLocalValue] = React18.useState(defaultValue);
1665
+ const id = React18.useId();
1666
+ const activeValue = value ?? localValue;
1667
+ const context = React18.useMemo(() => ({ value: activeValue, id }), [activeValue, id]);
1668
+ return /* @__PURE__ */ jsx20(TabsContext.Provider, { value: context, children: /* @__PURE__ */ jsx20(
1666
1669
  TabsPrimitive.Root,
1667
1670
  {
1671
+ ...props,
1672
+ ...activeValue === void 0 ? {} : { value: activeValue },
1673
+ orientation,
1674
+ onValueChange: (next) => {
1675
+ if (value === void 0) setLocalValue(next);
1676
+ onValueChange?.(next);
1677
+ },
1668
1678
  "data-slot": "tabs",
1669
- "data-orientation": orientation,
1670
- className: cn(
1671
- "group/tabs flex gap-2 data-horizontal:flex-col",
1672
- className
1673
- ),
1674
- ...props
1675
- }
1676
- );
1677
- }
1678
- var tabsListVariants = cva4(
1679
- "group/tabs-list relative inline-flex w-fit items-center justify-center rounded-4xl p-[3px] text-muted-foreground group-data-horizontal/tabs:h-9 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col group-data-vertical/tabs:rounded-2xl data-[variant=line]:rounded-none",
1680
- {
1681
- variants: {
1682
- variant: {
1683
- default: "bg-muted/70 border border-black/4 shadow-[inset_0_1px_2px_rgba(0,0,0,0.06)] dark:bg-white/4 dark:border-white/4 dark:shadow-[inset_0_1px_3px_rgba(0,0,0,0.3)]",
1684
- line: "gap-1 bg-transparent"
1685
- }
1686
- },
1687
- defaultVariants: {
1688
- variant: "default"
1689
- }
1690
- }
1691
- );
1692
- function TabsList({
1693
- className,
1694
- variant = "default",
1695
- ...props
1696
- }) {
1697
- return /* @__PURE__ */ jsx20(
1698
- TabsPrimitive.List,
1699
- {
1700
- "data-slot": "tabs-list",
1701
- "data-variant": variant,
1702
- className: cn(tabsListVariants({ variant }), className),
1703
- ...props
1679
+ className: cn("coline-tabs", className)
1704
1680
  }
1705
- );
1681
+ ) });
1706
1682
  }
1707
- function TabsTrigger({
1708
- className,
1709
- ...props
1710
- }) {
1711
- return /* @__PURE__ */ jsx20(
1712
- TabsPrimitive.Trigger,
1713
- {
1714
- "data-slot": "tabs-trigger",
1715
- className: cn(
1716
- "relative z-1 inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-xl border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/50 transition-[color,background-color,border-color] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start group-data-vertical/tabs:px-2.5 group-data-vertical/tabs:py-1.5 hover:text-foreground/80 focus-visible:border-ring disabled:pointer-events-none disabled:opacity-50 dark:text-muted-foreground/70 dark:hover:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
1717
- "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent group-data-[variant=line]/tabs-list:data-active:shadow-none dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
1718
- "data-active:text-foreground data-active:hover:text-foreground dark:data-active:text-foreground dark:data-active:hover:text-foreground",
1719
- "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:-bottom-1.25 group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
1720
- className
1721
- ),
1722
- ...props
1723
- }
1724
- );
1683
+ var tabsListVariants = cva4("coline-tabs-list", {
1684
+ variants: { variant: { default: "coline-tabs-raised", line: "coline-tabs-line" } },
1685
+ defaultVariants: { variant: "default" }
1686
+ });
1687
+ function TabsList({ className, variant = "default", ...props }) {
1688
+ return /* @__PURE__ */ jsx20(ListVariantContext.Provider, { value: variant ?? "default", children: /* @__PURE__ */ jsx20(TabsPrimitive.List, { ...props, "data-slot": "tabs-list", "data-variant": variant, className: cn(tabsListVariants({ variant }), className) }) });
1689
+ }
1690
+ function hasIndicator(children) {
1691
+ return React18.Children.toArray(children).some((child) => React18.isValidElement(child) && (child.type === TabsIndicator || hasIndicator(child.props.children)));
1692
+ }
1693
+ function TabsTrigger({ className, children, value, asChild = false, ...props }) {
1694
+ const tabs = React18.useContext(TabsContext);
1695
+ const variant = React18.useContext(ListVariantContext);
1696
+ const indicator = tabs?.value === value && variant === "default" && !hasIndicator(children) ? /* @__PURE__ */ jsx20(TabsIndicator, {}) : null;
1697
+ const content = asChild && React18.isValidElement(children) ? React18.cloneElement(children, void 0, indicator, children.props.children) : /* @__PURE__ */ jsxs6(Fragment, { children: [
1698
+ indicator,
1699
+ children
1700
+ ] });
1701
+ return /* @__PURE__ */ jsx20(TabsPrimitive.Trigger, { ...props, value, asChild, "data-slot": "tabs-trigger", className: cn("coline-tabs-trigger", className), children: content });
1725
1702
  }
1726
- function TabsIndicator({
1727
- className,
1728
- layoutId = "tabs-indicator"
1729
- }) {
1703
+ function TabsIndicator({ className, layoutId = "selection" }) {
1704
+ const tabs = React18.useContext(TabsContext);
1705
+ const fallbackId = React18.useId();
1706
+ const reducedMotion = useReducedMotion();
1730
1707
  return /* @__PURE__ */ jsx20(
1731
1708
  motion2.div,
1732
1709
  {
1733
- layout: "position",
1734
- layoutId,
1735
- className: cn(
1736
- "absolute inset-0 z-[-1] rounded-xl bg-background border border-black/6 shadow-[0_1px_3px_0_rgba(0,0,0,0.08)] dark:bg-white/7 dark:border-white/6 dark:shadow-[0_1px_3px_0_rgba(0,0,0,0.3),inset_0_1px_0_0_rgba(255,255,255,0.04)]",
1737
- className
1738
- ),
1739
- transition: TABS_LAYOUT_SPRING
1710
+ "aria-hidden": "true",
1711
+ "data-slot": "tabs-indicator",
1712
+ layout: true,
1713
+ layoutId: `${tabs?.id ?? fallbackId}-${layoutId}`,
1714
+ initial: false,
1715
+ className: cn("coline-tabs-indicator", className),
1716
+ transition: reducedMotion ? { duration: 0 } : TABS_LAYOUT_SPRING
1740
1717
  }
1741
1718
  );
1742
1719
  }
1743
- function TabsContent({
1744
- className,
1745
- ...props
1746
- }) {
1747
- return /* @__PURE__ */ jsx20(
1748
- TabsPrimitive.Content,
1749
- {
1750
- "data-slot": "tabs-content",
1751
- className: cn(
1752
- "flex-1 text-sm outline-none animate-in fade-in-0 slide-in-from-bottom-0.5 duration-100 ease-out",
1753
- className
1754
- ),
1755
- ...props
1756
- }
1757
- );
1720
+ function TabsContent({ className, ...props }) {
1721
+ return /* @__PURE__ */ jsx20(TabsPrimitive.Content, { ...props, "data-slot": "tabs-content", className: cn("coline-tabs-content", className) });
1758
1722
  }
1759
1723
 
1760
1724
  // src/components/textarea.tsx
@@ -1898,7 +1862,7 @@ function useLiveCollection(name, query, options) {
1898
1862
 
1899
1863
  // src/patterns/workspace.tsx
1900
1864
  import * as React23 from "react";
1901
- import { Fragment, jsx as jsx24, jsxs as jsxs6 } from "react/jsx-runtime";
1865
+ import { Fragment as Fragment2, jsx as jsx24, jsxs as jsxs7 } from "react/jsx-runtime";
1902
1866
  function AppPage({
1903
1867
  title,
1904
1868
  description,
@@ -1908,9 +1872,9 @@ function AppPage({
1908
1872
  width = "wide",
1909
1873
  className
1910
1874
  }) {
1911
- return /* @__PURE__ */ jsxs6("main", { className: cn("coline-page", className), "data-width": width, children: [
1912
- /* @__PURE__ */ jsxs6("header", { className: "coline-page-header", children: [
1913
- /* @__PURE__ */ jsxs6("div", { className: "coline-page-heading", children: [
1875
+ return /* @__PURE__ */ jsxs7("main", { className: cn("coline-page", className), "data-width": width, children: [
1876
+ /* @__PURE__ */ jsxs7("header", { className: "coline-page-header", children: [
1877
+ /* @__PURE__ */ jsxs7("div", { className: "coline-page-heading", children: [
1914
1878
  eyebrow && /* @__PURE__ */ jsx24("div", { className: "coline-eyebrow", children: eyebrow }),
1915
1879
  /* @__PURE__ */ jsx24("h1", { children: title }),
1916
1880
  description && /* @__PURE__ */ jsx24("p", { className: "coline-page-description", children: description })
@@ -1933,7 +1897,7 @@ function EmptyState({
1933
1897
  action,
1934
1898
  icon
1935
1899
  }) {
1936
- return /* @__PURE__ */ jsxs6("section", { className: "coline-empty", children: [
1900
+ return /* @__PURE__ */ jsxs7("section", { className: "coline-empty", children: [
1937
1901
  icon && /* @__PURE__ */ jsx24("div", { className: "coline-empty-icon", "aria-hidden": "true", children: icon }),
1938
1902
  /* @__PURE__ */ jsx24("h2", { children: title }),
1939
1903
  description && /* @__PURE__ */ jsx24("p", { children: description }),
@@ -1941,8 +1905,8 @@ function EmptyState({
1941
1905
  ] });
1942
1906
  }
1943
1907
  function AppErrorState({ message, onRetry }) {
1944
- return /* @__PURE__ */ jsxs6("section", { className: "coline-error", role: "alert", children: [
1945
- /* @__PURE__ */ jsxs6("div", { children: [
1908
+ return /* @__PURE__ */ jsxs7("section", { className: "coline-error", role: "alert", children: [
1909
+ /* @__PURE__ */ jsxs7("div", { children: [
1946
1910
  /* @__PURE__ */ jsx24("h2", { children: "Something went wrong" }),
1947
1911
  /* @__PURE__ */ jsx24("p", { children: message })
1948
1912
  ] }),
@@ -1953,7 +1917,7 @@ function AppLoadingState({
1953
1917
  label = "Loading",
1954
1918
  rows = 5
1955
1919
  }) {
1956
- return /* @__PURE__ */ jsxs6("div", { className: "coline-loading", role: "status", "aria-label": label, children: [
1920
+ return /* @__PURE__ */ jsxs7("div", { className: "coline-loading", role: "status", "aria-label": label, children: [
1957
1921
  Array.from({ length: Math.max(1, Math.min(rows, 12)) }, (_, i) => /* @__PURE__ */ jsx24("div", { style: { width: `${92 - i % 3 * 14}%` } }, i)),
1958
1922
  /* @__PURE__ */ jsx24("span", { className: "coline-sr-only", children: label })
1959
1923
  ] });
@@ -1962,7 +1926,7 @@ function StatusBadge({
1962
1926
  children,
1963
1927
  tone = "neutral"
1964
1928
  }) {
1965
- return /* @__PURE__ */ jsxs6("span", { className: "coline-status", "data-tone": tone, children: [
1929
+ return /* @__PURE__ */ jsxs7("span", { className: "coline-status", "data-tone": tone, children: [
1966
1930
  /* @__PURE__ */ jsx24("span", { "aria-hidden": "true" }),
1967
1931
  children
1968
1932
  ] });
@@ -1972,7 +1936,7 @@ function SplitView({
1972
1936
  detail,
1973
1937
  label = "Details"
1974
1938
  }) {
1975
- return /* @__PURE__ */ jsxs6("div", { className: "coline-split", "data-open": Boolean(detail), children: [
1939
+ return /* @__PURE__ */ jsxs7("div", { className: "coline-split", "data-open": Boolean(detail), children: [
1976
1940
  /* @__PURE__ */ jsx24("section", { className: "coline-split-list", children: list }),
1977
1941
  detail && /* @__PURE__ */ jsx24("aside", { className: "coline-split-detail", "aria-label": label, children: detail })
1978
1942
  ] });
@@ -1984,9 +1948,9 @@ function DetailPanel({
1984
1948
  children,
1985
1949
  actions
1986
1950
  }) {
1987
- return /* @__PURE__ */ jsxs6("section", { className: "coline-detail", children: [
1988
- /* @__PURE__ */ jsxs6("header", { children: [
1989
- /* @__PURE__ */ jsxs6("div", { children: [
1951
+ return /* @__PURE__ */ jsxs7("section", { className: "coline-detail", children: [
1952
+ /* @__PURE__ */ jsxs7("header", { children: [
1953
+ /* @__PURE__ */ jsxs7("div", { children: [
1990
1954
  subtitle && /* @__PURE__ */ jsx24("div", { className: "coline-eyebrow", children: subtitle }),
1991
1955
  /* @__PURE__ */ jsx24("h2", { children: title })
1992
1956
  ] }),
@@ -2005,8 +1969,8 @@ function RecordTable({
2005
1969
  empty
2006
1970
  }) {
2007
1971
  if (!records.length)
2008
- return /* @__PURE__ */ jsx24(Fragment, { children: empty ?? /* @__PURE__ */ jsx24(EmptyState, { title: "No results", description: "Try changing your filters." }) });
2009
- return /* @__PURE__ */ jsx24("div", { className: "coline-table-scroll", children: /* @__PURE__ */ jsxs6("table", { className: "coline-record-table", "aria-label": label, children: [
1972
+ return /* @__PURE__ */ jsx24(Fragment2, { children: empty ?? /* @__PURE__ */ jsx24(EmptyState, { title: "No results", description: "Try changing your filters." }) });
1973
+ return /* @__PURE__ */ jsx24("div", { className: "coline-table-scroll", children: /* @__PURE__ */ jsxs7("table", { className: "coline-record-table", "aria-label": label, children: [
2010
1974
  /* @__PURE__ */ jsx24("thead", { children: /* @__PURE__ */ jsx24("tr", { children: columns.map((column) => /* @__PURE__ */ jsx24(
2011
1975
  "th",
2012
1976
  {
@@ -2048,8 +2012,8 @@ function BoardColumn({
2048
2012
  children,
2049
2013
  action
2050
2014
  }) {
2051
- return /* @__PURE__ */ jsxs6("section", { className: "coline-board-column", children: [
2052
- /* @__PURE__ */ jsxs6("header", { children: [
2015
+ return /* @__PURE__ */ jsxs7("section", { className: "coline-board-column", children: [
2016
+ /* @__PURE__ */ jsxs7("header", { children: [
2053
2017
  /* @__PURE__ */ jsx24("h2", { children: title }),
2054
2018
  count !== void 0 && /* @__PURE__ */ jsx24("span", { className: "coline-count", children: count }),
2055
2019
  action
@@ -2063,7 +2027,7 @@ function BoardCard({
2063
2027
  footer,
2064
2028
  onOpen
2065
2029
  }) {
2066
- return /* @__PURE__ */ jsxs6("article", { className: "coline-board-card", children: [
2030
+ return /* @__PURE__ */ jsxs7("article", { className: "coline-board-card", children: [
2067
2031
  /* @__PURE__ */ jsx24("h3", { children: onOpen ? /* @__PURE__ */ jsx24("button", { type: "button", className: "coline-row-open", onClick: onOpen, children: title }) : title }),
2068
2032
  children && /* @__PURE__ */ jsx24("div", { className: "coline-board-card-body", children }),
2069
2033
  footer && /* @__PURE__ */ jsx24("footer", { children: footer })
@@ -2075,14 +2039,28 @@ function SettingsSection({
2075
2039
  children
2076
2040
  }) {
2077
2041
  const id = React23.useId();
2078
- return /* @__PURE__ */ jsxs6("section", { className: "coline-settings-section", "aria-labelledby": id, children: [
2079
- /* @__PURE__ */ jsxs6("div", { children: [
2042
+ return /* @__PURE__ */ jsxs7("section", { className: "coline-settings-section", "aria-labelledby": id, children: [
2043
+ /* @__PURE__ */ jsxs7("div", { children: [
2080
2044
  /* @__PURE__ */ jsx24("h2", { id, children: title }),
2081
2045
  description && /* @__PURE__ */ jsx24("p", { children: description })
2082
2046
  ] }),
2083
2047
  /* @__PURE__ */ jsx24("div", { className: "coline-settings-fields", children })
2084
2048
  ] });
2085
2049
  }
2050
+
2051
+ // src/patterns/filter-menu.tsx
2052
+ import * as React24 from "react";
2053
+ import { IconChevronDown as IconChevronDown2 } from "@tabler/icons-react";
2054
+ import { jsx as jsx25, jsxs as jsxs8 } from "react/jsx-runtime";
2055
+ var FilterMenu = React24.forwardRef(function FilterMenu2({ value, options, onValueChange, className, disabled, "aria-label": label }, ref) {
2056
+ return /* @__PURE__ */ jsxs8(DropdownMenu, { children: [
2057
+ /* @__PURE__ */ jsx25(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs8(Button, { ref, variant: "outline", disabled, "aria-label": label, className: cn("coline-filter-menu", className), children: [
2058
+ /* @__PURE__ */ jsx25("span", { children: options.find((option) => option.value === value)?.label ?? label }),
2059
+ /* @__PURE__ */ jsx25(IconChevronDown2, { "aria-hidden": "true" })
2060
+ ] }) }),
2061
+ /* @__PURE__ */ jsx25(DropdownMenuContent, { children: /* @__PURE__ */ jsx25(DropdownMenuRadioGroup, { "aria-label": label, value, onValueChange, children: options.map((option) => /* @__PURE__ */ jsx25(DropdownMenuRadioItem, { value: option.value, disabled: option.disabled ?? false, children: option.label }, option.value)) }) })
2062
+ ] });
2063
+ });
2086
2064
  export {
2087
2065
  AppErrorState,
2088
2066
  AppLoadingState,
@@ -2138,6 +2116,7 @@ export {
2138
2116
  FieldSeparator,
2139
2117
  FieldSet,
2140
2118
  FieldTitle,
2119
+ FilterMenu,
2141
2120
  Input,
2142
2121
  Label,
2143
2122
  PageToolbar,