diff --git a/clients/web/src/App.css b/clients/web/src/App.css index f4857672..673797e9 100644 --- a/clients/web/src/App.css +++ b/clients/web/src/App.css @@ -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 { diff --git a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.stories.tsx b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.stories.tsx index 6a8b52cb..bc125901 100644 --- a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.stories.tsx +++ b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.stories.tsx @@ -6,6 +6,7 @@ const meta: Meta = { title: "Groups/ResourcePreviewPanel", component: ResourcePreviewPanel, args: { + onRefresh: fn(), onSubscribe: fn(), onUnsubscribe: fn(), }, diff --git a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx index e5b2ebb6..1ba0ef28 100644 --- a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx +++ b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx @@ -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 ? ( {formatLastUpdated(lastUpdated)} ) : ( - + )} {mimeType} - - + + {annotations?.audience && ( )} @@ -106,12 +122,17 @@ export function ResourcePreviewPanel({ variant="priority" /> )} - - - + + + + + + ); } diff --git a/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.stories.tsx b/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.stories.tsx new file mode 100644 index 00000000..684bfb36 --- /dev/null +++ b/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { fn } from "storybook/test"; +import { ResourceSubscribedItem } from "./ResourceSubscribedItem"; + +const meta: Meta = { + title: "Groups/ResourceSubscribedItem", + component: ResourceSubscribedItem, + args: { + onUnsubscribe: fn(), + }, +}; + +export default meta; +type Story = StoryObj; + +export const WithTimestamp: Story = { + args: { + name: "config.json", + lastUpdated: "2026-03-17T10:30:00Z", + }, +}; + +export const WithoutTimestamp: Story = { + args: { + name: "schema.sql", + }, +}; diff --git a/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.tsx b/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.tsx new file mode 100644 index 00000000..fafbc6d7 --- /dev/null +++ b/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.tsx @@ -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 ( + + + {name} + {lastUpdated && {lastUpdated}} + + Unsubscribe + + ); +} diff --git a/clients/web/src/components/groups/ResourceTemplateInput/ResourceTemplateInput.stories.tsx b/clients/web/src/components/groups/ResourceTemplateInput/ResourceTemplateInput.stories.tsx deleted file mode 100644 index 0d5e78cd..00000000 --- a/clients/web/src/components/groups/ResourceTemplateInput/ResourceTemplateInput.stories.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { fn } from "storybook/test"; -import { ResourceTemplateInput } from "./ResourceTemplateInput"; - -const meta: Meta = { - title: "Groups/ResourceTemplateInput", - component: ResourceTemplateInput, - args: { - onVariableChange: fn(), - onSubmit: fn(), - }, -}; - -export default meta; -type Story = StoryObj; - -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", - }, - }, -}; diff --git a/clients/web/src/components/groups/ResourceTemplateInput/ResourceTemplateInput.tsx b/clients/web/src/components/groups/ResourceTemplateInput/ResourceTemplateInput.tsx deleted file mode 100644 index c639e0da..00000000 --- a/clients/web/src/components/groups/ResourceTemplateInput/ResourceTemplateInput.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Button, Group, Text, TextInput } from "@mantine/core"; - -export interface ResourceTemplateInputProps { - template: string; - variables: Record; - 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 ( - - {parts.map((part, index) => - part.type === "static" ? ( - {part.value} - ) : ( - - onVariableChange(part.value, e.currentTarget.value) - } - /> - ), - )} - - - ); -} diff --git a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.stories.tsx b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.stories.tsx new file mode 100644 index 00000000..291935e1 --- /dev/null +++ b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.stories.tsx @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { fn } from "storybook/test"; +import { ResourceTemplatePanel } from "./ResourceTemplatePanel"; + +const meta: Meta = { + title: "Groups/ResourceTemplatePanel", + component: ResourceTemplatePanel, + args: { + onReadResource: fn(), + }, +}; + +export default meta; +type Story = StoryObj; + +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}", + }, +}; diff --git a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx new file mode 100644 index 00000000..31d50f42 --- /dev/null +++ b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx @@ -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 { + return uriTemplate.replace(/\{(\w+)\}/g, (_, key: string) => variables[key]); +} + +function previewUri( + uriTemplate: string, + variables: Record, +): 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>(() => + 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 ( + + + {title ?? name} Template + + {previewUri(uriTemplate, variables)} + + + + {description && {description}} + + {variableNames.map((varName) => ( + + handleVariableChange(varName, e.currentTarget.value) + } + /> + ))} + + + + {annotations?.audience && ( + + )} + {annotations?.priority !== undefined && ( + + )} + + + + + ); +} diff --git a/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.stories.tsx b/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.stories.tsx index 9d33e4fe..7bc77191 100644 --- a/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.stories.tsx +++ b/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.stories.tsx @@ -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 = { title: "Screens/ResourcesScreen", @@ -13,7 +16,11 @@ const meta: Meta = { 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 = { export default meta; type Story = StoryObj; -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: [], diff --git a/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx b/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx index 02323a3d..d69cc002 100644 --- a/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx +++ b/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx @@ -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({ {filteredResources.map((resource) => ( - + onSelectUri(resource.uri)} + /> ))} @@ -126,9 +168,12 @@ export function ResourcesScreen({ {templates.map((template) => ( - onSelectTemplate(template.uriTemplate)} /> ))} @@ -142,14 +187,12 @@ export function ResourcesScreen({ {subscriptions.map((sub) => ( - - {sub.name} - {sub.lastUpdated && ( - - {formatLastUpdated(sub.lastUpdated)} - - )} - + onUnsubscribeResource(sub.uri)} + /> ))} @@ -160,13 +203,47 @@ export function ResourcesScreen({ - - {selectedResource ? ( - + + {selectedTemplate ? ( + <> + + + + {selectedResource && ( + + onReadResource(selectedResource.uri)} + onSubscribe={() => + onSubscribeResource(selectedResource.uri) + } + onUnsubscribe={() => + onUnsubscribeResource(selectedResource.uri) + } + /> + + )} + + ) : selectedResource ? ( + + onReadResource(selectedResource.uri)} + onSubscribe={() => onSubscribeResource(selectedResource.uri)} + onUnsubscribe={() => + onUnsubscribeResource(selectedResource.uri) + } + /> + ) : ( - Select a resource to preview + + Select a resource to preview + )} - + diff --git a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx index db043bf5..6b0c2e58 100644 --- a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx +++ b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx @@ -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()} /> ), }, diff --git a/clients/web/src/theme/UnstyledButton.ts b/clients/web/src/theme/UnstyledButton.ts index bc2641cd..fd37cf5b 100644 --- a/clients/web/src/theme/UnstyledButton.ts +++ b/clients/web/src/theme/UnstyledButton.ts @@ -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)" } }; diff --git a/specification/screenshots/v1-resource-screen.png b/specification/screenshots/v1-resource-screen.png new file mode 100644 index 00000000..19feb8bb Binary files /dev/null and b/specification/screenshots/v1-resource-screen.png differ diff --git a/specification/screenshots/v1-resource-template-read.png b/specification/screenshots/v1-resource-template-read.png new file mode 100644 index 00000000..356fe06a Binary files /dev/null and b/specification/screenshots/v1-resource-template-read.png differ diff --git a/specification/screenshots/v1-resource-template-selected.png b/specification/screenshots/v1-resource-template-selected.png new file mode 100644 index 00000000..ca642812 Binary files /dev/null and b/specification/screenshots/v1-resource-template-selected.png differ diff --git a/specification/screenshots/v1-resource-uri-selected.png b/specification/screenshots/v1-resource-uri-selected.png new file mode 100644 index 00000000..bb1d13b0 Binary files /dev/null and b/specification/screenshots/v1-resource-uri-selected.png differ diff --git a/specification/screenshots/v2-resources-screen.png b/specification/screenshots/v2-resources-screen.png new file mode 100644 index 00000000..8f8d680e Binary files /dev/null and b/specification/screenshots/v2-resources-screen.png differ