初始化

This commit is contained in:
2026-02-04 12:18:35 +08:00
parent 85db3f71d4
commit b496d7c7c3
240 changed files with 42693 additions and 0 deletions

43
GradientTextPlugin/.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
.kotlin
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

View File

@@ -0,0 +1,45 @@
plugins {
id("java")
id("org.jetbrains.kotlin.jvm") version "1.9.22"
id("org.jetbrains.intellij") version "1.16.1"
}
group = "com.gradientcode.plugin"
version = "1.0.0"
repositories {
maven { url = uri("https://maven.aliyun.com/repository/public/") }
maven { url = uri("https://maven.aliyun.com/repository/central/") }
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin/") }
maven { url = uri("https://repo.maven.apache.org/maven2/") }
maven { url = uri("https://plugins.gradle.org/m2/") }
mavenCentral()
}
intellij {
version.set("2025.3")
type.set("IC")
plugins.set(listOf(
"com.intellij.java",
"org.jetbrains.kotlin",
"PythonCore:233.14015.0",
"JavaScript"
))
}
tasks {
withType<JavaCompile> {
sourceCompatibility = "17"
targetCompatibility = "17"
}
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = "17"
}
patchPluginXml {
sinceBuild.set("233")
untilBuild.set("244.*")
}
}

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Mon Jan 12 11:06:37 CST 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
GradientTextPlugin/gradlew vendored Normal file
View File

@@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
GradientTextPlugin/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1 @@
rootProject.name = "GradientTextPlugin"

View File

@@ -0,0 +1,22 @@
package com.GradientTextPlugin;
/**
*
* @author binghuai
* @date 2026/1/12 11:06
* @Description
*/// TIP 要<b>运行</b>代码,请按 <shortcut actionId="Run"/> 或
// 点击装订区域中的 <icon src="AllIcons.Actions.Execute"/> 图标。
public class Main {
public static void main(String[] args) {
// TIP 当文本光标位于高亮显示的文本处时按 <shortcut actionId="ShowIntentionActions"/>
// 查看 IntelliJ IDEA 建议如何修正。
System.out.printf("Hello and welcome!");
for (int i = 1; i <= 5; i++) {
// TIP 按 <shortcut actionId="Debug"/> 开始调试代码。我们已经设置了一个 <icon src="AllIcons.Debugger.Db_set_breakpoint"/> 断点
// 但您始终可以通过按 <shortcut actionId="ToggleLineBreakpoint"/> 添加更多断点。
System.out.println("i = " + i);
}
}
}

41
jsonhero-web/.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: CI
on:
push:
branches: [main]
paths:
- ".github/workflows/main.yml"
- "app/**/*.ts"
- "app/**/*.tsx"
- "public/*"
- "styles/*"
- "worker/*"
- "tests/*"
- "package.json"
- "package-lock.json"
- "remix.config.js"
- "tsconfig.json"
- "wrangler.toml"
- "remix.env.d.ts"
- "tailwind.config.js"
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm test
- run: npm run build
- name: Publish app
uses: cloudflare/wrangler-action@1.3.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
environment: "production"

14
jsonhero-web/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
node_modules
/.cache
/build
/public/build
.env
/app/tailwind.css
/jsonDocs
.DS_Store
/dist
.mf
/meta.json
/stats.html
public/entry.worker.js

40
jsonhero-web/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,40 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost with document",
"url": "http://localhost:8787",
"webRoot": "${workspaceFolder}/app"
},
{
"name": "Debug Jest All Tests",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/node_modules/.bin/jest",
"--runInBand"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"name": "Debug Jest Test File",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/node_modules/.bin/jest",
"--runInBand"
],
"args": ["${fileBasename}", "--no-cache"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}

13
jsonhero-web/.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "typescript",
"tsconfig": "tsconfig.json",
"option": "watch",
"problemMatcher": ["$tsc-watch"],
"group": "build",
"label": "tsc: watch - tsconfig.json"
}
]
}

View File

@@ -0,0 +1,41 @@
## ⚡️ JSON Hero Contributing Guide
First of all, thanks for considering contributing to this project! If you have any questions please don't hesitate to reach out to [eric@jsonhero.io](mailto:eric@jsonhero.io) or join us on [Discord](https://discord.gg/JtBAxBr2m3).
JSON Hero is a Typescript React application built with [remix.run](https://remix.run), with support for deploying to Cloudflare workers.
To get started with contributing, please read our [Development guide](https://github.com/triggerdotdev/jsonhero-web/blob/main/DEVELOPMENT.md) first to get JSON Hero running locally.
### Running tests
Although there is less test-coverage for JSON Hero than there should be, tests should still be run to ensure builds have not been broken:
```bash
npm test
```
You can also run tests in "watch" mode:
```bash
npm run test:watch
```
### Making changes
Please make any changes to your forked repository in a branch other than `main`. If you are working on a bug fix, please use the `bug/` prefix for your branch name. If you are working on a feature, please use `features/`. If you are working on a specific issue please name the branch `issue-<issue number>`
Make sure to run the `npm lint` command to ensure there are no Typescript compile-time errors.
### Pull Requests
Please open a Pull Request against the `main` branch in the `triggerdotdev/jsonhero-web` repository. We will aim to address all newly opened PRs by the following Friday. If you haven't opened a Pull Request before, please check out GitHub's [Pull Request documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests)
### Other JSON Hero projects
If you'd like to contribute to the [VSCode extension](https://marketplace.visualstudio.com/items?itemName=JSONHero.jsonhero-vscode), please see the [triggerdotdev/vscode-extension](https://github.com/triggerdotdev/vscode-extension) repo.
For issues related to the JSON Schema inference, please check out [triggerdotdev/schema-infer](https://github.com/triggerdotdev/schema-infer).
The "Smart Preview" feature is in-part powered by the [@jsonhero/json-infer-types](https://github.com/triggerdotdev/json-infer-types) project.
If it's related to the Search functionality, please see the [triggerdotdev/fuzzy-json-search](https://github.com/triggerdotdev/fuzzy-json-search) repo.

View File

@@ -0,0 +1,83 @@
## 👩🏽‍💻 JSON Hero Local Development Guide
Welcome to JSON Hero development and thanks for being here! If you'd like to run JSON Hero locally, please use the following guide to get started. If you have any issues with this guide please feel free to email me at [eric@jsonhero.io](mailto:eric@jsonhero.io) or come leave a message in our open [Discord Channel](https://discord.gg/JtBAxBr2m3).
For more information about contributing to JSON Hero please see the [Contributing doc](https://github.com/triggerdotdev/jsonhero-web/blob/main/CONTRIBUTING.md).
### Install dependencies
Before you can run JSON Hero locally, you will need to install the following dependencies on your machine:
#### Git
You most likely already have git installed on your machine, but if not, you can install it from the [Git website](https://git-scm.com).
#### Node.js 16
Even though JSON Hero runs on [Cloudflare Workers](https://workers.cloudflare.com), which isn't a Node.js environment, you will still need Node.js 16 to run it locally. The recommended way to install Node.js is to download a pre-built package from the [Node.js website](https://nodejs.org/en/)
#### NPM
If you install Node.js through the above link, you should also have NPM automatically installed as well. To make sure, run the following command in your preferred Terminal:
```bash
npm ---version
```
### Fork JSON Hero on GitHub (optional)
To contribute code to JSON Hero, you should first create a fork of the [jsonhero-web](https://github.com/triggerdotdev/jsonhero-web) repository on GitHub. Follow [these instructions](https://docs.github.com/en/get-started/quickstart/fork-a-repo) on repository forking.
### Clone the repo
In your terminal, issue the following command to clone the repository to your local machine:
```bash
git clone https://github.com/triggerdotdev/jsonhero-web.git
```
Or if you've forked the repository:
```bash
git clone https://github.com/<github username>/jsonhero-web.git
```
Then `cd` into the repository:
```bash
cd jsonhero-web
```
### Prepare the repo
First, install npm dependencies:
```bash
npm install
```
Run the following command to create the `.env` file with a new `SESSION_SECRET` environment variable:
```bash
echo "SESSION_SECRET=$(openssl rand -hex 32)" > .env
```
Then, run `npm run build` or `npm run dev` to build.
Start the development server:
```bash
npm start
```
You should now be able to access your local JSON Hero server on [localhost:8787](http://localhost:8787)
> **Note** JSON documents created locally are not persisted across server restarts
### Previewing URLs
We currently use [OpenGraph Ninja](https://opengraph.ninja/) to power some of the Preview URL functionality.
### Deploying to Cloudflare
_Coming Soon_

12
jsonhero-web/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
# Builder
FROM node:16.17.0 as builder
WORKDIR /src
COPY . /src
# App
RUN cd /src
RUN npm install
RUN echo "SESSION_SECRET=abc123" > .env
RUN npm run build
CMD npm start

201
jsonhero-web/LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

159
jsonhero-web/README.md Normal file
View File

@@ -0,0 +1,159 @@
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/4a157bda-2a99-4ac3-6bc7-be08b4a46600/public">
<source media="(prefers-color-scheme: light)" srcset="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/31447544-b16f-49dd-c206-74b1802c6700/public">
<img width=200 alt="Trigger.dev logo" src="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/4a157bda-2a99-4ac3-6bc7-be08b4a46600/public">
</picture>
</div>
</br>
<p align="center">
<a href="https://console.algora.io/org/triggerdotdev/bounties?status=open"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Ftriggerdotdev%2Fbounties%3Fstatus%3Dopen" alt="Open Bounties" /></a>
<a href="https://console.algora.io/org/triggerdotdev/bounties?status=completed"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Ftriggerdotdev%2Fbounties%3Fstatus%3Dcompleted" alt="Rewarded Bounties" /></a>
</p>
# Brought to you by Trigger.dev
JSON Hero was created and is maintained by the team behind [Trigger.dev](https://trigger.dev). With Trigger.dev you can trigger workflows from APIs, on a schedule, or on demand. We make API calls easy with authentication handled for you, and you can add durable delays that survive server restarts.
# JSON Hero
JSON Hero makes reading and understand JSON files easy by giving you a clean and beautiful UI packed with extra features.
- View JSON any way you'd like: Column View, Tree View, Editor View, and more.
- Automatically infers the contents of strings and provides useful previews
- Creates an inferred JSON Schema that could be used to validate your JSON
- Quickly scan related values to check for edge cases
- Search your JSON files (both keys and values)
- Keyboard accessible
- Easily sharable URLs with path support
![JSON Hero Screenshot](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/0f5735b3-2421-470b-244c-7047fd77f700/public)
## Features
### Send to JSON Hero
Send your JSON to JSON Hero in a variety of ways
- Head to [jsonhero.io](https://jsonhero.io) and Drag and Drop a JSON file, or paste JSON or a JSON url in the provided form
- Include a Base64 encoded string of a JSON payload: [jsonhero.io/new?j=eyAiZm9vIjogImJhciIgfQ==](https://jsonhero.io/new?j=eyAiZm9vIjogImJhciIgfQ==)
- Include a JSON URL to the `new` endpoint: [jsonhero.io/new?url=https://jsonplaceholder.typicode.com/todos/1](https://jsonhero.io/new?url=https://jsonplaceholder.typicode.com/todos/1)
- Install the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=JSONHero.jsonhero-vscode) and open JSON from VS Code
- Raycast user? Check out our extension [here](https://www.raycast.com/maverickdotdev/open-in-json-hero)
- Use the unofficial API:
- Make a `POST` request to `jsonhero.io/api/create.json` with the following JSON body:
```json
{
"title": "test 123",
"content": { "foo": "bar" },
"readOnly": false, // this is optional, will make it so the document title cannot be edited or document cannot be deleted
"ttl": 3600 // this will expire the document after 3600 seconds, also optional
}
```
The JSON response will be the following:
```json
{
"id": "YKKduNySH7Ub",
"title": "test 123",
"location": "https://jsonhero.io/j/YKKduNySH7Ub"
}
```
### Column view
Inspired by macOS Finder, Column View is a new way to browse a JSON document.
![JSON Hero Column View](https://raw.githubusercontent.com/triggerdotdev/documentation-hosting/main/images/features-columnview.gif)
It has all the features you'd expect: Keyboard navigation, Path bar, history.
It also has a nifty feature that allows you to "hold" a descendent selected and travel up through the hierarchy, and then move between siblings and view the different values found at that path. It's hard to describe, but here is an animation to help demonstrate:
![Column View - Traverse with Context](https://raw.githubusercontent.com/triggerdotdev/documentation-hosting/main/images/features-traversewithcontext.gif)
As you can see, holding the `Option` (or `Alt` key on Windows) while moving to a parent keeps the part of the document selected and shows it in context of it's surrounding JSON. Then you can traverse between items in an array and compare the values of the selection across deep hierarchy cahnges.
### Editor view
View your entire JSON document in an editor, but keep the nice previews and related values you get from the sidebar as you move around the document:
![Editor view](https://raw.githubusercontent.com/triggerdotdev/documentation-hosting/main/images/features-editorview.gif)
### Tree view
Use a traditional tree view to traverse your JSON document, with collapsible sections and keyboard shortcuts. All while keeping the nice previews:
![Tree view](https://raw.githubusercontent.com/triggerdotdev/documentation-hosting/main/images/features-treeview.gif)
### Search
Quickly open a search panel and fuzzy search your entire JSON file in milliseconds. Searches through key names, key paths, values, and even pretty formatted values (e.g. Searching for `"Dec"` will find datetime strings in the month of December.)
![Search](https://raw.githubusercontent.com/triggerdotdev/documentation-hosting/main/images/features-search.gif)
### Content Previews
JSON Hero automatically infers the content of strings and provides useful previews and properties of the value you've selected. It's "Show Don't Tell" for JSON:
#### Dates and Times
![Preview colors](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/43f2c081-c09b-47db-cb10-8f15ee6a1a00/public)
#### Image URLs
![Preview colors](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/8a743bd5-a065-4f7f-1262-585c39c10100/public)
#### Website URLs
![Preview websites](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/cd7f2d28-2c8d-4b37-696d-e898937c3d00/public)
#### Tweet URLS
![Preview tweets](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/8455e9d6-1d3e-451e-a032-f3259204ef00/public)
#### JSON URLs
![Preview JSON](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/13743860-3d9c-4cac-dde9-881fba7eba00/public)
#### Colors
![Preview colors](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/22e37599-c2bd-4abd-79f2-466241d17b00/public)
### Related Values
Easily see all the related values across your entire JSON document for a specific field, including any `undefined` or `null` values.
![Editor view](https://raw.githubusercontent.com/triggerdotdev/documentation-hosting/main/images/features-relatedvalues.gif)
<!-- TODO -->
## Bugs and Feature Requests
Have a bug or a feature request? Feel free to [open a new issue](https://github.com/triggerdotdev/jsonhero-web/issues).
You can also join our [Discord channel](https://discord.gg/JtBAxBr2m3) to hang out and discuss anything you'd like.
## Developing
To run locally, first clone the repo and install the dependencies:
```bash
git clone https://github.com/triggerdotdev/jsonhero-web.git
cd jsonhero-web
npm install
```
Then, create a file at the root of the repo called `.env` and set the `SESSION_SECRET` value:
```
SESSION_SECRET=abc123
```
Then, run `npm run build` or `npm run dev` to build.
Now, run `npm start` and open your browser to `http://localhost:8787`

View File

@@ -0,0 +1,40 @@
## Deploying to Cloudflare
### Install and login to wrangler
```bash
npm install -g wrangler
wrangler login
```
### Create service
Go to workers tab from your [cloudflare profile](https://dash.cloudflare.com/profile) and create a new worker. Use HTTP Handler as service type. The name of worker must match the `name` field in `wrangler.toml`.
### Setup wrangler.toml
Edit the following variables in `wrangler.toml` and `wrangler.toml.dev`:
- `account_id`: Get account id by using
```bash
wrangler whoami
```
- `kv_namespaces`: Run the following comands to create a new KV namespace.
```bash
wrangler kv:namespace create DOCUMENTS # gives namespace id
wrangler kv:namespace create DOCUMENTS --preview # gives preview id for namespace
```
Replace current entry for `kv_namespaces` as:
```toml
kv_namespaces = [
{ binding = "DOCUMENTS", id = <YOUR_ID>, preview_id = <YOUR_PREVIEW_ID> }
]
```
### Configure Environment Variables
Set `SESSION_SECRET` environment for worker.
```bash
wrangler secret put SESSION_SECRET
```
Optionally set other secrets listed at the end of `wrangler.toml`.
### Publish worker
```bash
wrangler publish
```

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="#64748B">
<path d="M9 2a2 2 0 00-2 2v8a2 2 0 002 2h6a2 2 0 002-2V6.414A2 2 0 0016.414 5L14 2.586A2 2 0 0012.586 2H9z" />
<path d="M3 8a2 2 0 012-2v10h8a2 2 0 01-2 2H5a2 2 0 01-2-2V8z" />
</svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="#94A3B8">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 336 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.7071 5.29289C17.0976 5.68342 17.0976 6.31658 16.7071 6.70711L8.70711 14.7071C8.31658 15.0976 7.68342 15.0976 7.29289 14.7071L3.29289 10.7071C2.90237 10.3166 2.90237 9.68342 3.29289 9.29289C3.68342 8.90237 4.31658 8.90237 4.70711 9.29289L8 12.5858L15.2929 5.29289C15.6834 4.90237 16.3166 4.90237 16.7071 5.29289Z" fill="#64748B"/>
</svg>

After

Width:  |  Height:  |  Size: 486 B

9
jsonhero-web/app/bindings.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
export {};
declare global {
const DOCUMENTS: KVNamespace;
const SESSION_SECRET: string;
const GRAPH_JSON_API_KEY: string;
const GRAPH_JSON_COLLECTION: string;
const APIHERO_PROJECT_KEY: string;
}

View File

@@ -0,0 +1,30 @@
import { useEffect, useRef } from "react";
import { useOnScreen } from "~/hooks/useOnScreen";
export function AutoplayVideo({ src }: { src: string }) {
const elementRef = useRef<HTMLVideoElement>(null);
const isOnScreen = useOnScreen(elementRef);
useEffect(() => {
if (elementRef.current == null) return;
elementRef.current.muted = true;
elementRef.current.playsInline = true;
if (isOnScreen) {
elementRef.current.play();
} else {
elementRef.current.pause();
}
}, [isOnScreen]);
return (
<video
src={src}
ref={elementRef}
loop={true}
muted={true}
autoPlay={false}
/>
);
}

View File

@@ -0,0 +1,13 @@
import { memo } from "react";
function BlankColumnElement() {
return (
<div
className={
"column flex-none border-r-[1px] border-slate-300 w-80 transition dark:border-slate-600"
}
></div>
);
}
export const BlankColumn = memo(BlankColumnElement);

View File

@@ -0,0 +1,122 @@
import { json as jsonLang } from "@codemirror/lang-json";
import {
EditorView,
TransactionSpec,
useCodeMirror,
ViewUpdate,
} from "@uiw/react-codemirror";
import { useRef, useEffect } from "react";
import { useJsonDoc } from "~/hooks/useJsonDoc";
import { getEditorSetup } from "~/utilities/codeMirrorSetup";
import { darkTheme, lightTheme } from "~/utilities/codeMirrorTheme";
import { useTheme } from "./ThemeProvider";
import { useHotkeys } from "react-hotkeys-hook";
export type CodeEditorProps = {
content: string;
language?: "json";
readOnly?: boolean;
onChange?: (value: string) => void;
onUpdate?: (update: ViewUpdate) => void;
selection?: { start: number; end: number };
};
const languages = {
json: jsonLang,
};
type CodeEditorDefaultProps = Required<
Omit<CodeEditorProps, "content" | "onChange" | "onUpdate">
>;
const defaultProps: CodeEditorDefaultProps = {
language: "json",
readOnly: true,
selection: { start: 0, end: 0 },
};
export function CodeEditor(opts: CodeEditorProps) {
const { content, language, readOnly, onChange, onUpdate, selection } = {
...defaultProps,
...opts,
};
const [theme] = useTheme();
const extensions = getEditorSetup();
const languageExtension = languages[language];
extensions.push(languageExtension());
const editor = useRef(null);
const { setContainer, view, state } = useCodeMirror({
container: editor.current,
extensions,
editable: !readOnly,
contentEditable: !readOnly,
value: content,
autoFocus: false,
theme: theme === "light" ? lightTheme() : darkTheme(),
indentWithTab: false,
basicSetup: false,
onChange,
onUpdate,
});
useEffect(() => {
if (editor.current) {
setContainer(editor.current);
}
}, [editor.current]);
const setSelectionRef = useRef(false);
useEffect(() => {
if (setSelectionRef.current) {
return;
}
if (view) {
setSelectionRef.current = true;
const selectionStart = selection?.start ?? defaultProps.selection.start;
const selectionEnd = selection?.end ?? defaultProps.selection.end;
const lineNumber = state?.doc.lineAt(selectionStart).number;
const transactionSpec: TransactionSpec = {
selection: { anchor: selectionStart, head: selectionEnd },
effects: EditorView.scrollIntoView(selectionStart, {
y: "start",
yMargin: 100,
}),
};
view.dispatch(transactionSpec);
}
}, [selection, view, setSelectionRef.current]);
const { minimal } = useJsonDoc();
useHotkeys(
"ctrl+a,meta+a,command+a",
(e) => {
e.preventDefault();
view?.dispatch({ selection: { anchor: 0, head: state?.doc.length } });
},
[view, state]
);
return (
<div>
<div
className={`${
minimal ? "h-jsonViewerHeightMinimal" : "h-jsonViewerHeight"
} overflow-y-auto no-scrollbar`}
ref={editor}
/>
</div>
);
}

View File

@@ -0,0 +1,51 @@
import { json as jsonLang } from "@codemirror/lang-json";
import { useCodeMirror } from "@uiw/react-codemirror";
import { useRef, useEffect } from "react";
import { getViewerSetup } from "~/utilities/codeMirrorSetup";
import { darkTheme, lightTheme } from "~/utilities/codeMirrorTheme";
import { useTheme } from "./ThemeProvider";
import { useHotkeys } from "react-hotkeys-hook";
export function CodeViewer({ code, lang }: { code: string; lang?: "json" }) {
const editor = useRef(null);
const extensions = getViewerSetup();
if (!lang || lang === "json") {
extensions.push(jsonLang());
}
const [theme] = useTheme();
const { setContainer, view, state } = useCodeMirror({
container: editor.current,
extensions,
value: code,
editable: false,
contentEditable: false,
autoFocus: false,
basicSetup: false,
theme: theme === "light" ? lightTheme() : darkTheme(),
});
useEffect(() => {
if (editor.current) {
setContainer(editor.current);
}
}, [editor.current]);
useHotkeys(
"ctrl+a,meta+a,command+a",
(e) => {
e.preventDefault();
view?.dispatch({ selection: { anchor: 0, head: state?.doc.length } });
},
[view, state]
);
return (
<div>
<div ref={editor} />
</div>
);
}

View File

@@ -0,0 +1,43 @@
import { Title } from "./Primitives/Title";
import { colorForItemAtPath } from "~/utilities/colors";
import { IconComponent } from "~/useColumnView";
import { useJson } from "../hooks/useJson";
import { memo, useMemo } from "react";
import { useJsonDoc } from "~/hooks/useJsonDoc";
export type ColumnProps = {
id: string;
title: string;
icon?: IconComponent;
hasHighlightedElement: boolean;
children: React.ReactNode;
};
function ColumnElement(column: ColumnProps) {
const { id, title, children } = column;
const [json] = useJson();
const { minimal } = useJsonDoc();
const iconColor = useMemo(() => colorForItemAtPath(id, json), [id, json]);
return (
<div
className={
"column flex-none border-r-[1px] border-slate-300 w-80 transition dark:border-slate-600"
}
>
<div className="flex items-center text-slate-800 bg-slate-50 mb-[3px] p-2 pb-0 transition dark:bg-slate-900 dark:text-slate-300">
{column.icon && <column.icon className="h-6 w-6 mr-1" />}
<Title className="text-ellipsis overflow-hidden">{title}</Title>
</div>
<div
className={`overflow-y-auto ${
minimal ? "h-viewerHeightMinimal" : "h-viewerHeight"
} no-scrollbar`}
>
{children}
</div>
</div>
);
}
export const Column = memo(ColumnElement);

View File

@@ -0,0 +1,93 @@
import { ChevronRightIcon } from "@heroicons/react/outline";
import { Mono } from "./Primitives/Mono";
import { memo, useEffect, useMemo, useRef } from "react";
import { ColumnViewNode } from "~/useColumnView";
import { colorForItemAtPath } from "~/utilities/colors";
import { Body } from "./Primitives/Body";
export type ColumnItemProps = {
item: ColumnViewNode;
json: unknown;
isSelected: boolean;
isHighlighted: boolean;
onClick?: (id: string) => void;
};
function ColumnItemElement({
item,
json,
isSelected,
isHighlighted,
onClick,
}: ColumnItemProps) {
const htmlElement = useRef<HTMLDivElement>(null);
const showArrow = item.children.length > 0;
const stateStyle = useMemo<string>(() => {
if (isHighlighted) {
return "bg-slate-300 text-slate-700 hover:bg-slate-400 hover:bg-opacity-60 transition duration-75 ease-out dark:bg-white dark:bg-opacity-[15%] dark:text-slate-100";
}
if (isSelected) {
return "bg-slate-200 hover:bg-slate-300 transition duration-75 ease-out dark:bg-white dark:bg-opacity-[5%] dark:hover:bg-white dark:hover:bg-opacity-[10%] dark:text-slate-200";
}
return "hover:bg-slate-100 transition duration-75 ease-out dark:hover:bg-white dark:hover:bg-opacity-[5%] dark:text-slate-400";
}, [isSelected, isHighlighted]);
const iconColor = useMemo<string>(
() => colorForItemAtPath(item.id, json),
[item.id, json]
);
useEffect(() => {
if (isSelected || isHighlighted) {
htmlElement.current?.scrollIntoView({
block: "nearest",
inline: "center",
});
}
}, [isSelected, isHighlighted]);
return (
<div
className={`flex h-9 items-center justify-items-stretch mx-1 px-1 py-1 my-1 rounded-sm ${stateStyle}`}
onClick={() => onClick && onClick(item.id)}
ref={htmlElement}
>
<div className="w-4 flex-none flex-col justify-items-center">
{item.icon && (
<item.icon
className={`h-5 w-5 ${
isSelected && isHighlighted
? "text-slate-900 dark:text-slate-300"
: "text-slate-500"
}`}
/>
)}
</div>
<div className="flex flex-grow flex-shrink items-baseline justify-between truncate">
<Body className="flex-grow flex-shrink-0 pl-3 pr-2 ">{item.title}</Body>
{item.subtitle && (
<Mono
className={`truncate pr-1 transition duration-75 ${
isHighlighted
? "text-gray-500 dark:text-slate-100"
: "text-gray-400 dark:text-gray-500"
}`}
>
{item.subtitle}
</Mono>
)}
</div>
{showArrow && (
<ChevronRightIcon className="flex-none w-4 h-4 text-gray-400" />
)}
</div>
);
}
export const ColumnItem = memo(ColumnItemElement);

View File

@@ -0,0 +1,61 @@
import { JSONHeroPath } from "@jsonhero/path";
import { memo, useMemo } from "react";
import { useJson } from "~/hooks/useJson";
import {
useJsonColumnViewAPI,
useJsonColumnViewState,
} from "~/hooks/useJsonColumnView";
import { ColumnDefinition } from "~/useColumnView";
import { BlankColumn } from "./BlankColumn";
import { Column } from "./Column";
import { ColumnItem } from "./ColumnItem";
function ColumnsElement({ columns }: { columns: ColumnDefinition[] }) {
const [json] = useJson();
const { selectedPath, highlightedPath, highlightedNodeId } =
useJsonColumnViewState();
const { goToNodeId } = useJsonColumnViewAPI();
const highlightedItemIsValue = useMemo<boolean>(() => {
if (highlightedNodeId == null) {
return false;
}
const path = new JSONHeroPath(highlightedNodeId);
let item = path.first(json);
return typeof item !== "object";
}, [highlightedPath, json]);
return (
<div className="columns flex flex-grow overflow-x-auto focus:outline-none no-scrollbar">
{columns.map((column) => {
return (
<Column
key={column.id}
id={column.id}
title={column.title}
icon={column.icon}
hasHighlightedElement={
highlightedPath[highlightedPath.length - 2] === column.id
}
>
{column.items.map((item) => (
<ColumnItem
key={item.id}
item={item}
json={json}
isSelected={selectedPath.includes(item.id)}
isHighlighted={
highlightedPath[highlightedPath.length - 1] === item.id
}
onClick={(id) => goToNodeId(id, "columnView")}
/>
))}
</Column>
);
})}
{highlightedItemIsValue ? <BlankColumn /> : null}
</div>
);
}
export const Columns = memo(ColumnsElement);

View File

@@ -0,0 +1,74 @@
import { inferType } from "@jsonhero/json-infer-types";
import { JSONHeroPath } from "@jsonhero/path";
import { useJson } from "~/hooks/useJson";
import { useJsonColumnViewState } from "~/hooks/useJsonColumnView";
import { pathToDescendant } from "~/utilities/jsonColumnView";
import { JsonPreview } from "./JsonPreview";
import { JsonSchemaViewer } from "./JsonSchemaViewer";
import { TabContent, Tabs } from "./UI/Tabs";
const tabs = [
{ value: "json", label: "JSON" },
{ value: "schema", label: "Schema" },
];
export function ContainerInfo() {
const { selectedNodeId, highlightedNodeId } = useJsonColumnViewState();
if (!selectedNodeId || !highlightedNodeId) {
return <></>;
}
const [json] = useJson();
const selectedHeroPath = new JSONHeroPath(selectedNodeId);
const selectedJson = selectedHeroPath.first(json);
const selectedInfo = inferType(selectedJson);
const isSelectedLeafNode =
selectedInfo.name !== "object" && selectedInfo.name !== "array";
const highlightedHeroPath = new JSONHeroPath(highlightedNodeId);
const highlightedJson = highlightedHeroPath.first(json);
const highlightedInfo = inferType(highlightedJson);
const isHighlightedLeafNode =
highlightedInfo.name !== "object" && highlightedInfo.name !== "array";
const shouldHighlightInPreview =
selectedNodeId !== highlightedNodeId && !isHighlightedLeafNode;
const shouldDisplayCodePreview =
shouldHighlightInPreview || !isSelectedLeafNode;
if (!shouldDisplayCodePreview) {
return <></>;
}
return (
<Tabs tabs={tabs}>
<>
<TabContent value="json">
{shouldHighlightInPreview ? (
<JsonPreview
json={highlightedJson}
highlightPath={pathToDescendant(
highlightedNodeId,
selectedNodeId
)}
/>
) : (
<JsonPreview json={selectedJson} />
)}
</TabContent>
<TabContent value="schema">
{shouldHighlightInPreview ? (
<JsonSchemaViewer path={highlightedNodeId} />
) : (
<JsonSchemaViewer path={selectedNodeId} />
)}
</TabContent>
</>
</Tabs>
);
}

View File

@@ -0,0 +1,20 @@
import { useHotkeys } from "react-hotkeys-hook";
import { useSelectedInfo } from "../hooks/useSelectedInfo";
export function CopySelectedNodeShortcut() {
const selectedInfo = useSelectedInfo();
useHotkeys(
'shift+c,shift+C',
(e) => {
e.preventDefault();
const selectedJSON = selectedInfo?.name === "string"
? selectedInfo?.value
: JSON.stringify(selectedInfo?.value, null, 2);
navigator.clipboard.writeText(selectedJSON);
},
[selectedInfo]
);
return <></>;
}

View File

@@ -0,0 +1,28 @@
import React, { useCallback } from "react";
export type CopyTextProps = {
children?: React.ReactNode;
value: string;
className?: string;
onCopied?: () => void;
};
export function CopyText({
children,
value,
className,
onCopied,
}: CopyTextProps) {
const onClick = useCallback(() => {
navigator.clipboard.writeText(value);
if (onCopied) {
onCopied();
}
}, [value]);
return (
<div onClick={onClick} className={`${className}`}>
{children}
</div>
);
}

View File

@@ -0,0 +1,31 @@
import { ClipboardIcon } from "@heroicons/react/outline";
import { useCallback, useState } from "react";
import { CopyText } from "./CopyText";
import { Body } from "./Primitives/Body";
export type CopyTextButtonProps = {
value: string;
className?: string;
};
export function CopyTextButton({ value, className }: CopyTextButtonProps) {
const [copied, setCopied] = useState(false);
const onCopied = useCallback(() => {
setCopied(true);
const timeout = setTimeout(() => {
setCopied(false);
}, 1500);
}, [value]);
return (
<CopyText className={`${className}`} value={value} onCopied={onCopied}>
{copied ? (
<Body>Copied!</Body>
) : (
<div className="flex items-center">
<ClipboardIcon className="h-4 w-4 mr-[2px]" />
<Body>Copy</Body>
</div>
)}
</CopyText>
);
}

View File

@@ -0,0 +1,73 @@
import { FunctionComponent, useState } from "react";
import { CopyTextButton } from "./CopyTextButton";
import { Title } from "./Primitives/Title";
export type DataTableProps = {
rows: DataTableRow[];
};
export type DataTableRow = {
key: string;
value: string;
icon?: JSX.Element;
};
type DataRowProps = {
title: string;
value: string;
icon?: JSX.Element;
};
const DataRow: FunctionComponent<DataRowProps> = ({ title, value, icon }) => {
const [hovering, setHovering] = useState(false);
return (
<tr className="divide-solid divide-x transition dark:divide-slate-700">
<td className="flex items-baseline py-2 pr-3 text-base dark:text-slate-400">
<div className="flex-1 ml-1">{title}</div>
</td>
<td
onMouseOver={() => setHovering(true)}
onMouseOut={() => setHovering(false)}
className={`relative w-full h-full pl-2 py-2 text-base text-slate-800 transition dark:text-slate-300 break-all ${
hovering ? "bg-slate-100 dark:bg-slate-700" : "bg-transparent"
}`}
>
{value}
<div
className={`absolute top-0 right-0 flex justify-end h-full w-full transition ${
hovering ? "opacity-100" : "opacity-0"
}`}
>
<CopyTextButton
className="bg-slate-200 hover:bg-slate-300 h-fit mt-1 mr-1 px-2 py-0.5 rounded-sm transition hover:cursor-pointer dark:text-white dark:bg-slate-600 dark:hover:bg-slate-500"
value={value}
></CopyTextButton>
</div>
</td>
</tr>
);
};
export const DataTable: FunctionComponent<DataTableProps> = ({ rows }) => {
return (
<div>
<Title className="text-slate-700 dark:text-slate-400 mb-2">
Properties
</Title>
<table className="w-full table-auto border-y-[0.5px] border-slate-300 transition dark:border-slate-700">
<tbody className="divide-solid divide-y divide-slate-300 w-full transition dark:divide-slate-700">
{rows.map((row) => {
return (
<DataRow
key={row.key}
title={row.key}
value={row.value}
icon={row.icon}
/>
);
})}
</tbody>
</table>
</div>
);
};

View File

@@ -0,0 +1,81 @@
import { PencilAltIcon } from "@heroicons/react/outline";
import { useEffect, useRef, useState } from "react";
import { useFetcher } from "remix";
import { match } from "ts-pattern";
import { useJsonDoc } from "~/hooks/useJsonDoc";
export function DocumentTitle() {
const { doc } = useJsonDoc();
const [editedTitle, setEditedTitle] = useState(doc.title);
const updateDoc = useFetcher();
const ref = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (updateDoc.type === "done" && updateDoc.data.title) {
ref.current?.blur();
}
}, [updateDoc]);
if (doc.readOnly) {
return (
<div
className="flex justify-center items-center w-full"
title={doc.title}
>
<span
className={
"min-w-[15vw] border-none text-ellipsis text-slate-300 px-2 pl-10 py-1 rounded-sm bg-transparent placeholder:text-slate-400 focus:bg-black/30 focus:outline-none focus:border-none hover:cursor-text transition dark:bg-transparent dark:text-slate-200 dark:placeholder:text-slate-400 dark:focus:bg-black dark:focus:bg-opacity-10"
}
>
{doc.title}
</span>
</div>
);
} else {
return (
<updateDoc.Form method="post" action={`/actions/${doc.id}/update`}>
<div
className="flex justify-center items-center w-full"
title={doc.title}
>
<label className="relative block group">
<PencilAltIcon className="h-5 w-5 absolute top-1/2 transform -translate-y-1/2 left-3 text-white opacity-0 transition pointer-events-none group-hover:opacity-80 group-focus:opacity-80" />
<input
ref={ref}
className={
"min-w-[15vw] border-none text-ellipsis text-slate-300 px-2 pl-10 py-1 rounded-sm bg-transparent placeholder:text-slate-400 focus:bg-black/30 focus:outline-none focus:border-none hover:bg-black hover:bg-opacity-30 hover:cursor-text transition dark:bg-transparent dark:text-slate-200 dark:placeholder:text-slate-400 dark:focus:bg-black dark:focus:bg-opacity-10 dark:hover:bg-black dark:hover:bg-opacity-10"
}
type="text"
name="title"
spellCheck="false"
placeholder="Name your JSON file"
value={editedTitle}
onChange={(e) => setEditedTitle(e.target.value)}
/>
</label>
{match(editedTitle)
.with(doc.title, () => (
<p className="ml-2 text-transparent">Save</p>
))
.with("", () => (
<button
className="ml-2 text-lime-500 hover:text-lime-600 transition"
onClick={() => setEditedTitle(doc.title)}
>
Reset
</button>
))
.otherwise(() => (
<button
type="submit"
className="ml-2 text-lime-500 hover:text-lime-600 transition"
>
Save
</button>
))}
</div>
</updateDoc.Form>
);
}
}

View File

@@ -0,0 +1,90 @@
import { ArrowCircleDownIcon } from "@heroicons/react/outline";
import { useCallback, useRef } from "react";
import { useDropzone } from "react-dropzone";
import { Form, useSubmit } from "remix";
import invariant from "tiny-invariant";
export function DragAndDropForm() {
const formRef = useRef<HTMLFormElement>(null);
const filenameInputRef = useRef<HTMLInputElement>(null);
const rawJsonInputRef = useRef<HTMLInputElement>(null);
const submit = useSubmit();
const onDrop = useCallback(
(acceptedFiles: Array<File>) => {
if (!formRef.current || !filenameInputRef.current) {
return;
}
if (acceptedFiles.length === 0) {
return;
}
const firstFile = acceptedFiles[0];
const reader = new FileReader();
reader.onabort = () => console.log("file reading was aborted");
reader.onerror = () => console.log("file reading has failed");
reader.onload = () => {
if (reader.result == null) {
return;
}
let jsonValue: string | undefined = undefined;
if (typeof reader.result === "string") {
jsonValue = reader.result;
} else {
const decoder = new TextDecoder("utf-8");
jsonValue = decoder.decode(reader.result);
}
invariant(rawJsonInputRef.current, "rawJsonInputRef is null");
invariant(jsonValue, "jsonValue is undefined");
rawJsonInputRef.current.value = jsonValue;
submit(formRef.current);
};
reader.readAsArrayBuffer(firstFile);
filenameInputRef.current.value = firstFile.name;
},
[formRef.current, filenameInputRef.current, rawJsonInputRef.current]
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDropAccepted: onDrop,
maxFiles: 1,
maxSize: 1024 * 1024 * 1,
multiple: false,
accept: "application/json",
});
return (
<Form method="post" action="/actions/createFromFile" ref={formRef}>
<div
{...getRootProps()}
className="block min-w-[300px] cursor-pointer rounded-md border-2 border-dashed border-slate-600 bg-slate-900/40 p-4 text-base text-slate-300 focus:border-indigo-500 focus:ring-indigo-500"
>
<input {...getInputProps()} />
<div className="flex items-center">
<ArrowCircleDownIcon
className={`mr-3 inline h-6 w-6 ${
isDragActive ? "text-lime-500" : ""
}`}
/>
<p className={`${isDragActive ? "text-lime-500" : ""}`}>
{isDragActive
? "现在释放以打开…"
: "在此处拖放 JSON 文件,或点击选择"}
</p>
</div>
<input type="hidden" name="filename" ref={filenameInputRef} />
<input type="hidden" name="rawJson" ref={rawJsonInputRef} />
</div>
</Form>
);
}

View File

@@ -0,0 +1,20 @@
import { Link } from "remix";
export function ExampleDoc({
id,
title,
path,
}: {
id: string;
title: string;
path?: string;
}) {
return (
<Link
to={`/j/${id}${path ? `?path=${path}` : ""}`}
className="bg-slate-900 px-4 py-2 rounded-md whitespace-nowrap text-lime-300 transition hover:text-lime-500"
>
{title}
</Link>
);
}

View File

@@ -0,0 +1,28 @@
import { Form } from "remix";
export function ExampleUrl({
url,
title,
displayTitle,
}: {
url: string;
title: string;
displayTitle?: string;
}) {
return (
<Form
method="post"
action="/actions/createFromUrl?utm_source=example_url"
reloadDocument
>
<input type="hidden" name="jsonUrl" value={url} />
<input type="hidden" name="title" value={title} />
<button
type="submit"
className="bg-slate-900 px-4 py-2 rounded-md whitespace-nowrap text-lime-300 transition hover:text-lime-500"
>
{displayTitle ?? title}
</button>
</Form>
);
}

View File

@@ -0,0 +1,52 @@
import { FunctionComponent, useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { DocumentDownloadIcon } from "@heroicons/react/outline";
export const FileDropzone: FunctionComponent = ({ children }) => {
const onDrop = useCallback((acceptedFiles) => {
acceptedFiles.forEach((file: Blob) => {
const reader = new FileReader();
reader.onabort = () => console.log("file reading was aborted");
reader.onerror = () => console.log("file reading has failed");
reader.onload = () => {
if (typeof reader.result === "string") {
let json = JSON.parse(reader.result);
// dataSourceDispatch(setJSONAction("Needs title", json));
} else {
// dataSourceDispatch(setErrorAction("Can't read file"));
}
};
reader.readAsText(file);
});
}, []);
const { getRootProps, isDragActive } = useDropzone({
onDrop,
multiple: false,
maxFiles: 1,
accept: "application/json, text/*",
noDragEventsBubbling: true,
});
return (
<div
{...getRootProps()}
className={"absolute w-screen h-screen m-0 p-0 left-0 top-0"}
>
<div
className={`${
isDragActive ? "" : "hidden"
} absolute w-screen h-screen bg-black bg-opacity-50 flex justify-center items-center`}
>
<div className={"text-center"}>
{/*<input {...getInputProps()} />*/}
<DocumentDownloadIcon className={"w-72 h-72 text-white"} />
<p className={"text-white text-2xl"}>
Drag 'n' drop some files here, or click to select files
</p>
</div>
</div>
<div>{children}</div>
</div>
);
};

View File

@@ -0,0 +1,65 @@
import { useJsonDoc } from "~/hooks/useJsonDoc";
import { ArrowKeysIcon } from "./Icons/ArrowKeysIcon";
import { CopyShortcutIcon } from "./Icons/CopyShortcutIcon";
import { EscapeKeyIcon } from "./Icons/EscapeKeyIcon";
import { SquareBracketsIcon } from "./Icons/SquareBracketsIcon";
import { Body } from "./Primitives/Body";
import { ThemeModeToggler } from "./ThemeModeToggle";
import { GithubStarSmall } from "./UI/GithubStarSmall";
import { IndentPreference } from "~/components/IndentPreference";
import { ArrowRightIcon } from "@heroicons/react/outline";
import TriggerDevLogoImageDark from "~/assets/images/trigger-dev-logo-dark.png";
import TriggerDevLogoImage from "~/assets/images/trigger-dev-logo.png";
import TriggerDevLogoTriangleImage from "~/assets/images/td-triangle.png";
export function Footer() {
const { minimal } = useJsonDoc();
return (
<footer className="flex items-center justify-between w-screen h-[32px] flex-shrink-0 bg-slate-200 dark:bg-slate-800 border-t-[1px] border-slate-400 transition dark:border-slate-600">
<ol className="flex pl-3">
<li className="flex items-center">
<ArrowKeysIcon className="transition text-slate-300 dark:text-slate-500" />
<Body className="pl-2 pr-4 text-slate-800 transition dark:text-white">
Navigate
</Body>
</li>
<li className="flex items-center">
<SquareBracketsIcon className="transition text-slate-300 dark:text-slate-500" />
<Body className="pl-2 pr-4 text-slate-800 transition dark:text-white">
History
</Body>
</li>
<li className="flex items-center">
<EscapeKeyIcon className="transition text-slate-300 dark:text-slate-500" />
<Body className="pl-2 pr-4 text-slate-800 transition dark:text-white whitespace-nowrap">
Reset path
</Body>
</li>
<li className="flex items-center">
<CopyShortcutIcon className="transition text-slate-300 dark:text-slate-500" />
<Body className="flex pl-2 pr-4 text-slate-800 transition dark:text-white">
Copy&nbsp;
<span className="hidden lg:flex whitespace-nowrap">
selected&nbsp;
</span>
node
</Body>
</li>
</ol>
<ol className="flex gap-2 items-center h-full invisible md:visible">
{minimal && (
<li>
<GithubStarSmall />
</li>
)}
<li>
<IndentPreference />
</li>
<li>
<ThemeModeToggler />
</li>
</ol>
</footer>
);
}

View File

@@ -0,0 +1,94 @@
import { ShareIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline";
import { DocumentTitle } from "./DocumentTitle";
import { DiscordIconTransparent } from "./Icons/DiscordIconTransparent";
import { EmailIconTransparent } from "./Icons/EmailIconTransparent";
import { GithubStar } from "./UI/GithubStar";
import { Logo } from "./Icons/Logo";
import { Share } from "./Share";
import { NewDocument } from "./NewDocument";
import {
Popover,
PopoverArrow,
PopoverContent,
PopoverTrigger,
} from "./UI/Popover";
import { Form } from "remix";
import { useJsonDoc } from "~/hooks/useJsonDoc";
import { LogoTriggerdotdev } from "./Icons/LogoTriggerdotdev";
export function Header() {
const { doc } = useJsonDoc();
return (
<header className="flex items-center justify-between w-screen h-[40px] bg-indigo-700 dark:bg-slate-800 border-b-[1px] border-slate-600">
<div className="flex pl-2 gap-1 sm:gap-1.5 pt-0.5 h-8 justify-center items-center">
<div className="w-20 sm:w-24">
<Logo />
</div>
<p className="text-slate-300 text-sm font-sans">by</p>
<LogoTriggerdotdev className="w-16 sm:w-20 opacity-80 hover:opacity-100 transition duration-300" />
</div>
<DocumentTitle />
<ol className="flex text-sm items-center gap-2 px-4">
{!doc.readOnly && (
<Form
method="delete"
onSubmit={(e) =>
!confirm(
"这将从 jsonhero.io 永久删除此文档,您确定要继续吗?"
) && e.preventDefault()
}
>
<button type="submit">
<button className="flex items-center justify-center py-1 bg-slate-200 text-slate-800 bg-opacity-80 text-base font-bold px-2 rounded uppercase hover:cursor-pointer hover:bg-opacity-100 transition">
<TrashIcon className="w-4 h-4 mr-0.5"></TrashIcon>
</button>
</button>
</Form>
)}
<Popover>
<PopoverTrigger>
<button className="flex items-center justify-center bg-lime-500 text-slate-800 bg-opacity-90 text-base font-bold px-2 py-1 rounded uppercase hover:cursor-pointer hover:bg-opacity-100 transition">
<PlusIcon className="w-4 h-4 mr-0.5"></PlusIcon>
</button>
</PopoverTrigger>
<PopoverContent side="bottom" sideOffset={8}>
<NewDocument />
<PopoverArrow
className="fill-current text-indigo-700"
offset={20}
/>
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger>
<button className="flex items-center justify-center py-1 bg-slate-200 text-slate-800 bg-opacity-90 text-base font-bold px-2 rounded uppercase hover:cursor-pointer hover:bg-opacity-100 transition">
<ShareIcon className="w-4 h-4 mr-1"></ShareIcon>
</button>
</PopoverTrigger>
<PopoverContent side="bottom" sideOffset={8}>
<Share />
<PopoverArrow
className="fill-current text-indigo-700"
offset={20}
/>
</PopoverContent>
</Popover>
<li className="opacity-90 transition hover:cursor-pointer hover:opacity-100">
<GithubStar />
</li>
<li className="opacity-90 transition hover:cursor-pointer hover:opacity-100">
<a href="https://discord.gg/JtBAxBr2m3" target="_blank">
<DiscordIconTransparent />
</a>
</li>
</ol>
</header>
);
}

View File

@@ -0,0 +1,33 @@
import { Body } from "../Primitives/Body";
import { HomeApiHeroLaptop } from "./HomeApiHeroLaptop";
export function HomeApiHeroBanner() {
return (
<div className="flex items-center justify-start md:justify-center w-full h-40 bg-gradient-to-r from-purple-600 via-pink-500 to-purple-600 hover:backdrop-filter hover:backdrop-brightness-75 transition">
<div className="relative flex justify-center items-center w-1/2 md:w-full pl-6 md:px-6">
<div className="flex flex-col">
<Body className=" text-white text-[1rem] sm:text-[1.2rem] font-bold md:text-3xl leading-tight">
Early access to API Hero
</Body>
<p className="mb-2 text-white md:text-xl text-sm">
Make every API you use faster and more reliable.
</p>
<a
href="https://apihero.run"
target="new"
className="flex items-center justify-center px-3 py-2 mt-2 text-center text-md md:text-xl text-slate-800 font-bold bg-lime-500 rounded shadow-md hover:bg-lime-400 transition"
>
Get started &rarr;
</a>
</div>
<a
href="https://apihero.run"
target="new"
className="absolute md:relative -top-5 md:top-auto -right-[20rem] md:right-auto"
>
<HomeApiHeroLaptop className="w-50 md:w-80 mb-2"></HomeApiHeroLaptop>
</a>
</div>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import ApiHeroLaptop from "~/assets/images/apihero-laptop.png";
export type IconProps = {
className?: string;
};
export function HomeApiHeroLaptop({ className }: IconProps) {
return <img src={ApiHeroLaptop} className={className} />;
}

View File

@@ -0,0 +1,29 @@
import { AutoplayVideo } from "../AutoplayVideo";
import { ExtraLargeTitle } from "../Primitives/ExtraLargeTitle";
import { SmallSubtitle } from "../Primitives/SmallSubtitle";
import { HomeSection } from "./HomeSection";
import shareVideo from "~/assets/home/JsonHeroShare.mp4";
export function HomeCollaborateSection() {
return (
<HomeSection
containerClassName="py-10 px-6 bg-black md:py-36 lg:py-20"
reversed
>
<div className="w-full md:pl-10 md:w-1/2">
<ExtraLargeTitle className="text-white mb-4">
Collaborate with the whole world (and yourself)
</ExtraLargeTitle>
<SmallSubtitle className="mb-6 md:mb-10">
Easily share your JSON documents with any distant relative. Link right
to the part of the document you're on. Or save the link for some
casual browsing later in the evening while enjoying a glass of red.
</SmallSubtitle>
</div>
<div className="w-full md:w-1/2">
<AutoplayVideo src={shareVideo} />
</div>
</HomeSection>
);
}

View File

@@ -0,0 +1,30 @@
import { AutoplayVideo } from "../AutoplayVideo";
import { ExtraLargeTitle } from "../Primitives/ExtraLargeTitle";
import { SmallSubtitle } from "../Primitives/SmallSubtitle";
import { HomeSection } from "./HomeSection";
import edgeCasesVideo from "~/assets/home/UncoverEdgeCases.mp4";
export function HomeEdgeCasesSection() {
return (
<HomeSection
containerClassName="py-10 px-6 bg-black md:py-36 lg:py-20"
reversed
>
<div className="w-full md:pl-10 md:w-1/2">
<ExtraLargeTitle className="text-white mb-4">
Uncover edge cases
</ExtraLargeTitle>
<SmallSubtitle className="mb-6 md:mb-10">
Sometimes a field can be null, have an unexpected value or be missing
entirely. View any field's related values and see what to expect when
you least expect it. Or check out the inferred JSON schema to see what
your JSON is really made of.
</SmallSubtitle>
</div>
<div className="w-full md:w-1/2">
<AutoplayVideo src={edgeCasesVideo} />
</div>
</HomeSection>
);
}

View File

@@ -0,0 +1,93 @@
import {
FastForwardIcon,
MoonIcon,
ClockIcon,
CodeIcon,
LockOpenIcon,
CubeTransparentIcon,
} from "@heroicons/react/outline";
import { Body } from "../Primitives/Body";
import { LargeTitle } from "../Primitives/LargeTitle";
import { HomeGridFeatureItem } from "./HomeGridFeatureItem";
import { HomeSection } from "./HomeSection";
export function HomeFeatureGridSection() {
return (
<HomeSection containerClassName="bg-black">
<div className="flex flex-col px-4 pb-2 pt-6 md:py-12">
<LargeTitle className="mb-4 text-slate-300">
And lots more features
</LargeTitle>
<div className="flex flex-col gap-4 md:flex-row md:flex-wrap">
<HomeGridFeatureItem
icon={FastForwardIcon}
title="Keyboard shortcuts"
titleClassName="text-white"
>
<Body className="text-slate-400">
Move as fast as you can think after 3 coffees
</Body>
</HomeGridFeatureItem>
<HomeGridFeatureItem
icon={MoonIcon}
title="Dark mode"
titleClassName="text-white"
>
<Body className="text-slate-400">
Of course, were not animals.
</Body>
</HomeGridFeatureItem>
<HomeGridFeatureItem
icon={ClockIcon}
title="Code view"
titleClassName="text-white"
>
<Body className="text-slate-400">
Easily switch to the code view, so you can appear hardcore.
</Body>
</HomeGridFeatureItem>
<HomeGridFeatureItem
icon={CubeTransparentIcon}
title="Auto JSON Schema"
titleClassName="text-white"
>
<Body className="text-slate-400">
Automatically generates JSON Schema (draft 2020-12) from your
JSON.
</Body>
</HomeGridFeatureItem>
<HomeGridFeatureItem
icon={CodeIcon}
title="VS Code plugin"
titleClassName="text-white"
>
<Body className="text-slate-400">
Quickly view JSON files or selections in JSON Hero, right from VS
Code.{" "}
<a
className="whitespace-nowrap text-lime-300 hover:text-lime-500"
href="https://marketplace.visualstudio.com/items?itemName=JSONHero.jsonhero-vscode"
target="_blank"
rel="noopener noreferrer"
>
Get it here
</a>
.
</Body>
</HomeGridFeatureItem>
<HomeGridFeatureItem
icon={LockOpenIcon}
title="100% open source"
titleClassName="text-white"
>
<Body className="text-slate-400">
Use jsonhero.io or fork it on GitHub and run it yourself.
</Body>
</HomeGridFeatureItem>
</div>
</div>
</HomeSection>
);
}

View File

@@ -0,0 +1,53 @@
import { Link } from "remix";
import { DiscordIcon } from "../Icons/DiscordIcon";
import { EmailIcon } from "../Icons/EmailIcon";
import { GithubIcon } from "../Icons/GithubIcon";
import { Logo } from "../Icons/Logo";
import { TwitterIcon } from "../Icons/TwitterIcon";
export type HomeFooterProps = {
maxWidth?: string;
};
export function HomeFooter({ maxWidth = "1150px" }: HomeFooterProps) {
return (
<footer className="flex flex-col items-center w-full px-4 py-6 bg-black md:py-10">
<div
className="flex items-center justify-between w-full border-t-[1px] pt-9 border-slate-800"
style={{ maxWidth: maxWidth }}
>
<div className="flex flex-grow items-start">
<Logo />
</div>
<ol className="flex ml-2">
<li className="mr-2 hover:cursor-pointer text-white/70 hover:text-white transition">
<Link to="/privacy"></Link>
</li>
<li className="hover:cursor-pointer">
<a
href="https://github.com/triggerdotdev/jsonhero-web"
target="_blank"
>
<GithubIcon />
</a>
</li>
<li className="ml-2 hover:cursor-pointer">
<a href="mailto:hello@jsonhero.io">
<EmailIcon />
</a>
</li>
<li className="ml-2 hover:cursor-pointer">
<a href="https://discord.gg/JtBAxBr2m3" target="_blank">
<DiscordIcon />
</a>
</li>
<li className="ml-2 hover:cursor-pointer">
<a href="https://twitter.com/triggerdotdev" target="_blank">
<TwitterIcon />
</a>
</li>
</ol>
</div>
</footer>
);
}

View File

@@ -0,0 +1,13 @@
import { Body } from "../Primitives/Body";
import { GithubStar } from "../UI/GithubStar";
export function GithubBanner() {
return (
<div className="flex items-center justify-center w-full h-14 bg-indigo-600">
<div className="flex items-center">
<Body className="mr-3 text-xl text-white">Star us on GitHub 👉</Body>
<GithubStar />
</div>
</div>
);
}

View File

@@ -0,0 +1,21 @@
import { IconComponent } from "~/useColumnView";
import { Body } from "../Primitives/Body";
import { Title } from "../Primitives/Title";
export type HomeGridFeatureItemProps = {
icon: IconComponent;
title: string;
className?: string;
titleClassName?: string;
children: React.ReactNode;
};
export function HomeGridFeatureItem(props: HomeGridFeatureItemProps) {
return (
<div className="flex lg:basis-1/4 basis-1 md:basis-1/4 flex-grow flex-col p-6 rounded-sm bg-white bg-opacity-[7%]">
<props.icon className="w-10 h-10 min-h-[44px] text-indigo-700 mb-3" />
<Title className={props.titleClassName}>{props.title}</Title>
{props.children}
</div>
);
}

View File

@@ -0,0 +1,69 @@
import { DiscordIconTransparent } from "../Icons/DiscordIconTransparent";
import { EmailIconTransparent } from "../Icons/EmailIconTransparent";
import { TwitterIcon } from "../Icons/TwitterIcon";
import { Logo } from "../Icons/Logo";
import { NewDocument } from "../NewDocument";
import { GithubStar } from "../UI/GithubStar";
import {
Popover,
PopoverArrow,
PopoverContent,
PopoverTrigger,
} from "../UI/Popover";
import TriggerDevLogoImage from "~/assets/images/trigger-dev-logo.png";
import { LogoTriggerdotdev } from "../Icons/LogoTriggerdotdev";
export function HomeHeader({ fixed }: { fixed?: boolean }) {
return (
<header
className={`${
fixed ? "fixed" : ""
} z-20 flex h-12 justify-center bg-indigo-700 flex-col`}
>
<div className="flex items-center justify-between w-screen px-4">
<div className="flex gap-1 sm:gap-1.5 h-8 justify-center items-center">
<div className="w-24 sm:w-32">
<Logo />
</div>
<p className="text-slate-300 text-sm sm:text-base font-sans">by</p>
<LogoTriggerdotdev className="pt-0.5 w-16 sm:w-24 opacity-80 hover:opacity-100 transition duration-300" />
</div>
<ol className="flex items-center gap-2 sm:pr-4">
<Popover>
<PopoverTrigger>
<button className=" bg-lime-400 text-slate-900 text-lg font-bold px-2 py-0.5 rounded uppercase whitespace-nowrap cursor-pointer opacity-90 hover:opacity-100 transition">
</button>
</PopoverTrigger>
<PopoverContent side="bottom" sideOffset={30}>
<NewDocument />
<PopoverArrow
className="fill-current text-indigo-700"
offset={20}
/>
</PopoverContent>
</Popover>
<li className="hover:cursor-pointer hidden sm:block">
<GithubStar />
</li>
<li className="hover:cursor-pointer opacity-90 hover:opacity-100 transition hidden sm:block">
<a href="mailto:hello@jsonhero.io">
<EmailIconTransparent />
</a>
</li>
<li className="hover:cursor-pointer opacity-90 hover:opacity-100 transition hidden sm:block">
<a href="https://discord.gg/JtBAxBr2m3" target="_blank">
<DiscordIconTransparent />
</a>
</li>
<li className="hover:cursor-pointer opacity-90 hover:opacity-100 transition hidden sm:block">
<a href="https://twitter.com/triggerdotdev" target="_blank">
<TwitterIcon />
</a>
</li>
</ol>
</div>
</header>
);
}

View File

@@ -0,0 +1,39 @@
import { AutoplayVideo } from "../AutoplayVideo";
import { NewFile } from "../NewFile";
import { ExtraLargeTitle } from "../Primitives/ExtraLargeTitle";
import { SmallSubtitle } from "../Primitives/SmallSubtitle";
import heroVideo from "~/assets/home/JsonHero2.mp4";
const jsonHeroTitle = "JSON 很糟糕。";
const jsonHeroSlogan = "但我们正在让它变得更好。";
export function HomeHeroSection() {
return (
<div
className={`flex items-stretch flex-col md:flex-row bg-[rgb(56,52,139)] lg:p-6 lg:pb-16 pt-20 lg:pt-32`}
>
<div className="self-center md:w-1/2 md:pr-10 flex justify-end">
<div className=" max-w-3xl">
<AutoplayVideo src={heroVideo} />
</div>
</div>
<div className="self-center flex align-center md:w-1/2 px-6 pb-8 mt-8 lg:mt-0">
<div className="max-w-lg">
<ExtraLargeTitle className="text-lime-300">
{jsonHeroTitle}
</ExtraLargeTitle>
<ExtraLargeTitle className="text-white mb-4">
{jsonHeroSlogan}
</ExtraLargeTitle>
<SmallSubtitle className="text-slate-200 mb-8">
JSON
使 JSON JSON
JSON <em></em>
</SmallSubtitle>
<NewFile />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,158 @@
import React, { useEffect, useRef, useState } from "react";
import { JsonProvider } from "~/hooks/useJson";
import { JsonColumnViewProvider, useJsonColumnViewAPI, } from "~/hooks/useJsonColumnView";
import { JsonDocProvider } from "~/hooks/useJsonDoc";
import { JsonPreview } from "../JsonPreview";
import { PreviewValue } from "../Preview/PreviewValue";
import { ExtraLargeTitle } from "../Primitives/ExtraLargeTitle";
import { SmallSubtitle } from "../Primitives/SmallSubtitle";
import { PropertiesValue } from "../Properties/PropertiesValue";
import { HomeSection } from "./HomeSection";
const json = {
id: "a1c33bd1-0528-4de3-a745-44d95e7ac3d8",
title: "JSON Hero is a tool for JSON",
thumbnail: "https://media.giphy.com/media/13CoXDiaCcCoyk/giphy-downsized.gif",
createdAt: "2022-02-01T02:25:41-05:00",
tint: "#EAB308",
webpages: "https://www.theonion.com/",
youtube: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
json: "bourne",
};
const infoBoxData = [
{
title: "Images",
highlight: "$.thumbnail",
},
{
title: "Dates",
highlight: "$.createdAt",
},
{
title: "Colors",
highlight: "$.tint",
},
{
title: "URLs",
highlight: "$.webpages",
},
{
title: "Videos",
highlight: "$.youtube",
},
];
const autoplayDuration = 3000;
export function HomeInfoBoxSection() {
return (
<SampleJSONPreview initialSelection={infoBoxData[0].highlight}>
<HomeInfoBoxSectionContent/>
</SampleJSONPreview>
);
}
function HomeInfoBoxSectionContent() {
const [index, setIndex] = useState(0);
const api = useJsonColumnViewAPI();
const interval = useRef<NodeJS.Timer | null>(null);
useEffect(() => {
const selectedPath = infoBoxData[index].highlight;
api.goToNodeId(selectedPath, "home");
}, [index]);
const resetInterval = () => {
if (interval.current != null) {
clearInterval(interval.current);
}
interval.current = setInterval(() => {
setIndex((i) => (i = (i + 1) % infoBoxData.length));
}, autoplayDuration);
};
useEffect(() => {
resetInterval();
return () => {
if (interval.current == null) return;
clearInterval(interval.current);
};
}, []);
return (
<HomeSection containerClassName="bg-black p-6">
<div className="md:pr-4 lg:pr-10 flex flex-col w-full md:w-1/2">
<ExtraLargeTitle className="text-white mb-4">
<span className=" text-lime-300">{infoBoxData[index].title}</span> are
more than just strings
</ExtraLargeTitle>
<SmallSubtitle className="text-slate-400 mb-10">
We figure out what your strings are made of, so you don't have to.
</SmallSubtitle>
<ul className="flex w-full text-slate-300 mb-3">
{infoBoxData.map((value, i) => {
return (
<li
key={value.highlight}
onClick={() => {
resetInterval();
setIndex(i);
}}
className={`flex flex-grow justify-center px-4 py-2 cursor-pointer border-b-2 ${
index === i
? "text-white border-lime-500"
: "border-slate-600"
}`}
>
{value.title}
</li>
);
})}
</ul>
<div className="w-full">
<JsonPreview
json={json}
highlightPath={infoBoxData[index].highlight}
/>
</div>
</div>
<div className="relative w-full md:w-1/2 flex flex-col justify-center items-center py-5">
<div className="pointer-events-none absolute z-10 bottom-0 w-full h-[200px] bg-gradient-to-t from-slate-900 to-transparent mb-5"></div>
<div className="pointer-events-auto min-w-full max-w-full p-4 rounded-sm bg-slate-900 h-[65vh] overflow-y-auto custom-scrollbar">
<div className="pointer-events-none">
<div className="mb-4">
<PreviewValue/>
</div>
<PropertiesValue/>
</div>
</div>
</div>
</HomeSection>
);
}
function SampleJSONPreview({
children,
initialSelection,
}: {
children: React.ReactNode;
initialSelection: string;
}) {
return (
<JsonDocProvider
doc={{
id: "sample",
title: "Sample",
type: "raw",
readOnly: false,
contents: "",
}}
path={initialSelection}
>
<JsonProvider initialJson={json}>
<JsonColumnViewProvider>{children}</JsonColumnViewProvider>
</JsonProvider>
</JsonDocProvider>
);
}

View File

@@ -0,0 +1,26 @@
import { AutoplayVideo } from "../AutoplayVideo";
import { ExtraLargeTitle } from "../Primitives/ExtraLargeTitle";
import { SmallSubtitle } from "../Primitives/SmallSubtitle";
import { HomeSection } from "./HomeSection";
import searchVideo from "~/assets/home/JsonHeroSearch.mp4";
export function HomeSearchSection() {
return (
<HomeSection containerClassName="py-10 px-6 bg-black md:py-36 lg:py-20">
<div className="w-full md:pr-10 md:w-1/2">
<ExtraLargeTitle className="text-white mb-4">
Quickly search your whole JSON file
</ExtraLargeTitle>
<SmallSubtitle className="mb-6 md:mb-10">
Search for absolutely anything in your JSON file with blistering
speed. Use the fuzzy matching and keyboard shortcuts to make
navigating your files even faster.
</SmallSubtitle>
</div>
<div className="w-full md:w-1/2">
<AutoplayVideo src={searchVideo} />
</div>
</HomeSection>
);
}

View File

@@ -0,0 +1,28 @@
export type HomeSectionProps = {
containerClassName?: string;
maxWidth?: string;
reversed?: boolean;
flipped?: boolean;
children: React.ReactNode;
};
export function HomeSection({
containerClassName,
maxWidth = "1150px",
reversed = false,
flipped = false,
children,
}: HomeSectionProps) {
return (
<div className={`flex justify-center items-center ${containerClassName}`}>
<div
className={`flex flex-col md:flex-row w-full ${
reversed ? "md:flex-row-reverse" : ""
}${flipped ? "flex-col-reverse" : ""}`}
style={{ maxWidth: maxWidth }}
>
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,43 @@
import React from "react";
export type HomeSplitSectionProps = {
className?: string;
children: React.ReactNode;
};
export function HomeSplitSection({
className,
children,
}: HomeSplitSectionProps) {
return (
<div
className={`grid lg:grid-cols-2 items-center justify-items-center py-12 ${className}`}
>
{children}
</div>
);
}
export function HomeSplitTextContent({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="justify-self-center lg:justify-self-end max-w-2xl px-20 flex flex-col justify-center">
{children}
</div>
);
}
export function HomeSplitMediaContent({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex justify-center items-center px-10 py-5">
{children}
</div>
);
}

View File

@@ -0,0 +1,18 @@
export function ArrayIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
className={props.className}
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
key="array"
>
<path d="M5.8899 18.525C5.8899 17.2 4.80855 17.025 3.84224 17.025C3.52013 17.025 3.22104 17.05 2.89893 17.05V2.95C4.00329 2.95 5.8899 3.25 5.8899 1.475C5.8899 0.525001 5.19968 0 4.37141 0H2.04766C0.80526 0 0 0.700001 0 2.1V17.925C0 19.35 0.782252 20 2.04766 20H4.37141C5.19968 20 5.8899 19.475 5.8899 18.525Z" fill="currentColor"/>
<path d="M20 17.925V2.1C20 0.700001 19.1947 0 17.9523 0H15.6286C14.8003 0 14.1101 0.525001 14.1101 1.475C14.1101 2.825 15.3065 2.975 16.2958 2.975C16.5489 2.975 16.825 2.95 17.1011 2.95V17.05C16.0197 17.05 14.1101 16.8 14.1101 18.525C14.1101 19.525 14.9154 20 15.7436 20H18.0904C19.3098 20 20 19.25 20 17.925Z" fill="currentColor"/>
</svg>
);
};

View File

@@ -0,0 +1,33 @@
export function ArrowKeysIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
className={props.className}
width="62"
height="14"
viewBox="0 0 62 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="14" height="14" rx="1.53846" fill="currentColor" />
<path
d="M6.60956 4.48804C6.80972 4.23784 7.19026 4.23784 7.39043 4.48804L10.3501 8.18765C10.612 8.51503 10.3789 9 9.95969 9H4.04031C3.62106 9 3.38797 8.51503 3.64988 8.18765L6.60956 4.48804Z"
fill="black"
/>
<rect x="16" width="14" height="14" rx="1.53846" fill="currentColor" />
<path
d="M23.3904 9.51196C23.1903 9.76216 22.8097 9.76216 22.6096 9.51196L19.6499 5.81235C19.388 5.48496 19.6211 5 20.0403 5L25.9597 5C26.3789 5 26.612 5.48497 26.3501 5.81235L23.3904 9.51196Z"
fill="black"
/>
<rect x="32" width="14" height="14" rx="1.53846" fill="currentColor" />
<path
d="M36.488 7.39044C36.2378 7.19028 36.2378 6.80974 36.488 6.60957L40.1877 3.64988C40.515 3.38797 41 3.62106 41 4.04031L41 9.95969C41 10.3789 40.515 10.612 40.1877 10.3501L36.488 7.39044Z"
fill="black"
/>
<rect x="48" width="14" height="14" rx="1.53846" fill="currentColor" />
<path
d="M57.512 6.60956C57.7622 6.80972 57.7622 7.19026 57.512 7.39043L53.8123 10.3501C53.485 10.612 53 10.3789 53 9.95969L53 4.04031C53 3.62106 53.485 3.38797 53.8123 3.64988L57.512 6.60956Z"
fill="black"
/>
</svg>
);
}

View File

@@ -0,0 +1,23 @@
export function ArrowKeysUpDownIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
className={props.className}
width="28"
height="14"
viewBox="0 0 30 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="14" height="14" rx="1.53846" fill="currentColor" />
<path
d="M6.60956 4.48804C6.80972 4.23784 7.19026 4.23784 7.39043 4.48804L10.3501 8.18765C10.612 8.51503 10.3789 9 9.95969 9H4.04031C3.62106 9 3.38797 8.51503 3.64988 8.18765L6.60956 4.48804Z"
fill="black"
/>
<rect x="16" width="14" height="14" rx="1.53846" fill="currentColor" />
<path
d="M23.3904 9.51196C23.1903 9.76216 22.8097 9.76216 22.6096 9.51196L19.6499 5.81235C19.388 5.48496 19.6211 5 20.0403 5L25.9597 5C26.3789 5 26.612 5.48497 26.3501 5.81235L23.3904 9.51196Z"
fill="black"
/>
</svg>
);
}

View File

@@ -0,0 +1,23 @@
export function CopyShortcutIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
className={props.className}
width="30"
height="14"
viewBox="0 0 30 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="14" height="14" rx="1.53846" fill="currentColor" />
<rect x="16" width="14" height="14" rx="1.53846" fill="currentColor" />
<path
d="M5.64,10.22H8.25V7a.39.39,0,0,1,.38-.39H10l-3-3L4,6.65H5.26A.39.39,0,0,1,5.64,7v3.18Zm3,.78H5.26a.38.38,0,0,1-.39-.39V7.43H3.11a.39.39,0,0,1-.28-.66L6.72,2.86a.39.39,0,0,1,.55,0l3.88,3.91a.38.38,0,0,1-.27.66H9v3.18a.38.38,0,0,1-.39.39Z"
stroke="#0f172a" strokeWidth="0.35px" fill="#0f172a"
/>
<path
d="M23.81,9.52a1.66,1.66,0,0,0,.53-.27,1.57,1.57,0,0,0,.35-.42,1.07,1.07,0,0,0,.13-.53h1.6a2.26,2.26,0,0,1-.25,1,2.87,2.87,0,0,1-.7.85,3.35,3.35,0,0,1-1,.58,3.56,3.56,0,0,1-1.22.21,3.73,3.73,0,0,1-1.55-.31,3.2,3.2,0,0,1-1.11-.84,3.61,3.61,0,0,1-.67-1.22,4.91,4.91,0,0,1-.23-1.5V6.88a4.91,4.91,0,0,1,.23-1.5,3.54,3.54,0,0,1,.67-1.22,3.33,3.33,0,0,1,1.11-.84,3.94,3.94,0,0,1,2.83-.09,3,3,0,0,1,1,.59,2.66,2.66,0,0,1,.67.92,2.94,2.94,0,0,1,.23,1.17h-1.6a1.48,1.48,0,0,0-.45-1.07,1.65,1.65,0,0,0-.52-.33,1.75,1.75,0,0,0-.65-.12,1.59,1.59,0,0,0-1.44.78,2.27,2.27,0,0,0-.31.8,4.53,4.53,0,0,0-.09.91v.24a4.63,4.63,0,0,0,.09.92,2.46,2.46,0,0,0,.3.8,1.67,1.67,0,0,0,.56.56,1.63,1.63,0,0,0,.89.22A2.05,2.05,0,0,0,23.81,9.52Z"
fill="#0f172a"
/>
</svg>
);
}

View File

@@ -0,0 +1,18 @@
export function DiscordIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
className={props.className}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="12" cy="12" r="12" fill="#F8FAFC" />
<path
d="M18.0881 7.3374C18.0116 7.27279 17.9402 7.2032 17.8637 7.14356C17.554 6.88097 17.2269 6.63856 16.8846 6.41792C16.4342 6.13677 15.9516 5.90824 15.4464 5.73702C15.0844 5.61277 14.7172 5.51835 14.35 5.40901C14.2837 5.40901 14.2786 5.38414 14.3092 5.3245C14.3398 5.26485 14.4061 5.14558 14.4469 5.05115C14.4538 5.03366 14.4667 5.0191 14.4835 5.01001C14.5003 5.00092 14.5198 4.99789 14.5387 5.00146C14.809 5.04619 15.0844 5.07601 15.3547 5.13069C15.8281 5.229 16.2896 5.3756 16.7316 5.56805C17.1998 5.76225 17.6502 5.99501 18.0779 6.26385C18.2267 6.353 18.3697 6.45094 18.5063 6.5571C18.5891 6.62989 18.6566 6.71764 18.7051 6.81552C19.1108 7.51363 19.4521 8.24546 19.7251 9.00236C20.1066 10.0234 20.3983 11.0742 20.5971 12.1435C20.7042 12.715 20.7909 13.2866 20.8674 13.8631C20.9184 14.216 20.9388 14.5788 20.9745 14.9366C20.9745 15.0559 20.9745 15.1702 21 15.2895C21 15.3164 20.9911 15.3425 20.9745 15.3641C20.462 15.9257 19.8549 16.398 19.1794 16.7606C18.5379 17.1017 17.8516 17.3558 17.1395 17.5161C16.7511 17.6096 16.3554 17.6711 15.9564 17.7H15.7116C15.701 17.7002 15.6904 17.6981 15.6807 17.6938C15.671 17.6895 15.6624 17.6831 15.6555 17.6752C15.4413 17.4068 15.2323 17.1334 15.0232 16.8551V16.8253C16.3606 16.3823 17.5548 15.6041 18.4859 14.5689C18.3788 14.6434 18.2819 14.718 18.1748 14.7826C17.8739 14.9665 17.5781 15.1504 17.267 15.3193C16.7354 15.61 16.1728 15.8433 15.5892 16.0151C14.6422 16.3069 13.6595 16.474 12.6671 16.5121H12.3713H11.8155C11.4011 16.5146 10.9871 16.4897 10.5762 16.4376C10.1887 16.3879 9.80109 16.3332 9.41351 16.2636C8.86661 16.1567 8.33068 16.002 7.81221 15.8014C7.15233 15.5479 6.523 15.2246 5.93553 14.8372L5.55306 14.5788C6.01711 15.0934 6.54864 15.5462 7.13396 15.9257C7.72153 16.3044 8.35541 16.6099 9.02084 16.8352L8.98514 16.8899L8.39358 17.6553C8.38145 17.6729 8.36453 17.6868 8.34472 17.6956C8.3249 17.7044 8.30298 17.7076 8.28138 17.705C7.93875 17.691 7.59775 17.6511 7.26145 17.5857C6.76756 17.4952 6.28289 17.3621 5.81314 17.1881C5.27458 16.9934 4.76114 16.7382 4.28323 16.4277C3.86783 16.1551 3.48621 15.8365 3.14601 15.4784C3.14601 15.4784 3.12051 15.4386 3.10011 15.4287C3.06012 15.3983 3.03012 15.3571 3.01381 15.3103C2.9975 15.2635 2.99559 15.2131 3.00831 15.1653L3.05421 14.6335C3.0899 14.2856 3.1205 13.9426 3.1664 13.5947C3.2123 13.2468 3.28879 12.7647 3.36529 12.3472C3.51174 11.5311 3.7093 10.7244 3.95685 9.93177C4.16738 9.2543 4.42116 8.59033 4.71671 7.94373C4.91624 7.50667 5.14275 7.08178 5.39497 6.6714C5.46939 6.5728 5.56514 6.49137 5.67544 6.43284C6.1388 6.11857 6.63239 5.84893 7.14925 5.62769C7.71444 5.38251 8.30641 5.20075 8.91375 5.08594L9.47981 5.00643C9.49599 5.00328 9.51279 5.00611 9.52694 5.01438C9.54108 5.02265 9.55155 5.03575 9.55631 5.05115L9.7042 5.33942C9.7297 5.38415 9.7042 5.39907 9.6685 5.40901C9.41351 5.47859 9.15854 5.54319 8.90865 5.61774C8.45618 5.75584 8.01886 5.93729 7.60313 6.15946C7.24627 6.34465 6.9052 6.5574 6.58319 6.79565C6.3588 6.9696 6.14462 7.14853 5.92533 7.32745C5.9235 7.33135 5.92255 7.33557 5.92255 7.33986C5.92255 7.34415 5.9235 7.3484 5.92533 7.35229L5.99163 7.32248C6.471 7.09882 6.95037 6.86522 7.43994 6.65647C8.00719 6.4106 8.59831 6.22081 9.20443 6.08991C9.61682 5.99062 10.0361 5.92083 10.459 5.88114C10.8414 5.84635 11.2239 5.82649 11.6013 5.80661C11.79 5.80661 11.9787 5.80661 12.1673 5.80661C12.5141 5.80661 12.866 5.8414 13.2128 5.86625C13.8437 5.91322 14.4686 6.01806 15.0793 6.17936C15.6332 6.32264 16.1739 6.51049 16.6959 6.74099L17.9606 7.33243L18.0218 7.36224L18.0881 7.3374ZM9.35232 10.5679C9.08643 10.5761 8.82881 10.66 8.6113 10.8093C8.39378 10.9586 8.2259 11.1667 8.12839 11.4079C7.98657 11.7022 7.93351 12.0296 7.97541 12.3522C8.01397 12.7406 8.19505 13.1024 8.48538 13.371C8.61754 13.5006 8.77761 13.6 8.95401 13.6619C9.13041 13.7238 9.31872 13.7467 9.50531 13.7289C9.68475 13.7178 9.85988 13.6705 10.0196 13.5901C10.1794 13.5097 10.3203 13.3979 10.4335 13.2617C10.7252 12.9245 10.8682 12.4886 10.8312 12.049C10.8196 11.7253 10.7096 11.4122 10.515 11.1494C10.3862 10.9659 10.2123 10.8166 10.0093 10.7151C9.80628 10.6135 9.58046 10.563 9.35232 10.5679ZM16.1094 12.1733C16.1148 11.8593 16.0319 11.55 15.8697 11.2787C15.7548 11.0583 15.5775 10.8747 15.3587 10.7496C15.14 10.6245 14.889 10.5632 14.6356 10.5729C14.451 10.578 14.2698 10.6219 14.1043 10.7017C13.9388 10.7815 13.793 10.8953 13.6769 11.0351C13.5285 11.203 13.4159 11.398 13.3459 11.6088C13.2758 11.8196 13.2496 12.0419 13.2689 12.2627C13.2861 12.6947 13.4787 13.1023 13.8043 13.3959C13.9417 13.5243 14.1072 13.6205 14.2883 13.6773C14.4694 13.7342 14.6614 13.7501 14.8498 13.7239C15.1962 13.6764 15.5095 13.4978 15.7218 13.2269C15.9694 12.9284 16.106 12.5571 16.1094 12.1733Z"
fill="#4338CA"
/>
</svg>
);
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,13 @@
export function EmailIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#4338CA"/>
<path d="M12 0C5.37251 0 0 5.37251 0 12C0 18.6275 5.37251 24 12 24C18.6275 24 24 18.6275 24 12C24 5.37251 18.6275 0 12 0ZM4.02864 7.38421C4.29607 6.6387 4.79768 6.14156 5.58914 5.98842C5.87824 5.9324 6.17928 5.91671 6.47473 5.91596C8.32732 5.90961 10.1799 5.91297 12.0329 5.91297C13.9553 5.91297 15.8781 5.90588 17.8005 5.91596C18.8852 5.92156 19.6614 6.44821 19.9732 7.33155C20.1017 7.69534 20.0629 7.93327 19.6685 8.12674C17.3475 9.26668 15.0508 10.4567 12.7134 11.5611C12.3287 11.743 11.709 11.7397 11.3232 11.5574C8.97087 10.4451 6.65774 9.24988 4.32296 8.09948C3.97634 7.92878 3.9136 7.70468 4.02864 7.38384V7.38421ZM20.0935 15.7821C20.089 17.1977 19.2277 18.0788 17.808 18.0825C13.9467 18.0926 10.085 18.0923 6.22373 18.0825C4.83466 18.0792 3.94572 17.2534 3.92032 15.9031C3.88708 14.1223 3.90986 12.3399 3.91397 10.5586C3.91397 10.4279 3.95879 10.2972 3.99502 10.0966C4.23406 10.2019 4.42268 10.2759 4.60346 10.3659C6.91658 11.52 9.23195 12.6708 11.5376 13.8395C11.8685 14.0072 12.1311 14.0083 12.4624 13.8403C14.7677 12.6716 17.0827 11.5212 19.3954 10.367C19.5777 10.2763 19.7671 10.1993 20.058 10.069C20.0741 10.3752 20.0924 10.5635 20.0928 10.7514C20.095 12.428 20.098 14.1051 20.0928 15.7817L20.0935 15.7821Z" fill="white"/>
</svg>
);
}

View File

@@ -0,0 +1,12 @@
export function EmailIconTransparent(props: React.SVGProps<SVGSVGElement>) {
return (
<svg width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M12 0C5.37251 0 0 5.37251 0 12C0 18.6275 5.37251 24 12 24C18.6275 24 24 18.6275 24 12C24 5.37251 18.6275 0 12 0ZM4.02864 7.38421C4.29607 6.6387 4.79768 6.14156 5.58914 5.98842C5.87824 5.9324 6.17928 5.91671 6.47473 5.91596C8.32732 5.90961 10.1799 5.91297 12.0329 5.91297C13.9553 5.91297 15.8781 5.90588 17.8005 5.91596C18.8852 5.92156 19.6614 6.44821 19.9732 7.33155C20.1017 7.69534 20.0629 7.93327 19.6685 8.12674C17.3475 9.26668 15.0508 10.4567 12.7134 11.5611C12.3287 11.743 11.709 11.7397 11.3232 11.5574C8.97087 10.4451 6.65774 9.24988 4.32296 8.09948C3.97634 7.92878 3.9136 7.70468 4.02864 7.38384V7.38421ZM20.0935 15.7821C20.089 17.1977 19.2277 18.0788 17.808 18.0825C13.9467 18.0926 10.085 18.0923 6.22373 18.0825C4.83466 18.0792 3.94572 17.2534 3.92032 15.9031C3.88708 14.1223 3.90986 12.3399 3.91397 10.5586C3.91397 10.4279 3.95879 10.2972 3.99502 10.0966C4.23406 10.2019 4.42268 10.2759 4.60346 10.3659C6.91658 11.52 9.23195 12.6708 11.5376 13.8395C11.8685 14.0072 12.1311 14.0083 12.4624 13.8403C14.7677 12.6716 17.0827 11.5212 19.3954 10.367C19.5777 10.2763 19.7671 10.1993 20.058 10.069C20.0741 10.3752 20.0924 10.5635 20.0928 10.7514C20.095 12.428 20.098 14.1051 20.0928 15.7817L20.0935 15.7821Z" fill="white"/>
</svg>
);
}

View File

@@ -0,0 +1,26 @@
export function EscapeKeyIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
className={props.className}
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="14" height="14" rx="1.53846" fill="currentColor" />
<path
d="M3.21695 10C2.79876 10 2.42068 9.88168 2.08269 9.64504C1.75044 9.4084 1.48693 9.0687 1.29216 8.62595C1.09739 8.17557 1 7.63359 1 7C1 6.38168 1.09739 5.85114 1.29216 5.4084C1.49265 4.95802 1.75044 4.61069 2.06551 4.36641C2.38058 4.12214 2.71283 4 3.06228 4C3.48619 4 3.83563 4.12595 4.1106 4.37786C4.3913 4.62214 4.59753 4.9542 4.72928 5.37405C4.86677 5.79389 4.93551 6.25954 4.93551 6.77099C4.93551 6.93893 4.92692 7.10305 4.90973 7.26336C4.89827 7.41603 4.88682 7.52672 4.87536 7.59542H2.42641C2.49515 7.93893 2.61831 8.17939 2.7959 8.31679C2.97348 8.44656 3.18257 8.51145 3.42317 8.51145C3.69814 8.51145 3.9903 8.39695 4.29964 8.16794L4.78084 9.33588C4.5517 9.54962 4.29391 9.71374 4.00749 9.82824C3.72106 9.94275 3.45754 10 3.21695 10ZM2.40922 6.31298H3.68096C3.68096 6.0916 3.638 5.90076 3.55207 5.74046C3.47187 5.57252 3.32006 5.48855 3.09665 5.48855C2.93625 5.48855 2.79303 5.55344 2.66701 5.68321C2.54098 5.81298 2.45505 6.0229 2.40922 6.31298Z"
fill="#0F172A"
/>
<path
d="M7.06968 10C6.79471 10 6.50256 9.92748 6.19322 9.78244C5.8896 9.62977 5.62609 9.43511 5.40268 9.19847L6.05573 7.98473C6.44527 8.36641 6.79471 8.55725 7.10405 8.55725C7.25872 8.55725 7.36757 8.53053 7.43058 8.4771C7.49932 8.41603 7.53369 8.32824 7.53369 8.21374C7.53369 8.06107 7.45063 7.94275 7.2845 7.85878C7.11838 7.76718 6.92647 7.66412 6.70879 7.54962C6.54266 7.45801 6.37653 7.34351 6.2104 7.20611C6.05 7.06107 5.91538 6.87786 5.80654 6.65649C5.6977 6.43511 5.64327 6.16794 5.64327 5.85496C5.64327 5.29008 5.80081 4.83969 6.11588 4.50382C6.43095 4.16794 6.84054 4 7.34465 4C7.69982 4 8.00344 4.08015 8.25549 4.24046C8.51328 4.39313 8.73669 4.56489 8.92573 4.75573L8.27268 5.92366C8.11801 5.77099 7.9662 5.65267 7.81726 5.5687C7.66832 5.48473 7.52797 5.44275 7.39621 5.44275C7.14415 5.44275 7.01813 5.54962 7.01813 5.76336C7.01813 5.9084 7.09546 6.0229 7.25013 6.10687C7.41053 6.18321 7.59671 6.27481 7.80866 6.38168C7.98052 6.46565 8.14951 6.57634 8.31564 6.71374C8.4875 6.85115 8.62785 7.03054 8.73669 7.25191C8.85126 7.47328 8.90855 7.75573 8.90855 8.09924C8.90855 8.63359 8.75101 9.08397 8.43594 9.45038C8.1266 9.81679 7.67118 10 7.06968 10Z"
fill="#0F172A"
/>
<path
d="M11.5736 10C11.1669 10 10.8002 9.88168 10.4737 9.64504C10.1472 9.4084 9.88654 9.0687 9.69177 8.62595C9.50273 8.17557 9.4082 7.63359 9.4082 7C9.4082 6.36641 9.51418 5.82825 9.72614 5.3855C9.94382 4.93512 10.2274 4.5916 10.5768 4.35496C10.9263 4.11832 11.3044 4 11.7111 4C11.9689 4 12.2009 4.05343 12.4071 4.16031C12.6191 4.26718 12.8052 4.41221 12.9656 4.59542L12.2782 5.85496C12.1865 5.74809 12.1035 5.67557 12.029 5.6374C11.9545 5.59924 11.8772 5.58015 11.797 5.58015C11.522 5.58015 11.3072 5.70992 11.1525 5.96947C10.9979 6.22137 10.9205 6.56489 10.9205 7C10.9205 7.43511 11.0007 7.78244 11.1611 8.04198C11.3215 8.29389 11.5163 8.41985 11.7455 8.41985C11.8657 8.41985 11.9832 8.3855 12.0978 8.31679C12.2181 8.24046 12.3298 8.15267 12.4329 8.05344L13 9.33588C12.788 9.58779 12.5532 9.76336 12.2954 9.8626C12.0376 9.9542 11.797 10 11.5736 10Z"
fill="#0F172A"
/>
</svg>
);
}

View File

@@ -0,0 +1,18 @@
export function GithubIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
className={props.className}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="12" cy="12" r="12" fill="#F8FAFC" />
<path
d="M21 12.0523C20.9915 12.1541 20.982 12.2565 20.9741 12.3588C20.8837 13.6147 20.5087 14.8358 19.8759 15.935C19.1892 17.1392 18.2177 18.1681 17.0412 18.9371C16.376 19.3775 15.6509 19.7259 14.8867 19.9725C14.6521 20.0485 14.4508 19.9604 14.2906 19.7886C14.1508 19.642 14.0747 19.4487 14.0779 19.249C14.0779 18.3672 14.0779 17.4853 14.0779 16.6031C14.0863 16.3681 14.0628 16.1329 14.008 15.9038C13.9792 15.7993 13.9324 15.6997 13.8918 15.5924C14.0311 15.5634 14.1766 15.5377 14.3216 15.5032C15.1958 15.3068 16.0136 14.9915 16.7231 14.4432C17.5308 13.8177 18.0294 13.0111 18.176 12.0157C18.3187 11.0531 18.2037 10.1195 17.7728 9.233C17.6063 8.89501 17.3874 8.58384 17.1236 8.31036L17.0903 8.2737C17.3809 7.63017 17.4174 6.90541 17.193 6.23744C17.1338 6.05812 17.0463 5.88882 16.9335 5.73562C16.9232 5.71873 16.9081 5.70507 16.89 5.69624C16.8719 5.68742 16.8516 5.6838 16.8314 5.68583C16.1357 5.70242 15.4598 5.91436 14.8856 6.29599C14.6578 6.44107 14.4484 6.61171 14.2618 6.80437C14.253 6.8169 14.2399 6.82597 14.2248 6.82998C14.2097 6.83399 14.1936 6.83267 14.1795 6.82626C13.6814 6.65717 13.1643 6.58603 12.6409 6.54936C12.1713 6.51543 11.6997 6.51927 11.2308 6.56085C10.7502 6.59484 10.2762 6.68958 9.82082 6.84268C9.80583 6.84985 9.78867 6.85153 9.77251 6.84741C9.75634 6.84328 9.74225 6.83364 9.73283 6.82024C9.20278 6.26705 8.50548 5.89131 7.74132 5.74712C7.55238 5.711 7.35723 5.70771 7.16491 5.68801C7.14583 5.6851 7.1263 5.68795 7.10894 5.69617C7.09159 5.7044 7.07726 5.71759 7.0679 5.73398C6.87448 6.00312 6.75032 6.3133 6.70581 6.63856C6.63122 7.07823 6.66761 7.52889 6.81184 7.95192C6.85301 8.06958 6.91505 8.18012 6.97145 8.3027C6.90772 8.37329 6.83158 8.45374 6.75939 8.53746C6.25302 9.13009 5.92997 9.84975 5.82765 10.6131C5.72834 11.2252 5.75886 11.8505 5.91733 12.4507C6.1621 13.3378 6.6872 14.0377 7.44973 14.5713C8.04589 14.9899 8.71085 15.2624 9.41924 15.4408C9.65331 15.4999 9.88963 15.5503 10.1231 15.6012C10.0859 15.7046 10.0408 15.8086 10.0103 15.9175C9.94909 16.1717 9.92142 16.4324 9.92798 16.6934C9.93155 16.7171 9.92539 16.7411 9.91082 16.7604C9.89625 16.7796 9.87445 16.7925 9.85015 16.7963C9.51067 16.9011 9.15221 16.935 8.79827 16.8959C8.46398 16.8686 8.14004 16.7699 7.84961 16.607C7.58186 16.4549 7.35663 16.2415 7.19367 15.9853C6.92069 15.5618 6.54507 15.269 6.03915 15.1481C5.87828 15.104 5.70835 15.1016 5.54621 15.1409C5.51726 15.1481 5.48911 15.158 5.46217 15.1705C5.38152 15.2104 5.35557 15.2756 5.40577 15.3472C5.45449 15.4143 5.5113 15.4755 5.57497 15.5295C5.70582 15.6389 5.85359 15.7374 5.97429 15.8578C6.24219 16.126 6.4255 16.4505 6.5727 16.793C6.79548 17.3091 7.18352 17.6686 7.68661 17.9209C8.20945 18.1819 8.77007 18.2443 9.34762 18.1945C9.53769 18.1775 9.72663 18.1453 9.91896 18.1201C9.91896 18.1305 9.92234 18.1458 9.92234 18.1606C9.92234 18.5321 9.92572 18.9037 9.92234 19.2753C9.92243 19.3969 9.89197 19.5168 9.8336 19.6244C9.77522 19.7321 9.69069 19.8243 9.58732 19.8931C9.51652 19.946 9.4329 19.9803 9.34451 19.9927C9.25611 20.0052 9.1659 19.9954 9.08253 19.9643C6.95171 19.2321 5.31609 17.9187 4.17567 16.0242C3.63235 15.1126 3.27001 14.1103 3.10744 13.0691C2.81135 11.2274 3.13103 9.3421 4.01965 7.68951C4.90826 6.03693 6.31908 4.70396 8.04532 3.88597C8.87822 3.48473 9.7727 3.21731 10.6939 3.09412C10.9951 3.05418 11.2991 3.0394 11.602 3.00985C11.6251 3.00985 11.6471 3.00328 11.6702 3H12.3346C12.3572 3.00328 12.3792 3.00766 12.4023 3.00985C12.6685 3.03283 12.9353 3.04761 13.2003 3.0799C14.0888 3.18707 14.9544 3.42898 15.7655 3.79677C17.2635 4.46177 18.5425 5.5162 19.4608 6.84323C20.3464 8.10221 20.8692 9.56789 20.9752 11.0887C20.9831 11.1905 20.9914 11.2926 21 11.3951V12.0523Z"
fill="#4338CA"
/>
</svg>
);
}

View File

@@ -0,0 +1,30 @@
export type IconProps = {
className?: string;
};
export function GithubIconSimple({ className }: IconProps) {
return (
<svg
className={className}
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_571_3822)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 0C5.37017 0 0 5.50708 0 12.306C0 17.745 3.44015 22.3532 8.20626 23.9849C8.80295 24.0982 9.02394 23.7205 9.02394 23.3881C9.02394 23.0935 9.01657 22.3229 9.00921 21.2956C5.67219 22.0359 4.96501 19.6487 4.96501 19.6487C4.41989 18.2285 3.63168 17.8508 3.63168 17.8508C2.54144 17.0878 3.71271 17.1029 3.71271 17.1029C4.91344 17.1936 5.55433 18.372 5.55433 18.372C6.62247 20.2531 8.36096 19.7092 9.04604 19.3919C9.15654 18.5987 9.46593 18.0548 9.80479 17.745C7.13812 17.4353 4.33886 16.3777 4.33886 11.6638C4.33886 10.3192 4.80295 9.2238 5.57643 8.36261C5.4512 8.05288 5.03867 6.79887 5.69429 5.1067C5.69429 5.1067 6.7035 4.77432 8.99447 6.36827C9.95212 6.09632 10.9761 5.96034 12 5.95279C13.0166 5.95279 14.0479 6.09632 15.0055 6.36827C17.2965 4.77432 18.3057 5.1067 18.3057 5.1067C18.9613 6.79887 18.5488 8.05288 18.4236 8.36261C19.1897 9.2238 19.6538 10.3192 19.6538 11.6638C19.6538 16.3928 16.8471 17.4278 14.1731 17.7375C14.6004 18.1152 14.9908 18.8706 14.9908 20.0189C14.9908 21.6657 14.9761 22.9877 14.9761 23.3957C14.9761 23.728 15.1897 24.1058 15.8011 23.9849C20.5672 22.3532 24 17.745 24 12.3135C24 5.50708 18.6298 0 12 0Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_571_3822">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
);
}

View File

@@ -0,0 +1,27 @@
export function LoadingIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
className={props.className}
xmlns="http://www.w3.org/2000/svg"
width="26"
height="26"
viewBox="0 0 26 26"
fill="none"
>
<circle
cx="13"
cy="13"
r="10"
stroke="black"
strokeOpacity="0.3"
strokeWidth="4"
/>
<path
d="M13 23C7.47715 23 3 18.5228 3 13"
stroke="#4338CA"
strokeWidth="4"
strokeLinecap="round"
/>
</svg>
);
}

View File

@@ -0,0 +1,55 @@
import { Link } from "remix";
export function Logo({
className,
width = "100%",
}: {
className?: string;
width?: string;
}) {
return (
<Link to="/" aria-label="JSON Hero homepage" className="w-40">
<svg
className={className}
width={width}
height="50"
viewBox="0 0 263 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M94.8087 35.3033V1.39929H102.661L111.501 18.2473L114.829 25.7353H115.037C114.898 23.9326 114.707 21.922 114.465 19.7033C114.222 17.4846 114.101 15.37 114.101 13.3593V1.39929H121.381V35.3033H113.529L104.689 18.4033L101.361 11.0193H101.153C101.326 12.8913 101.517 14.902 101.725 17.0513C101.967 19.2006 102.089 21.2806 102.089 23.2913V35.3033H94.8087Z"
fill="white"
/>
<path
d="M73.0419 35.9273C69.9912 35.9273 67.3045 35.2166 64.9819 33.7953C62.6939 32.3739 60.8912 30.3459 59.5739 27.7113C58.2912 25.0419 57.6499 21.8699 57.6499 18.1953C57.6499 14.4859 58.2912 11.3486 59.5739 8.78327C60.8912 6.18327 62.6939 4.20727 64.9819 2.85527C67.3045 1.4686 69.9912 0.775269 73.0419 0.775269C76.0925 0.775269 78.7619 1.4686 81.0499 2.85527C83.3725 4.20727 85.1752 6.18327 86.4579 8.78327C87.7752 11.3833 88.4339 14.5206 88.4339 18.1953C88.4339 21.8699 87.7752 25.0419 86.4579 27.7113C85.1752 30.3459 83.3725 32.3739 81.0499 33.7953C78.7619 35.2166 76.0925 35.9273 73.0419 35.9273ZM73.0419 29.3233C75.3645 29.3233 77.2019 28.3179 78.5539 26.3073C79.9059 24.2966 80.5819 21.5926 80.5819 18.1953C80.5819 14.7979 79.9059 12.1459 78.5539 10.2393C77.2019 8.3326 75.3645 7.37927 73.0419 7.37927C70.7192 7.37927 68.8819 8.3326 67.5299 10.2393C66.1779 12.1459 65.5019 14.7979 65.5019 18.1953C65.5019 21.5926 66.1779 24.2966 67.5299 26.3073C68.8819 28.3179 70.7192 29.3233 73.0419 29.3233Z"
fill="white"
/>
<path
d="M40.7154 35.9273C38.4967 35.9273 36.278 35.5113 34.0593 34.6793C31.8753 33.8473 29.9167 32.6339 28.1833 31.0393L32.5513 25.7873C33.7647 26.8273 35.1167 27.6766 36.6073 28.3353C38.098 28.9939 39.5367 29.3233 40.9234 29.3233C42.518 29.3233 43.6967 29.0286 44.4594 28.4393C45.2567 27.8499 45.6553 27.0526 45.6553 26.0473C45.6553 24.9726 45.2047 24.1926 44.3034 23.7073C43.4367 23.1873 42.258 22.6153 40.7673 21.9913L36.3474 20.1193C35.2034 19.6339 34.1114 18.9926 33.0714 18.1953C32.0314 17.3633 31.182 16.3406 30.5233 15.1273C29.8647 13.9139 29.5354 12.4926 29.5354 10.8633C29.5354 8.99127 30.038 7.2926 31.0434 5.76727C32.0834 4.24193 33.5047 3.0286 35.3074 2.12727C37.1447 1.22594 39.242 0.775269 41.5993 0.775269C43.5407 0.775269 45.482 1.1566 47.4234 1.91927C49.3647 2.68194 51.0634 3.79127 52.5194 5.24727L48.6194 10.0833C47.51 9.2166 46.4007 8.55794 45.2914 8.10727C44.182 7.62194 42.9514 7.37927 41.5993 7.37927C40.282 7.37927 39.2247 7.6566 38.4273 8.21127C37.6647 8.73127 37.2834 9.4766 37.2834 10.4473C37.2834 11.4873 37.7687 12.2673 38.7393 12.7873C39.7447 13.3073 40.9754 13.8619 42.4314 14.4513L46.7994 16.2193C48.8447 17.0513 50.474 18.1953 51.6874 19.6513C52.9007 21.1073 53.5074 23.0313 53.5074 25.4233C53.5074 27.2953 53.0047 29.0286 51.9994 30.6233C50.994 32.2179 49.538 33.5006 47.6314 34.4713C45.7247 35.4419 43.4194 35.9273 40.7154 35.9273Z"
fill="white"
/>
<path
d="M11.6583 35.9273C9.09298 35.9273 6.92631 35.4246 5.15831 34.4193C3.39031 33.3793 1.91698 31.8366 0.738312 29.7913L5.93831 25.9433C6.56231 27.0873 7.29031 27.9366 8.12231 28.4913C8.95431 29.046 9.80365 29.3233 10.6703 29.3233C12.057 29.3233 13.097 28.9073 13.7903 28.0753C14.5183 27.2086 14.8823 25.6486 14.8823 23.3953V1.39929H22.5263V24.0193C22.5263 26.2033 22.145 28.1966 21.3823 29.9993C20.6196 31.802 19.4236 33.2406 17.7943 34.3153C16.1996 35.39 14.1543 35.9273 11.6583 35.9273Z"
fill="white"
/>
<path
d="M247.108 35.9273C244.058 35.9273 241.371 35.2166 239.048 33.7953C236.76 32.3739 234.958 30.3459 233.64 27.7113C232.358 25.0419 231.716 21.8699 231.716 18.1953C231.716 14.4859 232.358 11.3486 233.64 8.78327C234.958 6.18327 236.76 4.20727 239.048 2.85527C241.371 1.4686 244.058 0.775269 247.108 0.775269C250.159 0.775269 252.828 1.4686 255.116 2.85527C257.439 4.20727 259.242 6.18327 260.524 8.78327C261.842 11.3833 262.5 14.5206 262.5 18.1953C262.5 21.8699 261.842 25.0419 260.524 27.7113C259.242 30.3459 257.439 32.3739 255.116 33.7953C252.828 35.2166 250.159 35.9273 247.108 35.9273ZM247.108 29.3233C249.431 29.3233 251.268 28.3179 252.62 26.3073C253.972 24.2966 254.648 21.5926 254.648 18.1953C254.648 14.7979 253.972 12.1459 252.62 10.2393C251.268 8.3326 249.431 7.37927 247.108 7.37927C244.786 7.37927 242.948 8.3326 241.596 10.2393C240.244 12.1459 239.568 14.7979 239.568 18.1953C239.568 21.5926 240.244 24.2966 241.596 26.3073C242.948 28.3179 244.786 29.3233 247.108 29.3233Z"
fill="#BFF164"
/>
<path
d="M201.438 35.3033V1.39929H213.658C216.05 1.39929 218.234 1.72863 220.21 2.38729C222.186 3.01129 223.763 4.08596 224.942 5.61129C226.12 7.13663 226.71 9.25129 226.71 11.9553C226.71 14.4513 226.155 16.514 225.046 18.1433C223.971 19.738 222.515 20.934 220.678 21.7313L228.374 35.3033H219.794L213.294 23.0833H209.082V35.3033H201.438ZM209.082 16.9993H213.034C215.044 16.9993 216.57 16.5833 217.61 15.7513C218.684 14.8846 219.222 13.6193 219.222 11.9553C219.222 10.2913 218.684 9.12996 217.61 8.47129C216.57 7.81263 215.044 7.48329 213.034 7.48329H209.082V16.9993Z"
fill="#BFF164"
/>
<path
d="M172.949 35.3033V1.39929H194.165V7.84729H180.593V14.6593H192.137V21.0553H180.593V28.8553H194.685V35.3033H172.949Z"
fill="#BFF164"
/>
<path
d="M137.91 35.3033V1.39929H145.554V14.4513H157.254V1.39929H164.95V35.3033H157.254V21.1593H145.554V35.3033H137.91Z"
fill="#BFF164"
/>
</svg>
</Link>
);
}

View File

@@ -0,0 +1,70 @@
export function LogoTriggerdotdev({
className,
width = "100%",
}: {
className?: string;
width?: string;
}) {
return (
<a href="https://trigger.dev/" aria-label="Trigger.dev">
<svg
className={`${className}`}
width={width}
height="30"
viewBox="0 0 169 30"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M44.0084 4.04088H30.6671H31.1941V7.67807H35.686V23.329H39.489V7.67807H44.0084V4.04088Z"
fill="#E2E8F0"
/>
<path
d="M47.646 11.9215V9.55178H44.0911V23.329H47.646V16.7435C47.646 13.8503 49.9884 13.0236 51.8348 13.2441V9.27623C50.0986 9.27623 48.3625 10.0478 47.646 11.9215Z"
fill="#E2E8F0"
/>
<path
d="M55.6379 7.89851C56.8505 7.89851 57.8426 6.90655 57.8426 5.7217C57.8426 4.53686 56.8505 3.51733 55.6379 3.51733C54.453 3.51733 53.4609 4.53686 53.4609 5.7217C53.4609 6.90655 54.453 7.89851 55.6379 7.89851ZM53.8743 23.329H57.4292V9.55178H53.8743V23.329Z"
fill="#E2E8F0"
/>
<path
d="M70.9327 9.55179V11.2602C69.9681 9.96509 68.48 9.16603 66.5234 9.16603C62.6103 9.16603 59.6616 12.3623 59.6616 16.22C59.6616 20.1052 62.6103 23.2739 66.5234 23.2739C68.48 23.2739 69.9681 22.4749 70.9327 21.1798V22.6677C70.9327 24.8445 69.5548 26.0569 67.3226 26.0569C65.2007 26.0569 64.2913 25.2027 63.7126 24.1281L60.6812 25.864C61.8938 28.096 64.2637 29.2257 67.2124 29.2257C70.85 29.2257 74.4049 27.1867 74.4049 22.6677V9.55179H70.9327ZM67.0746 19.9949C64.8424 19.9949 63.2165 18.4243 63.2165 16.22C63.2165 14.0432 64.8424 12.4726 67.0746 12.4726C69.3068 12.4726 70.9327 14.0432 70.9327 16.22C70.9327 18.4243 69.3068 19.9949 67.0746 19.9949Z"
fill="#E2E8F0"
/>
<path
d="M87.8808 9.55179V11.2602C86.9163 9.96509 85.4282 9.16603 83.4716 9.16603C79.5584 9.16603 76.6097 12.3623 76.6097 16.22C76.6097 20.1052 79.5584 23.2739 83.4716 23.2739C85.4282 23.2739 86.9163 22.4749 87.8808 21.1798V22.6677C87.8808 24.8445 86.5029 26.0569 84.2708 26.0569C82.1488 26.0569 81.2394 25.2027 80.6607 24.1281L77.6294 25.864C78.8419 28.096 81.2119 29.2257 84.1605 29.2257C87.7981 29.2257 91.3531 27.1867 91.3531 22.6677V9.55179H87.8808ZM84.0227 19.9949C81.7906 19.9949 80.1647 18.4243 80.1647 16.22C80.1647 14.0432 81.7906 12.4726 84.0227 12.4726C86.2549 12.4726 87.8808 14.0432 87.8808 16.22C87.8808 18.4243 86.2549 19.9949 84.0227 19.9949Z"
fill="#E2E8F0"
/>
<path
d="M97.2782 17.9008H107.667C107.75 17.4324 107.805 16.964 107.805 16.4404C107.805 12.3899 104.912 9.16603 100.833 9.16603C96.5066 9.16603 93.5579 12.3348 93.5579 16.4404C93.5579 20.546 96.479 23.7148 101.109 23.7148C103.754 23.7148 105.821 22.6402 107.116 20.7665L104.25 19.1132C103.644 19.9123 102.542 20.4909 101.164 20.4909C99.2899 20.4909 97.7742 19.7194 97.2782 17.9008ZM97.2231 15.1454C97.6364 13.3819 98.9316 12.3623 100.833 12.3623C102.321 12.3623 103.809 13.1614 104.25 15.1454H97.2231Z"
fill="#E2E8F0"
/>
<path
d="M113.468 11.9215V9.55178H109.914V23.329H113.468V16.7435C113.468 13.8503 115.811 13.0236 117.657 13.2441V9.27623C115.921 9.27623 114.185 10.0478 113.468 11.9215Z"
fill="#E2E8F0"
/>
<path
d="M119.008 23.6874C120.303 23.6874 121.35 22.6403 121.35 21.3452C121.35 20.0502 120.303 19.0031 119.008 19.0031C117.712 19.0031 116.665 20.0502 116.665 21.3452C116.665 22.6403 117.712 23.6874 119.008 23.6874Z"
fill="#E2E8F0"
/>
<path
d="M133.944 4.04102V11.1776C132.952 9.91011 131.491 9.16616 129.479 9.16616C125.787 9.16616 122.755 12.3349 122.755 16.4405C122.755 20.5462 125.787 23.7149 129.479 23.7149C131.491 23.7149 132.952 22.9709 133.944 21.7034V23.3292H137.499V4.04102L133.944 4.04102ZM130.141 20.3257C127.936 20.3257 126.31 18.7551 126.31 16.4405C126.31 14.126 127.936 12.5553 130.141 12.5553C132.318 12.5553 133.944 14.126 133.944 16.4405C133.944 18.7551 132.318 20.3257 130.141 20.3257Z"
fill="#E2E8F0"
/>
<path
d="M143.203 17.9009H153.592C153.675 17.4325 153.73 16.9641 153.73 16.4406C153.73 12.39 150.837 9.16617 146.758 9.16617C142.432 9.16617 139.483 12.3349 139.483 16.4406C139.483 20.5462 142.404 23.7149 147.034 23.7149C149.679 23.7149 151.746 22.6403 153.041 20.7666L150.175 19.1133C149.569 19.9124 148.467 20.4911 147.089 20.4911C145.215 20.4911 143.699 19.7195 143.203 17.9009ZM143.148 15.1455C143.561 13.382 144.857 12.3625 146.758 12.3625C148.246 12.3625 149.734 13.1616 150.175 15.1455H143.148Z"
fill="#E2E8F0"
/>
<path
d="M164.45 9.55192L161.088 19.196L157.754 9.55192H153.84L159.076 23.3292H163.127L168.363 9.55192H164.45Z"
fill="#E2E8F0"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.32238 9.89169L13.6403 0.682007L26.8195 23.5069H0.461029L5.77893 14.2969L9.54072 16.4686L7.9849 19.1632H19.2957L13.6403 9.3691L12.0845 12.0637L8.32238 9.89169Z"
fill="#E2E8F0"
/>
</svg>
</a>
);
}

View File

@@ -0,0 +1,29 @@
import { motion } from "framer-motion";
import { transition } from "../../utilities/animationConstants";
export const MoonIcon = () => {
const variants = {
initial: { scale: 0.6, rotate: 90 },
animate: { scale: 1, rotate: 0, transition },
whileTap: { scale: 0.95, rotate: 15 },
};
return (
<motion.svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 50 50"
key="moon"
>
<motion.path
d="M 43.81 29.354 C 43.688 28.958 43.413 28.626 43.046 28.432 C 42.679 28.238 42.251 28.198 41.854 28.321 C 36.161 29.886 30.067 28.272 25.894 24.096 C 21.722 19.92 20.113 13.824 21.683 8.133 C 21.848 7.582 21.697 6.985 21.29 6.578 C 20.884 6.172 20.287 6.022 19.736 6.187 C 10.659 8.728 4.691 17.389 5.55 26.776 C 6.408 36.163 13.847 43.598 23.235 44.451 C 32.622 45.304 41.28 39.332 43.816 30.253 C 43.902 29.96 43.9 29.647 43.81 29.354 Z"
fill="currentColor"
initial="initial"
animate="animate"
whileTap="whileTap"
variants={variants}
/>
</motion.svg>
);
};

View File

@@ -0,0 +1,18 @@
export function ObjectIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
className={props.className}
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
key="object"
>
<path d="M8.63302 22C7.03731 21.9857 5.84052 21.4581 5.04267 20.4171C4.24481 19.3761 3.84589 17.836 3.84589 15.7968C3.84589 15.1408 3.75799 14.6132 3.58219 14.2139C3.40639 13.8004 3.12241 13.4724 2.73024 13.2299L2.5274 13.123C2.33807 13.0232 2.20284 12.9091 2.12171 12.7807C2.04057 12.6524 2 12.4884 2 12.2888V11.6898C2 11.4902 2.04057 11.3262 2.12171 11.1979C2.20284 11.0695 2.33807 10.9554 2.5274 10.8556L2.75052 10.7273C3.14269 10.4848 3.41991 10.164 3.58219 9.76471C3.75799 9.36542 3.84589 8.83779 3.84589 8.18182C3.84589 6.1426 4.24481 4.60963 5.04267 3.58289C5.84052 2.54189 7.03055 2.01426 8.61273 2H8.73444C8.85615 2 8.95757 2.04278 9.03871 2.12834C9.13337 2.2139 9.1807 2.32799 9.1807 2.47059V3.86096C9.1807 3.98931 9.14013 4.10339 9.05899 4.20321C8.97785 4.28877 8.87643 4.33155 8.75473 4.33155H8.67359C8.06505 4.37433 7.59175 4.53832 7.25368 4.82353C6.9156 5.10873 6.67895 5.5508 6.54372 6.14973C6.40849 6.7344 6.34087 7.53298 6.34087 8.54546C6.34087 9.40107 6.20564 10.1283 5.93518 10.7273C5.67825 11.3119 5.31313 11.7326 4.83982 11.9893C5.31313 12.2317 5.67825 12.6595 5.93518 13.2727C6.20564 13.8717 6.34087 14.5918 6.34087 15.4332C6.34087 16.4456 6.40849 17.2513 6.54372 17.8503C6.67895 18.4349 6.9156 18.8699 7.25368 19.1551C7.59175 19.4545 8.05829 19.6185 8.6533 19.6471H8.75473C8.87643 19.6613 8.97785 19.7112 9.05899 19.7968C9.14013 19.8824 9.1807 19.9893 9.1807 20.1176V21.5294C9.1807 21.6578 9.13337 21.7647 9.03871 21.8503C8.95757 21.9501 8.85615 22 8.73444 22H8.63302Z" fill="currentColor"/>
<path d="M15.367 2C16.9627 2.01426 18.1595 2.54189 18.9573 3.58289C19.7552 4.62389 20.1541 6.16399 20.1541 8.20321C20.1541 8.85918 20.242 9.39394 20.4178 9.80749C20.5936 10.2068 20.8776 10.5205 21.2698 10.7487L21.4726 10.8556C21.6619 10.9697 21.7972 11.0909 21.8783 11.2193C21.9594 11.3476 22 11.5045 22 11.6898V12.3102C22 12.4955 21.9594 12.6524 21.8783 12.7807C21.7972 12.9091 21.6619 13.0303 21.4726 13.1444L21.2495 13.2513C20.8708 13.4938 20.5936 13.8217 20.4178 14.2353C20.242 14.6346 20.1541 15.1622 20.1541 15.8182C20.1541 19.8966 18.5652 21.9572 15.3873 22H15.2656C15.1439 22 15.0357 21.9501 14.941 21.8503C14.8599 21.7647 14.8193 21.6578 14.8193 21.5294V20.1176C14.8193 19.9893 14.8599 19.8824 14.941 19.7968C15.0221 19.7112 15.1236 19.6613 15.2453 19.6471H15.3264C15.9349 19.6185 16.4083 19.4617 16.7463 19.1765C17.0844 18.8913 17.3211 18.4563 17.4563 17.8717C17.5915 17.2727 17.6591 16.467 17.6591 15.4545C17.6591 14.5847 17.7876 13.8574 18.0445 13.2727C18.315 12.6881 18.6869 12.2674 19.1602 12.0107C18.6869 11.754 18.315 11.3262 18.0445 10.7273C17.7876 10.1141 17.6591 9.38681 17.6591 8.54546C17.6591 7.53298 17.5915 6.7344 17.4563 6.14973C17.3211 5.5508 17.0844 5.10873 16.7463 4.82353C16.4083 4.53832 15.9417 4.38146 15.3467 4.35294L15.2453 4.33155C15.1371 4.33155 15.0357 4.28877 14.941 4.20321C14.8599 4.10339 14.8193 3.98931 14.8193 3.86096V2.47059C14.8193 2.32799 14.8599 2.2139 14.941 2.12834C15.0357 2.04278 15.1439 2 15.2656 2H15.367Z" fill="currentColor"/>
</svg>
);
};

View File

@@ -0,0 +1,14 @@
export type ShortcutIconProps = {
children: React.ReactNode;
className?: string;
};
export function ShortcutIcon({ className, children }: ShortcutIconProps) {
return (
<span
className={`flex items-center justify-center rounded ${className ?? ""}`}
>
{children}
</span>
);
}

View File

@@ -0,0 +1,20 @@
export function SquareBracketsIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
className={props.className}
width="30"
height="14"
viewBox="0 0 30 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="14" height="14" rx="1.53846" fill="currentColor" />
<path d="M6 11V3H9V4.5H7.5V9.5H9V11H6Z" fill="#0F172A" />
<rect x="16" width="14" height="14" rx="1.53846" fill="currentColor" />
<path
d="M25 3V11L21.9997 11V9.5H23.5V4.5H21.9997V3.00002L25 3Z"
fill="#0F172A"
/>
</svg>
);
}

View File

@@ -0,0 +1,17 @@
export function StringIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
className={props.className}
viewBox="-2 -5 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.536 6.845a3.908 3.908 0 0 1 2.598.566l-.025-.032a4.114 4.114 0 0 1 1.766 2.478 4.228 4.228 0 0 1-.391 3.047 4.026 4.026 0 0 1-2.33 1.92 3.898 3.898 0 0 1-2.973-.273h-.03a1.296 1.296 0 0 0-.082-.045c-.033-.018-.066-.035-.096-.056a4.236 4.236 0 0 1-.99-.88A7.746 7.746 0 0 1 .095 9.743a8.717 8.717 0 0 1 .191-3.498c.3-1.14.827-2.203 1.55-3.12A8.282 8.282 0 0 1 4.477.918 8.047 8.047 0 0 1 7.763 0c.287 0 .562.117.765.326.203.209.317.492.317.787 0 .296-.114.579-.317.788a1.066 1.066 0 0 1-.765.326c-.96.04-1.895.332-2.716.847A5.758 5.758 0 0 0 3.07 5.17a6.53 6.53 0 0 0-.905 2.906 3.962 3.962 0 0 1 2.37-1.232ZM15.53 6.83c.901-.12 1.815.079 2.591.565h-.006a4.105 4.105 0 0 1 1.761 2.473 4.22 4.22 0 0 1-.39 3.04 4.016 4.016 0 0 1-2.324 1.917 3.886 3.886 0 0 1-2.966-.273h-.036c-.03-.021-.063-.038-.097-.056a1.317 1.317 0 0 1-.08-.045 4.226 4.226 0 0 1-.987-.879 7.782 7.782 0 0 1-1.902-3.848 8.715 8.715 0 0 1 .194-3.49 8.564 8.564 0 0 1 1.546-3.111A8.276 8.276 0 0 1 15.469.92 8.037 8.037 0 0 1 18.742 0c.286 0 .56.117.763.325.202.209.316.491.316.786 0 .295-.114.577-.316.786a1.063 1.063 0 0 1-.763.325 5.526 5.526 0 0 0-2.707.846 5.738 5.738 0 0 0-1.968 2.092 6.519 6.519 0 0 0-.902 2.9 3.952 3.952 0 0 1 2.364-1.23Z"
fill="currentColor"
/>
</svg>
);
}

View File

@@ -0,0 +1,72 @@
import { motion } from "framer-motion";
import { transition } from "../../utilities/animationConstants";
export const SunIcon = () => {
const whileTap = { scale: 0.95, rotate: 15 };
const raysVariants = {
initial: { rotate: 45 },
animate: { rotate: 0, transition },
};
const coreVariants = {
initial: { scale: 1.5 },
animate: { scale: 1, transition },
};
return (
<motion.svg
key="sun"
width="1em"
height="1em"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
whileTap={whileTap}
// Centers the rotation anchor point vertically & horizontally
style={{ originX: "50%", originY: "50%" }}
>
<motion.circle
cx="11.9998"
cy="11.9998"
r="5.75375"
fill="currentColor"
initial="initial"
animate="animate"
variants={coreVariants}
/>
<motion.g initial="initial" animate="animate" variants={raysVariants}>
<circle
cx="3.08982"
cy="6.85502"
r="1.71143"
transform="rotate(-60 3.08982 6.85502)"
fill="currentColor"
/>
<circle
cx="3.0903"
cy="17.1436"
r="1.71143"
transform="rotate(-120 3.0903 17.1436)"
fill="currentColor"
/>
<circle cx="12" cy="22.2881" r="1.71143" fill="currentColor" />
<circle
cx="20.9101"
cy="17.1436"
r="1.71143"
transform="rotate(-60 20.9101 17.1436)"
fill="currentColor"
/>
<circle
cx="20.9101"
cy="6.8555"
r="1.71143"
transform="rotate(-120 20.9101 6.8555)"
fill="currentColor"
/>
<circle cx="12" cy="1.71143" r="1.71143" fill="currentColor" />
</motion.g>
</motion.svg>
);
};

View File

@@ -0,0 +1,17 @@
export function TreeIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
className={props.className}
width="28"
height="30"
viewBox="0 0 28 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.1805 30C14.5093 30 14.8245 29.8694 15.0571 29.6368C15.2895 29.4044 15.4201 29.089 15.4201 28.7602V22.5619H21.6184C23.2624 22.5619 24.839 21.9091 26.0014 20.7467C27.1638 19.5843 27.8167 18.0077 27.8167 16.3636C27.8234 14.3646 26.854 12.4882 25.2197 11.3368C25.2971 10.9512 25.3364 10.5588 25.3375 10.1653C25.3379 8.64671 24.7808 7.18076 23.7722 6.04565C22.7633 4.91048 21.3726 4.18522 19.8643 4.00752C19.2285 2.5124 18.0292 1.3282 16.5263 0.711248C15.0232 0.0942975 13.3379 0.0942975 11.8348 0.711248C10.332 1.3282 9.13259 2.51246 8.49682 4.00752C6.98853 4.18522 5.59785 4.91048 4.58897 6.04565C3.58025 7.18082 3.02318 8.64671 3.02362 10.1653C3.0247 10.5588 3.06405 10.9511 3.14144 11.3368C1.50714 12.4882 0.537772 14.3646 0.544468 16.3636C0.544468 18.0077 1.19734 19.5843 2.35974 20.7467C3.52214 21.9091 5.09872 22.5619 6.74276 22.5619H12.941V28.7602C12.941 29.089 13.0716 29.4044 13.304 29.6368C13.5366 29.8694 13.8518 30 14.1806 30H14.1805ZM6.74257 20.0827C5.75616 20.0827 4.81014 19.6908 4.11272 18.9934C3.41531 18.296 3.02337 17.35 3.02337 16.3636C3.02273 15.6644 3.22118 14.9796 3.59561 14.389C3.96981 13.7986 4.50443 13.3267 5.13716 13.029C5.41473 12.8941 5.63221 12.6606 5.7468 12.3742C5.86138 12.0875 5.86505 11.7685 5.75696 11.4794C5.31725 10.3533 5.4569 9.0835 6.13052 8.07999C6.80414 7.07625 7.92631 6.466 9.13497 6.4463C9.18145 6.4463 9.30857 6.46489 9.35202 6.46489C9.63457 6.47851 9.91302 6.39311 10.1391 6.22341C10.3655 6.05371 10.5255 5.8103 10.5916 5.53507C10.8614 4.46133 11.5979 3.56463 12.599 3.0914C13.6002 2.6184 14.7606 2.6184 15.7617 3.0914C16.7628 3.56461 17.4993 4.46133 17.7691 5.53507C17.8363 5.80962 17.9965 6.05239 18.2227 6.22186C18.4486 6.39135 18.7266 6.47739 19.0087 6.46485C19.083 6.46485 19.1544 6.46485 19.1388 6.44928L19.139 6.4495C20.3615 6.43934 21.5099 7.03579 22.2045 8.04207C22.8991 9.04841 23.0497 10.3333 22.607 11.473C22.4987 11.7621 22.5024 12.0811 22.6169 12.3678C22.7317 12.6545 22.949 12.8879 23.2268 13.0226C24.2385 13.5174 24.9716 14.444 25.2202 15.5424C25.4691 16.6408 25.2066 17.7928 24.5066 18.675C23.8066 19.5572 22.7445 20.0748 21.6182 20.0826H15.42V16.9151L19.8735 13.6424C20.1495 13.4519 20.3365 13.1579 20.3919 12.8272C20.4472 12.4965 20.3664 12.1575 20.1677 11.8875C19.969 11.6175 19.6692 11.4394 19.3371 11.3942C19.0049 11.3488 18.6685 11.4398 18.4045 11.6467L15.4199 13.8379V8.92563C15.4199 8.48267 15.1836 8.07347 14.8002 7.8521C14.4167 7.63074 13.9441 7.63074 13.5606 7.8521C13.1771 8.07347 12.9408 8.48272 12.9408 8.92563V15.1239L9.96256 12.9018C9.60716 12.6372 9.13741 12.5823 8.73054 12.7578C8.32368 12.9334 8.04136 13.3125 7.9899 13.7527C7.93844 14.1927 8.12565 14.627 8.48105 14.8916L12.9408 18.2045V20.0827L6.74257 20.0827Z"
fill="currentColor"
/>
</svg>
);
}

View File

@@ -0,0 +1,18 @@
export function TwitterIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
className={props.className}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="12" cy="12" r="12" fill="#F8FAFC" />
<path
d="M5.43319 6H5.44396C5.50679 6.07822 5.56783 6.15853 5.63271 6.23467C6.27615 6.99369 6.98755 7.67709 7.80461 8.24238C8.73734 8.88746 9.75135 9.33515 10.8628 9.5487C11.3459 9.64122 11.8363 9.68896 12.3279 9.69132C12.3435 9.69076 12.359 9.68937 12.3744 9.68715C12.3664 9.65326 12.3582 9.62536 12.3538 9.59694C12.2968 9.24285 12.298 8.8816 12.3572 8.52789C12.5788 7.19811 13.6625 6.20938 14.9433 6.03181C15.0276 6.02008 15.1125 6.01069 15.1971 6H15.5621C15.5811 6.00425 15.6004 6.00747 15.6198 6.00965C15.9463 6.0339 16.2671 6.10973 16.5707 6.23441C16.9759 6.40128 17.347 6.64347 17.665 6.94858C17.6748 6.9577 17.6863 6.96472 17.6988 6.9692C17.7113 6.97368 17.7246 6.97554 17.7378 6.97465C18.2937 6.87178 18.8309 6.68327 19.3309 6.41562C19.4296 6.36347 19.5276 6.30924 19.6387 6.24901C19.3596 6.95666 18.9111 7.50057 18.2866 7.89872C18.8855 7.84345 19.4486 7.66119 20 7.42652C19.9379 7.51934 19.8738 7.60773 19.8071 7.6943C19.4338 8.17928 19.0121 8.61524 18.5141 8.96803C18.4917 8.98129 18.4736 9.00084 18.4618 9.02433C18.4501 9.04783 18.4453 9.07427 18.4479 9.10048C18.4686 9.8569 18.3891 10.6127 18.2115 11.3476C17.928 12.5242 17.41 13.6293 16.6897 14.5943C15.6526 15.9935 14.2124 17.0293 12.5695 17.5579C12.1008 17.7133 11.6191 17.8245 11.1303 17.8901C10.8077 17.9305 10.4838 17.9657 10.1596 17.9845C9.618 18.016 9.07662 17.996 8.53576 17.9558C8.04402 17.9215 7.55544 17.8504 7.07398 17.743C6.48868 17.6143 5.9222 17.4092 5.38857 17.1329C5.26034 17.0656 5.13699 16.9916 5.01132 16.9207L5.01517 16.9071H5.07672C5.59886 16.9102 6.12023 16.9071 6.63826 16.8229C7.15926 16.7393 7.66519 16.5776 8.13954 16.3431C8.63963 16.0944 9.09073 15.7695 9.53619 15.4334C9.54091 15.4282 9.54481 15.4223 9.54773 15.4159C9.1274 15.4081 8.75888 15.2882 8.4837 15.1716C7.99209 14.9607 7.53859 14.6678 7.14194 14.3049C6.77034 13.9696 6.44618 13.5941 6.21717 13.1417C6.15818 13.0254 6.11177 12.9026 6.05535 12.7736C6.52466 12.8779 6.96935 12.8317 7.40942 12.7071C5.75248 12.3707 4.93798 10.8409 5.00748 9.69654C5.43653 9.89705 5.89019 9.99691 6.35411 10.0566C6.22307 9.94633 6.08663 9.84568 5.96661 9.72757C5.13109 8.90571 4.83078 7.91854 5.09083 6.76267C5.15514 6.48825 5.27143 6.2292 5.43319 6Z"
fill="#4338CA"
/>
</svg>
);
}

View File

@@ -0,0 +1,37 @@
import React from "react";
import { Body } from "./Primitives/Body";
import { usePreferences } from "~/components/PreferencesProvider";
const MIN_INDENT = 1;
const MAX_INDENT = 8;
export function IndentPreference() {
const [preferences, setPreferences] = usePreferences();
const updatePreferences = (e: React.ChangeEvent<HTMLInputElement>) => {
let newIdent = Number(e.target.value);
if (newIdent < MIN_INDENT) newIdent = MIN_INDENT;
if (newIdent > MAX_INDENT) newIdent = MAX_INDENT;
e.target.value = newIdent.toString();
setPreferences({ ...preferences, indent: newIdent });
};
return (
<div className="flex items-center -mt-0.5">
<label
className="pr-2 text-slate-800 transition dark:text-white"
htmlFor="indent"
>
<Body>Indent</Body>
</label>
<input
type="number"
className="py-0 pr-0 pl-1 w-9 rounded-sm text-sm h-[23px] bg-slate-300 transition hover:bg-slate-400 hover:bg-opacity-50 dark:bg-slate-800 dark:text-slate-400 hover:cursor-pointer hover:dark:bg-slate-700 hover:dark:bg-opacity-70"
defaultValue={preferences?.indent}
min={MIN_INDENT}
max={MAX_INDENT}
onChange={updatePreferences}
/>
</div>
);
}

View File

@@ -0,0 +1,135 @@
import { inferType } from "@jsonhero/json-infer-types";
import { JSONHeroPath } from "@jsonhero/path";
import { useCallback, useMemo, useState } from "react";
import { useJson } from "~/hooks/useJson";
import {
useJsonColumnViewAPI,
useJsonColumnViewState,
} from "~/hooks/useJsonColumnView";
import { concatenated, getHierarchicalTypes } from "~/utilities/dataType";
import { formatRawValue } from "~/utilities/formatter";
import { isNullable } from "~/utilities/nullable";
import { CopyTextButton } from "./CopyTextButton";
import { Body } from "./Primitives/Body";
import { LargeMono } from "./Primitives/LargeMono";
import { Title } from "./Primitives/Title";
import { ValueIcon, ValueIconSize } from "./ValueIcon";
export type InfoHeaderProps = {
relatedPaths: string[];
};
export function InfoHeader({ relatedPaths }: InfoHeaderProps) {
const { selectedNodeId, highlightedNodeId, selectedNodes } =
useJsonColumnViewState();
const { goToNodeId } = useJsonColumnViewAPI();
if (!selectedNodeId || !highlightedNodeId || selectedNodes.length === 0) {
return <EmptyState />;
}
const selectedNode = selectedNodes[selectedNodes.length - 1];
const [json] = useJson();
const selectedHeroPath = new JSONHeroPath(selectedNodeId);
const selectedJson = selectedHeroPath.first(json);
const selectedInfo = inferType(selectedJson);
const formattedSelectedInfo = formatRawValue(selectedInfo);
const selectedName = selectedNode.longTitle ?? selectedNode.title;
const isSelectedLeafNode =
selectedInfo.name !== "object" && selectedInfo.name !== "array";
const canBeNull = useMemo(() => {
return isNullable(relatedPaths, json);
}, [relatedPaths, json]);
const [hovering, setHovering] = useState(false);
console.warn(selectedInfo);
const newPath = formattedSelectedInfo.replace(/^#/, "$").replace(/\//g, ".");
const handleClick = useCallback(() => {
goToNodeId(newPath, "pathBar");
}, [newPath, goToNodeId]);
return (
<div className="mb-4 pb-4">
<div className="flex items-center">
<Title className="flex-1 mr-2 overflow-hidden overflow-ellipsis break-words text-slate-700 transition dark:text-slate-200">
{ selectedName ?? "nothing" }
</Title>
<div>
<ValueIcon
monochrome
type={selectedInfo}
size={ValueIconSize.Medium}
/>
</div>
</div>
<div
className="relative w-full h-full"
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
>
{isSelectedLeafNode && (
<LargeMono
className={`z-10 py-1 mb-1 text-slate-800 overflow-ellipsis break-words transition rounded-sm dark:text-slate-300 ${
hovering ? "bg-slate-100 dark:bg-slate-700" : "bg-transparent"
}`}
>
{selectedNode.name === "$ref" && checkPathExists(json, newPath) ? (
<button onClick={handleClick}>
{formatRawValue(selectedInfo)}
</button>
) : (
formatRawValue(selectedInfo)
)}
</LargeMono>
)}
<div
className={`absolute top-1 right-0 flex justify-end h-full w-fit transition ${
hovering ? "opacity-100" : "opacity-0"
}`}
>
<CopyTextButton
className="bg-slate-200 hover:bg-slate-300 h-fit mr-1 px-2 py-0.5 rounded-sm transition hover:cursor-pointer dark:text-white dark:bg-slate-600 dark:hover:bg-slate-500"
value={formatRawValue(selectedInfo)}
></CopyTextButton>
</div>
</div>
<div className="flex text-gray-400">
<Body className="flex-1">
{concatenated(getHierarchicalTypes(selectedInfo))}
</Body>
{canBeNull && <Body>Can be null</Body>}
</div>
</div>
);
}
function checkPathExists(json: unknown, newPath: string) {
const heroPath = new JSONHeroPath(newPath);
const node = heroPath.first(json);
return Boolean(node);
}
function EmptyState() {
return (
<div className="mb-4 pb-4 border-b border-slate-300">
<div className="flex items-center">
<Title className="flex-1 mr-2 text-slate-800 transition dark:text-slate-300">
Nothing selected
</Title>
</div>
<div>
<div>
<Title className="text-slate-800 mb-1 overflow-ellipsis break-words dark:text-slate-300">
null
</Title>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,39 @@
import { PreviewValue } from "./Preview/PreviewValue";
import { RelatedValues } from "./RelatedValues";
import { PropertiesValue } from "./Properties/PropertiesValue";
import { InfoHeader } from "./InfoHeader";
import { ContainerInfo } from "./ContainerInfo";
import { useSelectedInfo } from "~/hooks/useSelectedInfo";
import { useRelatedPaths } from "~/hooks/useRelatedPaths";
import { useJsonDoc } from "~/hooks/useJsonDoc";
export function InfoPanel() {
const { minimal } = useJsonDoc();
const selectedInfo = useSelectedInfo();
const relatedPaths = useRelatedPaths();
if (!selectedInfo) {
return <></>;
}
return (
<>
<div
className={`${
minimal ? "h-inspectorHeightMinimal" : "h-inspectorHeight"
} p-4 bg-white border-l-[1px] border-slate-300 overflow-y-auto no-scrollbar transition dark:bg-slate-800 dark:border-slate-600`}
>
<InfoHeader relatedPaths={relatedPaths} />
<div className="mb-4">
<PreviewValue />
</div>
<PropertiesValue />
<ContainerInfo />
<RelatedValues relatedPaths={relatedPaths} />
</div>
</>
);
}

View File

@@ -0,0 +1,74 @@
import {
useJsonColumnViewAPI,
useJsonColumnViewState,
} from "../hooks/useJsonColumnView";
import { useHotkeys } from "react-hotkeys-hook";
import { Columns } from "./Columns";
import { CopySelectedNodeShortcut } from "./CopySelectedNode";
export function JsonColumnView() {
const { getColumnViewProps, columns } = useJsonColumnViewState();
return (
<>
<KeyboardShortcuts />
<div {...getColumnViewProps()}>
<Columns columns={columns} />
</div>
</>
);
}
function KeyboardShortcuts() {
const api = useJsonColumnViewAPI();
useHotkeys(
"down",
(e) => {
e.preventDefault();
api.goToNextSibling();
},
{ enabled: true },
[api]
);
useHotkeys(
"up",
(e) => {
e.preventDefault();
api.goToPreviousSibling();
},
[api]
);
useHotkeys(
"right",
(e) => {
e.preventDefault();
api.goToChildren();
},
[api]
);
useHotkeys(
"left,alt+left",
(e) => {
e.preventDefault();
api.goToParent({ source: e });
},
[api]
);
useHotkeys(
"esc",
(e) => {
e.preventDefault();
api.resetSelection();
},
[api]
);
return <>
<CopySelectedNodeShortcut />
</>;
}

View File

@@ -0,0 +1,91 @@
import { CodeEditor } from "./CodeEditor";
import { useJson } from "~/hooks/useJson";
import { useCallback, useMemo, useRef } from "react";
import {
useJsonColumnViewAPI,
useJsonColumnViewState,
} from "~/hooks/useJsonColumnView";
import { ViewUpdate } from "@uiw/react-codemirror";
import jsonMap from "json-source-map";
import { JSONHeroPath } from "@jsonhero/path";
import {usePreferences} from '~/components/PreferencesProvider'
export function JsonEditor() {
const [json] = useJson();
const { selectedNodeId } = useJsonColumnViewState();
const { goToNodeId } = useJsonColumnViewAPI();
const [preferences] = usePreferences();
const jsonMapped = useMemo(() => {
return jsonMap.stringify(json, null, preferences?.indent || 2);
}, [json, preferences]);
const selection = useMemo<{ start: number; end: number } | undefined>(() => {
if (!selectedNodeId) {
return;
}
const path = new JSONHeroPath(selectedNodeId);
const pointer = path.jsonPointer();
const location = jsonMapped.pointers[pointer];
if (location) {
if (location.key) {
return { start: location.key.pos, end: location.valueEnd.pos };
}
return { start: location.value.pos, end: location.valueEnd.pos };
}
}, [selectedNodeId, jsonMapped]);
const currentSelectedLine = useRef<number | undefined>(undefined);
const onUpdate = useCallback(
(update: ViewUpdate) => {
if (!update.selectionSet) {
return;
}
const range = update.state.selection.ranges[0];
const line = update.state.doc.lineAt(range.anchor);
if (
currentSelectedLine.current &&
currentSelectedLine.current === line.number
) {
return;
}
currentSelectedLine.current = line.number;
// Find the key if the selected line using jsonMapped.pointers
const pointerEntry = Object.entries(jsonMapped.pointers).find(
([pointer, info]) => {
return info.value.line === line.number - 1;
}
);
if (!pointerEntry) {
return;
}
const [pointer] = pointerEntry;
const path = JSONHeroPath.fromPointer(pointer);
goToNodeId(path.toString(), "editor");
},
[goToNodeId]
);
return (
<CodeEditor
language="json"
content={jsonMapped.json}
readOnly={true}
onUpdate={onUpdate}
selection={selection}
/>
);
}

View File

@@ -0,0 +1,186 @@
import { RangeSetBuilder } from "@codemirror/rangeset";
import { JSONHeroPath } from "@jsonhero/path";
import {
useCodeMirror,
EditorView,
Decoration,
Facet,
ViewPlugin,
Compartment,
TransactionSpec,
} from "@uiw/react-codemirror";
import jsonMap from "json-source-map";
import { useRef, useEffect, useMemo, useState } from "react";
import { getPreviewSetup } from "~/utilities/codeMirrorSetup";
import { lightTheme, darkTheme } from "~/utilities/codeMirrorTheme";
import { CopyTextButton } from "./CopyTextButton";
import { useTheme } from "./ThemeProvider";
import {usePreferences} from '~/components/PreferencesProvider';
import { useHotkeys } from "react-hotkeys-hook";
export type JsonPreviewProps = {
json: unknown;
highlightPath?: string;
};
export function JsonPreview({ json, highlightPath }: JsonPreviewProps) {
const editor = useRef(null);
const [preferences] = usePreferences();
const jsonMapped = useMemo(() => {
return jsonMap.stringify(json, null, preferences?.indent || 2);
}, [json, preferences]);
const lines: LineRange | undefined = useMemo(() => {
if (!highlightPath) {
return;
}
let path = new JSONHeroPath(highlightPath);
let pointer = path.jsonPointer();
let selectionInfo = jsonMapped.pointers[pointer];
return {
from: selectionInfo.value.line + 1,
to: selectionInfo.valueEnd.line + 1,
};
}, [jsonMapped, highlightPath]);
const extensions = getPreviewSetup();
const highlighting = new Compartment();
if (lines) {
extensions.push(highlighting.of(highlightLineRange(lines)));
}
const [theme] = useTheme();
const { setContainer, view, state } = useCodeMirror({
container: editor.current,
extensions,
value: jsonMapped.json,
editable: false,
contentEditable: false,
autoFocus: false,
basicSetup: false,
theme: theme === "light" ? lightTheme() : darkTheme(),
});
useEffect(() => {
if (editor.current) {
setContainer(editor.current);
}
}, [editor.current]);
useEffect(() => {
if (!view) {
return;
}
let transactionSpec: TransactionSpec = {
changes: { from: 0, to: view.state.doc.length, insert: jsonMapped.json },
};
let range = lines;
if (range != null) {
transactionSpec.effects = highlighting.reconfigure(
highlightLineRange(range)
);
}
view.dispatch(transactionSpec);
}, [view, highlighting, jsonMapped, highlightPath]);
useHotkeys(
"ctrl+a,meta+a,command+a",
(e) => {
e.preventDefault();
view?.dispatch({ selection: { anchor: 0, head: state?.doc.length } });
},
[view, state]
);
const [hovering, setHovering] = useState(false);
return (
<div
className="relative w-full h-full"
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
>
<div ref={editor} />
<div
className={`absolute top-1 right-0 flex justify-end w-full transition ${
hovering ? "opacity-100" : "opacity-0"
}`}
>
<CopyTextButton
value={jsonMapped.json}
className="bg-slate-200 hover:bg-slate-300 h-fit mr-1 px-2 py-0.5 rounded-sm transition hover:cursor-pointer dark:text-white dark:bg-slate-700 dark:hover:bg-slate-600"
></CopyTextButton>
</div>
</div>
);
}
interface LineRange {
from: number;
to: number;
}
const baseTheme = EditorView.baseTheme({
"&light .cm-highlighted": { backgroundColor: "#ffee0055" },
"&dark .cm-highlighted": { backgroundColor: "#ffee0055" },
});
const highlightedRange = Facet.define<LineRange, LineRange>({
combine: (values) => (values.length ? values[0] : { from: -1, to: -1 }),
});
function highlightLineRange(range: LineRange | null) {
return [
baseTheme,
range == null ? [] : highlightedRange.of(range),
highlightLineRangePlugin,
];
}
const lineHighlightDecoration = Decoration.line({
attributes: { class: "cm-highlighted" },
});
function highlightLines(view: EditorView) {
let highlightRange = view.state.facet(highlightedRange);
let builder = new RangeSetBuilder();
for (let { from, to } of view.visibleRanges) {
for (let pos = from; pos <= to; ) {
let line = view.state.doc.lineAt(pos);
if (
line.number >= highlightRange.from &&
line.number <= highlightRange.to
) {
builder.add(line.from, line.from, lineHighlightDecoration);
}
pos = line.to + 1;
}
}
return builder.finish();
}
const highlightLineRangePlugin = ViewPlugin.fromClass(
class {
decorations: any;
constructor(view: any) {
this.decorations = highlightLines(view);
}
update(update: { docChanged: any; viewportChanged: any; view: any }) {
if (update.docChanged || update.viewportChanged)
this.decorations = highlightLines(update.view);
}
},
{
decorations: (v) => v.decorations,
}
);

View File

@@ -0,0 +1,54 @@
import { JSONHeroPath } from "@jsonhero/path";
import { useMemo, useState } from "react";
import { useJsonSchema } from "~/hooks/useJsonSchema";
import { CodeViewer } from "./CodeViewer";
import { CopyTextButton } from "./CopyTextButton";
import {usePreferences} from '~/components/PreferencesProvider'
export function JsonSchemaViewer({ path }: { path: string }) {
const schema = useJsonSchema();
const schemaPath = schemaPathFromPath(path);
const schemaJson = schemaPath.first(schema);
const [hovering, setHovering] = useState(false);
const [preferences] = usePreferences();
const code = useMemo(() => {
return JSON.stringify(schemaJson, null, preferences?.indent || 2);
}, [schemaJson, preferences]);
return (
<div
className="relative w-full h-full"
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
>
<CodeViewer code={code} lang="json" />
<div
className={`absolute top-1 right-0 flex justify-end w-full transition ${
hovering ? "opacity-100" : "opacity-0"
}`}
>
<CopyTextButton
value={code}
className="bg-slate-200 hover:bg-slate-300 h-fit mr-1 px-2 py-0.5 rounded-sm transition hover:cursor-pointer dark:text-white dark:bg-slate-700 dark:hover:bg-slate-600"
></CopyTextButton>
</div>
</div>
);
}
function schemaPathFromPath(path: JSONHeroPath | string): JSONHeroPath {
const heroPath = typeof path === "string" ? new JSONHeroPath(path) : path;
if (heroPath.isRoot) {
return heroPath;
}
return heroPath.components.slice(1).reduce((acc, component) => {
if (component.isArray) {
return acc.child("items");
} else {
return acc.child("properties").child(component.toString());
}
}, new JSONHeroPath("$"));
}

View File

@@ -0,0 +1,250 @@
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
import { useEffect, useRef } from "react";
import {
useJsonColumnViewAPI,
useJsonColumnViewState,
} from "~/hooks/useJsonColumnView";
import { useJsonDoc } from "~/hooks/useJsonDoc";
import { JsonTreeViewNode, useJsonTreeViewContext } from "~/hooks/useJsonTree";
import { VirtualNode } from "~/hooks/useVirtualTree";
import { CopySelectedNodeShortcut } from "./CopySelectedNode";
import { Body } from "./Primitives/Body";
import { Mono } from "./Primitives/Mono";
export function JsonTreeView() {
const { selectedNodeId, selectedNodeSource } = useJsonColumnViewState();
const { goToNodeId } = useJsonColumnViewAPI();
const { tree, parentRef } = useJsonTreeViewContext();
// Scroll to the selected node when this component is first rendered.
const scrolledToNodeRef = useRef(false);
useEffect(() => {
if (!scrolledToNodeRef.current && selectedNodeId) {
tree.scrollToNode(selectedNodeId);
scrolledToNodeRef.current = true;
}
}, [selectedNodeId, scrolledToNodeRef]);
// Yup, this is hacky.
// This is to prevent the selection not changing the first time you try to move to a new node in the tree
const focusCount = useRef<number>(0);
// This focuses and scrolls to the selected node when the selectedNodeId
// is set from a source other than this tree (e.g. the search bar, path bar, related values).
useEffect(() => {
if (
tree.focusedNodeId &&
selectedNodeId &&
tree.focusedNodeId !== selectedNodeId
) {
if (selectedNodeId === "$") {
return;
}
if (selectedNodeSource !== "tree" && focusCount.current > 0) {
focusCount.current = focusCount.current + 1;
tree.focusNode(selectedNodeId);
tree.scrollToNode(selectedNodeId);
}
}
}, [tree.focusedNodeId, goToNodeId, selectedNodeId, selectedNodeSource]);
// This is what syncs the tree view's focused node to the column view selected node
const previousFocusedNodeId = useRef<string | null>(null);
useEffect(() => {
let updated = false;
if (!previousFocusedNodeId.current) {
previousFocusedNodeId.current = tree.focusedNodeId;
updated = true;
}
if (
tree.focusedNodeId &&
(updated || previousFocusedNodeId.current !== tree.focusedNodeId)
) {
previousFocusedNodeId.current = tree.focusedNodeId;
goToNodeId(tree.focusedNodeId, "tree");
}
}, [previousFocusedNodeId, tree.focusedNodeId, tree.focusNode, goToNodeId]);
const treeRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (treeRef.current) {
treeRef.current.focus({ preventScroll: true });
}
}, [treeRef.current]);
const { minimal } = useJsonDoc();
return (
<>
<CopySelectedNodeShortcut />
<div
className="text-white w-full"
ref={parentRef}
style={{
height: `calc(100vh - ${minimal ? "66px" : "106px"})`,
overflowY: "auto",
overflowX: "hidden",
}}
>
<div
className="relative w-full outline-none"
style={{ height: `${tree.totalSize}px` }}
{...tree.getTreeProps()}
ref={treeRef}
>
{tree.nodes.map((virtualNode) => (
<TreeViewNode
virtualNode={virtualNode}
key={virtualNode.node.id}
onToggle={(node, e) => tree.toggleNode(node.id, e)}
selectedNodeId={selectedNodeId}
/>
))}
</div>
</div>
</>
);
}
function TreeViewNode({
virtualNode,
onToggle,
selectedNodeId,
}: {
virtualNode: VirtualNode<JsonTreeViewNode>;
selectedNodeId?: string;
onToggle?: (node: JsonTreeViewNode, e: MouseEvent) => void;
}) {
const { node, virtualItem, depth } = virtualNode;
const indentClassName = computeTreeNodePaddingClass(depth);
const isSelected = selectedNodeId === node.id;
return (
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualNode.size}px`,
transform: `translateY(${virtualNode.start}px)`,
}}
key={virtualNode.node.id}
{...virtualNode.getItemProps()}
>
<div
className={`h-full flex pl-5 rounded-sm select-none ${
isSelected
? "bg-indigo-700"
: virtualItem.index % 2
? "dark:bg-slate-900"
: "bg-slate-100 bg-opacity-90 dark:bg-slate-800 dark:bg-opacity-30"
}`}
>
<div className={`pl-2 w-2/6 items-center flex`}>
{node.children && node.children.length > 0 && (
<span
onClick={(e) => {
if (onToggle) {
e.preventDefault();
onToggle(node, e.nativeEvent);
}
}}
>
{virtualNode.isCollapsed ? (
<ChevronRightIcon
className={`w-4 h-4 mr-1 -ml-5 ${
isSelected
? "text-slate-100"
: "text-slate-600 dark:text-slate-100"
}`}
/>
) : (
<ChevronDownIcon
className={`w-4 h-4 mr-1 -ml-5 ${
isSelected
? "text-slate-100"
: "text-slate-600 dark:text-slate-100"
}`}
/>
)}
</span>
)}
<Body
className={`${indentClassName} leading-8 truncate whitespace-nowrap pl-2 pr-2 ${
isSelected
? "text-slate-100"
: "text-slate-700 dark:text-slate-200"
}`}
>
{node.longTitle ?? node.name}
</Body>
</div>
<div className="flex w-4/6 items-center">
<span className="mr-2">
{node.icon && (
<node.icon
className={`h-5 w-5 ${
isSelected
? "text-slate-100"
: "text-slate-400 dark:text-slate-500"
}`}
/>
)}
</span>
{node.subtitle && (
<Mono
className={`truncate pr-1 transition ${
isSelected
? "text-slate-100"
: "text-slate-500 dark:text-slate-200"
}`}
>
{node.subtitle}
</Mono>
)}
</div>
</div>
</div>
);
}
function computeTreeNodePaddingClass(depth: number) {
switch (depth) {
case 0:
return "ml-[4px] border-l border-slate-400/70";
case 1:
return "ml-[calc(12px_+_4px)] border-l border-pink-400/70";
case 2:
return "ml-[calc(12px_*_2_+_4px)] border-l border-blue-400/70";
case 3:
return "ml-[calc(12px_*_3_+_4px)] border-l border-orange-400/70";
case 4:
return "ml-[calc(12px_*_4_+_4px)] border-l border-emerald-400/70";
case 5:
return "ml-[calc(12px_*_5_+_4px)] border-l border-pink-400/70";
case 6:
return "ml-[calc(12px_*_6_+_4px)] border-l border-blue-400/70";
case 7:
return "ml-[calc(12px_*_7_+_4px)] border-l border-orange-400/70";
case 8:
return "ml-[calc(12px_*_8_+_4px)] border-l border-emerald-400/70";
case 9:
return "ml-[calc(12px_*_9_+_4px)] border-l border-pink-400/70";
case 10:
return "ml-[calc(12px_*_10_+_4px)] border-l border-orange-400/70";
default:
return "ml-[calc(12px_*_11_+_4px)] border-l border-slate-400/70";
}
}

View File

@@ -0,0 +1,21 @@
import React from "react";
import { PathBar, PathHistoryControls } from "./PathBar";
import { SearchBar } from "./SearchBar";
export function JsonView({ children }: { children: React.ReactNode }) {
return (
<div className="path-bar-and-column-wrapper flex flex-col flex-grow overflow-x-hidden border-l-[1px] border-slate-300 transition dark:border-slate-600">
<div className="flex justify-between p-1 bg-slate-200 border-slate-300 border-b-[1px] transition dark:bg-slate-900 dark:border-slate-600">
<div className="flex-shrink-0 flex-grow-0">
<PathHistoryControls />
</div>
<div className="flex-1 pr-2 min-w-0">
<PathBar />
</div>
<SearchBar />
</div>
{children}
</div>
);
}

View File

@@ -0,0 +1,20 @@
import { DragAndDropForm } from "./DragAndDropForm";
import { Title } from "./Primitives/Title";
import { SampleUrls } from "./SampleUrls";
import { UrlForm } from "./UrlForm";
export function NewDocument() {
return (
<div className="bg-indigo-700 text-white rounded-sm shadow-md w-96 max-w-max p-3 transition">
<div className="flex flex-col">
<UrlForm className="mb-2" />
<DragAndDropForm />
<div className="mt-4">
<Title className="mb-2 text-slate-200"> JSON</Title>
<SampleUrls />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,20 @@
import { DragAndDropForm } from "./DragAndDropForm";
import { Title } from "./Primitives/Title";
import { SampleUrls } from "./SampleUrls";
import { UrlForm } from "./UrlForm";
export function NewFile() {
return (
<div>
<div className="mb-4">
<UrlForm />
</div>
<DragAndDropForm />
<div className="mt-4 pt-5">
<Title className="mb-2 text-slate-200">No JSON? Try it out:</Title>
<SampleUrls />
</div>
</div>
);
}

View File

@@ -0,0 +1,17 @@
export type OpenInNewWindowProps = {
children?: React.ReactNode;
url?: string;
className?: string;
};
export function OpenInNewWindow({
url,
className,
children,
}: OpenInNewWindowProps) {
return (
<a href={url} target="_blank" className={`${className} relative`}>
{children}
</a>
);
}

View File

@@ -0,0 +1,222 @@
import {
ChevronRightIcon,
ArrowLeftIcon,
ArrowRightIcon,
PencilAltIcon,
CheckIcon,
} from "@heroicons/react/outline";
import { ColumnViewNode } from "~/useColumnView";
import { Body } from "./Primitives/Body";
import {
useJsonColumnViewAPI,
useJsonColumnViewState,
} from "../hooks/useJsonColumnView";
import { useHotkeys } from "react-hotkeys-hook";
import { memo, useEffect, useRef, useState } from "react";
import { useJson } from '~/hooks/useJson';
import { JSONHeroPath } from '@jsonhero/path';
export function PathBar() {
const [isEditable, setIsEditable] = useState(false);
const { selectedNodes, highlightedNodeId } = useJsonColumnViewState();
const { goToNodeId } = useJsonColumnViewAPI();
const [json] = useJson();
if (isEditable) {
return (
<PathBarText
selectedNodes={selectedNodes}
onConfirm={(newPath) => {
setIsEditable(false);
const heroPath = new JSONHeroPath(newPath);
const node = heroPath.first(json);
if (node) {
goToNodeId(newPath, 'pathBar');
}
}}
/>
);
}
return (
<PathBarLink
selectedNodes={selectedNodes}
highlightedNodeId={highlightedNodeId}
enableEdit={() => setIsEditable(true)}
/>
);
}
export function PathBarText({ selectedNodes, onConfirm }: { selectedNodes: ColumnViewNode[], onConfirm: (newPath: string) => void; }) {
const [path, setPath] = useState('');
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
setPath(selectedNodes.at(-1)?.id || '');
}, [selectedNodes]);
useEffect(() => {
if (ref.current) {
ref.current.focus();
}
}, [ref]);
return (
<form
onSubmit={(e) => {
onConfirm(path);
e.preventDefault();
}}
// onBlur={() => onConfirm(path)}
className="flex overflow-x-hidden items-center bg-slate-300 dark:bg-slate-700 rounded-sm"
>
<label className="grow">
<input
ref={ref}
className={
"w-full border-none outline-none text-ellipsis text-base px-2 py-0 rounded-sm bg-transparent dark:text-slate-200"
}
style={{ boxShadow: 'none' }}
type="text"
name="title"
spellCheck="false"
placeholder="Name your JSON file"
value={path}
onChange={(e) => setPath(e.target.value)}
/>
</label>
<button type="submit" className="flex ml-auto justify-center items-center w-[26px] h-[26px] hover:bg-slate-400 dark:text-slate-400 dark:hover:bg-white dark:hover:bg-opacity-[10%]">
<CheckIcon className='h-5' />
</button>
</form>
);
}
export type PathBarLinkProps = {
selectedNodes: ColumnViewNode[];
highlightedNodeId?: string;
enableEdit: () => void;
};
export function PathBarLink({
selectedNodes,
highlightedNodeId,
enableEdit,
}: PathBarLinkProps) {
const { goToNodeId } = useJsonColumnViewAPI();
return (
<div
className="flex flex-shrink-0 flex-grow-0 overflow-x-hidden"
onClick={(event) => {
if (event.detail == 2) {
enableEdit();
}
}}
>
{selectedNodes.map((node, index) => {
return (
<PathBarItem
key={index}
node={node}
isHighlighted={highlightedNodeId === node.id}
onClick={(id) => goToNodeId(id, "pathBar")}
isLast={index == selectedNodes.length - 1}
/>
);
})}
<button
className="flex ml-auto justify-center items-center w-[26px] h-[26px] hover:bg-slate-300 dark:text-slate-400 dark:hover:bg-white dark:hover:bg-opacity-[10%]"
onClick={enableEdit}>
<PencilAltIcon className='h-5' />
</button>
</div>
);
}
export function PathHistoryControls() {
const { canGoBack, canGoForward } = useJsonColumnViewState();
const { goBack, goForward } = useJsonColumnViewAPI();
useHotkeys(
"[",
() => {
goBack();
},
[goBack]
);
useHotkeys(
"]",
() => {
goForward();
},
[goForward]
);
return (
<div className="flex h-full">
<button
className="flex justify-center items-center w-[26px] h-[26px] disabled:text-slate-400 disabled:text-opacity-50 text-slate-700 hover:bg-slate-300 hover:disabled:bg-transparent rounded-sm transition dark:disabled:text-slate-700 dark:text-slate-400 dark:hover:bg-white dark:hover:bg-opacity-[5%] dark:hover:disabled:bg-transparent"
disabled={!canGoBack}
onClick={goBack}
>
<ArrowLeftIcon className="w-5 h-6" />
</button>
<button
className="flex justify-center items-center w-[26px] h-[26px] disabled:text-slate-400 disabled:text-opacity-50 text-slate-700 hover:bg-slate-300 hover:disabled:bg-transparent rounded-sm transition dark:disabled:text-slate-700 dark:text-slate-400 dark:hover:bg-white dark:hover:bg-opacity-[5%] dark:hover:disabled:bg-transparent"
disabled={!canGoForward}
onClick={goForward}
>
<ArrowRightIcon className="w-5 h-6" />
</button>
</div>
);
}
function PathBarElement({
node,
isHighlighted,
onClick,
isLast,
}: {
node: ColumnViewNode;
isHighlighted: boolean;
onClick?: (id: string) => void;
isLast: boolean;
}) {
return (
<div
className="flex items-center min-w-0"
style={{
flexShrink: 1,
}}
>
<div
className={`flex items-center hover:cursor-pointer min-w-0 transition ${isHighlighted
? "text-slate-700 bg-slate-300 px-2 py-[3px] rounded-sm dark:text-white dark:bg-slate-700"
: "hover:bg-slate-300 px-2 py-[3px] rounded-sm transition dark:hover:bg-white dark:hover:bg-opacity-[5%]"
}`}
style={{
flexShrink: 1,
}}
onClick={() => onClick && onClick(node.id)}
>
<div className="w-4 flex-shrink-[0.5] flex-grow-0 flex-col justify-items-center whitespace-nowrap overflow-x-hidden transition dark:text-slate-400">
{node.icon && <node.icon className="h-3 w-3" />}
</div>
<Body className="flex-shrink flex-grow-0 whitespace-nowrap overflow-x-hidden text-ellipsis transition dark:text-slate-400">
{node.title}
</Body>
</div>
{isLast ? (
<></>
) : (
<ChevronRightIcon className="flex-grow-0 flex-shrink-[0.5] w-4 h-4 text-slate-400 whitespace-nowrap overflow-x-hidden" />
)}
</div>
);
}
const PathBarItem = memo(PathBarElement);

View File

@@ -0,0 +1,132 @@
import { ChevronRightIcon, EyeIcon } from "@heroicons/react/outline";
import { useMemo } from "react";
import { useJsonColumnViewAPI } from "~/hooks/useJsonColumnView";
import { ColumnViewNode, IconComponent } from "~/useColumnView";
import { Body } from "./Primitives/Body";
import eyeIcon from "~/assets/svgs/EyeIcon.svg";
export type PathPreviewProps = {
nodes: ColumnViewNode[];
maxComponents?: number;
enabled?: boolean;
};
type ValueComponent = {
type: "value";
id: string;
title: string;
icon?: IconComponent;
};
type EllipsisComponent = {
type: "ellipsis";
id: "ellipsis";
};
type Component = ValueComponent | EllipsisComponent;
export function PathPreview({
nodes,
maxComponents,
enabled,
}: PathPreviewProps) {
const isEnabled = useMemo(() => {
if (enabled === undefined) {
return true;
}
return enabled;
}, [enabled]);
const { goToNodeId } = useJsonColumnViewAPI();
const components = useMemo<Array<Component>>(() => {
if (maxComponents == null || nodes.length <= maxComponents) {
return nodes.map((n) => {
return { type: "value", id: n.id, title: n.title, icon: n.icon };
});
}
let components = Array<Component>();
//add the elements up to the ellipsis
for (let index = 0; index < maxComponents - 1; index++) {
const node = nodes[index];
components.push({
type: "value",
id: node.id,
title: node.title,
icon: node.icon,
});
}
//add ellipsis
components.push({ type: "ellipsis", id: "ellipsis" });
//add final element
const lastNode = nodes[nodes.length - 1];
components.push({
type: "value",
id: lastNode.id,
title: lastNode.title,
icon: lastNode.icon,
});
return components;
}, [nodes, maxComponents]);
return (
<div
className={`flex select-none pl-7 ${
isEnabled
? `relative transition hover:bg-slate-200 hover:cursor-pointer dark:hover:bg-slate-600 after:transition after:absolute after:h-3 after:w-3 after:opacity-0 hover:after:opacity-100 after:top-1 after:left-1 after:content-[''] after:bg-[url('${eyeIcon}')] after:bg-no-repeat`
: "disabled"
}`}
onClick={() =>
isEnabled &&
goToNodeId(components[components.length - 1].id, "relatedValues")
}
>
<div
className={`flex rounded-sm px-2 ${
isEnabled
? ""
: "hover:bg-slate-100 hover:cursor-pointer dark:hover:bg-slate-600"
}`}
>
{components.map((node, index) => {
if (node.type === "ellipsis") {
return (
<div
key={node.id}
className="flex flex-none items-center min-w-0"
>
<div className="flex-none text-md"></div>
<ChevronRightIcon className="flex-none w-4 h-4 text-slate-400 whitespace-nowrap overflow-x-hidden" />
</div>
);
} else {
return (
<div className="flex items-center min-w-0" key={node.id}>
<div className="flex items-center min-w-0">
<div className="w-4 flex-shrink-[0.5] flex-grow-0 flex-col justify-items-center whitespace-nowrap overflow-x-hidden transition dark:text-slate-300">
{node.icon && <node.icon className="h-3 w-3" />}
</div>
<Body className="flex-shrink flex-grow-0 whitespace-nowrap overflow-x-hidden text-ellipsis transition dark:text-slate-300">
{node.title}
</Body>
</div>
{index == components.length - 1 ? (
<></>
) : (
<ChevronRightIcon className="flex-grow-0 flex-shrink-[0.5] w-4 h-4 text-slate-400 whitespace-nowrap overflow-x-hidden" />
)}
</div>
);
}
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,58 @@
import {createContext, Dispatch, ReactNode, SetStateAction, useContext, useEffect, useState} from 'react';
interface Preferences {
indent: number;
}
const PreferencesDefaults: Preferences = {
indent: 2,
};
type PreferencesContextType = [
Preferences | undefined,
Dispatch<SetStateAction<Preferences | undefined>>
];
const PreferencesContext = createContext<PreferencesContextType | undefined>(undefined);
const loadPreferences = (): Preferences => {
const savedPreferences = localStorage.getItem('preferences');
const parsedPreferences = JSON.parse(savedPreferences || '{}');
for (const [key, value] of Object.entries(PreferencesDefaults)) {
if (!parsedPreferences[key]) parsedPreferences[key] = value;
}
return parsedPreferences;
};
const savePreferences = (preferences: Preferences) => localStorage.setItem('preferences', JSON.stringify(preferences));
export function PreferencesProvider({
children,
}: {
children: ReactNode;
}) {
const [preferences, setPreferences] = useState<Preferences>();
useEffect(() => {
const preferences = loadPreferences();
setPreferences(preferences);
}, []);
useEffect(() => {
if (preferences === undefined) return;
savePreferences(preferences);
}, [preferences]);
return (
<PreferencesContext.Provider value={[preferences, setPreferences]}>
{children}
</PreferencesContext.Provider>
);
}
export function usePreferences() {
const context = useContext(PreferencesContext);
if (context === undefined) {
throw new Error('usePreferences must be used within a PreferencesProvider');
}
return context;
}

Some files were not shown because too many files have changed in this diff Show More