diff --git a/packages/graphiql-console/src/App.tsx b/packages/graphiql-console/src/App.tsx
index 0e6b1cb2542..c2a4746a640 100644
--- a/packages/graphiql-console/src/App.tsx
+++ b/packages/graphiql-console/src/App.tsx
@@ -1,12 +1,16 @@
+import {GraphiQLSection} from './sections/GraphiQL/index.ts'
+
import React from 'react'
import {AppProvider} from '@shopify/polaris'
import '@shopify/polaris/build/esm/styles.css'
+import 'graphiql/style.css'
+import 'graphiql/setup-workers/vite'
function App() {
return (
- GraphiQL Console
+
)
}
diff --git a/packages/graphiql-console/src/sections/GraphiQL/GraphiQL.module.scss b/packages/graphiql-console/src/sections/GraphiQL/GraphiQL.module.scss
new file mode 100644
index 00000000000..a2f5a09fd5a
--- /dev/null
+++ b/packages/graphiql-console/src/sections/GraphiQL/GraphiQL.module.scss
@@ -0,0 +1,127 @@
+.Container {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ width: 100%;
+}
+
+.ErrorBanner {
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+
+.Header {
+ display: grid;
+ grid-template-columns: 1fr; // Single column on narrow screens
+ align-items: center;
+ padding: 16px;
+ border-bottom: 1px solid var(--p-color-border, #e1e3e5);
+ background: #ffffff;
+ gap: 16px;
+}
+
+.LeftSection {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.RightSection {
+ justify-self: start; // Left-align by default (single column on narrow screens)
+}
+
+// Right-align only on wide screens with two-column layout
+@media only screen and (min-width: 1081px) {
+ .RightSection {
+ justify-self: end;
+ }
+}
+
+.StatusSection {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.ControlsSection {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.ScopesNote {
+ display: inline-flex;
+ align-items: center;
+ height: 100%;
+}
+
+.LinksSection {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ a {
+ line-height: 0;
+
+ &:hover .Polaris-Text--root {
+ text-decoration: underline;
+ }
+
+ span.Polaris-Text--root {
+ max-width: max(12vw, 150px);
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+ }
+}
+
+.GraphiQLContainer {
+ flex-grow: 1;
+ overflow: hidden;
+
+ // Ensure GraphiQL component fills container
+ > div {
+ height: 100%;
+ }
+}
+
+// Icon shrinking utility
+.with-shrunk-icon .Polaris-Icon {
+ height: 1rem;
+ width: 1rem;
+ margin: 0.125rem;
+}
+
+// Responsive design
+@media only screen and (max-width: 1550px) {
+ .StatusSection,
+ .ControlsSection,
+ .LinksSection {
+ // Hide labels like "Status:", "API version:", "Store:", "App:"
+ :global(.top-bar-section-title) {
+ display: none;
+ }
+ }
+}
+
+@media only screen and (max-width: 1150px) {
+ .LinksSection a span.Polaris-Text--root {
+ max-width: max(12vw, 140px);
+ }
+}
+
+// Switch to two-column layout at wider screens
+@media only screen and (min-width: 1081px) {
+ .Header {
+ grid-template-columns: 7fr 5fr;
+ }
+}
+
+@media only screen and (max-width: 650px) {
+ .LinksSection a span.Polaris-Text--root {
+ max-width: 17vw;
+ }
+}
diff --git a/packages/graphiql-console/src/sections/GraphiQL/GraphiQL.test.tsx b/packages/graphiql-console/src/sections/GraphiQL/GraphiQL.test.tsx
new file mode 100644
index 00000000000..2703fdaadd7
--- /dev/null
+++ b/packages/graphiql-console/src/sections/GraphiQL/GraphiQL.test.tsx
@@ -0,0 +1,219 @@
+import {GraphiQLSection} from './GraphiQL.tsx'
+import React from 'react'
+import {render, screen, fireEvent} from '@testing-library/react'
+import {describe, test, expect, vi, beforeEach} from 'vitest'
+import {AppProvider} from '@shopify/polaris'
+import type {ServerStatus} from '@/hooks/useServerStatus'
+
+// Mock the hooks
+const mockUseServerStatus = vi.fn()
+vi.mock('@/hooks/useServerStatus', () => ({
+ useServerStatus: (options: any) => mockUseServerStatus(options),
+}))
+
+// Mock child components
+vi.mock('@/components/StatusBadge/StatusBadge.tsx', () => ({
+ StatusBadge: ({status}: {status: ServerStatus}) =>
{JSON.stringify(status)}
,
+}))
+
+vi.mock('@/components/ErrorBanner/ErrorBanner.tsx', () => ({
+ ErrorBanner: ({isVisible}: {isVisible: boolean}) => (
+
+ ErrorBanner
+
+ ),
+}))
+
+vi.mock('@/components/LinkPills/LinkPills.tsx', () => ({
+ LinkPills: ({status}: {status: ServerStatus}) => {JSON.stringify(status)}
,
+}))
+
+vi.mock('@/components/ApiVersionSelector/ApiVersionSelector.tsx', () => ({
+ ApiVersionSelector: ({
+ versions,
+ value,
+ onChange,
+ }: {
+ versions: string[]
+ value: string
+ onChange: (version: string) => void
+ }) => (
+
+
+
+ ),
+}))
+
+vi.mock('@/components/GraphiQLEditor/GraphiQLEditor.tsx', () => ({
+ GraphiQLEditor: ({config, apiVersion}: {config: any; apiVersion: string}) => (
+
+ {JSON.stringify(config)}
+
+ ),
+}))
+
+// Helper to wrap components in AppProvider
+function renderWithProvider(element: React.ReactElement) {
+ return render({element})
+}
+
+describe('', () => {
+ beforeEach(() => {
+ // Reset mocks before each test
+
+ // Default mock implementation
+ mockUseServerStatus.mockReturnValue({
+ serverIsLive: true,
+ appIsInstalled: true,
+ storeFqdn: 'test-store.myshopify.com',
+ appName: 'Test App',
+ appUrl: 'http://localhost:3000',
+ })
+
+ // Mock window.__GRAPHIQL_CONFIG__
+ ;(window as any).__GRAPHIQL_CONFIG__ = undefined
+ })
+
+ test('renders all child components', () => {
+ renderWithProvider()
+
+ expect(screen.getByTestId('status-badge')).toBeDefined()
+ expect(screen.getByTestId('link-pills')).toBeDefined()
+ expect(screen.getByTestId('api-version-selector')).toBeDefined()
+ expect(screen.getByTestId('graphiql-editor')).toBeDefined()
+ })
+
+ test('ErrorBanner visible when serverIsLive=false', () => {
+ mockUseServerStatus.mockReturnValue({
+ serverIsLive: false,
+ appIsInstalled: true,
+ })
+
+ renderWithProvider()
+ const errorBanner = screen.getByTestId('error-banner')
+
+ expect(errorBanner).toBeDefined()
+ expect(errorBanner.getAttribute('data-visible')).toBe('true')
+ })
+
+ test('ErrorBanner not rendered when serverIsLive=true', () => {
+ mockUseServerStatus.mockReturnValue({
+ serverIsLive: true,
+ appIsInstalled: true,
+ })
+
+ renderWithProvider()
+
+ // ErrorBanner should not be in DOM when server is live
+ expect(screen.queryByTestId('error-banner')).toBeNull()
+ })
+
+ test('passes correct props to StatusBadge', () => {
+ const mockStatus: ServerStatus = {
+ serverIsLive: true,
+ appIsInstalled: true,
+ storeFqdn: 'test-store.myshopify.com',
+ appName: 'Test App',
+ appUrl: 'http://localhost:3000',
+ }
+ mockUseServerStatus.mockReturnValue(mockStatus)
+
+ renderWithProvider()
+ const statusBadge = screen.getByTestId('status-badge')
+
+ expect(statusBadge).toBeDefined()
+ expect(statusBadge.textContent).toContain('"serverIsLive":true')
+ expect(statusBadge.textContent).toContain('"appIsInstalled":true')
+ })
+
+ test('passes correct props to LinkPills', () => {
+ const mockStatus: ServerStatus = {
+ serverIsLive: true,
+ appIsInstalled: true,
+ storeFqdn: 'test-store.myshopify.com',
+ appName: 'Test App',
+ appUrl: 'http://localhost:3000',
+ }
+ mockUseServerStatus.mockReturnValue(mockStatus)
+
+ renderWithProvider()
+ const linkPills = screen.getByTestId('link-pills')
+
+ expect(linkPills).toBeDefined()
+ expect(linkPills.textContent).toContain('test-store.myshopify.com')
+ })
+
+ test('getConfig() reads window.__GRAPHIQL_CONFIG__', () => {
+ const customConfig = {
+ baseUrl: 'http://localhost:4000',
+ apiVersion: '2023-01',
+ apiVersions: ['2023-01'],
+ appName: 'Custom App',
+ appUrl: 'http://localhost:3000',
+ storeFqdn: 'custom.myshopify.com',
+ }
+
+ ;(window as any).__GRAPHIQL_CONFIG__ = customConfig
+
+ renderWithProvider()
+ const editor = screen.getByTestId('graphiql-editor')
+
+ expect(editor).toBeDefined()
+ const editorConfig = JSON.parse(editor.textContent ?? '{}')
+ expect(editorConfig.baseUrl).toBe('http://localhost:4000')
+ expect(editorConfig.appName).toBe('Custom App')
+
+ // Cleanup
+ ;(window as any).__GRAPHIQL_CONFIG__ = undefined
+ })
+
+ test('getConfig() falls back to defaults in development', () => {
+ // Ensure no global config
+ ;(window as any).__GRAPHIQL_CONFIG__ = undefined
+
+ renderWithProvider()
+ const editor = screen.getByTestId('graphiql-editor')
+
+ expect(editor).toBeDefined()
+ const editorConfig = JSON.parse(editor.textContent ?? '{}')
+
+ // Should have default values
+ expect(editorConfig.apiVersion).toBe('2024-10')
+ expect(editorConfig.apiVersions).toEqual(['2024-01', '2024-04', '2024-07', '2024-10', 'unstable'])
+ })
+
+ test('ApiVersionSelector receives correct versions and value', () => {
+ renderWithProvider()
+ const selector = screen.getByTestId('api-version-selector')
+
+ expect(selector).toBeDefined()
+ expect(selector.getAttribute('data-versions')).toBe('2024-01,2024-04,2024-07,2024-10,unstable')
+ expect(selector.getAttribute('data-value')).toBe('2024-10')
+ })
+
+ test('calls useServerStatus with correct baseUrl', () => {
+ renderWithProvider()
+
+ expect(mockUseServerStatus).toHaveBeenCalledWith(
+ expect.objectContaining({
+ baseUrl: expect.any(String),
+ }),
+ )
+ })
+
+ test('version selection updates GraphiQL editor', () => {
+ renderWithProvider()
+
+ // Initial state
+ let editor = screen.getByTestId('graphiql-editor')
+ expect(editor.getAttribute('data-api-version')).toBe('2024-10')
+
+ // Trigger version change
+ const button = screen.getByText('Change Version')
+ fireEvent.click(button)
+
+ // Re-find after state update
+ editor = screen.getByTestId('graphiql-editor')
+ expect(editor.getAttribute('data-api-version')).toBe('new-version')
+ })
+})
diff --git a/packages/graphiql-console/src/sections/GraphiQL/GraphiQL.tsx b/packages/graphiql-console/src/sections/GraphiQL/GraphiQL.tsx
new file mode 100644
index 00000000000..e655b16dc79
--- /dev/null
+++ b/packages/graphiql-console/src/sections/GraphiQL/GraphiQL.tsx
@@ -0,0 +1,92 @@
+import * as styles from './GraphiQL.module.scss'
+import React, {useState, useMemo} from 'react'
+import {Text} from '@shopify/polaris'
+import type {GraphiQLConfig} from '@/types/config.ts'
+import {useServerStatus} from '@/hooks/useServerStatus.ts'
+import {StatusBadge} from '@/components/StatusBadge/StatusBadge.tsx'
+import {ErrorBanner} from '@/components/ErrorBanner/ErrorBanner.tsx'
+import {LinkPills} from '@/components/LinkPills/LinkPills.tsx'
+import {ApiVersionSelector} from '@/components/ApiVersionSelector/ApiVersionSelector.tsx'
+import {GraphiQLEditor} from '@/components/GraphiQLEditor/GraphiQLEditor.tsx'
+import {validateConfig} from '@/utils/configValidation.ts'
+
+// Helper to get config from window or fallback to env/defaults
+// Security: Validates window.__GRAPHIQL_CONFIG__ to prevent XSS attacks
+function getConfig(): GraphiQLConfig {
+ // Fallback config for development
+ const fallbackConfig: GraphiQLConfig = {
+ baseUrl: import.meta.env.VITE_GRAPHIQL_BASE_URL ?? 'http://localhost:3457',
+ apiVersion: '2024-10',
+ apiVersions: ['2024-01', '2024-04', '2024-07', '2024-10', 'unstable'],
+ appName: 'Development App',
+ appUrl: 'http://localhost:3000',
+ storeFqdn: 'test-store.myshopify.com',
+ }
+
+ if (typeof window !== 'undefined' && window.__GRAPHIQL_CONFIG__) {
+ // SECURITY: Validate and sanitize config before use
+ return validateConfig(window.__GRAPHIQL_CONFIG__, fallbackConfig)
+ }
+
+ return fallbackConfig
+}
+
+export function GraphiQLSection() {
+ const config = useMemo(() => getConfig(), [])
+ const [selectedVersion, setSelectedVersion] = useState(config.apiVersion)
+
+ const status = useServerStatus({baseUrl: config.baseUrl})
+
+ const handleVersionChange = (version: string) => {
+ setSelectedVersion(version)
+ // GraphiQL component (Track 6) will use this version in fetcher URL
+ }
+
+ return (
+
+ {/* Error banner - shown when server disconnects */}
+ {!status.serverIsLive && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ GraphiQL runs on the same access scopes you've defined in the TOML file for your app.
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/graphiql-console/src/sections/GraphiQL/index.ts b/packages/graphiql-console/src/sections/GraphiQL/index.ts
new file mode 100644
index 00000000000..66528cdf05f
--- /dev/null
+++ b/packages/graphiql-console/src/sections/GraphiQL/index.ts
@@ -0,0 +1 @@
+export {GraphiQLSection} from './GraphiQL.tsx'