Redesign Resources screen: template panel, subscriptions, hover states, and callback architecture

- Rename ResourceTemplateInput to ResourceTemplatePanel with panel layout
  (title header, URI with live variable preview, labeled inputs, Read Resource button)
- ResourceTemplatePanel manages its own variable state via useState;
  single onReadResource(uri) callback returns fully resolved URI
- Extract ResourceSubscribedItem group component with name, timestamp, and
  subtle Unsubscribe button
- Add hover state to listItem variant via theme classNames + App.css pseudo-selector
- Add Refresh button to ResourcePreviewPanel
- Refactor ResourcesScreen callbacks: onSelectUri, onSelectTemplate,
  onReadResource, onSubscribeResource, onUnsubscribeResource replace
  per-item onClick/onUnsubscribe props; screen wires callbacks to children
- Define data-only types (ResourceItem, TemplateListItem, SubscriptionItem,
  SelectedResource, SelectedTemplate) separating data from callbacks
- Extract named .withProps() subcomponents (FooterRow, AnnotationGroup,
  ActionGroup, Spacer) for AGENTS.md compliance
- Update all stories (ResourceTemplatePanel, ResourcesScreen, ConnectedView)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-03-29 20:04:25 -04:00
commit a0e763a315
18 changed files with 520 additions and 200 deletions

View file

@ -105,6 +105,16 @@
animation: inspector-spin 1s linear infinite;
}
/* ── List item hover ────────────────────────────────────── */
.list-item:hover {
background-color: var(--mantine-color-gray-1);
}
[data-mantine-color-scheme="dark"] .list-item:hover {
background-color: var(--mantine-color-dark-5);
}
/* ── Grid alignment ─────────────────────────────────────── */
.grid-align-start {

View file

@ -6,6 +6,7 @@ const meta: Meta<typeof ResourcePreviewPanel> = {
title: "Groups/ResourcePreviewPanel",
component: ResourcePreviewPanel,
args: {
onRefresh: fn(),
onSubscribe: fn(),
onUnsubscribe: fn(),
},

View file

@ -1,4 +1,4 @@
import { Flex, Group, Stack, Text, Title } from "@mantine/core";
import { Button, Flex, Group, Stack, Text, Title } from "@mantine/core";
import { AnnotationBadge } from "../../elements/AnnotationBadge/AnnotationBadge";
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
import { CopyButton } from "../../elements/CopyButton/CopyButton";
@ -11,6 +11,7 @@ export interface ResourcePreviewPanelProps {
content: string;
lastUpdated?: string;
isSubscribed: boolean;
onRefresh: () => void;
onSubscribe: () => void;
onUnsubscribe: () => void;
}
@ -62,6 +63,20 @@ const MimeText = Text.withProps({
c: "dimmed",
});
const FooterRow = Group.withProps({
justify: "space-between",
});
const AnnotationGroup = Group.withProps({
gap: "xs",
});
const ActionGroup = Group.withProps({
gap: "xs",
});
const Spacer = Flex.withProps({});
export function ResourcePreviewPanel({
uri,
mimeType,
@ -69,6 +84,7 @@ export function ResourcePreviewPanel({
content,
lastUpdated,
isSubscribed,
onRefresh,
onSubscribe,
onUnsubscribe,
}: ResourcePreviewPanelProps) {
@ -91,12 +107,12 @@ export function ResourcePreviewPanel({
{lastUpdated ? (
<TimestampText>{formatLastUpdated(lastUpdated)}</TimestampText>
) : (
<Flex />
<Spacer />
)}
<MimeText>{mimeType}</MimeText>
</MetaRow>
<Group justify="space-between">
<Group gap="xs">
<FooterRow>
<AnnotationGroup>
{annotations?.audience && (
<AnnotationBadge label={annotations.audience} variant="audience" />
)}
@ -106,12 +122,17 @@ export function ResourcePreviewPanel({
variant="priority"
/>
)}
</Group>
<SubscribeButton
subscribed={isSubscribed}
onToggle={isSubscribed ? onUnsubscribe : onSubscribe}
/>
</Group>
</AnnotationGroup>
<ActionGroup>
<Button variant="subtle" size="sm" onClick={onRefresh}>
Refresh
</Button>
<SubscribeButton
subscribed={isSubscribed}
onToggle={isSubscribed ? onUnsubscribe : onSubscribe}
/>
</ActionGroup>
</FooterRow>
</Stack>
);
}

View file

@ -0,0 +1,27 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { ResourceSubscribedItem } from "./ResourceSubscribedItem";
const meta: Meta<typeof ResourceSubscribedItem> = {
title: "Groups/ResourceSubscribedItem",
component: ResourceSubscribedItem,
args: {
onUnsubscribe: fn(),
},
};
export default meta;
type Story = StoryObj<typeof ResourceSubscribedItem>;
export const WithTimestamp: Story = {
args: {
name: "config.json",
lastUpdated: "2026-03-17T10:30:00Z",
},
};
export const WithoutTimestamp: Story = {
args: {
name: "schema.sql",
},
};

View file

@ -0,0 +1,43 @@
import { Button, Group, Stack, Text } from "@mantine/core";
export interface ResourceSubscribedItemProps {
name: string;
lastUpdated?: string;
onUnsubscribe: () => void;
}
const NameText = Text.withProps({
size: "sm",
fw: 500,
});
const TimestampText = Text.withProps({
size: "xs",
c: "dimmed",
});
const SubtleButton = Button.withProps({
variant: "subtle",
size: "xs",
});
const ItemRow = Group.withProps({
justify: "space-between",
wrap: "nowrap",
});
export function ResourceSubscribedItem({
name,
lastUpdated,
onUnsubscribe,
}: ResourceSubscribedItemProps) {
return (
<ItemRow>
<Stack gap={2}>
<NameText>{name}</NameText>
{lastUpdated && <TimestampText>{lastUpdated}</TimestampText>}
</Stack>
<SubtleButton onClick={onUnsubscribe}>Unsubscribe</SubtleButton>
</ItemRow>
);
}

View file

@ -1,39 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { ResourceTemplateInput } from "./ResourceTemplateInput";
const meta: Meta<typeof ResourceTemplateInput> = {
title: "Groups/ResourceTemplateInput",
component: ResourceTemplateInput,
args: {
onVariableChange: fn(),
onSubmit: fn(),
},
};
export default meta;
type Story = StoryObj<typeof ResourceTemplateInput>;
export const SingleVariable: Story = {
args: {
template: "user/{id}",
variables: {},
},
};
export const MultipleVariables: Story = {
args: {
template: "org/{orgId}/user/{userId}",
variables: {},
},
};
export const FilledIn: Story = {
args: {
template: "org/{orgId}/user/{userId}",
variables: {
orgId: "acme-corp",
userId: "user-42",
},
},
};

View file

@ -1,74 +0,0 @@
import { Button, Group, Text, TextInput } from "@mantine/core";
export interface ResourceTemplateInputProps {
template: string;
variables: Record<string, string>;
onVariableChange: (name: string, value: string) => void;
onSubmit: () => void;
}
interface TemplatePart {
type: "static" | "variable";
value: string;
}
function parseTemplate(template: string): TemplatePart[] {
const parts: TemplatePart[] = [];
const regex = /\{(\w+)\}/g;
let lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = regex.exec(template)) !== null) {
if (match.index > lastIndex) {
parts.push({
type: "static",
value: template.slice(lastIndex, match.index),
});
}
parts.push({ type: "variable", value: match[1] });
lastIndex = regex.lastIndex;
}
if (lastIndex < template.length) {
parts.push({ type: "static", value: template.slice(lastIndex) });
}
return parts;
}
const StaticPart = Text.withProps({
size: "sm",
pb: 6,
});
export function ResourceTemplateInput({
template,
variables,
onVariableChange,
onSubmit,
}: ResourceTemplateInputProps) {
const parts = parseTemplate(template);
return (
<Group gap="xs" align="flex-end">
{parts.map((part, index) =>
part.type === "static" ? (
<StaticPart key={index}>{part.value}</StaticPart>
) : (
<TextInput
key={part.value}
size="sm"
placeholder={part.value}
value={variables[part.value] ?? ""}
onChange={(e) =>
onVariableChange(part.value, e.currentTarget.value)
}
/>
),
)}
<Button size="sm" onClick={onSubmit}>
Go
</Button>
</Group>
);
}

View file

@ -0,0 +1,48 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { ResourceTemplatePanel } from "./ResourceTemplatePanel";
const meta: Meta<typeof ResourceTemplatePanel> = {
title: "Groups/ResourceTemplatePanel",
component: ResourceTemplatePanel,
args: {
onReadResource: fn(),
},
};
export default meta;
type Story = StoryObj<typeof ResourceTemplatePanel>;
export const SingleVariable: Story = {
args: {
name: "User Profile",
uriTemplate: "file:///users/{userId}/profile",
description: "Fetch a user profile by their unique identifier.",
},
};
export const MultipleVariables: Story = {
args: {
name: "Table Row",
title: "Database Table Row",
uriTemplate: "db://tables/{tableName}/rows/{rowId}",
description: "Access a specific row in a database table.",
},
};
export const WithAnnotations: Story = {
args: {
name: "Dynamic Text Resource",
uriTemplate: "resource://dynamic/{resourceId}",
description:
"Plaintext dynamic resource fabricated from the {resourceId} variable, which must be an integer.",
annotations: { audience: "developer", priority: 0.8 },
},
};
export const NoDescription: Story = {
args: {
name: "Simple Template",
uriTemplate: "file:///data/{filename}",
},
};

View file

@ -0,0 +1,146 @@
import { useState, useMemo } from "react";
import { Button, Group, Stack, Text, TextInput, Title } from "@mantine/core";
import { AnnotationBadge } from "../../elements/AnnotationBadge/AnnotationBadge";
import { CopyButton } from "../../elements/CopyButton/CopyButton";
export interface ResourceTemplatePanelProps {
name: string;
title?: string;
uriTemplate: string;
description?: string;
annotations?: { audience?: string; priority?: number };
onReadResource: (uri: string) => void;
}
function parseVariableNames(uriTemplate: string): string[] {
const names: string[] = [];
const regex = /\{(\w+)\}/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(uriTemplate)) !== null) {
names.push(match[1]);
}
return names;
}
function resolveUri(
uriTemplate: string,
variables: Record<string, string>,
): string {
return uriTemplate.replace(/\{(\w+)\}/g, (_, key: string) => variables[key]);
}
function previewUri(
uriTemplate: string,
variables: Record<string, string>,
): string {
return uriTemplate.replace(/\{(\w+)\}/g, (match, key: string) =>
variables[key]?.length > 0 ? variables[key] : match,
);
}
function priorityLabel(priority: number): string {
if (priority >= 0.7) return "priority: high";
if (priority >= 0.4) return "priority: medium";
return "priority: low";
}
const HeaderRow = Group.withProps({
justify: "space-between",
wrap: "nowrap",
});
const UriGroup = Group.withProps({
gap: "xs",
wrap: "nowrap",
});
const UriText = Text.withProps({
size: "sm",
c: "blue",
truncate: "end",
});
const DescriptionText = Text.withProps({
size: "sm",
c: "dimmed",
});
const FooterRow = Group.withProps({
justify: "space-between",
});
const AnnotationGroup = Group.withProps({
gap: "xs",
});
export function ResourceTemplatePanel({
name,
title,
uriTemplate,
description,
annotations,
onReadResource,
}: ResourceTemplatePanelProps) {
const variableNames = useMemo(
() => parseVariableNames(uriTemplate),
[uriTemplate],
);
const [variables, setVariables] = useState<Record<string, string>>(() =>
Object.fromEntries(variableNames.map((n) => [n, ""])),
);
function handleVariableChange(varName: string, value: string) {
setVariables((prev) => ({ ...prev, [varName]: value }));
}
const canSubmit = variableNames.every((n) => variables[n]?.length > 0);
function handleSubmit() {
onReadResource(resolveUri(uriTemplate, variables));
}
return (
<Stack gap="md">
<HeaderRow>
<Title order={4}>{title ?? name} Template</Title>
<UriGroup>
<UriText>{previewUri(uriTemplate, variables)}</UriText>
<CopyButton value={previewUri(uriTemplate, variables)} />
</UriGroup>
</HeaderRow>
{description && <DescriptionText>{description}</DescriptionText>}
<Stack gap="sm">
{variableNames.map((varName) => (
<TextInput
key={varName}
label={varName}
placeholder={`Enter ${varName}`}
value={variables[varName] ?? ""}
onChange={(e) =>
handleVariableChange(varName, e.currentTarget.value)
}
/>
))}
</Stack>
<FooterRow>
<AnnotationGroup>
{annotations?.audience && (
<AnnotationBadge label={annotations.audience} variant="audience" />
)}
{annotations?.priority !== undefined && (
<AnnotationBadge
label={priorityLabel(annotations.priority)}
variant="priority"
/>
)}
</AnnotationGroup>
<Button size="sm" disabled={!canSubmit} onClick={handleSubmit}>
Read Resource
</Button>
</FooterRow>
</Stack>
);
}

View file

@ -1,9 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { ResourcesScreen } from "./ResourcesScreen";
import type { ResourceListItemProps } from "../../groups/ResourceListItem/ResourceListItem";
import type { ResourceTemplateInputProps } from "../../groups/ResourceTemplateInput/ResourceTemplateInput";
import type { ResourcePreviewPanelProps } from "../../groups/ResourcePreviewPanel/ResourcePreviewPanel";
import type {
ResourceItem,
TemplateListItem,
SelectedResource,
SelectedTemplate,
} from "./ResourcesScreen";
const meta: Meta<typeof ResourcesScreen> = {
title: "Screens/ResourcesScreen",
@ -13,7 +16,11 @@ const meta: Meta<typeof ResourcesScreen> = {
searchText: "",
onSearchChange: fn(),
onRefreshList: fn(),
onSelectResource: fn(),
onSelectUri: fn(),
onSelectTemplate: fn(),
onReadResource: fn(),
onSubscribeResource: fn(),
onUnsubscribeResource: fn(),
listChanged: false,
subscriptions: [],
templates: [],
@ -23,45 +30,41 @@ const meta: Meta<typeof ResourcesScreen> = {
export default meta;
type Story = StoryObj<typeof ResourcesScreen>;
const sampleResources: ResourceListItemProps[] = [
const sampleResources: ResourceItem[] = [
{
name: "config.json",
uri: "file:///config.json",
annotations: { audience: "developer", priority: 0.8 },
selected: false,
onClick: fn(),
},
{
name: "README.md",
uri: "file:///README.md",
selected: false,
onClick: fn(),
},
{
name: "schema.sql",
uri: "file:///schema.sql",
annotations: { priority: 0.5 },
selected: false,
onClick: fn(),
},
];
const sampleTemplates: ResourceTemplateInputProps[] = [
const sampleTemplates: TemplateListItem[] = [
{
template: "file:///users/{userId}/profile",
variables: { userId: "" },
onVariableChange: fn(),
onSubmit: fn(),
name: "User Profile",
uriTemplate: "file:///users/{userId}/profile",
selected: false,
},
{
template: "db://tables/{tableName}/rows/{rowId}",
variables: { tableName: "", rowId: "" },
onVariableChange: fn(),
onSubmit: fn(),
name: "Table Row",
title: "Database Table Row",
uriTemplate: "db://tables/{tableName}/rows/{rowId}",
selected: false,
},
];
const selectedResourceProps: ResourcePreviewPanelProps = {
const selectedResourceData: SelectedResource = {
uri: "file:///config.json",
mimeType: "application/json",
annotations: { audience: "developer", priority: 0.8 },
@ -76,8 +79,12 @@ const selectedResourceProps: ResourcePreviewPanelProps = {
),
lastUpdated: "2026-03-17T10:30:00Z",
isSubscribed: true,
onSubscribe: fn(),
onUnsubscribe: fn(),
};
const selectedTemplateData: SelectedTemplate = {
name: "User Profile",
uriTemplate: "file:///users/{userId}/profile",
description: "Fetch a user profile by their unique identifier.",
};
export const WithResources: Story = {
@ -91,9 +98,13 @@ export const ResourceSelected: Story = {
resources: sampleResources.map((r) =>
r.uri === "file:///config.json" ? { ...r, selected: true } : r,
),
selectedResource: selectedResourceProps,
selectedResource: selectedResourceData,
subscriptions: [
{ name: "config.json", lastUpdated: "2026-03-17T10:30:00Z" },
{
name: "config.json",
uri: "file:///config.json",
lastUpdated: "2026-03-17T10:30:00Z",
},
],
},
};
@ -105,6 +116,42 @@ export const WithTemplates: Story = {
},
};
export const TemplateSelected: Story = {
args: {
resources: sampleResources,
templates: sampleTemplates.map((t) =>
t.uriTemplate === "file:///users/{userId}/profile"
? { ...t, selected: true }
: t,
),
selectedTemplate: selectedTemplateData,
},
};
export const TemplateWithResource: Story = {
args: {
resources: sampleResources,
templates: sampleTemplates.map((t) =>
t.uriTemplate === "file:///users/{userId}/profile"
? { ...t, selected: true }
: t,
),
selectedTemplate: selectedTemplateData,
selectedResource: {
uri: "file:///users/42/profile",
mimeType: "application/json",
annotations: { audience: "developer", priority: 0.8 },
content: JSON.stringify(
{ id: 42, name: "Alice", email: "alice@example.com" },
null,
2,
),
lastUpdated: "2026-03-17T11:15:00Z",
isSubscribed: false,
},
},
};
export const Empty: Story = {
args: {
resources: [],

View file

@ -11,26 +11,61 @@ import {
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { ResourceListItem } from "../../groups/ResourceListItem/ResourceListItem";
import { ResourcePreviewPanel } from "../../groups/ResourcePreviewPanel/ResourcePreviewPanel";
import { ResourceTemplateInput } from "../../groups/ResourceTemplateInput/ResourceTemplateInput";
import type { ResourceListItemProps } from "../../groups/ResourceListItem/ResourceListItem";
import type { ResourcePreviewPanelProps } from "../../groups/ResourcePreviewPanel/ResourcePreviewPanel";
import type { ResourceTemplateInputProps } from "../../groups/ResourceTemplateInput/ResourceTemplateInput";
import { ResourceSubscribedItem } from "../../groups/ResourceSubscribedItem/ResourceSubscribedItem";
import { ResourceTemplatePanel } from "../../groups/ResourceTemplatePanel/ResourceTemplatePanel";
export interface SubscriptionInfo {
export interface ResourceItem {
name: string;
uri: string;
annotations?: { audience?: string; priority?: number };
selected: boolean;
}
export interface TemplateListItem {
name: string;
title?: string;
uriTemplate: string;
selected: boolean;
}
export interface SubscriptionItem {
name: string;
uri: string;
lastUpdated?: string;
}
export interface SelectedResource {
uri: string;
mimeType: string;
annotations?: { audience?: string; priority?: number };
content: string;
lastUpdated?: string;
isSubscribed: boolean;
}
export interface SelectedTemplate {
name: string;
title?: string;
uriTemplate: string;
description?: string;
annotations?: { audience?: string; priority?: number };
}
export interface ResourcesScreenProps {
resources: ResourceListItemProps[];
templates: ResourceTemplateInputProps[];
subscriptions: SubscriptionInfo[];
selectedResource?: ResourcePreviewPanelProps;
resources: ResourceItem[];
templates: TemplateListItem[];
subscriptions: SubscriptionItem[];
selectedResource?: SelectedResource;
selectedTemplate?: SelectedTemplate;
listChanged: boolean;
searchText: string;
onSearchChange: (text: string) => void;
onRefreshList: () => void;
onSelectResource: (uri: string) => void;
onSelectUri: (uri: string) => void;
onSelectTemplate: (uriTemplate: string) => void;
onReadResource: (uri: string) => void;
onSubscribeResource: (uri: string) => void;
onUnsubscribeResource: (uri: string) => void;
}
const PageContainer = Container.withProps({
@ -55,18 +90,12 @@ const EmptyState = Text.withProps({
py: "xl",
});
const SubscriptionMeta = Text.withProps({
span: true,
c: "dimmed",
size: "xs",
});
function formatSectionCount(label: string, count: number): string {
return `${label} (${count})`;
}
function formatLastUpdated(lastUpdated: string): string {
return ` — ${lastUpdated}`;
function templateDisplayName(item: TemplateListItem): string {
return item.title ?? item.name;
}
export function ResourcesScreen({
@ -74,10 +103,16 @@ export function ResourcesScreen({
templates,
subscriptions,
selectedResource,
selectedTemplate,
listChanged,
searchText,
onSearchChange,
onRefreshList,
onSelectUri,
onSelectTemplate,
onReadResource,
onSubscribeResource,
onUnsubscribeResource,
}: ResourcesScreenProps) {
const filteredResources = resources.filter((r) =>
r.name.toLowerCase().includes(searchText.toLowerCase()),
@ -113,7 +148,14 @@ export function ResourcesScreen({
<Accordion.Panel>
<Stack gap="xs">
{filteredResources.map((resource) => (
<ResourceListItem key={resource.uri} {...resource} />
<ResourceListItem
key={resource.uri}
name={resource.name}
uri={resource.uri}
annotations={resource.annotations}
selected={resource.selected}
onClick={() => onSelectUri(resource.uri)}
/>
))}
</Stack>
</Accordion.Panel>
@ -126,9 +168,12 @@ export function ResourcesScreen({
<Accordion.Panel>
<Stack gap="xs">
{templates.map((template) => (
<ResourceTemplateInput
key={template.template}
{...template}
<ResourceListItem
key={template.uriTemplate}
name={templateDisplayName(template)}
uri={template.uriTemplate}
selected={template.selected}
onClick={() => onSelectTemplate(template.uriTemplate)}
/>
))}
</Stack>
@ -142,14 +187,12 @@ export function ResourcesScreen({
<Accordion.Panel>
<Stack gap="xs">
{subscriptions.map((sub) => (
<Text key={sub.name} size="sm">
{sub.name}
{sub.lastUpdated && (
<SubscriptionMeta>
{formatLastUpdated(sub.lastUpdated)}
</SubscriptionMeta>
)}
</Text>
<ResourceSubscribedItem
key={sub.name}
name={sub.name}
lastUpdated={sub.lastUpdated}
onUnsubscribe={() => onUnsubscribeResource(sub.uri)}
/>
))}
</Stack>
</Accordion.Panel>
@ -160,13 +203,47 @@ export function ResourcesScreen({
</Grid.Col>
<Grid.Col span={8}>
<DetailCard>
{selectedResource ? (
<ResourcePreviewPanel {...selectedResource} />
<Stack gap="md">
{selectedTemplate ? (
<>
<DetailCard>
<ResourceTemplatePanel
{...selectedTemplate}
onReadResource={onReadResource}
/>
</DetailCard>
{selectedResource && (
<DetailCard>
<ResourcePreviewPanel
{...selectedResource}
onRefresh={() => onReadResource(selectedResource.uri)}
onSubscribe={() =>
onSubscribeResource(selectedResource.uri)
}
onUnsubscribe={() =>
onUnsubscribeResource(selectedResource.uri)
}
/>
</DetailCard>
)}
</>
) : selectedResource ? (
<DetailCard>
<ResourcePreviewPanel
{...selectedResource}
onRefresh={() => onReadResource(selectedResource.uri)}
onSubscribe={() => onSubscribeResource(selectedResource.uri)}
onUnsubscribe={() =>
onUnsubscribeResource(selectedResource.uri)
}
/>
</DetailCard>
) : (
<EmptyState>Select a resource to preview</EmptyState>
<DetailCard>
<EmptyState>Select a resource to preview</EmptyState>
</DetailCard>
)}
</DetailCard>
</Stack>
</Grid.Col>
</Grid>
</PageContainer>

View file

@ -249,57 +249,66 @@ export const ResourcesActive: Story = {
name: "config.json",
uri: "file:///config.json",
annotations: { audience: "developer", priority: 0.8 },
selected: true,
onClick: fn(),
selected: false,
},
{
name: "README.md",
uri: "file:///README.md",
selected: false,
onClick: fn(),
},
{
name: "schema.sql",
uri: "file:///schema.sql",
annotations: { priority: 0.5 },
selected: false,
onClick: fn(),
},
]}
templates={[
{
template: "file:///users/{userId}/profile",
variables: { userId: "" },
onVariableChange: fn(),
onSubmit: fn(),
name: "User Profile",
uriTemplate: "file:///users/{userId}/profile",
selected: true,
},
{
name: "Table Row",
title: "Database Table Row",
uriTemplate: "db://tables/{tableName}/rows/{rowId}",
selected: false,
},
]}
subscriptions={[
{ name: "config.json", lastUpdated: "2026-03-17T10:30:00Z" },
{
name: "config.json",
uri: "file:///config.json",
lastUpdated: "2026-03-17T10:30:00Z",
},
]}
selectedTemplate={{
name: "User Profile",
uriTemplate: "file:///users/{userId}/profile",
description: "Fetch a user profile by their unique identifier.",
}}
selectedResource={{
uri: "file:///config.json",
uri: "file:///users/42/profile",
mimeType: "application/json",
annotations: { audience: "developer", priority: 0.8 },
content: JSON.stringify(
{
name: "my-project",
version: "1.0.0",
settings: { debug: true, logLevel: "info" },
},
{ id: 42, name: "Alice", email: "alice@example.com" },
null,
2,
),
lastUpdated: "2026-03-17T10:30:00Z",
isSubscribed: true,
onSubscribe: fn(),
onUnsubscribe: fn(),
lastUpdated: "2026-03-17T11:15:00Z",
isSubscribed: false,
}}
listChanged={false}
searchText=""
onSearchChange={fn()}
onRefreshList={fn()}
onSelectResource={fn()}
onSelectUri={fn()}
onSelectTemplate={fn()}
onReadResource={fn()}
onSubscribeResource={fn()}
onUnsubscribeResource={fn()}
/>
),
},

View file

@ -1,6 +1,10 @@
import { UnstyledButton } from "@mantine/core";
export const ThemeUnstyledButton = UnstyledButton.extend({
classNames: (_theme, props) => {
if (props.variant === "listItem") return { root: "list-item" };
return {};
},
styles: (_theme, props) => {
if (props.variant === "listItem") {
return { root: { borderRadius: "var(--mantine-radius-md)" } };

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB