mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 06:37:15 +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/**',
|
||||
// We bundle it all anyway, so there are no node_modules
|
||||
'packages/web/app/**',
|
||||
// We bundle it all anyway, so there are no node_modules
|
||||
'packages/libraries/laboratory/**',
|
||||
'**/*.spec.ts',
|
||||
'**/*.test.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 }}
|
||||
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
|
||||
if: ${{ inputs.codegen == 'true' }}
|
||||
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
|
||||
with:
|
||||
actor: typescript-typecheck
|
||||
buildLaboratory: true
|
||||
|
||||
- name: get cpu count
|
||||
id: cpu-cores
|
||||
|
|
|
|||
|
|
@ -287,12 +287,7 @@ describe('Execution', () => {
|
|||
parseSpecialCharSequences: false,
|
||||
},
|
||||
);
|
||||
cy.dataCy('env-editor-mini').within(() => {
|
||||
cy.get('textarea').type('{"foo":"injected"}', {
|
||||
force: true,
|
||||
parseSpecialCharSequences: false,
|
||||
});
|
||||
});
|
||||
setMonacoEditorContents('env-editor-mini', '{"foo":"injected"}');
|
||||
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@
|
|||
"ip": "npm:neoip@2.1.0",
|
||||
"miniflare@3>undici": "^7.18.2",
|
||||
"tailwindcss": "3.4.17",
|
||||
"@graphql-hive/laboratory>tailwindcss": "4.1.18",
|
||||
"@hive/app>tailwindcss": "4.1.18",
|
||||
"@tailwindcss/node>tailwindcss": "4.1.18",
|
||||
"@tailwindcss/vite>tailwindcss": "4.1.18",
|
||||
|
|
@ -155,6 +156,7 @@
|
|||
"seroval@<1.4.1": "^1.4.1",
|
||||
"fast-xml-parser@<5.3.8": "^5.3.8",
|
||||
"minimatch@10.x.x": "^10.2.2",
|
||||
"amqplib": "^0.8.0",
|
||||
"minimatch@9.x.x": "^9.0.6",
|
||||
"minimatch@3.x.x": "^3.1.3",
|
||||
"minimatch@4.x.x": "^4.2.4",
|
||||
|
|
@ -179,7 +181,8 @@
|
|||
"bentocache": "patches/bentocache.patch",
|
||||
"nextra": "patches/nextra.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": [
|
||||
"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 (
|
||||
<span>
|
||||
<GraphQLType type={props.type.ofType} />
|
||||
<span className="text-neutral-10!">!</span>
|
||||
<span className="text-muted-foreground!">!</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -23,9 +23,9 @@ export const GraphQLType = (props: {
|
|||
if (props.type instanceof GraphQLList) {
|
||||
return (
|
||||
<span>
|
||||
<span className="text-neutral-10!">[</span>
|
||||
<span className="text-muted-foreground!">[</span>
|
||||
<GraphQLType type={props.type.ofType} />
|
||||
<span className="text-neutral-10!">]</span>
|
||||
<span className="text-muted-foreground!">]</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import type { SVGProps } from 'react';
|
||||
import type { LucideProps } from 'lucide-react';
|
||||
|
||||
export const GraphQLIcon = (props: LucideProps) => {
|
||||
|
|
@ -8,7 +9,7 @@ export const GraphQLIcon = (props: LucideProps) => {
|
|||
viewBox="0 0 100 100"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
{...(props as SVGProps<SVGSVGElement>)}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
|
|
@ -16,35 +16,20 @@ import {
|
|||
FolderIcon,
|
||||
RotateCcwIcon,
|
||||
} from 'lucide-react';
|
||||
import { GraphQLType } from '@/laboratory/components/graphql-type';
|
||||
import { GraphQLIcon } from '@/laboratory/components/icons';
|
||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
||||
import { Button } from '@/laboratory/components/ui/button';
|
||||
import { Checkbox } from '@/laboratory/components/ui/checkbox';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/laboratory/components/ui/collapsible';
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
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';
|
||||
import type { LaboratoryOperation } from '../../lib/operations';
|
||||
import { getOpenPaths, isArgInQuery, isPathInQuery } from '../../lib/operations.utils';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { GraphQLType } from '../graphql-type';
|
||||
import { GraphQLIcon } from '../icons';
|
||||
import { Button } from '../ui/button';
|
||||
import { Checkbox } from '../ui/checkbox';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible';
|
||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../ui/empty';
|
||||
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '../ui/input-group';
|
||||
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||
import { useLaboratory } from './context';
|
||||
|
||||
export const BuilderArgument = (props: {
|
||||
field: GraphQLArgument;
|
||||
|
|
@ -76,8 +61,8 @@ export const BuilderArgument = (props: {
|
|||
<Button
|
||||
key={props.field.name}
|
||||
variant="ghost"
|
||||
className={cn('text-neutral-10 p-1! w-full justify-start text-xs', {
|
||||
'text-neutral-11': isInQuery,
|
||||
className={cn('text-muted-foreground p-1! w-full justify-start text-xs', {
|
||||
'text-foreground-primary': isInQuery,
|
||||
})}
|
||||
size="sm"
|
||||
>
|
||||
|
|
@ -157,9 +142,9 @@ export const BuilderScalarField = (props: {
|
|||
<Button
|
||||
variant="ghost"
|
||||
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={{
|
||||
|
|
@ -167,10 +152,10 @@ export const BuilderScalarField = (props: {
|
|||
}}
|
||||
size="sm"
|
||||
>
|
||||
<div className="bg-neutral-2 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="bg-card absolute left-0 top-0 -z-20 size-full" />
|
||||
<div className="group-hover:bg-accent/50 absolute left-0 top-0 -z-10 size-full transition-colors" />
|
||||
<ChevronDownIcon
|
||||
className={cn('text-neutral-10 size-4 transition-all', {
|
||||
className={cn('text-muted-foreground size-4 transition-all', {
|
||||
'-rotate-90': !isOpen,
|
||||
})}
|
||||
/>
|
||||
|
|
@ -191,7 +176,7 @@ export const BuilderScalarField = (props: {
|
|||
{props.field.name}: <GraphQLType type={props.field.type} />
|
||||
</Button>
|
||||
</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 && (
|
||||
<div>
|
||||
{args.length > 0 && (
|
||||
|
|
@ -200,9 +185,9 @@ export const BuilderScalarField = (props: {
|
|||
<Button
|
||||
variant="ghost"
|
||||
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={{
|
||||
|
|
@ -211,7 +196,7 @@ export const BuilderScalarField = (props: {
|
|||
size="sm"
|
||||
>
|
||||
<ChevronDownIcon
|
||||
className={cn('text-neutral-10 size-4 transition-all', {
|
||||
className={cn('text-muted-foreground size-4 transition-all', {
|
||||
'-rotate-90': !isOpen,
|
||||
})}
|
||||
/>
|
||||
|
|
@ -220,7 +205,7 @@ export const BuilderScalarField = (props: {
|
|||
[arguments]
|
||||
</Button>
|
||||
</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 => (
|
||||
<BuilderArgument
|
||||
key={arg.name}
|
||||
|
|
@ -244,8 +229,8 @@ export const BuilderScalarField = (props: {
|
|||
<Button
|
||||
key={props.field.name}
|
||||
variant="ghost"
|
||||
className={cn('text-neutral-10 p-1! w-full justify-start text-xs', {
|
||||
'text-neutral-11': isInQuery,
|
||||
className={cn('text-muted-foreground p-1! w-full justify-start text-xs', {
|
||||
'text-foreground-primary': isInQuery,
|
||||
})}
|
||||
size="sm"
|
||||
>
|
||||
|
|
@ -335,9 +320,9 @@ export const BuilderObjectField = (props: {
|
|||
<Button
|
||||
variant="ghost"
|
||||
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={{
|
||||
|
|
@ -345,10 +330,10 @@ export const BuilderObjectField = (props: {
|
|||
}}
|
||||
size="sm"
|
||||
>
|
||||
<div className="bg-neutral-2 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="bg-card absolute left-0 top-0 -z-20 size-full" />
|
||||
<div className="group-hover:bg-accent/50 absolute left-0 top-0 -z-10 size-full transition-colors" />
|
||||
<ChevronDownIcon
|
||||
className={cn('text-neutral-10 size-4 transition-all', {
|
||||
className={cn('text-muted-foreground size-4 transition-all', {
|
||||
'-rotate-90': !isOpen,
|
||||
})}
|
||||
/>
|
||||
|
|
@ -369,7 +354,7 @@ export const BuilderObjectField = (props: {
|
|||
{props.field.name}: <GraphQLType type={props.field.type} />
|
||||
</Button>
|
||||
</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 && (
|
||||
<div>
|
||||
{args.length > 0 && (
|
||||
|
|
@ -378,9 +363,9 @@ export const BuilderObjectField = (props: {
|
|||
<Button
|
||||
variant="ghost"
|
||||
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={{
|
||||
|
|
@ -389,7 +374,7 @@ export const BuilderObjectField = (props: {
|
|||
size="sm"
|
||||
>
|
||||
<ChevronDownIcon
|
||||
className={cn('text-neutral-10 size-4 transition-all', {
|
||||
className={cn('text-muted-foreground size-4 transition-all', {
|
||||
'-rotate-90': !isOpen,
|
||||
})}
|
||||
/>
|
||||
|
|
@ -398,7 +383,7 @@ export const BuilderObjectField = (props: {
|
|||
[arguments]
|
||||
</Button>
|
||||
</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 => (
|
||||
<BuilderArgument
|
||||
key={arg.name}
|
||||
|
|
@ -529,7 +514,7 @@ export const Builder = (props: {
|
|||
}, [defaultEndpoint, setEndpointValue]);
|
||||
|
||||
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">
|
||||
<span className="text-base font-medium">Builder</span>
|
||||
<div className="ml-auto flex items-center">
|
||||
|
|
@ -542,7 +527,7 @@ export const Builder = (props: {
|
|||
className="p-1! size-6 rounded-sm"
|
||||
disabled={openPaths.length === 0}
|
||||
>
|
||||
<CopyMinusIcon className="text-neutral-10 size-4" />
|
||||
<CopyMinusIcon className="text-muted-foreground size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Collapse all</TooltipContent>
|
||||
|
|
@ -581,7 +566,7 @@ export const Builder = (props: {
|
|||
onValueChange={setTabValue}
|
||||
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">
|
||||
<TabsTrigger value="query" disabled={queryFields.length === 0} className="text-xs">
|
||||
Query
|
||||
|
|
@ -654,7 +639,7 @@ export const Builder = (props: {
|
|||
<Empty className="px-0! h-96 w-full">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<FolderIcon className="text-neutral-10 size-6" />
|
||||
<FolderIcon className="text-muted-foreground size-6" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle className="text-base">No endpoint selected</EmptyTitle>
|
||||
<EmptyDescription className="text-xs">
|
||||
|
|
@ -7,8 +7,10 @@ import {
|
|||
TrashIcon,
|
||||
XIcon,
|
||||
} from 'lucide-react';
|
||||
import { GraphQLIcon } from '@/laboratory/components/icons';
|
||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
||||
import { TooltipTrigger } from '@radix-ui/react-tooltip';
|
||||
import type { LaboratoryCollection, LaboratoryCollectionOperation } from '../../lib/collections';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { GraphQLIcon } from '../icons';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
|
@ -19,13 +21,9 @@ import {
|
|||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/laboratory/components/ui/alert-dialog';
|
||||
import { Button } from '@/laboratory/components/ui/button';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/laboratory/components/ui/collapsible';
|
||||
} from '../ui/alert-dialog';
|
||||
import { Button } from '../ui/button';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible';
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
|
|
@ -33,16 +31,11 @@ import {
|
|||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from '@/laboratory/components/ui/empty';
|
||||
import { Input } from '@/laboratory/components/ui/input';
|
||||
import { ScrollArea, ScrollBar } from '@/laboratory/components/ui/scroll-area';
|
||||
import { Tooltip, TooltipContent } from '@/laboratory/components/ui/tooltip';
|
||||
import type {
|
||||
LaboratoryCollection,
|
||||
LaboratoryCollectionOperation,
|
||||
} from '@/laboratory/lib/collections';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import { TooltipTrigger } from '@radix-ui/react-tooltip';
|
||||
} from '../ui/empty';
|
||||
import { Input } from '../ui/input';
|
||||
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
||||
import { Tooltip, TooltipContent } from '../ui/tooltip';
|
||||
import { useLaboratory } from './context';
|
||||
|
||||
export const CollectionItem = (props: { collection: LaboratoryCollection }) => {
|
||||
const {
|
||||
|
|
@ -64,13 +57,13 @@ export const CollectionItem = (props: { collection: LaboratoryCollection }) => {
|
|||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
{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}
|
||||
{checkPermissions?.('collections:delete') && (
|
||||
|
|
@ -80,7 +73,7 @@ export const CollectionItem = (props: { collection: LaboratoryCollection }) => {
|
|||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
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 => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
|
|
@ -120,7 +113,7 @@ export const CollectionItem = (props: { collection: LaboratoryCollection }) => {
|
|||
)}
|
||||
</Button>
|
||||
</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 &&
|
||||
props.collection.operations.map(operation => {
|
||||
const isActive = activeOperation?.id === operation.id;
|
||||
|
|
@ -130,7 +123,7 @@ export const CollectionItem = (props: { collection: LaboratoryCollection }) => {
|
|||
key={operation.name}
|
||||
variant="ghost"
|
||||
className={cn('group w-full justify-start gap-2 px-2', {
|
||||
'bg-accent_80': isActive,
|
||||
'bg-accent/50': isActive,
|
||||
})}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
|
|
@ -156,7 +149,7 @@ export const CollectionItem = (props: { collection: LaboratoryCollection }) => {
|
|||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
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 => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
|
|
@ -219,7 +212,7 @@ export const CollectionsSearchResult = (props: { items: CollectionsSearchResultI
|
|||
key={operation.name}
|
||||
variant="ghost"
|
||||
className={cn('group w-full justify-start gap-2 px-2', {
|
||||
'bg-accent_80': isActive,
|
||||
'bg-accent/50': isActive,
|
||||
})}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
|
|
@ -237,8 +230,8 @@ export const CollectionsSearchResult = (props: { items: CollectionsSearchResultI
|
|||
}}
|
||||
>
|
||||
<GraphQLIcon className="size-4 text-pink-500" />
|
||||
<span className="text-neutral-10 truncate">{operation.parent.name}</span>
|
||||
<span className="text-neutral-10">{' / '}</span>
|
||||
<span className="text-muted-foreground truncate">{operation.parent.name}</span>
|
||||
<span className="text-muted-foreground">{' / '}</span>
|
||||
{operation.name}
|
||||
</Button>
|
||||
);
|
||||
|
|
@ -282,7 +275,7 @@ export const Collections = () => {
|
|||
className="p-1! size-6 rounded-sm"
|
||||
onClick={openAddCollectionDialog}
|
||||
>
|
||||
<FolderPlusIcon className="text-neutral-11 size-4" />
|
||||
<FolderPlusIcon className="text-primary size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add collection</TooltipContent>
|
||||
|
|
@ -290,8 +283,8 @@ export const Collections = () => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-neutral-5 relative border-b p-3">
|
||||
<SearchIcon className="text-neutral-10 absolute left-5 top-1/2 size-4 -translate-y-1/2" />
|
||||
<div className="border-border relative border-b p-3">
|
||||
<SearchIcon className="text-muted-foreground absolute left-5 top-1/2 size-4 -translate-y-1/2" />
|
||||
<Input
|
||||
type="text"
|
||||
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"
|
||||
onClick={() => setSearch('')}
|
||||
>
|
||||
<XIcon className="text-neutral-10 size-4" />
|
||||
<XIcon className="text-muted-foreground size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -321,7 +314,7 @@ export const Collections = () => {
|
|||
<Empty className="px-0! w-full">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<SearchIcon className="text-neutral-10 size-6" />
|
||||
<SearchIcon className="text-muted-foreground size-6" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle className="text-base">No results found</EmptyTitle>
|
||||
<EmptyDescription className="text-xs">
|
||||
|
|
@ -336,7 +329,7 @@ export const Collections = () => {
|
|||
<Empty className="px-0! w-full">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<FolderIcon className="text-neutral-10 size-6" />
|
||||
<FolderIcon className="text-muted-foreground size-6" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle className="text-base">No collections yet</EmptyTitle>
|
||||
<EmptyDescription className="text-xs">
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { FilePlus2Icon, FolderPlusIcon, PlayIcon, RefreshCcwIcon, ServerIcon } from 'lucide-react';
|
||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
|
|
@ -10,7 +9,8 @@ import {
|
|||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
} from '@/laboratory/components/ui/command';
|
||||
} from '../ui/command';
|
||||
import { useLaboratory } from './context';
|
||||
|
||||
export function Command(props: { open?: boolean; onOpenChange?: (open: boolean) => void }) {
|
||||
const {
|
||||
|
|
@ -5,47 +5,36 @@ import {
|
|||
type LaboratoryCollectionOperation,
|
||||
type LaboratoryCollectionsActions,
|
||||
type LaboratoryCollectionsState,
|
||||
} from '@/laboratory/lib/collections';
|
||||
import {
|
||||
type LaboratoryEndpointActions,
|
||||
type LaboratoryEndpointState,
|
||||
} from '@/laboratory/lib/endpoint';
|
||||
import type { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from '@/laboratory/lib/env';
|
||||
} from '../../lib/collections';
|
||||
import { type LaboratoryEndpointActions, type LaboratoryEndpointState } from '../../lib/endpoint';
|
||||
import type { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from '../../lib/env';
|
||||
import type {
|
||||
LaboratoryHistory,
|
||||
LaboratoryHistoryActions,
|
||||
LaboratoryHistoryState,
|
||||
} from '@/laboratory/lib/history';
|
||||
} from '../../lib/history';
|
||||
import {
|
||||
type LaboratoryOperation,
|
||||
type LaboratoryOperationsActions,
|
||||
type LaboratoryOperationsState,
|
||||
} from '@/laboratory/lib/operations';
|
||||
} from '../../lib/operations';
|
||||
import {
|
||||
LaboratoryPlugin,
|
||||
LaboratoryPluginsActions,
|
||||
LaboratoryPluginsState,
|
||||
} from '@/laboratory/lib/plugins';
|
||||
} from '../../lib/plugins';
|
||||
import type {
|
||||
LaboratoryPreflight,
|
||||
LaboratoryPreflightActions,
|
||||
LaboratoryPreflightState,
|
||||
} from '@/laboratory/lib/preflight';
|
||||
} from '../../lib/preflight';
|
||||
import type {
|
||||
LaboratorySettings,
|
||||
LaboratorySettingsActions,
|
||||
LaboratorySettingsState,
|
||||
} from '@/laboratory/lib/settings';
|
||||
import type {
|
||||
LaboratoryTab,
|
||||
LaboratoryTabsActions,
|
||||
LaboratoryTabsState,
|
||||
} from '@/laboratory/lib/tabs';
|
||||
import type {
|
||||
LaboratoryTest,
|
||||
LaboratoryTestActions,
|
||||
LaboratoryTestState,
|
||||
} from '@/laboratory/lib/tests';
|
||||
} from '../../lib/settings';
|
||||
import type { LaboratoryTab, LaboratoryTabsActions, LaboratoryTabsState } from '../../lib/tabs';
|
||||
import type { LaboratoryTest, LaboratoryTestActions, LaboratoryTestState } from '../../lib/tests';
|
||||
|
||||
type LaboratoryContextState = LaboratoryCollectionsState &
|
||||
LaboratoryEndpointState &
|
||||
|
|
@ -58,6 +47,7 @@ type LaboratoryContextState = LaboratoryCollectionsState &
|
|||
LaboratoryPluginsState &
|
||||
LaboratoryTestState & {
|
||||
isFullScreen?: boolean;
|
||||
theme?: 'light' | 'dark';
|
||||
};
|
||||
type LaboratoryContextActions = LaboratoryCollectionsActions &
|
||||
LaboratoryEndpointActions &
|
||||
|
|
@ -73,9 +63,7 @@ type LaboratoryContextActions = LaboratoryCollectionsActions &
|
|||
openUpdateEndpointDialog?: () => void;
|
||||
openAddTestDialog?: () => void;
|
||||
openPreflightPromptModal?: (props: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
placeholder: string;
|
||||
defaultValue?: string;
|
||||
onSubmit?: (value: string | null) => void;
|
||||
}) => void;
|
||||
|
|
@ -86,9 +74,12 @@ type LaboratoryContextActions = LaboratoryCollectionsActions &
|
|||
) => boolean;
|
||||
};
|
||||
|
||||
const LaboratoryContext = createContext<LaboratoryContextState & LaboratoryContextActions>(
|
||||
{} as LaboratoryContextState & LaboratoryContextActions,
|
||||
);
|
||||
type LaboratoryContext = LaboratoryContextState &
|
||||
LaboratoryContextActions & {
|
||||
container: HTMLDivElement | null;
|
||||
};
|
||||
|
||||
const LaboratoryContext = createContext<LaboratoryContext>({} as LaboratoryContext);
|
||||
|
||||
export const useLaboratory = () => {
|
||||
return useContext(LaboratoryContext);
|
||||
|
|
@ -108,6 +99,7 @@ export interface LaboratoryPermissions {
|
|||
}
|
||||
|
||||
export interface LaboratoryApi {
|
||||
theme?: 'light' | 'dark';
|
||||
defaultEndpoint?: string | null;
|
||||
onEndpointChange?: (endpoint: string | null) => void;
|
||||
defaultSchemaIntrospection?: IntrospectionQuery | null;
|
||||
|
|
@ -144,9 +136,7 @@ export interface LaboratoryApi {
|
|||
openUpdateEndpointDialog?: () => void;
|
||||
openAddTestDialog?: () => void;
|
||||
openPreflightPromptModal?: (props: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
placeholder: string;
|
||||
defaultValue?: string;
|
||||
onSubmit?: (value: string | null) => void;
|
||||
}) => void;
|
||||
|
|
@ -176,7 +166,9 @@ export interface LaboratoryApi {
|
|||
|
||||
export type LaboratoryContextProps = LaboratoryContextState &
|
||||
LaboratoryContextActions &
|
||||
LaboratoryApi;
|
||||
LaboratoryApi & {
|
||||
container: HTMLDivElement | null;
|
||||
};
|
||||
|
||||
export const LaboratoryProvider = (props: React.PropsWithChildren<LaboratoryContextProps>) => {
|
||||
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 { initializeMode } from 'monaco-graphql/initializeMode';
|
||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
||||
import MonacoEditor, { loader } from '@monaco-editor/react';
|
||||
import { useLaboratory } from './context';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).monaco = monaco;
|
||||
(window as Window & typeof globalThis & { monaco: typeof monaco }).monaco = monaco;
|
||||
}
|
||||
|
||||
loader.config({ monaco });
|
||||
|
|
@ -73,7 +73,7 @@ const darkTheme: monaco.editor.IStandaloneThemeData = {
|
|||
],
|
||||
colors: {
|
||||
'editor.foreground': '#f6f8fa',
|
||||
'editor.background': '#18181b',
|
||||
'editor.background': '#0f1214',
|
||||
'editor.selectionBackground': '#2A2F34',
|
||||
'editor.inactiveSelectionBackground': '#2A2F34',
|
||||
'editor.lineHighlightBackground': '#2A2F34',
|
||||
|
|
@ -87,6 +87,15 @@ const darkTheme: monaco.editor.IStandaloneThemeData = {
|
|||
|
||||
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', {
|
||||
tokenizer: {
|
||||
root: [
|
||||
|
|
@ -104,19 +113,21 @@ monaco.languages.setMonarchTokensProvider('dotenv', {
|
|||
},
|
||||
});
|
||||
|
||||
export const Editor = forwardRef<
|
||||
{
|
||||
setValue: (value: string) => void;
|
||||
},
|
||||
React.ComponentProps<typeof MonacoEditor> & {
|
||||
uri?: monaco.Uri;
|
||||
variablesUri?: monaco.Uri;
|
||||
extraLibs?: string[];
|
||||
}
|
||||
>((props, ref) => {
|
||||
export type EditorHandle = {
|
||||
setValue: (value: string) => void;
|
||||
};
|
||||
|
||||
export type EditorProps = React.ComponentProps<typeof MonacoEditor> & {
|
||||
uri?: monaco.Uri;
|
||||
variablesUri?: monaco.Uri;
|
||||
extraLibs?: string[];
|
||||
};
|
||||
|
||||
const EditorInner = forwardRef<EditorHandle, EditorProps>((props, ref) => {
|
||||
const id = useId();
|
||||
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(() => {
|
||||
if (introspection) {
|
||||
|
|
@ -147,23 +158,46 @@ export const Editor = forwardRef<
|
|||
}, [introspection, props.uri?.toString(), props.variablesUri?.toString()]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.extraLibs) {
|
||||
for (const lib of props.extraLibs) {
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
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`,
|
||||
);
|
||||
void (async function () {
|
||||
if (!props.extraLibs?.length) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
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(
|
||||
ref,
|
||||
|
|
@ -177,18 +211,25 @@ export const Editor = forwardRef<
|
|||
[],
|
||||
);
|
||||
|
||||
if (!typescriptReady && props.language === 'typescript') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="size-full">
|
||||
<div className="size-full overflow-hidden">
|
||||
<MonacoEditor
|
||||
className="size-full"
|
||||
{...props}
|
||||
theme="hive-laboratory-dark"
|
||||
theme={theme === 'dark' ? 'hive-laboratory-dark' : 'hive-laboratory-light'}
|
||||
onMount={editor => {
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
loading={null}
|
||||
options={{
|
||||
...props.options,
|
||||
lineNumbers: 'on',
|
||||
cursorStyle: 'line',
|
||||
cursorBlinking: 'smooth',
|
||||
padding: {
|
||||
top: 16,
|
||||
},
|
||||
|
|
@ -206,3 +247,5 @@ export const Editor = forwardRef<
|
|||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const Editor = EditorInner as unknown as (props: EditorProps) => JSX.Element;
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
||||
import { Editor } from '@/laboratory/components/laboratory/editor';
|
||||
import { useLaboratory } from './context';
|
||||
import { Editor } from './editor';
|
||||
|
||||
export const Env = () => {
|
||||
const { env, setEnv } = useLaboratory();
|
||||
|
||||
return (
|
||||
<div className="bg-neutral-2 size-full">
|
||||
<div className="bg-card size-full">
|
||||
<Editor
|
||||
defaultValue={Object.entries(env?.variables ?? {})
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
||||
import { Operation } from '@/laboratory/components/laboratory/operation';
|
||||
import { LaboratoryHistoryRequest } from '@/laboratory/lib/history';
|
||||
import { LaboratoryHistoryRequest } from '../../lib/history';
|
||||
import { useLaboratory } from './context';
|
||||
import { Operation } from './operation';
|
||||
|
||||
export const HistoryItem = () => {
|
||||
const { activeTab, history } = useLaboratory();
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
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 {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
|
@ -12,24 +13,13 @@ import {
|
|||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/laboratory/components/ui/alert-dialog';
|
||||
import { Button } from '@/laboratory/components/ui/button';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/laboratory/components/ui/collapsible';
|
||||
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';
|
||||
} from '../ui/alert-dialog';
|
||||
import { Button } from '../ui/button';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible';
|
||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../ui/empty';
|
||||
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||
import { useLaboratory } from './context';
|
||||
|
||||
export const HistoryOperationItem = (props: { historyItem: LaboratoryHistoryRequest }) => {
|
||||
const { activeTab, addTab, setActiveTab, deleteHistory } = useLaboratory();
|
||||
|
|
@ -57,8 +47,8 @@ export const HistoryOperationItem = (props: { historyItem: LaboratoryHistoryRequ
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn('bg-neutral-3 group sticky top-0 w-full justify-start px-2', {
|
||||
'bg-neutral-2': isActive,
|
||||
className={cn('bg-background group sticky top-0 w-full justify-start px-2', {
|
||||
'bg-accent/50': isActive,
|
||||
})}
|
||||
onClick={() => {
|
||||
setActiveTab(
|
||||
|
|
@ -78,7 +68,7 @@ export const HistoryOperationItem = (props: { historyItem: LaboratoryHistoryRequ
|
|||
'text-red-500': isError,
|
||||
})}
|
||||
/>
|
||||
<span className="text-neutral-10">
|
||||
<span className="text-muted-foreground">
|
||||
{format(new Date(props.historyItem.createdAt), 'HH:mm')}
|
||||
</span>
|
||||
<div className="truncate">{props.historyItem.operation.name || 'Untitled'}</div>
|
||||
|
|
@ -89,7 +79,7 @@ export const HistoryOperationItem = (props: { historyItem: LaboratoryHistoryRequ
|
|||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
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 => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
|
|
@ -137,13 +127,13 @@ export const HistoryGroup = (props: { group: { date: string; items: LaboratoryHi
|
|||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
{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}
|
||||
<Tooltip>
|
||||
|
|
@ -152,7 +142,7 @@ export const HistoryGroup = (props: { group: { date: string; items: LaboratoryHi
|
|||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
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 => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
|
|
@ -188,7 +178,7 @@ export const HistoryGroup = (props: { group: { date: string; items: LaboratoryHi
|
|||
</Tooltip>
|
||||
</Button>
|
||||
</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 => {
|
||||
return <HistoryOperationItem key={h.id} historyItem={h as LaboratoryHistoryRequest} />;
|
||||
})}
|
||||
|
|
@ -239,7 +229,7 @@ export const History = () => {
|
|||
|
||||
return (
|
||||
<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>
|
||||
<div className="ml-auto flex items-center">
|
||||
<Tooltip>
|
||||
|
|
@ -249,7 +239,7 @@ export const History = () => {
|
|||
<Button
|
||||
variant="ghost"
|
||||
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}
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
|
|
@ -296,7 +286,7 @@ export const History = () => {
|
|||
<Empty className="px-0! w-full">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<ClockIcon className="text-neutral-10 size-6" />
|
||||
<ClockIcon className="text-muted-foreground size-6" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle className="text-base">No history yet</EmptyTitle>
|
||||
<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 monacoStyles from 'monaco-editor/min/vs/editor/editor.main.css?inline';
|
||||
import * as z from 'zod';
|
||||
import { Markdown } from '@/components/v2/markdown';
|
||||
import { Collections } from '@/laboratory/components/laboratory/collections';
|
||||
import { Command } from '@/laboratory/components/laboratory/command';
|
||||
import {
|
||||
LaboratoryPermission,
|
||||
LaboratoryPermissions,
|
||||
LaboratoryProvider,
|
||||
useLaboratory,
|
||||
type LaboratoryApi,
|
||||
} from '@/laboratory/components/laboratory/context';
|
||||
import { Env } from '@/laboratory/components/laboratory/env';
|
||||
import { History } from '@/laboratory/components/laboratory/history';
|
||||
import { HistoryItem } from '@/laboratory/components/laboratory/history-item';
|
||||
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 { useForm } from '@tanstack/react-form';
|
||||
import { useCollections } from '../../lib/collections';
|
||||
import { useEndpoint } from '../../lib/endpoint';
|
||||
import { useEnv } from '../../lib/env';
|
||||
import { useHistory } from '../../lib/history';
|
||||
import { useOperations } from '../../lib/operations';
|
||||
import { LaboratoryPluginTab, usePlugins } from '../../lib/plugins';
|
||||
import { usePreflight } from '../../lib/preflight';
|
||||
import { useSettings } from '../../lib/settings';
|
||||
import { LaboratoryTabCustom, useTabs } from '../../lib/tabs';
|
||||
import { useTests } from '../../lib/tests';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
|
|
@ -27,7 +25,7 @@ import {
|
|||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/laboratory/components/ui/dialog';
|
||||
} from '../ui/dialog';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -36,7 +34,7 @@ import {
|
|||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/laboratory/components/ui/dropdown-menu';
|
||||
} from '../ui/dropdown-menu';
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
|
|
@ -44,34 +42,59 @@ import {
|
|||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
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 {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from '@/laboratory/components/ui/field';
|
||||
import { Input } from '@/laboratory/components/ui/input';
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from '@/laboratory/components/ui/resizable';
|
||||
import { Toaster } from '@/laboratory/components/ui/sonner';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/laboratory/components/ui/tooltip';
|
||||
import { useCollections } from '@/laboratory/lib/collections';
|
||||
import { useEndpoint } from '@/laboratory/lib/endpoint';
|
||||
import { useEnv } from '@/laboratory/lib/env';
|
||||
import { useHistory } from '@/laboratory/lib/history';
|
||||
import { useOperations } from '@/laboratory/lib/operations';
|
||||
import { LaboratoryPluginTab, usePlugins } from '@/laboratory/lib/plugins';
|
||||
import { usePreflight } from '@/laboratory/lib/preflight';
|
||||
import { useSettings } from '@/laboratory/lib/settings';
|
||||
import { LaboratoryTabCustom, useTabs } from '@/laboratory/lib/tabs';
|
||||
import { useTests } from '@/laboratory/lib/tests';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import { useForm } from '@tanstack/react-form';
|
||||
LaboratoryPermission,
|
||||
LaboratoryPermissions,
|
||||
LaboratoryProvider,
|
||||
useLaboratory,
|
||||
type LaboratoryApi,
|
||||
} from './context';
|
||||
import { Env } from './env';
|
||||
import { History } from './history';
|
||||
import { HistoryItem } from './history-item';
|
||||
import { Operation } from './operation';
|
||||
import { Preflight } from './preflight';
|
||||
import { Settings } from './settings';
|
||||
import { Tabs } from './tabs';
|
||||
|
||||
const ShadowRootContainer = (props: { children: ReactNode }) => {
|
||||
const hostRef = useRef<HTMLDivElement | null>(null);
|
||||
const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!hostRef.current || shadowRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
setShadowRoot(hostRef.current.attachShadow({ mode: 'open' }));
|
||||
}, [shadowRoot]);
|
||||
|
||||
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({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
|
|
@ -88,9 +111,7 @@ const addTestFormSchema = z.object({
|
|||
const PreflightPromptModal = (props: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
placeholder: string;
|
||||
defaultValue?: string;
|
||||
onSubmit?: (value: string | null) => void;
|
||||
}) => {
|
||||
|
|
@ -125,6 +146,7 @@ const PreflightPromptModal = (props: {
|
|||
<DialogHeader>
|
||||
<DialogTitle>Preflight prompt</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>Enter values for the preflight script.</DialogDescription>
|
||||
<form
|
||||
id="preflight-prompt-form"
|
||||
onSubmit={e => {
|
||||
|
|
@ -136,15 +158,8 @@ const PreflightPromptModal = (props: {
|
|||
<form.Field name="value">
|
||||
{field => {
|
||||
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
|
||||
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>{props.title}</FieldLabel>
|
||||
{props.description && (
|
||||
<FieldDescription>
|
||||
<Markdown content={props.description} />
|
||||
</FieldDescription>
|
||||
)}
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
|
|
@ -238,7 +253,7 @@ const LaboratoryContent = () => {
|
|||
<Empty className="px-0! w-full">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<FileIcon className="text-neutral-10 size-6" />
|
||||
<FileIcon className="text-muted-foreground size-6" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No operation selected</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
|
|
@ -285,7 +300,7 @@ const LaboratoryContent = () => {
|
|||
className={cn(
|
||||
'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"
|
||||
size="icon"
|
||||
onClick={() => setActivePanel(activePanel === 'collections' ? null : 'collections')}
|
||||
className={cn('text-neutral-10 hover:text-neutral-11', {
|
||||
'text-neutral-11': activePanel === 'collections',
|
||||
className={cn('text-muted-foreground hover:text-foreground', {
|
||||
'text-foreground': activePanel === 'collections',
|
||||
})}
|
||||
>
|
||||
<FoldersIcon className="size-5" />
|
||||
|
|
@ -309,7 +324,7 @@ const LaboratoryContent = () => {
|
|||
className={cn(
|
||||
'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"
|
||||
size="icon"
|
||||
onClick={() => setActivePanel(activePanel === 'history' ? null : 'history')}
|
||||
className={cn('text-neutral-10 hover:text-neutral-11', {
|
||||
'text-neutral-11': activePanel === 'history',
|
||||
className={cn('text-muted-foreground hover:text-foreground', {
|
||||
'text-foreground': activePanel === 'history',
|
||||
})}
|
||||
>
|
||||
<HistoryIcon className="size-5" />
|
||||
|
|
@ -331,7 +346,7 @@ const LaboratoryContent = () => {
|
|||
className={cn(
|
||||
'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"
|
||||
size="icon"
|
||||
onClick={() => setActivePanel(activePanel === 'history' ? null : 'history')}
|
||||
className={cn('text-neutral-10 hover:text-neutral-11', {
|
||||
'text-neutral-11': activePanel === 'history',
|
||||
className={cn('text-muted-foreground hover:text-foreground', {
|
||||
'text-foreground': activePanel === 'history',
|
||||
})}
|
||||
>
|
||||
<SettingsIcon className="size-5" />
|
||||
|
|
@ -418,7 +433,7 @@ const LaboratoryContent = () => {
|
|||
<div className="w-full">
|
||||
<Tabs />
|
||||
</div>
|
||||
<div className="bg-neutral-3 flex-1 overflow-hidden">{contentNode}</div>
|
||||
<div className="bg-card flex-1 overflow-hidden">{contentNode}</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
|
|
@ -428,46 +443,50 @@ const LaboratoryContent = () => {
|
|||
export type LaboratoryProps = LaboratoryApi;
|
||||
|
||||
export const Laboratory = (
|
||||
props: Pick<
|
||||
LaboratoryProps,
|
||||
| 'permissions'
|
||||
| 'defaultEndpoint'
|
||||
| 'onEndpointChange'
|
||||
| 'defaultCollections'
|
||||
| 'onCollectionsChange'
|
||||
| 'onCollectionCreate'
|
||||
| 'onCollectionUpdate'
|
||||
| 'onCollectionDelete'
|
||||
| 'onCollectionOperationCreate'
|
||||
| 'onCollectionOperationUpdate'
|
||||
| 'onCollectionOperationDelete'
|
||||
| 'defaultOperations'
|
||||
| 'onOperationsChange'
|
||||
| 'defaultActiveOperationId'
|
||||
| 'onActiveOperationIdChange'
|
||||
| 'onOperationCreate'
|
||||
| 'onOperationUpdate'
|
||||
| 'onOperationDelete'
|
||||
| 'defaultHistory'
|
||||
| 'onHistoryChange'
|
||||
| 'onHistoryCreate'
|
||||
| 'onHistoryUpdate'
|
||||
| 'onHistoryDelete'
|
||||
| 'defaultTabs'
|
||||
| 'onTabsChange'
|
||||
| 'defaultPreflight'
|
||||
| 'onPreflightChange'
|
||||
| 'defaultEnv'
|
||||
| 'onEnvChange'
|
||||
| 'defaultActiveTabId'
|
||||
| 'onActiveTabIdChange'
|
||||
| 'defaultSettings'
|
||||
| 'onSettingsChange'
|
||||
| 'defaultTests'
|
||||
| 'onTestsChange'
|
||||
| 'plugins'
|
||||
| 'defaultPluginsState'
|
||||
| 'onPluginsStateChange'
|
||||
props: Partial<
|
||||
Pick<
|
||||
LaboratoryProps,
|
||||
| 'permissions'
|
||||
| 'defaultEndpoint'
|
||||
| 'onEndpointChange'
|
||||
| 'defaultCollections'
|
||||
| 'onCollectionsChange'
|
||||
| 'onCollectionCreate'
|
||||
| 'onCollectionUpdate'
|
||||
| 'onCollectionDelete'
|
||||
| 'onCollectionOperationCreate'
|
||||
| 'onCollectionOperationUpdate'
|
||||
| 'onCollectionOperationDelete'
|
||||
| 'defaultOperations'
|
||||
| 'onOperationsChange'
|
||||
| 'defaultActiveOperationId'
|
||||
| 'onActiveOperationIdChange'
|
||||
| 'onOperationCreate'
|
||||
| 'onOperationUpdate'
|
||||
| 'onOperationDelete'
|
||||
| 'defaultHistory'
|
||||
| 'onHistoryChange'
|
||||
| 'onHistoryCreate'
|
||||
| 'onHistoryUpdate'
|
||||
| 'onHistoryDelete'
|
||||
| 'defaultTabs'
|
||||
| 'onTabsChange'
|
||||
| 'defaultPreflight'
|
||||
| 'onPreflightChange'
|
||||
| 'defaultEnv'
|
||||
| 'onEnvChange'
|
||||
| 'defaultActiveTabId'
|
||||
| 'onActiveTabIdChange'
|
||||
| 'defaultSettings'
|
||||
| 'onSettingsChange'
|
||||
| 'defaultTests'
|
||||
| 'onTestsChange'
|
||||
| 'plugins'
|
||||
| 'defaultPluginsState'
|
||||
| 'onPluginsStateChange'
|
||||
| 'theme'
|
||||
| 'defaultSchemaIntrospection'
|
||||
>
|
||||
>,
|
||||
) => {
|
||||
const checkPermissions = useCallback(
|
||||
|
|
@ -485,51 +504,11 @@ export const Laboratory = (
|
|||
[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 envApi = useEnv(props);
|
||||
const preflightApi = usePreflight({
|
||||
...props,
|
||||
envApi,
|
||||
openPreflightPromptModal,
|
||||
});
|
||||
|
||||
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 [isFullScreen, setIsFullScreen] = useState(false);
|
||||
|
|
@ -626,193 +636,192 @@ export const Laboratory = (
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('hive-laboratory bg-neutral-3 size-full', {
|
||||
'fixed inset-0 z-50': isFullScreen,
|
||||
})}
|
||||
style={
|
||||
{
|
||||
'--color-primary': 'var(--color-orange-500)',
|
||||
} 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}
|
||||
<ShadowRootContainer>
|
||||
<style>{`${laboratoryStyles}\n${monacoStyles}`}</style>
|
||||
<div
|
||||
className={cn('hive-laboratory bg-background size-full', props.theme, {
|
||||
'fixed inset-0 z-50': isFullScreen,
|
||||
})}
|
||||
ref={containerRef}
|
||||
>
|
||||
<LaboratoryContent />
|
||||
</LaboratoryProvider>
|
||||
</div>
|
||||
<Toaster richColors closeButton position="top-right" theme={props.theme} />
|
||||
<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}
|
||||
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 { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
import { Builder } from '@/laboratory/components/laboratory/builder';
|
||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
||||
import { Editor } from '@/laboratory/components/laboratory/editor';
|
||||
import { Badge } from '@/laboratory/components/ui/badge';
|
||||
import { Button } from '@/laboratory/components/ui/button';
|
||||
import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu';
|
||||
import { useForm } from '@tanstack/react-form';
|
||||
import type {
|
||||
LaboratoryHistory,
|
||||
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 {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
|
|
@ -29,45 +35,19 @@ import {
|
|||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/laboratory/components/ui/dialog';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from '@/laboratory/components/ui/dropdown-menu';
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from '@/laboratory/components/ui/empty';
|
||||
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';
|
||||
} from '../ui/dialog';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from '../ui/dropdown-menu';
|
||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../ui/empty';
|
||||
import { Field, FieldGroup, FieldLabel } from '../ui/field';
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../ui/resizable';
|
||||
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||
import { Spinner } from '../ui/spinner';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||
import { Toggle } from '../ui/toggle';
|
||||
import { Builder } from './builder';
|
||||
import { useLaboratory } from './context';
|
||||
import { Editor } from './editor';
|
||||
|
||||
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">
|
||||
{historyItem?.preflightLogs?.map((log, 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
|
||||
className={cn('text-xs font-medium', {
|
||||
'text-blue-400': log.level === 'info',
|
||||
'text-green-400': log.level === 'log',
|
||||
'text-yellow-400': log.level === 'warn',
|
||||
'text-red-400': log.level === 'error',
|
||||
'text-neutral-10': log.level === 'system',
|
||||
'text-gray-400': log.level === 'system',
|
||||
})}
|
||||
>
|
||||
{log.level.toUpperCase()}
|
||||
|
|
@ -208,7 +188,7 @@ export const ResponseSubscription = ({
|
|||
|
||||
return (
|
||||
<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
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{isActiveOperationLoading ? (
|
||||
|
|
@ -224,7 +204,7 @@ export const ResponseSubscription = ({
|
|||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
{historyItem?.responses
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
.map((response, i) => {
|
||||
|
|
@ -237,11 +217,7 @@ export const ResponseSubscription = ({
|
|||
const height = 20.5 * value.split('\n').length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border-neutral-5 border-b"
|
||||
style={{ height: `${height}px` }}
|
||||
key={i}
|
||||
>
|
||||
<div className="border-border border-b" style={{ height: `${height}px` }} key={i}>
|
||||
<Editor
|
||||
key={response.createdAt}
|
||||
value={value}
|
||||
|
|
@ -316,7 +292,7 @@ export const Response = ({ historyItem }: { historyItem?: LaboratoryHistoryReque
|
|||
</Badge>
|
||||
)}
|
||||
{historyItem?.duration && (
|
||||
<Badge variant="outline" className="bg-neutral-2">
|
||||
<Badge variant="outline" className="bg-card">
|
||||
<ClockIcon className="size-3" />
|
||||
<span>
|
||||
{Math.round((historyItem as LaboratoryHistoryRequest).duration!)}
|
||||
|
|
@ -325,7 +301,7 @@ export const Response = ({ historyItem }: { historyItem?: LaboratoryHistoryReque
|
|||
</Badge>
|
||||
)}
|
||||
{historyItem?.size && (
|
||||
<Badge variant="outline" className="bg-neutral-2">
|
||||
<Badge variant="outline" className="bg-card">
|
||||
<FileTextIcon className="size-3" />
|
||||
<span>
|
||||
{Math.round((historyItem as LaboratoryHistoryRequest).size! / 1024)}
|
||||
|
|
@ -336,13 +312,13 @@ export const Response = ({ historyItem }: { historyItem?: LaboratoryHistoryReque
|
|||
</div>
|
||||
) : null}
|
||||
</TabsList>
|
||||
<TabsContent value="response">
|
||||
<TabsContent value="response" className="overflow-hidden">
|
||||
<ResponseBody historyItem={historyItem} />
|
||||
</TabsContent>
|
||||
<TabsContent value="headers">
|
||||
<TabsContent value="headers" className="overflow-hidden">
|
||||
<ResponseHeaders historyItem={historyItem} />
|
||||
</TabsContent>
|
||||
<TabsContent value="preflight">
|
||||
<TabsContent value="preflight" className="overflow-hidden">
|
||||
<ResponsePreflight historyItem={historyItem} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
|
@ -549,7 +525,7 @@ export const Query = (props: {
|
|||
);
|
||||
|
||||
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}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
|
|
@ -607,7 +583,7 @@ export const Query = (props: {
|
|||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</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>
|
||||
{checkPermissions?.('collectionsOperations:create') && (
|
||||
<Toggle
|
||||
|
|
@ -650,7 +626,7 @@ export const Query = (props: {
|
|||
size="sm"
|
||||
variant="default"
|
||||
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={() => {
|
||||
setPreflight({
|
||||
...(preflight ?? { script: '', enabled: true }),
|
||||
|
|
@ -715,23 +691,21 @@ export const Query = (props: {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="size-full">
|
||||
<Editor
|
||||
uri={monaco.Uri.file(`operation_${endpoint}.graphql`)}
|
||||
variablesUri={variablesUri}
|
||||
value={operation?.query ?? ''}
|
||||
onChange={value => {
|
||||
updateActiveOperation({
|
||||
query: value ?? '',
|
||||
});
|
||||
}}
|
||||
language="graphql"
|
||||
theme="hive-laboratory"
|
||||
options={{
|
||||
readOnly: props.isReadOnly,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Editor
|
||||
uri={monaco.Uri.file(`operation_${endpoint}.graphql`)}
|
||||
variablesUri={variablesUri}
|
||||
value={operation?.query ?? ''}
|
||||
onChange={value => {
|
||||
updateActiveOperation({
|
||||
query: value ?? '',
|
||||
});
|
||||
}}
|
||||
language="graphql"
|
||||
theme="hive-laboratory"
|
||||
options={{
|
||||
readOnly: props.isReadOnly,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -761,7 +735,7 @@ export const Operation = (props: {
|
|||
}, [props.historyItem]);
|
||||
|
||||
return (
|
||||
<div className="bg-neutral-2 size-full">
|
||||
<div className="bg-card size-full">
|
||||
<ResizablePanelGroup direction="horizontal" className="size-full">
|
||||
<ResizablePanel defaultSize={25}>
|
||||
<Builder operation={operation} isReadOnly={isReadOnly} />
|
||||
|
|
@ -773,7 +747,7 @@ export const Operation = (props: {
|
|||
<Query operation={operation} isReadOnly={isReadOnly} />
|
||||
</ResizablePanel>
|
||||
<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">
|
||||
<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">
|
||||
|
|
@ -786,13 +760,13 @@ export const Operation = (props: {
|
|||
Extensions
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="variables">
|
||||
<TabsContent value="variables" className="overflow-hidden">
|
||||
<Variables operation={operation} isReadOnly={isReadOnly} />
|
||||
</TabsContent>
|
||||
<TabsContent value="headers">
|
||||
<TabsContent value="headers" className="overflow-hidden">
|
||||
<Headers operation={operation} isReadOnly={isReadOnly} />
|
||||
</TabsContent>
|
||||
<TabsContent value="extensions">
|
||||
<TabsContent value="extensions" className="overflow-hidden">
|
||||
<Extensions operation={operation} isReadOnly={isReadOnly} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
|
@ -813,7 +787,7 @@ export const Operation = (props: {
|
|||
<Empty className="size-full">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<HistoryIcon className="text-neutral-10 size-6" />
|
||||
<HistoryIcon className="text-muted-foreground size-6" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No history yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
|
|
@ -1,23 +1,13 @@
|
|||
import { useCallback } from 'react';
|
||||
import { HistoryIcon, PlayIcon } from 'lucide-react';
|
||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
||||
import { Editor } from '@/laboratory/components/laboratory/editor';
|
||||
import { Button } from '@/laboratory/components/ui/button';
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
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';
|
||||
import { runIsolatedLabScript } from '../../lib/preflight';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Button } from '../ui/button';
|
||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../ui/empty';
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../ui/resizable';
|
||||
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
||||
import { useLaboratory } from './context';
|
||||
import { Editor } from './editor';
|
||||
|
||||
export const Preflight = () => {
|
||||
const {
|
||||
|
|
@ -41,12 +31,10 @@ export const Preflight = () => {
|
|||
const result = await runIsolatedLabScript(
|
||||
preflight?.script ?? '',
|
||||
env ?? { variables: {} },
|
||||
(title, defaultValue, options) => {
|
||||
(placeholder, defaultValue) => {
|
||||
return new Promise(resolve => {
|
||||
openPreflightPromptModal?.({
|
||||
title,
|
||||
description: options?.description,
|
||||
placeholder: options?.placeholder,
|
||||
placeholder,
|
||||
defaultValue,
|
||||
onSubmit: value => {
|
||||
resolve(value);
|
||||
|
|
@ -65,9 +53,9 @@ export const Preflight = () => {
|
|||
|
||||
return (
|
||||
<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="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>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button variant="default" size="sm" className="h-6 rounded-sm" onClick={run}>
|
||||
|
|
@ -97,7 +85,7 @@ export const Preflight = () => {
|
|||
request: {
|
||||
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;
|
||||
plugins: {
|
||||
${plugins
|
||||
|
|
@ -253,10 +241,10 @@ export const Preflight = () => {
|
|||
</div>
|
||||
</ResizablePanel>
|
||||
<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 ? (
|
||||
<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>
|
||||
<div className="ml-auto flex items-center gap-2" />
|
||||
</div>
|
||||
|
|
@ -264,14 +252,14 @@ export const Preflight = () => {
|
|||
<div className="flex flex-col gap-1.5 whitespace-pre-wrap p-3">
|
||||
{preflight?.lastTestResult?.logs.map((log, 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
|
||||
className={cn('text-xs font-medium', {
|
||||
'text-blue-400': log.level === 'info',
|
||||
'text-green-400': log.level === 'log',
|
||||
'text-yellow-400': log.level === 'warn',
|
||||
'text-red-400': log.level === 'error',
|
||||
'text-neutral-10': log.level === 'system',
|
||||
'text-gray-400': log.level === 'system',
|
||||
})}
|
||||
>
|
||||
{log.level.toUpperCase()}
|
||||
|
|
@ -287,7 +275,7 @@ export const Preflight = () => {
|
|||
<Empty className="size-full">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<HistoryIcon className="text-neutral-10 size-6" />
|
||||
<HistoryIcon className="text-muted-foreground size-6" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No logs yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
|
|
@ -1,21 +1,9 @@
|
|||
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 { 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({
|
||||
fetch: z.object({
|
||||
|
|
@ -37,7 +25,7 @@ export const Settings = () => {
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="bg-neutral-2 size-full p-3">
|
||||
<div className="bg-card size-full p-3">
|
||||
<form
|
||||
id="settings-form"
|
||||
onSubmit={form.handleSubmit}
|
||||
|
|
@ -13,28 +13,28 @@ import {
|
|||
SettingsIcon,
|
||||
XIcon,
|
||||
} from 'lucide-react';
|
||||
import { GraphQLIcon } from '@/laboratory/components/icons';
|
||||
import { useLaboratory } from '@/laboratory/components/laboratory/context';
|
||||
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 { getOperationName, getOperationType } from '../../lib/operations.utils';
|
||||
import { LaboratoryPluginTab } from '../../lib/plugins';
|
||||
import type {
|
||||
LaboratoryTab,
|
||||
LaboratoryTabHistory,
|
||||
LaboratoryTabOperation,
|
||||
LaboratoryTabTest,
|
||||
} from '@/laboratory/lib/tabs';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
} from '../../lib/tabs';
|
||||
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: {
|
||||
item: LaboratoryTab;
|
||||
|
|
@ -213,7 +213,7 @@ export const Tab = (props: {
|
|||
}
|
||||
|
||||
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') {
|
||||
|
|
@ -239,7 +239,7 @@ export const Tab = (props: {
|
|||
return customTab.icon;
|
||||
}
|
||||
|
||||
return <FileIcon className="text-neutral-8 size-4" />;
|
||||
return <FileIcon className="text-muted-foreground size-4" />;
|
||||
}, [props.item, isError]);
|
||||
|
||||
return (
|
||||
|
|
@ -250,7 +250,7 @@ export const Tab = (props: {
|
|||
asHandle
|
||||
className={cn(
|
||||
'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',
|
||||
)}
|
||||
>
|
||||
|
|
@ -278,9 +278,8 @@ export const Tab = (props: {
|
|||
>
|
||||
<div
|
||||
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',
|
||||
props.activeTab?.id === props.item.id &&
|
||||
'border-neutral-11 bg-neutral-2 text-neutral-11',
|
||||
'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 && 'border-primary bg-card text-foreground',
|
||||
)}
|
||||
onClick={() => {
|
||||
props.setActiveTab(props.item);
|
||||
|
|
@ -297,9 +296,9 @@ export const Tab = (props: {
|
|||
{tabIcon}
|
||||
{tabName}
|
||||
{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
|
||||
className="text-neutral-10 size-3"
|
||||
className="text-muted-foreground size-3"
|
||||
onMouseDown={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
|
|
@ -310,7 +309,7 @@ export const Tab = (props: {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-neutral-5 mb-px w-px" />
|
||||
<div className="bg-border mb-px w-px" />
|
||||
</Sortable.Item>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
|
|
@ -412,7 +411,7 @@ export const Tabs = ({ className }: { className?: string }) => {
|
|||
<div
|
||||
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">
|
||||
<ScrollArea className="size-full whitespace-nowrap">
|
||||
<div className="flex items-stretch">
|
||||
|
|
@ -467,7 +466,7 @@ export const Tabs = ({ className }: { className?: string }) => {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-neutral-11 hover:text-neutral-11"
|
||||
className="text-primary hover:text-primary"
|
||||
onClick={handleAddOperation}
|
||||
>
|
||||
<CirclePlus className="size-4" />
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Children, Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
interface ItemProps {
|
||||
label: string;
|
||||
|
|
@ -36,16 +36,16 @@ export const Tabs = ({ children, suffix }: TabsProps) => {
|
|||
|
||||
return (
|
||||
<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-neutral-5 absolute bottom-0 left-0 -z-10 h-px w-full" />
|
||||
<div className="bg-background relative z-10 flex h-12 w-full items-center overflow-hidden">
|
||||
<div className="bg-border absolute bottom-0 left-0 -z-10 h-px w-full" />
|
||||
<div className="flex h-full w-max items-stretch">
|
||||
{Children.map(filteredChildren, child => (
|
||||
<Fragment key={child?.props.label}>
|
||||
<div
|
||||
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,
|
||||
},
|
||||
)}
|
||||
|
|
@ -53,7 +53,7 @@ export const Tabs = ({ children, suffix }: TabsProps) => {
|
|||
>
|
||||
{child.props.label}
|
||||
</div>
|
||||
<div className="bg-neutral-5 mb-px w-px" />
|
||||
<div className="bg-border mb-px w-px" />
|
||||
</Fragment>
|
||||
))}
|
||||
</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 { cn } from '../../lib/utils';
|
||||
import { buttonVariants } from './button';
|
||||
|
||||
function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||
|
|
@ -24,7 +24,7 @@ function AlertDialogOverlay({
|
|||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -42,7 +42,7 @@ function AlertDialogContent({
|
|||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -91,7 +91,7 @@ function AlertDialogDescription({
|
|||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn('text-neutral-10 text-sm', className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
default: 'border-transparent bg-neutral-11 text-neutral-2 [a&]:hover:bg-neutral-11/90',
|
||||
secondary: 'border-transparent bg-neutral-2 text-neutral-11 [a&]:hover:bg-neutral-2/90',
|
||||
default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
||||
destructive:
|
||||
'border-transparent bg-red-500 text-neutral-12 [a&]:hover:bg-red-900 focus-visible:ring-red-200',
|
||||
outline: 'text-neutral-11 [a&]:hover:bg-neutral-2 [a&]:hover:text-neutral-12',
|
||||
'border-transparent text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/40 bg-destructive/60',
|
||||
outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
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 { cn } from '../../lib/utils';
|
||||
import { Separator } from './separator';
|
||||
|
||||
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",
|
||||
|
|
@ -48,7 +48,7 @@ function ButtonGroupText({
|
|||
return (
|
||||
<Comp
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -66,7 +66,7 @@ function ButtonGroupSeparator({
|
|||
data-slot="button-group-separator"
|
||||
orientation={orientation}
|
||||
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,
|
||||
)}
|
||||
{...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'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -40,7 +40,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
|||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn('text-neutral-10 text-sm', className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { CheckIcon } from 'lucide-react';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -2,21 +2,15 @@
|
|||
|
||||
import { Command as CommandPrimitive } from 'cmdk';
|
||||
import { SearchIcon } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/laboratory/components/ui/dialog';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './dialog';
|
||||
|
||||
function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -47,7 +41,7 @@ function CommandDialog({
|
|||
className={cn('overflow-hidden p-0', className)}
|
||||
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}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
|
|
@ -65,7 +59,7 @@ function CommandInput({
|
|||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -102,7 +96,7 @@ function CommandGroup({
|
|||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -117,7 +111,7 @@ function CommandSeparator({
|
|||
return (
|
||||
<CommandPrimitive.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}
|
||||
/>
|
||||
);
|
||||
|
|
@ -128,7 +122,7 @@ function CommandItem({ className, ...props }: React.ComponentProps<typeof Comman
|
|||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -140,7 +134,7 @@ function CommandShortcut({ className, ...props }: React.ComponentProps<'span'>)
|
|||
return (
|
||||
<span
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState } from 'react';
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
function ContextMenu({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||
|
|
@ -43,7 +44,7 @@ function ContextMenuSubTrigger({
|
|||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -62,7 +63,7 @@ function ContextMenuSubContent({
|
|||
<ContextMenuPrimitive.SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -74,17 +75,22 @@ function ContextMenuContent({
|
|||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
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 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}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
<>
|
||||
<ContextMenuPrimitive.Portal container={container}>
|
||||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
'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}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
<div ref={setContainer} style={{ display: 'contents' }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +109,7 @@ function ContextMenuItem({
|
|||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -121,7 +127,7 @@ function ContextMenuCheckboxItem({
|
|||
<ContextMenuPrimitive.CheckboxItem
|
||||
data-slot="context-menu-checkbox-item"
|
||||
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,
|
||||
)}
|
||||
checked={checked}
|
||||
|
|
@ -146,7 +152,7 @@ function ContextMenuRadioItem({
|
|||
<ContextMenuPrimitive.RadioItem
|
||||
data-slot="context-menu-radio-item"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -172,7 +178,7 @@ function ContextMenuLabel({
|
|||
<ContextMenuPrimitive.Label
|
||||
data-slot="context-menu-label"
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
|
@ -185,7 +191,7 @@ function ContextMenuSeparator({
|
|||
return (
|
||||
<ContextMenuPrimitive.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}
|
||||
/>
|
||||
);
|
||||
|
|
@ -195,7 +201,7 @@ function ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span
|
|||
return (
|
||||
<span
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState } from 'react';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
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>) {
|
||||
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>) {
|
||||
|
|
@ -26,7 +34,7 @@ function DialogOverlay({
|
|||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -48,7 +56,7 @@ function DialogContent({
|
|||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -57,7 +65,7 @@ function DialogContent({
|
|||
{showCloseButton && (
|
||||
<DialogPrimitive.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 />
|
||||
<span className="sr-only">Close</span>
|
||||
|
|
@ -105,7 +113,7 @@ function DialogDescription({
|
|||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn('text-neutral-10 text-sm', className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState } from 'react';
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
|
|
@ -23,18 +24,22 @@ function DropdownMenuContent({
|
|||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
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-y-auto overflow-x-hidden rounded-md border p-1 shadow-md',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
<>
|
||||
<DropdownMenuPrimitive.Portal container={container}>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'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}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
<div ref={setContainer} style={{ display: 'contents' }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +62,7 @@ function DropdownMenuItem({
|
|||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -75,7 +80,7 @@ function DropdownMenuCheckboxItem({
|
|||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
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,
|
||||
)}
|
||||
checked={checked}
|
||||
|
|
@ -106,7 +111,7 @@ function DropdownMenuRadioItem({
|
|||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -145,7 +150,7 @@ function DropdownMenuSeparator({
|
|||
return (
|
||||
<DropdownMenuPrimitive.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}
|
||||
/>
|
||||
);
|
||||
|
|
@ -155,7 +160,7 @@ function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'spa
|
|||
return (
|
||||
<span
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
|
@ -178,7 +183,7 @@ function DropdownMenuSubTrigger({
|
|||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -197,7 +202,7 @@ function DropdownMenuSubContent({
|
|||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
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'>) {
|
||||
return (
|
||||
|
|
@ -30,7 +30,7 @@ const emptyMediaVariants = cva(
|
|||
variants: {
|
||||
variant: {
|
||||
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: {
|
||||
|
|
@ -69,7 +69,7 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
|||
<div
|
||||
data-slot="empty-description"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { useMemo } from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Label } from '@/laboratory/components/ui/label';
|
||||
import { Separator } from '@/laboratory/components/ui/separator';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Label } from './label';
|
||||
import { Separator } from './separator';
|
||||
|
||||
function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
|
||||
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: {
|
||||
orientation: {
|
||||
vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
|
||||
|
|
@ -105,7 +105,7 @@ function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>)
|
|||
className={cn(
|
||||
'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-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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -131,9 +131,9 @@ function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
|||
<p
|
||||
data-slot="field-description"
|
||||
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',
|
||||
'[&>a:hover]:text-neutral-11 [&>a]:underline [&>a]:underline-offset-4',
|
||||
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -161,7 +161,7 @@ function FieldSeparator({
|
|||
<Separator className="absolute inset-0 top-1/2" />
|
||||
{children && (
|
||||
<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"
|
||||
>
|
||||
{children}
|
||||
|
|
@ -209,7 +209,7 @@ function FieldError({
|
|||
<div
|
||||
role="alert"
|
||||
data-slot="field-error"
|
||||
className={cn('text-sm font-normal text-red-500', className)}
|
||||
className={cn('text-destructive text-sm font-normal', className)}
|
||||
{...props}
|
||||
>
|
||||
{content}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Button } from '@/laboratory/components/ui/button';
|
||||
import { Input } from '@/laboratory/components/ui/input';
|
||||
import { Textarea } from '@/laboratory/components/ui/textarea';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Button } from './button';
|
||||
import { Input } from './input';
|
||||
import { Textarea } from './textarea';
|
||||
|
||||
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
|
|
@ -10,7 +10,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|||
data-slot="input-group"
|
||||
role="group"
|
||||
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',
|
||||
|
||||
// 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]',
|
||||
|
||||
// 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,
|
||||
)}
|
||||
|
|
@ -33,7 +33,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|||
}
|
||||
|
||||
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: {
|
||||
align: {
|
||||
|
|
@ -57,7 +57,6 @@ function InputGroupAddon({
|
|||
...props
|
||||
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||
<div
|
||||
role="group"
|
||||
data-slot="input-group-addon"
|
||||
|
|
@ -111,7 +110,7 @@ function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
|
|||
return (
|
||||
<span
|
||||
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,
|
||||
)}
|
||||
{...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 { cn } from '../../lib/utils';
|
||||
|
||||
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { GripVerticalIcon } from 'lucide-react';
|
||||
import * as ResizablePrimitive from 'react-resizable-panels';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
function ResizablePanelGroup({
|
||||
className,
|
||||
|
|
@ -30,13 +30,13 @@ function ResizableHandle({
|
|||
<ResizablePrimitive.PanelResizeHandle
|
||||
data-slot="resizable-handle"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{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" />
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { cn } from '@/laboratory/lib/utils';
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
|
|
@ -43,7 +43,7 @@ function ScrollBar({
|
|||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-neutral-5 relative flex-1 rounded-full"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
);
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
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 { cn } from '../../lib/utils';
|
||||
|
||||
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||
|
|
@ -27,7 +28,7 @@ function SelectTrigger({
|
|||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -47,12 +48,14 @@ function SelectContent({
|
|||
align = 'center',
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
const { container } = useLaboratory();
|
||||
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Portal container={container}>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
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' &&
|
||||
'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,
|
||||
|
|
@ -81,7 +84,7 @@ function SelectLabel({ className, ...props }: React.ComponentProps<typeof Select
|
|||
return (
|
||||
<SelectPrimitive.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}
|
||||
/>
|
||||
);
|
||||
|
|
@ -96,7 +99,7 @@ function SelectItem({
|
|||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -118,7 +121,7 @@ function SelectSeparator({
|
|||
return (
|
||||
<SelectPrimitive.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}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
|
|
@ -15,7 +15,7 @@ function Separator({
|
|||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -6,14 +6,11 @@ import {
|
|||
TriangleAlertIcon,
|
||||
} from 'lucide-react';
|
||||
import { Toaster as Sonner, type ToasterProps } from 'sonner';
|
||||
import { useTheme } from '@/components/theme/theme-provider';
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps['theme']}
|
||||
theme={props.theme}
|
||||
className="group"
|
||||
icons={{
|
||||
success: <CircleCheckIcon className="size-4" />,
|
||||
|
|
@ -24,9 +21,9 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||
}}
|
||||
style={
|
||||
{
|
||||
'--normal-bg': 'var(--neutral-2)',
|
||||
'--normal-text': 'var(--neutral-11)',
|
||||
'--normal-border': 'var(--neutral-4)',
|
||||
'--normal-bg': 'var(--popover)',
|
||||
'--normal-text': 'var(--popover-foreground)',
|
||||
'--normal-border': 'var(--border)',
|
||||
'--border-radius': 'var(--radius)',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
|
|
@ -10,8 +10,6 @@ import {
|
|||
useState,
|
||||
} from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { useComposedRefs } from '@/laboratory/lib/compose-refs';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import {
|
||||
closestCenter,
|
||||
closestCorners,
|
||||
|
|
@ -49,6 +47,8 @@ import {
|
|||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { useComposedRefs } from '../../lib/compose-refs';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
const orientationConfig = {
|
||||
vertical: {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Loader2Icon } from 'lucide-react';
|
||||
import { cn } from '@/laboratory/lib/utils';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
function Spinner({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
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 { cn } from '../../lib/utils';
|
||||
|
||||
function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn('flex flex-col gap-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
<TabsPrimitive.Root data-slot="tabs" className={cn('flex flex-col', className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -16,7 +12,7 @@ function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimi
|
|||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -29,7 +25,7 @@ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPr
|
|||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
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,
|
||||
)}
|
||||
{...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 { cn } from '@/laboratory/lib/utils';
|
||||
import * as TogglePrimitive from '@radix-ui/react-toggle';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
default: 'bg-transparent',
|
||||
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: {
|
||||
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 type { LaboratoryOperation } from '@/laboratory/lib/operations';
|
||||
import type { LaboratoryTabsActions, LaboratoryTabsState } from '@/laboratory/lib/tabs';
|
||||
import type { LaboratoryOperation } from './operations';
|
||||
import type { LaboratoryTabsActions, LaboratoryTabsState } from './tabs';
|
||||
|
||||
export interface LaboratoryCollectionOperation extends LaboratoryOperation {
|
||||
id: string;
|
||||
|
|
@ -25,7 +25,6 @@ export const useEndpoint = (props: {
|
|||
onEndpointChange?: (endpoint: string | null) => void;
|
||||
defaultSchemaIntrospection?: IntrospectionQuery | null;
|
||||
}): LaboratoryEndpointState & LaboratoryEndpointActions => {
|
||||
// eslint-disable-next-line react/hook-use-state
|
||||
const [endpoint, _setEndpoint] = useState<string | null>(props.defaultEndpoint ?? null);
|
||||
const [introspection, setIntrospection] = useState<IntrospectionQuery | null>(null);
|
||||
|
||||
|
|
@ -16,7 +16,6 @@ export const useEnv = (props: {
|
|||
defaultEnv?: LaboratoryEnv | null;
|
||||
onEnvChange?: (env: LaboratoryEnv | null) => void;
|
||||
}): LaboratoryEnvState & LaboratoryEnvActions => {
|
||||
// eslint-disable-next-line react/hook-use-state
|
||||
const [env, _setEnv] = useState<LaboratoryEnv>(props.defaultEnv ?? { variables: {} });
|
||||
|
||||
const setEnv = useCallback(
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import type { LaboratoryOperation } from '@/laboratory/lib/operations';
|
||||
import type { LaboratoryPreflightLog } from '@/laboratory/lib/preflight';
|
||||
import type { LaboratoryOperation } from './operations';
|
||||
import type { LaboratoryPreflightLog } from './preflight';
|
||||
|
||||
export interface LaboratoryHistoryRequest {
|
||||
id: string;
|
||||
|
|
@ -2,16 +2,13 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
import type { GraphQLSchema } from 'graphql';
|
||||
import { createClient } from 'graphql-ws';
|
||||
import { decompressFromEncodedURIComponent } from 'lz-string';
|
||||
import {
|
||||
LaboratoryPermission,
|
||||
LaboratoryPermissions,
|
||||
} from '@/laboratory/components/laboratory/context';
|
||||
import { LaboratoryPermission, LaboratoryPermissions } from '../components/laboratory/context';
|
||||
import type {
|
||||
LaboratoryCollectionOperation,
|
||||
LaboratoryCollectionsActions,
|
||||
LaboratoryCollectionsState,
|
||||
} from '@/laboratory/lib/collections';
|
||||
import type { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from '@/laboratory/lib/env';
|
||||
} from './collections';
|
||||
import type { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from './env';
|
||||
import {
|
||||
addArgToField,
|
||||
addPathToQuery,
|
||||
|
|
@ -19,22 +16,11 @@ import {
|
|||
getOperationName,
|
||||
handleTemplate,
|
||||
removeArgFromField,
|
||||
} from '@/laboratory/lib/operations.utils';
|
||||
import {
|
||||
LaboratoryPlugin,
|
||||
LaboratoryPluginsActions,
|
||||
LaboratoryPluginsState,
|
||||
} 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';
|
||||
} from './operations.utils';
|
||||
import { LaboratoryPlugin, LaboratoryPluginsActions, LaboratoryPluginsState } from './plugins';
|
||||
import type { LaboratoryPreflightActions, LaboratoryPreflightState } from './preflight';
|
||||
import type { LaboratorySettingsActions, LaboratorySettingsState } from './settings';
|
||||
import type { LaboratoryTabOperation, LaboratoryTabsActions, LaboratoryTabsState } from './tabs';
|
||||
|
||||
export interface LaboratoryOperation {
|
||||
id: string;
|
||||
|
|
@ -100,7 +86,6 @@ export const useOperations = (
|
|||
pluginsApi?: LaboratoryPluginsState & LaboratoryPluginsActions;
|
||||
} & LaboratoryOperationsCallbacks,
|
||||
): LaboratoryOperationsState & LaboratoryOperationsActions => {
|
||||
// eslint-disable-next-line react/hook-use-state
|
||||
const [operations, _setOperations] = useState<LaboratoryOperation[]>(
|
||||
props.defaultOperations ?? [],
|
||||
);
|
||||
|
|
@ -21,7 +21,7 @@ import {
|
|||
} from 'graphql';
|
||||
import type { Maybe } from 'graphql/jsutils/Maybe';
|
||||
import { get } from 'lodash';
|
||||
import type { LaboratoryOperation } from '@/laboratory/lib/operations';
|
||||
import type { LaboratoryOperation } from './operations';
|
||||
|
||||
export function healQuery(query: string) {
|
||||
return query.replace(/\{(\s+)?\}/g, '');
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
import type { LaboratoryContextProps } from '@/laboratory/components/laboratory/context';
|
||||
import { LaboratoryTabCustom } from '@/laboratory/lib/tabs';
|
||||
import type { LaboratoryContextProps } from '../components/laboratory/context';
|
||||
import { LaboratoryTabCustom } from './tabs';
|
||||
|
||||
export interface LaboratoryPluginTab<State = Record<string, unknown>> {
|
||||
type: string;
|
||||
|
|
@ -56,7 +56,6 @@ export const usePlugins = (props: {
|
|||
defaultPluginsState?: Record<string, any>;
|
||||
onPluginsStateChange?: (state: Record<string, any>) => void;
|
||||
}): LaboratoryPluginsState & LaboratoryPluginsActions => {
|
||||
// eslint-disable-next-line react/hook-use-state
|
||||
const [pluginsState, _setPluginsState] = useState<Record<string, any>>({
|
||||
...props.plugins?.reduce(
|
||||
(acc, plugin) => {
|
||||
|
|
@ -1,14 +1,7 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
import cryptoJsSource from 'crypto-js/crypto-js.js?raw';
|
||||
import type { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from '@/laboratory/lib/env';
|
||||
import { LaboratoryPlugin } from '@/laboratory/lib/plugins';
|
||||
|
||||
export interface LaboratoryPreflightPromptField {
|
||||
title?: string;
|
||||
defaultValue?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
import type { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from './env';
|
||||
import { LaboratoryPlugin } from './plugins';
|
||||
|
||||
export interface LaboratoryPreflightLog {
|
||||
level: 'log' | 'warn' | 'error' | 'info' | 'system';
|
||||
|
|
@ -48,15 +41,7 @@ export const usePreflight = (props: {
|
|||
defaultPreflight?: LaboratoryPreflight | null;
|
||||
onPreflightChange?: (preflight: LaboratoryPreflight | null) => void;
|
||||
envApi: LaboratoryEnvState & LaboratoryEnvActions;
|
||||
openPreflightPromptModal?: (props: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
onSubmit?: (value: string | null) => void;
|
||||
}) => void;
|
||||
}): LaboratoryPreflightState & LaboratoryPreflightActions => {
|
||||
// eslint-disable-next-line react/hook-use-state
|
||||
const [preflight, _setPreflight] = useState<LaboratoryPreflight | null>(
|
||||
props.defaultPreflight ?? null,
|
||||
);
|
||||
|
|
@ -78,19 +63,7 @@ export const usePreflight = (props: {
|
|||
return runIsolatedLabScript(
|
||||
preflight.script,
|
||||
props.envApi?.env ?? { variables: {} },
|
||||
(title, defaultValue, options) => {
|
||||
return new Promise(resolve => {
|
||||
props.openPreflightPromptModal?.({
|
||||
title,
|
||||
description: options?.description,
|
||||
placeholder: options?.placeholder,
|
||||
defaultValue,
|
||||
onSubmit: value => {
|
||||
resolve(value);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
undefined,
|
||||
plugins,
|
||||
pluginsState,
|
||||
);
|
||||
|
|
@ -100,7 +73,10 @@ export const usePreflight = (props: {
|
|||
|
||||
const setLastTestResult = useCallback(
|
||||
(result: LaboratoryPreflightResult | null) => {
|
||||
_setPreflight({ ...(preflight ?? { script: '', enabled: true }), lastTestResult: result });
|
||||
_setPreflight({
|
||||
...(preflight ?? { script: '', enabled: true }),
|
||||
lastTestResult: result,
|
||||
});
|
||||
props.onPreflightChange?.({
|
||||
...(preflight ?? { script: '', enabled: true }),
|
||||
lastTestResult: result,
|
||||
|
|
@ -120,11 +96,7 @@ export const usePreflight = (props: {
|
|||
export async function runIsolatedLabScript(
|
||||
script: string,
|
||||
env: LaboratoryEnv,
|
||||
prompt?: (
|
||||
title: string,
|
||||
defaultValue: string,
|
||||
options?: { placeholder?: string; description?: string },
|
||||
) => Promise<string | null>,
|
||||
prompt?: (placeholder: string, defaultValue: string) => Promise<string | null>,
|
||||
plugins: LaboratoryPlugin[] = [],
|
||||
pluginsState: Record<string, any> = {},
|
||||
): Promise<LaboratoryPreflightResult> {
|
||||
|
|
@ -184,10 +156,10 @@ export async function runIsolatedLabScript(
|
|||
request: {
|
||||
headers: new Headers()
|
||||
},
|
||||
prompt: (title, defaultValue, options) => {
|
||||
prompt: (placeholder, defaultValue) => {
|
||||
return new Promise((resolve) => {
|
||||
promptResolve = resolve;
|
||||
self.postMessage({ type: 'prompt', title, defaultValue, options: options ?? {} });
|
||||
self.postMessage({ type: 'prompt', placeholder, defaultValue });
|
||||
});
|
||||
},
|
||||
plugins: {
|
||||
|
|
@ -256,13 +228,29 @@ export async function runIsolatedLabScript(
|
|||
}
|
||||
} else if (data.type === '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') {
|
||||
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') {
|
||||
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') {
|
||||
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') {
|
||||
headers[data.name] = data.value;
|
||||
|
|
@ -273,7 +261,7 @@ export async function runIsolatedLabScript(
|
|||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
} 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 });
|
||||
});
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@ export const useSettings = (props: {
|
|||
defaultSettings?: LaboratorySettings | null;
|
||||
onSettingsChange?: (settings: LaboratorySettings | null) => void;
|
||||
}): LaboratorySettingsState & LaboratorySettingsActions => {
|
||||
// eslint-disable-next-line react/hook-use-state
|
||||
const [settings, _setSettings] = useState<LaboratorySettings>(
|
||||
props.defaultSettings ?? {
|
||||
fetch: {
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
import type { LaboratoryEnv } from '@/laboratory/lib/env';
|
||||
import type { LaboratoryHistoryRequest } from '@/laboratory/lib/history';
|
||||
import type { LaboratoryOperation } from '@/laboratory/lib/operations';
|
||||
import type { LaboratoryPreflight } from '@/laboratory/lib/preflight';
|
||||
import type { LaboratoryTest } from '@/laboratory/lib/tests';
|
||||
import type { LaboratoryEnv } from './env';
|
||||
import type { LaboratoryHistoryRequest } from './history';
|
||||
import type { LaboratoryOperation } from './operations';
|
||||
import type { LaboratoryPreflight } from './preflight';
|
||||
import type { LaboratoryTest } from './tests';
|
||||
|
||||
export interface LaboratoryTabOperation {
|
||||
id: string;
|
||||
|
|
@ -88,10 +88,8 @@ export const useTabs = (props: {
|
|||
onTabsChange?: (tabs: LaboratoryTab[]) => void;
|
||||
onActiveTabIdChange?: (tabId: string | null) => void;
|
||||
}): LaboratoryTabsState & LaboratoryTabsActions => {
|
||||
// eslint-disable-next-line react/hook-use-state
|
||||
const [tabs, _setTabs] = useState<LaboratoryTab[]>(props.defaultTabs ?? []);
|
||||
|
||||
// eslint-disable-next-line react/hook-use-state
|
||||
const [activeTab, _setActiveTab] = useState<LaboratoryTab | null>(
|
||||
props.defaultTabs?.find(t => t.id === props.defaultActiveTabId) ??
|
||||
props.defaultTabs?.[0] ??
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { LaboratoryOperation } from '@/laboratory/lib/operations';
|
||||
import type { LaboratoryOperation } from './operations';
|
||||
|
||||
export interface LaboratoryTestTaskBase {
|
||||
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",
|
||||
"rootDir": "../.."
|
||||
},
|
||||
"files": ["src/index.ts"]
|
||||
"files": ["src/index.ts"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
"module": "esnext",
|
||||
"rootDir": "../../.."
|
||||
},
|
||||
"files": ["src/index.ts"]
|
||||
"files": ["src/index.ts"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
"module": "esnext",
|
||||
"rootDir": "../.."
|
||||
},
|
||||
"files": ["src/index.ts"]
|
||||
"files": ["src/index.ts"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
"module": "esnext",
|
||||
"rootDir": "../.."
|
||||
},
|
||||
"files": ["src/index.ts"]
|
||||
"files": ["src/index.ts"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,5 +7,6 @@
|
|||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"files": ["src/index.ts"]
|
||||
"files": ["src/index.ts"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
"module": "esnext",
|
||||
"rootDir": "../.."
|
||||
},
|
||||
"files": ["src/index.ts"]
|
||||
"files": ["src/index.ts"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
"module": "esnext",
|
||||
"rootDir": "../.."
|
||||
},
|
||||
"files": ["src/index.ts"]
|
||||
"files": ["src/index.ts"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
"module": "esnext",
|
||||
"rootDir": "../.."
|
||||
},
|
||||
"files": ["src/index.ts"]
|
||||
"files": ["src/index.ts"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
"module": "esnext",
|
||||
"rootDir": "../.."
|
||||
},
|
||||
"files": ["src/index.ts"]
|
||||
"files": ["src/index.ts"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
"@graphiql/react": "1.0.0-alpha.4",
|
||||
"@graphiql/toolkit": "0.9.1",
|
||||
"@graphql-codegen/client-preset-swc-plugin": "0.2.0",
|
||||
"@graphql-hive/laboratory": "workspace:*",
|
||||
"@graphql-inspector/core": "7.1.2",
|
||||
"@graphql-inspector/patch": "0.1.3",
|
||||
"@graphql-tools/mock": "9.0.25",
|
||||
|
|
|
|||
|
|
@ -293,10 +293,48 @@
|
|||
--chart-grid: 0 0% 45%;
|
||||
}
|
||||
|
||||
.hive-laboratory {
|
||||
--primary: 40 89% 60%;
|
||||
--background: 223 70% 4%;
|
||||
--card: 220 21.43% 5.49%;
|
||||
:root {
|
||||
--hive-laboratory-radius: var(--radius);
|
||||
--hive-laboratory-background: var(--neutral-2);
|
||||
--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