@bestcodetools/graphql-playground 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.dockerignore ADDED
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ dist
3
+ .git
4
+ .github
5
+ .vscode
6
+ coverage
7
+ npm-debug.log
8
+ Dockerfile*
@@ -0,0 +1,48 @@
1
+ name: Docker Image
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ docker:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+
15
+ steps:
16
+ - name: Checkout
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Set up QEMU
20
+ uses: docker/setup-qemu-action@v3
21
+
22
+ - name: Set up Docker Buildx
23
+ uses: docker/setup-buildx-action@v3
24
+
25
+ - name: Log in to Docker Hub
26
+ uses: docker/login-action@v3
27
+ with:
28
+ registry: docker.io
29
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
30
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
31
+
32
+ - name: Extract Docker metadata
33
+ id: meta
34
+ uses: docker/metadata-action@v5
35
+ with:
36
+ images: douglasdomingues/graphql-playground
37
+ tags: |
38
+ type=raw,value=latest
39
+ type=ref,event=tag
40
+
41
+ - name: Build and push multi-arch image
42
+ uses: docker/build-push-action@v6
43
+ with:
44
+ context: .
45
+ push: true
46
+ platforms: linux/amd64,linux/arm64
47
+ tags: ${{ steps.meta.outputs.tags }}
48
+ labels: ${{ steps.meta.outputs.labels }}
@@ -0,0 +1,22 @@
1
+ name: Publish Package to npmjs
2
+ on:
3
+ release:
4
+ types: [published]
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-node@v4
14
+ with:
15
+ node-version: '20.x'
16
+ registry-url: 'https://registry.npmjs.org'
17
+ - run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
18
+ - run: npm ci
19
+ - run: npm publish --provenance --access public
20
+ env:
21
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
22
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
package/Dockerfile ADDED
@@ -0,0 +1,28 @@
1
+ FROM node:22-bookworm-slim AS build
2
+ WORKDIR /app
3
+
4
+ COPY package*.json ./
5
+ RUN npm install
6
+
7
+ COPY tsconfig.json ./
8
+ COPY src ./src
9
+ COPY public ./public
10
+
11
+ RUN npm run build
12
+
13
+ FROM node:22-bookworm-slim AS runtime
14
+ WORKDIR /app
15
+
16
+ ENV NODE_ENV=production
17
+ ENV PLAYGROUND_PORT=3000
18
+ ENV PLAYGROUND_LIVE_RELOAD=false
19
+
20
+ COPY package*.json ./
21
+ RUN npm install --omit=dev
22
+
23
+ COPY --from=build /app/dist ./dist
24
+ COPY --from=build /app/public ./public
25
+
26
+ EXPOSE 3000
27
+
28
+ CMD ["node", "dist/standalone.js"]
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # GraphQL Playground
2
+
3
+ A standalone GraphQL Playground package with a custom dark UI, schema explorer, smart editors, workspace import/export, and Docker support.
4
+
5
+ ## What This Package Is For
6
+
7
+ This package serves a browser-based GraphQL playground that helps developers:
8
+
9
+ - write and run GraphQL queries, mutations, and subscriptions
10
+ - inspect the schema in a side panel
11
+ - edit variables and headers with guided autocomplete
12
+ - view formatted JSON responses with syntax highlighting
13
+ - save, import, and export full workspaces with multiple tabs
14
+
15
+ It is designed to be embedded into an Express application or run as a standalone local server.
16
+
17
+ ## Features
18
+
19
+ - standalone Express server for quick local usage
20
+ - custom schema viewer with search and hover details
21
+ - query editor with GraphQL autocomplete and inline tooltips
22
+ - variables editor with schema-aware suggestions
23
+ - headers editor with common header suggestions
24
+ - response viewer with JSON syntax highlighting
25
+ - multi-tab workspace with import/export support
26
+ - Docker image support for a compiled runtime
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install @bestcodetools/graphql-playground
32
+ ```
33
+
34
+ ## Standalone Usage
35
+
36
+ For development:
37
+
38
+ ```bash
39
+ npm run standalone
40
+ ```
41
+
42
+ The playground will be available at:
43
+
44
+ ```text
45
+ http://localhost:3000/playground
46
+ ```
47
+
48
+ You can change the port with:
49
+
50
+ ```bash
51
+ PLAYGROUND_PORT=4000 npm run standalone
52
+ ```
53
+
54
+ ## Compiled Runtime
55
+
56
+ To run the compiled standalone server without `ts-node-dev`:
57
+
58
+ ```bash
59
+ npm run build
60
+ npm start
61
+ ```
62
+
63
+ ## Docker Usage
64
+
65
+ Build the image:
66
+
67
+ ```bash
68
+ docker build -t graphql-playground .
69
+ ```
70
+
71
+ Run the container:
72
+
73
+ ```bash
74
+ docker run -p 3000:3000 graphql-playground
75
+ ```
76
+
77
+ Then open:
78
+
79
+ ```text
80
+ http://localhost:3000/playground
81
+ ```
82
+
83
+ ## Available Scripts
84
+
85
+ - `npm run standalone`: starts the standalone server with `ts-node-dev`
86
+ - `npm run build`: transpiles TypeScript into `dist`
87
+ - `npm start`: runs the transpiled standalone server
88
+ - `npm test`: runs the Jest test suite
89
+ - `npm run transpile:sass:watch`: watches and transpiles Sass files
90
+
91
+ ## Testing
92
+
93
+ This package includes a basic integration test for the standalone server.
94
+
95
+ Run:
96
+
97
+ ```bash
98
+ npm test
99
+ ```
100
+
101
+ The test verifies that the standalone server starts on an automatically assigned port and responds with `200` on the configured playground path.
102
+
103
+ ## Notes
104
+
105
+ - The standalone runtime disables live reload in production mode.
106
+ - Workspace export sanitizes sensitive header values such as authorization, token, and key headers by replacing them with placeholders.
107
+
108
+ ## License
109
+
110
+ ISC
package/jest.config.js ADDED
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/tests'],
5
+ testMatch: ['**/*.test.ts'],
6
+ moduleFileExtensions: ['ts', 'js', 'json'],
7
+ clearMocks: true
8
+ };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@bestcodetools/graphql-playground",
3
+ "version": "0.0.0",
4
+ "description": "A standalone GraphQL Playground package with a custom dark UI, schema explorer, smart editors, workspace import/export, and Docker support.",
5
+ "keywords": [],
6
+ "homepage": "https://github.com/BestCodeTools/graphql-playground#readme",
7
+ "bugs": {
8
+ "url": "https://github.com/BestCodeTools/graphql-playground/issues"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/BestCodeTools/graphql-playground.git"
13
+ },
14
+ "license": "ISC",
15
+ "author": "BestCodeTools",
16
+ "type": "commonjs",
17
+ "main": "index.js",
18
+ "directories": {
19
+ "test": "tests"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc --outDir dist",
23
+ "start": "node dist/standalone.js",
24
+ "standalone": "ts-node-dev --transpile-only src/standalone.ts",
25
+ "transpile:sass:watch": "sass --watch public/styles:public/styles",
26
+ "test": "jest"
27
+ },
28
+ "dependencies": {
29
+ "express": "^4.21.2"
30
+ },
31
+ "devDependencies": {
32
+ "@types/connect-livereload": "^0.6.3",
33
+ "@types/express": "^5.0.0",
34
+ "@types/jest": "^29.5.14",
35
+ "@types/livereload": "^0.9.5",
36
+ "@types/node": "^22.13.4",
37
+ "connect-livereload": "^0.6.1",
38
+ "jest": "^29.7.0",
39
+ "livereload": "^0.9.3",
40
+ "sass": "^1.85.0",
41
+ "ts-jest": "^29.2.5",
42
+ "ts-node": "^10.9.2",
43
+ "ts-node-dev": "^2.0.0",
44
+ "typescript": "^5.7.3"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }
@@ -0,0 +1,38 @@
1
+ <div class="config-tabs">
2
+ <div class="tab-item" ng-repeat="tab in $ctrl.tabs track by tab.id"
3
+ ng-class="{ 'active': $ctrl.activeTab === tab.id }"
4
+ ng-click="$ctrl.setActiveTab(tab.id)">
5
+ {{ $ctrl.t(tab.titleKey) }}
6
+ </div>
7
+ </div>
8
+ <div class="config-content">
9
+ <div ng-if="$ctrl.activeTab === 'headers'">
10
+ <h3>{{ $ctrl.t('config.shared_headers') }}</h3>
11
+ <table class="headers-table">
12
+ <thead>
13
+ <tr>
14
+ <th class="property-name">{{ $ctrl.t('config.property') }}</th>
15
+ <th class="property-value">{{ $ctrl.t('config.value') }}</th>
16
+ <th class="remove-action"></th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ <tr ng-repeat="header in $ctrl.headers">
21
+ <td class="property-name"><input placeholder="{{ $ctrl.t('config.key_placeholder') }}" type="text" ng-model="header.key"></td>
22
+ <td class="property-value"><input placeholder="{{ $ctrl.t('config.value_placeholder') }}" type="text" ng-model="header.value"></td>
23
+ <td class="remove-action"><button ng-click="$ctrl.removeHeader($index)">🗑️</button></td>
24
+ </tr>
25
+ </tbody>
26
+ </table>
27
+ <button ng-click="$ctrl.addHeader()">{{ $ctrl.t('config.add_header') }}</button>
28
+ </div>
29
+ <div ng-if="$ctrl.activeTab === 'other'">
30
+ <h3>{{ $ctrl.t('config.other') }}</h3>
31
+ <p class="workspace-actions-note">{{ $ctrl.getWorkspaceLabel('note') }}</p>
32
+ <div class="workspace-actions">
33
+ <button type="button" ng-click="$ctrl.handleExportWorkspace()">{{ $ctrl.getWorkspaceLabel('export') }}</button>
34
+ <button type="button" ng-click="$ctrl.triggerImportWorkspace()">{{ $ctrl.getWorkspaceLabel('import') }}</button>
35
+ <input id="workspace-import-input" class="workspace-import-input" type="file" accept=".json,application/json" onchange="angular.element(this).scope().$ctrl.handleImportWorkspaceFile(event)">
36
+ </div>
37
+ </div>
38
+ </div>
@@ -0,0 +1,16 @@
1
+ <div class="field-args" ng-if="$ctrl.hasArgs()">
2
+ <span class="field-args-paren">(</span>
3
+ <span class="field-arg" ng-repeat="arg in $ctrl.fieldArgs track by arg.name || $index">
4
+ <span class="field-arg-name" ng-mouseover="$ctrl.showArgTooltip($event, arg)" ng-mouseout="$ctrl.hideTooltip()" on-show-tooltip="$ctrl.forwardShowTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()">{{ arg.name }}</span>
5
+ <span class="colon">:</span>
6
+ <field-type field-ref="arg" on-show-tooltip="$ctrl.forwardShowTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()"></field-type>
7
+ <span class="field-arg-default" ng-if="arg.defaultValue">
8
+ <span class="equal">=</span> {{ arg.defaultValue }}
9
+ </span>
10
+ <span class="field-arg-description" ng-if="arg.description" ng-mouseover="$ctrl.showArgTooltip($event, arg)" ng-mouseout="$ctrl.hideTooltip()" on-show-tooltip="$ctrl.forwardShowTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()">
11
+ {{ arg.description }}
12
+ </span>
13
+ <span class="field-arg-separator" ng-if="!$last">, </span>
14
+ </span>
15
+ <span class="field-args-paren">)</span>
16
+ </div>
@@ -0,0 +1,27 @@
1
+ <span class="field-type-wrapper" ng-if="$ctrl.getTypeRef()">
2
+ <span ng-if="$ctrl.isListType($ctrl.getTypeRef())">
3
+ <span class="field-type-prefix">[</span>
4
+ <field-type field-ref="$ctrl.getTypeRef().ofType" on-show-tooltip="$ctrl.forwardShowTooltip($event, payload)" on-hide-tooltip="$ctrl.forwardHideTooltip()"></field-type>
5
+ <span class="field-type-suffix">]</span>
6
+ </span>
7
+ <span ng-if="$ctrl.isNonNullType($ctrl.getTypeRef())">
8
+ <field-type field-ref="$ctrl.getTypeRef().ofType" on-show-tooltip="$ctrl.forwardShowTooltip($event, payload)" on-hide-tooltip="$ctrl.forwardHideTooltip()"></field-type>
9
+ <span class="field-type-non-null">!</span>
10
+ </span>
11
+ <span ng-if="!$ctrl.isListType($ctrl.getTypeRef()) && !$ctrl.isNonNullType($ctrl.getTypeRef()) && $ctrl.hasNavigableType($ctrl.getTypeRef())">
12
+ <a
13
+ class="field-type-link"
14
+ href="#{{ $ctrl.getTypeAnchor($ctrl.getTypeRef()) }}"
15
+ ng-click="$ctrl.onTypeAnchorClick($event)"
16
+ ng-mouseover="$ctrl.showTooltip($event)"
17
+ ng-mouseout="$ctrl.hideTooltip()"
18
+ on-show-tooltip="$ctrl.forwardShowTooltip($event, payload)"
19
+ on-hide-tooltip="$ctrl.hideTooltip()"
20
+ >
21
+ <span class="field-type">{{ $ctrl.getTypeLabel($ctrl.getTypeRef()) }}</span>
22
+ </a>
23
+ </span>
24
+ <span class="field-type" ng-if="!$ctrl.isListType($ctrl.getTypeRef()) && !$ctrl.isNonNullType($ctrl.getTypeRef()) && !$ctrl.hasNavigableType($ctrl.getTypeRef())" ng-mouseover="$ctrl.showTooltip($event)" ng-mouseout="$ctrl.hideTooltip()" on-show-tooltip="$ctrl.forwardShowTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()">
25
+ {{ $ctrl.getTypeLabel($ctrl.getTypeRef()) }}
26
+ </span>
27
+ </span>
@@ -0,0 +1,3 @@
1
+ <div class="headers-editor-container">
2
+ <textarea aria-label="Request headers editor" ng-model="$ctrl.headers"></textarea>
3
+ </div>
@@ -0,0 +1,3 @@
1
+ <div class="query-editor-container">
2
+ <textarea aria-label="GraphQL query editor" ng-model="$ctrl.query"></textarea>
3
+ </div>
@@ -0,0 +1,3 @@
1
+ <div class="response-viewer-container">
2
+ <textarea aria-label="GraphQL response viewer" ng-model="$ctrl.result"></textarea>
3
+ </div>
@@ -0,0 +1,74 @@
1
+ <div class="schema-viewer-wrapper" ng-class="{ 'open': $ctrl.open }">
2
+ <button class="btn btn-toggle-schema-viewer" ng-click="$ctrl.toggleSchemaViewer()">Schema</button>
3
+ <div class="schema-viewer-error" ng-show="$ctrl.loadError">
4
+ <div>
5
+ {{ $ctrl.t('schema.error') }}
6
+ </div>
7
+ <div class="error-details">
8
+ {{ $ctrl.loadError }}
9
+ </div>
10
+ <button class="btn schema-download-retry" ng-click="$ctrl.retryLoadSchema()">{{ $ctrl.t('schema.retry') }}</button>
11
+ </div>
12
+ <div class="schema-viewer-panel">
13
+ <div class="editor-panel-title schema-panel-title">
14
+ <span>{{ $ctrl.t('schema.title') }}</span>
15
+ </div>
16
+ <div class="schema-panel-toolbar">
17
+ <input type="text" class="schema-search-input" ng-model="$ctrl.searchTerm" placeholder="{{ $ctrl.t('schema.search_placeholder') }}">
18
+ <div class="schema-panel-note">{{ $ctrl.t('schema.note') }}</div>
19
+ </div>
20
+ <div class="schema-panel-body">
21
+ <div class="type-definition" id="{{type.kind}}_{{type.name||type.ofType.name}}"
22
+ ng-repeat="type in $ctrl.getFilteredTypes() track by (type.kind + '_' + type.name)">
23
+ <div ng-if="type.kind === 'OBJECT'">
24
+ <span class="keyword">type</span> {{ type.name }} <span class="bracket">{</span>
25
+ <div class="field" ng-repeat="field in type.fields" ng-if="$ctrl.shouldShowField(type, field)">
26
+ <span class="field-name" ng-class="{ 'is-actionable': $ctrl.getOperationForType(type.name) }" ng-mousedown="$ctrl.onFieldMouseDown($event, type, field)" ng-mouseover="$ctrl.showFieldTooltip($event, field)" ng-mouseout="$ctrl.hideTooltip()">{{ field.name }}</span><field-args field-args="field.args" on-show-tooltip="$ctrl.forwardTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()"></field-args><span class="colon">:</span> <field-type field-ref="field" on-show-tooltip="$ctrl.forwardTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()"></field-type>
27
+ <!--<span ng-if="field.type.kind==='LIST'" class="array-bracket">[</span><a href="#{{field.type.kind}}_{{field.type.name}}" ng-click="$ctrl.onTypeAnchorClick($event)"><span class="field-type">{{ field.type.name || field.type.ofType.name }}</span></a><span ng-if="field.type.kind==='LIST'" class="array-bracket">]</span> -->
28
+ </div>
29
+ <span class="bracket">}</span>
30
+ </div>
31
+ <div ng-if="type.kind === 'ENUM'">
32
+ <span class="keyword">enum</span> {{ type.name }} <span class="bracket">{</span>
33
+ <div class="field" ng-repeat="value in type.enumValues">
34
+ <span class="enum-name" ng-mouseover="$ctrl.showEnumTooltip($event, value)" ng-mouseout="$ctrl.hideTooltip()">{{ value.name }}</span>
35
+ </div>
36
+ <span class="bracket">}</span>
37
+ </div>
38
+ <div ng-if="type.kind === 'INTERFACE'">
39
+ <span class="keyword">interface</span> {{ type.name }} <span class="bracket">{</span>
40
+ <div class="field" ng-repeat="field in type.fields" ng-if="$ctrl.shouldShowField(type, field)">
41
+ <span class="field-name" ng-class="{ 'is-actionable': $ctrl.getOperationForType(type.name) }" ng-mousedown="$ctrl.onFieldMouseDown($event, type, field)" ng-mouseover="$ctrl.showFieldTooltip($event, field)" ng-mouseout="$ctrl.hideTooltip()">{{ field.name }}</span><field-args field-args="field.args" on-show-tooltip="$ctrl.forwardTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()"></field-args><span class="colon">:</span> <field-type field-ref="field" on-show-tooltip="$ctrl.forwardTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()"></field-type>
42
+ </div>
43
+ <span class="bracket">}</span>
44
+ </div>
45
+ <div ng-if="type.kind === 'UNION'">
46
+ <span class="keyword">union</span> {{ type.name }} <span class="equal">=</span>
47
+ <div class="field" ng-repeat="possibleType in type.possibleTypes" ng-if="$ctrl.shouldShowField(type, possibleType)">
48
+ <span class="field-name" ng-mouseover="$ctrl.showPossibleTypeTooltip($event, possibleType)" ng-mouseout="$ctrl.hideTooltip()">{{ possibleType.name }}</span>
49
+ </div>
50
+ </div>
51
+ <div ng-if="type.kind === 'INPUT_OBJECT'">
52
+ <span class="keyword">input</span> {{ type.name }} <span class="bracket">{</span>
53
+ <div class="field" ng-repeat="inputField in type.inputFields" ng-if="$ctrl.shouldShowField(type, inputField)">
54
+ <span class="field-name" ng-mouseover="$ctrl.showInputFieldTooltip($event, inputField)" ng-mouseout="$ctrl.hideTooltip()">{{ inputField.name }}</span><span class="colon">:</span> <field-type field-ref="inputField" on-show-tooltip="$ctrl.forwardTooltip($event, payload)" on-hide-tooltip="$ctrl.hideTooltip()"></field-type>
55
+ </div>
56
+ <span class="bracket">}</span>
57
+ </div>
58
+ <div ng-if="type.kind === 'SCALAR'">
59
+ <span class="keyword">scalar</span> {{ type.name }}
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ <div class="schema-tooltip" ng-if="$ctrl.tooltip.visible" ng-style="{ top: $ctrl.tooltip.top + 'px', left: $ctrl.tooltip.left + 'px' }">
65
+ <div class="schema-tooltip-signature">
66
+ <span class="schema-tooltip-name schema-tooltip-name-{{ $ctrl.tooltip.labelClass }}">{{ $ctrl.tooltip.label }}</span>
67
+ <span class="colon" ng-if="$ctrl.tooltip.typeTokens.length">:</span>
68
+ <span class="schema-tooltip-type-tokens" ng-if="$ctrl.tooltip.typeTokens.length">
69
+ <span ng-repeat="token in $ctrl.tooltip.typeTokens track by $index" class="{{ token.className }}">{{ token.text }}</span>
70
+ </span>
71
+ </div>
72
+ <div class="schema-tooltip-description" ng-if="$ctrl.tooltip.description">{{ $ctrl.tooltip.description }}</div>
73
+ </div>
74
+ </div>
@@ -0,0 +1,3 @@
1
+ <div class="variables-editor-container">
2
+ <textarea aria-label="GraphQL variables editor" ng-model="$ctrl.variables"></textarea>
3
+ </div>
@@ -0,0 +1,114 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" ng-app="app">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Playground</title>
7
+ <link rel="stylesheet" href="./styles/main.css?v=20260409r">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/codemirror.min.css">
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/hint/show-hint.min.css">
11
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/codemirror.min.js"></script>
13
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/edit/matchbrackets.min.js"></script>
14
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/hint/show-hint.min.js"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
16
+ <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
17
+ <script src="./js/main.js?v=20260409p"></script>
18
+ <script src="./js/components/config-editor.js?v=20260409e"></script>
19
+ <script src="./js/components/query-editor.js?v=20260409t"></script>
20
+ <script src="./js/components/variables-editor.js?v=20260409k"></script>
21
+ <script src="./js/components/headers-editor.js?v=20260409i"></script>
22
+ <script src="./js/components/response-viewer.js?v=20260409c"></script>
23
+ <script src="./js/components/schema-viewer.js?v=20260409g"></script>
24
+ </head>
25
+ <body ng-class="{ 'ready': main.ready }" ng-controller="MainController as main">
26
+ <nav class="top-nav">
27
+ <div class="top-nav-brand">
28
+ <div class="top-nav-title-row">
29
+ <div class="top-nav-title">{{ main.t('app.name') }}</div>
30
+ <span class="top-nav-badge">{{ main.appVersion }}</span>
31
+ </div>
32
+ <div class="top-nav-subtitle">{{ main.t('app.developed_by') }}</div>
33
+ </div>
34
+ <div class="top-nav-actions">
35
+ <label class="locale-label" for="locale-select">{{ main.t('nav.language') }}</label>
36
+ <select id="locale-select" class="locale-select" ng-model="main.locale" ng-change="main.setLocale(main.locale)" ng-options="locale.code as main.t(locale.labelKey) for locale in main.locales"></select>
37
+ <button class="config-btn" ng-click="main.openConfig()">{{ main.t('nav.settings') }}</button>
38
+ </div>
39
+ </nav>
40
+
41
+ <div class="editor">
42
+ <div class="input-group">
43
+ <input class="form-control" type="text" ng-model="main.url" placeholder="URL">
44
+ <button ng-click="main.send()">{{ main.t('actions.send') }}</button>
45
+ </div>
46
+ <div class="tabs">
47
+ <div class="tab-item" ng-repeat="tab in main.tabs track by tab.id" ng-class="{ 'active': main.activeTab === $index }" ng-mousedown="main.handleTabMouseDown($event, $index)">
48
+ <a href="" ng-if="!tab.isEditingTitle" ng-click="main.activeTab = $index" ng-dblclick="main.beginTabTitleEdit($index, $event)">{{ tab.title }}</a>
49
+ <input
50
+ ng-if="tab.isEditingTitle"
51
+ id="tab-title-editor-{{ tab.id }}"
52
+ class="tab-title-input"
53
+ type="text"
54
+ ng-model="tab.titleDraft"
55
+ ng-click="$event.stopPropagation()"
56
+ ng-blur="main.commitTabTitleEdit($index)"
57
+ ng-keydown="main.handleTabTitleKeydown($event, $index)">
58
+ <button type="button" class="tab-close-btn" ng-if="main.tabs.length > 1" ng-click="main.closeTab($index); $event.stopPropagation()" aria-label="Close tab">&times;</button>
59
+ </div>
60
+ <div class="add-tab">
61
+ <a href="" ng-click="main.addTab()">+</a>
62
+ </div>
63
+ </div>
64
+ <div class="tab-content" ng-repeat="tab in main.tabs" ng-show="main.activeTab === $index">
65
+ <div class="query-and-result-container">
66
+ <div class="query-editor">
67
+ <div class="editor-panel-title">
68
+ <span>{{ main.t('editor.query') }}</span>
69
+ <span class="editor-panel-actions">
70
+ <button type="button" class="editor-panel-action" ng-click="tab.queryEditorApi && tab.queryEditorApi.resetToOperation('query')">{{ main.t('actions.new_query') }}</button>
71
+ <button type="button" class="editor-panel-action" ng-click="tab.queryEditorApi && tab.queryEditorApi.resetToOperation('mutation')">{{ main.t('actions.new_mutation') }}</button>
72
+ </span>
73
+ </div>
74
+ <query-editor query="tab.query" schema="main.schema" api="tab.queryEditorApi" on-change="main.persistTabs()"></query-editor>
75
+ </div>
76
+ <div class="actions">
77
+ <button type="button" class="send-fab" ng-click="main.send()" aria-label="{{ main.t('actions.send') }}" title="{{ main.t('actions.send') }}">
78
+ &#9654;
79
+ </button>
80
+ </div>
81
+ <div class="result">
82
+ <div class="editor-panel-title">{{ main.t('editor.response') }}</div>
83
+ <response-viewer result="tab.result" api="tab.responseViewerApi"></response-viewer>
84
+ </div>
85
+ </div>
86
+ <div class="variables-and-headers-container">
87
+ <div class="variables-editor">
88
+ <div class="editor-panel-title">
89
+ <span>{{ main.t('editor.variables') }}</span>
90
+ <button type="button" class="editor-panel-action" ng-click="tab.variablesEditorApi && tab.variablesEditorApi.format()">{{ main.t('actions.format') }}</button>
91
+ </div>
92
+ <variables-editor variables="tab.variables" query="tab.query" schema="main.schema" api="tab.variablesEditorApi" on-change="main.persistTabs()"></variables-editor>
93
+ </div>
94
+ <div class="headers-editor">
95
+ <div class="editor-panel-title">
96
+ <span>{{ main.t('editor.headers') }}</span>
97
+ <button type="button" class="editor-panel-action" ng-click="tab.headersEditorApi && tab.headersEditorApi.format()">{{ main.t('actions.format') }}</button>
98
+ </div>
99
+ <headers-editor headers="tab.headers" api="tab.headersEditorApi" on-change="main.persistTabs()"></headers-editor>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ <schema-viewer schema="main.schema" load-error="main.loadSchemaError" retry-load-schema="main.loadSchema()" insert-schema-operation="main.insertSchemaOperation(snippet)"></schema-viewer>
105
+ <div class="modal-overlay" ng-show="main.showConfig"></div>
106
+ <div class="modal config-modal" ng-show="main.showConfig">
107
+ <div class="modal-content">
108
+ <h2>{{ main.t('modal.settings') }}</h2>
109
+ <config-editor export-workspace="main.downloadWorkspace()" import-workspace="main.importWorkspace(workspace)"></config-editor>
110
+ <button class="close-btn" ng-click="main.closeConfig()">{{ main.t('actions.close') }}</button>
111
+ </div>
112
+ </div>
113
+ </body>
114
+ </html>