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:
parent
ee89ec9beb
commit
a0e763a315
2 changed files with 520 additions and 200 deletions
|
|
@ -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}",
|
||||
},
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue