-
- History
- {(clearStatus || hasItems) && (
-
- )}
-
+
{hasFavorites && (
@@ -166,6 +171,10 @@ export const HistoryItem: FC = props => {
toggleFavorite(props.item);
};
+ const variablesSnippet = props.item.variables
+ ? formatVariables(props.item.variables)
+ : null;
+
return (
-
{isEditable ? (
@@ -193,54 +202,71 @@ export const HistoryItem: FC = props => {
>
) : (
<>
-
-
- {displayName}
-
-
-
-
-
-
-
-
-
+
+
+
+
+ {displayName}
+
+
+
+ {variablesSnippet && (
+
+
+ {variablesSnippet}
+
+
+ )}
+
+
+
+
+
+
+
+
- {props.item.favorite ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
-
+
+ {props.item.favorite ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
>
)}
@@ -256,3 +282,19 @@ export function formatQuery(query?: string) {
.replaceAll('}', ' } ')
.replaceAll(/[\s]{2,}/g, ' ');
}
+
+export function formatVariables(variables: string) {
+ try {
+ const parsed = JSON.parse(variables) as Record;
+ const entries = Object.entries(parsed);
+ if (!entries.length) {
+ return null;
+ }
+ return entries
+ .slice(0, 3)
+ .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
+ .join(', ');
+ } catch {
+ return variables.slice(0, 60);
+ }
+}
diff --git a/packages/graphiql-plugin-history/src/history.stories.tsx b/packages/graphiql-plugin-history/src/history.stories.tsx
new file mode 100644
index 00000000000..2efee2e1bf5
--- /dev/null
+++ b/packages/graphiql-plugin-history/src/history.stories.tsx
@@ -0,0 +1,265 @@
+import type { ReactNode } from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import {
+ GraphiQLProvider,
+ Tooltip,
+ PanelHeader,
+ Button,
+} from '@graphiql/react';
+import { History, HistoryItem } from './components';
+import { HistoryStore } from './context';
+import './style.css';
+
+// ---------------------------------------------------------------------------
+// Shared decorator: History items need GraphiQLProvider + HistoryStore
+// ---------------------------------------------------------------------------
+
+function withHistoryContext(children: ReactNode) {
+ return (
+
+ Promise.resolve({ data: {} })}>
+
+
+ {children}
+
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Meta
+// ---------------------------------------------------------------------------
+
+const meta: Meta = {
+ title: 'Plugins/History',
+ tags: ['autodocs'],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+// ---------------------------------------------------------------------------
+// PanelHeader (no context required)
+// ---------------------------------------------------------------------------
+
+export const Header: Story = {
+ render: () => (
+
+ ),
+};
+
+// ---------------------------------------------------------------------------
+// Single item — query only
+// ---------------------------------------------------------------------------
+
+export const ItemQueryOnly: Story = {
+ render: () =>
+ withHistoryContext(
+ ,
+ ),
+};
+
+// ---------------------------------------------------------------------------
+// Single item — with variables snippet
+// ---------------------------------------------------------------------------
+
+export const ItemWithVariables: Story = {
+ render: () =>
+ withHistoryContext(
+ ,
+ ),
+};
+
+// ---------------------------------------------------------------------------
+// Favorite item
+// ---------------------------------------------------------------------------
+
+export const ItemFavorite: Story = {
+ render: () =>
+ withHistoryContext(
+ ,
+ ),
+};
+
+// ---------------------------------------------------------------------------
+// Custom label
+// ---------------------------------------------------------------------------
+
+export const ItemWithLabel: Story = {
+ render: () =>
+ withHistoryContext(
+ ,
+ ),
+};
+
+// ---------------------------------------------------------------------------
+// Empty state (full History component)
+// ---------------------------------------------------------------------------
+
+export const Empty: Story = {
+ render: () => withHistoryContext(),
+};
+
+// ---------------------------------------------------------------------------
+// Few rows
+// ---------------------------------------------------------------------------
+
+export const FewRows: Story = {
+ render: () =>
+ withHistoryContext(
+ ,
+ ),
+};
+
+// ---------------------------------------------------------------------------
+// Many rows
+// ---------------------------------------------------------------------------
+
+const manyItems = Array.from({ length: 10 }, (_, i) => ({
+ query: `query Query${i + 1} { node(id: "${i + 1}") { id ... on User { name } } }`,
+ operationName: `Query${i + 1}`,
+ favorite: false,
+}));
+
+export const ManyRows: Story = {
+ render: () =>
+ withHistoryContext(
+
+ {manyItems.map(item => (
+
+ ))}
+
,
+ ),
+};
+
+// ---------------------------------------------------------------------------
+// Mixed: favorites + regular + with variables
+// ---------------------------------------------------------------------------
+
+export const Mixed: Story = {
+ render: () =>
+ withHistoryContext(
+ <>
+
+
+
+ >,
+ ),
+};
diff --git a/packages/graphiql-plugin-history/src/style.css b/packages/graphiql-plugin-history/src/style.css
index ac536135f29..7d6e977623c 100644
--- a/packages/graphiql-plugin-history/src/style.css
+++ b/packages/graphiql-plugin-history/src/style.css
@@ -1,44 +1,29 @@
-.graphiql-history-header {
- font-size: var(--font-size-h2);
- font-weight: var(--font-weight-medium);
+.graphiql-history {
display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.graphiql-history-header button {
- font-size: var(--font-size-inline-code);
- padding: var(--px-6) var(--px-10);
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
}
.graphiql-history-items {
- margin: var(--px-16) 0 0;
+ margin: 0;
list-style: none;
padding: 0;
+ overflow-y: auto;
+ flex: 1;
}
.graphiql-history-item {
- border-radius: var(--border-radius-4);
- color: hsla(var(--color-neutral), var(--alpha-secondary));
+ border-bottom: 1px solid oklch(var(--border-default));
display: flex;
- font-size: var(--font-size-inline-code);
- font-family: var(--font-family-mono);
- height: 34px;
+ align-items: stretch;
&:hover {
- color: hsl(var(--color-neutral));
- background-color: hsla(var(--color-neutral), var(--alpha-background-light));
- }
-
- &:not(:first-child) {
- margin-top: var(--px-4);
+ background-color: oklch(var(--accent-blue) / 0.06);
}
&.editable {
- background-color: hsla(
- var(--color-primary),
- var(--alpha-background-medium)
- );
+ background-color: oklch(var(--accent-blue) / 0.08);
& > input {
background: transparent;
@@ -46,27 +31,27 @@
flex: 1;
margin: 0;
outline: none;
- padding: 0 var(--px-10);
+ padding: 0 var(--px-14);
width: 100%;
+ color: oklch(var(--fg-default));
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-mono);
&::placeholder {
- color: hsla(var(--color-neutral), var(--alpha-secondary));
+ color: oklch(var(--fg-disabled));
}
}
& > button {
- color: hsl(var(--color-primary));
+ color: oklch(var(--accent-blue));
padding: 0 var(--px-10);
&:active {
- background-color: hsla(
- var(--color-primary),
- var(--alpha-background-heavy)
- );
+ background-color: oklch(var(--accent-blue) / 0.12);
}
&:focus {
- outline: hsl(var(--color-primary)) auto 1px;
+ outline: oklch(var(--accent-blue)) auto 1px;
}
& > svg {
@@ -76,30 +61,92 @@
}
}
-button.graphiql-history-item-label {
+.graphiql-history-item-inner {
+ padding: var(--px-8) var(--px-14);
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
flex: 1;
- padding: var(--px-8) var(--px-10);
+}
+
+.graphiql-history-item-row {
+ display: flex;
+ align-items: center;
+ gap: var(--px-6);
+ min-width: 0;
+}
+
+.graphiql-history-item-status {
+ flex-shrink: 0;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background-color: oklch(var(--accent-green));
+}
+
+.graphiql-history-item-status.error {
+ background-color: oklch(var(--accent-red));
+}
+
+.graphiql-history-item-label {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: oklch(var(--fg-default));
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-mono);
+ text-align: left;
+}
+
+.graphiql-history-item-meta {
+ display: flex;
+ align-items: center;
+ gap: var(--px-6);
+ min-width: 0;
+ padding-left: 12px;
+}
+
+.graphiql-history-item-variables {
+ flex: 1;
+ min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ color: oklch(var(--fg-subtle));
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-eyebrow);
+}
+
+.graphiql-history-item-actions {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ padding-right: var(--px-6);
}
-button.graphiql-history-item-action {
+.graphiql-history-item-action {
align-items: center;
- color: hsla(var(--color-neutral), var(--alpha-secondary));
+ color: oklch(var(--fg-disabled));
display: flex;
- padding: var(--px-8) var(--px-6);
+ padding: var(--px-6);
+ border-radius: var(--radius-sm);
&:hover {
- color: hsl(var(--color-neutral));
+ color: oklch(var(--fg-default));
+ background-color: oklch(var(--fg-default) / 0.06);
}
& > svg {
- height: 14px;
- width: 14px;
+ height: 12px;
+ width: 12px;
}
}
.graphiql-history-item-spacer {
- height: var(--px-16);
+ height: 1px;
+ background-color: oklch(var(--border-strong));
+ margin: var(--px-8) 0;
}
diff --git a/packages/graphiql-react/.storybook/main.ts b/packages/graphiql-react/.storybook/main.ts
index dddd2d457b7..cbe952ed5bf 100644
--- a/packages/graphiql-react/.storybook/main.ts
+++ b/packages/graphiql-react/.storybook/main.ts
@@ -1,7 +1,10 @@
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
- stories: ['../src/**/*.stories.@(ts|tsx)'],
+ stories: [
+ '../src/**/*.stories.@(ts|tsx)',
+ '../../graphiql-plugin-history/src/**/*.stories.@(ts|tsx)',
+ ],
addons: ['@storybook/addon-a11y', '@storybook/addon-vitest'],
framework: { name: '@storybook/react-vite', options: {} },
};
diff --git a/packages/graphiql/cypress/e2e/history.cy.ts b/packages/graphiql/cypress/e2e/history.cy.ts
index e38a5b2598a..da9b7d48675 100644
--- a/packages/graphiql/cypress/e2e/history.cy.ts
+++ b/packages/graphiql/cypress/e2e/history.cy.ts
@@ -82,7 +82,7 @@ describe('history', () => {
cy.get('ul.graphiql-history-items li').should('have.length', 2);
cy.get(
- '.graphiql-history-item:nth-child(2) > button[aria-label="Delete from history"]',
+ '.graphiql-history-item:nth-child(2) button[aria-label="Delete from history"]',
).click();
cy.get('.graphiql-history-item').should('have.length', 1);
});
@@ -95,7 +95,7 @@ describe('history', () => {
cy.get('button[aria-label="Show History"]').click();
cy.get('ul.graphiql-history-items li').should('have.length', 2);
- cy.get('.graphiql-history-header > button:last-child').click();
+ cy.get('.graphiql-panel-header-actions button:last-child').click();
cy.get('.graphiql-history-item').should('have.length', 0);
});
@@ -113,7 +113,7 @@ describe('history', () => {
const items = '.graphiql-history ul:last-of-type .graphiql-history-item';
cy.get(
- '.graphiql-history-item:nth-child(2) > button[aria-label="Add favorite"]',
+ '.graphiql-history-item:nth-child(2) button[aria-label="Add favorite"]',
).click();
cy.get('.graphiql-history ul').should('have.length', 2); // favorites and items
cy.get(favorites).should('have.length', 1);
@@ -121,7 +121,7 @@ describe('history', () => {
cy.get('.graphiql-history-item-label').eq(0).should('have.text', 'Test'); // favorite so now at top of a list
cy.get(
- '.graphiql-history-item:nth-child(1) > button[aria-label="Remove favorite"]',
+ '.graphiql-history-item:nth-child(1) button[aria-label="Remove favorite"]',
).click();
cy.get('.graphiql-history ul').should('have.length', 1); // just items
cy.get(items).should('have.length', 2);