Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
48e9eb3
Make hand-built ui parts render-capable via Base UI useRender
lifeiscontent Jun 24, 2026
b83b9c3
avatar-group: move the magnitude context provider to the components tier
lifeiscontent Jun 24, 2026
5490efa
toggle-group: move the magnitude context provider to the components tier
lifeiscontent Jun 24, 2026
d721029
toolbar: move the density context provider to the components tier
lifeiscontent Jun 24, 2026
25c3ff5
tabs: single-element ui parts; move composition to components
lifeiscontent Jun 24, 2026
20d4cf8
table: single-element ui parts; move scroll frame + variant provider …
lifeiscontent Jun 24, 2026
1ad485a
Format toggle-group components file (slipped a scoped --fix)
lifeiscontent Jun 24, 2026
3907e6c
avatar: move AvatarGroupContext to components (context = composition …
lifeiscontent Jun 24, 2026
686c715
tabs: move TabsVariantContext to components (context = composition co…
lifeiscontent Jun 24, 2026
07762fa
table: move TableVariantContext + useTableVariant to components
lifeiscontent Jun 24, 2026
e3ee765
toggle: move ToggleGroupContext to components
lifeiscontent Jun 24, 2026
751083f
toolbar: move ToolbarDensityContext to components
lifeiscontent Jun 24, 2026
68685b5
Format avatar-group components file (full --fix; scoped run slipped it)
lifeiscontent Jun 24, 2026
e06faa6
avatar: ui Avatar magnitude is required (no default in ui)
lifeiscontent Jun 24, 2026
b04616b
button: derive variant-prop types in variants.ts (XVariantProps conve…
lifeiscontent Jun 24, 2026
19cff2c
Add StrictVariantProps + apply to Button (variant props optional iff …
lifeiscontent Jun 24, 2026
5b79256
Add propel component operating protocol (packages/propel/CLAUDE.md)
lifeiscontent Jun 24, 2026
7bdb260
protocol: ui mirrors Base UI anatomy (flatten X.Root/X.Part to X/XPart)
lifeiscontent Jun 24, 2026
3c21102
protocol: don't destructure props you don't use (children passthrough)
lifeiscontent Jun 24, 2026
58493d6
icon-button: decouple from Button + rename per protocol
lifeiscontent Jun 24, 2026
f118753
button: variant props all required (StrictVariantProps, no defaults);…
lifeiscontent Jun 24, 2026
452df29
Extract shared control chrome to internal; Button + IconButton compos…
lifeiscontent Jun 24, 2026
a5c5f99
badge: variant-prop types in variants.ts (StrictVariantProps); BadgeP…
lifeiscontent Jun 24, 2026
11794c3
sweep: variant-prop types in variants.ts (avatar, switch, slider-cont…
lifeiscontent Jun 24, 2026
43cb8b9
sweep: variant-prop types in variants.ts (alert-dialog-icon, banner, …
lifeiscontent Jun 24, 2026
ded01aa
sweep: variant-prop types in variants.ts (form, fieldset, checkbox-gr…
lifeiscontent Jun 24, 2026
f585df1
sweep: variant-prop types in variants.ts (progress: root, track, indi…
lifeiscontent Jun 24, 2026
d45f39e
sweep: variant-prop types in variants.ts (context-menu item/checkbox/…
lifeiscontent Jun 24, 2026
0d4293d
sweep: variant-prop types in variants.ts (radio-group, navigation-men…
lifeiscontent Jun 24, 2026
ac59d4d
sweep: variant-prop types in variants.ts (menu-popup, workspace-avata…
lifeiscontent Jun 24, 2026
d358a71
sweep: variant-prop types in variants.ts (toast-status-icon)
lifeiscontent Jun 24, 2026
6ecb415
sweep: menu-item emphasis + nav-item level required (no-defaults); fi…
lifeiscontent Jun 24, 2026
c4f4669
field-label-group: split composition into components, styled containe…
lifeiscontent Jun 24, 2026
b9133c0
index.tsx: export * per local public file (drop explicit enumerations)
lifeiscontent Jun 24, 2026
f14172f
protocol: index.tsx = export * per local public file (cross-tier + va…
lifeiscontent Jun 24, 2026
d4169a7
ui: drop pointless destructuring (rule 7a)
lifeiscontent Jun 24, 2026
9a25f5d
components: drop host-namespaced Menu aliases; compose Menu directly …
lifeiscontent Jun 24, 2026
d14c35f
menu/context-menu: unify submenu naming on base-ui (Submenu, no Root)
lifeiscontent Jun 24, 2026
b199ea0
Eliminate the Root idiom (base-ui convention: propel drops .Root)
lifeiscontent Jun 24, 2026
c0f854e
menu/context-menu: content is children, not a repurposed label (base-…
lifeiscontent Jun 24, 2026
bd8067d
button: rename full-width axis stretch -> sizing (hug|fill); ban nati…
lifeiscontent Jun 24, 2026
26edc88
Split the button/link conflation into Button / ButtonAnchor / Anchor;…
lifeiscontent Jun 24, 2026
d3e2e05
protocol: variant is a smell — name the real axis; split element from…
lifeiscontent Jun 24, 2026
629fea2
anchor: emphasis(solid·subtle) -> prominence(primary·secondary)
lifeiscontent Jun 24, 2026
30838d7
audit renames: Badge drops dead variant; Form variant->layout; TableH…
lifeiscontent Jun 24, 2026
d14620e
tabs: variant(contained·underline) -> appearance — the tab visual sty…
lifeiscontent Jun 24, 2026
c75dece
table: cell-trigger variant(editable·action) -> layout — it's the cel…
lifeiscontent Jun 24, 2026
e62a381
menu: variant(default·with-description) -> derived layout
lifeiscontent Jun 24, 2026
26940fc
split Progress into LinearProgress + CircularProgress (different shap…
lifeiscontent Jun 24, 2026
40bc7f0
table: variant(table·spreadsheet) -> mode — a behavior mode of one Ta…
lifeiscontent Jun 24, 2026
c5069ae
banner: variant(page·inline) -> placement (+ restore <Name>VariantPro…
lifeiscontent Jun 24, 2026
b15be45
navigation-menu: Link variant(item·card) -> presentation
lifeiscontent Jun 24, 2026
1024476
protocol: reflow CLAUDE.md formatting
lifeiscontent Jun 24, 2026
d49c36f
menu: remove the mis-named emphasis axis (it was just the cursor)
lifeiscontent Jun 24, 2026
86e072c
protocol: add Prop vocabulary section (the axes + the why)
lifeiscontent Jun 24, 2026
2a28a77
ui: stop Portal/List/etc. leaking className & style (ui must not expo…
lifeiscontent Jun 24, 2026
424c58e
rename ButtonAnchor -> AnchorButton (match IconButton) + add pending …
lifeiscontent Jun 25, 2026
953b5a6
protocol: document part-of (prefix) vs kind-of (suffix) naming (CLAUD…
lifeiscontent Jun 25, 2026
942d5b5
rename AnchorButton -> ButtonAnchor (it's a kind of Anchor, not Button)
lifeiscontent Jun 25, 2026
3b0edc9
docs: rename packages/propel/CLAUDE.md -> AGENTS.md; CLAUDE.md is now…
lifeiscontent Jun 25, 2026
f8dde39
fix the button/link 2x2: AnchorButton (<a>+button-look) + ButtonAncho…
lifeiscontent Jun 25, 2026
9a1673f
components: add the missing component variants for every ui folder
lifeiscontent Jun 25, 2026
5f29b93
protocol: make 6d (subtype naming) bulletproof
lifeiscontent Jun 25, 2026
1dfa348
split ui/progress -> ui/linear-progress + ui/circular-progress for tr…
lifeiscontent Jun 25, 2026
e8a1538
Enhance Accordion components with children prop support
codingwolf-at Jun 25, 2026
2a3de81
Merge remote-tracking branch 'origin/feat/render-capable-base' into f…
codingwolf-at Jun 25, 2026
47aeee3
Enhance AlertDialog components with children prop support
codingwolf-at Jun 25, 2026
f53783b
Enhance BadgeDismiss component with optional children prop
codingwolf-at Jun 25, 2026
571d85c
Enhance Banner components with optional children prop
codingwolf-at Jun 25, 2026
8a0f6a3
Add Menu integration to Breadcrumb stories
codingwolf-at Jun 25, 2026
849d539
Enhance spinner components with optional children prop
codingwolf-at Jun 25, 2026
e383fe4
Enhance CheckboxInlineStartNode component with optional children prop
codingwolf-at Jun 25, 2026
d8c348b
Enhance CircularProgress components with optional children prop
codingwolf-at Jun 25, 2026
06060f5
Enhance context menu components with optional children prop
codingwolf-at Jun 25, 2026
518f691
Enhance dialog and drawer components with optional children prop
codingwolf-at Jun 25, 2026
2f2bef0
Enhance IconButtonIcon component with optional children prop
codingwolf-at Jun 25, 2026
fcad9e8
Enhance menu components with optional children prop
codingwolf-at Jun 25, 2026
5d82ad5
Enhance menubar components with optional children prop
codingwolf-at Jun 25, 2026
f7b5da4
Enhance navigation menu components with optional children prop
codingwolf-at Jun 25, 2026
0dc1ca5
Enhance nav item components with optional children prop
codingwolf-at Jun 25, 2026
bea07f3
Enhance pagination components with optional children prop
codingwolf-at Jun 25, 2026
b8b823e
Enhance popover components with optional children prop
codingwolf-at Jun 25, 2026
e02ed5c
Enhance preview card components with optional children prop
codingwolf-at Jun 25, 2026
a82784b
Enhance table components with optional children prop
codingwolf-at Jun 25, 2026
b36db41
Enhance toast components with optional children prop
codingwolf-at Jun 25, 2026
e3c02ae
Enhance toolbar components with optional children prop
codingwolf-at Jun 25, 2026
432b7e8
Enhance tooltip components with optional children prop
codingwolf-at Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
11 changes: 9 additions & 2 deletions packages/propel/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,15 @@ const config: StorybookConfig = {
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
shouldRemoveUndefinedFromOptional: true,
// Only document propel's own props, not the spread DOM / Base UI attributes.
propFilter: (prop) => !prop.parent || !prop.parent.fileName.includes("node_modules"),
// Only document propel's own props, not the spread DOM / Base UI attributes — plus
// `children`, the composition slot. react-docgen attributes `children` to React's
// node_modules types, so the base filter would always drop it; yet react-docgen only
// emits `children` at all when a part explicitly redeclares it in its own props type
// (e.g. the atomic slot parts that take a bare icon / svg / label). Keeping it here
// surfaces those opt-in, TSDoc'd `children` slots instead of an empty "couldn't be
// auto-generated" args table, without adding a `children` row to every component.
propFilter: (prop) =>
prop.name === "children" || !prop.parent || !prop.parent.fileName.includes("node_modules"),
},
},
// Storybook runs its own Vite build; add the Tailwind v4 plugin so propel's
Expand Down
242 changes: 242 additions & 0 deletions packages/propel/AGENTS.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/propel/CLAUDE.md
10 changes: 5 additions & 5 deletions packages/propel/src/components/accordion/accordion-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import {
AccordionPanel as AccordionPanelRoot,
AccordionPanel as AccordionPanelElement,
AccordionPanelContent,
type AccordionPanelProps as AccordionPanelRootProps,
type AccordionPanelProps as AccordionPanelElementProps,
} from "../../ui/accordion";

export type AccordionPanelProps = AccordionPanelRootProps;
export type AccordionPanelProps = AccordionPanelElementProps;

/**
* The ready-made accordion panel: composes the atomic `AccordionPanel` with the
* `AccordionPanelContent` padding wrapper so content is inset from the trigger's edges.
*/
export function AccordionPanel({ children, ...props }: AccordionPanelProps) {
return (
<AccordionPanelRoot {...props}>
<AccordionPanelElement {...props}>
<AccordionPanelContent>{children}</AccordionPanelContent>
</AccordionPanelRoot>
</AccordionPanelElement>
);
}
10 changes: 5 additions & 5 deletions packages/propel/src/components/accordion/accordion-trigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { ChevronDown } from "lucide-react";
import type * as React from "react";

import {
AccordionTrigger as AccordionTriggerRoot,
type AccordionTriggerProps as AccordionTriggerRootProps,
AccordionTrigger as AccordionTriggerElement,
type AccordionTriggerProps as AccordionTriggerElementProps,
AccordionTriggerIcon,
AccordionTriggerIndicator,
AccordionTriggerTitle,
} from "../../ui/accordion";

export type AccordionTriggerProps = AccordionTriggerRootProps & {
export type AccordionTriggerProps = AccordionTriggerElementProps & {
/**
* Node rendered before the label (inline-start), matching the Figma header icon. Sized to the
* trigger's `--node-size`. Decorative, kept out of the name.
Expand All @@ -24,12 +24,12 @@ export type AccordionTriggerProps = AccordionTriggerRootProps & {
*/
export function AccordionTrigger({ inlineStartNode, children, ...props }: AccordionTriggerProps) {
return (
<AccordionTriggerRoot {...props}>
<AccordionTriggerElement {...props}>
{inlineStartNode ? <AccordionTriggerIcon>{inlineStartNode}</AccordionTriggerIcon> : null}
<AccordionTriggerTitle>{children}</AccordionTriggerTitle>
<AccordionTriggerIndicator>
<ChevronDown />
</AccordionTriggerIndicator>
</AccordionTriggerRoot>
</AccordionTriggerElement>
);
}
4 changes: 2 additions & 2 deletions packages/propel/src/components/accordion/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { AccordionPanel, type AccordionPanelProps } from "./accordion-panel";
export { AccordionTrigger, type AccordionTriggerProps } from "./accordion-trigger";
export * from "./accordion-panel";
export * from "./accordion-trigger";
// Re-export the atomic structural parts so a full accordion is importable from this convenience.
export {
Accordion,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => (
<AlertDialog>
<Button variant="primary" tone="danger" magnitude="xl" render={<AlertDialogTrigger />}>
<Button
sizing="hug"
prominence="primary"
tone="danger"
magnitude="xl"
render={<AlertDialogTrigger />}
>
Delete project
</Button>
<AlertDialogContent>
Expand All @@ -62,10 +68,22 @@ export const Default: Story = {
</AlertDialogIntro>
</AlertDialogHeader>
<AlertDialogActions>
<Button variant="secondary" tone="neutral" magnitude="xl" render={<AlertDialogClose />}>
<Button
sizing="hug"
prominence="secondary"
tone="neutral"
magnitude="xl"
render={<AlertDialogClose />}
>
Cancel
</Button>
<Button variant="primary" tone="danger" magnitude="xl" render={<AlertDialogClose />}>
<Button
sizing="hug"
prominence="primary"
tone="danger"
magnitude="xl"
render={<AlertDialogClose />}
>
Delete
</Button>
</AlertDialogActions>
Expand Down
2 changes: 1 addition & 1 deletion packages/propel/src/components/alert-dialog/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { AlertDialogContent, type AlertDialogContentProps } from "./alert-dialog-content";
export * from "./alert-dialog-content";
// Re-export the atomic parts so a full alert dialog can be assembled from one entry.
export {
AlertDialog,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { ArrowRight, Plus } from "lucide-react";
import { expect } from "storybook/test";

import { iconControl } from "../../storybook/icon-control";
import { AnchorButton, AnchorButtonIcon, AnchorButtonLabel, AnchorButtonSpinner } from "./index";

// The ready-made button-looking link: a navigation `<a>` with the control chrome plus optional
// inline-start/inline-end nodes beside the label. Same surface as the ready-made `Button`, but it
// navigates (`href`) instead of acting.
const meta = {
title: "Components/AnchorButton",
component: AnchorButton,
subcomponents: { AnchorButtonIcon, AnchorButtonLabel, AnchorButtonSpinner },
argTypes: { inlineStartNode: iconControl, inlineEndNode: iconControl },
args: {
children: "Link",
href: "#",
prominence: "primary",
tone: "neutral",
magnitude: "md",
sizing: "hug",
},
} satisfies Meta<typeof AnchorButton>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};

/** Inline-start and inline-end nodes sit beside the label and are decorative. */
export const WithIcons: Story = {
parameters: { controls: { disable: true } },
render: (args) => (
<div className="flex items-center gap-3">
<AnchorButton {...args} inlineStartNode={<Plus />}>
New page
</AnchorButton>
<AnchorButton {...args} prominence="secondary" inlineEndNode={<ArrowRight />}>
Learn more
</AnchorButton>
</div>
),
};

/**
* `loading` shows a spinner + sets `aria-busy` while a navigation is pending (e.g. a router's
* pending state).
*/
export const Loading: Story = { args: { loading: true } };

/** It renders a real `<a>` carrying the given `href`, so it navigates rather than acts. */
export const RendersAnchor: Story = {
tags: ["!dev", "!autodocs", "!manifest"],
args: { href: "https://example.com" },
play: async ({ canvas }) => {
const link = canvas.getByRole("link", { name: "Link" });
await expect(link).toHaveAttribute("href", "https://example.com");
},
};
50 changes: 50 additions & 0 deletions packages/propel/src/components/anchor-button/anchor-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { LoaderCircle } from "lucide-react";
import type * as React from "react";

import {
AnchorButton as AnchorButtonElement,
AnchorButtonIcon,
AnchorButtonLabel,
type AnchorButtonProps as AnchorButtonElementProps,
AnchorButtonSpinner,
} from "../../ui/anchor-button";

export type AnchorButtonProps = AnchorButtonElementProps & {
/** Node before the label (inline-start), sized to `--node-size`. Decorative. */
inlineStartNode?: React.ReactNode;
/** Node after the label (inline-end), sized to `--node-size`. Decorative. */
inlineEndNode?: React.ReactNode;
/**
* Shows a spinner in place of the inline-start node and sets `aria-busy` while a navigation is
* pending — drive it from your router's pending state (e.g. React Router's `useNavigation`),
* since a link's destination can load before the page changes.
*/
loading?: boolean;
};

/**
* The ready-made `AnchorButton`: a button-looking navigation link that composes the atomic
* `AnchorButton` with an optional `inlineStartNode`/`inlineEndNode` and a `loading` spinner for
* pending navigations. Content — `children`, the inline nodes, `loading` — is not a variant.
*/
export function AnchorButton({
inlineStartNode,
inlineEndNode,
loading = false,
children,
...props
}: AnchorButtonProps) {
return (
<AnchorButtonElement aria-busy={loading ? true : undefined} {...props}>
{loading ? (
<AnchorButtonSpinner>
<LoaderCircle />
</AnchorButtonSpinner>
) : inlineStartNode ? (
<AnchorButtonIcon>{inlineStartNode}</AnchorButtonIcon>
) : null}
<AnchorButtonLabel>{children}</AnchorButtonLabel>
{!loading && inlineEndNode ? <AnchorButtonIcon>{inlineEndNode}</AnchorButtonIcon> : null}
</AnchorButtonElement>
);
}
13 changes: 13 additions & 0 deletions packages/propel/src/components/anchor-button/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export * from "./anchor-button";
export {
AnchorButtonIcon,
type AnchorButtonIconProps,
AnchorButtonLabel,
type AnchorButtonLabelProps,
AnchorButtonSpinner,
type AnchorButtonSpinnerProps,
type AnchorButtonMagnitude,
type AnchorButtonProminence,
type AnchorButtonSizing,
type AnchorButtonTone,
} from "../../ui/anchor-button";
58 changes: 58 additions & 0 deletions packages/propel/src/components/anchor/anchor.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect } from "storybook/test";

import { Anchor, type AnchorMagnitude, type AnchorProminence } from "./index";

const PROMINENCES: AnchorProminence[] = ["primary", "secondary"];
const MAGNITUDES: AnchorMagnitude[] = ["sm", "md", "lg", "xl"];

// The inline text link. For a button-styled nav link use `AnchorButton`; for a link-styled action
// use `ButtonAnchor`.
const meta = {
title: "Components/Anchor",
component: Anchor,
args: { children: "Anchor", href: "#", prominence: "primary", magnitude: "md" },
} satisfies Meta<typeof Anchor>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};

/** `prominence`: `primary` is the blue link; `secondary` the muted gray inline link. */
export const Prominences: Story = {
argTypes: { prominence: { control: false }, children: { control: false } },
render: (args) => (
<div className="flex items-center gap-4">
{PROMINENCES.map((prominence) => (
<Anchor key={prominence} {...args} prominence={prominence}>
{prominence} link
</Anchor>
))}
</div>
),
};

/** Text sizes (Figma S/Base/L/XL map to sm/md/lg/xl). */
export const Magnitudes: Story = {
argTypes: { magnitude: { control: false }, children: { control: false } },
render: (args) => (
<div className="flex items-center gap-4">
{MAGNITUDES.map((magnitude) => (
<Anchor key={magnitude} {...args} magnitude={magnitude}>
{magnitude}
</Anchor>
))}
</div>
),
};

/** It renders a real `<a>` carrying the given `href`. */
export const RendersAnchor: Story = {
tags: ["!dev", "!autodocs", "!manifest"],
args: { href: "https://example.com" },
play: async ({ canvas }) => {
const link = canvas.getByRole("link", { name: "Anchor" });
await expect(link).toHaveAttribute("href", "https://example.com");
},
};
3 changes: 3 additions & 0 deletions packages/propel/src/components/anchor/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Ready-made 1:1 re-export of the ui primitive. Drop to `@plane/propel/ui/anchor` only for the
// lower-level parts. (`Anchor` is a single styled `<a>`, so there is nothing extra to compose.)
export * from "../../ui/anchor";
2 changes: 1 addition & 1 deletion packages/propel/src/components/autocomplete/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { AutocompleteContent, type AutocompleteContentProps } from "./autocomplete-content";
export * from "./autocomplete-content";
// Re-export the atomic autocomplete parts so a full autocomplete can be assembled from one entry.
export {
Autocomplete,
Expand Down
31 changes: 31 additions & 0 deletions packages/propel/src/components/avatar-group/avatar-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type * as React from "react";

import { type AvatarMagnitude } from "../../ui/avatar";
import {
AvatarGroup as AvatarGroupElement,
type AvatarGroupProps as AvatarGroupElementProps,
} from "../../ui/avatar-group";
import { AvatarGroupContext } from "../avatar/avatar-group-context";

// Figma's "Avatar Groups" component only defines three sizes (Small/Base/Large = 16/20/24px), so
// groups are limited to the matching magnitudes — narrower than a standalone Avatar's full scale.
export type AvatarGroupMagnitude = Extract<AvatarMagnitude, "2xs" | "xs" | "sm">;

export type AvatarGroupProps = AvatarGroupElementProps & {
/** Shared size for every avatar in the group; an avatar's own `magnitude` overrides it. */
magnitude: AvatarGroupMagnitude;
children?: React.ReactNode;
};

/**
* The ready-made overlapping avatar stack: shares `magnitude` with every `Avatar` inside via
* context (an avatar's own `magnitude` still wins), composed around the styled `ui/avatar-group`
* container. Each `Avatar`'s own `border-subtle` is the single ring that separates them.
*/
export function AvatarGroup({ magnitude, children, ...props }: AvatarGroupProps) {
return (
<AvatarGroupContext.Provider value={magnitude}>
<AvatarGroupElement {...props}>{children}</AvatarGroupElement>
</AvatarGroupContext.Provider>
);
}
4 changes: 1 addition & 3 deletions packages/propel/src/components/avatar-group/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
// Ready-made 1:1 re-export of the ui primitive. Drop down to `@plane/propel/ui/avatar-group` only
// when you need the lower-level parts.
export * from "../../ui/avatar-group";
export * from "./avatar-group";
10 changes: 10 additions & 0 deletions packages/propel/src/components/avatar/avatar-group-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as React from "react";

import type { AvatarMagnitude } from "../../ui/avatar";

/**
* Set by the components `AvatarGroup` so every `Avatar` inside it shares one `magnitude` (an
* avatar's own `magnitude` still wins). Lives in the components tier: a context is cross-tree
* coordination — composition — not a single-element `ui` concern.
*/
export const AvatarGroupContext = React.createContext<AvatarMagnitude | undefined>(undefined);
Loading