mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
Enhancement/hive lab lib (#7697)
This commit is contained in:
parent
5cb956ca23
commit
1bf05f048f
108 changed files with 6688 additions and 2016 deletions
5
.changeset/strong-hornets-jog.md
Normal file
5
.changeset/strong-hornets-jog.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@graphql-hive/laboratory': major
|
||||||
|
---
|
||||||
|
|
||||||
|
First release
|
||||||
5
.changeset/young-papayas-hear.md
Normal file
5
.changeset/young-papayas-hear.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@graphql-hive/render-laboratory': major
|
||||||
|
---
|
||||||
|
|
||||||
|
Render laboratory util for yoga
|
||||||
|
|
@ -109,6 +109,8 @@ module.exports = {
|
||||||
'packages/migrations/**',
|
'packages/migrations/**',
|
||||||
// We bundle it all anyway, so there are no node_modules
|
// We bundle it all anyway, so there are no node_modules
|
||||||
'packages/web/app/**',
|
'packages/web/app/**',
|
||||||
|
// We bundle it all anyway, so there are no node_modules
|
||||||
|
'packages/libraries/laboratory/**',
|
||||||
'**/*.spec.ts',
|
'**/*.spec.ts',
|
||||||
'**/*.test.ts',
|
'**/*.test.ts',
|
||||||
'**/*.e2e.ts',
|
'**/*.e2e.ts',
|
||||||
|
|
|
||||||
6
.github/actions/setup/action.yml
vendored
6
.github/actions/setup/action.yml
vendored
|
|
@ -54,6 +54,12 @@ runs:
|
||||||
working-directory: ${{ inputs.workingDirectory }}
|
working-directory: ${{ inputs.workingDirectory }}
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: build laboratory
|
||||||
|
shell: bash
|
||||||
|
if: ${{ inputs.buildLaboratory == 'true' }}
|
||||||
|
working-directory: ${{ inputs.workingDirectory }}
|
||||||
|
run: pnpm turbo build --filter=./packages/libraries/laboratory --color
|
||||||
|
|
||||||
- name: generate graphql types
|
- name: generate graphql types
|
||||||
if: ${{ inputs.codegen == 'true' }}
|
if: ${{ inputs.codegen == 'true' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
|
||||||
1
.github/workflows/typescript-typecheck.yaml
vendored
1
.github/workflows/typescript-typecheck.yaml
vendored
|
|
@ -15,6 +15,7 @@ jobs:
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
actor: typescript-typecheck
|
actor: typescript-typecheck
|
||||||
|
buildLaboratory: true
|
||||||
|
|
||||||
- name: get cpu count
|
- name: get cpu count
|
||||||
id: cpu-cores
|
id: cpu-cores
|
||||||
|
|
|
||||||
|
|
@ -287,12 +287,7 @@ describe('Execution', () => {
|
||||||
parseSpecialCharSequences: false,
|
parseSpecialCharSequences: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
cy.dataCy('env-editor-mini').within(() => {
|
setMonacoEditorContents('env-editor-mini', '{"foo":"injected"}');
|
||||||
cy.get('textarea').type('{"foo":"injected"}', {
|
|
||||||
force: true,
|
|
||||||
parseSpecialCharSequences: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.intercept({
|
cy.intercept({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,7 @@
|
||||||
"ip": "npm:neoip@2.1.0",
|
"ip": "npm:neoip@2.1.0",
|
||||||
"miniflare@3>undici": "^7.18.2",
|
"miniflare@3>undici": "^7.18.2",
|
||||||
"tailwindcss": "3.4.17",
|
"tailwindcss": "3.4.17",
|
||||||
|
"@graphql-hive/laboratory>tailwindcss": "4.1.18",
|
||||||
"@hive/app>tailwindcss": "4.1.18",
|
"@hive/app>tailwindcss": "4.1.18",
|
||||||
"@tailwindcss/node>tailwindcss": "4.1.18",
|
"@tailwindcss/node>tailwindcss": "4.1.18",
|
||||||
"@tailwindcss/vite>tailwindcss": "4.1.18",
|
"@tailwindcss/vite>tailwindcss": "4.1.18",
|
||||||
|
|
@ -155,6 +156,7 @@
|
||||||
"seroval@<1.4.1": "^1.4.1",
|
"seroval@<1.4.1": "^1.4.1",
|
||||||
"fast-xml-parser@<5.3.8": "^5.3.8",
|
"fast-xml-parser@<5.3.8": "^5.3.8",
|
||||||
"minimatch@10.x.x": "^10.2.2",
|
"minimatch@10.x.x": "^10.2.2",
|
||||||
|
"amqplib": "^0.8.0",
|
||||||
"minimatch@9.x.x": "^9.0.6",
|
"minimatch@9.x.x": "^9.0.6",
|
||||||
"minimatch@3.x.x": "^3.1.3",
|
"minimatch@3.x.x": "^3.1.3",
|
||||||
"minimatch@4.x.x": "^4.2.4",
|
"minimatch@4.x.x": "^4.2.4",
|
||||||
|
|
@ -179,7 +181,8 @@
|
||||||
"bentocache": "patches/bentocache.patch",
|
"bentocache": "patches/bentocache.patch",
|
||||||
"nextra": "patches/nextra.patch",
|
"nextra": "patches/nextra.patch",
|
||||||
"nextra-theme-docs": "patches/nextra-theme-docs.patch",
|
"nextra-theme-docs": "patches/nextra-theme-docs.patch",
|
||||||
"@graphql-codegen/schema-ast": "patches/@graphql-codegen__schema-ast.patch"
|
"@graphql-codegen/schema-ast": "patches/@graphql-codegen__schema-ast.patch",
|
||||||
|
"@fastify/vite": "patches/@fastify__vite.patch"
|
||||||
},
|
},
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"msw"
|
"msw"
|
||||||
|
|
|
||||||
22
packages/libraries/laboratory/components.json
Normal file
22
packages/libraries/laboratory/components.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/laboratory/components",
|
||||||
|
"utils": "@/laboratory/lib/utils",
|
||||||
|
"ui": "@/laboratory/components/ui",
|
||||||
|
"lib": "@/laboratory/lib",
|
||||||
|
"hooks": "@/laboratory/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
16
packages/libraries/laboratory/index.html
Normal file
16
packages/libraries/laboratory/index.html
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="h-full w-full">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Hive Laboratory</title>
|
||||||
|
|
||||||
|
<link href="/src/index.css" rel="stylesheet" />
|
||||||
|
<!-- <script src="https://cdn.jsdelivr.net/npm/react-scan/dist/auto.global.js"></script> -->
|
||||||
|
</head>
|
||||||
|
<body class="dark h-full w-full">
|
||||||
|
<div id="root" class="h-full w-full"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
145
packages/libraries/laboratory/package.json
Normal file
145
packages/libraries/laboratory/package.json
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
{
|
||||||
|
"name": "@graphql-hive/laboratory",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/hive-laboratory.cjs.js",
|
||||||
|
"module": "./dist/hive-laboratory.es.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/hive-laboratory.es.js",
|
||||||
|
"require": "./dist/hive-laboratory.cjs.js"
|
||||||
|
},
|
||||||
|
"./umd": "./dist/hive-laboratory.umd.js"
|
||||||
|
},
|
||||||
|
"jsdelivr": "./dist/hive-laboratory.umd.js",
|
||||||
|
"unpkg": "./dist/hive-laboratory.umd.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build --config vite.lib.config.ts && vite build --config vite.umd.config.ts",
|
||||||
|
"dev": "vite",
|
||||||
|
"dev:electron": "VITE_TARGET=electron concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tanstack/react-form": "^1.23.8",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"graphql-ws": "^6.0.6",
|
||||||
|
"lucide-react": "^0.548.0",
|
||||||
|
"lz-string": "^1.5.0",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"zod": "^4.1.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@dagrejs/dagre": "^1.1.8",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@eslint/js": "^9.39.2",
|
||||||
|
"@mlc-ai/web-llm": "^0.2.80",
|
||||||
|
"@monaco-editor/react": "4.8.0-rc.2",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@rollup/plugin-commonjs": "^29.0.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||||
|
"@tailwindcss/cli": "^4.1.18",
|
||||||
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@tanstack/react-form": "^1.27.7",
|
||||||
|
"@tanstack/react-router": "^1.154.13",
|
||||||
|
"@tanstack/react-router-devtools": "^1.154.13",
|
||||||
|
"@tanstack/router-plugin": "^1.154.13",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/lodash": "^4.17.23",
|
||||||
|
"@types/node": "^24.10.9",
|
||||||
|
"@types/react": "18.3.18",
|
||||||
|
"@types/react-dom": "18.3.5",
|
||||||
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
|
"@xyflow/react": "^12.10.0",
|
||||||
|
"autoprefixer": "^10.4.23",
|
||||||
|
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"color": "^5.0.3",
|
||||||
|
"concurrently": "^9.2.1",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"esbuild": "^0.25.12",
|
||||||
|
"eslint": "^9.39.2",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.26",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"graphql": "^16.12.0",
|
||||||
|
"graphql-ws": "^6.0.6",
|
||||||
|
"lodash": "^4.17.23",
|
||||||
|
"lucide-react": "^0.548.0",
|
||||||
|
"lz-string": "^1.5.0",
|
||||||
|
"monaco-editor": "^0.52.2",
|
||||||
|
"monaco-graphql": "^1.7.3",
|
||||||
|
"monacopilot": "^1.2.12",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"postcss-prefixwrap": "^1.57.2",
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"react-resizable-panels": "^3.0.6",
|
||||||
|
"react-shadow": "^20.6.0",
|
||||||
|
"rollup-plugin-typescript2": "^0.36.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
|
"tailwindcss-scoped-preflight": "^3.5.7",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"tsup": "^8.5.1",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.53.1",
|
||||||
|
"unplugin-dts": "1.0.0-beta.6",
|
||||||
|
"vite": "npm:rolldown-vite@7.1.14",
|
||||||
|
"vite-plugin-commonjs": "^0.10.4",
|
||||||
|
"vite-plugin-dts": "^4.5.4",
|
||||||
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
|
"wait-on": "^9.0.3",
|
||||||
|
"zod": "^4.3.6"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://registry.npmjs.org",
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"sideEffects": [
|
||||||
|
"**/*.css"
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"appId": "com.guild.hive.laboratory",
|
||||||
|
"productName": "Hive Laboratory",
|
||||||
|
"artifactName": "HiveLab-${version}-Do_Not_Open_(Seriously_It's_Pre-Release)-${os}-${arch}.${ext}",
|
||||||
|
"files": [
|
||||||
|
"dist/**/*",
|
||||||
|
"electron/**/*"
|
||||||
|
],
|
||||||
|
"directories": {
|
||||||
|
"buildResources": "assets"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ export const GraphQLType = (props: {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<GraphQLType type={props.type.ofType} />
|
<GraphQLType type={props.type.ofType} />
|
||||||
<span className="text-neutral-10!">!</span>
|
<span className="text-muted-foreground!">!</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -23,9 +23,9 @@ export const GraphQLType = (props: {
|
||||||
if (props.type instanceof GraphQLList) {
|
if (props.type instanceof GraphQLList) {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<span className="text-neutral-10!">[</span>
|
<span className="text-muted-foreground!">[</span>
|
||||||
<GraphQLType type={props.type.ofType} />
|
<GraphQLType type={props.type.ofType} />
|
||||||
<span className="text-neutral-10!">]</span>
|
<span className="text-muted-foreground!">]</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { SVGProps } from 'react';
|
||||||
import type { LucideProps } from 'lucide-react';
|
import type { LucideProps } from 'lucide-react';
|
||||||
|
|
||||||
export const GraphQLIcon = (props: LucideProps) => {
|
export const GraphQLIcon = (props: LucideProps) => {
|
||||||
|
|
@ -8,7 +9,7 @@ export const GraphQLIcon = (props: LucideProps) => {
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{...props}
|
{...(props as SVGProps<SVGSVGElement>)}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
|
|
@ -16,35 +16,20 @@ import {
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
RotateCcwIcon,
|
RotateCcwIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { GraphQLType } from '@/laboratory/components/graphql-type';
|
import type { LaboratoryOperation } from '../../lib/operations';
|
||||||
import { GraphQLIcon } from '@/laboratory/components/icons';
|
import { getOpenPaths, isArgInQuery, isPathInQuery } from '../../lib/operations.utils';
|
||||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
import { cn } from '../../lib/utils';
|
||||||
import { Button } from '@/laboratory/components/ui/button';
|
import { GraphQLType } from '../graphql-type';
|
||||||
import { Checkbox } from '@/laboratory/components/ui/checkbox';
|
import { GraphQLIcon } from '../icons';
|
||||||
import {
|
import { Button } from '../ui/button';
|
||||||
Collapsible,
|
import { Checkbox } from '../ui/checkbox';
|
||||||
CollapsibleContent,
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible';
|
||||||
CollapsibleTrigger,
|
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../ui/empty';
|
||||||
} from '@/laboratory/components/ui/collapsible';
|
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '../ui/input-group';
|
||||||
import {
|
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
||||||
Empty,
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||||
EmptyDescription,
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||||
EmptyHeader,
|
import { useLaboratory } from './context';
|
||||||
EmptyMedia,
|
|
||||||
EmptyTitle,
|
|
||||||
} from '@/laboratory/components/ui/empty';
|
|
||||||
import {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton,
|
|
||||||
InputGroupInput,
|
|
||||||
} from '@/laboratory/components/ui/input-group';
|
|
||||||
import { ScrollArea, ScrollBar } from '@/laboratory/components/ui/scroll-area';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/laboratory/components/ui/tabs';
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/laboratory/components/ui/tooltip';
|
|
||||||
import type { LaboratoryOperation } from '@/laboratory/lib/operations';
|
|
||||||
import { getOpenPaths, isArgInQuery, isPathInQuery } from '@/laboratory/lib/operations.utils';
|
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
|
|
||||||
export const BuilderArgument = (props: {
|
export const BuilderArgument = (props: {
|
||||||
field: GraphQLArgument;
|
field: GraphQLArgument;
|
||||||
|
|
@ -76,8 +61,8 @@ export const BuilderArgument = (props: {
|
||||||
<Button
|
<Button
|
||||||
key={props.field.name}
|
key={props.field.name}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn('text-neutral-10 p-1! w-full justify-start text-xs', {
|
className={cn('text-muted-foreground p-1! w-full justify-start text-xs', {
|
||||||
'text-neutral-11': isInQuery,
|
'text-foreground-primary': isInQuery,
|
||||||
})}
|
})}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
|
|
@ -157,9 +142,9 @@ export const BuilderScalarField = (props: {
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-neutral-10 bg-neutral-2 p-1! group sticky top-0 z-10 w-full justify-start overflow-hidden text-xs',
|
'text-muted-foreground bg-card p-1! group sticky top-0 z-10 w-full justify-start overflow-hidden text-xs',
|
||||||
{
|
{
|
||||||
'text-neutral-11': isInQuery,
|
'text-foreground-primary': isInQuery,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -167,10 +152,10 @@ export const BuilderScalarField = (props: {
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<div className="bg-neutral-2 absolute left-0 top-0 -z-20 size-full" />
|
<div className="bg-card absolute left-0 top-0 -z-20 size-full" />
|
||||||
<div className="group-hover:bg-neutral-2 absolute left-0 top-0 -z-10 size-full transition-colors" />
|
<div className="group-hover:bg-accent/50 absolute left-0 top-0 -z-10 size-full transition-colors" />
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={cn('text-neutral-10 size-4 transition-all', {
|
className={cn('text-muted-foreground size-4 transition-all', {
|
||||||
'-rotate-90': !isOpen,
|
'-rotate-90': !isOpen,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
@ -191,7 +176,7 @@ export const BuilderScalarField = (props: {
|
||||||
{props.field.name}: <GraphQLType type={props.field.type} />
|
{props.field.name}: <GraphQLType type={props.field.type} />
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="border-neutral-5 relative z-0 ml-3 flex flex-col border-l pl-2">
|
<CollapsibleContent className="border-border relative z-0 ml-3 flex flex-col border-l pl-2">
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div>
|
<div>
|
||||||
{args.length > 0 && (
|
{args.length > 0 && (
|
||||||
|
|
@ -200,9 +185,9 @@ export const BuilderScalarField = (props: {
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-neutral-10 bg-neutral-2 p-1! group sticky top-0 z-10 w-full justify-start overflow-hidden text-xs',
|
'text-muted-foreground bg-card p-1! group sticky top-0 z-10 w-full justify-start overflow-hidden text-xs',
|
||||||
{
|
{
|
||||||
'text-neutral-11': hasArgs,
|
'text-foreground-primary': hasArgs,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -211,7 +196,7 @@ export const BuilderScalarField = (props: {
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={cn('text-neutral-10 size-4 transition-all', {
|
className={cn('text-muted-foreground size-4 transition-all', {
|
||||||
'-rotate-90': !isOpen,
|
'-rotate-90': !isOpen,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
@ -220,7 +205,7 @@ export const BuilderScalarField = (props: {
|
||||||
[arguments]
|
[arguments]
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="border-neutral-5 ml-3 flex flex-col border-l pl-2">
|
<CollapsibleContent className="border-border ml-3 flex flex-col border-l pl-2">
|
||||||
{args.map(arg => (
|
{args.map(arg => (
|
||||||
<BuilderArgument
|
<BuilderArgument
|
||||||
key={arg.name}
|
key={arg.name}
|
||||||
|
|
@ -244,8 +229,8 @@ export const BuilderScalarField = (props: {
|
||||||
<Button
|
<Button
|
||||||
key={props.field.name}
|
key={props.field.name}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn('text-neutral-10 p-1! w-full justify-start text-xs', {
|
className={cn('text-muted-foreground p-1! w-full justify-start text-xs', {
|
||||||
'text-neutral-11': isInQuery,
|
'text-foreground-primary': isInQuery,
|
||||||
})}
|
})}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
|
|
@ -335,9 +320,9 @@ export const BuilderObjectField = (props: {
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-neutral-10 bg-neutral-2 p-1! group sticky top-0 z-10 w-full justify-start overflow-hidden text-xs',
|
'text-muted-foreground bg-card p-1! group sticky top-0 z-10 w-full justify-start overflow-hidden text-xs',
|
||||||
{
|
{
|
||||||
'text-neutral-11': isInQuery,
|
'text-foreground-primary': isInQuery,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -345,10 +330,10 @@ export const BuilderObjectField = (props: {
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<div className="bg-neutral-2 absolute left-0 top-0 -z-20 size-full" />
|
<div className="bg-card absolute left-0 top-0 -z-20 size-full" />
|
||||||
<div className="group-hover:bg-neutral-2 absolute left-0 top-0 -z-10 size-full transition-colors" />
|
<div className="group-hover:bg-accent/50 absolute left-0 top-0 -z-10 size-full transition-colors" />
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={cn('text-neutral-10 size-4 transition-all', {
|
className={cn('text-muted-foreground size-4 transition-all', {
|
||||||
'-rotate-90': !isOpen,
|
'-rotate-90': !isOpen,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
@ -369,7 +354,7 @@ export const BuilderObjectField = (props: {
|
||||||
{props.field.name}: <GraphQLType type={props.field.type} />
|
{props.field.name}: <GraphQLType type={props.field.type} />
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="border-neutral-5 relative z-0 ml-4 flex flex-col border-l pl-1">
|
<CollapsibleContent className="border-border relative z-0 ml-4 flex flex-col border-l pl-1">
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div>
|
<div>
|
||||||
{args.length > 0 && (
|
{args.length > 0 && (
|
||||||
|
|
@ -378,9 +363,9 @@ export const BuilderObjectField = (props: {
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-neutral-10 bg-neutral-2 p-1! group sticky top-0 z-10 w-full justify-start overflow-hidden text-xs',
|
'text-muted-foreground bg-card p-1! group sticky top-0 z-10 w-full justify-start overflow-hidden text-xs',
|
||||||
{
|
{
|
||||||
'text-neutral-11': hasArgs,
|
'text-foreground-primary': hasArgs,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -389,7 +374,7 @@ export const BuilderObjectField = (props: {
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={cn('text-neutral-10 size-4 transition-all', {
|
className={cn('text-muted-foreground size-4 transition-all', {
|
||||||
'-rotate-90': !isOpen,
|
'-rotate-90': !isOpen,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
@ -398,7 +383,7 @@ export const BuilderObjectField = (props: {
|
||||||
[arguments]
|
[arguments]
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="border-neutral-5 ml-4 flex flex-col border-l pl-1">
|
<CollapsibleContent className="border-border ml-4 flex flex-col border-l pl-1">
|
||||||
{args.map(arg => (
|
{args.map(arg => (
|
||||||
<BuilderArgument
|
<BuilderArgument
|
||||||
key={arg.name}
|
key={arg.name}
|
||||||
|
|
@ -529,7 +514,7 @@ export const Builder = (props: {
|
||||||
}, [defaultEndpoint, setEndpointValue]);
|
}, [defaultEndpoint, setEndpointValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-neutral-2 flex size-full flex-col overflow-hidden">
|
<div className="bg-card flex size-full flex-col overflow-hidden">
|
||||||
<div className="flex items-center px-3 pt-3">
|
<div className="flex items-center px-3 pt-3">
|
||||||
<span className="text-base font-medium">Builder</span>
|
<span className="text-base font-medium">Builder</span>
|
||||||
<div className="ml-auto flex items-center">
|
<div className="ml-auto flex items-center">
|
||||||
|
|
@ -542,7 +527,7 @@ export const Builder = (props: {
|
||||||
className="p-1! size-6 rounded-sm"
|
className="p-1! size-6 rounded-sm"
|
||||||
disabled={openPaths.length === 0}
|
disabled={openPaths.length === 0}
|
||||||
>
|
>
|
||||||
<CopyMinusIcon className="text-neutral-10 size-4" />
|
<CopyMinusIcon className="text-muted-foreground size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Collapse all</TooltipContent>
|
<TooltipContent>Collapse all</TooltipContent>
|
||||||
|
|
@ -581,7 +566,7 @@ export const Builder = (props: {
|
||||||
onValueChange={setTabValue}
|
onValueChange={setTabValue}
|
||||||
className="flex size-full flex-col gap-0"
|
className="flex size-full flex-col gap-0"
|
||||||
>
|
>
|
||||||
<div className="border-neutral-5 flex items-center border-b p-3">
|
<div className="border-border flex items-center border-b p-3">
|
||||||
<TabsList className="w-full">
|
<TabsList className="w-full">
|
||||||
<TabsTrigger value="query" disabled={queryFields.length === 0} className="text-xs">
|
<TabsTrigger value="query" disabled={queryFields.length === 0} className="text-xs">
|
||||||
Query
|
Query
|
||||||
|
|
@ -654,7 +639,7 @@ export const Builder = (props: {
|
||||||
<Empty className="px-0! h-96 w-full">
|
<Empty className="px-0! h-96 w-full">
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyMedia variant="icon">
|
<EmptyMedia variant="icon">
|
||||||
<FolderIcon className="text-neutral-10 size-6" />
|
<FolderIcon className="text-muted-foreground size-6" />
|
||||||
</EmptyMedia>
|
</EmptyMedia>
|
||||||
<EmptyTitle className="text-base">No endpoint selected</EmptyTitle>
|
<EmptyTitle className="text-base">No endpoint selected</EmptyTitle>
|
||||||
<EmptyDescription className="text-xs">
|
<EmptyDescription className="text-xs">
|
||||||
|
|
@ -7,8 +7,10 @@ import {
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { GraphQLIcon } from '@/laboratory/components/icons';
|
import { TooltipTrigger } from '@radix-ui/react-tooltip';
|
||||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
import type { LaboratoryCollection, LaboratoryCollectionOperation } from '../../lib/collections';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
import { GraphQLIcon } from '../icons';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
|
|
@ -19,13 +21,9 @@ import {
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '@/laboratory/components/ui/alert-dialog';
|
} from '../ui/alert-dialog';
|
||||||
import { Button } from '@/laboratory/components/ui/button';
|
import { Button } from '../ui/button';
|
||||||
import {
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible';
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from '@/laboratory/components/ui/collapsible';
|
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
|
|
@ -33,16 +31,11 @@ import {
|
||||||
EmptyHeader,
|
EmptyHeader,
|
||||||
EmptyMedia,
|
EmptyMedia,
|
||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
} from '@/laboratory/components/ui/empty';
|
} from '../ui/empty';
|
||||||
import { Input } from '@/laboratory/components/ui/input';
|
import { Input } from '../ui/input';
|
||||||
import { ScrollArea, ScrollBar } from '@/laboratory/components/ui/scroll-area';
|
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
||||||
import { Tooltip, TooltipContent } from '@/laboratory/components/ui/tooltip';
|
import { Tooltip, TooltipContent } from '../ui/tooltip';
|
||||||
import type {
|
import { useLaboratory } from './context';
|
||||||
LaboratoryCollection,
|
|
||||||
LaboratoryCollectionOperation,
|
|
||||||
} from '@/laboratory/lib/collections';
|
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import { TooltipTrigger } from '@radix-ui/react-tooltip';
|
|
||||||
|
|
||||||
export const CollectionItem = (props: { collection: LaboratoryCollection }) => {
|
export const CollectionItem = (props: { collection: LaboratoryCollection }) => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -64,13 +57,13 @@ export const CollectionItem = (props: { collection: LaboratoryCollection }) => {
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="bg-neutral-3 group sticky top-0 w-full justify-start px-2"
|
className="bg-background group sticky top-0 w-full justify-start px-2"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<FolderOpenIcon className="text-neutral-10 size-4" />
|
<FolderOpenIcon className="text-muted-foreground size-4" />
|
||||||
) : (
|
) : (
|
||||||
<FolderIcon className="text-neutral-10 size-4" />
|
<FolderIcon className="text-muted-foreground size-4" />
|
||||||
)}
|
)}
|
||||||
{props.collection.name}
|
{props.collection.name}
|
||||||
{checkPermissions?.('collections:delete') && (
|
{checkPermissions?.('collections:delete') && (
|
||||||
|
|
@ -80,7 +73,7 @@ export const CollectionItem = (props: { collection: LaboratoryCollection }) => {
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
className="text-neutral-10 p-1! pr-0! ml-auto opacity-0 transition-opacity hover:text-red-500 group-hover:opacity-100"
|
className="text-muted-foreground hover:text-destructive p-1! pr-0! ml-auto opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
|
@ -120,7 +113,7 @@ export const CollectionItem = (props: { collection: LaboratoryCollection }) => {
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className={cn('border-neutral-5 ml-4 flex flex-col gap-1 border-l pl-2')}>
|
<CollapsibleContent className={cn('border-border ml-4 flex flex-col gap-1 border-l pl-2')}>
|
||||||
{isOpen &&
|
{isOpen &&
|
||||||
props.collection.operations.map(operation => {
|
props.collection.operations.map(operation => {
|
||||||
const isActive = activeOperation?.id === operation.id;
|
const isActive = activeOperation?.id === operation.id;
|
||||||
|
|
@ -130,7 +123,7 @@ export const CollectionItem = (props: { collection: LaboratoryCollection }) => {
|
||||||
key={operation.name}
|
key={operation.name}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn('group w-full justify-start gap-2 px-2', {
|
className={cn('group w-full justify-start gap-2 px-2', {
|
||||||
'bg-accent_80': isActive,
|
'bg-accent/50': isActive,
|
||||||
})}
|
})}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -156,7 +149,7 @@ export const CollectionItem = (props: { collection: LaboratoryCollection }) => {
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
className="text-neutral-10 p-1! pr-0! ml-auto opacity-0 transition-opacity hover:text-red-500 group-hover:opacity-100"
|
className="text-muted-foreground hover:text-destructive p-1! pr-0! ml-auto opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
|
@ -219,7 +212,7 @@ export const CollectionsSearchResult = (props: { items: CollectionsSearchResultI
|
||||||
key={operation.name}
|
key={operation.name}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn('group w-full justify-start gap-2 px-2', {
|
className={cn('group w-full justify-start gap-2 px-2', {
|
||||||
'bg-accent_80': isActive,
|
'bg-accent/50': isActive,
|
||||||
})}
|
})}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -237,8 +230,8 @@ export const CollectionsSearchResult = (props: { items: CollectionsSearchResultI
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GraphQLIcon className="size-4 text-pink-500" />
|
<GraphQLIcon className="size-4 text-pink-500" />
|
||||||
<span className="text-neutral-10 truncate">{operation.parent.name}</span>
|
<span className="text-muted-foreground truncate">{operation.parent.name}</span>
|
||||||
<span className="text-neutral-10">{' / '}</span>
|
<span className="text-muted-foreground">{' / '}</span>
|
||||||
{operation.name}
|
{operation.name}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
@ -282,7 +275,7 @@ export const Collections = () => {
|
||||||
className="p-1! size-6 rounded-sm"
|
className="p-1! size-6 rounded-sm"
|
||||||
onClick={openAddCollectionDialog}
|
onClick={openAddCollectionDialog}
|
||||||
>
|
>
|
||||||
<FolderPlusIcon className="text-neutral-11 size-4" />
|
<FolderPlusIcon className="text-primary size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Add collection</TooltipContent>
|
<TooltipContent>Add collection</TooltipContent>
|
||||||
|
|
@ -290,8 +283,8 @@ export const Collections = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-neutral-5 relative border-b p-3">
|
<div className="border-border relative border-b p-3">
|
||||||
<SearchIcon className="text-neutral-10 absolute left-5 top-1/2 size-4 -translate-y-1/2" />
|
<SearchIcon className="text-muted-foreground absolute left-5 top-1/2 size-4 -translate-y-1/2" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
|
|
@ -306,7 +299,7 @@ export const Collections = () => {
|
||||||
className="p-1! absolute right-5 top-1/2 size-6 -translate-y-1/2 rounded-sm"
|
className="p-1! absolute right-5 top-1/2 size-6 -translate-y-1/2 rounded-sm"
|
||||||
onClick={() => setSearch('')}
|
onClick={() => setSearch('')}
|
||||||
>
|
>
|
||||||
<XIcon className="text-neutral-10 size-4" />
|
<XIcon className="text-muted-foreground size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -321,7 +314,7 @@ export const Collections = () => {
|
||||||
<Empty className="px-0! w-full">
|
<Empty className="px-0! w-full">
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyMedia variant="icon">
|
<EmptyMedia variant="icon">
|
||||||
<SearchIcon className="text-neutral-10 size-6" />
|
<SearchIcon className="text-muted-foreground size-6" />
|
||||||
</EmptyMedia>
|
</EmptyMedia>
|
||||||
<EmptyTitle className="text-base">No results found</EmptyTitle>
|
<EmptyTitle className="text-base">No results found</EmptyTitle>
|
||||||
<EmptyDescription className="text-xs">
|
<EmptyDescription className="text-xs">
|
||||||
|
|
@ -336,7 +329,7 @@ export const Collections = () => {
|
||||||
<Empty className="px-0! w-full">
|
<Empty className="px-0! w-full">
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyMedia variant="icon">
|
<EmptyMedia variant="icon">
|
||||||
<FolderIcon className="text-neutral-10 size-6" />
|
<FolderIcon className="text-muted-foreground size-6" />
|
||||||
</EmptyMedia>
|
</EmptyMedia>
|
||||||
<EmptyTitle className="text-base">No collections yet</EmptyTitle>
|
<EmptyTitle className="text-base">No collections yet</EmptyTitle>
|
||||||
<EmptyDescription className="text-xs">
|
<EmptyDescription className="text-xs">
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Fragment, useEffect, useState } from 'react';
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
import { FilePlus2Icon, FolderPlusIcon, PlayIcon, RefreshCcwIcon, ServerIcon } from 'lucide-react';
|
import { FilePlus2Icon, FolderPlusIcon, PlayIcon, RefreshCcwIcon, ServerIcon } from 'lucide-react';
|
||||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
|
||||||
import {
|
import {
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
|
|
@ -10,7 +9,8 @@ import {
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
CommandShortcut,
|
CommandShortcut,
|
||||||
} from '@/laboratory/components/ui/command';
|
} from '../ui/command';
|
||||||
|
import { useLaboratory } from './context';
|
||||||
|
|
||||||
export function Command(props: { open?: boolean; onOpenChange?: (open: boolean) => void }) {
|
export function Command(props: { open?: boolean; onOpenChange?: (open: boolean) => void }) {
|
||||||
const {
|
const {
|
||||||
|
|
@ -5,47 +5,36 @@ import {
|
||||||
type LaboratoryCollectionOperation,
|
type LaboratoryCollectionOperation,
|
||||||
type LaboratoryCollectionsActions,
|
type LaboratoryCollectionsActions,
|
||||||
type LaboratoryCollectionsState,
|
type LaboratoryCollectionsState,
|
||||||
} from '@/laboratory/lib/collections';
|
} from '../../lib/collections';
|
||||||
import {
|
import { type LaboratoryEndpointActions, type LaboratoryEndpointState } from '../../lib/endpoint';
|
||||||
type LaboratoryEndpointActions,
|
import type { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from '../../lib/env';
|
||||||
type LaboratoryEndpointState,
|
|
||||||
} from '@/laboratory/lib/endpoint';
|
|
||||||
import type { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from '@/laboratory/lib/env';
|
|
||||||
import type {
|
import type {
|
||||||
LaboratoryHistory,
|
LaboratoryHistory,
|
||||||
LaboratoryHistoryActions,
|
LaboratoryHistoryActions,
|
||||||
LaboratoryHistoryState,
|
LaboratoryHistoryState,
|
||||||
} from '@/laboratory/lib/history';
|
} from '../../lib/history';
|
||||||
import {
|
import {
|
||||||
type LaboratoryOperation,
|
type LaboratoryOperation,
|
||||||
type LaboratoryOperationsActions,
|
type LaboratoryOperationsActions,
|
||||||
type LaboratoryOperationsState,
|
type LaboratoryOperationsState,
|
||||||
} from '@/laboratory/lib/operations';
|
} from '../../lib/operations';
|
||||||
import {
|
import {
|
||||||
LaboratoryPlugin,
|
LaboratoryPlugin,
|
||||||
LaboratoryPluginsActions,
|
LaboratoryPluginsActions,
|
||||||
LaboratoryPluginsState,
|
LaboratoryPluginsState,
|
||||||
} from '@/laboratory/lib/plugins';
|
} from '../../lib/plugins';
|
||||||
import type {
|
import type {
|
||||||
LaboratoryPreflight,
|
LaboratoryPreflight,
|
||||||
LaboratoryPreflightActions,
|
LaboratoryPreflightActions,
|
||||||
LaboratoryPreflightState,
|
LaboratoryPreflightState,
|
||||||
} from '@/laboratory/lib/preflight';
|
} from '../../lib/preflight';
|
||||||
import type {
|
import type {
|
||||||
LaboratorySettings,
|
LaboratorySettings,
|
||||||
LaboratorySettingsActions,
|
LaboratorySettingsActions,
|
||||||
LaboratorySettingsState,
|
LaboratorySettingsState,
|
||||||
} from '@/laboratory/lib/settings';
|
} from '../../lib/settings';
|
||||||
import type {
|
import type { LaboratoryTab, LaboratoryTabsActions, LaboratoryTabsState } from '../../lib/tabs';
|
||||||
LaboratoryTab,
|
import type { LaboratoryTest, LaboratoryTestActions, LaboratoryTestState } from '../../lib/tests';
|
||||||
LaboratoryTabsActions,
|
|
||||||
LaboratoryTabsState,
|
|
||||||
} from '@/laboratory/lib/tabs';
|
|
||||||
import type {
|
|
||||||
LaboratoryTest,
|
|
||||||
LaboratoryTestActions,
|
|
||||||
LaboratoryTestState,
|
|
||||||
} from '@/laboratory/lib/tests';
|
|
||||||
|
|
||||||
type LaboratoryContextState = LaboratoryCollectionsState &
|
type LaboratoryContextState = LaboratoryCollectionsState &
|
||||||
LaboratoryEndpointState &
|
LaboratoryEndpointState &
|
||||||
|
|
@ -58,6 +47,7 @@ type LaboratoryContextState = LaboratoryCollectionsState &
|
||||||
LaboratoryPluginsState &
|
LaboratoryPluginsState &
|
||||||
LaboratoryTestState & {
|
LaboratoryTestState & {
|
||||||
isFullScreen?: boolean;
|
isFullScreen?: boolean;
|
||||||
|
theme?: 'light' | 'dark';
|
||||||
};
|
};
|
||||||
type LaboratoryContextActions = LaboratoryCollectionsActions &
|
type LaboratoryContextActions = LaboratoryCollectionsActions &
|
||||||
LaboratoryEndpointActions &
|
LaboratoryEndpointActions &
|
||||||
|
|
@ -73,9 +63,7 @@ type LaboratoryContextActions = LaboratoryCollectionsActions &
|
||||||
openUpdateEndpointDialog?: () => void;
|
openUpdateEndpointDialog?: () => void;
|
||||||
openAddTestDialog?: () => void;
|
openAddTestDialog?: () => void;
|
||||||
openPreflightPromptModal?: (props: {
|
openPreflightPromptModal?: (props: {
|
||||||
title?: string;
|
placeholder: string;
|
||||||
description?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
onSubmit?: (value: string | null) => void;
|
onSubmit?: (value: string | null) => void;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|
@ -86,9 +74,12 @@ type LaboratoryContextActions = LaboratoryCollectionsActions &
|
||||||
) => boolean;
|
) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LaboratoryContext = createContext<LaboratoryContextState & LaboratoryContextActions>(
|
type LaboratoryContext = LaboratoryContextState &
|
||||||
{} as LaboratoryContextState & LaboratoryContextActions,
|
LaboratoryContextActions & {
|
||||||
);
|
container: HTMLDivElement | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LaboratoryContext = createContext<LaboratoryContext>({} as LaboratoryContext);
|
||||||
|
|
||||||
export const useLaboratory = () => {
|
export const useLaboratory = () => {
|
||||||
return useContext(LaboratoryContext);
|
return useContext(LaboratoryContext);
|
||||||
|
|
@ -108,6 +99,7 @@ export interface LaboratoryPermissions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LaboratoryApi {
|
export interface LaboratoryApi {
|
||||||
|
theme?: 'light' | 'dark';
|
||||||
defaultEndpoint?: string | null;
|
defaultEndpoint?: string | null;
|
||||||
onEndpointChange?: (endpoint: string | null) => void;
|
onEndpointChange?: (endpoint: string | null) => void;
|
||||||
defaultSchemaIntrospection?: IntrospectionQuery | null;
|
defaultSchemaIntrospection?: IntrospectionQuery | null;
|
||||||
|
|
@ -144,9 +136,7 @@ export interface LaboratoryApi {
|
||||||
openUpdateEndpointDialog?: () => void;
|
openUpdateEndpointDialog?: () => void;
|
||||||
openAddTestDialog?: () => void;
|
openAddTestDialog?: () => void;
|
||||||
openPreflightPromptModal?: (props: {
|
openPreflightPromptModal?: (props: {
|
||||||
title?: string;
|
placeholder: string;
|
||||||
description?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
onSubmit?: (value: string | null) => void;
|
onSubmit?: (value: string | null) => void;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|
@ -176,7 +166,9 @@ export interface LaboratoryApi {
|
||||||
|
|
||||||
export type LaboratoryContextProps = LaboratoryContextState &
|
export type LaboratoryContextProps = LaboratoryContextState &
|
||||||
LaboratoryContextActions &
|
LaboratoryContextActions &
|
||||||
LaboratoryApi;
|
LaboratoryApi & {
|
||||||
|
container: HTMLDivElement | null;
|
||||||
|
};
|
||||||
|
|
||||||
export const LaboratoryProvider = (props: React.PropsWithChildren<LaboratoryContextProps>) => {
|
export const LaboratoryProvider = (props: React.PropsWithChildren<LaboratoryContextProps>) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { forwardRef, useEffect, useId, useImperativeHandle, useRef } from 'react';
|
import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from 'react';
|
||||||
import * as monaco from 'monaco-editor';
|
import * as monaco from 'monaco-editor';
|
||||||
import { initializeMode } from 'monaco-graphql/initializeMode';
|
import { initializeMode } from 'monaco-graphql/initializeMode';
|
||||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
|
||||||
import MonacoEditor, { loader } from '@monaco-editor/react';
|
import MonacoEditor, { loader } from '@monaco-editor/react';
|
||||||
|
import { useLaboratory } from './context';
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
(window as any).monaco = monaco;
|
(window as Window & typeof globalThis & { monaco: typeof monaco }).monaco = monaco;
|
||||||
}
|
}
|
||||||
|
|
||||||
loader.config({ monaco });
|
loader.config({ monaco });
|
||||||
|
|
@ -73,7 +73,7 @@ const darkTheme: monaco.editor.IStandaloneThemeData = {
|
||||||
],
|
],
|
||||||
colors: {
|
colors: {
|
||||||
'editor.foreground': '#f6f8fa',
|
'editor.foreground': '#f6f8fa',
|
||||||
'editor.background': '#18181b',
|
'editor.background': '#0f1214',
|
||||||
'editor.selectionBackground': '#2A2F34',
|
'editor.selectionBackground': '#2A2F34',
|
||||||
'editor.inactiveSelectionBackground': '#2A2F34',
|
'editor.inactiveSelectionBackground': '#2A2F34',
|
||||||
'editor.lineHighlightBackground': '#2A2F34',
|
'editor.lineHighlightBackground': '#2A2F34',
|
||||||
|
|
@ -87,6 +87,15 @@ const darkTheme: monaco.editor.IStandaloneThemeData = {
|
||||||
|
|
||||||
monaco.editor.defineTheme('hive-laboratory-dark', darkTheme);
|
monaco.editor.defineTheme('hive-laboratory-dark', darkTheme);
|
||||||
|
|
||||||
|
const lightTheme: monaco.editor.IStandaloneThemeData = {
|
||||||
|
base: 'vs',
|
||||||
|
inherit: true,
|
||||||
|
rules: [],
|
||||||
|
colors: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
monaco.editor.defineTheme('hive-laboratory-light', lightTheme);
|
||||||
|
|
||||||
monaco.languages.setMonarchTokensProvider('dotenv', {
|
monaco.languages.setMonarchTokensProvider('dotenv', {
|
||||||
tokenizer: {
|
tokenizer: {
|
||||||
root: [
|
root: [
|
||||||
|
|
@ -104,19 +113,21 @@ monaco.languages.setMonarchTokensProvider('dotenv', {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Editor = forwardRef<
|
export type EditorHandle = {
|
||||||
{
|
setValue: (value: string) => void;
|
||||||
setValue: (value: string) => void;
|
};
|
||||||
},
|
|
||||||
React.ComponentProps<typeof MonacoEditor> & {
|
export type EditorProps = React.ComponentProps<typeof MonacoEditor> & {
|
||||||
uri?: monaco.Uri;
|
uri?: monaco.Uri;
|
||||||
variablesUri?: monaco.Uri;
|
variablesUri?: monaco.Uri;
|
||||||
extraLibs?: string[];
|
extraLibs?: string[];
|
||||||
}
|
};
|
||||||
>((props, ref) => {
|
|
||||||
|
const EditorInner = forwardRef<EditorHandle, EditorProps>((props, ref) => {
|
||||||
const id = useId();
|
const id = useId();
|
||||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
||||||
const { introspection, endpoint } = useLaboratory();
|
const { introspection, endpoint, theme } = useLaboratory();
|
||||||
|
const [typescriptReady, setTypescriptReady] = useState(!!monaco.languages.typescript);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (introspection) {
|
if (introspection) {
|
||||||
|
|
@ -147,23 +158,46 @@ export const Editor = forwardRef<
|
||||||
}, [introspection, props.uri?.toString(), props.variablesUri?.toString()]);
|
}, [introspection, props.uri?.toString(), props.variablesUri?.toString()]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.extraLibs) {
|
void (async function () {
|
||||||
for (const lib of props.extraLibs) {
|
if (!props.extraLibs?.length) {
|
||||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
return;
|
||||||
target: monaco.languages.typescript.ScriptTarget.ESNext, // supports top-level await
|
|
||||||
module: monaco.languages.typescript.ModuleKind.ESNext, // treat file as module
|
|
||||||
allowNonTsExtensions: true,
|
|
||||||
allowJs: true,
|
|
||||||
lib: ['esnext', 'webworker'], // if running in sandbox
|
|
||||||
});
|
|
||||||
|
|
||||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
|
||||||
lib,
|
|
||||||
`file:///hive-lab-globals-${id}.d.ts`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, []);
|
if (!monaco.languages.typescript) {
|
||||||
|
await import('monaco-editor/esm/vs/language/typescript/monaco.contribution');
|
||||||
|
setTypescriptReady(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ts = monaco.languages.typescript;
|
||||||
|
|
||||||
|
if (!ts) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraLibs = Object.values(ts.typescriptDefaults.getExtraLibs()).map(lib => lib.content);
|
||||||
|
|
||||||
|
if (props.extraLibs.every(lib => extraLibs.includes(lib))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeId = id.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
|
|
||||||
|
ts.typescriptDefaults.setCompilerOptions({
|
||||||
|
target: ts.ScriptTarget.ESNext,
|
||||||
|
module: ts.ModuleKind.ESNext,
|
||||||
|
allowNonTsExtensions: true,
|
||||||
|
allowJs: true,
|
||||||
|
lib: ['esnext', 'webworker'],
|
||||||
|
});
|
||||||
|
|
||||||
|
ts.typescriptDefaults.setExtraLibs(
|
||||||
|
props.extraLibs.map((content, index) => ({
|
||||||
|
content,
|
||||||
|
filePath: `file:///hive-lab-globals-${safeId}-${index}.d.ts`,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
}, [id, props.extraLibs]);
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
ref,
|
ref,
|
||||||
|
|
@ -177,18 +211,25 @@ export const Editor = forwardRef<
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!typescriptReady && props.language === 'typescript') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="size-full">
|
<div className="size-full overflow-hidden">
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
className="size-full"
|
className="size-full"
|
||||||
{...props}
|
{...props}
|
||||||
theme="hive-laboratory-dark"
|
theme={theme === 'dark' ? 'hive-laboratory-dark' : 'hive-laboratory-light'}
|
||||||
onMount={editor => {
|
onMount={editor => {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
}}
|
}}
|
||||||
loading={null}
|
loading={null}
|
||||||
options={{
|
options={{
|
||||||
...props.options,
|
...props.options,
|
||||||
|
lineNumbers: 'on',
|
||||||
|
cursorStyle: 'line',
|
||||||
|
cursorBlinking: 'smooth',
|
||||||
padding: {
|
padding: {
|
||||||
top: 16,
|
top: 16,
|
||||||
},
|
},
|
||||||
|
|
@ -206,3 +247,5 @@ export const Editor = forwardRef<
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const Editor = EditorInner as unknown as (props: EditorProps) => JSX.Element;
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
import { useLaboratory } from './context';
|
||||||
import { Editor } from '@/laboratory/components/laboratory/editor';
|
import { Editor } from './editor';
|
||||||
|
|
||||||
export const Env = () => {
|
export const Env = () => {
|
||||||
const { env, setEnv } = useLaboratory();
|
const { env, setEnv } = useLaboratory();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-neutral-2 size-full">
|
<div className="bg-card size-full">
|
||||||
<Editor
|
<Editor
|
||||||
defaultValue={Object.entries(env?.variables ?? {})
|
defaultValue={Object.entries(env?.variables ?? {})
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
import { LaboratoryHistoryRequest } from '../../lib/history';
|
||||||
import { Operation } from '@/laboratory/components/laboratory/operation';
|
import { useLaboratory } from './context';
|
||||||
import { LaboratoryHistoryRequest } from '@/laboratory/lib/history';
|
import { Operation } from './operation';
|
||||||
|
|
||||||
export const HistoryItem = () => {
|
export const HistoryItem = () => {
|
||||||
const { activeTab, history } = useLaboratory();
|
const { activeTab, history } = useLaboratory();
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { ClockIcon, FolderClockIcon, FolderOpenIcon, HistoryIcon, TrashIcon } from 'lucide-react';
|
import { ClockIcon, FolderClockIcon, FolderOpenIcon, HistoryIcon, TrashIcon } from 'lucide-react';
|
||||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
import type { LaboratoryHistory, LaboratoryHistoryRequest } from '../../lib/history';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
|
|
@ -12,24 +13,13 @@ import {
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '@/laboratory/components/ui/alert-dialog';
|
} from '../ui/alert-dialog';
|
||||||
import { Button } from '@/laboratory/components/ui/button';
|
import { Button } from '../ui/button';
|
||||||
import {
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible';
|
||||||
Collapsible,
|
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../ui/empty';
|
||||||
CollapsibleContent,
|
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
||||||
CollapsibleTrigger,
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||||
} from '@/laboratory/components/ui/collapsible';
|
import { useLaboratory } from './context';
|
||||||
import {
|
|
||||||
Empty,
|
|
||||||
EmptyDescription,
|
|
||||||
EmptyHeader,
|
|
||||||
EmptyMedia,
|
|
||||||
EmptyTitle,
|
|
||||||
} from '@/laboratory/components/ui/empty';
|
|
||||||
import { ScrollArea, ScrollBar } from '@/laboratory/components/ui/scroll-area';
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/laboratory/components/ui/tooltip';
|
|
||||||
import type { LaboratoryHistory, LaboratoryHistoryRequest } from '@/laboratory/lib/history';
|
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
|
|
||||||
export const HistoryOperationItem = (props: { historyItem: LaboratoryHistoryRequest }) => {
|
export const HistoryOperationItem = (props: { historyItem: LaboratoryHistoryRequest }) => {
|
||||||
const { activeTab, addTab, setActiveTab, deleteHistory } = useLaboratory();
|
const { activeTab, addTab, setActiveTab, deleteHistory } = useLaboratory();
|
||||||
|
|
@ -57,8 +47,8 @@ export const HistoryOperationItem = (props: { historyItem: LaboratoryHistoryRequ
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={cn('bg-neutral-3 group sticky top-0 w-full justify-start px-2', {
|
className={cn('bg-background group sticky top-0 w-full justify-start px-2', {
|
||||||
'bg-neutral-2': isActive,
|
'bg-accent/50': isActive,
|
||||||
})}
|
})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveTab(
|
setActiveTab(
|
||||||
|
|
@ -78,7 +68,7 @@ export const HistoryOperationItem = (props: { historyItem: LaboratoryHistoryRequ
|
||||||
'text-red-500': isError,
|
'text-red-500': isError,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<span className="text-neutral-10">
|
<span className="text-muted-foreground">
|
||||||
{format(new Date(props.historyItem.createdAt), 'HH:mm')}
|
{format(new Date(props.historyItem.createdAt), 'HH:mm')}
|
||||||
</span>
|
</span>
|
||||||
<div className="truncate">{props.historyItem.operation.name || 'Untitled'}</div>
|
<div className="truncate">{props.historyItem.operation.name || 'Untitled'}</div>
|
||||||
|
|
@ -89,7 +79,7 @@ export const HistoryOperationItem = (props: { historyItem: LaboratoryHistoryRequ
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
className="text-neutral-10 p-1! pr-0! ml-auto opacity-0 transition-opacity hover:text-red-500 group-hover:opacity-100"
|
className="text-muted-foreground hover:text-destructive p-1! pr-0! ml-auto opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
|
@ -137,13 +127,13 @@ export const HistoryGroup = (props: { group: { date: string; items: LaboratoryHi
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="bg-neutral-3 group sticky top-0 w-full justify-start px-2"
|
className="bg-background group sticky top-0 w-full justify-start px-2"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<FolderOpenIcon className="text-neutral-10 size-4" />
|
<FolderOpenIcon className="text-muted-foreground size-4" />
|
||||||
) : (
|
) : (
|
||||||
<FolderClockIcon className="text-neutral-10 size-4" />
|
<FolderClockIcon className="text-muted-foreground size-4" />
|
||||||
)}
|
)}
|
||||||
{props.group.date}
|
{props.group.date}
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|
@ -152,7 +142,7 @@ export const HistoryGroup = (props: { group: { date: string; items: LaboratoryHi
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
className="text-neutral-10 p-1! pr-0! ml-auto opacity-0 transition-opacity hover:text-red-500 group-hover:opacity-100"
|
className="text-muted-foreground hover:text-destructive p-1! pr-0! ml-auto opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
|
@ -188,7 +178,7 @@ export const HistoryGroup = (props: { group: { date: string; items: LaboratoryHi
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className={cn('border-neutral-5 ml-4 flex flex-col gap-1 border-l pl-2')}>
|
<CollapsibleContent className={cn('border-border ml-4 flex flex-col gap-1 border-l pl-2')}>
|
||||||
{props.group.items.map(h => {
|
{props.group.items.map(h => {
|
||||||
return <HistoryOperationItem key={h.id} historyItem={h as LaboratoryHistoryRequest} />;
|
return <HistoryOperationItem key={h.id} historyItem={h as LaboratoryHistoryRequest} />;
|
||||||
})}
|
})}
|
||||||
|
|
@ -239,7 +229,7 @@ export const History = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid size-full grid-rows-[auto_1fr] pb-0">
|
<div className="grid size-full grid-rows-[auto_1fr] pb-0">
|
||||||
<div className="border-neutral-5 flex h-12 items-center gap-2 border-b p-3">
|
<div className="border-border flex h-12 items-center gap-2 border-b p-3">
|
||||||
<span className="text-base font-medium">History</span>
|
<span className="text-base font-medium">History</span>
|
||||||
<div className="ml-auto flex items-center">
|
<div className="ml-auto flex items-center">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|
@ -249,7 +239,7 @@ export const History = () => {
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
className="text-neutral-10 p-1! size-6 rounded-sm hover:text-red-500"
|
className="text-muted-foreground hover:text-destructive p-1! size-6 rounded-sm"
|
||||||
disabled={history.length === 0}
|
disabled={history.length === 0}
|
||||||
>
|
>
|
||||||
<TrashIcon className="size-4" />
|
<TrashIcon className="size-4" />
|
||||||
|
|
@ -296,7 +286,7 @@ export const History = () => {
|
||||||
<Empty className="px-0! w-full">
|
<Empty className="px-0! w-full">
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyMedia variant="icon">
|
<EmptyMedia variant="icon">
|
||||||
<ClockIcon className="text-neutral-10 size-6" />
|
<ClockIcon className="text-muted-foreground size-6" />
|
||||||
</EmptyMedia>
|
</EmptyMedia>
|
||||||
<EmptyTitle className="text-base">No history yet</EmptyTitle>
|
<EmptyTitle className="text-base">No history yet</EmptyTitle>
|
||||||
<EmptyDescription className="text-xs">
|
<EmptyDescription className="text-xs">
|
||||||
|
|
@ -1,24 +1,22 @@
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { ReactNode, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import laboratoryStyles from '../../index.css?inline';
|
||||||
import { FileIcon, FoldersIcon, HistoryIcon, SettingsIcon } from 'lucide-react';
|
import { FileIcon, FoldersIcon, HistoryIcon, SettingsIcon } from 'lucide-react';
|
||||||
|
import monacoStyles from 'monaco-editor/min/vs/editor/editor.main.css?inline';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import { Markdown } from '@/components/v2/markdown';
|
import { useForm } from '@tanstack/react-form';
|
||||||
import { Collections } from '@/laboratory/components/laboratory/collections';
|
import { useCollections } from '../../lib/collections';
|
||||||
import { Command } from '@/laboratory/components/laboratory/command';
|
import { useEndpoint } from '../../lib/endpoint';
|
||||||
import {
|
import { useEnv } from '../../lib/env';
|
||||||
LaboratoryPermission,
|
import { useHistory } from '../../lib/history';
|
||||||
LaboratoryPermissions,
|
import { useOperations } from '../../lib/operations';
|
||||||
LaboratoryProvider,
|
import { LaboratoryPluginTab, usePlugins } from '../../lib/plugins';
|
||||||
useLaboratory,
|
import { usePreflight } from '../../lib/preflight';
|
||||||
type LaboratoryApi,
|
import { useSettings } from '../../lib/settings';
|
||||||
} from '@/laboratory/components/laboratory/context';
|
import { LaboratoryTabCustom, useTabs } from '../../lib/tabs';
|
||||||
import { Env } from '@/laboratory/components/laboratory/env';
|
import { useTests } from '../../lib/tests';
|
||||||
import { History } from '@/laboratory/components/laboratory/history';
|
import { cn } from '../../lib/utils';
|
||||||
import { HistoryItem } from '@/laboratory/components/laboratory/history-item';
|
import { Button } from '../ui/button';
|
||||||
import { Operation } from '@/laboratory/components/laboratory/operation';
|
|
||||||
import { Preflight } from '@/laboratory/components/laboratory/preflight';
|
|
||||||
import { Settings } from '@/laboratory/components/laboratory/settings';
|
|
||||||
import { Tabs } from '@/laboratory/components/laboratory/tabs';
|
|
||||||
import { Button } from '@/laboratory/components/ui/button';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
|
|
@ -27,7 +25,7 @@ import {
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/laboratory/components/ui/dialog';
|
} from '../ui/dialog';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
@ -36,7 +34,7 @@ import {
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/laboratory/components/ui/dropdown-menu';
|
} from '../ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
|
|
@ -44,34 +42,59 @@ import {
|
||||||
EmptyHeader,
|
EmptyHeader,
|
||||||
EmptyMedia,
|
EmptyMedia,
|
||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
} from '@/laboratory/components/ui/empty';
|
} from '../ui/empty';
|
||||||
|
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
||||||
|
import { Input } from '../ui/input';
|
||||||
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../ui/resizable';
|
||||||
|
import { Toaster } from '../ui/sonner';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||||
|
import { Collections } from './collections';
|
||||||
|
import { Command } from './command';
|
||||||
import {
|
import {
|
||||||
Field,
|
LaboratoryPermission,
|
||||||
FieldDescription,
|
LaboratoryPermissions,
|
||||||
FieldError,
|
LaboratoryProvider,
|
||||||
FieldGroup,
|
useLaboratory,
|
||||||
FieldLabel,
|
type LaboratoryApi,
|
||||||
} from '@/laboratory/components/ui/field';
|
} from './context';
|
||||||
import { Input } from '@/laboratory/components/ui/input';
|
import { Env } from './env';
|
||||||
import {
|
import { History } from './history';
|
||||||
ResizableHandle,
|
import { HistoryItem } from './history-item';
|
||||||
ResizablePanel,
|
import { Operation } from './operation';
|
||||||
ResizablePanelGroup,
|
import { Preflight } from './preflight';
|
||||||
} from '@/laboratory/components/ui/resizable';
|
import { Settings } from './settings';
|
||||||
import { Toaster } from '@/laboratory/components/ui/sonner';
|
import { Tabs } from './tabs';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/laboratory/components/ui/tooltip';
|
|
||||||
import { useCollections } from '@/laboratory/lib/collections';
|
const ShadowRootContainer = (props: { children: ReactNode }) => {
|
||||||
import { useEndpoint } from '@/laboratory/lib/endpoint';
|
const hostRef = useRef<HTMLDivElement | null>(null);
|
||||||
import { useEnv } from '@/laboratory/lib/env';
|
const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null);
|
||||||
import { useHistory } from '@/laboratory/lib/history';
|
|
||||||
import { useOperations } from '@/laboratory/lib/operations';
|
useLayoutEffect(() => {
|
||||||
import { LaboratoryPluginTab, usePlugins } from '@/laboratory/lib/plugins';
|
if (!hostRef.current || shadowRoot) {
|
||||||
import { usePreflight } from '@/laboratory/lib/preflight';
|
return;
|
||||||
import { useSettings } from '@/laboratory/lib/settings';
|
}
|
||||||
import { LaboratoryTabCustom, useTabs } from '@/laboratory/lib/tabs';
|
|
||||||
import { useTests } from '@/laboratory/lib/tests';
|
setShadowRoot(hostRef.current.attachShadow({ mode: 'open' }));
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
}, [shadowRoot]);
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!shadowRoot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.head.querySelectorAll('style').forEach(style => {
|
||||||
|
if (style.textContent?.includes('[data-sonner-toaster]')) {
|
||||||
|
shadowRoot.append(style.cloneNode(true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [shadowRoot]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={hostRef} className="hive-laboratory-host size-full">
|
||||||
|
{shadowRoot ? createPortal(props.children, shadowRoot) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const addCollectionFormSchema = z.object({
|
const addCollectionFormSchema = z.object({
|
||||||
name: z.string().min(1, 'Name is required'),
|
name: z.string().min(1, 'Name is required'),
|
||||||
|
|
@ -88,9 +111,7 @@ const addTestFormSchema = z.object({
|
||||||
const PreflightPromptModal = (props: {
|
const PreflightPromptModal = (props: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
title?: string;
|
placeholder: string;
|
||||||
description?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
onSubmit?: (value: string | null) => void;
|
onSubmit?: (value: string | null) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -125,6 +146,7 @@ const PreflightPromptModal = (props: {
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Preflight prompt</DialogTitle>
|
<DialogTitle>Preflight prompt</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
<DialogDescription>Enter values for the preflight script.</DialogDescription>
|
||||||
<form
|
<form
|
||||||
id="preflight-prompt-form"
|
id="preflight-prompt-form"
|
||||||
onSubmit={e => {
|
onSubmit={e => {
|
||||||
|
|
@ -136,15 +158,8 @@ const PreflightPromptModal = (props: {
|
||||||
<form.Field name="value">
|
<form.Field name="value">
|
||||||
{field => {
|
{field => {
|
||||||
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field data-invalid={isInvalid}>
|
<Field data-invalid={isInvalid}>
|
||||||
<FieldLabel htmlFor={field.name}>{props.title}</FieldLabel>
|
|
||||||
{props.description && (
|
|
||||||
<FieldDescription>
|
|
||||||
<Markdown content={props.description} />
|
|
||||||
</FieldDescription>
|
|
||||||
)}
|
|
||||||
<Input
|
<Input
|
||||||
id={field.name}
|
id={field.name}
|
||||||
name={field.name}
|
name={field.name}
|
||||||
|
|
@ -238,7 +253,7 @@ const LaboratoryContent = () => {
|
||||||
<Empty className="px-0! w-full">
|
<Empty className="px-0! w-full">
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyMedia variant="icon">
|
<EmptyMedia variant="icon">
|
||||||
<FileIcon className="text-neutral-10 size-6" />
|
<FileIcon className="text-muted-foreground size-6" />
|
||||||
</EmptyMedia>
|
</EmptyMedia>
|
||||||
<EmptyTitle>No operation selected</EmptyTitle>
|
<EmptyTitle>No operation selected</EmptyTitle>
|
||||||
<EmptyDescription>
|
<EmptyDescription>
|
||||||
|
|
@ -285,7 +300,7 @@ const LaboratoryContent = () => {
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-10 flex aspect-square h-12 w-full items-center justify-center border-l-2 border-transparent',
|
'relative z-10 flex aspect-square h-12 w-full items-center justify-center border-l-2 border-transparent',
|
||||||
{
|
{
|
||||||
'border-neutral-11': activePanel === 'collections',
|
'border-primary': activePanel === 'collections',
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -293,8 +308,8 @@ const LaboratoryContent = () => {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setActivePanel(activePanel === 'collections' ? null : 'collections')}
|
onClick={() => setActivePanel(activePanel === 'collections' ? null : 'collections')}
|
||||||
className={cn('text-neutral-10 hover:text-neutral-11', {
|
className={cn('text-muted-foreground hover:text-foreground', {
|
||||||
'text-neutral-11': activePanel === 'collections',
|
'text-foreground': activePanel === 'collections',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<FoldersIcon className="size-5" />
|
<FoldersIcon className="size-5" />
|
||||||
|
|
@ -309,7 +324,7 @@ const LaboratoryContent = () => {
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-10 flex aspect-square h-12 w-full items-center justify-center border-l-2 border-transparent',
|
'relative z-10 flex aspect-square h-12 w-full items-center justify-center border-l-2 border-transparent',
|
||||||
{
|
{
|
||||||
'border-neutral-11': activePanel === 'history',
|
'border-primary': activePanel === 'history',
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -317,8 +332,8 @@ const LaboratoryContent = () => {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setActivePanel(activePanel === 'history' ? null : 'history')}
|
onClick={() => setActivePanel(activePanel === 'history' ? null : 'history')}
|
||||||
className={cn('text-neutral-10 hover:text-neutral-11', {
|
className={cn('text-muted-foreground hover:text-foreground', {
|
||||||
'text-neutral-11': activePanel === 'history',
|
'text-foreground': activePanel === 'history',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<HistoryIcon className="size-5" />
|
<HistoryIcon className="size-5" />
|
||||||
|
|
@ -331,7 +346,7 @@ const LaboratoryContent = () => {
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-10 mt-auto flex aspect-square h-12 w-full items-center justify-center border-l-2 border-transparent',
|
'relative z-10 mt-auto flex aspect-square h-12 w-full items-center justify-center border-l-2 border-transparent',
|
||||||
{
|
{
|
||||||
'border-neutral-11': activePanel === 'settings',
|
'border-primary': activePanel === 'settings',
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -343,8 +358,8 @@ const LaboratoryContent = () => {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setActivePanel(activePanel === 'history' ? null : 'history')}
|
onClick={() => setActivePanel(activePanel === 'history' ? null : 'history')}
|
||||||
className={cn('text-neutral-10 hover:text-neutral-11', {
|
className={cn('text-muted-foreground hover:text-foreground', {
|
||||||
'text-neutral-11': activePanel === 'history',
|
'text-foreground': activePanel === 'history',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<SettingsIcon className="size-5" />
|
<SettingsIcon className="size-5" />
|
||||||
|
|
@ -418,7 +433,7 @@ const LaboratoryContent = () => {
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Tabs />
|
<Tabs />
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-neutral-3 flex-1 overflow-hidden">{contentNode}</div>
|
<div className="bg-card flex-1 overflow-hidden">{contentNode}</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -428,46 +443,50 @@ const LaboratoryContent = () => {
|
||||||
export type LaboratoryProps = LaboratoryApi;
|
export type LaboratoryProps = LaboratoryApi;
|
||||||
|
|
||||||
export const Laboratory = (
|
export const Laboratory = (
|
||||||
props: Pick<
|
props: Partial<
|
||||||
LaboratoryProps,
|
Pick<
|
||||||
| 'permissions'
|
LaboratoryProps,
|
||||||
| 'defaultEndpoint'
|
| 'permissions'
|
||||||
| 'onEndpointChange'
|
| 'defaultEndpoint'
|
||||||
| 'defaultCollections'
|
| 'onEndpointChange'
|
||||||
| 'onCollectionsChange'
|
| 'defaultCollections'
|
||||||
| 'onCollectionCreate'
|
| 'onCollectionsChange'
|
||||||
| 'onCollectionUpdate'
|
| 'onCollectionCreate'
|
||||||
| 'onCollectionDelete'
|
| 'onCollectionUpdate'
|
||||||
| 'onCollectionOperationCreate'
|
| 'onCollectionDelete'
|
||||||
| 'onCollectionOperationUpdate'
|
| 'onCollectionOperationCreate'
|
||||||
| 'onCollectionOperationDelete'
|
| 'onCollectionOperationUpdate'
|
||||||
| 'defaultOperations'
|
| 'onCollectionOperationDelete'
|
||||||
| 'onOperationsChange'
|
| 'defaultOperations'
|
||||||
| 'defaultActiveOperationId'
|
| 'onOperationsChange'
|
||||||
| 'onActiveOperationIdChange'
|
| 'defaultActiveOperationId'
|
||||||
| 'onOperationCreate'
|
| 'onActiveOperationIdChange'
|
||||||
| 'onOperationUpdate'
|
| 'onOperationCreate'
|
||||||
| 'onOperationDelete'
|
| 'onOperationUpdate'
|
||||||
| 'defaultHistory'
|
| 'onOperationDelete'
|
||||||
| 'onHistoryChange'
|
| 'defaultHistory'
|
||||||
| 'onHistoryCreate'
|
| 'onHistoryChange'
|
||||||
| 'onHistoryUpdate'
|
| 'onHistoryCreate'
|
||||||
| 'onHistoryDelete'
|
| 'onHistoryUpdate'
|
||||||
| 'defaultTabs'
|
| 'onHistoryDelete'
|
||||||
| 'onTabsChange'
|
| 'defaultTabs'
|
||||||
| 'defaultPreflight'
|
| 'onTabsChange'
|
||||||
| 'onPreflightChange'
|
| 'defaultPreflight'
|
||||||
| 'defaultEnv'
|
| 'onPreflightChange'
|
||||||
| 'onEnvChange'
|
| 'defaultEnv'
|
||||||
| 'defaultActiveTabId'
|
| 'onEnvChange'
|
||||||
| 'onActiveTabIdChange'
|
| 'defaultActiveTabId'
|
||||||
| 'defaultSettings'
|
| 'onActiveTabIdChange'
|
||||||
| 'onSettingsChange'
|
| 'defaultSettings'
|
||||||
| 'defaultTests'
|
| 'onSettingsChange'
|
||||||
| 'onTestsChange'
|
| 'defaultTests'
|
||||||
| 'plugins'
|
| 'onTestsChange'
|
||||||
| 'defaultPluginsState'
|
| 'plugins'
|
||||||
| 'onPluginsStateChange'
|
| 'defaultPluginsState'
|
||||||
|
| 'onPluginsStateChange'
|
||||||
|
| 'theme'
|
||||||
|
| 'defaultSchemaIntrospection'
|
||||||
|
>
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const checkPermissions = useCallback(
|
const checkPermissions = useCallback(
|
||||||
|
|
@ -485,51 +504,11 @@ export const Laboratory = (
|
||||||
[props.permissions],
|
[props.permissions],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [isPreflightPromptModalOpen, setIsPreflightPromptModalOpen] = useState(false);
|
|
||||||
|
|
||||||
const [preflightPromptModalProps, setPreflightPromptModalProps] = useState<{
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
onSubmit?: (value: string | null) => void;
|
|
||||||
}>({
|
|
||||||
title: undefined,
|
|
||||||
description: undefined,
|
|
||||||
placeholder: undefined,
|
|
||||||
defaultValue: undefined,
|
|
||||||
onSubmit: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const openPreflightPromptModal = useCallback(
|
|
||||||
(props: {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
onSubmit?: (value: string | null) => void;
|
|
||||||
}) => {
|
|
||||||
setPreflightPromptModalProps({
|
|
||||||
title: props.title,
|
|
||||||
description: props.description,
|
|
||||||
placeholder: props.placeholder,
|
|
||||||
defaultValue: props.defaultValue,
|
|
||||||
onSubmit: props.onSubmit,
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsPreflightPromptModalOpen(true);
|
|
||||||
}, 200);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const settingsApi = useSettings(props);
|
const settingsApi = useSettings(props);
|
||||||
const envApi = useEnv(props);
|
const envApi = useEnv(props);
|
||||||
const preflightApi = usePreflight({
|
const preflightApi = usePreflight({
|
||||||
...props,
|
...props,
|
||||||
envApi,
|
envApi,
|
||||||
openPreflightPromptModal,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const pluginsApi = usePlugins(props);
|
const pluginsApi = usePlugins(props);
|
||||||
|
|
@ -613,6 +592,37 @@ export const Laboratory = (
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [isPreflightPromptModalOpen, setIsPreflightPromptModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const [preflightPromptModalProps, setPreflightPromptModalProps] = useState<{
|
||||||
|
placeholder: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
onSubmit?: (value: string | null) => void;
|
||||||
|
}>({
|
||||||
|
placeholder: '',
|
||||||
|
defaultValue: undefined,
|
||||||
|
onSubmit: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const openPreflightPromptModal = useCallback(
|
||||||
|
(props: {
|
||||||
|
placeholder: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
onSubmit?: (value: string | null) => void;
|
||||||
|
}) => {
|
||||||
|
setPreflightPromptModalProps({
|
||||||
|
placeholder: props.placeholder,
|
||||||
|
defaultValue: props.defaultValue,
|
||||||
|
onSubmit: props.onSubmit,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsPreflightPromptModalOpen(true);
|
||||||
|
}, 200);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||||
|
|
@ -626,193 +636,192 @@ export const Laboratory = (
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<ShadowRootContainer>
|
||||||
className={cn('hive-laboratory bg-neutral-3 size-full', {
|
<style>{`${laboratoryStyles}\n${monacoStyles}`}</style>
|
||||||
'fixed inset-0 z-50': isFullScreen,
|
<div
|
||||||
})}
|
className={cn('hive-laboratory bg-background size-full', props.theme, {
|
||||||
style={
|
'fixed inset-0 z-50': isFullScreen,
|
||||||
{
|
})}
|
||||||
'--color-primary': 'var(--color-orange-500)',
|
ref={containerRef}
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
ref={containerRef}
|
|
||||||
>
|
|
||||||
<Toaster richColors closeButton position="top-right" />
|
|
||||||
<Dialog open={isUpdateEndpointDialogOpen} onOpenChange={setIsUpdateEndpointDialogOpen}>
|
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Update endpoint</DialogTitle>
|
|
||||||
<DialogDescription>Update the endpoint of your laboratory.</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<form
|
|
||||||
id="update-endpoint-form"
|
|
||||||
onSubmit={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
void updateEndpointForm.handleSubmit();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FieldGroup>
|
|
||||||
<updateEndpointForm.Field name="endpoint">
|
|
||||||
{field => {
|
|
||||||
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={e => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
placeholder="Enter endpoint"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</updateEndpointForm.Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="outline">Cancel</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button type="submit" form="update-endpoint-form">
|
|
||||||
Update endpoint
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
<PreflightPromptModal
|
|
||||||
open={isPreflightPromptModalOpen}
|
|
||||||
onOpenChange={setIsPreflightPromptModalOpen}
|
|
||||||
{...preflightPromptModalProps}
|
|
||||||
/>
|
|
||||||
<Dialog open={isAddCollectionDialogOpen} onOpenChange={setIsAddCollectionDialogOpen}>
|
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Add collection</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Add a new collection of operations to your laboratory.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<form
|
|
||||||
id="add-collection-form"
|
|
||||||
onSubmit={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
void addCollectionForm.handleSubmit();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FieldGroup>
|
|
||||||
<addCollectionForm.Field name="name">
|
|
||||||
{field => {
|
|
||||||
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid}>
|
|
||||||
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={e => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
placeholder="Enter name of the collection"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</addCollectionForm.Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="outline">Cancel</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button type="submit" form="add-collection-form">
|
|
||||||
Add collection
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
<Dialog open={isAddTestDialogOpen} onOpenChange={setIsAddTestDialogOpen}>
|
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Add test</DialogTitle>
|
|
||||||
<DialogDescription>Add a new test to your laboratory.</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<form
|
|
||||||
id="add-test-form"
|
|
||||||
onSubmit={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
void addTestForm.handleSubmit();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FieldGroup>
|
|
||||||
<addTestForm.Field name="name">
|
|
||||||
{field => {
|
|
||||||
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
|
||||||
return (
|
|
||||||
<Field data-invalid={isInvalid}>
|
|
||||||
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={e => field.handleChange(e.target.value)}
|
|
||||||
aria-invalid={isInvalid}
|
|
||||||
placeholder="Enter name of the test"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</addTestForm.Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="outline">Cancel</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button type="submit" form="add-test-form">
|
|
||||||
Add test
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<LaboratoryProvider
|
|
||||||
{...props}
|
|
||||||
{...testsApi}
|
|
||||||
{...settingsApi}
|
|
||||||
{...pluginsApi}
|
|
||||||
{...envApi}
|
|
||||||
{...preflightApi}
|
|
||||||
{...tabsApi}
|
|
||||||
{...endpointApi}
|
|
||||||
{...collectionsApi}
|
|
||||||
{...operationsApi}
|
|
||||||
{...historyApi}
|
|
||||||
openAddCollectionDialog={openAddCollectionDialog}
|
|
||||||
openUpdateEndpointDialog={openUpdateEndpointDialog}
|
|
||||||
openAddTestDialog={openAddTestDialog}
|
|
||||||
openPreflightPromptModal={openPreflightPromptModal}
|
|
||||||
goToFullScreen={goToFullScreen}
|
|
||||||
exitFullScreen={exitFullScreen}
|
|
||||||
isFullScreen={isFullScreen}
|
|
||||||
checkPermissions={checkPermissions}
|
|
||||||
>
|
>
|
||||||
<LaboratoryContent />
|
<Toaster richColors closeButton position="top-right" theme={props.theme} />
|
||||||
</LaboratoryProvider>
|
<Dialog open={isUpdateEndpointDialogOpen} onOpenChange={setIsUpdateEndpointDialogOpen}>
|
||||||
</div>
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update endpoint</DialogTitle>
|
||||||
|
<DialogDescription>Update the endpoint of your laboratory.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<form
|
||||||
|
id="update-endpoint-form"
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
void updateEndpointForm.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FieldGroup>
|
||||||
|
<updateEndpointForm.Field name="endpoint">
|
||||||
|
{field => {
|
||||||
|
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={e => field.handleChange(e.target.value)}
|
||||||
|
aria-invalid={isInvalid}
|
||||||
|
placeholder="Enter endpoint"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</updateEndpointForm.Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button type="submit" form="update-endpoint-form">
|
||||||
|
Update endpoint
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
<PreflightPromptModal
|
||||||
|
open={isPreflightPromptModalOpen}
|
||||||
|
onOpenChange={setIsPreflightPromptModalOpen}
|
||||||
|
{...preflightPromptModalProps}
|
||||||
|
/>
|
||||||
|
<Dialog open={isAddCollectionDialogOpen} onOpenChange={setIsAddCollectionDialogOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add collection</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Add a new collection of operations to your laboratory.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<form
|
||||||
|
id="add-collection-form"
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
void addCollectionForm.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FieldGroup>
|
||||||
|
<addCollectionForm.Field name="name">
|
||||||
|
{field => {
|
||||||
|
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
||||||
|
return (
|
||||||
|
<Field data-invalid={isInvalid}>
|
||||||
|
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={e => field.handleChange(e.target.value)}
|
||||||
|
aria-invalid={isInvalid}
|
||||||
|
placeholder="Enter name of the collection"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</addCollectionForm.Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button type="submit" form="add-collection-form">
|
||||||
|
Add collection
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
<Dialog open={isAddTestDialogOpen} onOpenChange={setIsAddTestDialogOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add test</DialogTitle>
|
||||||
|
<DialogDescription>Add a new test to your laboratory.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<form
|
||||||
|
id="add-test-form"
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
void addTestForm.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FieldGroup>
|
||||||
|
<addTestForm.Field name="name">
|
||||||
|
{field => {
|
||||||
|
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
||||||
|
return (
|
||||||
|
<Field data-invalid={isInvalid}>
|
||||||
|
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={e => field.handleChange(e.target.value)}
|
||||||
|
aria-invalid={isInvalid}
|
||||||
|
placeholder="Enter name of the test"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</addTestForm.Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button type="submit" form="add-test-form">
|
||||||
|
Add test
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<LaboratoryProvider
|
||||||
|
{...props}
|
||||||
|
{...testsApi}
|
||||||
|
{...settingsApi}
|
||||||
|
{...pluginsApi}
|
||||||
|
{...envApi}
|
||||||
|
{...preflightApi}
|
||||||
|
{...tabsApi}
|
||||||
|
{...endpointApi}
|
||||||
|
{...collectionsApi}
|
||||||
|
{...operationsApi}
|
||||||
|
{...historyApi}
|
||||||
|
container={containerRef.current}
|
||||||
|
openAddCollectionDialog={openAddCollectionDialog}
|
||||||
|
openUpdateEndpointDialog={openUpdateEndpointDialog}
|
||||||
|
openAddTestDialog={openAddTestDialog}
|
||||||
|
openPreflightPromptModal={openPreflightPromptModal}
|
||||||
|
goToFullScreen={goToFullScreen}
|
||||||
|
exitFullScreen={exitFullScreen}
|
||||||
|
isFullScreen={isFullScreen}
|
||||||
|
checkPermissions={checkPermissions}
|
||||||
|
>
|
||||||
|
<LaboratoryContent />
|
||||||
|
</LaboratoryProvider>
|
||||||
|
</div>
|
||||||
|
</ShadowRootContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -16,11 +16,17 @@ import { compressToEncodedURIComponent } from 'lz-string';
|
||||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Builder } from '@/laboratory/components/laboratory/builder';
|
import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu';
|
||||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
import { useForm } from '@tanstack/react-form';
|
||||||
import { Editor } from '@/laboratory/components/laboratory/editor';
|
import type {
|
||||||
import { Badge } from '@/laboratory/components/ui/badge';
|
LaboratoryHistory,
|
||||||
import { Button } from '@/laboratory/components/ui/button';
|
LaboratoryHistoryRequest,
|
||||||
|
LaboratoryHistorySubscription,
|
||||||
|
} from '../../lib/history';
|
||||||
|
import type { LaboratoryOperation } from '../../lib/operations';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
import { Badge } from '../ui/badge';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
|
|
@ -29,45 +35,19 @@ import {
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/laboratory/components/ui/dialog';
|
} from '../ui/dialog';
|
||||||
import {
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from '../ui/dropdown-menu';
|
||||||
DropdownMenu,
|
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../ui/empty';
|
||||||
DropdownMenuContent,
|
import { Field, FieldGroup, FieldLabel } from '../ui/field';
|
||||||
DropdownMenuItem,
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../ui/resizable';
|
||||||
} from '@/laboratory/components/ui/dropdown-menu';
|
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
||||||
import {
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||||
Empty,
|
import { Spinner } from '../ui/spinner';
|
||||||
EmptyDescription,
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||||
EmptyHeader,
|
import { Toggle } from '../ui/toggle';
|
||||||
EmptyMedia,
|
import { Builder } from './builder';
|
||||||
EmptyTitle,
|
import { useLaboratory } from './context';
|
||||||
} from '@/laboratory/components/ui/empty';
|
import { Editor } from './editor';
|
||||||
import { Field, FieldGroup, FieldLabel } from '@/laboratory/components/ui/field';
|
|
||||||
import {
|
|
||||||
ResizableHandle,
|
|
||||||
ResizablePanel,
|
|
||||||
ResizablePanelGroup,
|
|
||||||
} from '@/laboratory/components/ui/resizable';
|
|
||||||
import { ScrollArea, ScrollBar } from '@/laboratory/components/ui/scroll-area';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/laboratory/components/ui/select';
|
|
||||||
import { Spinner } from '@/laboratory/components/ui/spinner';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/laboratory/components/ui/tabs';
|
|
||||||
import { Toggle } from '@/laboratory/components/ui/toggle';
|
|
||||||
import type {
|
|
||||||
LaboratoryHistory,
|
|
||||||
LaboratoryHistoryRequest,
|
|
||||||
LaboratoryHistorySubscription,
|
|
||||||
} from '@/laboratory/lib/history';
|
|
||||||
import type { LaboratoryOperation } from '@/laboratory/lib/operations';
|
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu';
|
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
|
|
||||||
const variablesUri = monaco.Uri.file('variables.json');
|
const variablesUri = monaco.Uri.file('variables.json');
|
||||||
|
|
||||||
|
|
@ -178,14 +158,14 @@ export const ResponsePreflight = ({ historyItem }: { historyItem?: LaboratoryHis
|
||||||
<div className="flex flex-col gap-1.5 whitespace-pre-wrap p-3">
|
<div className="flex flex-col gap-1.5 whitespace-pre-wrap p-3">
|
||||||
{historyItem?.preflightLogs?.map((log, i) => (
|
{historyItem?.preflightLogs?.map((log, i) => (
|
||||||
<div className="gap-2 font-mono" key={i}>
|
<div className="gap-2 font-mono" key={i}>
|
||||||
<span className="text-neutral-10 text-xs">{log.createdAt}</span>{' '}
|
<span className="text-muted-foreground text-xs">{log.createdAt}</span>{' '}
|
||||||
<span
|
<span
|
||||||
className={cn('text-xs font-medium', {
|
className={cn('text-xs font-medium', {
|
||||||
'text-blue-400': log.level === 'info',
|
'text-blue-400': log.level === 'info',
|
||||||
'text-green-400': log.level === 'log',
|
'text-green-400': log.level === 'log',
|
||||||
'text-yellow-400': log.level === 'warn',
|
'text-yellow-400': log.level === 'warn',
|
||||||
'text-red-400': log.level === 'error',
|
'text-red-400': log.level === 'error',
|
||||||
'text-neutral-10': log.level === 'system',
|
'text-gray-400': log.level === 'system',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{log.level.toUpperCase()}
|
{log.level.toUpperCase()}
|
||||||
|
|
@ -208,7 +188,7 @@ export const ResponseSubscription = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="border-neutral-5 flex h-12 border-b p-3 text-base font-medium">
|
<div className="border-border flex h-12 border-b p-3 text-base font-medium">
|
||||||
Subscription
|
Subscription
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
{isActiveOperationLoading ? (
|
{isActiveOperationLoading ? (
|
||||||
|
|
@ -224,7 +204,7 @@ export const ResponseSubscription = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<ScrollArea className="h-full">
|
<ScrollArea className="h-full">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col overflow-hidden">
|
||||||
{historyItem?.responses
|
{historyItem?.responses
|
||||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||||
.map((response, i) => {
|
.map((response, i) => {
|
||||||
|
|
@ -237,11 +217,7 @@ export const ResponseSubscription = ({
|
||||||
const height = 20.5 * value.split('\n').length;
|
const height = 20.5 * value.split('\n').length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="border-border border-b" style={{ height: `${height}px` }} key={i}>
|
||||||
className="border-neutral-5 border-b"
|
|
||||||
style={{ height: `${height}px` }}
|
|
||||||
key={i}
|
|
||||||
>
|
|
||||||
<Editor
|
<Editor
|
||||||
key={response.createdAt}
|
key={response.createdAt}
|
||||||
value={value}
|
value={value}
|
||||||
|
|
@ -316,7 +292,7 @@ export const Response = ({ historyItem }: { historyItem?: LaboratoryHistoryReque
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{historyItem?.duration && (
|
{historyItem?.duration && (
|
||||||
<Badge variant="outline" className="bg-neutral-2">
|
<Badge variant="outline" className="bg-card">
|
||||||
<ClockIcon className="size-3" />
|
<ClockIcon className="size-3" />
|
||||||
<span>
|
<span>
|
||||||
{Math.round((historyItem as LaboratoryHistoryRequest).duration!)}
|
{Math.round((historyItem as LaboratoryHistoryRequest).duration!)}
|
||||||
|
|
@ -325,7 +301,7 @@ export const Response = ({ historyItem }: { historyItem?: LaboratoryHistoryReque
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{historyItem?.size && (
|
{historyItem?.size && (
|
||||||
<Badge variant="outline" className="bg-neutral-2">
|
<Badge variant="outline" className="bg-card">
|
||||||
<FileTextIcon className="size-3" />
|
<FileTextIcon className="size-3" />
|
||||||
<span>
|
<span>
|
||||||
{Math.round((historyItem as LaboratoryHistoryRequest).size! / 1024)}
|
{Math.round((historyItem as LaboratoryHistoryRequest).size! / 1024)}
|
||||||
|
|
@ -336,13 +312,13 @@ export const Response = ({ historyItem }: { historyItem?: LaboratoryHistoryReque
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="response">
|
<TabsContent value="response" className="overflow-hidden">
|
||||||
<ResponseBody historyItem={historyItem} />
|
<ResponseBody historyItem={historyItem} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="headers">
|
<TabsContent value="headers" className="overflow-hidden">
|
||||||
<ResponseHeaders historyItem={historyItem} />
|
<ResponseHeaders historyItem={historyItem} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="preflight">
|
<TabsContent value="preflight" className="overflow-hidden">
|
||||||
<ResponsePreflight historyItem={historyItem} />
|
<ResponsePreflight historyItem={historyItem} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
@ -549,7 +525,7 @@ export const Query = (props: {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid size-full grid-rows-[auto_1fr] pb-0">
|
<div className="grid size-full grid-rows-[auto_1fr] overflow-hidden pb-0">
|
||||||
<Dialog open={isSaveToCollectionDialogOpen} onOpenChange={setIsSaveToCollectionDialogOpen}>
|
<Dialog open={isSaveToCollectionDialogOpen} onOpenChange={setIsSaveToCollectionDialogOpen}>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|
@ -607,7 +583,7 @@ export const Query = (props: {
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<div className="border-neutral-5 flex w-full items-center gap-2 border-b p-3">
|
<div className="border-border flex w-full items-center gap-2 overflow-hidden border-b p-3">
|
||||||
<span className="text-base font-medium">Operation</span>
|
<span className="text-base font-medium">Operation</span>
|
||||||
{checkPermissions?.('collectionsOperations:create') && (
|
{checkPermissions?.('collectionsOperations:create') && (
|
||||||
<Toggle
|
<Toggle
|
||||||
|
|
@ -650,7 +626,7 @@ export const Query = (props: {
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="default"
|
variant="default"
|
||||||
pressed={preflight?.enabled}
|
pressed={preflight?.enabled}
|
||||||
className="bg-neutral-3 hover:bg-neutral-2 hover:text-neutral-12 h-6 rounded-sm border shadow-sm data-[state=on]:bg-transparent"
|
className="hover:text-accent-foreground bg-input/30 border-input hover:bg-input/50 h-6 rounded-sm border shadow-sm data-[state=on]:bg-transparent"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPreflight({
|
setPreflight({
|
||||||
...(preflight ?? { script: '', enabled: true }),
|
...(preflight ?? { script: '', enabled: true }),
|
||||||
|
|
@ -715,23 +691,21 @@ export const Query = (props: {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="size-full">
|
<Editor
|
||||||
<Editor
|
uri={monaco.Uri.file(`operation_${endpoint}.graphql`)}
|
||||||
uri={monaco.Uri.file(`operation_${endpoint}.graphql`)}
|
variablesUri={variablesUri}
|
||||||
variablesUri={variablesUri}
|
value={operation?.query ?? ''}
|
||||||
value={operation?.query ?? ''}
|
onChange={value => {
|
||||||
onChange={value => {
|
updateActiveOperation({
|
||||||
updateActiveOperation({
|
query: value ?? '',
|
||||||
query: value ?? '',
|
});
|
||||||
});
|
}}
|
||||||
}}
|
language="graphql"
|
||||||
language="graphql"
|
theme="hive-laboratory"
|
||||||
theme="hive-laboratory"
|
options={{
|
||||||
options={{
|
readOnly: props.isReadOnly,
|
||||||
readOnly: props.isReadOnly,
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -761,7 +735,7 @@ export const Operation = (props: {
|
||||||
}, [props.historyItem]);
|
}, [props.historyItem]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-neutral-2 size-full">
|
<div className="bg-card size-full">
|
||||||
<ResizablePanelGroup direction="horizontal" className="size-full">
|
<ResizablePanelGroup direction="horizontal" className="size-full">
|
||||||
<ResizablePanel defaultSize={25}>
|
<ResizablePanel defaultSize={25}>
|
||||||
<Builder operation={operation} isReadOnly={isReadOnly} />
|
<Builder operation={operation} isReadOnly={isReadOnly} />
|
||||||
|
|
@ -773,7 +747,7 @@ export const Operation = (props: {
|
||||||
<Query operation={operation} isReadOnly={isReadOnly} />
|
<Query operation={operation} isReadOnly={isReadOnly} />
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle />
|
||||||
<ResizablePanel minSize={10} defaultSize={30} className="overflow-visible!">
|
<ResizablePanel minSize={10} defaultSize={30}>
|
||||||
<Tabs className="grid size-full grid-rows-[auto_1fr]" defaultValue="variables">
|
<Tabs className="grid size-full grid-rows-[auto_1fr]" defaultValue="variables">
|
||||||
<TabsList className="h-[49.5px] w-full justify-start rounded-none border-b bg-transparent p-3">
|
<TabsList className="h-[49.5px] w-full justify-start rounded-none border-b bg-transparent p-3">
|
||||||
<TabsTrigger value="variables" className="grow-0 rounded-sm">
|
<TabsTrigger value="variables" className="grow-0 rounded-sm">
|
||||||
|
|
@ -786,13 +760,13 @@ export const Operation = (props: {
|
||||||
Extensions
|
Extensions
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="variables">
|
<TabsContent value="variables" className="overflow-hidden">
|
||||||
<Variables operation={operation} isReadOnly={isReadOnly} />
|
<Variables operation={operation} isReadOnly={isReadOnly} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="headers">
|
<TabsContent value="headers" className="overflow-hidden">
|
||||||
<Headers operation={operation} isReadOnly={isReadOnly} />
|
<Headers operation={operation} isReadOnly={isReadOnly} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="extensions">
|
<TabsContent value="extensions" className="overflow-hidden">
|
||||||
<Extensions operation={operation} isReadOnly={isReadOnly} />
|
<Extensions operation={operation} isReadOnly={isReadOnly} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
@ -813,7 +787,7 @@ export const Operation = (props: {
|
||||||
<Empty className="size-full">
|
<Empty className="size-full">
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyMedia variant="icon">
|
<EmptyMedia variant="icon">
|
||||||
<HistoryIcon className="text-neutral-10 size-6" />
|
<HistoryIcon className="text-muted-foreground size-6" />
|
||||||
</EmptyMedia>
|
</EmptyMedia>
|
||||||
<EmptyTitle>No history yet</EmptyTitle>
|
<EmptyTitle>No history yet</EmptyTitle>
|
||||||
<EmptyDescription>
|
<EmptyDescription>
|
||||||
|
|
@ -1,23 +1,13 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { HistoryIcon, PlayIcon } from 'lucide-react';
|
import { HistoryIcon, PlayIcon } from 'lucide-react';
|
||||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
import { runIsolatedLabScript } from '../../lib/preflight';
|
||||||
import { Editor } from '@/laboratory/components/laboratory/editor';
|
import { cn } from '../../lib/utils';
|
||||||
import { Button } from '@/laboratory/components/ui/button';
|
import { Button } from '../ui/button';
|
||||||
import {
|
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../ui/empty';
|
||||||
Empty,
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../ui/resizable';
|
||||||
EmptyDescription,
|
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
||||||
EmptyHeader,
|
import { useLaboratory } from './context';
|
||||||
EmptyMedia,
|
import { Editor } from './editor';
|
||||||
EmptyTitle,
|
|
||||||
} from '@/laboratory/components/ui/empty';
|
|
||||||
import {
|
|
||||||
ResizableHandle,
|
|
||||||
ResizablePanel,
|
|
||||||
ResizablePanelGroup,
|
|
||||||
} from '@/laboratory/components/ui/resizable';
|
|
||||||
import { ScrollArea, ScrollBar } from '@/laboratory/components/ui/scroll-area';
|
|
||||||
import { runIsolatedLabScript } from '@/laboratory/lib/preflight';
|
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
|
|
||||||
export const Preflight = () => {
|
export const Preflight = () => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -41,12 +31,10 @@ export const Preflight = () => {
|
||||||
const result = await runIsolatedLabScript(
|
const result = await runIsolatedLabScript(
|
||||||
preflight?.script ?? '',
|
preflight?.script ?? '',
|
||||||
env ?? { variables: {} },
|
env ?? { variables: {} },
|
||||||
(title, defaultValue, options) => {
|
(placeholder, defaultValue) => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
openPreflightPromptModal?.({
|
openPreflightPromptModal?.({
|
||||||
title,
|
placeholder,
|
||||||
description: options?.description,
|
|
||||||
placeholder: options?.placeholder,
|
|
||||||
defaultValue,
|
defaultValue,
|
||||||
onSubmit: value => {
|
onSubmit: value => {
|
||||||
resolve(value);
|
resolve(value);
|
||||||
|
|
@ -65,9 +53,9 @@ export const Preflight = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResizablePanelGroup direction="horizontal" className="size-full">
|
<ResizablePanelGroup direction="horizontal" className="size-full">
|
||||||
<ResizablePanel defaultSize={50} className="bg-neutral-2">
|
<ResizablePanel defaultSize={50} className="bg-card">
|
||||||
<div className="grid size-full grid-rows-[auto_1fr] pb-0">
|
<div className="grid size-full grid-rows-[auto_1fr] pb-0">
|
||||||
<div className="border-neutral-5 flex w-full items-center gap-2 border-b p-3">
|
<div className="border-border flex w-full items-center gap-2 border-b p-3">
|
||||||
<span className="text-base font-medium">Preflight</span>
|
<span className="text-base font-medium">Preflight</span>
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
<Button variant="default" size="sm" className="h-6 rounded-sm" onClick={run}>
|
<Button variant="default" size="sm" className="h-6 rounded-sm" onClick={run}>
|
||||||
|
|
@ -97,7 +85,7 @@ export const Preflight = () => {
|
||||||
request: {
|
request: {
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
};
|
};
|
||||||
prompt: (title: string, defaultValue: string, options?: { placeholder?: string; description?: string }) => Promise<string | null>;
|
prompt: (placeholder: string, defaultValue: string) => Promise<string | null>;
|
||||||
CryptoJS: typeof CryptoJS;
|
CryptoJS: typeof CryptoJS;
|
||||||
plugins: {
|
plugins: {
|
||||||
${plugins
|
${plugins
|
||||||
|
|
@ -253,10 +241,10 @@ export const Preflight = () => {
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle />
|
||||||
<ResizablePanel minSize={10} defaultSize={50} className="bg-neutral-2">
|
<ResizablePanel minSize={10} defaultSize={50} className="bg-card">
|
||||||
{preflight?.lastTestResult?.logs && preflight?.lastTestResult?.logs.length > 0 ? (
|
{preflight?.lastTestResult?.logs && preflight?.lastTestResult?.logs.length > 0 ? (
|
||||||
<div className="grid size-full grid-rows-[auto_1fr] pb-0">
|
<div className="grid size-full grid-rows-[auto_1fr] pb-0">
|
||||||
<div className="border-neutral-5 flex h-12 w-full items-center gap-2 border-b p-3">
|
<div className="border-border flex h-12 w-full items-center gap-2 border-b p-3">
|
||||||
<span className="text-base font-medium">Logs</span>
|
<span className="text-base font-medium">Logs</span>
|
||||||
<div className="ml-auto flex items-center gap-2" />
|
<div className="ml-auto flex items-center gap-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -264,14 +252,14 @@ export const Preflight = () => {
|
||||||
<div className="flex flex-col gap-1.5 whitespace-pre-wrap p-3">
|
<div className="flex flex-col gap-1.5 whitespace-pre-wrap p-3">
|
||||||
{preflight?.lastTestResult?.logs.map((log, i) => (
|
{preflight?.lastTestResult?.logs.map((log, i) => (
|
||||||
<div className="gap-2 font-mono" key={i}>
|
<div className="gap-2 font-mono" key={i}>
|
||||||
<span className="text-neutral-10 text-xs">{log.createdAt}</span>{' '}
|
<span className="text-muted-foreground text-xs">{log.createdAt}</span>{' '}
|
||||||
<span
|
<span
|
||||||
className={cn('text-xs font-medium', {
|
className={cn('text-xs font-medium', {
|
||||||
'text-blue-400': log.level === 'info',
|
'text-blue-400': log.level === 'info',
|
||||||
'text-green-400': log.level === 'log',
|
'text-green-400': log.level === 'log',
|
||||||
'text-yellow-400': log.level === 'warn',
|
'text-yellow-400': log.level === 'warn',
|
||||||
'text-red-400': log.level === 'error',
|
'text-red-400': log.level === 'error',
|
||||||
'text-neutral-10': log.level === 'system',
|
'text-gray-400': log.level === 'system',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{log.level.toUpperCase()}
|
{log.level.toUpperCase()}
|
||||||
|
|
@ -287,7 +275,7 @@ export const Preflight = () => {
|
||||||
<Empty className="size-full">
|
<Empty className="size-full">
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyMedia variant="icon">
|
<EmptyMedia variant="icon">
|
||||||
<HistoryIcon className="text-neutral-10 size-6" />
|
<HistoryIcon className="text-muted-foreground size-6" />
|
||||||
</EmptyMedia>
|
</EmptyMedia>
|
||||||
<EmptyTitle>No logs yet</EmptyTitle>
|
<EmptyTitle>No logs yet</EmptyTitle>
|
||||||
<EmptyDescription>
|
<EmptyDescription>
|
||||||
|
|
@ -1,21 +1,9 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/laboratory/components/ui/card';
|
|
||||||
import { Field, FieldGroup, FieldLabel } from '@/laboratory/components/ui/field';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/laboratory/components/ui/select';
|
|
||||||
import { useForm } from '@tanstack/react-form';
|
import { useForm } from '@tanstack/react-form';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||||
|
import { Field, FieldGroup, FieldLabel } from '../ui/field';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||||
|
import { useLaboratory } from './context';
|
||||||
|
|
||||||
const settingsFormSchema = z.object({
|
const settingsFormSchema = z.object({
|
||||||
fetch: z.object({
|
fetch: z.object({
|
||||||
|
|
@ -37,7 +25,7 @@ export const Settings = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-neutral-2 size-full p-3">
|
<div className="bg-card size-full p-3">
|
||||||
<form
|
<form
|
||||||
id="settings-form"
|
id="settings-form"
|
||||||
onSubmit={form.handleSubmit}
|
onSubmit={form.handleSubmit}
|
||||||
|
|
@ -13,28 +13,28 @@ import {
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { GraphQLIcon } from '@/laboratory/components/icons';
|
import { getOperationName, getOperationType } from '../../lib/operations.utils';
|
||||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
import { LaboratoryPluginTab } from '../../lib/plugins';
|
||||||
import { Button } from '@/laboratory/components/ui/button';
|
|
||||||
import {
|
|
||||||
ContextMenu,
|
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuTrigger,
|
|
||||||
} from '@/laboratory/components/ui/context-menu';
|
|
||||||
import { ScrollArea, ScrollBar } from '@/laboratory/components/ui/scroll-area';
|
|
||||||
import * as Sortable from '@/laboratory/components/ui/sortable';
|
|
||||||
import { Spinner } from '@/laboratory/components/ui/spinner';
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/laboratory/components/ui/tooltip';
|
|
||||||
import { getOperationName, getOperationType } from '@/laboratory/lib/operations.utils';
|
|
||||||
import { LaboratoryPluginTab } from '@/laboratory/lib/plugins';
|
|
||||||
import type {
|
import type {
|
||||||
LaboratoryTab,
|
LaboratoryTab,
|
||||||
LaboratoryTabHistory,
|
LaboratoryTabHistory,
|
||||||
LaboratoryTabOperation,
|
LaboratoryTabOperation,
|
||||||
LaboratoryTabTest,
|
LaboratoryTabTest,
|
||||||
} from '@/laboratory/lib/tabs';
|
} from '../../lib/tabs';
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
|
import { GraphQLIcon } from '../icons';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from '../ui/context-menu';
|
||||||
|
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
||||||
|
import * as Sortable from '../ui/sortable';
|
||||||
|
import { Spinner } from '../ui/spinner';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||||
|
import { useLaboratory } from './context';
|
||||||
|
|
||||||
export const Tab = (props: {
|
export const Tab = (props: {
|
||||||
item: LaboratoryTab;
|
item: LaboratoryTab;
|
||||||
|
|
@ -213,7 +213,7 @@ export const Tab = (props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.item.type === 'settings') {
|
if (props.item.type === 'settings') {
|
||||||
return <SettingsIcon className="text-neutral-10 size-4" />;
|
return <SettingsIcon className="size-4 text-gray-400" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.item.type === 'test') {
|
if (props.item.type === 'test') {
|
||||||
|
|
@ -239,7 +239,7 @@ export const Tab = (props: {
|
||||||
return customTab.icon;
|
return customTab.icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <FileIcon className="text-neutral-8 size-4" />;
|
return <FileIcon className="text-muted-foreground size-4" />;
|
||||||
}, [props.item, isError]);
|
}, [props.item, isError]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -250,7 +250,7 @@ export const Tab = (props: {
|
||||||
asHandle
|
asHandle
|
||||||
className={cn(
|
className={cn(
|
||||||
'data-dragging:opacity-0 flex h-12 w-max items-stretch',
|
'data-dragging:opacity-0 flex h-12 w-max items-stretch',
|
||||||
props.isOverlay && 'bg-neutral-3',
|
props.isOverlay && 'bg-background',
|
||||||
props.isOverlay && !isActive && 'h-12',
|
props.isOverlay && !isActive && 'h-12',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -278,9 +278,8 @@ export const Tab = (props: {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-neutral-10 hover:text-neutral-11 group relative flex h-full cursor-pointer items-center gap-2 border-t-2 border-transparent px-3 pb-1 text-sm transition-all',
|
'text-muted-foreground hover:text-foreground group relative flex h-full cursor-pointer items-center gap-2 border-t-2 border-transparent px-3 pb-1 text-sm transition-all',
|
||||||
props.activeTab?.id === props.item.id &&
|
props.activeTab?.id === props.item.id && 'border-primary bg-card text-foreground',
|
||||||
'border-neutral-11 bg-neutral-2 text-neutral-11',
|
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.setActiveTab(props.item);
|
props.setActiveTab(props.item);
|
||||||
|
|
@ -297,9 +296,9 @@ export const Tab = (props: {
|
||||||
{tabIcon}
|
{tabIcon}
|
||||||
{tabName}
|
{tabName}
|
||||||
{props.isOperationLoading(props.item.id) && <Spinner className="size-3" />}
|
{props.isOperationLoading(props.item.id) && <Spinner className="size-3" />}
|
||||||
{props.item.readOnly && <LockIcon className="text-neutral-10 size-3" />}
|
{props.item.readOnly && <LockIcon className="size-3 text-gray-400" />}
|
||||||
<XIcon
|
<XIcon
|
||||||
className="text-neutral-10 size-3"
|
className="text-muted-foreground size-3"
|
||||||
onMouseDown={e => {
|
onMouseDown={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
|
@ -310,7 +309,7 @@ export const Tab = (props: {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-neutral-5 mb-px w-px" />
|
<div className="bg-border mb-px w-px" />
|
||||||
</Sortable.Item>
|
</Sortable.Item>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
|
|
@ -412,7 +411,7 @@ export const Tabs = ({ className }: { className?: string }) => {
|
||||||
<div
|
<div
|
||||||
className={cn('relative z-10 grid size-full grid-cols-[1fr_auto] overflow-hidden', className)}
|
className={cn('relative z-10 grid size-full grid-cols-[1fr_auto] overflow-hidden', className)}
|
||||||
>
|
>
|
||||||
<div className="bg-neutral-5 absolute bottom-0 left-0 -z-10 h-px w-full" />
|
<div className="bg-border absolute bottom-0 left-0 -z-10 h-px w-full" />
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<ScrollArea className="size-full whitespace-nowrap">
|
<ScrollArea className="size-full whitespace-nowrap">
|
||||||
<div className="flex items-stretch">
|
<div className="flex items-stretch">
|
||||||
|
|
@ -467,7 +466,7 @@ export const Tabs = ({ className }: { className?: string }) => {
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-neutral-11 hover:text-neutral-11"
|
className="text-primary hover:text-primary"
|
||||||
onClick={handleAddOperation}
|
onClick={handleAddOperation}
|
||||||
>
|
>
|
||||||
<CirclePlus className="size-4" />
|
<CirclePlus className="size-4" />
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Children, Fragment, useEffect, useMemo, useState } from 'react';
|
import { Children, Fragment, useEffect, useMemo, useState } from 'react';
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
interface ItemProps {
|
interface ItemProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -36,16 +36,16 @@ export const Tabs = ({ children, suffix }: TabsProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid size-full grid-rows-[auto_1fr] pb-0">
|
<div className="grid size-full grid-rows-[auto_1fr] pb-0">
|
||||||
<div className="bg-neutral-3 relative z-10 flex h-12 w-full items-center overflow-hidden">
|
<div className="bg-background relative z-10 flex h-12 w-full items-center overflow-hidden">
|
||||||
<div className="bg-neutral-5 absolute bottom-0 left-0 -z-10 h-px w-full" />
|
<div className="bg-border absolute bottom-0 left-0 -z-10 h-px w-full" />
|
||||||
<div className="flex h-full w-max items-stretch">
|
<div className="flex h-full w-max items-stretch">
|
||||||
{Children.map(filteredChildren, child => (
|
{Children.map(filteredChildren, child => (
|
||||||
<Fragment key={child?.props.label}>
|
<Fragment key={child?.props.label}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-neutral-10 hover:text-neutral-11 group relative flex cursor-pointer items-center gap-2 border-t-2 border-transparent px-3 pb-1 font-medium transition-all',
|
'text-muted-foreground hover:text-foreground group relative flex cursor-pointer items-center gap-2 border-t-2 border-transparent px-3 pb-1 font-medium transition-all',
|
||||||
{
|
{
|
||||||
'border-neutral-11 bg-neutral-2 text-neutral-11':
|
'border-primary bg-card text-foreground-primary':
|
||||||
activeTab === child.props.label,
|
activeTab === child.props.label,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
|
|
@ -53,7 +53,7 @@ export const Tabs = ({ children, suffix }: TabsProps) => {
|
||||||
>
|
>
|
||||||
{child.props.label}
|
{child.props.label}
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-neutral-5 mb-px w-px" />
|
<div className="bg-border mb-px w-px" />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { buttonVariants } from '@/laboratory/components/ui/button';
|
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
import { buttonVariants } from './button';
|
||||||
|
|
||||||
function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||||
|
|
@ -24,7 +24,7 @@ function AlertDialogOverlay({
|
||||||
<AlertDialogPrimitive.Overlay
|
<AlertDialogPrimitive.Overlay
|
||||||
data-slot="alert-dialog-overlay"
|
data-slot="alert-dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-neutral-1/50 fixed inset-0 z-50',
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -42,7 +42,7 @@ function AlertDialogContent({
|
||||||
<AlertDialogPrimitive.Content
|
<AlertDialogPrimitive.Content
|
||||||
data-slot="alert-dialog-content"
|
data-slot="alert-dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-neutral-3 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -91,7 +91,7 @@ function AlertDialogDescription({
|
||||||
return (
|
return (
|
||||||
<AlertDialogPrimitive.Description
|
<AlertDialogPrimitive.Description
|
||||||
data-slot="alert-dialog-description"
|
data-slot="alert-dialog-description"
|
||||||
className={cn('text-neutral-10 text-sm', className)}
|
className={cn('text-muted-foreground text-sm', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
import { cva, type VariantProps } from 'class-variance-authority';
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import { Slot } from '@radix-ui/react-slot';
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-red-200 aria-invalid:border-red-500 transition-[color,box-shadow] overflow-hidden',
|
'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'border-transparent bg-neutral-11 text-neutral-2 [a&]:hover:bg-neutral-11/90',
|
default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
||||||
secondary: 'border-transparent bg-neutral-2 text-neutral-11 [a&]:hover:bg-neutral-2/90',
|
secondary:
|
||||||
|
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
||||||
destructive:
|
destructive:
|
||||||
'border-transparent bg-red-500 text-neutral-12 [a&]:hover:bg-red-900 focus-visible:ring-red-200',
|
'border-transparent text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/40 bg-destructive/60',
|
||||||
outline: 'text-neutral-11 [a&]:hover:bg-neutral-2 [a&]:hover:text-neutral-12',
|
outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { cva, type VariantProps } from 'class-variance-authority';
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
import { Separator } from '@/laboratory/components/ui/separator';
|
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import { Slot } from '@radix-ui/react-slot';
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
import { Separator } from './separator';
|
||||||
|
|
||||||
const buttonGroupVariants = cva(
|
const buttonGroupVariants = cva(
|
||||||
"flex w-fit items-stretch *:focus-visible:z-10 *:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
|
"flex w-fit items-stretch *:focus-visible:z-10 *:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
|
||||||
|
|
@ -48,7 +48,7 @@ function ButtonGroupText({
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-neutral-3 flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -66,7 +66,7 @@ function ButtonGroupSeparator({
|
||||||
data-slot="button-group-separator"
|
data-slot="button-group-separator"
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-neutral-5 m-0! relative self-stretch data-[orientation=vertical]:h-auto',
|
'bg-input m-0! relative self-stretch data-[orientation=vertical]:h-auto',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
56
packages/libraries/laboratory/src/components/ui/button.tsx
Normal file
56
packages/libraries/laboratory/src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
|
destructive:
|
||||||
|
'!text-white hover:bg-destructive/90 focus-visible:ring-destructive/40 bg-destructive/60',
|
||||||
|
outline:
|
||||||
|
'border shadow-sm hover:text-accent-foreground bg-input/30 border-input hover:bg-input/50',
|
||||||
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
|
ghost: 'hover:text-accent-foreground hover:bg-accent/50',
|
||||||
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||||
|
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||||
|
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||||
|
icon: 'size-9',
|
||||||
|
'icon-sm': 'size-8',
|
||||||
|
'icon-lg': 'size-10',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'button'> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : 'button';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-neutral-2 text-neutral-11 flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -40,7 +40,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-description"
|
data-slot="card-description"
|
||||||
className={cn('text-neutral-10 text-sm', className)}
|
className={cn('text-muted-foreground text-sm', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { CheckIcon } from 'lucide-react';
|
import { CheckIcon } from 'lucide-react';
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
data-slot="checkbox"
|
data-slot="checkbox"
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-neutral-5 data-[state=checked]:bg-neutral-11 data-[state=checked]:text-neutral-2 data-[state=checked]:border-neutral-11 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-red-200 aria-invalid:border-red-500 peer size-4 shrink-0 rounded-[4px] border shadow-sm outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
'border-input bg-input/30 data-[state=checked]:text-primary-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/40 aria-invalid:border-destructive peer size-4 shrink-0 rounded-[4px] border shadow-sm outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -2,21 +2,15 @@
|
||||||
|
|
||||||
import { Command as CommandPrimitive } from 'cmdk';
|
import { Command as CommandPrimitive } from 'cmdk';
|
||||||
import { SearchIcon } from 'lucide-react';
|
import { SearchIcon } from 'lucide-react';
|
||||||
import {
|
import { cn } from '../../lib/utils';
|
||||||
Dialog,
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './dialog';
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/laboratory/components/ui/dialog';
|
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
|
|
||||||
function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
|
function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive
|
<CommandPrimitive
|
||||||
data-slot="command"
|
data-slot="command"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-neutral-4 text-neutral-11 flex size-full flex-col overflow-hidden rounded-md',
|
'bg-popover text-popover-foreground flex size-full flex-col overflow-hidden rounded-md',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -47,7 +41,7 @@ function CommandDialog({
|
||||||
className={cn('overflow-hidden p-0', className)}
|
className={cn('overflow-hidden p-0', className)}
|
||||||
showCloseButton={showCloseButton}
|
showCloseButton={showCloseButton}
|
||||||
>
|
>
|
||||||
<Command className="[&_[cmdk-group-heading]]:text-neutral-10 **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:size-5">
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:size-5">
|
||||||
{children}
|
{children}
|
||||||
</Command>
|
</Command>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
@ -65,7 +59,7 @@ function CommandInput({
|
||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
data-slot="command-input"
|
data-slot="command-input"
|
||||||
className={cn(
|
className={cn(
|
||||||
'placeholder:text-neutral-10 outline-hidden flex h-10 w-full rounded-md bg-transparent py-3 text-sm disabled:cursor-not-allowed disabled:opacity-50',
|
'placeholder:text-muted-foreground outline-hidden flex h-10 w-full rounded-md bg-transparent py-3 text-sm disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -102,7 +96,7 @@ function CommandGroup({
|
||||||
<CommandPrimitive.Group
|
<CommandPrimitive.Group
|
||||||
data-slot="command-group"
|
data-slot="command-group"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-neutral-11 [&_[cmdk-group-heading]]:text-neutral-10 overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
|
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -117,7 +111,7 @@ function CommandSeparator({
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive.Separator
|
<CommandPrimitive.Separator
|
||||||
data-slot="command-separator"
|
data-slot="command-separator"
|
||||||
className={cn('bg-neutral-5 -mx-1 h-px', className)}
|
className={cn('bg-border -mx-1 h-px', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -128,7 +122,7 @@ function CommandItem({ className, ...props }: React.ComponentProps<typeof Comman
|
||||||
<CommandPrimitive.Item
|
<CommandPrimitive.Item
|
||||||
data-slot="command-item"
|
data-slot="command-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[selected=true]:bg-neutral-2 data-[selected=true]:text-neutral-12 [&_svg:not([class*='text-'])]:text-neutral-10 outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -140,7 +134,7 @@ function CommandShortcut({ className, ...props }: React.ComponentProps<'span'>)
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="command-shortcut"
|
data-slot="command-shortcut"
|
||||||
className={cn('text-neutral-10 ml-auto text-xs tracking-widest', className)}
|
className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { useState } from 'react';
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
|
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
function ContextMenu({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
function ContextMenu({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||||
|
|
@ -43,7 +44,7 @@ function ContextMenuSubTrigger({
|
||||||
data-slot="context-menu-sub-trigger"
|
data-slot="context-menu-sub-trigger"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-neutral-2 focus:text-neutral-12 data-[state=open]:bg-neutral-2 data-[state=open]:text-neutral-12 [&_svg:not([class*='text-'])]:text-neutral-10 outline-hidden data-inset:pl-8 flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden data-inset:pl-8 flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -62,7 +63,7 @@ function ContextMenuSubContent({
|
||||||
<ContextMenuPrimitive.SubContent
|
<ContextMenuPrimitive.SubContent
|
||||||
data-slot="context-menu-sub-content"
|
data-slot="context-menu-sub-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-neutral-4 text-neutral-11 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -74,17 +75,22 @@ function ContextMenuContent({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||||
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Portal>
|
<>
|
||||||
<ContextMenuPrimitive.Content
|
<ContextMenuPrimitive.Portal container={container}>
|
||||||
data-slot="context-menu-content"
|
<ContextMenuPrimitive.Content
|
||||||
className={cn(
|
data-slot="context-menu-content"
|
||||||
'bg-neutral-4 text-neutral-11 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--radix-context-menu-content-available-height) origin-(--radix-context-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md',
|
className={cn(
|
||||||
className,
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--radix-context-menu-content-available-height) origin-(--radix-context-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md',
|
||||||
)}
|
className,
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
</ContextMenuPrimitive.Portal>
|
/>
|
||||||
|
</ContextMenuPrimitive.Portal>
|
||||||
|
<div ref={setContainer} style={{ display: 'contents' }} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,7 +109,7 @@ function ContextMenuItem({
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-neutral-2 focus:text-neutral-12 data-[variant=destructive]:*:[svg]:text-red-500! [&_svg:not([class*='text-'])]:text-neutral-10 outline-hidden data-disabled:pointer-events-none data-inset:pl-8 data-disabled:opacity-50 relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[variant=destructive]:text-red-500 data-[variant=destructive]:focus:bg-red-100 data-[variant=destructive]:focus:text-red-500 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive! [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden data-disabled:pointer-events-none data-inset:pl-8 data-disabled:opacity-50 relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -121,7 +127,7 @@ function ContextMenuCheckboxItem({
|
||||||
<ContextMenuPrimitive.CheckboxItem
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
data-slot="context-menu-checkbox-item"
|
data-slot="context-menu-checkbox-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-neutral-2 focus:text-neutral-12 outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
|
|
@ -146,7 +152,7 @@ function ContextMenuRadioItem({
|
||||||
<ContextMenuPrimitive.RadioItem
|
<ContextMenuPrimitive.RadioItem
|
||||||
data-slot="context-menu-radio-item"
|
data-slot="context-menu-radio-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-neutral-2 focus:text-neutral-12 outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -172,7 +178,7 @@ function ContextMenuLabel({
|
||||||
<ContextMenuPrimitive.Label
|
<ContextMenuPrimitive.Label
|
||||||
data-slot="context-menu-label"
|
data-slot="context-menu-label"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn('text-neutral-11 data-inset:pl-8 px-2 py-1.5 text-sm font-medium', className)}
|
className={cn('text-foreground data-inset:pl-8 px-2 py-1.5 text-sm font-medium', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -185,7 +191,7 @@ function ContextMenuSeparator({
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Separator
|
<ContextMenuPrimitive.Separator
|
||||||
data-slot="context-menu-separator"
|
data-slot="context-menu-separator"
|
||||||
className={cn('bg-neutral-5 -mx-1 my-1 h-px', className)}
|
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -195,7 +201,7 @@ function ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="context-menu-shortcut"
|
data-slot="context-menu-shortcut"
|
||||||
className={cn('text-neutral-10 ml-auto text-xs tracking-widest', className)}
|
className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { useState } from 'react';
|
||||||
import { XIcon } from 'lucide-react';
|
import { XIcon } from 'lucide-react';
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||||
|
|
@ -11,7 +12,14 @@ function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogPrimitive.Portal data-slot="dialog-portal" {...props} container={container} />
|
||||||
|
<div ref={setContainer} style={{ display: 'contents' }} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
|
@ -26,7 +34,7 @@ function DialogOverlay({
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-neutral-1/50 fixed inset-0 z-50',
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -48,7 +56,7 @@ function DialogContent({
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-neutral-3 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -57,7 +65,7 @@ function DialogContent({
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<DialogPrimitive.Close
|
<DialogPrimitive.Close
|
||||||
data-slot="dialog-close"
|
data-slot="dialog-close"
|
||||||
className="ring-offset-neutral-2 focus:ring-ring data-[state=open]:bg-neutral-2 data-[state=open]:text-neutral-10 rounded-xs focus:outline-hidden absolute right-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-xs focus:outline-hidden absolute right-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
|
|
@ -105,7 +113,7 @@ function DialogDescription({
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
data-slot="dialog-description"
|
data-slot="dialog-description"
|
||||||
className={cn('text-neutral-10 text-sm', className)}
|
className={cn('text-muted-foreground text-sm', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { useState } from 'react';
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||||
|
|
@ -23,18 +24,22 @@ function DropdownMenuContent({
|
||||||
sideOffset = 4,
|
sideOffset = 4,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Portal>
|
<>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Portal container={container}>
|
||||||
data-slot="dropdown-menu-content"
|
<DropdownMenuPrimitive.Content
|
||||||
sideOffset={sideOffset}
|
data-slot="dropdown-menu-content"
|
||||||
className={cn(
|
sideOffset={sideOffset}
|
||||||
'bg-neutral-4 text-neutral-11 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md',
|
className={cn(
|
||||||
className,
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md',
|
||||||
)}
|
className,
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
</DropdownMenuPrimitive.Portal>
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
<div ref={setContainer} style={{ display: 'contents' }} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +62,7 @@ function DropdownMenuItem({
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-neutral-2 focus:text-neutral-12 data-[variant=destructive]:*:[svg]:text-red-500! [&_svg:not([class*='text-'])]:text-neutral-10 outline-hidden data-disabled:pointer-events-none data-inset:pl-8 data-disabled:opacity-50 relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[variant=destructive]:text-red-500 data-[variant=destructive]:focus:bg-red-100 data-[variant=destructive]:focus:text-red-500 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive! [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden data-disabled:pointer-events-none data-inset:pl-8 data-disabled:opacity-50 relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -75,7 +80,7 @@ function DropdownMenuCheckboxItem({
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-neutral-2 focus:text-neutral-12 outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
|
|
@ -106,7 +111,7 @@ function DropdownMenuRadioItem({
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
data-slot="dropdown-menu-radio-item"
|
data-slot="dropdown-menu-radio-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-neutral-2 focus:text-neutral-12 outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -145,7 +150,7 @@ function DropdownMenuSeparator({
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
data-slot="dropdown-menu-separator"
|
data-slot="dropdown-menu-separator"
|
||||||
className={cn('bg-neutral-5 -mx-1 my-1 h-px', className)}
|
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -155,7 +160,7 @@ function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'spa
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="dropdown-menu-shortcut"
|
data-slot="dropdown-menu-shortcut"
|
||||||
className={cn('text-neutral-10 ml-auto text-xs tracking-widest', className)}
|
className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -178,7 +183,7 @@ function DropdownMenuSubTrigger({
|
||||||
data-slot="dropdown-menu-sub-trigger"
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-neutral-2 focus:text-neutral-12 data-[state=open]:bg-neutral-2 data-[state=open]:text-neutral-12 [&_svg:not([class*='text-'])]:text-neutral-10 outline-hidden data-inset:pl-8 flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden data-inset:pl-8 flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -197,7 +202,7 @@ function DropdownMenuSubContent({
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
data-slot="dropdown-menu-sub-content"
|
data-slot="dropdown-menu-sub-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-neutral-4 text-neutral-11 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-hidden rounded-md border p-1 shadow-lg',
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-hidden rounded-md border p-1 shadow-lg',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { cva, type VariantProps } from 'class-variance-authority';
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
function Empty({ className, ...props }: React.ComponentProps<'div'>) {
|
function Empty({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -30,7 +30,7 @@ const emptyMediaVariants = cva(
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-transparent',
|
default: 'bg-transparent',
|
||||||
icon: "bg-neutral-3 text-neutral-11 flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
@ -69,7 +69,7 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
||||||
<div
|
<div
|
||||||
data-slot="empty-description"
|
data-slot="empty-description"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-neutral-10 [&>a:hover]:text-neutral-11 text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
|
'text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { cva, type VariantProps } from 'class-variance-authority';
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
import { Label } from '@/laboratory/components/ui/label';
|
import { cn } from '../../lib/utils';
|
||||||
import { Separator } from '@/laboratory/components/ui/separator';
|
import { Label } from './label';
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
import { Separator } from './separator';
|
||||||
|
|
||||||
function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
|
function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -51,7 +51,7 @@ function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldVariants = cva('group/field flex w-full gap-3 data-[invalid=true]:text-red-500', {
|
const fieldVariants = cva('group/field flex w-full gap-3 data-[invalid=true]:text-destructive', {
|
||||||
variants: {
|
variants: {
|
||||||
orientation: {
|
orientation: {
|
||||||
vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
|
vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
|
||||||
|
|
@ -105,7 +105,7 @@ function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>)
|
||||||
className={cn(
|
className={cn(
|
||||||
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
|
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
|
||||||
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
|
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
|
||||||
'has-[>[data-state=checked]]:bg-neutral-11/5 has-[>[data-state=checked]]:border-neutral-11',
|
'has-[>[data-state=checked]]:border-primary has-[>[data-state=checked]]:bg-primary/10',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -131,9 +131,9 @@ function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
||||||
<p
|
<p
|
||||||
data-slot="field-description"
|
data-slot="field-description"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-neutral-10 text-sm font-normal leading-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
|
'text-muted-foreground text-sm font-normal leading-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
|
||||||
'nth-last-2:-mt-1 last:mt-0 [[data-variant=legend]+&]:-mt-1.5',
|
'nth-last-2:-mt-1 last:mt-0 [[data-variant=legend]+&]:-mt-1.5',
|
||||||
'[&>a:hover]:text-neutral-11 [&>a]:underline [&>a]:underline-offset-4',
|
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -161,7 +161,7 @@ function FieldSeparator({
|
||||||
<Separator className="absolute inset-0 top-1/2" />
|
<Separator className="absolute inset-0 top-1/2" />
|
||||||
{children && (
|
{children && (
|
||||||
<span
|
<span
|
||||||
className="bg-neutral-3 text-neutral-10 relative mx-auto block w-fit px-2"
|
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
||||||
data-slot="field-separator-content"
|
data-slot="field-separator-content"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -209,7 +209,7 @@ function FieldError({
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
data-slot="field-error"
|
data-slot="field-error"
|
||||||
className={cn('text-sm font-normal text-red-500', className)}
|
className={cn('text-destructive text-sm font-normal', className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { cva, type VariantProps } from 'class-variance-authority';
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
import { Button } from '@/laboratory/components/ui/button';
|
import { cn } from '../../lib/utils';
|
||||||
import { Input } from '@/laboratory/components/ui/input';
|
import { Button } from './button';
|
||||||
import { Textarea } from '@/laboratory/components/ui/textarea';
|
import { Input } from './input';
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
import { Textarea } from './textarea';
|
||||||
|
|
||||||
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -10,7 +10,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
data-slot="input-group"
|
data-slot="input-group"
|
||||||
role="group"
|
role="group"
|
||||||
className={cn(
|
className={cn(
|
||||||
'group/input-group border-neutral-4 dark:bg-neutral-4/30 relative flex w-full items-center rounded-md border shadow-sm outline-none transition-[color,box-shadow]',
|
'group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-sm outline-none transition-[color,box-shadow]',
|
||||||
'h-9 min-w-0 has-[>textarea]:h-auto',
|
'h-9 min-w-0 has-[>textarea]:h-auto',
|
||||||
|
|
||||||
// Variants based on alignment.
|
// Variants based on alignment.
|
||||||
|
|
@ -23,7 +23,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',
|
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',
|
||||||
|
|
||||||
// Error state.
|
// Error state.
|
||||||
'has-[[data-slot][aria-invalid=true]]:border-red-500 has-[[data-slot][aria-invalid=true]]:ring-red-500/20 dark:has-[[data-slot][aria-invalid=true]]:ring-red-500/40',
|
'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
|
||||||
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
@ -33,7 +33,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputGroupAddonVariants = cva(
|
const inputGroupAddonVariants = cva(
|
||||||
"text-neutral-10 flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
align: {
|
align: {
|
||||||
|
|
@ -57,7 +57,6 @@ function InputGroupAddon({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
|
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
data-slot="input-group-addon"
|
data-slot="input-group-addon"
|
||||||
|
|
@ -111,7 +110,7 @@ function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-neutral-10 flex items-center gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
"text-muted-foreground flex items-center gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
19
packages/libraries/laboratory/src/components/ui/input.tsx
Normal file
19
packages/libraries/laboratory/src/components/ui/input.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-input/30 border-input h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-sm outline-none transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||||
|
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||||
|
'aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input };
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { GripVerticalIcon } from 'lucide-react';
|
import { GripVerticalIcon } from 'lucide-react';
|
||||||
import * as ResizablePrimitive from 'react-resizable-panels';
|
import * as ResizablePrimitive from 'react-resizable-panels';
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
function ResizablePanelGroup({
|
function ResizablePanelGroup({
|
||||||
className,
|
className,
|
||||||
|
|
@ -30,13 +30,13 @@ function ResizableHandle({
|
||||||
<ResizablePrimitive.PanelResizeHandle
|
<ResizablePrimitive.PanelResizeHandle
|
||||||
data-slot="resizable-handle"
|
data-slot="resizable-handle"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-neutral-5 focus-visible:ring-ring focus-visible:outline-hidden relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
|
'bg-border focus-visible:ring-ring focus-visible:outline-hidden relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{withHandle && (
|
{withHandle && (
|
||||||
<div className="bg-neutral-5 rounded-xs z-10 flex h-4 w-3 items-center justify-center border">
|
<div className="bg-border rounded-xs z-10 flex h-4 w-3 items-center justify-center border">
|
||||||
<GripVerticalIcon className="size-2.5" />
|
<GripVerticalIcon className="size-2.5" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
function ScrollArea({
|
function ScrollArea({
|
||||||
className,
|
className,
|
||||||
|
|
@ -43,7 +43,7 @@ function ScrollBar({
|
||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
data-slot="scroll-area-thumb"
|
data-slot="scroll-area-thumb"
|
||||||
className="bg-neutral-5 relative flex-1 rounded-full"
|
className="bg-border relative flex-1 rounded-full"
|
||||||
/>
|
/>
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
);
|
);
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
import { useLaboratory } from '@/components/laboratory/context';
|
||||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||||
|
|
@ -27,7 +28,7 @@ function SelectTrigger({
|
||||||
data-slot="select-trigger"
|
data-slot="select-trigger"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-neutral-5 data-placeholder:text-neutral-10 [&_svg:not([class*='text-'])]:text-neutral-10 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-red-200 aria-invalid:border-red-500 flex w-fit items-center justify-between gap-2 whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"border-input data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-input/30 hover:bg-input/50 flex w-fit items-center justify-between gap-2 whitespace-nowrap rounded-md border px-3 py-2 text-sm shadow-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -47,12 +48,14 @@ function SelectContent({
|
||||||
align = 'center',
|
align = 'center',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
const { container } = useLaboratory();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal container={container}>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
data-slot="select-content"
|
data-slot="select-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-neutral-4 text-neutral-11 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-[var(--radix-select-content-available-height)] min-w-[8rem] origin-[var(--radix-select-content-transform-origin)] overflow-y-auto overflow-x-hidden rounded-md border shadow-md',
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-[var(--radix-select-content-available-height)] min-w-[8rem] origin-[var(--radix-select-content-transform-origin)] overflow-y-auto overflow-x-hidden rounded-md border shadow-md',
|
||||||
position === 'popper' &&
|
position === 'popper' &&
|
||||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
className,
|
className,
|
||||||
|
|
@ -81,7 +84,7 @@ function SelectLabel({ className, ...props }: React.ComponentProps<typeof Select
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Label
|
<SelectPrimitive.Label
|
||||||
data-slot="select-label"
|
data-slot="select-label"
|
||||||
className={cn('text-neutral-10 px-2 py-1.5 text-xs', className)}
|
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -96,7 +99,7 @@ function SelectItem({
|
||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
data-slot="select-item"
|
data-slot="select-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-neutral-2 focus:text-neutral-12 [&_svg:not([class*='text-'])]:text-neutral-10 outline-hidden *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 data-disabled:pointer-events-none data-disabled:opacity-50 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 data-disabled:pointer-events-none data-disabled:opacity-50 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -118,7 +121,7 @@ function SelectSeparator({
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Separator
|
<SelectPrimitive.Separator
|
||||||
data-slot="select-separator"
|
data-slot="select-separator"
|
||||||
className={cn('bg-neutral-5 pointer-events-none -mx-1 my-1 h-px', className)}
|
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
function Separator({
|
function Separator({
|
||||||
className,
|
className,
|
||||||
|
|
@ -15,7 +15,7 @@ function Separator({
|
||||||
decorative={decorative}
|
decorative={decorative}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-neutral-5 shrink-0 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px',
|
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -6,14 +6,11 @@ import {
|
||||||
TriangleAlertIcon,
|
TriangleAlertIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Toaster as Sonner, type ToasterProps } from 'sonner';
|
import { Toaster as Sonner, type ToasterProps } from 'sonner';
|
||||||
import { useTheme } from '@/components/theme/theme-provider';
|
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
const { theme } = useTheme();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sonner
|
<Sonner
|
||||||
theme={theme as ToasterProps['theme']}
|
theme={props.theme}
|
||||||
className="group"
|
className="group"
|
||||||
icons={{
|
icons={{
|
||||||
success: <CircleCheckIcon className="size-4" />,
|
success: <CircleCheckIcon className="size-4" />,
|
||||||
|
|
@ -24,9 +21,9 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
}}
|
}}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
'--normal-bg': 'var(--neutral-2)',
|
'--normal-bg': 'var(--popover)',
|
||||||
'--normal-text': 'var(--neutral-11)',
|
'--normal-text': 'var(--popover-foreground)',
|
||||||
'--normal-border': 'var(--neutral-4)',
|
'--normal-border': 'var(--border)',
|
||||||
'--border-radius': 'var(--radius)',
|
'--border-radius': 'var(--radius)',
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
@ -10,8 +10,6 @@ import {
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import * as ReactDOM from 'react-dom';
|
import * as ReactDOM from 'react-dom';
|
||||||
import { useComposedRefs } from '@/laboratory/lib/compose-refs';
|
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import {
|
import {
|
||||||
closestCenter,
|
closestCenter,
|
||||||
closestCorners,
|
closestCorners,
|
||||||
|
|
@ -49,6 +47,8 @@ import {
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { Slot } from '@radix-ui/react-slot';
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
|
import { useComposedRefs } from '../../lib/compose-refs';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
const orientationConfig = {
|
const orientationConfig = {
|
||||||
vertical: {
|
vertical: {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Loader2Icon } from 'lucide-react';
|
import { Loader2Icon } from 'lucide-react';
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
function Spinner({ className, ...props }: React.ComponentProps<'svg'>) {
|
function Spinner({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||||
return (
|
return (
|
||||||
24
packages/libraries/laboratory/src/components/ui/switch.tsx
Normal file
24
packages/libraries/laboratory/src/components/ui/switch.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot="switch"
|
||||||
|
className={cn(
|
||||||
|
'focus-visible:border-ring focus-visible:ring-ring/50 data-[state=unchecked]:bg-input/80 data-[state=checked]:bg-primary peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-sm outline-none transition-all focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
className={cn(
|
||||||
|
'bg-background data-[state=unchecked]:bg-foreground data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch };
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.Root
|
<TabsPrimitive.Root data-slot="tabs" className={cn('flex flex-col', className)} {...props} />
|
||||||
data-slot="tabs"
|
|
||||||
className={cn('flex flex-col gap-2', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16,7 +12,7 @@ function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimi
|
||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
data-slot="tabs-list"
|
data-slot="tabs-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-neutral-2 dark:bg-neutral-3 text-neutral-10 inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
|
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -29,7 +25,7 @@ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPr
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
data-slot="tabs-trigger"
|
data-slot="tabs-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=active]:text-neutral-11 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring data-[state=active]:border-neutral-4 data-[state=active]:bg-neutral-4/30 text-neutral-8 inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 py-1 text-sm font-medium transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring data-[state=active]:border-input data-[state=active]:bg-input/30 text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 py-1 text-sm font-medium transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
16
packages/libraries/laboratory/src/components/ui/textarea.tsx
Normal file
16
packages/libraries/laboratory/src/components/ui/textarea.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea };
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { cva, type VariantProps } from 'class-variance-authority';
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import * as TogglePrimitive from '@radix-ui/react-toggle';
|
import * as TogglePrimitive from '@radix-ui/react-toggle';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
const toggleVariants = cva(
|
const toggleVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-neutral-3 hover:text-neutral-10 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-neutral-2 data-[state=on]:text-neutral-12 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-red-200 aria-invalid:border-red-500 whitespace-nowrap",
|
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-transparent',
|
default: 'bg-transparent',
|
||||||
outline:
|
outline:
|
||||||
'border border-neutral-5 bg-transparent shadow-sm hover:bg-neutral-2 hover:text-neutral-12',
|
'border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: 'h-9 px-2 min-w-9',
|
default: 'h-9 px-2 min-w-9',
|
||||||
59
packages/libraries/laboratory/src/components/ui/tooltip.tsx
Normal file
59
packages/libraries/laboratory/src/components/ui/tooltip.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
function TooltipProvider({
|
||||||
|
delayDuration = 0,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Provider
|
||||||
|
data-slot="tooltip-provider"
|
||||||
|
delayDuration={delayDuration}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||||
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 0,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||||
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TooltipPrimitive.Portal container={container}>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
data-slot="tooltip-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-[var(--radix-tooltip-content-transform-origin)] text-balance rounded-md px-3 py-1.5 text-xs',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
<div ref={setContainer} style={{ display: 'contents' }} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||||
161
packages/libraries/laboratory/src/index.css
Normal file
161
packages/libraries/laboratory/src/index.css
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
@import 'tailwindcss';
|
||||||
|
@import 'tw-animate-css';
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-background: hsl(var(--background));
|
||||||
|
--color-foreground: hsl(var(--foreground));
|
||||||
|
--color-card: hsl(var(--card));
|
||||||
|
--color-card-foreground: hsl(var(--card-foreground));
|
||||||
|
--color-popover: hsl(var(--popover));
|
||||||
|
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||||
|
--color-primary: hsl(var(--primary));
|
||||||
|
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||||
|
--color-secondary: hsl(var(--secondary));
|
||||||
|
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||||
|
--color-muted: hsl(var(--muted));
|
||||||
|
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||||
|
--color-accent: hsl(var(--accent));
|
||||||
|
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||||
|
--color-destructive: hsl(var(--destructive));
|
||||||
|
--color-border: hsl(var(--border));
|
||||||
|
--color-input: hsl(var(--input));
|
||||||
|
--color-ring: hsl(var(--ring));
|
||||||
|
--color-chart-1: hsl(var(--chart-1));
|
||||||
|
--color-chart-2: hsl(var(--chart-2));
|
||||||
|
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
|
||||||
|
--color-sidebar-primary: hsl(var(--sidebar-primary));
|
||||||
|
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
|
||||||
|
--color-sidebar-accent: hsl(var(--sidebar-accent));
|
||||||
|
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
|
||||||
|
--color-sidebar-border: hsl(var(--sidebar-border));
|
||||||
|
--color-sidebar-ring: hsl(var(--sidebar-ring));
|
||||||
|
}
|
||||||
|
|
||||||
|
:host,
|
||||||
|
.hive-laboratory {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hive-laboratory {
|
||||||
|
--color-neutral-1: 0 0% 99%;
|
||||||
|
--color-neutral-2: 180 9% 97%;
|
||||||
|
--color-neutral-3: 210 7% 95%;
|
||||||
|
--color-neutral-4: 200 6% 92%;
|
||||||
|
--color-neutral-5: 210 6% 89%;
|
||||||
|
--color-neutral-6: 204 5% 79%;
|
||||||
|
--color-neutral-7: 197 4% 67%;
|
||||||
|
--color-neutral-8: 196 4% 51%;
|
||||||
|
--color-neutral-9: 193 9% 32%;
|
||||||
|
--color-neutral-10: 188 15% 21%;
|
||||||
|
--color-neutral-11: 188 19% 15%;
|
||||||
|
--color-neutral-12: 175 23% 10%;
|
||||||
|
|
||||||
|
--color-accent: 206 96% 35%;
|
||||||
|
|
||||||
|
--color-ring: 216 58% 49%;
|
||||||
|
|
||||||
|
--radius: var(--hive-laboratory-radius, 0.5rem);
|
||||||
|
--background: var(--hive-laboratory-background, var(--color-neutral-2));
|
||||||
|
--foreground: var(--hive-laboratory-foreground, var(--color-neutral-11));
|
||||||
|
|
||||||
|
--muted: var(--hive-laboratory-muted, 24 9.8% 10%);
|
||||||
|
--muted-foreground: var(--hive-laboratory-muted-foreground, var(--color-neutral-11));
|
||||||
|
|
||||||
|
--popover: var(--hive-laboratory-popover, var(--color-neutral-3));
|
||||||
|
--popover-foreground: var(--hive-laboratory-popover-foreground, var(--color-neutral-11));
|
||||||
|
|
||||||
|
--card: var(--hive-laboratory-card, var(--color-neutral-1));
|
||||||
|
--card-foreground: var(--hive-laboratory-card-foreground, var(--color-neutral-11));
|
||||||
|
|
||||||
|
--border: var(--hive-laboratory-border, var(--color-neutral-5));
|
||||||
|
--input: var(--hive-laboratory-input, var(--color-neutral-5));
|
||||||
|
|
||||||
|
--primary: var(--hive-laboratory-primary, var(--color-accent));
|
||||||
|
--primary-foreground: var(--hive-laboratory-primary-foreground, var(--color-neutral-1));
|
||||||
|
|
||||||
|
--secondary: var(--hive-laboratory-secondary, var(--color-neutral-3));
|
||||||
|
--secondary-foreground: var(--hive-laboratory-secondary-foreground, var(--color-neutral-11));
|
||||||
|
|
||||||
|
--accent: var(--hive-laboratory-accent, var(--color-neutral-4));
|
||||||
|
--accent-foreground: var(--hive-laboratory-accent-foreground, var(--color-neutral-11));
|
||||||
|
|
||||||
|
--destructive: var(--hive-laboratory-destructive, var(--red-500));
|
||||||
|
--destructive-foreground: var(--hive-laboratory-destructive-foreground, var(--color-neutral-1));
|
||||||
|
|
||||||
|
--ring: var(--hive-laboratory-ring, var(--color-ring));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hive-laboratory.dark {
|
||||||
|
--color-neutral-1: 210 21% 5%;
|
||||||
|
--color-neutral-2: 210 16% 7%;
|
||||||
|
--color-neutral-3: 214 14% 10%;
|
||||||
|
--color-neutral-4: 210 13% 13%;
|
||||||
|
--color-neutral-5: 210 12% 16%;
|
||||||
|
--color-neutral-6: 210 11% 21%;
|
||||||
|
--color-neutral-7: 210 12% 27%;
|
||||||
|
--color-neutral-8: 210 11% 34%;
|
||||||
|
--color-neutral-9: 210 10% 43%;
|
||||||
|
--color-neutral-10: 213 9% 58%;
|
||||||
|
--color-neutral-11: 210 11% 75%;
|
||||||
|
--color-neutral-12: 204 14% 93%;
|
||||||
|
|
||||||
|
--color-accent: 48 100% 83%;
|
||||||
|
|
||||||
|
--radius: var(--hive-laboratory-radius, 0.5rem);
|
||||||
|
--background: var(--hive-laboratory-background, var(--color-neutral-1));
|
||||||
|
--foreground: var(--hive-laboratory-foreground, var(--color-neutral-11));
|
||||||
|
|
||||||
|
--muted: var(--hive-laboratory-muted, var(--color-neutral-3));
|
||||||
|
--muted-foreground: var(--hive-laboratory-muted-foreground, var(--color-neutral-11));
|
||||||
|
|
||||||
|
--popover: var(--hive-laboratory-popover, var(--color-neutral-3));
|
||||||
|
--popover-foreground: var(--hive-laboratory-popover-foreground, var(--color-neutral-11));
|
||||||
|
|
||||||
|
--card: var(--hive-laboratory-card, var(--color-neutral-2));
|
||||||
|
--card-foreground: var(--hive-laboratory-card-foreground, var(--color-neutral-11));
|
||||||
|
|
||||||
|
--border: var(--hive-laboratory-border, var(--color-neutral-5));
|
||||||
|
--input: var(--hive-laboratory-input, var(--color-neutral-5));
|
||||||
|
|
||||||
|
--primary: var(--hive-laboratory-primary, var(--color-accent));
|
||||||
|
--primary-foreground: var(--hive-laboratory-primary-foreground, var(--color-neutral-1));
|
||||||
|
|
||||||
|
--secondary: var(--hive-laboratory-secondary, var(--color-neutral-3));
|
||||||
|
--secondary-foreground: var(--hive-laboratory-secondary-foreground, var(--color-neutral-11));
|
||||||
|
|
||||||
|
--accent: var(--hive-laboratory-accent, var(--color-neutral-6));
|
||||||
|
--accent-foreground: var(--hive-laboratory-accent-foreground, var(--color-neutral-11));
|
||||||
|
|
||||||
|
--destructive: var(--hive-laboratory-destructive, var(--red-500));
|
||||||
|
--destructive-foreground: var(--hive-laboratory-destructive-foreground, var(--color-neutral-1));
|
||||||
|
|
||||||
|
--ring: var(--hive-laboratory-ring, var(--color-ring));
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
* {
|
||||||
|
--tw-border-style: solid;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
@apply outline-ring/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
:after,
|
||||||
|
:before,
|
||||||
|
::backdrop {
|
||||||
|
border: 0 solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer color {
|
||||||
|
.hive-laboratory {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
77
packages/libraries/laboratory/src/index.tsx
Normal file
77
packages/libraries/laboratory/src/index.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { Laboratory } from './components/laboratory/laboratory';
|
||||||
|
|
||||||
|
export * from './components/laboratory/laboratory';
|
||||||
|
export * from './components/laboratory/context';
|
||||||
|
export * from './components/laboratory/editor';
|
||||||
|
export * from './components/ui/dialog';
|
||||||
|
export * from './components/ui/tabs';
|
||||||
|
export * from './lib/endpoint';
|
||||||
|
export * from './lib/collections';
|
||||||
|
export * from './lib/env';
|
||||||
|
export * from './lib/history';
|
||||||
|
export * from './lib/operations';
|
||||||
|
export * from './lib/preflight';
|
||||||
|
export * from './lib/settings';
|
||||||
|
export * from './lib/tabs';
|
||||||
|
export * from './lib/tests';
|
||||||
|
export * from './lib/plugins';
|
||||||
|
|
||||||
|
export const renderLaboratory = (el: HTMLElement) => {
|
||||||
|
const prefix = 'hive-laboratory';
|
||||||
|
|
||||||
|
const getLocalStorage = (key: string) => {
|
||||||
|
const value = localStorage.getItem(`${prefix}:${key}`);
|
||||||
|
return value ? JSON.parse(value) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLocalStorage = (key: string, value: unknown) => {
|
||||||
|
localStorage.setItem(`${prefix}:${key}`, JSON.stringify(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
throw new Error('Laboratory element not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReactDOM.createRoot(el).render(
|
||||||
|
<Laboratory
|
||||||
|
theme="dark"
|
||||||
|
defaultEndpoint={getLocalStorage('endpoint') ?? null}
|
||||||
|
onEndpointChange={endpoint => {
|
||||||
|
setLocalStorage('endpoint', endpoint ?? '');
|
||||||
|
}}
|
||||||
|
defaultCollections={getLocalStorage('collections') ?? []}
|
||||||
|
onCollectionsChange={collections => {
|
||||||
|
setLocalStorage('collections', collections);
|
||||||
|
}}
|
||||||
|
defaultTabs={getLocalStorage('tabs') ?? []}
|
||||||
|
onTabsChange={tabs => {
|
||||||
|
setLocalStorage('tabs', tabs);
|
||||||
|
}}
|
||||||
|
defaultOperations={getLocalStorage('operations') ?? []}
|
||||||
|
onOperationsChange={operations => {
|
||||||
|
setLocalStorage('operations', operations);
|
||||||
|
}}
|
||||||
|
defaultActiveTabId={getLocalStorage('activeTabId') ?? null}
|
||||||
|
onActiveTabIdChange={activeTabId => {
|
||||||
|
setLocalStorage('activeTabId', activeTabId ?? '');
|
||||||
|
}}
|
||||||
|
defaultPreflight={getLocalStorage('preflight') ?? null}
|
||||||
|
onPreflightChange={preflight => {
|
||||||
|
setLocalStorage('preflight', preflight ?? '');
|
||||||
|
}}
|
||||||
|
defaultEnv={getLocalStorage('env') ?? null}
|
||||||
|
onEnvChange={env => {
|
||||||
|
setLocalStorage('env', env ?? '');
|
||||||
|
}}
|
||||||
|
defaultSettings={getLocalStorage('settings') ?? null}
|
||||||
|
onSettingsChange={settings => {
|
||||||
|
setLocalStorage('settings', settings ?? '');
|
||||||
|
}}
|
||||||
|
defaultHistory={getLocalStorage('history') ?? []}
|
||||||
|
onHistoryChange={history => {
|
||||||
|
setLocalStorage('history', history);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import type { LaboratoryOperation } from '@/laboratory/lib/operations';
|
import type { LaboratoryOperation } from './operations';
|
||||||
import type { LaboratoryTabsActions, LaboratoryTabsState } from '@/laboratory/lib/tabs';
|
import type { LaboratoryTabsActions, LaboratoryTabsState } from './tabs';
|
||||||
|
|
||||||
export interface LaboratoryCollectionOperation extends LaboratoryOperation {
|
export interface LaboratoryCollectionOperation extends LaboratoryOperation {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -25,7 +25,6 @@ export const useEndpoint = (props: {
|
||||||
onEndpointChange?: (endpoint: string | null) => void;
|
onEndpointChange?: (endpoint: string | null) => void;
|
||||||
defaultSchemaIntrospection?: IntrospectionQuery | null;
|
defaultSchemaIntrospection?: IntrospectionQuery | null;
|
||||||
}): LaboratoryEndpointState & LaboratoryEndpointActions => {
|
}): LaboratoryEndpointState & LaboratoryEndpointActions => {
|
||||||
// eslint-disable-next-line react/hook-use-state
|
|
||||||
const [endpoint, _setEndpoint] = useState<string | null>(props.defaultEndpoint ?? null);
|
const [endpoint, _setEndpoint] = useState<string | null>(props.defaultEndpoint ?? null);
|
||||||
const [introspection, setIntrospection] = useState<IntrospectionQuery | null>(null);
|
const [introspection, setIntrospection] = useState<IntrospectionQuery | null>(null);
|
||||||
|
|
||||||
|
|
@ -16,7 +16,6 @@ export const useEnv = (props: {
|
||||||
defaultEnv?: LaboratoryEnv | null;
|
defaultEnv?: LaboratoryEnv | null;
|
||||||
onEnvChange?: (env: LaboratoryEnv | null) => void;
|
onEnvChange?: (env: LaboratoryEnv | null) => void;
|
||||||
}): LaboratoryEnvState & LaboratoryEnvActions => {
|
}): LaboratoryEnvState & LaboratoryEnvActions => {
|
||||||
// eslint-disable-next-line react/hook-use-state
|
|
||||||
const [env, _setEnv] = useState<LaboratoryEnv>(props.defaultEnv ?? { variables: {} });
|
const [env, _setEnv] = useState<LaboratoryEnv>(props.defaultEnv ?? { variables: {} });
|
||||||
|
|
||||||
const setEnv = useCallback(
|
const setEnv = useCallback(
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import type { LaboratoryOperation } from '@/laboratory/lib/operations';
|
import type { LaboratoryOperation } from './operations';
|
||||||
import type { LaboratoryPreflightLog } from '@/laboratory/lib/preflight';
|
import type { LaboratoryPreflightLog } from './preflight';
|
||||||
|
|
||||||
export interface LaboratoryHistoryRequest {
|
export interface LaboratoryHistoryRequest {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -2,16 +2,13 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import type { GraphQLSchema } from 'graphql';
|
import type { GraphQLSchema } from 'graphql';
|
||||||
import { createClient } from 'graphql-ws';
|
import { createClient } from 'graphql-ws';
|
||||||
import { decompressFromEncodedURIComponent } from 'lz-string';
|
import { decompressFromEncodedURIComponent } from 'lz-string';
|
||||||
import {
|
import { LaboratoryPermission, LaboratoryPermissions } from '../components/laboratory/context';
|
||||||
LaboratoryPermission,
|
|
||||||
LaboratoryPermissions,
|
|
||||||
} from '@/laboratory/components/laboratory/context';
|
|
||||||
import type {
|
import type {
|
||||||
LaboratoryCollectionOperation,
|
LaboratoryCollectionOperation,
|
||||||
LaboratoryCollectionsActions,
|
LaboratoryCollectionsActions,
|
||||||
LaboratoryCollectionsState,
|
LaboratoryCollectionsState,
|
||||||
} from '@/laboratory/lib/collections';
|
} from './collections';
|
||||||
import type { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from '@/laboratory/lib/env';
|
import type { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from './env';
|
||||||
import {
|
import {
|
||||||
addArgToField,
|
addArgToField,
|
||||||
addPathToQuery,
|
addPathToQuery,
|
||||||
|
|
@ -19,22 +16,11 @@ import {
|
||||||
getOperationName,
|
getOperationName,
|
||||||
handleTemplate,
|
handleTemplate,
|
||||||
removeArgFromField,
|
removeArgFromField,
|
||||||
} from '@/laboratory/lib/operations.utils';
|
} from './operations.utils';
|
||||||
import {
|
import { LaboratoryPlugin, LaboratoryPluginsActions, LaboratoryPluginsState } from './plugins';
|
||||||
LaboratoryPlugin,
|
import type { LaboratoryPreflightActions, LaboratoryPreflightState } from './preflight';
|
||||||
LaboratoryPluginsActions,
|
import type { LaboratorySettingsActions, LaboratorySettingsState } from './settings';
|
||||||
LaboratoryPluginsState,
|
import type { LaboratoryTabOperation, LaboratoryTabsActions, LaboratoryTabsState } from './tabs';
|
||||||
} from '@/laboratory/lib/plugins';
|
|
||||||
import type {
|
|
||||||
LaboratoryPreflightActions,
|
|
||||||
LaboratoryPreflightState,
|
|
||||||
} from '@/laboratory/lib/preflight';
|
|
||||||
import type { LaboratorySettingsActions, LaboratorySettingsState } from '@/laboratory/lib/settings';
|
|
||||||
import type {
|
|
||||||
LaboratoryTabOperation,
|
|
||||||
LaboratoryTabsActions,
|
|
||||||
LaboratoryTabsState,
|
|
||||||
} from '@/laboratory/lib/tabs';
|
|
||||||
|
|
||||||
export interface LaboratoryOperation {
|
export interface LaboratoryOperation {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -100,7 +86,6 @@ export const useOperations = (
|
||||||
pluginsApi?: LaboratoryPluginsState & LaboratoryPluginsActions;
|
pluginsApi?: LaboratoryPluginsState & LaboratoryPluginsActions;
|
||||||
} & LaboratoryOperationsCallbacks,
|
} & LaboratoryOperationsCallbacks,
|
||||||
): LaboratoryOperationsState & LaboratoryOperationsActions => {
|
): LaboratoryOperationsState & LaboratoryOperationsActions => {
|
||||||
// eslint-disable-next-line react/hook-use-state
|
|
||||||
const [operations, _setOperations] = useState<LaboratoryOperation[]>(
|
const [operations, _setOperations] = useState<LaboratoryOperation[]>(
|
||||||
props.defaultOperations ?? [],
|
props.defaultOperations ?? [],
|
||||||
);
|
);
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
} from 'graphql';
|
} from 'graphql';
|
||||||
import type { Maybe } from 'graphql/jsutils/Maybe';
|
import type { Maybe } from 'graphql/jsutils/Maybe';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import type { LaboratoryOperation } from '@/laboratory/lib/operations';
|
import type { LaboratoryOperation } from './operations';
|
||||||
|
|
||||||
export function healQuery(query: string) {
|
export function healQuery(query: string) {
|
||||||
return query.replace(/\{(\s+)?\}/g, '');
|
return query.replace(/\{(\s+)?\}/g, '');
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import type { LaboratoryContextProps } from '@/laboratory/components/laboratory/context';
|
import type { LaboratoryContextProps } from '../components/laboratory/context';
|
||||||
import { LaboratoryTabCustom } from '@/laboratory/lib/tabs';
|
import { LaboratoryTabCustom } from './tabs';
|
||||||
|
|
||||||
export interface LaboratoryPluginTab<State = Record<string, unknown>> {
|
export interface LaboratoryPluginTab<State = Record<string, unknown>> {
|
||||||
type: string;
|
type: string;
|
||||||
|
|
@ -56,7 +56,6 @@ export const usePlugins = (props: {
|
||||||
defaultPluginsState?: Record<string, any>;
|
defaultPluginsState?: Record<string, any>;
|
||||||
onPluginsStateChange?: (state: Record<string, any>) => void;
|
onPluginsStateChange?: (state: Record<string, any>) => void;
|
||||||
}): LaboratoryPluginsState & LaboratoryPluginsActions => {
|
}): LaboratoryPluginsState & LaboratoryPluginsActions => {
|
||||||
// eslint-disable-next-line react/hook-use-state
|
|
||||||
const [pluginsState, _setPluginsState] = useState<Record<string, any>>({
|
const [pluginsState, _setPluginsState] = useState<Record<string, any>>({
|
||||||
...props.plugins?.reduce(
|
...props.plugins?.reduce(
|
||||||
(acc, plugin) => {
|
(acc, plugin) => {
|
||||||
|
|
@ -1,14 +1,7 @@
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import cryptoJsSource from 'crypto-js/crypto-js.js?raw';
|
import cryptoJsSource from 'crypto-js/crypto-js.js?raw';
|
||||||
import type { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from '@/laboratory/lib/env';
|
import type { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from './env';
|
||||||
import { LaboratoryPlugin } from '@/laboratory/lib/plugins';
|
import { LaboratoryPlugin } from './plugins';
|
||||||
|
|
||||||
export interface LaboratoryPreflightPromptField {
|
|
||||||
title?: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
description?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LaboratoryPreflightLog {
|
export interface LaboratoryPreflightLog {
|
||||||
level: 'log' | 'warn' | 'error' | 'info' | 'system';
|
level: 'log' | 'warn' | 'error' | 'info' | 'system';
|
||||||
|
|
@ -48,15 +41,7 @@ export const usePreflight = (props: {
|
||||||
defaultPreflight?: LaboratoryPreflight | null;
|
defaultPreflight?: LaboratoryPreflight | null;
|
||||||
onPreflightChange?: (preflight: LaboratoryPreflight | null) => void;
|
onPreflightChange?: (preflight: LaboratoryPreflight | null) => void;
|
||||||
envApi: LaboratoryEnvState & LaboratoryEnvActions;
|
envApi: LaboratoryEnvState & LaboratoryEnvActions;
|
||||||
openPreflightPromptModal?: (props: {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
onSubmit?: (value: string | null) => void;
|
|
||||||
}) => void;
|
|
||||||
}): LaboratoryPreflightState & LaboratoryPreflightActions => {
|
}): LaboratoryPreflightState & LaboratoryPreflightActions => {
|
||||||
// eslint-disable-next-line react/hook-use-state
|
|
||||||
const [preflight, _setPreflight] = useState<LaboratoryPreflight | null>(
|
const [preflight, _setPreflight] = useState<LaboratoryPreflight | null>(
|
||||||
props.defaultPreflight ?? null,
|
props.defaultPreflight ?? null,
|
||||||
);
|
);
|
||||||
|
|
@ -78,19 +63,7 @@ export const usePreflight = (props: {
|
||||||
return runIsolatedLabScript(
|
return runIsolatedLabScript(
|
||||||
preflight.script,
|
preflight.script,
|
||||||
props.envApi?.env ?? { variables: {} },
|
props.envApi?.env ?? { variables: {} },
|
||||||
(title, defaultValue, options) => {
|
undefined,
|
||||||
return new Promise(resolve => {
|
|
||||||
props.openPreflightPromptModal?.({
|
|
||||||
title,
|
|
||||||
description: options?.description,
|
|
||||||
placeholder: options?.placeholder,
|
|
||||||
defaultValue,
|
|
||||||
onSubmit: value => {
|
|
||||||
resolve(value);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
plugins,
|
plugins,
|
||||||
pluginsState,
|
pluginsState,
|
||||||
);
|
);
|
||||||
|
|
@ -100,7 +73,10 @@ export const usePreflight = (props: {
|
||||||
|
|
||||||
const setLastTestResult = useCallback(
|
const setLastTestResult = useCallback(
|
||||||
(result: LaboratoryPreflightResult | null) => {
|
(result: LaboratoryPreflightResult | null) => {
|
||||||
_setPreflight({ ...(preflight ?? { script: '', enabled: true }), lastTestResult: result });
|
_setPreflight({
|
||||||
|
...(preflight ?? { script: '', enabled: true }),
|
||||||
|
lastTestResult: result,
|
||||||
|
});
|
||||||
props.onPreflightChange?.({
|
props.onPreflightChange?.({
|
||||||
...(preflight ?? { script: '', enabled: true }),
|
...(preflight ?? { script: '', enabled: true }),
|
||||||
lastTestResult: result,
|
lastTestResult: result,
|
||||||
|
|
@ -120,11 +96,7 @@ export const usePreflight = (props: {
|
||||||
export async function runIsolatedLabScript(
|
export async function runIsolatedLabScript(
|
||||||
script: string,
|
script: string,
|
||||||
env: LaboratoryEnv,
|
env: LaboratoryEnv,
|
||||||
prompt?: (
|
prompt?: (placeholder: string, defaultValue: string) => Promise<string | null>,
|
||||||
title: string,
|
|
||||||
defaultValue: string,
|
|
||||||
options?: { placeholder?: string; description?: string },
|
|
||||||
) => Promise<string | null>,
|
|
||||||
plugins: LaboratoryPlugin[] = [],
|
plugins: LaboratoryPlugin[] = [],
|
||||||
pluginsState: Record<string, any> = {},
|
pluginsState: Record<string, any> = {},
|
||||||
): Promise<LaboratoryPreflightResult> {
|
): Promise<LaboratoryPreflightResult> {
|
||||||
|
|
@ -184,10 +156,10 @@ export async function runIsolatedLabScript(
|
||||||
request: {
|
request: {
|
||||||
headers: new Headers()
|
headers: new Headers()
|
||||||
},
|
},
|
||||||
prompt: (title, defaultValue, options) => {
|
prompt: (placeholder, defaultValue) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
promptResolve = resolve;
|
promptResolve = resolve;
|
||||||
self.postMessage({ type: 'prompt', title, defaultValue, options: options ?? {} });
|
self.postMessage({ type: 'prompt', placeholder, defaultValue });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
|
|
@ -256,13 +228,29 @@ export async function runIsolatedLabScript(
|
||||||
}
|
}
|
||||||
} else if (data.type === 'log') {
|
} else if (data.type === 'log') {
|
||||||
if (data.level === 'log') {
|
if (data.level === 'log') {
|
||||||
logs.push({ level: 'log', message: data.message, createdAt: new Date().toISOString() });
|
logs.push({
|
||||||
|
level: 'log',
|
||||||
|
message: data.message,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
} else if (data.level === 'warn') {
|
} else if (data.level === 'warn') {
|
||||||
logs.push({ level: 'warn', message: data.message, createdAt: new Date().toISOString() });
|
logs.push({
|
||||||
|
level: 'warn',
|
||||||
|
message: data.message,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
} else if (data.level === 'error') {
|
} else if (data.level === 'error') {
|
||||||
logs.push({ level: 'error', message: data.message, createdAt: new Date().toISOString() });
|
logs.push({
|
||||||
|
level: 'error',
|
||||||
|
message: data.message,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
} else if (data.level === 'info') {
|
} else if (data.level === 'info') {
|
||||||
logs.push({ level: 'info', message: data.message, createdAt: new Date().toISOString() });
|
logs.push({
|
||||||
|
level: 'info',
|
||||||
|
message: data.message,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (data.type === 'header') {
|
} else if (data.type === 'header') {
|
||||||
headers[data.name] = data.value;
|
headers[data.name] = data.value;
|
||||||
|
|
@ -273,7 +261,7 @@ export async function runIsolatedLabScript(
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
} else if (data.type === 'prompt') {
|
} else if (data.type === 'prompt') {
|
||||||
void prompt?.(data.title, data.defaultValue, data.options).then(value => {
|
void prompt?.(data.placeholder, data.defaultValue).then(value => {
|
||||||
worker.postMessage({ type: 'prompt:result', value });
|
worker.postMessage({ type: 'prompt:result', value });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -18,7 +18,6 @@ export const useSettings = (props: {
|
||||||
defaultSettings?: LaboratorySettings | null;
|
defaultSettings?: LaboratorySettings | null;
|
||||||
onSettingsChange?: (settings: LaboratorySettings | null) => void;
|
onSettingsChange?: (settings: LaboratorySettings | null) => void;
|
||||||
}): LaboratorySettingsState & LaboratorySettingsActions => {
|
}): LaboratorySettingsState & LaboratorySettingsActions => {
|
||||||
// eslint-disable-next-line react/hook-use-state
|
|
||||||
const [settings, _setSettings] = useState<LaboratorySettings>(
|
const [settings, _setSettings] = useState<LaboratorySettings>(
|
||||||
props.defaultSettings ?? {
|
props.defaultSettings ?? {
|
||||||
fetch: {
|
fetch: {
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import type { LaboratoryEnv } from '@/laboratory/lib/env';
|
import type { LaboratoryEnv } from './env';
|
||||||
import type { LaboratoryHistoryRequest } from '@/laboratory/lib/history';
|
import type { LaboratoryHistoryRequest } from './history';
|
||||||
import type { LaboratoryOperation } from '@/laboratory/lib/operations';
|
import type { LaboratoryOperation } from './operations';
|
||||||
import type { LaboratoryPreflight } from '@/laboratory/lib/preflight';
|
import type { LaboratoryPreflight } from './preflight';
|
||||||
import type { LaboratoryTest } from '@/laboratory/lib/tests';
|
import type { LaboratoryTest } from './tests';
|
||||||
|
|
||||||
export interface LaboratoryTabOperation {
|
export interface LaboratoryTabOperation {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -88,10 +88,8 @@ export const useTabs = (props: {
|
||||||
onTabsChange?: (tabs: LaboratoryTab[]) => void;
|
onTabsChange?: (tabs: LaboratoryTab[]) => void;
|
||||||
onActiveTabIdChange?: (tabId: string | null) => void;
|
onActiveTabIdChange?: (tabId: string | null) => void;
|
||||||
}): LaboratoryTabsState & LaboratoryTabsActions => {
|
}): LaboratoryTabsState & LaboratoryTabsActions => {
|
||||||
// eslint-disable-next-line react/hook-use-state
|
|
||||||
const [tabs, _setTabs] = useState<LaboratoryTab[]>(props.defaultTabs ?? []);
|
const [tabs, _setTabs] = useState<LaboratoryTab[]>(props.defaultTabs ?? []);
|
||||||
|
|
||||||
// eslint-disable-next-line react/hook-use-state
|
|
||||||
const [activeTab, _setActiveTab] = useState<LaboratoryTab | null>(
|
const [activeTab, _setActiveTab] = useState<LaboratoryTab | null>(
|
||||||
props.defaultTabs?.find(t => t.id === props.defaultActiveTabId) ??
|
props.defaultTabs?.find(t => t.id === props.defaultActiveTabId) ??
|
||||||
props.defaultTabs?.[0] ??
|
props.defaultTabs?.[0] ??
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import type { LaboratoryOperation } from '@/laboratory/lib/operations';
|
import type { LaboratoryOperation } from './operations';
|
||||||
|
|
||||||
export interface LaboratoryTestTaskBase {
|
export interface LaboratoryTestTaskBase {
|
||||||
id: string;
|
id: string;
|
||||||
53
packages/libraries/laboratory/src/main.tsx
Normal file
53
packages/libraries/laboratory/src/main.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { Laboratory } from './components/laboratory/laboratory';
|
||||||
|
|
||||||
|
const getLocalStorage = (key: string) => {
|
||||||
|
const value = localStorage.getItem(key);
|
||||||
|
return value ? JSON.parse(value) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLocalStorage = (key: string, value: unknown) => {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<Laboratory
|
||||||
|
theme="dark"
|
||||||
|
defaultEndpoint={getLocalStorage('endpoint') ?? null}
|
||||||
|
onEndpointChange={endpoint => {
|
||||||
|
setLocalStorage('endpoint', endpoint ?? '');
|
||||||
|
}}
|
||||||
|
defaultCollections={getLocalStorage('collections') ?? []}
|
||||||
|
onCollectionsChange={collections => {
|
||||||
|
setLocalStorage('collections', collections);
|
||||||
|
}}
|
||||||
|
defaultTabs={getLocalStorage('tabs') ?? []}
|
||||||
|
onTabsChange={tabs => {
|
||||||
|
setLocalStorage('tabs', tabs);
|
||||||
|
}}
|
||||||
|
defaultOperations={getLocalStorage('operations') ?? []}
|
||||||
|
onOperationsChange={operations => {
|
||||||
|
setLocalStorage('operations', operations);
|
||||||
|
}}
|
||||||
|
defaultActiveTabId={getLocalStorage('activeTabId') ?? null}
|
||||||
|
onActiveTabIdChange={activeTabId => {
|
||||||
|
setLocalStorage('activeTabId', activeTabId ?? '');
|
||||||
|
}}
|
||||||
|
defaultPreflight={getLocalStorage('preflight') ?? null}
|
||||||
|
onPreflightChange={preflight => {
|
||||||
|
setLocalStorage('preflight', preflight ?? '');
|
||||||
|
}}
|
||||||
|
defaultEnv={getLocalStorage('env') ?? null}
|
||||||
|
onEnvChange={env => {
|
||||||
|
setLocalStorage('env', env ?? '');
|
||||||
|
}}
|
||||||
|
defaultSettings={getLocalStorage('settings') ?? null}
|
||||||
|
onSettingsChange={settings => {
|
||||||
|
setLocalStorage('settings', settings ?? '');
|
||||||
|
}}
|
||||||
|
defaultHistory={getLocalStorage('history') ?? []}
|
||||||
|
onHistoryChange={history => {
|
||||||
|
setLocalStorage('history', history);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
109
packages/libraries/laboratory/src/plugins/target-env.tsx
Normal file
109
packages/libraries/laboratory/src/plugins/target-env.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { GlobeIcon } from 'lucide-react';
|
||||||
|
import { Editor } from '../components/laboratory/editor';
|
||||||
|
import { LaboratoryPlugin } from '../lib/plugins';
|
||||||
|
|
||||||
|
export const TargetEnvPlugin = (props: {
|
||||||
|
organizationSlug: string;
|
||||||
|
projectSlug: string;
|
||||||
|
targetSlug: string;
|
||||||
|
}) => {
|
||||||
|
const targetId = `${props.organizationSlug}/${props.projectSlug}/${props.targetSlug}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: 'targetEnv',
|
||||||
|
name: 'Target Environment',
|
||||||
|
description: 'Environment variables for the target',
|
||||||
|
preflight: {
|
||||||
|
lab: {
|
||||||
|
definition: `
|
||||||
|
targetEnvironment: {
|
||||||
|
set: (key: string, value: string) => void;
|
||||||
|
get: (key: string) => string;
|
||||||
|
delete: (key: string) => void;
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
props: {
|
||||||
|
targetId,
|
||||||
|
},
|
||||||
|
object: (props, state, setState) => {
|
||||||
|
return {
|
||||||
|
targetEnvironment: {
|
||||||
|
set: (key: string, value: string) => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
[props.targetId]: {
|
||||||
|
...state[props.targetId],
|
||||||
|
[key]: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
get: (key: string) => {
|
||||||
|
return state[props.targetId]?.[key];
|
||||||
|
},
|
||||||
|
delete: (key: string) => {
|
||||||
|
const newState = JSON.parse(JSON.stringify(state));
|
||||||
|
|
||||||
|
delete newState[props.targetId][key];
|
||||||
|
|
||||||
|
setState(newState);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: 'Open Target Environment Variables',
|
||||||
|
icon: <GlobeIcon />,
|
||||||
|
onClick: laboratory => {
|
||||||
|
const tab =
|
||||||
|
laboratory.tabs.find(t => t.type === 'target-env') ??
|
||||||
|
laboratory.addTab({
|
||||||
|
type: 'target-env',
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
laboratory.setActiveTab(tab);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
type: 'target-env',
|
||||||
|
name: 'Target Environment Variables',
|
||||||
|
icon: <GlobeIcon className="size-4 text-orange-400" />,
|
||||||
|
component: (_tab, _laboratory, state, setState) => {
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
defaultValue={Object.entries(state?.[targetId] ?? {})
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join('\n')}
|
||||||
|
onChange={value => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
[targetId]: Object.fromEntries(
|
||||||
|
value
|
||||||
|
?.split('\n')
|
||||||
|
.filter(line => line.trim() && !line.trim().startsWith('#'))
|
||||||
|
.map(line => {
|
||||||
|
const parts = line.split(/=(.*)/s);
|
||||||
|
|
||||||
|
return [parts[0].trim(), (parts[1] ?? '').trim()];
|
||||||
|
}) ?? [],
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
language="dotenv"
|
||||||
|
options={{
|
||||||
|
scrollbar: {
|
||||||
|
horizontal: 'hidden',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} satisfies LaboratoryPlugin<Record<string, Record<string, string>>>;
|
||||||
|
};
|
||||||
32
packages/libraries/laboratory/tsconfig.app.json
Normal file
32
packages/libraries/laboratory/tsconfig.app.json
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": false,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
10
packages/libraries/laboratory/tsconfig.json
Normal file
10
packages/libraries/laboratory/tsconfig.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
packages/libraries/laboratory/tsconfig.node.json
Normal file
25
packages/libraries/laboratory/tsconfig.node.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": false,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts", "vite.lib.config.ts", "vite.umd.config.ts"]
|
||||||
|
}
|
||||||
30
packages/libraries/laboratory/vite.config.ts
Normal file
30
packages/libraries/laboratory/vite.config.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import path from 'path';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import monacoEditor from 'vite-plugin-monaco-editor';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig(() => {
|
||||||
|
return {
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tailwindcss(),
|
||||||
|
// @ts-expect-error temp
|
||||||
|
monacoEditor.default({
|
||||||
|
languageWorkers: ['json', 'typescript', 'editorWorkerService'],
|
||||||
|
customWorkers: [
|
||||||
|
{
|
||||||
|
label: 'graphql',
|
||||||
|
entry: 'monaco-graphql/dist/graphql.worker',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
83
packages/libraries/laboratory/vite.lib.config.ts
Normal file
83
packages/libraries/laboratory/vite.lib.config.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import path from 'path';
|
||||||
|
import dts from 'unplugin-dts/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import monacoEditor from 'vite-plugin-monaco-editor';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
const externals = [
|
||||||
|
'@tanstack/react-form',
|
||||||
|
'date-fns',
|
||||||
|
'graphql-ws',
|
||||||
|
'lucide-react',
|
||||||
|
'lz-string',
|
||||||
|
'react',
|
||||||
|
'react-dom',
|
||||||
|
'react-dom/client',
|
||||||
|
'react/jsx-runtime',
|
||||||
|
'react/jsx-dev-runtime',
|
||||||
|
'tslib',
|
||||||
|
'zod',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tailwindcss(),
|
||||||
|
// @ts-expect-error temporary package typing mismatch
|
||||||
|
monacoEditor.default({
|
||||||
|
languageWorkers: ['json', 'typescript', 'editorWorkerService'],
|
||||||
|
customWorkers: [
|
||||||
|
{
|
||||||
|
label: 'graphql',
|
||||||
|
entry: 'monaco-graphql/dist/graphql.worker',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
dts({
|
||||||
|
include: ['src/index.tsx', 'src/lib/**/*.ts', 'src/components/**/*.tsx'],
|
||||||
|
exclude: ['src/main.tsx'],
|
||||||
|
insertTypesEntry: true,
|
||||||
|
staticImport: true,
|
||||||
|
outDirs: ['dist'],
|
||||||
|
tsconfigPath: './tsconfig.app.json',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
copyPublicDir: false,
|
||||||
|
cssCodeSplit: false,
|
||||||
|
commonjsOptions: {
|
||||||
|
esmExternals: true,
|
||||||
|
},
|
||||||
|
lib: {
|
||||||
|
entry: path.resolve(__dirname, './src/index.tsx'),
|
||||||
|
name: 'HiveLaboratory',
|
||||||
|
formats: ['es', 'cjs'],
|
||||||
|
fileName: format => `hive-laboratory.${format}.js`,
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: externals,
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
react: 'React',
|
||||||
|
'react-dom': 'ReactDOM',
|
||||||
|
'react-dom/client': 'ReactDOM',
|
||||||
|
'@tanstack/react-form': 'TanStackReactForm',
|
||||||
|
'date-fns': 'dateFns',
|
||||||
|
'graphql-ws': 'graphqlWs',
|
||||||
|
'lucide-react': 'LucideReact',
|
||||||
|
'lz-string': 'LZString',
|
||||||
|
'react/jsx-runtime': 'ReactJSXRuntime',
|
||||||
|
'react/jsx-dev-runtime': 'ReactJSXRuntime',
|
||||||
|
tslib: 'tslib',
|
||||||
|
zod: 'Zod',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
49
packages/libraries/laboratory/vite.umd.config.ts
Normal file
49
packages/libraries/laboratory/vite.umd.config.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import path from 'path';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import monacoEditor from 'vite-plugin-monaco-editor';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
define: {
|
||||||
|
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||||
|
'global.process.env.NODE_ENV': JSON.stringify('production'),
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tailwindcss(),
|
||||||
|
// @ts-expect-error temporary package typing mismatch
|
||||||
|
monacoEditor.default({
|
||||||
|
languageWorkers: ['json', 'typescript', 'editorWorkerService'],
|
||||||
|
customWorkers: [
|
||||||
|
{
|
||||||
|
label: 'graphql',
|
||||||
|
entry: 'monaco-graphql/dist/graphql.worker',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
copyPublicDir: false,
|
||||||
|
emptyOutDir: false,
|
||||||
|
cssCodeSplit: false,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
// UMD consumers can execute this in plain browser contexts without Node globals.
|
||||||
|
intro:
|
||||||
|
"var process = typeof globalThis !== 'undefined' && globalThis.process ? globalThis.process : { env: { NODE_ENV: 'production' } };",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lib: {
|
||||||
|
entry: path.resolve(__dirname, './src/index.tsx'),
|
||||||
|
name: 'HiveLaboratory',
|
||||||
|
formats: ['umd'],
|
||||||
|
fileName: () => 'hive-laboratory.umd.js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
1
packages/libraries/render-laboratory/.gitignore
vendored
Normal file
1
packages/libraries/render-laboratory/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
src/laboratory.ts
|
||||||
1193
packages/libraries/render-laboratory/CHANGELOG.md
Normal file
1193
packages/libraries/render-laboratory/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load diff
74
packages/libraries/render-laboratory/package.json
Normal file
74
packages/libraries/render-laboratory/package.json
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"name": "@graphql-hive/render-laboratory",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"description": "",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/graphql-hive/console.git",
|
||||||
|
"directory": "packages/render-laboratory"
|
||||||
|
},
|
||||||
|
"author": "Michael Skorokhodov <michael@the-guild.dev>",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"main": "dist/cjs/index.js",
|
||||||
|
"module": "dist/esm/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/typings/index.d.cts",
|
||||||
|
"default": "./dist/cjs/index.js"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/typings/index.d.ts",
|
||||||
|
"default": "./dist/esm/index.js"
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"types": "./dist/typings/index.d.ts",
|
||||||
|
"default": "./dist/esm/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/typings/*.d.cts",
|
||||||
|
"default": "./dist/cjs/*.js"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/typings/*.d.ts",
|
||||||
|
"default": "./dist/esm/*.js"
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"types": "./dist/typings/*.d.ts",
|
||||||
|
"default": "./dist/esm/*.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"typings": "dist/typings/index.d.ts",
|
||||||
|
"keywords": [
|
||||||
|
"graphql",
|
||||||
|
"server",
|
||||||
|
"api",
|
||||||
|
"graphql-server"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "node scripts/yoga-bundle-string.mjs && bob build",
|
||||||
|
"check": "tsc --pretty --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@graphql-hive/laboratory": "workspace:*",
|
||||||
|
"graphql-yoga": "5.13.3"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"directory": "dist",
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"buildOptions": {
|
||||||
|
"input": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"definition": "dist/typings/index.d.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const directoryName = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const inputPath = path.resolve(
|
||||||
|
directoryName,
|
||||||
|
'../../../../node_modules/@graphql-hive/laboratory/dist',
|
||||||
|
);
|
||||||
|
const jsFile = path.resolve(inputPath, 'hive-laboratory.umd.js');
|
||||||
|
const cssFile = path.resolve(inputPath, 'laboratory.css');
|
||||||
|
const editorWorkerServiceFile = path.resolve(
|
||||||
|
inputPath,
|
||||||
|
'monacoeditorwork',
|
||||||
|
'editor.worker.bundle.js',
|
||||||
|
);
|
||||||
|
const graphqlWorkerFile = path.resolve(inputPath, 'monacoeditorwork', 'graphql.worker.bundle.js');
|
||||||
|
const jsonWorkerFile = path.resolve(inputPath, 'monacoeditorwork', 'json.worker.bundle.js');
|
||||||
|
const typescriptWorkerFile = path.resolve(inputPath, 'monacoeditorwork', 'ts.worker.bundle.js');
|
||||||
|
const faviconFile = path.resolve(
|
||||||
|
directoryName,
|
||||||
|
'../../../../packages/web/app/public/just-logo.svg',
|
||||||
|
);
|
||||||
|
const faviconDarkFile = path.resolve(
|
||||||
|
directoryName,
|
||||||
|
'../../../../packages/web/app/public/just-logo-black.svg',
|
||||||
|
);
|
||||||
|
|
||||||
|
const outFile = path.resolve(directoryName, '..', 'src', 'laboratory.ts');
|
||||||
|
|
||||||
|
const [
|
||||||
|
jsContents,
|
||||||
|
faviconContents,
|
||||||
|
faviconDarkContents,
|
||||||
|
cssContents,
|
||||||
|
editorWorkerServiceContents,
|
||||||
|
graphqlWorkerContents,
|
||||||
|
jsonWorkerContents,
|
||||||
|
typescriptWorkerContents,
|
||||||
|
] = await Promise.all([
|
||||||
|
fs.promises.readFile(jsFile, 'utf-8'),
|
||||||
|
fs.promises.readFile(faviconFile, 'base64'),
|
||||||
|
fs.promises.readFile(faviconDarkFile, 'base64'),
|
||||||
|
fs.promises.readFile(cssFile, 'utf-8'),
|
||||||
|
fs.promises.readFile(editorWorkerServiceFile, 'utf-8'),
|
||||||
|
fs.promises.readFile(graphqlWorkerFile, 'utf-8'),
|
||||||
|
fs.promises.readFile(jsonWorkerFile, 'utf-8'),
|
||||||
|
fs.promises.readFile(typescriptWorkerFile, 'utf-8'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
outFile,
|
||||||
|
[
|
||||||
|
`export const js: string = ${JSON.stringify(jsContents)}`,
|
||||||
|
`export const favicon: string = ${JSON.stringify(
|
||||||
|
`data:image/x-icon;base64,${faviconContents}`,
|
||||||
|
)}`,
|
||||||
|
`export const faviconDark: string = ${JSON.stringify(
|
||||||
|
`data:image/svg+xml;base64,${faviconDarkContents}`,
|
||||||
|
)}`,
|
||||||
|
`export const css: string = ${JSON.stringify(cssContents)}`,
|
||||||
|
`export const editorWorkerService: string = ${JSON.stringify(editorWorkerServiceContents)}`,
|
||||||
|
`export const graphqlWorker: string = ${JSON.stringify(graphqlWorkerContents)}`,
|
||||||
|
`export const jsonWorker: string = ${JSON.stringify(jsonWorkerContents)}`,
|
||||||
|
`export const typescriptWorker: string = ${JSON.stringify(typescriptWorkerContents)}`,
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
70
packages/libraries/render-laboratory/src/index.ts
Normal file
70
packages/libraries/render-laboratory/src/index.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import type { GraphiQLOptions } from 'graphql-yoga';
|
||||||
|
import {
|
||||||
|
editorWorkerService,
|
||||||
|
favicon,
|
||||||
|
faviconDark,
|
||||||
|
graphqlWorker,
|
||||||
|
js,
|
||||||
|
jsonWorker,
|
||||||
|
typescriptWorker,
|
||||||
|
} from './laboratory.js';
|
||||||
|
|
||||||
|
export const renderLaboratory = (opts?: GraphiQLOptions) => /* HTML */ `
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>${opts?.title || 'Hive Laboratory'}</title>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/svg+xml"
|
||||||
|
media="(prefers-color-scheme: light)"
|
||||||
|
href="${favicon}"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/svg+xml"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
href="${faviconDark}"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body id="body" class="no-focus-outline">
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function prepareBlob(workerContent) {
|
||||||
|
const blob = new Blob([workerContent], { type: 'application/javascript' });
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
|
}
|
||||||
|
const workers = {
|
||||||
|
editorWorkerService: prepareBlob(${JSON.stringify(editorWorkerService)}),
|
||||||
|
typescript: prepareBlob(${JSON.stringify(typescriptWorker)}),
|
||||||
|
json: prepareBlob(${JSON.stringify(jsonWorker)}),
|
||||||
|
graphql: prepareBlob(${JSON.stringify(graphqlWorker)}),
|
||||||
|
};
|
||||||
|
self['MonacoEnvironment'] = {
|
||||||
|
globalAPI: false,
|
||||||
|
getWorkerUrl: function (moduleId, label) {
|
||||||
|
return workers[label];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
${js};
|
||||||
|
|
||||||
|
HiveLaboratory.renderLaboratory(window.document.querySelector('#root'));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
14
packages/libraries/render-laboratory/tsconfig.json
Normal file
14
packages/libraries/render-laboratory/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"target": "es2017",
|
||||||
|
"module": "esnext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,5 +5,6 @@
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"rootDir": "../.."
|
"rootDir": "../.."
|
||||||
},
|
},
|
||||||
"files": ["src/index.ts"]
|
"files": ["src/index.ts"],
|
||||||
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,6 @@
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"rootDir": "../../.."
|
"rootDir": "../../.."
|
||||||
},
|
},
|
||||||
"files": ["src/index.ts"]
|
"files": ["src/index.ts"],
|
||||||
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,6 @@
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"rootDir": "../.."
|
"rootDir": "../.."
|
||||||
},
|
},
|
||||||
"files": ["src/index.ts"]
|
"files": ["src/index.ts"],
|
||||||
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,6 @@
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"rootDir": "../.."
|
"rootDir": "../.."
|
||||||
},
|
},
|
||||||
"files": ["src/index.ts"]
|
"files": ["src/index.ts"],
|
||||||
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true
|
"emitDecoratorMetadata": true
|
||||||
},
|
},
|
||||||
"files": ["src/index.ts"]
|
"files": ["src/index.ts"],
|
||||||
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,6 @@
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"rootDir": "../.."
|
"rootDir": "../.."
|
||||||
},
|
},
|
||||||
"files": ["src/index.ts"]
|
"files": ["src/index.ts"],
|
||||||
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,6 @@
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"rootDir": "../.."
|
"rootDir": "../.."
|
||||||
},
|
},
|
||||||
"files": ["src/index.ts"]
|
"files": ["src/index.ts"],
|
||||||
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,6 @@
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"rootDir": "../.."
|
"rootDir": "../.."
|
||||||
},
|
},
|
||||||
"files": ["src/index.ts"]
|
"files": ["src/index.ts"],
|
||||||
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,6 @@
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"rootDir": "../.."
|
"rootDir": "../.."
|
||||||
},
|
},
|
||||||
"files": ["src/index.ts"]
|
"files": ["src/index.ts"],
|
||||||
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
"@graphiql/react": "1.0.0-alpha.4",
|
"@graphiql/react": "1.0.0-alpha.4",
|
||||||
"@graphiql/toolkit": "0.9.1",
|
"@graphiql/toolkit": "0.9.1",
|
||||||
"@graphql-codegen/client-preset-swc-plugin": "0.2.0",
|
"@graphql-codegen/client-preset-swc-plugin": "0.2.0",
|
||||||
|
"@graphql-hive/laboratory": "workspace:*",
|
||||||
"@graphql-inspector/core": "7.1.2",
|
"@graphql-inspector/core": "7.1.2",
|
||||||
"@graphql-inspector/patch": "0.1.3",
|
"@graphql-inspector/patch": "0.1.3",
|
||||||
"@graphql-tools/mock": "9.0.25",
|
"@graphql-tools/mock": "9.0.25",
|
||||||
|
|
|
||||||
|
|
@ -293,10 +293,48 @@
|
||||||
--chart-grid: 0 0% 45%;
|
--chart-grid: 0 0% 45%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hive-laboratory {
|
:root {
|
||||||
--primary: 40 89% 60%;
|
--hive-laboratory-radius: var(--radius);
|
||||||
--background: 223 70% 4%;
|
--hive-laboratory-background: var(--neutral-2);
|
||||||
--card: 220 21.43% 5.49%;
|
--hive-laboratory-foreground: var(--neutral-11);
|
||||||
|
--hive-laboratory-card: var(--neutral-1);
|
||||||
|
--hive-laboratory-card-foreground: var(--neutral-11);
|
||||||
|
--hive-laboratory-popover: var(--neutral-3);
|
||||||
|
--hive-laboratory-popover-foreground: var(--neutral-11);
|
||||||
|
--hive-laboratory-primary: var(--accent);
|
||||||
|
--hive-laboratory-primary-foreground: var(--neutral-1);
|
||||||
|
--hive-laboratory-secondary: var(--neutral-3);
|
||||||
|
--hive-laboratory-secondary-foreground: var(--neutral-11);
|
||||||
|
--hive-laboratory-muted: var(--neutral-3);
|
||||||
|
--hive-laboratory-muted-foreground: var(--neutral-11);
|
||||||
|
--hive-laboratory-accent: var(--neutral-4);
|
||||||
|
--hive-laboratory-accent-foreground: var(--neutral-11);
|
||||||
|
--hive-laboratory-destructive: var(--red-500);
|
||||||
|
--hive-laboratory-border: var(--neutral-5);
|
||||||
|
--hive-laboratory-input: var(--neutral-5);
|
||||||
|
--hive-laboratory-ring: var(--ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--hive-laboratory-radius: var(--radius);
|
||||||
|
--hive-laboratory-background: var(--neutral-1);
|
||||||
|
--hive-laboratory-foreground: var(--neutral-11);
|
||||||
|
--hive-laboratory-card: var(--neutral-2);
|
||||||
|
--hive-laboratory-card-foreground: var(--neutral-11);
|
||||||
|
--hive-laboratory-popover: var(--neutral-3);
|
||||||
|
--hive-laboratory-popover-foreground: var(--neutral-11);
|
||||||
|
--hive-laboratory-primary: var(--accent);
|
||||||
|
--hive-laboratory-primary-foreground: var(--neutral-1);
|
||||||
|
--hive-laboratory-secondary: var(--neutral-3);
|
||||||
|
--hive-laboratory-secondary-foreground: var(--neutral-11);
|
||||||
|
--hive-laboratory-muted: var(--neutral-3);
|
||||||
|
--hive-laboratory-muted-foreground: var(--neutral-11);
|
||||||
|
--hive-laboratory-accent: var(--neutral-6);
|
||||||
|
--hive-laboratory-accent-foreground: var(--neutral-11);
|
||||||
|
--hive-laboratory-destructive: var(--red-500);
|
||||||
|
--hive-laboratory-border: var(--neutral-5);
|
||||||
|
--hive-laboratory-input: var(--neutral-5);
|
||||||
|
--hive-laboratory-ring: var(--ring);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import { cva, type VariantProps } from 'class-variance-authority';
|
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import { Slot } from '@radix-ui/react-slot';
|
|
||||||
|
|
||||||
const buttonVariants = cva(
|
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-red-200 aria-invalid:border-red-500",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: 'bg-accent text-neutral-2 hover:bg-accent_80',
|
|
||||||
destructive: 'bg-red-500 text-neutral-12! hover:bg-red-400 focus-visible:ring-red-200',
|
|
||||||
outline: 'border bg-neutral-3 shadow-sm hover:bg-neutral-2 hover:text-neutral-12',
|
|
||||||
secondary: 'bg-neutral-2 text-neutral-11 hover:bg-neutral-2/80',
|
|
||||||
ghost: 'hover:bg-neutral-2 hover:text-neutral-12',
|
|
||||||
link: 'text-neutral-11 underline-offset-4 hover:underline',
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
|
||||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
|
||||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
|
||||||
icon: 'size-9',
|
|
||||||
'icon-sm': 'size-8',
|
|
||||||
'icon-lg': 'size-10',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: 'default',
|
|
||||||
size: 'default',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function Button({
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'button'> &
|
|
||||||
VariantProps<typeof buttonVariants> & {
|
|
||||||
asChild?: boolean;
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : 'button';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="button"
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type={type}
|
|
||||||
data-slot="input"
|
|
||||||
className={cn(
|
|
||||||
'file:text-neutral-11 placeholder:text-neutral-10 selection:bg-neutral-11 selection:text-neutral-2 border-neutral-5 h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-sm outline-none transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
||||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
|
||||||
'aria-invalid:ring-red-200 aria-invalid:border-red-500',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Input };
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
|
||||||
|
|
||||||
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<SwitchPrimitive.Root
|
|
||||||
data-slot="switch"
|
|
||||||
className={cn(
|
|
||||||
'data-[state=unchecked]:bg-neutral-5 focus-visible:border-ring focus-visible:ring-ring/50 data-[state=checked]:bg-neutral-2 peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-sm outline-none transition-all focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SwitchPrimitive.Thumb
|
|
||||||
data-slot="switch-thumb"
|
|
||||||
className={cn(
|
|
||||||
'bg-neutral-3 pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SwitchPrimitive.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Switch };
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
|
|
||||||
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
|
||||||
return (
|
|
||||||
<textarea
|
|
||||||
data-slot="textarea"
|
|
||||||
className={cn(
|
|
||||||
'border-neutral-4 placeholder:text-neutral-10 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-red-500/20 dark:aria-invalid:ring-red-500/40 aria-invalid:border-red-500 dark:bg-neutral-4/30 flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Textarea };
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import { cn } from '@/laboratory/lib/utils';
|
|
||||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
||||||
|
|
||||||
function TooltipProvider({
|
|
||||||
delayDuration = 0,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
||||||
return (
|
|
||||||
<TooltipPrimitive.Provider
|
|
||||||
data-slot="tooltip-provider"
|
|
||||||
delayDuration={delayDuration}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
||||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TooltipContent({
|
|
||||||
className,
|
|
||||||
sideOffset = 0,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipPrimitive.Content
|
|
||||||
data-slot="tooltip-content"
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
'bg-neutral-11 text-neutral-2 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-[var(--radix-tooltip-content-transform-origin)] text-balance rounded-md px-3 py-1.5 text-xs',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<TooltipPrimitive.Arrow className="bg-neutral-11 fill-neutral-11 z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
|
||||||
</TooltipPrimitive.Content>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue