Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/stage-4-composites.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@reactive/silk-core': minor
'@reactive/silk': minor
---

Stage 4 composites: product components, serializable models, and composition standard.

- Add `@reactive/silk-core/models` (Identity/Stat/Media/Post/Comment/Notification/Profile/FeedEntry) and composite recipes.
- Close primitive gaps: Card/Surface `interactive` hover elevation, Inline `direction`, Stack `rail`, `StatusDot`.
- Ship composites: MediaObject, ActionBar, StatGroup, EmptyState, PostCard, Comment, CommentThread, Notification, FeedItem, ProfileCard, SettingsPanel.
- Document the compound-first dual API in `docs/COMPOSITES.md`.
- Retrofit Identity onto Inline (additive `model` prop — non-breaking).
- SocialFeed exit fixture; registry sync script emits consumer-owned composite source.
20 changes: 20 additions & 0 deletions apps/docs/src/components/ActionBar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ActionBar } from '@reactive/silk';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';

const meta = {
title: 'Components/Composite/ActionBar',
} satisfies Meta;

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

export const Primary: Story = {
render: () => (
<ActionBar.Root aria-label="Post actions">
<ActionBar.Action>Like</ActionBar.Action>
<ActionBar.Action>Reply</ActionBar.Action>
<ActionBar.Spacer />
<ActionBar.Action tone="danger">Report</ActionBar.Action>
</ActionBar.Root>
),
};
69 changes: 69 additions & 0 deletions apps/docs/src/components/CommentThread.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { CommentThread } from '@reactive/silk';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';

const meta = {
title: 'Components/Composite/CommentThread',
component: CommentThread,
args: {
maxDepth: 3,
comments: [
{
id: 'c1',
author: {
id: 'u1',
name: 'Ada Lovelace',
meta: '@ada',
fallback: 'AL',
},
body: 'The Analytical Engine weaves algebraic patterns just as the Jacquard loom weaves flowers and leaves.',
createdAt: '2026-07-26T10:00:00.000Z',
replyCount: 2,
hasMoreReplies: false,
actions: [
{ id: 'like', label: 'Like' },
{ id: 'reply', label: 'Reply' },
],
replies: [
{
id: 'c2',
author: {
id: 'u2',
name: 'Charles Babbage',
meta: '@charles',
fallback: 'CB',
},
body: 'A useful analogy — though the carry chain is where the metaphor frays.',
createdAt: '2026-07-26T11:00:00.000Z',
replyCount: 1,
hasMoreReplies: false,
actions: [
{ id: 'like', label: 'Like' },
{ id: 'reply', label: 'Reply' },
],
replies: [
{
id: 'c3',
author: {
id: 'u1',
name: 'Ada Lovelace',
meta: '@ada',
fallback: 'AL',
},
body: 'Agreed. Keep the rail light and the byline on one line.',
createdAt: '2026-07-26T12:00:00.000Z',
replyCount: 0,
hasMoreReplies: false,
actions: [{ id: 'reply', label: 'Reply' }],
},
],
},
],
},
],
},
} satisfies Meta<typeof CommentThread>;

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

export const Primary: Story = {};
21 changes: 21 additions & 0 deletions apps/docs/src/components/EmptyState.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Button, EmptyState } from '@reactive/silk';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';

const meta = {
title: 'Components/Composite/EmptyState',
component: EmptyState,
args: {
title: 'Nothing here',
description: 'When content arrives, it will show up in this space.',
},
} satisfies Meta<typeof EmptyState>;

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

export const Primary: Story = {};
export const WithAction: Story = {
args: {
action: <Button tone="accent">Create post</Button>,
},
};
33 changes: 33 additions & 0 deletions apps/docs/src/components/FeedItem.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FeedItem, Stack } from '@reactive/silk';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';

const meta = {
title: 'Components/Composite/FeedItem',
component: FeedItem,
} satisfies Meta<typeof FeedItem>;

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

export const Post: Story = {
args: {
entry: {
type: 'post',
value: {
id: 'p1',
author: { id: 'u1', name: 'Ada', fallback: 'A' },
body: 'A feed post',
createdAt: '2026-07-26T10:00:00.000Z',
},
},
},
};

export const Loading: Story = {
render: () => (
<Stack gap="4">
<FeedItem loading />
<FeedItem loading />
</Stack>
),
};
11 changes: 11 additions & 0 deletions apps/docs/src/components/Identity.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,14 @@ export const FallbackOnly: Story = {
meta: 'docs composite',
},
};

export const FromModel: Story = {
args: {
model: {
id: 'u1',
name: 'Ada Lovelace',
meta: '@ada',
fallback: 'AL',
},
},
};
32 changes: 32 additions & 0 deletions apps/docs/src/components/MediaObject.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Avatar, MediaObject, Text } from '@reactive/silk';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import type { JSX } from 'react';

const meta = {
title: 'Components/Composite/MediaObject',
} satisfies Meta;

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

export const Primary: Story = {
render: (): JSX.Element => (
<MediaObject media={<Avatar fallback="AL" />}>
<Text role="label">Ada Lovelace</Text>
<Text role="caption" tone="secondary">
Beside content
</Text>
</MediaObject>
),
};

export const MediaEnd: Story = {
render: (): JSX.Element => (
<MediaObject media={<Avatar fallback="AL" />} mediaPosition="end">
<Text role="label">Ada Lovelace</Text>
<Text role="caption" tone="secondary">
Media on the end
</Text>
</MediaObject>
),
};
43 changes: 43 additions & 0 deletions apps/docs/src/components/Notification.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Notification, Stack } from '@reactive/silk';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import type { JSX } from 'react';

const meta = {
title: 'Components/Composite/Notification',
} satisfies Meta;

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

export const Unread: Story = {
render: (): JSX.Element => (
<Notification
model={{
id: 'n1',
kind: 'mention',
actor: { id: 'u1', name: 'Ada', fallback: 'A' },
text: 'mentioned you in a note',
createdAt: '2026-07-26T10:00:00.000Z',
read: false,
href: '#n1',
}}
/>
),
};

export const Read: Story = {
render: (): JSX.Element => (
<Stack gap="3">
<Notification
model={{
id: 'n2',
kind: 'follow',
actor: { id: 'u2', name: 'Charles', fallback: 'C' },
text: 'followed you',
createdAt: '2026-07-26T09:00:00.000Z',
read: true,
}}
/>
</Stack>
),
};
38 changes: 38 additions & 0 deletions apps/docs/src/components/PostCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { PostCard } from '@reactive/silk';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';

const meta = {
title: 'Components/Composite/PostCard',
component: PostCard,
args: {
model: {
id: 'p1',
author: {
id: 'u1',
name: 'Ada Lovelace',
meta: '@ada',
fallback: 'AL',
},
body: 'The Analytical Engine weaves algebraic patterns.',
createdAt: '2026-07-26T10:00:00.000Z',
stats: [
{
id: 'likes',
label: 'Likes',
value: 128,
delta: { value: 12, direction: 'up' as const },
},
],
actions: [
{ id: 'like', label: 'Like' },
{ id: 'reply', label: 'Reply' },
],
},
},
} satisfies Meta<typeof PostCard>;

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

export const Primary: Story = {};
export const WithLink: Story = { args: { href: '#post-p1' } };
29 changes: 29 additions & 0 deletions apps/docs/src/components/ProfileCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ProfileCard } from '@reactive/silk';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';

const meta = {
title: 'Components/Composite/ProfileCard',
component: ProfileCard,
args: {
model: {
identity: {
id: 'u1',
name: 'Ada Lovelace',
meta: '@ada',
fallback: 'AL',
},
bio: 'Mathematician · Writer · First programmer',
stats: [
{ id: 'followers', label: 'Followers', value: '12.4k' },
{ id: 'following', label: 'Following', value: 240 },
],
actions: [{ id: 'follow', label: 'Follow', tone: 'accent' as const }],
},
},
} satisfies Meta<typeof ProfileCard>;

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

export const Stacked: Story = {};
export const Horizontal: Story = { args: { layout: 'horizontal' } };
33 changes: 33 additions & 0 deletions apps/docs/src/components/SettingsPanel.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
Field,
Input,
SettingsPanel,
Switch,
} from '@reactive/silk';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';

const meta = {
title: 'Components/Composite/SettingsPanel',
} satisfies Meta;

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

export const Primary: Story = {
render: () => (
<SettingsPanel.Root>
<SettingsPanel.Section>
<SettingsPanel.SectionTitle>Account</SettingsPanel.SectionTitle>
<SettingsPanel.Row label="Display name">
<Field.Root>
<Field.Label>Display name</Field.Label>
<Input defaultValue="Ada Lovelace" />
</Field.Root>
</SettingsPanel.Row>
<SettingsPanel.Row label="Email notifications">
<Switch aria-label="Email notifications" defaultChecked />
</SettingsPanel.Row>
</SettingsPanel.Section>
</SettingsPanel.Root>
),
};
24 changes: 24 additions & 0 deletions apps/docs/src/components/StatGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { StatGroup } from '@reactive/silk';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';

const meta = {
title: 'Components/Composite/StatGroup',
component: StatGroup,
args: {
stats: [
{
id: 'likes',
label: 'Likes',
value: 128,
delta: { value: 12, direction: 'up' as const },
},
{ id: 'replies', label: 'Replies', value: 14 },
],
},
} satisfies Meta<typeof StatGroup>;

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

export const Primary: Story = {};
export const Vertical: Story = { args: { orientation: 'vertical' } };
Loading
Loading