diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index a5bc21a..0000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,169 +0,0 @@ -parser: '@typescript-eslint/parser' - -parserOptions: - ecmaFeatures: - jsx: true - ecmaVersion: latest - sourceType: module - project: ./tsconfig.json - -env: - browser: true - es6: true - node: true - jest: true - -ignorePatterns: ['index.js'] - -plugins: - - import - - '@typescript-eslint' - -extends: - - eslint:recommended - - plugin:prettier/recommended - -rules: - # 0 = off, 1 = warn, 2 = error - 'space-before-function-paren': 0 - 'no-useless-constructor': 0 - 'no-undef': 2 - 'no-console': [2, { allow: ['error', 'warn', 'info', 'assert'] }] - 'comma-dangle': ['error', 'only-multiline'] - 'no-unused-vars': 0 - 'no-var': 2 - 'one-var-declaration-per-line': 2 - 'prefer-const': 2 - 'no-const-assign': 2 - 'no-duplicate-imports': 2 - 'no-use-before-define': [2, { 'functions': false, 'classes': false }] - 'eqeqeq': [2, 'always', { 'null': 'ignore' }] - 'no-case-declarations': 0 - 'no-restricted-syntax': - [ - 2, - { - 'selector': 'BinaryExpression[operator=/(==|===|!=|!==)/][left.raw=true], BinaryExpression[operator=/(==|===|!=|!==)/][right.raw=true]', - 'message': Don't compare for equality against boolean literals, - }, - ] - - # https://github.com/benmosher/eslint-plugin-import/pull/334 - 'import/no-duplicates': 2 - 'import/first': 2 - 'import/newline-after-import': 2 - 'import/order': - [ - 2, - { - 'newlines-between': 'always', - 'alphabetize': { 'order': 'asc' }, - 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], - }, - ] - -overrides: - - files: - - ./**/*{.ts,.tsx} - rules: - 'no-unused-vars': [2, { varsIgnorePattern: '^_', argsIgnorePattern: '^_', ignoreRestSiblings: true }] - 'no-undef': 0 - # TypeScript declare merge - 'no-redeclare': 0 - 'no-useless-constructor': 0 - 'no-dupe-class-members': 0 - 'no-case-declarations': 0 - 'no-duplicate-imports': 0 - # TypeScript Interface and Type - 'no-use-before-define': 0 - - '@typescript-eslint/adjacent-overload-signatures': 2 - '@typescript-eslint/await-thenable': 2 - '@typescript-eslint/consistent-type-assertions': 2 - '@typescript-eslint/ban-types': - [ - 'error', - { - 'types': - { - 'String': { 'message': 'Use string instead', 'fixWith': 'string' }, - 'Number': { 'message': 'Use number instead', 'fixWith': 'number' }, - 'Boolean': { 'message': 'Use boolean instead', 'fixWith': 'boolean' }, - 'Function': { 'message': 'Use explicit type instead' }, - }, - }, - ] - '@typescript-eslint/explicit-member-accessibility': - [ - 'error', - { - accessibility: 'explicit', - overrides: - { - accessors: 'no-public', - constructors: 'no-public', - methods: 'no-public', - properties: 'no-public', - parameterProperties: 'explicit', - }, - }, - ] - '@typescript-eslint/method-signature-style': 2 - '@typescript-eslint/no-floating-promises': 2 - '@typescript-eslint/no-implied-eval': 2 - '@typescript-eslint/no-for-in-array': 2 - '@typescript-eslint/no-inferrable-types': 2 - '@typescript-eslint/no-invalid-void-type': 2 - '@typescript-eslint/no-misused-new': 2 - '@typescript-eslint/no-misused-promises': 2 - '@typescript-eslint/no-namespace': 2 - '@typescript-eslint/no-non-null-asserted-optional-chain': 2 - '@typescript-eslint/no-throw-literal': 2 - '@typescript-eslint/no-unnecessary-boolean-literal-compare': 2 - '@typescript-eslint/prefer-for-of': 2 - '@typescript-eslint/prefer-nullish-coalescing': 2 - '@typescript-eslint/switch-exhaustiveness-check': 2 - '@typescript-eslint/prefer-optional-chain': 2 - '@typescript-eslint/prefer-readonly': 2 - '@typescript-eslint/prefer-string-starts-ends-with': 0 - '@typescript-eslint/no-array-constructor': 2 - '@typescript-eslint/require-await': 2 - '@typescript-eslint/return-await': 2 - '@typescript-eslint/ban-ts-comment': - [2, { 'ts-expect-error': false, 'ts-ignore': true, 'ts-nocheck': true, 'ts-check': false }] - '@typescript-eslint/naming-convention': - [ - 2, - { - selector: 'memberLike', - format: ['camelCase', 'PascalCase'], - modifiers: ['private'], - leadingUnderscore: 'forbid', - }, - ] - '@typescript-eslint/no-unused-vars': - [2, { varsIgnorePattern: '^_', argsIgnorePattern: '^_', ignoreRestSiblings: true }] - '@typescript-eslint/member-ordering': - [ - 2, - { - default: - [ - 'public-static-field', - 'protected-static-field', - 'private-static-field', - 'public-static-method', - 'protected-static-method', - 'private-static-method', - 'public-instance-field', - 'protected-instance-field', - 'private-instance-field', - 'public-constructor', - 'protected-constructor', - 'private-constructor', - 'public-instance-method', - 'protected-instance-method', - 'private-instance-method', - ], - }, - ] diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..4ffd99f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +patreon: twlite +custom: ['https://buymemomo.com/twilight'] diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a9aa7c8..99911ef 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -17,21 +17,25 @@ on: - main tags-ignore: - '**' - paths-ignore: - - '**/*.md' - - LICENSE - - '**/*.gitignore' - - .editorconfig - - docs/** + paths: + - packages/webview/** + - bun.lock + - package.json + - turbo.json + - .cargo/** + - rustfmt.toml + - .github/workflows/CI.yml pull_request: branches: - main - paths-ignore: - - '**/*.md' - - LICENSE - - '**/*.gitignore' - - .editorconfig - - docs/** + paths: + - packages/webview/** + - bun.lock + - package.json + - turbo.json + - .cargo/** + - rustfmt.toml + - .github/workflows/CI.yml concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -211,10 +215,17 @@ jobs: path: | ~/.bun/install/cache node_modules - key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb', '**/package-lock.json') }} + key: ${{ runner.os }}-bun-${{ runner.os == 'Windows' && 'hoisted-' || '' }}${{ hashFiles('bun.lock') }} restore-keys: | ${{ runner.os }}-bun- + # The Windows runner can retain an isolated .bun dependency tree whose + # package links fail with EPERM when the NAPI CLI loads its dependencies. + - name: Reset cached Windows dependencies + if: runner.os == 'Windows' + shell: pwsh + run: if (Test-Path node_modules) { Remove-Item -LiteralPath node_modules -Recurse -Force } + - name: Cache Cargo registry uses: actions/cache@v4 with: @@ -229,7 +240,7 @@ jobs: - name: Cache Cargo build uses: actions/cache@v4 with: - path: target/ + path: packages/webview/target/ key: ${{ matrix.settings.target }}-cargo-build-${{ github.sha }} restore-keys: | ${{ matrix.settings.target }}-cargo-build- @@ -378,17 +389,23 @@ jobs: done - name: Install dependencies + if: runner.os != 'Windows' run: bun install --frozen-lockfile + - name: Install Windows dependencies with hoisted linker + if: runner.os == 'Windows' + run: bun install --frozen-lockfile --linker hoisted + - name: Build run: ${{ matrix.settings.build_script || format('bun run build --target {0}', matrix.settings.target) }} shell: bash + working-directory: packages/webview - name: Upload artifact uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.settings.target }} - path: ${{ env.APP_NAME }}.*.node + path: packages/webview/${{ env.APP_NAME }}.*.node if-no-files-found: error retention-days: 7 @@ -430,13 +447,16 @@ jobs: node -v bun --version bun install --frozen-lockfile - bun run build + cd packages/webview + # FreeBSD exports only the unsupported native stub; its generated + # bindings intentionally omit the public API, so skip the JS typecheck. + bun run build:native - name: Upload artifact uses: actions/upload-artifact@v4 with: name: bindings-x86_64-unknown-freebsd - path: ${{ env.APP_NAME }}.*.node + path: packages/webview/${{ env.APP_NAME }}.*.node if-no-files-found: error retention-days: 7 @@ -470,37 +490,173 @@ jobs: - name: Download all artifacts uses: actions/download-artifact@v4 with: - path: artifacts + path: packages/webview/artifacts - name: Move artifacts run: bun run artifacts + working-directory: packages/webview - name: List packages run: ls -R ./npm shell: bash + working-directory: packages/webview - name: Extract version from commit message id: version + shell: bash run: | - COMMIT_MSG=$(git log -1 --pretty=%B) - if echo "$COMMIT_MSG" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+$"; then - echo "is_release=true" >> $GITHUB_OUTPUT - echo "tag=latest" >> $GITHUB_OUTPUT - elif echo "$COMMIT_MSG" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]"; then - echo "is_release=true" >> $GITHUB_OUTPUT - echo "tag=next" >> $GITHUB_OUTPUT + set -euo pipefail + COMMIT_MSG="$(git log -1 --pretty=%B)" + stable_regex='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' + prerelease_regex='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)-[A-Za-z0-9-]+\.(0|[1-9][0-9]*)$' + + if [[ "$COMMIT_MSG" =~ $stable_regex ]]; then + VERSION="$COMMIT_MSG" + TAG=latest + IS_RELEASE=true + elif [[ "$COMMIT_MSG" =~ $prerelease_regex ]]; then + VERSION="$COMMIT_MSG" + TAG="${VERSION#*-}" + TAG="${TAG%%.*}" + if [[ "$TAG" =~ ^[0-9]+$ || "$TAG" =~ ^[vV][0-9]+$ || "$TAG" == x || "$TAG" == X ]]; then + TAG=next + fi + IS_RELEASE=true else - echo "is_release=false" >> $GITHUB_OUTPUT + VERSION= + TAG=latest + IS_RELEASE=false + fi + + echo "is_release=$IS_RELEASE" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + - name: Verify release version + if: steps.version.outputs.is_release == 'true' + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + WEBVIEW_VERSION="$(node -p 'require("./packages/webview/package.json").version')" + if [[ "$WEBVIEW_VERSION" != "$VERSION" ]]; then + echo "Release version $VERSION does not match packages/webview/package.json version $WEBVIEW_VERSION." >&2 + exit 1 fi - name: Publish to npm if: steps.version.outputs.is_release == 'true' run: | npm config set provenance true - npm publish --access public --tag ${{ steps.version.outputs.tag }} + npm publish --access public --tag "${{ steps.version.outputs.tag }}" + working-directory: packages/webview + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Build and publish creator packages + if: steps.version.outputs.is_release == 'true' env: + VERSION: ${{ steps.version.outputs.version }} + TAG: ${{ steps.version.outputs.tag }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + set -euo pipefail + + WEBVIEW_VERSION="$(node -p 'require("./packages/webview/package.json").version')" + if [[ "$WEBVIEW_VERSION" != "$VERSION" ]]; then + echo "Refusing creator publish: release version $VERSION does not match WebviewJS version $WEBVIEW_VERSION." >&2 + exit 1 + fi + + bun --filter create-webview build + + STAGING_DIR="$(mktemp -d "$RUNNER_TEMP/create-webview.XXXXXX")" + cp packages/create-webview/package.json "$STAGING_DIR/package.json" + cp packages/create-webview/README.md "$STAGING_DIR/README.md" + cp packages/create-webview/LICENSE "$STAGING_DIR/LICENSE" + cp -R packages/create-webview/dist "$STAGING_DIR/dist" + + node --input-type=module - "$STAGING_DIR/package.json" "$VERSION" <<'NODE' + import { readFile, writeFile } from 'node:fs/promises'; + const [manifestPath, version] = process.argv.slice(2); + const manifest = JSON.parse(await readFile(manifestPath, 'utf8')); + manifest.name = 'create-webview'; + manifest.version = version; + if (manifest.bin?.['create-webview'] !== './dist/index.js' || manifest.bin?.['create-webview-app'] !== './dist/index.js') { + throw new Error('Staged creator manifest does not contain both shared CLI aliases.'); + } + await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8'); + NODE + + node --input-type=module - "$STAGING_DIR/package.json" "$VERSION" <<'NODE' + import { access, readFile, readdir } from 'node:fs/promises'; + const [manifestPath, version] = process.argv.slice(2); + const manifest = JSON.parse(await readFile(manifestPath, 'utf8')); + const files = (await readdir(new URL('.', `file://${manifestPath}`))).sort(); + if (manifest.name !== 'create-webview' || manifest.version !== version) { + throw new Error(`Unexpected staged manifest identity: ${manifest.name}@${manifest.version}`); + } + for (const expected of ['LICENSE', 'README.md', 'dist', 'package.json']) { + if (!files.includes(expected)) throw new Error(`Missing staged package file: ${expected}`); + } + await access(new URL('./dist/index.js', `file://${manifestPath}`)); + NODE + + npm config set provenance true + + package_version_exists() { + local package_name="$1" + local output + if output="$(npm view "${package_name}@${VERSION}" version --json 2>&1)"; then + echo "${package_name}@${VERSION} is already published; skipping." + return 0 + fi + + if [[ "$output" == *E404* ]]; then + return 10 + fi + + printf '%s\n' "$output" >&2 + echo "Could not verify whether ${package_name}@${VERSION} exists." >&2 + return 2 + } + + publish_if_missing() { + local package_name="$1" + local status=0 + package_version_exists "$package_name" || status=$? + + if [[ "$status" -eq 0 ]]; then + return 0 + elif [[ "$status" -ne 10 ]]; then + return "$status" + fi + + echo "Publishing ${package_name}@${VERSION} with npm tag ${TAG}." + npm publish --access public --tag "$TAG" + } + + cd "$STAGING_DIR" + publish_if_missing create-webview + + node --input-type=module - "package.json" <<'NODE' + import { readFile, writeFile } from 'node:fs/promises'; + const manifest = JSON.parse(await readFile('package.json', 'utf8')); + manifest.name = 'create-webview-app'; + await writeFile('package.json', `${JSON.stringify(manifest, null, 2)}\n`, 'utf8'); + NODE + + node --input-type=module - "$VERSION" <<'NODE' + import { readFile } from 'node:fs/promises'; + const [version] = process.argv.slice(2); + const manifest = JSON.parse(await readFile('package.json', 'utf8')); + if (manifest.name !== 'create-webview-app' || manifest.version !== version) { + throw new Error('Mirror package identity changed unexpectedly.'); + } + NODE + publish_if_missing create-webview-app - name: Skip publish (not a release) if: steps.version.outputs.is_release == 'false' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..3c28d57 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,64 @@ +name: Deploy docs + +on: + push: + branches: + - main + paths: + - apps/docs/** + - bun.lock + - package.json + - turbo.json + - .github/workflows/docs.yml + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Configure GitHub Pages + uses: actions/configure-pages@v5 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.14 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build static docs + run: bun run build --filter=@webviewjs/docs + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: apps/docs/out + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5e12853..3d8a95d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: Lint +name: Lint and docs on: push: @@ -7,27 +7,31 @@ on: tags-ignore: - '**' pull_request: + branches: + - main + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true + jobs: - lint: - name: Lint + checks: + name: Repository checks runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Setup bun + - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.14 - - name: Setup node + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 24 - - name: Install + - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt @@ -38,19 +42,45 @@ jobs: sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libglib2.0-dev pkg-config - name: Install dependencies - run: bun install + run: bun install --frozen-lockfile - - name: ESLint + - name: Oxlint run: bun run lint - - name: Cargo fmt - run: cargo fmt -- --check + - name: Format check + run: bun run format:check - name: Clippy - run: bun run clippy + run: cargo clippy --manifest-path packages/webview/Cargo.toml -- -D warnings - name: Cargo check - run: bun run check + run: cargo check --manifest-path packages/webview/Cargo.toml + + - name: Build native addon for tests + run: bun run build:native --target x86_64-unknown-linux-gnu + working-directory: packages/webview - name: Run tests run: bun run test + + docs: + name: Build documentation site + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.14 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build static docs + run: bun run build --filter=@webviewjs/docs diff --git a/.gitignore b/.gitignore index 9fe1d5c..acdcab1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,156 +1,31 @@ - -# Created by https://www.toptal.com/developers/gitignore/api/node -# Edit at https://www.toptal.com/developers/gitignore?templates=node - -### Node ### -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories +# JavaScript tooling node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache +.turbo/ *.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' +coverage/ *.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# End of https://www.toptal.com/developers/gitignore/api/node - -superpowers - -#Added by cargo - -/target -Cargo.lock - +.npm/ +dist/ + +# Fumadocs / Next.js static site +apps/docs/.next/ +apps/docs/.source/ +apps/docs/out/ +apps/docs/next-env.d.ts + +# Webview build output +packages/webview/dist/ +packages/webview/target/ +packages/webview/npm/ +packages/webview/artifacts/ +packages/webview/Cargo.lock *.node -.pnp.* - -# Bun -bun.lockb +target/ -# Yarn (legacy) -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions - -# Build artifacts -*.a -*.lib -*.o -*.obj -*.so -*.dylib -*.dll -*.exe - -# Rust -.cargo-cache/ -Cargo.lock - -# IDE -.idea/ -*.swp -*.swo -*~ +# Local environment and logs +.env +.env.* +!.env.example +logs/ +*.log .DS_Store diff --git a/.prettierignore b/.prettierignore index fc90384..5fb656a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,11 @@ -target -.yarn -npm \ No newline at end of file +node_modules +.turbo +**/.next +**/.source +**/out +**/dist +packages/webview/target +packages/webview/npm +packages/webview/js-bindings.js +packages/webview/js-bindings.d.ts +*.node diff --git a/AGENTS.md b/AGENTS.md index 97a0974..4e9b753 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# WebviewJS — CLAUDE.md +# WebviewJS repository instructions ## What this project is @@ -6,94 +6,114 @@ A native Node.js (NAPI-RS) binding to [wry](https://github.com/tauri-apps/wry) a ## Repo layout -``` -src/ Rust source (NAPI-RS bindings) -cli/ The `webview` / `webviewjs` CLI (pure JS, ESM) - index.mjs Entry point — argument parsing, help, dispatch - build.mjs Compile logic for node (SEA), deno compile, bun build --compile - utils.mjs String helpers -docs/ GitHub Pages site (served at webview.js.org) - README.md SYMLINK → ../README.md (the root README is the Pages homepage) - api/ API reference markdown - guides/ How-to guides - getting-started/ - platform/ -examples/ Runnable .mjs examples -npm/ Per-platform NAPI binary packages -scripts/ Build helpers -__test__/ Node.js built-in test runner tests -index.js Generated JS bindings (do not edit manually) -index.d.ts Generated TypeScript types (do not edit manually) -js-bindings.js / js-bindings.d.ts Lower-level generated bindings +```text +apps/docs/ Static Next.js + Fumadocs site for webview.js.org + app/ App Router routes and layouts + content/docs/ Markdown/MDX documentation source + public/CNAME GitHub Pages custom domain + out/ Static export output (generated) +apps/examples/ Runnable .mjs examples and their assets +packages/webview/ Published @webviewjs/webview package + src/ Rust NAPI-RS bindings + lib/ Handwritten TypeScript public API layer + cli/ Shipped ESM webview/webviewjs CLI + scripts/ Build helpers + __test__/ Node.js built-in test runner tests + npm/ Per-platform NAPI packages + js-bindings.js Generated and tracked NAPI-RS bindings + js-bindings.d.ts Generated and tracked NAPI-RS declarations +packages/create-webview/ Official WebviewJS project scaffolder +.cargo/ Root Cargo configuration +.github/workflows/ Native CI, repository checks, and Pages deployment +.husky/ Root Git hooks +assets/ Repository-level project assets +skills/ WebviewJS skill content ``` ## Tech stack -| Layer | Tool | -| -------------------- | -------------------------------- | -| Rust bindings | NAPI-RS (`@napi-rs/cli`) | -| Package manager | Bun (use `bun` for all JS tasks) | -| JS runtime for tests | Node.js >= 24 (`node --test`) | -| Linter | oxlint | -| Formatter | Prettier | -| Rust formatter | `cargo fmt` | +| Layer | Tool | +| -------------------- | ----------------------------- | +| Monorepo | Bun workspaces + Turborepo | +| Rust bindings | NAPI-RS (`@napi-rs/cli`) | +| Package manager | Bun 1.3.14 | +| JS runtime for tests | Node.js >= 24 (`node --test`) | +| Linter | Root Oxlint | +| Formatter | Root Prettier | +| Rust formatter | `cargo fmt` | ## Common commands +Run these from the repository root: + ```bash -bun install # install JS deps -bun run build # release build (Rust + bindings) -bun run build:debug # debug build -node --test __test__/*.test.* # run tests -bun run lint # oxlint -bun run format # prettier + cargo fmt -cargo check # fast Rust type check -cargo clippy # Rust lints +bun install +bun run dev +bun run build +bun run test +bun run lint +bun run lint:fix +bun run format +bun run format:check +bun run check +bun run clippy ``` -## Key conventions +To build the published package directly: + +```bash +bun --filter @webviewjs/webview build +``` -- **Do not edit** `index.js`, `index.d.ts`, `js-bindings.js`, `js-bindings.d.ts` — these are generated by `napi` from Rust source. -- CLI files (`cli/*.mjs`) are plain ESM — no build step, shipped as-is. -- Tests live in `__test__/` and use `node:test` (not Jest/Vitest/AVA). -- Examples in `examples/` are standalone `.mjs` files; run them directly with `node examples/foo.mjs` after building. +Run an example after building the package with `node apps/examples/simple.mjs`. To work on docs, use `bun --filter @webviewjs/docs dev` or `bun --filter @webviewjs/docs build`. -## CLI (`webview --build`) +## Key conventions -The CLI compiles a user's app into a standalone executable. The runtime is **auto-detected** from globals: +- Handwritten public library source belongs in `packages/webview/lib/`; TypeScript writes CommonJS and declarations to `packages/webview/dist/`. +- Native NAPI classes remain the public runtime objects. TypeScript explicitly augments them with JS-only lifecycle, event, protocol, and IPC behavior. +- Do not edit `packages/webview/js-bindings.js` or `packages/webview/js-bindings.d.ts`; NAPI-RS generates them from Rust source and both generated files are intentionally tracked. +- `packages/webview/cli/index.mjs` is the permanent plain ESM npm bin shim. The TypeScript CLI implementation lives in `packages/webview/lib/cli/` and builds to `packages/webview/dist/cli/`. +- Tests live in `packages/webview/__test__/` and use `node:test`, not Bun test, Jest, Vitest, or AVA. +- Examples in `apps/examples/` import `@webviewjs/webview` through the Bun workspace dependency so they use the same package entry point as consumers. +- Documentation lives in `apps/docs/content/docs/`. Its public routes start at the domain root, such as `/getting-started/installation` and `/api/application`. +- Keep repository-level tools in the root package. Application and package dependencies belong to their respective workspaces. -- `globalThis.Bun` present → default `--runtime bun` -- `globalThis.Deno` present → default `--runtime deno` -- otherwise → default `--runtime node` +## CLI (`webview build`) -Override with `--runtime `. Each runtime uses a different compiler: +The CLI compiles a user's app into a standalone executable. Node.js is the default runtime; choose Bun or Deno with `--runtime `: -| Runtime | Mechanism | -| ------- | ---------------------------------------------------------------------------------------- | -| `node` | Node.js SEA (Single Executable Application) via `--experimental-sea-config` + `postject` | -| `deno` | `deno compile --allow-all --no-check` | -| `bun` | `bun build --compile` | +| Runtime | Mechanism | +| ------- | --------------------------------------------------------------------------------------------------------- | +| `node` | Node.js SEA; Node 24 uses `--experimental-sea-config` + pinned Postject; Node 25.5+ can use `--build-sea` | +| `deno` | Deno 2 `compile --bundle --self-extracting --allow-all` | +| `bun` | `bun build --compile` | -The `--resources` flag (JSON asset map) is **node-only** — it maps to the SEA `assets` field. +The entry file is positional, for example `webview build src/main.ts`. Use `--asset` to embed files or directories; `--resources` remains available for JSON asset maps. Each runtime adapter embeds the matching WebviewJS native addon. ## Docs / GitHub Pages -- Site root: `docs/` → served at **webview.js.org** -- `docs/README.md` is a **git symlink** to `../README.md` (the repo root README is the Pages homepage) -- The symlink is stored in git as mode `120000`. On Windows it appears as a regular file in the working tree (Windows symlink limitation) — this is expected. It resolves correctly on the Linux Pages server. -- Navigation links in `docs/` use paths relative to `docs/` (e.g. `./getting-started/installation.md`). Links in the root README use `./docs/`-prefixed paths for GitHub.com rendering. -- To add a new guide: create `docs/guides/your-guide.md`, then add a row to the Guides table in the root `README.md`. +- The Fumadocs site is in `apps/docs/` and exports to `apps/docs/out/`. +- Documentation source is in `apps/docs/content/docs/`; the section order is controlled by its `meta.json` files. +- The static site includes `apps/docs/public/CNAME` for **webview.js.org**, static Orama search, `/llms.txt`, `/llms-full.txt`, and per-page Markdown. +- To add a guide, create it under `apps/docs/content/docs/guides/` and add it to `apps/docs/content/docs/guides/meta.json`. +- Documentation routes use the domain root, for example `/guides/custom-protocols`. ## Adding a new CLI flag -1. Add the option object to `options` in `cli/index.mjs`. -2. Destructure from `args.values` in the relevant branch. -3. Pass it through to the appropriate `build*` function in `cli/build.mjs`. -4. Update `docs/guides/building-executables.md` options table. +1. Add or update argument parsing in `packages/webview/lib/cli/args.ts`. +2. Pass the parsed option to the relevant command or runtime implementation under `packages/webview/lib/cli/`. +3. Update runtime-specific behavior in `packages/webview/lib/cli/runtimes/` when needed. +4. Update the options table in `apps/docs/content/docs/guides/building-executables.md`. + +`packages/webview/cli/index.mjs` is the small permanent ESM npm-bin bootstrap. Do not put CLI implementation logic there. ## Rust / NAPI notes -- Bindings are declared in `src/`. After editing Rust, run `bun run build` to regenerate `index.js` / `index.d.ts`. -- Target list is in `package.json` under `napi.targets`. -- Each target gets its own npm package under `npm/`. -- The `.node` binary is loaded at runtime via NAPI-RS's platform detection in `index.js`. +- Bindings are declared in `packages/webview/src/`. After editing Rust, build from the Webview package with `bun --filter @webviewjs/webview build` to regenerate `js-bindings.js` / `js-bindings.d.ts` and compile the TypeScript layer. +- The NAPI target list is in `packages/webview/package.json` under `napi.targets`. +- Each target gets its own package under `packages/webview/npm/`. +- The crate manifest is `packages/webview/Cargo.toml`. There is no root Cargo workspace; root Rust commands should pass this manifest explicitly. +- Native build outputs and Cargo's `target/` directory live under `packages/webview/`. +- Native builds are platform-specific and are not cached by Turborepo. +- Publishing the public package must run from `packages/webview/`, never from the private repository root. +- The creator source lives only in `packages/create-webview/`. `create-webview` is the canonical npm package; `create-webview-app` is published by mirroring that exact package during release. There is intentionally no `packages/create-webview-app/` workspace. diff --git a/README.md b/README.md index 23e34ab..cef9f8b 100644 --- a/README.md +++ b/README.md @@ -1,570 +1,77 @@ -# `@webviewjs/webview` +# WebviewJS [![CI](https://github.com/webviewjs/webview/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/webviewjs/webview/actions/workflows/CI.yml) [![npm](https://img.shields.io/npm/v/%40webviewjs%2Fwebview?logo=npm)](https://www.npmjs.com/package/@webviewjs/webview) [![License](https://img.shields.io/github/license/webviewjs/webview)](https://github.com/webviewjs/webview/blob/main/LICENSE) -Build lightweight native desktop applications with JavaScript and the webview -engine already provided by the operating system. WebviewJS is a typed N-API -binding to [tao](https://github.com/tauri-apps/tao) and -[wry](https://github.com/tauri-apps/wry), with a small JavaScript surface and -first-class support for Node.js, Bun, and Deno. +WebviewJS is a typed JavaScript binding for building desktop applications with the webview provided by the operating system. It connects Node.js, Bun, or Deno to native windows through N-API and the `tao` and `wry` libraries. -It is a good fit for utilities, internal tools, desktop companions, and -existing web applications that need a native window without bundling a second -browser engine. +This repository contains the published package, its documentation site, runnable examples, and the tooling used to develop them. -[Get started](https://webview.js.org/quickstart.md) · -[API reference](https://webview.js.org/api/application.md) · -[Examples](./examples) · -[Platform notes](https://webview.js.org/platforms/linux.md) +[Documentation](https://webview.js.org) · [npm package](https://www.npmjs.com/package/@webviewjs/webview) · [Examples](https://github.com/webviewjs/webview/tree/main/apps/examples) · [Issues](https://github.com/webviewjs/webview/issues) -![WebviewJS preview](https://github.com/webviewjs/webview/raw/main/assets/preview.png) +## Repository -## Highlights +| Workspace | Purpose | +| ------------------------- | -------------------------------------------------------------- | +| `packages/webview` | Published `@webviewjs/webview` native binding and CLI | +| `packages/create-webview` | Official WebviewJS project scaffolder (`create-webview`) | +| `apps/docs` | Documentation site at [webview.js.org](https://webview.js.org) | +| `apps/examples` | Runnable JavaScript examples | -- Native windows backed by WebView2, WebKit, or WebKitGTK instead of a bundled browser runtime. -- Non-blocking event pumping that keeps ordinary JavaScript timers and I/O responsive. -- Typed APIs for windows, webviews, menus, dialogs, cookies, DevTools, and window controls. -- Shared browser contexts for profiles, cookies, cache, storage, and automation. -- System tray icons, native notifications, and platform-specific window extensions. -- IPC through `window.ipc.postMessage()` plus Promise-based `webview.expose()` namespaces. -- Fetch-compatible asynchronous custom protocols, including Hono routing without an HTTP server. -- A CLI for compiling Node.js, Deno, or Bun applications into standalone executables. - -> [!NOTE] -> WebviewJS provides the native window and webview layer. It is intentionally -> smaller than an application framework such as Electron or Tauri, so you can -> choose your own frontend, routing, state management, and build tools. - -## Documentation - -The complete documentation is available at [webview.js.org](https://webview.js.org). For an index designed for documentation tools and assistants, see [llms.txt](https://webview.js.org/llms.txt). - -### Getting started - -| | | -| ------------------------------------------------------ | ------------------------------- | -| [Installation](https://webview.js.org/installation.md) | System requirements and setup | -| [Quick Start](https://webview.js.org/quickstart.md) | Your first window in minutes | -| [Event Loop](https://webview.js.org/event-loop.md) | How the non-blocking pump works | - -### API reference - -| | | -| ------------------------------------------------------------- | ----------------------------------------------------- | -| [Application](https://webview.js.org/api/application.md) | Root object, event loop, windows, menus | -| [BrowserWindow](https://webview.js.org/api/browser-window.md) | OS window, size, position, cursor, decorations | -| [Webview](https://webview.js.org/api/webview.md) | Embedded browser, navigation, cookies, script, bounds | -| [WebContext](https://webview.js.org/api/web-context.md) | Shared browser data, profiles, and automation | -| [System Tray](https://webview.js.org/api/tray.md) | Tray icons, menus, updates, and pointer events | -| [Notification](https://webview.js.org/api/notification.md) | Native desktop notifications and lifecycle events | -| [Menu](https://webview.js.org/api/menu.md) | Native menu bar construction | -| [Types](https://webview.js.org/api/types.md) | Shared interfaces and enums | - -### Guides - -| | | -| ----------------------------------------------------------------------------- | ----------------------------------------------- | -| [Building Executables](https://webview.js.org/guides/building-executables.md) | Compile to `.exe` / binary with node, deno, bun | -| [IPC Messaging](https://webview.js.org/guides/ipc-messaging.md) | Page ↔ Node communication | -| [Menus](https://webview.js.org/guides/menus.md) | Building menu bars with roles and accelerators | -| [Multiple Windows](https://webview.js.org/guides/multiple-windows.md) | Managing several windows | -| [Cookies & Storage](https://webview.js.org/guides/cookies-and-storage.md) | Reading, writing, and clearing cookies | -| [Custom Protocols](https://webview.js.org/guides/custom-protocols.md) | Serving local content to the webview | - -### Platform notes - -| | | -| ------------------------------------------------------ | ----------------------------------------- | -| [Windows](https://webview.js.org/platforms/windows.md) | WebView2, taskbar, DPI | -| [macOS](https://webview.js.org/platforms/macos.md) | WebKit, main-thread requirement, app menu | -| [Linux](https://webview.js.org/platforms/linux.md) | WebKitGTK, Wayland/X11, menu limitations | -| [iOS](https://webview.js.org) | Orientation, status bar, and gestures | -| [Android](https://webview.js.org) | Content rectangle and configuration | - -## Installation +## Development -Install the package with your preferred JavaScript package manager. The native -platform package is resolved automatically through optional dependencies. +Install [Bun 1.3.14](https://bun.sh/), [Rust stable](https://www.rust-lang.org/tools/install), and [Node.js 24 or newer](https://nodejs.org/) for package tests. ```bash -npm install @webviewjs/webview -# or -bun add @webviewjs/webview -# or -pnpm add @webviewjs/webview -``` - -Keep optional dependencies enabled when installing. They contain the native -addon selected for the current operating system and architecture. - -### System requirements - -| Platform | Requirements | -| --------------- | ----------------------------------------------------------------------------------------------------------- | -| Windows | WebView2. It ships with Windows 11 and current Edge installations; Windows 10 can install it automatically. | -| macOS | macOS 10.15 Catalina or later. WebKit is built in. | -| Linux | WebKitGTK 4.1 and `libxdo`. See the [Linux platform guide](https://webview.js.org/platforms/linux.md). | -| Android and iOS | Native project setup and platform SDKs. See the [hosted documentation](https://webview.js.org). | - -For distribution and platform-specific behavior, review the complete -[installation guide](https://webview.js.org/installation.md) before shipping. - -## Supported platforms - -| Target | Platform | Architecture | Status | Notes | -| ------------------------------- | -------- | ------------ | ------------ | ---------------------------------------------- | -| `x86_64-pc-windows-msvc` | Windows | x64 | Supported | WebView2 | -| `i686-pc-windows-msvc` | Windows | x86 | Supported | WebView2 | -| `aarch64-pc-windows-msvc` | Windows | arm64 | Supported | WebView2 | -| `x86_64-apple-darwin` | macOS | x64 | Supported | WebKit | -| `aarch64-apple-darwin` | macOS | arm64 | Supported | WebKit | -| `x86_64-unknown-linux-gnu` | Linux | x64 | Supported | WebKitGTK 4.1, X11, and Wayland | -| `i686-unknown-linux-gnu` | Linux | x86 | Supported | WebKitGTK 4.1, X11, and Wayland | -| `aarch64-unknown-linux-gnu` | Linux | arm64 | Supported | WebKitGTK 4.1, X11, and Wayland | -| `armv7-unknown-linux-gnueabihf` | Linux | armv7 | Supported | WebKitGTK 4.1, X11, and Wayland | -| `aarch64-linux-android` | Android | arm64 | Experimental | Platform APIs are still evolving | -| `armv7-linux-androideabi` | Android | armv7 | Experimental | Platform APIs are still evolving | -| `x86_64-unknown-freebsd` | FreeBSD | x64 | Stub | Package resolution only; no GUI implementation | - -## Examples - -### Quick start - -```js -import { Application } from '@webviewjs/webview'; - -const app = new Application(); -const window = app.createBrowserWindow({ - title: 'WebviewJS', - width: 1024, - height: 768, -}); - -window.createWebview({ url: 'https://example.com' }); -app.run(); -``` - -For CommonJS projects, use the same API through `require()`: - -```js -const { Application } = require('@webviewjs/webview'); - -const app = new Application(); -const window = app.createBrowserWindow({ title: 'WebviewJS' }); -window.createWebview({ html: '

Hello from WebviewJS

' }); -app.run(); -``` - -The application owns native resources created through it. Call `app.exit()` -when your application needs to shut down explicitly: - -```js -process.on('SIGINT', () => { - app.exit(); -}); -``` - -### Event pumping - -`app.whenReady()` starts the non-blocking event pump by default: - -```js -await app.whenReady({ interval: 16, ref: true }); -``` - -For manual startup, disable auto-run: - -```js -const ready = app.whenReady({ autoRun: false }); -app.run({ interval: 16, ref: true }); -await ready; -``` - -`interval` defaults to `16` milliseconds and `ref` defaults to `true`. Use `app.pumpEvents()` for manual pumping. - -### System tray - -Keep a strong JavaScript reference when you need to call tray methods or keep -its listeners reachable: - -```js -let tray = null; - -app.whenReady().then(() => { - tray = app.createTrayIcon({ - id: 'main', - icon: { data: rgba, width: 16, height: 16 }, - tooltip: 'My application', - menu: { items: [{ id: 'quit', label: 'Quit' }] }, - }); - - tray.on('click', (event) => console.log(event)); -}); -``` - -See the [system tray reference](https://webview.js.org/api/tray.md) and -[runnable tray example](./examples/tray.mjs). - -### Notifications - -```js -import { Notification } from '@webviewjs/webview'; - -const notification = new Notification('Build complete', { - body: 'The release executable is ready.', -}); - -notification.on('click', () => console.log('notification clicked')); -notification.on('error', ({ error }) => console.error(error)); -``` - -Notification permission is always `"granted"` for native applications. See the -[notification reference](https://webview.js.org/api/notification.md) and -[runnable notification example](./examples/notification.mjs). - -### IPC and exposed functions - -The webview page can send messages to Node through `window.ipc.postMessage()`: - -```js -const webview = window.createWebview({ ipcName: 'bindings' }); -webview.onIpcMessage((message) => console.log(message.body.toString())); -``` - -`ipcName` adds an alias, so the page can use `window.bindings.postMessage(...)`; `window.ipc` remains available. - -For typed request/response style calls, expose a namespace: - -```js -webview.expose('native', { - version: '0.1.4', - readConfig: async () => JSON.parse(await readFile('./config.json', 'utf8')), -}); -``` - -In the page: - -```js -console.log(window.native.version); -const config = await window.native.readConfig(); -``` - -Every exposed function returns a Promise in the page. Values, arguments, and results must be JSON-serializable. Violations use `SerializationError`. - -### Asynchronous custom protocols - -Register a protocol before creating its webview: - -```js -window.registerProtocol('app', async (request) => { - const filePath = join(process.cwd(), 'dist', new URL(request.url).pathname); - try { - return new Response(await readFile(filePath), { - headers: { 'Content-Type': 'text/html; charset=utf-8' }, - }); - } catch { - return new Response('Not found', { - status: 404, - headers: { 'Content-Type': 'text/plain; charset=utf-8' }, - }); - } -}); - -window.createWebview({ url: 'app://localhost/index.html' }); -``` - -See [Custom Protocols](https://webview.js.org/guides/custom-protocols.md), [IPC](https://webview.js.org/guides/ipc-messaging.md), and the runnable [custom protocol](examples/custom-protocol.mjs) and [expose](examples/expose.mjs) examples. - -### Menu system - -WebviewJS provides a cross-platform menu system that works on macOS, Windows, and Linux. - -#### Basic menu setup - -```js -import { Application } from '@webviewjs/webview'; - -const app = new Application(); - -// Set global application menu -app.setMenu({ - items: [ - { - label: 'File', - submenu: { - items: [ - { id: 'new', label: 'New', accelerator: 'CmdOrCtrl+N' }, - { id: 'open', label: 'Open', accelerator: 'CmdOrCtrl+O' }, - { role: 'separator' }, - { id: 'quit', label: 'Quit', accelerator: 'CmdOrCtrl+Q' }, - ], - }, - }, - { - label: 'Edit', - submenu: { - items: [{ role: 'copy' }, { role: 'paste' }, { role: 'cut' }, { role: 'selectall' }], - }, - }, - ], -}); - -const window = app.createBrowserWindow(); -const webview = window.createWebview({ url: 'https://nodejs.org' }); - -app.run(); -``` - -#### Menu event handling - -```js -import { Application } from '@webviewjs/webview'; - -const app = new Application(); - -// Handle menu events -app.on('custom-menu-click', ({ customMenuEvent: menuEvent }) => { - console.log(`Menu item clicked: ${menuEvent.id}`); - console.log(`From window: ${menuEvent.windowId}`); - - // Handle specific menu items - switch (menuEvent.id) { - case 'new': - console.log('Creating new document...'); - break; - case 'open': - console.log('Opening file...'); - break; - case 'quit': - app.exit(); - break; - } -}); - -// Set up menu... -app.setMenu({/* ... */}); -``` - -#### Window-specific menus - -```js -const app = new Application(); - -// Create window with custom menu -const window = app.createBrowserWindow({ - title: 'Custom Window', - menu: { - items: [ - { - id: 'window-action', - label: 'Window Action', - accelerator: 'Ctrl+W', - }, - ], - }, -}); - -// Or check if window has a menu -if (window.hasMenu()) { - console.log('This window has a menu'); -} -``` - -#### Menu item options - -- **`id`**: Unique identifier for the menu item (used in events) -- **`label`**: Display text for the menu item -- **`enabled`**: Whether the item is clickable (default: true) -- **`accelerator`**: Keyboard shortcut (e.g., "CmdOrCtrl+N", "Alt+F4") -- **`submenu`**: Nested menu items -- **`role`**: Predefined menu items with built-in behavior - -#### Predefined menu roles - -- **`"copy"`**: Standard copy action -- **`"paste"`**: Standard paste action -- **`"cut"`**: Standard cut action -- **`"selectall"`**: Select all text action -- **`"separator"`**: Visual separator line - -### IPC - -```js -const app = new Application(); -const window = app.createBrowserWindow(); - -const webview = window.createWebview({ - html: ` - - - Webview - - -

Hello world!

- - - - - `, - preload: `window.onIpcMessage = function(data) { - const output = document.getElementById('output'); - output.innerText = \`Server Sent A Message: \${data}\`; - }`, -}); - -if (!webview.isDevtoolsOpen()) webview.openDevtools(); - -webview.onIpcMessage((data) => { - const reply = `You sent ${data.body.toString('utf-8')}`; - webview.evaluateScript(`onIpcMessage("${reply}")`); -}); - -app.run(); -``` - -### Closing the application - -You can close the application, windows, and webviews gracefully to ensure all resources (including temporary folders) are cleaned up properly. - -```js -const app = new Application(); -const window = app.createBrowserWindow(); -const webview = window.createWebview({ url: 'https://nodejs.org' }); - -app.on('application-close-requested', () => { - console.log('Application is closing, cleaning up resources...'); -}); - -app.on('window-close-requested', () => { - console.log('Window close requested'); -}); - -// Close the application gracefully (cleans up temp folders) -app.exit(); - -// Or hide/show the window -window.hide(); // Hide the window -window.show(); // Show the window again - -// Or reload the webview -webview.reload(); +bun install +bun run build +bun run test +bun run lint +bun run format:check +bun run check ``` -For more details on application lifecycle and disposal, see the -[Application API reference](https://webview.js.org/api/application.md) and the -[closing example](./examples/close-example.mjs). - -### Keep strong references - -Retain `BrowserWindow`, `Webview`, `WebContext`, and `TrayIcon` wrappers for as -long as you need to call their methods or retain their JavaScript listeners. -Avoid discarded temporary handles: +## Working on the package -```js -const windows = []; +Build the native package and its TypeScript API with: -app.whenReady().then(() => { - const window = app.createBrowserWindow(); - const webview = window.createWebview({ url: 'https://example.com' }); - windows.push({ window, webview }); -}); +```bash +bun --filter @webviewjs/webview build ``` -The root `Application` owns native resources created through it. `app.exit()`, -`app[Symbol.dispose]()`, and application garbage collection dispose those -resources in shutdown order. Retained wrappers then report `isDisposed() === -true`, and method calls fail with a disposed error. Individual windows, -webviews, contexts, and tray icons also support `dispose()` and -`Symbol.dispose`. - -Check out [examples](./examples) directory for more examples: - -- **[menu-system.mjs](./examples/menu-system.mjs)** - Comprehensive menu system demonstration with all features -- **[window-menus.mjs](./examples/window-menus.mjs)** - Window-specific vs global menu examples -- **[http/](./examples/http/)** - Serving content from a web server to webview -- **[transparent.mjs](./examples/transparent.mjs)** - Transparent window example -- **[close-example.mjs](./examples/close-example.mjs)** - Graceful application closing - -Run any example with: `node examples/menu-system.mjs` (after building the project) - -## Building executables - -The `webview` CLI creates a standalone executable by delegating to the -selected runtime's compiler: +## Working on the docs -- Node.js uses Node's Single Executable Application (SEA) workflow. -- Deno uses `deno compile`. -- Bun uses `bun build --compile`. - -It is not a desktop application installer builder, package manager, or full -application bundler. It does not create platform installers or perform -cross-compilation. The WebviewJS `.node` addon is platform-specific, so build -on the target operating system and architecture, or prepare the matching -native addon and runtime toolchain manually before packaging. Use platform -distribution tools alongside this CLI for installers, release metadata, -signing, and notarization. - -> [!NOTE] -> The CLI is evolving. Review the runtime and platform requirements before -> distributing an executable. - -The `webview` CLI compiles your app into a single self-contained executable. The runtime is auto-detected (`Bun` → bun, `Deno` → deno, otherwise Node.js), or you can override it: +Start the documentation site locally with: ```bash -# Auto-detected runtime -webview --build --input ./path/to/your/script.js --output ./dist --name my-app - -# Explicit runtime -webview --build --runtime node --input ./src/index.js --name my-app -webview --build --runtime deno --input ./src/index.ts --name my-app -webview --build --runtime bun --input ./src/index.ts --name my-app +bun --filter @webviewjs/docs dev ``` -| Flag | Default | Description | -| -------------------- | ------------- | ----------------------------- | -| `--runtime` / `-R` | auto-detected | `node`, `deno`, or `bun` | -| `--input` / `-i` | `./index.js` | Entry file | -| `--output` / `-o` | `./dist` | Output directory | -| `--name` / `-n` | `webviewjs` | Executable name | -| `--resources` / `-r` | none | JSON asset map (Node.js only) | - -For runtime-specific details, asset embedding, code signing, and release -guidance, see [Building Executables](https://webview.js.org/guides/building-executables.md). - -## Agent skill +## Examples -This repository includes a reusable WebviewJS skill for coding agents. Install -it with: +Build the package, then run an example with Node.js: ```bash -npx skills add webviewjs/webview +bun --filter @webviewjs/webview build +node apps/examples/simple.mjs ``` -It covers application structure, native prerequisites, platform constraints, -API patterns, and executable builds. - -## Development +## Project structure -### Prerequisites - -- [Bun](https://bun.sh/) >= 1.3.0 -- [Rust](https://www.rust-lang.org/) stable toolchain -- [Node.js](https://nodejs.org/) >= 24 (for testing) +```text +apps/ + docs/ Documentation site + examples/ Runnable examples +packages/ + webview/ @webviewjs/webview package + create-webview/ Official WebviewJS project scaffolder +``` -### Setup +## Contributing -```bash -bun install -``` +Issues and pull requests are welcome. For bugs or feature requests, [open an issue](https://github.com/webviewjs/webview/issues); before contributing code, review the repository guidance in [`AGENTS.md`](./AGENTS.md). -### Build +## License -```bash -bun run build -``` +MIT. See [`LICENSE`](./LICENSE). diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore new file mode 100644 index 0000000..9e429e4 --- /dev/null +++ b/apps/docs/.gitignore @@ -0,0 +1,26 @@ +# deps +/node_modules + +# generated content +.source + +# test & build +/coverage +/.next/ +/out/ +/build +*.tsbuildinfo + +# misc +.DS_Store +*.pem +/.pnp +.pnp.js +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# others +.env*.local +.vercel +next-env.d.ts \ No newline at end of file diff --git a/apps/docs/README.md b/apps/docs/README.md new file mode 100644 index 0000000..04b4be3 --- /dev/null +++ b/apps/docs/README.md @@ -0,0 +1,16 @@ +# WebviewJS documentation site + +This workspace powers [webview.js.org](https://webview.js.org). The guides and +API reference live in `apps/docs/content/docs`; the custom homepage lives in +`apps/docs/app/(home)`. + +From the repository root, run: + +```bash +bun install +bun --filter @webviewjs/docs dev +bun --filter @webviewjs/docs build +``` + +The build exports the static site to `apps/docs/out/`, including search, +`llms.txt`, `llms-full.txt`, and per-page Markdown aliases. diff --git a/apps/docs/app/(docs)/[...slug]/page.tsx b/apps/docs/app/(docs)/[...slug]/page.tsx new file mode 100644 index 0000000..a859486 --- /dev/null +++ b/apps/docs/app/(docs)/[...slug]/page.tsx @@ -0,0 +1,65 @@ +import { source } from '@/lib/source'; +import { + DocsBody, + DocsDescription, + DocsPage, + DocsTitle, + MarkdownCopyButton, + ViewOptionsPopover, +} from 'fumadocs-ui/layouts/docs/page'; +import { notFound } from 'next/navigation'; +import { getMDXComponents } from '@/components/mdx'; +import type { Metadata } from 'next'; +import { createRelativeLink } from 'fumadocs-ui/mdx'; +import { getPageImageUrl, getPageMarkdownUrl, gitConfig } from '@/lib/shared'; + +type DocsPageProps = { params: Promise<{ slug: string[] }> }; + +export default async function Page(props: DocsPageProps) { + const params = await props.params; + const page = source.getPage(params.slug); + if (!page) notFound(); + + const MDX = page.data.body; + const markdownUrl = getPageMarkdownUrl(page).url; + + return ( + + {page.data.title} + {page.data.description} +
+ + +
+ + + +
+ ); +} + +export async function generateStaticParams() { + return source.generateParams(); +} + +export async function generateMetadata(props: DocsPageProps): Promise { + const params = await props.params; + const page = source.getPage(params.slug); + if (!page) notFound(); + + return { + title: page.data.title, + description: page.data.description, + openGraph: { + images: getPageImageUrl(page).url, + }, + }; +} diff --git a/apps/docs/app/(docs)/layout.tsx b/apps/docs/app/(docs)/layout.tsx new file mode 100644 index 0000000..ebb4c94 --- /dev/null +++ b/apps/docs/app/(docs)/layout.tsx @@ -0,0 +1,12 @@ +import { source } from '@/lib/source'; +import { DocsLayout } from 'fumadocs-ui/layouts/docs'; +import { baseOptions } from '@/lib/layout.shared'; +import type { ReactNode } from 'react'; + +export default function Layout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/docs/app/(home)/layout.tsx b/apps/docs/app/(home)/layout.tsx new file mode 100644 index 0000000..4222cb0 --- /dev/null +++ b/apps/docs/app/(home)/layout.tsx @@ -0,0 +1,11 @@ +import { baseOptions } from '@/lib/layout.shared'; +import { HomeLayout } from 'fumadocs-ui/layouts/home'; +import type { ReactNode } from 'react'; + +export default function Layout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/docs/app/(home)/page.tsx b/apps/docs/app/(home)/page.tsx new file mode 100644 index 0000000..5146513 --- /dev/null +++ b/apps/docs/app/(home)/page.tsx @@ -0,0 +1,39 @@ +import type { Metadata } from 'next'; +import { Executable } from '@/components/home/executable'; +import { Hero } from '@/components/home/hero'; +import { HeroFeatures } from '@/components/home/hero-features'; +import { JavaScriptNative } from '@/components/home/javascript-native'; +import { SystemWebview } from '@/components/home/system-webview'; + +const title = 'WebviewJS | Native webviews. JavaScript.'; +const description = + 'Create native desktop windows from JavaScript using the webview already provided by the operating system.'; + +export const metadata: Metadata = { + title: { absolute: title }, + description, + openGraph: { + title, + description, + url: '/', + siteName: 'WebviewJS', + type: 'website', + }, + twitter: { + card: 'summary_large_image', + title, + description, + }, +}; + +export default function Page() { + return ( +
+ + + + + +
+ ); +} diff --git a/apps/docs/app/api/search/route.ts b/apps/docs/app/api/search/route.ts new file mode 100644 index 0000000..aaaff7f --- /dev/null +++ b/apps/docs/app/api/search/route.ts @@ -0,0 +1,9 @@ +import { source } from '@/lib/source'; +import { createFromSource } from 'fumadocs-core/search/server'; + +export const revalidate = false; + +export const { staticGET: GET } = createFromSource(source, { + // https://docs.orama.com/docs/orama-js/supported-languages + language: 'english', +}); diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css new file mode 100644 index 0000000..97216da --- /dev/null +++ b/apps/docs/app/global.css @@ -0,0 +1,3134 @@ +@import 'tailwindcss'; +@import 'fumadocs-ui/css/neutral.css'; +@import 'fumadocs-ui/css/preset.css'; + +html { + scrollbar-gutter: stable; + scrollbar-width: thin; + scrollbar-color: rgb(120 120 120 / 0.55) transparent; +} + +* { + scrollbar-width: thin; + scrollbar-color: rgb(120 120 120 / 0.55) transparent; +} + +*::-webkit-scrollbar { + width: 3px; + height: 3px; +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +*::-webkit-scrollbar-thumb { + border-radius: 4px; + background: rgb(120 120 120 / 0.55); +} + +html:has(.home-layout) { + background: #030303; + scrollbar-color: rgb(245 245 245 / 0.22) transparent; + scrollbar-gutter: stable; + scrollbar-width: thin; +} + +.home-layout * { + scrollbar-color: rgb(245 245 245 / 0.22) transparent; +} + +.home-layout *::-webkit-scrollbar-thumb { + background: rgb(245 245 245 / 0.22); +} + +html > body[data-scroll-locked] { + margin-right: 0 !important; + --removed-body-scroll-bar-size: 0px !important; +} + +.home-layout { + --fd-layout-width: 1500px; + --wjs-black: #030303; + --wjs-black-soft: #070707; + --wjs-surface: #0b0b0c; + --wjs-surface-raised: #101011; + --wjs-white: #f5f5f5; + --wjs-muted: #949494; + --wjs-dim: #5c5c5c; + --wjs-red: #c8152f; + --wjs-red-bright: #ff2347; + --wjs-red-deep: #730b1c; + min-height: 100vh; + color: var(--wjs-white); + color-scheme: dark; + background: var(--wjs-black); +} + +body:has(.home-layout) { + background: #030303; +} + +#nd-home-layout #nd-nav { + height: 72px; +} + +#nd-home-layout #nd-nav > nav, +#nd-home-layout #nd-nav > nav > div:first-child { + height: 72px; +} + +#nd-home-layout #nd-nav[data-open], +#nd-home-layout #nd-nav[data-open] > nav { + height: auto; +} + +#nd-home-layout #nd-nav > nav { + border-color: rgb(255 255 255 / 0.09); + color: #f5f5f5; + background: rgb(3 3 3 / 0.88); + backdrop-filter: blur(14px); +} + +#nd-home-layout #nd-nav > nav > div { + padding-inline: clamp(20px, 4vw, 64px); +} + +#nd-home-layout #nd-nav ul:first-of-type { + gap: 4px; + padding-inline: 30px; +} + +#nd-home-layout #nd-nav ul:first-of-type a { + padding-inline: 12px; + color: rgb(245 245 245 / 0.55); + font-size: 13px; +} + +#nd-home-layout #nd-nav ul:first-of-type a:hover, +#nd-home-layout #nd-nav ul:first-of-type a[data-active='true'] { + color: #f5f5f5; +} + +#nd-home-layout #nd-nav button[data-search-full] { + max-width: 260px; + border-color: rgb(255 255 255 / 0.12); + border-radius: 5px; + color: rgb(245 245 245 / 0.48); + background: #0b0b0c; + box-shadow: none; +} + +#nd-home-layout #nd-nav button[data-search-full] kbd { + border-color: rgb(255 255 255 / 0.12); + color: rgb(245 245 245 / 0.48); + background: #101011; +} + +#nd-home-layout #nd-nav [data-theme-toggle] { + display: none; +} + +#nd-home-layout #nd-nav [data-open] > div > [role='separator'] { + display: none; +} + +#nd-home-layout #nd-nav button:not([data-theme-toggle]):hover { + color: #f5f5f5; + background-color: rgb(245 245 245 / 0.08); +} + +#nd-home-layout #nd-nav [data-open] > div > a { + color: rgb(245 245 245 / 0.72); +} + +#nd-home-layout #nd-nav [data-open] > div > a:hover { + color: #f5f5f5; + background: rgb(245 245 245 / 0.06); +} + +#nd-home-layout #nd-nav button[data-search-full]:focus-visible, +.home-layout :is(a, button):focus-visible { + outline: 2px solid #ff2347; + outline-offset: 3px; +} + +.wjs-wordmark { + display: inline-flex; + align-items: center; + gap: 10px; + color: inherit; + font-size: 15px; + font-weight: 650; + letter-spacing: -0.035em; +} + +.wjs-wordmark img { + flex: none; +} + +@media (max-width: 767px) { + #nd-home-layout #nd-nav, + #nd-home-layout #nd-nav > nav, + #nd-home-layout #nd-nav > nav > div:first-child { + height: 64px; + } + + #nd-home-layout #nd-nav[data-open], + #nd-home-layout #nd-nav[data-open] > nav { + height: auto; + } + + #nd-home-layout #nd-nav > nav > div { + padding-inline: 18px; + } +} + +/* WebviewJS landing page */ +.home-layout { + --fd-layout-width: 1280px; + color: #f5f5f5; + color-scheme: dark; + background: #030303; +} + +body:has(.home-layout) { + overflow-x: hidden; + background: #030303; +} + +.home-container { + width: calc(100% - 36px); + max-width: 1220px; + margin-inline: auto; +} + +#nd-home-layout #nd-nav, +#nd-home-layout #nd-nav > nav, +#nd-home-layout #nd-nav > nav > div:first-child { + height: 52px; +} + +#nd-home-layout #nd-nav > nav > div { + padding-inline: max(20px, calc((100vw - 1220px) / 2)); +} + +#nd-home-layout #nd-nav ul:first-of-type { + gap: 2px; + padding-inline: 18px; +} + +#nd-home-layout #nd-nav ul:first-of-type a { + padding-inline: 10px; + font-size: 11px; +} + +#nd-home-layout #nd-nav button[data-search-full] { + width: 174px; + min-height: 28px; + margin-right: 0; + border-radius: 6px; + font-size: 10px; +} + +#nd-home-layout #nd-nav button[data-search-full] kbd { + font-size: 9px; +} + +#nd-home-layout .wjs-wordmark { + gap: 7px; + font-size: 11px; + font-weight: 650; +} + +#nd-home-layout .wjs-wordmark img { + width: 20px; + height: 20px; +} + +#nd-home-layout #nd-nav [data-theme-toggle] { + display: none; +} + +.home-layout :is(a, button):focus-visible { + outline: 2px solid #ff2347; + outline-offset: 3px; +} + +.home-hero { + position: relative; + isolation: isolate; + min-height: 418px; + overflow: hidden; + border-bottom: 1px solid rgb(255 255 255 / 0.08); + background: #030303; +} + +.hero-scene, +.closing-scene { + position: absolute; + z-index: 0; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.home-hero-content { + position: relative; + z-index: 1; + display: grid; + min-height: 418px; + grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr); + align-items: center; + gap: 15px; + padding-block: 30px 25px; +} + +.hero-copy { + position: relative; + z-index: 2; + min-width: 0; + max-width: 380px; +} + +.home-eyebrow { + display: flex; + align-items: center; + gap: 9px; + margin: 0 0 13px; + color: rgb(245 245 245 / 0.55); + font-family: var(--font-mono, ui-monospace), monospace; + font-size: 8px; + font-weight: 500; + letter-spacing: 0.19em; + line-height: 1.2; + text-transform: uppercase; +} + +.home-eyebrow > span { + width: 6px; + height: 6px; + flex: none; + border-radius: 50%; + background: #ff1744; + box-shadow: 0 0 12px 2px rgb(255 23 68 / 0.7); +} + +.hero-title, +.home-section-title, +.cta-title { + margin: 0; + color: #f5f5f5; + font-weight: 650; + letter-spacing: -0.075em; + text-wrap: balance; +} + +.hero-title { + font-size: clamp(42px, 6.45vw, 82px); + line-height: 0.91; +} + +.hero-title > span, +.home-section-title > span, +.cta-title > span { + color: #ff1744; +} + +.hero-lead { + max-width: 325px; + margin: 15px 0 0; + color: #a5a5a5; + font-size: clamp(11px, 1.23vw, 15px); + line-height: 1.48; +} + +.hero-actions, +.cta-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-top: 16px; +} + +.home-button { + display: inline-flex; + min-height: 36px; + align-items: center; + justify-content: center; + gap: 7px; + padding: 0 14px; + border: 1px solid transparent; + border-radius: 5px; + color: #f7f7f7; + font-size: 10px; + font-weight: 650; + line-height: 1; + text-decoration: none; + transition: + background-color 140ms ease, + border-color 140ms ease, + color 140ms ease, + box-shadow 140ms ease; +} + +.home-button svg { + width: 13px; + height: 13px; +} + +.home-button-primary { + border-color: #ff1744; + background: #ed0735; + box-shadow: 0 5px 22px rgb(220 0 41 / 0.27); +} + +.home-button-primary:hover { + border-color: #ff4969; + background: #ff1744; +} + +.home-button-outline { + border-color: rgb(255 255 255 / 0.22); + background: rgb(5 5 6 / 0.72); +} + +.home-button-outline:hover { + border-color: rgb(255 255 255 / 0.5); + background: rgb(255 255 255 / 0.07); +} + +.hero-install { + display: flex; + width: min(100%, 270px); + min-height: 33px; + align-items: center; + gap: 9px; + margin-top: 17px; + padding: 0 9px; + border: 1px solid rgb(255 255 255 / 0.15); + border-radius: 5px; + background: rgb(4 4 5 / 0.7); + color: #ff1744; + font-family: var(--font-mono, ui-monospace), monospace; + font-size: 10px; +} + +.hero-install code { + min-width: 0; + flex: 1; + overflow-x: auto; + color: #f0f0f0; + white-space: nowrap; +} + +.hero-install button { + display: grid; + width: 22px; + height: 24px; + flex: none; + place-items: center; + border: 0; + background: transparent; + color: #9d9d9d; + cursor: pointer; +} + +.hero-install button:hover { + color: #fff; +} + +.hero-install button svg { + width: 13px; + height: 13px; +} + +.hero-art { + position: relative; + z-index: 1; + min-width: 0; + margin-inline: 0 -3%; +} + +.native-window { + position: relative; + width: 100%; + aspect-ratio: 1.36; + overflow: hidden; + border: 1px solid rgb(255 23 68 / 0.48); + border-radius: 8px; + background: linear-gradient(145deg, rgb(18 10 12 / 0.97), rgb(5 5 6 / 0.98) 60%); + box-shadow: + 0 20px 46px rgb(0 0 0 / 0.65), + 0 0 36px rgb(200 0 34 / 0.16), + inset 0 1px 0 rgb(255 255 255 / 0.07); +} + +.native-window::before { + position: absolute; + inset: 1px; + border-radius: 7px; + background: radial-gradient(ellipse at 82% 90%, rgb(156 0 30 / 0.25), transparent 48%); + content: ''; + pointer-events: none; +} + +.native-window-titlebar, +.browser-titlebar, +.terminal-titlebar { + position: relative; + z-index: 1; + display: flex; + height: 28px; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid rgb(255 255 255 / 0.09); + background: rgb(16 16 17 / 0.93); + color: rgb(255 255 255 / 0.65); + font-size: 8px; +} + +.native-window-titlebar { + padding-inline: 10px; +} + +.window-controls { + display: flex; + align-items: center; + gap: 5px; +} + +.window-controls i { + display: block; + width: 7px; + height: 7px; + border-radius: 50%; + background: #ff1744; +} + +.window-controls i:nth-child(2) { + background: #ffd52e; +} + +.window-controls i:nth-child(3) { + background: #17d35a; +} + +.window-control-mark, +.browser-title-mark { + width: 7px; + height: 7px; + border: 1px solid rgb(255 255 255 / 0.22); + border-radius: 50%; +} + +.native-window-inner { + position: relative; + height: calc(100% - 33px); + margin: 4px 5px 5px; + overflow: hidden; + border: 1px solid rgb(255 255 255 / 0.12); + border-radius: 5px; + background: rgb(6 6 7 / 0.74); +} + +.native-window-address { + display: flex; + height: 25px; + align-items: center; + justify-content: space-between; + padding-inline: 11px; + border-bottom: 1px solid rgb(255 255 255 / 0.09); + background: rgb(10 10 11 / 0.9); + color: rgb(255 255 255 / 0.55); + font-family: var(--font-mono, ui-monospace), monospace; + font-size: 8px; +} + +.native-window-address > span { + display: flex; + align-items: center; + gap: 6px; +} + +.native-window-address i { + width: 5px; + height: 5px; + border-radius: 50%; + background: #ff1744; + box-shadow: 0 0 8px rgb(255 23 68 / 0.8); +} + +.native-window-address small { + color: rgb(255 255 255 / 0.35); + font-size: 7px; + letter-spacing: 0.14em; +} + +.native-window-page { + position: relative; + display: flex; + height: calc(100% - 25px); + flex-direction: column; + justify-content: center; + padding: 12% 9%; + overflow: hidden; +} + +.window-outline { + position: absolute; + top: 23%; + right: 9%; + width: 33%; + height: 63%; +} + +.window-red-rule { + width: 25px; + height: 2px; + margin-bottom: 10px; + background: #ff1744; + box-shadow: 0 0 12px rgb(255 23 68 / 0.75); +} + +.native-window-page p { + position: relative; + z-index: 1; + margin: 0; + color: rgb(245 245 245 / 0.6); + font-size: 9px; +} + +.native-window-page h2 { + position: relative; + z-index: 1; + margin: 2px 0 0; + color: #f7f7f7; + font-size: clamp(20px, 3.2vw, 36px); + font-weight: 650; + letter-spacing: -0.075em; + line-height: 1; +} + +.native-window-page h2 span { + color: #ff1744; +} + +.native-window-page .window-caption { + margin-top: 12px; + font-size: 8px; +} + +.hero-badge { + position: absolute; + z-index: 3; + display: inline-flex; + min-height: 27px; + align-items: center; + gap: 6px; + padding: 0 9px; + border: 1px solid rgb(255 23 68 / 0.54); + border-radius: 5px; + background: rgb(9 5 6 / 0.92); + color: rgb(255 255 255 / 0.86); + font-size: 8px; + white-space: nowrap; +} + +.hero-badge svg { + width: 10px; + height: 10px; + color: #ff1744; + fill: #ff1744; +} + +.hero-badge-top { + top: -24px; + right: -8px; +} + +.hero-badge-bottom { + bottom: -21px; + left: 0; +} + +.home-section { + position: relative; + border-top: 1px solid rgb(255 255 255 / 0.075); + background: #050505; +} + +.home-section-heading { + display: grid; + grid-template-columns: minmax(0, 1.18fr) minmax(0, 0.82fr); + align-items: center; + gap: 34px; +} + +.home-section-title { + font-size: clamp(34px, 5.45vw, 72px); + line-height: 0.96; +} + +.home-section-heading > p { + max-width: 375px; + margin: 0; + color: #a0a0a0; + font-size: clamp(11px, 1.2vw, 14px); + line-height: 1.55; +} + +.install-section { + padding-block: 50px 48px; + background: radial-gradient(ellipse at 23% 90%, rgb(88 0 17 / 0.12), transparent 35%), #050505; +} + +.install-heading { + grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr); +} + +.install-heading > p { + max-width: 265px; + font-size: clamp(10px, 1.05vw, 13.5px); +} + +.install-heading .home-section-title { + white-space: nowrap; +} + +.install-panel { + margin-top: 29px; + overflow: hidden; + border: 1px solid rgb(255 255 255 / 0.15); + border-radius: 8px; + background: linear-gradient(115deg, rgb(13 13 14 / 0.96), rgb(8 8 9 / 0.96)); + box-shadow: 0 18px 50px rgb(0 0 0 / 0.22); +} + +.install-tabs { + display: flex; + min-height: 47px; + border-bottom: 1px solid rgb(255 255 255 / 0.13); +} + +.install-tab { + position: relative; + display: inline-flex; + min-width: 170px; + align-items: center; + justify-content: center; + gap: 8px; + padding: 0 18px; + border: 0; + background: transparent; + color: rgb(245 245 245 / 0.59); + cursor: pointer; + font-size: 10px; + transition: color 140ms ease; +} + +.install-tab:hover, +.install-tab.is-active { + color: #fff; +} + +.install-tab.is-active::after { + position: absolute; + right: 0; + bottom: -1px; + left: 0; + height: 2px; + background: #ff1744; + box-shadow: 0 0 10px rgb(255 23 68 / 0.55); + content: ''; +} + +.install-tab svg { + width: 13px; + height: 13px; +} + +.install-command-panel { + display: flex; + min-height: 77px; + align-items: center; + gap: 15px; + padding: 12px 25px; +} + +.install-prompt { + color: #9d9d9d; + font-family: var(--font-mono, ui-monospace), monospace; + font-size: 13px; +} + +.install-command-panel code { + min-width: 0; + flex: 1; + overflow-x: auto; + color: #f1f1f1; + font-family: var(--font-mono, ui-monospace), monospace; + font-size: clamp(12px, 1.5vw, 16px); + font-weight: 600; + white-space: pre; +} + +.install-command-panel code > span { + color: #ff1744; +} + +.install-copy { + display: grid; + width: 28px; + height: 30px; + flex: none; + place-items: center; + border: 0; + background: transparent; + color: #bdbdbd; + cursor: pointer; +} + +.install-copy:hover { + color: #fff; +} + +.install-copy svg { + width: 15px; + height: 15px; +} + +.platform-section { + padding-block: 43px 55px; + background: radial-gradient(ellipse at 77% 98%, rgb(94 0 19 / 0.1), transparent 40%), #030303; +} + +.platform-flow { + position: relative; + margin-top: 36px; +} + +.platform-connectors { + position: absolute; + z-index: 0; + top: 0; + left: 0; + width: 100%; + height: 90px; + overflow: visible; + filter: drop-shadow(0 0 4px rgb(255 23 68 / 0.6)); +} + +.platform-grid { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: 170px 158px 158px; + justify-content: space-between; + gap: 12px; +} + +.platform-card { + display: flex; + min-height: 66px; + align-items: center; + gap: 13px; + padding: 10px 20px; + border: 1px solid rgb(255 255 255 / 0.2); + border-radius: 6px; + background: #070708; + box-shadow: 0 8px 24px rgb(0 0 0 / 0.35); +} + +.platform-mark { + display: grid; + width: 22px; + height: 24px; + flex: none; + place-items: center; + color: #ff1744; + filter: drop-shadow(0 0 7px rgb(255 23 68 / 0.35)); +} + +.platform-mark svg { + display: block; + width: 100%; + height: 100%; +} + +.platform-copy { + display: flex; + min-width: 0; + flex-direction: column; + gap: 4px; +} + +.platform-copy strong { + color: #f2f2f2; + font-size: 10px; + font-weight: 600; +} + +.platform-copy small { + color: #838383; + font-family: var(--font-mono, ui-monospace), monospace; + font-size: 7px; + letter-spacing: 0.14em; + white-space: nowrap; +} + +.javascript-section { + padding-block: 42px 41px; + background: radial-gradient(ellipse at 76% 80%, rgb(88 0 18 / 0.1), transparent 36%), #050505; +} + +.javascript-showcase { + display: grid; + grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr); + align-items: center; + gap: 20px; + margin-top: 20px; +} + +.code-editor, +.browser-demo, +.terminal-panel { + overflow: hidden; + border: 1px solid rgb(255 255 255 / 0.16); + border-radius: 7px; + background: #09090a; + box-shadow: 0 16px 44px rgb(0 0 0 / 0.4); +} + +.code-editor { + min-width: 0; + height: 274px; +} + +.code-toolbar { + display: flex; + height: 31px; + align-items: center; + justify-content: space-between; + padding-inline: 10px; + border-bottom: 1px solid rgb(255 255 255 / 0.1); + background: #0c0c0d; +} + +.code-file { + display: inline-flex; + align-items: center; + gap: 8px; + color: #c7c7c7; + font-family: var(--font-mono, ui-monospace), monospace; + font-size: 9px; +} + +.code-file strong { + color: #44dfa6; + font-size: 9px; + font-weight: 700; +} + +.code-toolbar button { + display: inline-flex; + align-items: center; + gap: 6px; + border: 0; + background: transparent; + color: #aaa; + cursor: pointer; + font-size: 8px; +} + +.code-toolbar button:hover { + color: #fff; +} + +.code-toolbar button svg { + width: 11px; + height: 11px; +} + +.code-scroll { + height: calc(100% - 31px); + overflow: auto; + padding: 9px 8px 10px; +} + +.code-scroll pre { + margin: 0; + color: #e7e7e7; + font-family: var(--font-mono, ui-monospace), monospace; + font-size: 9px; + line-height: 1.72; +} + +.code-scroll code { + display: block; + width: max-content; + min-width: 100%; +} + +.code-line { + display: block; + min-height: 1.72em; + white-space: pre; +} + +.code-number { + display: inline-block; + width: 17px; + margin-right: 9px; + color: #707070; + text-align: right; + user-select: none; +} + +.code-keyword { + color: #ff344e; +} + +.code-string { + color: #ff765e; +} + +.code-accent { + color: #4fd7c4; +} + +.code-number-value { + color: #efc26b; +} + +.browser-showcase { + min-width: 0; +} + +.browser-demo { + position: relative; + background: radial-gradient(ellipse at 80% 100%, rgb(153 0 30 / 0.19), transparent 48%), #09090a; + box-shadow: + 0 17px 42px rgb(0 0 0 / 0.5), + 0 0 22px rgb(168 0 31 / 0.1); +} + +.browser-titlebar { + height: 29px; + padding-inline: 9px; +} + +.browser-address { + display: flex; + height: 27px; + align-items: center; + gap: 9px; + padding: 0 8px; + border-bottom: 1px solid rgb(255 255 255 / 0.08); + background: #0c0c0d; + color: rgb(255 255 255 / 0.48); + font-size: 10px; +} + +.browser-address code { + min-width: 0; + flex: 1; + overflow: hidden; + padding: 4px 7px; + border: 1px solid rgb(255 255 255 / 0.1); + border-radius: 4px; + color: rgb(255 255 255 / 0.52); + font-family: var(--font-mono, ui-monospace), monospace; + font-size: 7px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.browser-content { + position: relative; + display: flex; + min-height: 151px; + flex-direction: column; + justify-content: center; + padding: 22px 10%; + overflow: hidden; +} + +.browser-red-rule { + width: 42px; + height: 3px; + margin-bottom: 12px; + border-radius: 3px; + background: #ff1744; + box-shadow: 0 0 14px rgb(255 23 68 / 0.5); +} + +.browser-content h3 { + margin: 0; + color: #f3f3f3; + font-size: 14px; + font-weight: 550; + letter-spacing: -0.03em; +} + +.browser-placeholder { + display: flex; + width: 58%; + flex-direction: column; + gap: 7px; + margin-top: 10px; +} + +.browser-placeholder i { + display: block; + height: 5px; + border-radius: 1px; + background: rgb(255 255 255 / 0.14); +} + +.browser-placeholder i:nth-child(2) { + width: 78%; + background: rgb(255 255 255 / 0.11); +} + +.browser-placeholder i:nth-child(3) { + width: 52%; + background: rgb(255 255 255 / 0.08); +} + +.browser-outline { + position: absolute; + top: 24%; + right: 9%; + width: 27%; + height: 52%; + border: 1px solid rgb(255 23 68 / 0.24); + border-radius: 3px; +} + +.feature-icons { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 4px; + margin: 14px 0 0; + padding: 0; + list-style: none; +} + +.feature-icons li { + display: flex; + min-width: 0; + flex-direction: column; + align-items: center; + gap: 4px; + color: rgb(245 245 245 / 0.63); + text-align: center; +} + +.feature-icons svg { + width: 12px; + height: 12px; + color: #e2e2e2; + stroke-width: 1.5; +} + +.feature-icons span { + font-size: 7px; + line-height: 1.2; +} + +.executable-section { + padding-block: 38px 23px; + background: #030303; +} + +.terminal-panel { + margin-top: 24px; +} + +.terminal-titlebar { + height: 28px; + justify-content: flex-start; + gap: 10px; + padding-inline: 10px; + font-size: 8px; +} + +.terminal-content { + display: grid; + min-height: 62px; + grid-template-columns: minmax(0, 1fr) 44px auto; + align-items: center; + gap: 10px; + padding: 10px 20px; +} + +.terminal-command { + display: flex; + min-width: 0; + align-items: center; + gap: 13px; + overflow: hidden; + font-family: var(--font-mono, ui-monospace), monospace; + font-size: 10px; +} + +.terminal-command > span { + color: #ff1744; +} + +.terminal-command code { + overflow-x: auto; + color: #efefef; + font-weight: 600; + white-space: nowrap; +} + +.terminal-arrow { + width: 21px; + height: 21px; + justify-self: center; + color: #ff1744; + stroke-width: 1.5; +} + +.terminal-output { + display: flex; + align-items: center; + gap: 9px; + padding-left: 16px; + border-left: 1px solid rgb(255 255 255 / 0.1); +} + +.terminal-output > svg { + width: 31px; + height: 34px; + color: #ff1744; + stroke-width: 1.2; + filter: drop-shadow(0 0 8px rgb(255 23 68 / 0.42)); +} + +.terminal-output > span { + display: flex; + flex-direction: column; + gap: 3px; + font-family: var(--font-mono, ui-monospace), monospace; +} + +.terminal-output small { + color: #8d8d8d; + font-size: 7px; + letter-spacing: 0.17em; +} + +.terminal-output strong { + color: #ff304f; + font-size: 10px; + font-weight: 600; + white-space: nowrap; +} + +.terminal-guide { + display: flex; + width: fit-content; + align-items: center; + gap: 6px; + margin: 10px 0 0 auto; + color: rgb(245 245 245 / 0.42); + font-size: 9px; + text-decoration: none; +} + +.terminal-guide:hover { + color: #fff; +} + +.terminal-guide svg { + width: 11px; + height: 11px; +} + +.home-cta { + position: relative; + isolation: isolate; + min-height: 279px; + overflow: hidden; + border-top: 1px solid rgb(255 255 255 / 0.075); + background: #030303; +} + +.closing-scene { + opacity: 0.95; +} + +.cta-content { + position: relative; + z-index: 1; + display: flex; + min-height: 279px; + align-items: center; + transform: translateY(-8px); +} + +.cta-copy { + max-width: 460px; +} + +.cta-copy .home-eyebrow { + margin-bottom: 12px; +} + +.cta-title { + font-size: clamp(43px, 6.3vw, 80px); + line-height: 0.9; +} + +.cta-copy > p:not(.home-eyebrow) { + margin: 12px 0 0; + color: #b5b5b5; + font-size: 11px; + line-height: 1.5; +} + +.cta-actions { + margin-top: 16px; +} + +@media (min-width: 640px) { + .home-container { + width: min(86.5%, 1220px); + } +} + +@media (min-width: 700px) and (max-width: 1023px) { + #nd-home-layout #nd-nav > nav > div { + padding-right: 4.5vw !important; + padding-left: 6.8vw !important; + } + + #nd-home-layout #nd-nav > nav > div > div:first-of-type { + display: flex !important; + } + + #nd-home-layout #nd-nav > nav > div > div:nth-of-type(2) { + display: none !important; + } + + .hero-copy { + transform: translateY(16px); + } +} + +@media (min-width: 1000px) { + #nd-home-layout #nd-nav > nav { + padding-inline: max(32px, calc((100vw - 1320px) / 2)); + } + + .home-container { + width: min(88%, 1220px); + } + + .home-hero, + .home-hero-content { + min-height: 490px; + } + + .hero-copy { + max-width: 440px; + } + + .hero-art { + margin-inline: 0 -3%; + } + + .home-section-title { + font-size: clamp(50px, 5vw, 72px); + } + + .hero-title { + font-size: clamp(64px, 6.1vw, 82px); + } + + .native-window-page h2 { + font-size: clamp(34px, 3.4vw, 52px); + } + + .code-scroll pre { + font-size: 11px; + } + + .feature-icons span { + font-size: 8px; + } +} + +@media (max-width: 699px) { + #nd-home-layout #nd-nav, + #nd-home-layout #nd-nav > nav, + #nd-home-layout #nd-nav > nav > div:first-child { + height: 56px; + } + + #nd-home-layout #nd-nav > nav > div { + padding-inline: 18px; + } + + #nd-home-layout #nd-nav ul:first-of-type { + padding-inline: 4px; + } + + #nd-home-layout #nd-nav button[data-search-full] { + width: 144px; + margin-right: 4px; + } + + .home-container { + width: calc(100% - 36px); + } + + .home-hero { + min-height: 0; + } + + .home-hero-content { + min-height: 0; + grid-template-columns: minmax(0, 1fr); + gap: 38px; + padding-block: 48px 42px; + } + + .hero-copy { + max-width: 460px; + } + + .hero-title { + font-size: clamp(52px, 12vw, 78px); + } + + .hero-lead { + max-width: 400px; + font-size: 14px; + } + + .hero-art { + width: min(100%, 540px); + margin: 0 auto 10px; + } + + .hero-badge-top { + right: -5px; + } + + .home-section-heading, + .install-heading { + grid-template-columns: minmax(0, 1fr); + gap: 14px; + } + + .home-section-title { + font-size: clamp(38px, 9vw, 56px); + } + + .home-section-heading > p { + max-width: 470px; + font-size: 13px; + } + + .install-heading .home-section-title { + white-space: normal; + } + + .install-section, + .platform-section, + .javascript-section, + .executable-section { + padding-block: 42px; + } + + .install-panel { + margin-top: 22px; + } + + .install-tab { + min-width: 0; + flex: 1; + } + + .install-command-panel { + min-height: 68px; + gap: 10px; + padding-inline: 14px; + } + + .install-command-panel code { + font-size: 12px; + } + + .platform-flow { + margin-top: 23px; + } + + .platform-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + } + + .platform-card { + min-height: 84px; + flex-direction: column; + justify-content: center; + gap: 6px; + padding: 8px 3px; + text-align: center; + } + + .platform-copy { + align-items: center; + gap: 3px; + } + + .platform-copy small { + font-size: 6px; + letter-spacing: 0.08em; + } + + .platform-connectors { + opacity: 0.5; + } + + .javascript-showcase { + grid-template-columns: minmax(0, 1fr); + gap: 20px; + margin-top: 22px; + } + + .code-editor { + height: 270px; + } + + .code-scroll pre { + font-size: 10px; + } + + .browser-showcase { + width: min(100%, 500px); + justify-self: center; + } + + .browser-content { + min-height: 185px; + } + + .terminal-content { + grid-template-columns: minmax(0, 1fr) 25px auto; + gap: 5px; + padding-inline: 11px; + } + + .terminal-command { + gap: 8px; + font-size: 9px; + } + + .terminal-output { + gap: 5px; + padding-left: 8px; + } + + .terminal-output > svg { + width: 24px; + height: 27px; + } + + .terminal-output strong { + font-size: 8px; + } + + .home-cta, + .cta-content { + min-height: 330px; + } + + .closing-scene { + width: 150%; + left: -25%; + } + + .cta-title { + font-size: clamp(48px, 11vw, 68px); + } +} + +@media (max-width: 390px) { + .home-container { + width: calc(100% - 30px); + } + + .hero-title { + font-size: 50px; + } + + .home-button { + min-height: 34px; + padding-inline: 11px; + font-size: 9px; + } + + .install-tab { + gap: 6px; + padding-inline: 8px; + font-size: 9px; + } + + .install-command-panel { + gap: 7px; + padding-inline: 9px; + } + + .install-command-panel code { + font-size: 10px; + } + + .terminal-arrow { + width: 17px; + } + + .terminal-output strong { + font-size: 7px; + } + + .terminal-output > svg { + width: 20px; + } +} + +@media (prefers-reduced-motion: reduce) { + .home-layout *, + .home-layout *::before, + .home-layout *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} + +/* Readability and shared-grid refinement */ +.home-container { + box-sizing: border-box; + width: min(100%, 1280px); + max-width: 1280px; + padding-inline: clamp(24px, 4vw, 48px); +} + +#nd-home-layout #nd-nav, +#nd-home-layout #nd-nav > nav, +#nd-home-layout #nd-nav > nav > div:first-child { + height: 64px; +} + +#nd-home-layout #nd-nav > nav > div { + box-sizing: border-box; + width: 100%; + max-width: 1280px; + padding-inline: clamp(24px, 4vw, 48px); +} + +#nd-home-layout #nd-nav ul:first-of-type { + gap: 4px; + padding-inline: 20px; +} + +#nd-home-layout #nd-nav ul:first-of-type a { + padding-inline: 10px; + color: rgb(245 245 245 / 0.72); + font-size: 14px; + line-height: 1.2; +} + +#nd-home-layout #nd-nav ul:first-of-type a:hover, +#nd-home-layout #nd-nav ul:first-of-type a[data-active='true'] { + color: #fff; +} + +#nd-home-layout .wjs-wordmark { + gap: 9px; + font-size: 15px; + letter-spacing: -0.03em; +} + +#nd-home-layout .wjs-wordmark img { + width: 27px; + height: 27px; +} + +#nd-home-layout #nd-nav button[data-search-full] { + width: min(260px, 24vw); + min-height: 36px; + margin-right: 12px; + padding-inline: 12px; + border-radius: 6px; + color: rgb(245 245 245 / 0.68); + font-size: 14px; +} + +#nd-home-layout #nd-nav button[data-search-full] kbd { + min-width: 20px; + padding-inline: 5px; + color: rgb(245 245 245 / 0.68); + font-size: 11px; + line-height: 1.5; +} + +.home-hero, +.home-hero-content { + min-height: clamp(620px, 52vw, 760px); +} + +.home-hero-content { + grid-template-columns: minmax(0, 0.86fr) minmax(0, 1.14fr); + gap: clamp(28px, 3vw, 48px); + padding-block: 64px; +} + +.hero-copy { + max-width: 600px; + transform: none; +} + +.home-eyebrow { + gap: 10px; + margin-bottom: 18px; + color: rgb(245 245 245 / 0.7); + font-size: 12px; + letter-spacing: 0.19em; +} + +.home-eyebrow > span { + width: 7px; + height: 7px; +} + +.hero-title { + font-size: clamp(64px, 6vw, 88px); + line-height: 0.96; + letter-spacing: -0.07em; +} + +.hero-lead { + max-width: 530px; + margin-top: 20px; + color: rgb(245 245 245 / 0.72); + font-size: 17px; + line-height: 1.55; +} + +.hero-actions, +.cta-actions { + gap: 12px; + margin-top: 24px; +} + +.home-button { + min-height: 44px; + gap: 9px; + padding-inline: 18px; + font-size: 14px; + font-weight: 600; +} + +.home-button svg { + width: 16px; + height: 16px; +} + +.hero-install { + width: min(100%, 360px); + min-height: 46px; + gap: 12px; + margin-top: 23px; + padding-inline: 13px; + font-size: 14px; +} + +.hero-install-panel { + width: min(100%, 360px); + margin-top: 23px; +} + +.hero-install-tabs { + display: flex; + align-items: center; + gap: 5px; + min-height: 31px; +} + +.hero-install-tab { + display: inline-flex; + min-height: 30px; + align-items: center; + gap: 6px; + padding: 0 9px; + border: 1px solid transparent; + border-radius: 5px 5px 0 0; + background: transparent; + color: rgb(245 245 245 / 0.7); + cursor: pointer; + font-size: 12px; + font-weight: 550; + transition: + color 140ms ease, + background-color 140ms ease, + border-color 140ms ease; +} + +.hero-install-tab:hover { + color: #fff; +} + +.hero-install-tab.is-active { + border-bottom-color: #ff1744; + background: linear-gradient(180deg, rgb(255 23 68 / 0.1), rgb(255 23 68 / 0.025)); + color: #fff; +} + +.hero-install-tab svg { + width: 14px; + height: 14px; + flex: none; +} + +.hero-install-panel .hero-install { + width: 100%; + margin-top: 6px; +} + +.hero-install code > span { + color: #ff1744; +} + +.hero-install button { + width: 30px; + height: 32px; +} + +.hero-install button svg { + width: 16px; + height: 16px; +} + +.hero-art { + width: 100%; + max-width: 720px; + margin-inline: 0; + justify-self: end; +} + +.native-window-titlebar { + height: 38px; + padding-inline: 15px; + font-size: 12px; +} + +.window-controls { + gap: 6px; +} + +.window-controls i { + width: 9px; + height: 9px; +} + +.native-window-inner { + height: calc(100% - 43px); + margin: 5px 6px 6px; +} + +.native-window-address { + height: 34px; + padding-inline: 15px; + font-size: 12px; +} + +.native-window-address i { + width: 6px; + height: 6px; +} + +.native-window-address small { + font-size: 12px; +} + +.native-window-page { + height: calc(100% - 34px); + padding-inline: 10%; +} + +.window-red-rule { + width: 38px; + height: 3px; + margin-bottom: 15px; +} + +.native-window-page p { + font-size: 13px; +} + +.native-window-page h2 { + margin-top: 5px; + font-size: clamp(34px, 3.1vw, 46px); + letter-spacing: -0.065em; +} + +.native-window-page .window-caption { + margin-top: 18px; + font-size: 13px; +} + +.hero-badge { + min-height: 36px; + gap: 8px; + padding-inline: 12px; + font-size: 12px; +} + +.hero-badge svg { + width: 14px; + height: 14px; +} + +.hero-badge-top { + top: -25px; + right: -12px; +} + +.hero-badge-bottom { + bottom: -25px; + left: 0; +} + +.home-section-heading { + grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr); + gap: clamp(32px, 5vw, 72px); +} + +.home-section-title { + font-size: clamp(48px, 4.5vw, 64px); + line-height: 1; + letter-spacing: -0.065em; +} + +.home-section-heading > p { + max-width: 470px; + color: rgb(245 245 245 / 0.7); + font-size: 16px; + line-height: 1.55; +} + +.install-section, +.platform-section, +.javascript-section, +.executable-section { + padding-block: clamp(88px, 7.5vw, 120px); +} + +.install-heading { + grid-template-columns: minmax(0, 1.18fr) minmax(0, 0.82fr); +} + +.install-heading .home-section-title { + white-space: normal; + font-size: clamp(56px, 4.6vw, 64px); +} + +.install-heading > p { + max-width: 470px; + color: rgb(245 245 245 / 0.7); + font-size: 16px; + line-height: 1.55; +} + +.install-panel { + min-height: 168px; + margin-top: 42px; + border-radius: 9px; +} + +.install-tabs { + min-height: 56px; +} + +.install-tab { + min-width: 210px; + gap: 10px; + padding-inline: 24px; + font-size: 15px; +} + +.install-tab svg { + width: 17px; + height: 17px; +} + +.install-tab.is-active { + background: linear-gradient(180deg, rgb(255 23 68 / 0.1), transparent); +} + +.install-command-panel { + min-height: 110px; + gap: 18px; + padding: 20px 28px; +} + +.install-prompt { + font-size: 16px; +} + +.install-command-panel code { + font-size: clamp(18px, 1.35vw, 20px); + font-weight: 600; +} + +.install-copy { + width: 40px; + height: 40px; + border-radius: 6px; +} + +.install-copy:hover { + background: rgb(255 255 255 / 0.08); + color: #fff; +} + +.install-copy svg { + width: 19px; + height: 19px; +} + +.platform-flow { + margin-top: 48px; +} + +.platform-connectors { + top: 0; + height: 112px; + filter: drop-shadow(0 0 3px rgb(255 23 68 / 0.35)); +} + +.platform-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 18px; +} + +.platform-card { + width: 100%; + max-width: 260px; + min-height: 100px; + justify-self: center; + gap: 18px; + padding: 18px 24px; + border-radius: 8px; +} + +.platform-mark { + width: 34px; + height: 36px; +} + +.platform-copy { + gap: 7px; +} + +.platform-copy strong { + font-size: 16px; +} + +.platform-copy small { + color: rgb(245 245 245 / 0.62); + font-size: 11px; + letter-spacing: 0.14em; +} + +.javascript-showcase { + gap: clamp(24px, 3vw, 44px); + margin-top: 48px; +} + +.code-editor { + height: auto; + min-height: 354px; + border-radius: 9px; +} + +.code-toolbar { + height: 44px; + padding-inline: 16px; +} + +.code-file { + gap: 10px; + font-size: 13px; +} + +.code-file strong { + font-size: 13px; +} + +.code-toolbar button { + min-height: 32px; + gap: 8px; + padding-inline: 8px; + font-size: 12px; +} + +.code-toolbar button svg { + width: 15px; + height: 15px; +} + +.code-scroll { + height: auto; + min-height: 308px; + padding: 14px 13px 18px; +} + +.code-scroll pre { + font-size: 14px; + line-height: 1.55; +} + +.code-line { + min-height: 1.55em; +} + +.code-number { + width: 21px; + margin-right: 13px; + color: rgb(255 255 255 / 0.5); +} + +.browser-demo { + border-radius: 9px; +} + +.browser-titlebar { + height: 40px; + padding-inline: 14px; + font-size: 12px; +} + +.browser-address { + height: 38px; + gap: 12px; + padding-inline: 12px; + font-size: 15px; +} + +.browser-address code { + padding: 6px 9px; + font-size: 12px; +} + +.browser-content { + min-height: 228px; + padding-inline: 10%; +} + +.browser-red-rule { + width: 56px; + height: 4px; + margin-bottom: 16px; +} + +.browser-content h3 { + font-size: 20px; +} + +.browser-placeholder { + gap: 9px; + margin-top: 14px; +} + +.browser-placeholder i { + height: 7px; +} + +.feature-icons { + gap: 10px; + margin-top: 21px; +} + +.feature-icons li { + gap: 7px; +} + +.feature-icons svg { + width: 18px; + height: 18px; +} + +.feature-icons span { + color: rgb(245 245 245 / 0.78); + font-size: 12px; + line-height: 1.25; +} + +.terminal-panel { + margin-top: 42px; + border-radius: 9px; +} + +.terminal-titlebar { + height: 40px; + gap: 12px; + padding-inline: 16px; + font-size: 13px; +} + +.terminal-titlebar .window-controls i { + width: 8px; + height: 8px; +} + +.terminal-content { + min-height: 92px; + grid-template-columns: minmax(0, 1fr) 54px auto; + gap: 16px; + padding: 16px 26px; +} + +.terminal-command { + gap: 15px; + font-size: 15px; +} + +.terminal-arrow { + width: 28px; + height: 28px; +} + +.terminal-output { + gap: 13px; + padding-left: 22px; +} + +.terminal-output > svg { + width: 40px; + height: 44px; +} + +.terminal-output > span { + gap: 5px; +} + +.terminal-output small { + color: rgb(245 245 245 / 0.6); + font-size: 12px; +} + +.terminal-output strong { + font-size: 15px; +} + +.home-cta, +.cta-content { + min-height: clamp(440px, 36vw, 520px); +} + +.cta-content { + transform: none; +} + +.cta-copy { + max-width: 560px; +} + +.cta-copy .home-eyebrow { + margin-bottom: 16px; +} + +.cta-title { + font-size: clamp(58px, 4.8vw, 72px); + line-height: 0.98; + letter-spacing: -0.065em; +} + +.cta-copy > p:not(.home-eyebrow) { + margin-top: 18px; + color: rgb(245 245 245 / 0.72); + font-size: 16px; + line-height: 1.55; +} + +.cta-actions { + margin-top: 24px; +} + +@media (min-width: 700px) and (max-width: 1023px) { + #nd-home-layout #nd-nav > nav > div > div:first-of-type { + display: flex !important; + gap: 0; + } + + #nd-home-layout #nd-nav > nav > div > div:nth-of-type(2) { + display: none !important; + } + + #nd-home-layout #nd-nav button[data-search-full] { + width: 220px; + margin-right: 0; + } + + #nd-home-layout #nd-nav > nav > div > div:first-of-type > ul svg { + width: 18px; + height: 18px; + } + + #nd-home-layout #nd-nav ul:first-of-type { + gap: 0; + padding-inline: 14px; + } + + #nd-home-layout #nd-nav ul:first-of-type a { + padding-inline: 7px; + font-size: 13px; + } + + .home-section-heading { + grid-template-columns: minmax(0, 1fr); + gap: 18px; + } + + .home-section-heading > p { + max-width: 680px; + } + + .install-heading { + grid-template-columns: minmax(0, 1fr); + } + + .install-heading > p { + max-width: 600px; + } + + .hero-title { + font-size: clamp(58px, 7vw, 68px); + } + + .home-section-title { + font-size: clamp(48px, 6.4vw, 58px); + } + + .install-heading .home-section-title { + font-size: clamp(52px, 7vw, 62px); + } + + .code-scroll pre { + font-size: 13px; + } + + .feature-icons { + gap: 5px; + } + + .feature-icons span { + font-size: 11px; + } + + .install-section, + .platform-section, + .javascript-section, + .executable-section { + padding-block: 76px; + } +} + +@media (max-width: 699px) { + #nd-home-layout #nd-nav, + #nd-home-layout #nd-nav > nav, + #nd-home-layout #nd-nav > nav > div:first-child { + height: 60px; + } + + #nd-home-layout #nd-nav > nav > div { + padding-inline: 22px; + } + + #nd-home-layout .wjs-wordmark { + gap: 8px; + font-size: 14px; + } + + #nd-home-layout .wjs-wordmark img { + width: 25px; + height: 25px; + } + + .home-hero, + .home-hero-content { + min-height: 0; + } + + .home-hero-content { + grid-template-columns: minmax(0, 1fr); + gap: 56px; + padding-block: 58px 54px; + } + + .hero-copy { + max-width: 520px; + transform: none; + } + + .hero-title { + font-size: clamp(54px, 14vw, 70px); + } + + .hero-lead { + font-size: 16px; + } + + .hero-art { + width: min(100%, 540px); + margin: 0 auto 12px; + } + + .home-section-heading, + .install-heading { + grid-template-columns: minmax(0, 1fr); + gap: 17px; + } + + .home-section-title, + .install-heading .home-section-title { + font-size: clamp(42px, 11vw, 54px); + } + + .home-section-heading > p, + .install-heading > p { + max-width: 520px; + font-size: 16px; + } + + .install-section, + .platform-section, + .javascript-section, + .executable-section { + padding-block: 58px; + } + + .install-heading .home-section-title { + white-space: normal; + } + + .install-panel { + min-height: 0; + margin-top: 30px; + } + + .install-tabs { + min-height: 52px; + } + + .install-tab { + min-width: 0; + flex: 1; + gap: 8px; + padding-inline: 8px; + font-size: 14px; + } + + .install-command-panel { + min-height: 84px; + gap: 12px; + padding: 14px 16px; + } + + .install-command-panel code { + font-size: clamp(14px, 4vw, 17px); + } + + .install-prompt { + font-size: 15px; + } + + .platform-flow { + margin-top: 32px; + } + + .platform-grid { + grid-template-columns: minmax(0, 1fr); + gap: 14px; + } + + .platform-card { + max-width: none; + min-height: 82px; + justify-content: flex-start; + gap: 18px; + padding: 14px 22px; + } + + .platform-copy strong { + font-size: 16px; + } + + .platform-copy small { + font-size: 12px; + } + + .platform-connectors { + display: none; + } + + .javascript-showcase { + grid-template-columns: minmax(0, 1fr); + gap: 24px; + margin-top: 34px; + } + + .code-editor { + min-height: 340px; + } + + .code-scroll { + min-height: 294px; + padding: 14px 10px 16px; + } + + .code-scroll pre { + font-size: 13px; + } + + .code-number { + margin-right: 8px; + } + + .browser-showcase { + width: 100%; + justify-self: center; + } + + .browser-content { + min-height: 220px; + } + + .feature-icons { + gap: 4px; + } + + .feature-icons li { + gap: 6px; + } + + .feature-icons svg { + width: 17px; + height: 17px; + } + + .feature-icons span { + font-size: 11px; + } + + .terminal-panel { + margin-top: 32px; + } + + .terminal-content { + min-height: 0; + grid-template-columns: minmax(0, 1fr); + gap: 16px; + padding: 18px; + } + + .terminal-command { + gap: 12px; + overflow: visible; + font-size: 14px; + } + + .terminal-command code { + overflow: visible; + white-space: normal; + overflow-wrap: anywhere; + } + + .terminal-arrow { + display: none; + } + + .terminal-output { + justify-self: end; + gap: 9px; + padding-top: 12px; + padding-left: 0; + border-top: 1px solid rgb(255 255 255 / 0.1); + border-left: 0; + } + + .terminal-output > svg { + width: 27px; + height: 31px; + } + + .terminal-output small { + font-size: 12px; + } + + .terminal-output strong { + font-size: 13px; + } + + .home-cta, + .cta-content { + min-height: 390px; + } + + .closing-scene { + width: 150%; + left: -25%; + } + + .cta-title { + font-size: clamp(56px, 13vw, 68px); + } + + .cta-copy > p:not(.home-eyebrow) { + font-size: 16px; + } +} + +@media (max-width: 390px) { + .home-container { + padding-inline: 20px; + } + + .hero-title { + font-size: 54px; + } + + .hero-actions, + .cta-actions { + gap: 9px; + } + + .home-button { + padding-inline: 13px; + font-size: 14px; + } + + .install-tab { + gap: 6px; + padding-inline: 5px; + font-size: 14px; + } + + .install-command-panel { + gap: 8px; + padding-inline: 10px; + } + + .install-command-panel code { + font-size: 14px; + } + + .code-scroll pre { + font-size: 14px; + } + + .feature-icons { + gap: 2px; + } + + .feature-icons span { + font-size: 12px; + } + + .terminal-output { + gap: 8px; + } + + .terminal-output > svg { + width: 22px; + } + + .terminal-output strong { + font-size: 12px; + } +} + +/* Supplied WebviewJS artwork and the application UI mockup. */ +.hero-scene { + background: + linear-gradient(90deg, rgb(3 3 3 / 0.36), transparent 62%), + linear-gradient(0deg, rgb(3 3 3 / 0.34), transparent 72%), + #030303 url('/images/home-hero.webp') center center / cover no-repeat; +} + +.home-hero, +.home-hero-content { + min-height: clamp(680px, 52vw, 760px); +} + +.closing-scene { + left: 0; + width: 100%; + background: #030303 url('/images/home-cta.webp') center center / cover no-repeat; + opacity: 1; +} + +.application-showcase { + min-width: 0; +} + +.application-demo { + overflow: hidden; + border: 1px solid rgb(255 255 255 / 0.16); + border-radius: 9px; + background: #09090a; + box-shadow: + 0 17px 42px rgb(0 0 0 / 0.5), + 0 0 22px rgb(168 0 31 / 0.1); +} + +.application-titlebar { + position: relative; + display: flex; + height: 40px; + align-items: center; + justify-content: space-between; + padding-inline: 14px; + border-bottom: 1px solid rgb(255 255 255 / 0.1); + background: #0c0c0d; + color: rgb(245 245 245 / 0.8); + font-size: 13px; +} + +.application-titlebar > span:nth-child(2) { + position: absolute; + left: 50%; + transform: translateX(-50%); +} + +.application-title-mark { + width: 8px; + height: 8px; + border: 1px solid rgb(255 255 255 / 0.25); + border-radius: 50%; +} + +.application-shell { + display: grid; + min-height: 286px; + grid-template-columns: 52px minmax(0, 1fr); +} + +.application-sidebar { + display: flex; + flex-direction: column; + align-items: center; + gap: 11px; + padding: 14px 7px; + border-right: 1px solid rgb(255 255 255 / 0.08); + background: rgb(255 255 255 / 0.018); +} + +.application-brand-mark, +.application-nav-item { + display: grid; + width: 30px; + height: 30px; + place-items: center; + border-radius: 7px; +} + +.application-brand-mark { + margin-bottom: 5px; + border: 1px solid rgb(255 23 68 / 0.36); + background: rgb(255 23 68 / 0.1); + color: #ff3858; + font-size: 13px; + font-weight: 700; +} + +.application-nav-item { + color: rgb(245 245 245 / 0.52); +} + +.application-nav-item.is-active { + background: rgb(255 23 68 / 0.12); + color: #ff4562; +} + +.application-nav-item svg { + width: 16px; + height: 16px; + stroke-width: 1.8; +} + +.application-main { + min-width: 0; + padding: 17px 18px 16px; +} + +.application-toolbar, +.application-activity-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.application-overline { + color: rgb(245 245 245 / 0.58); + font-size: 12px; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.application-toolbar h3 { + margin: 5px 0 0; + color: #f5f5f5; + font-size: 18px; + font-weight: 600; + letter-spacing: -0.035em; +} + +.application-status { + display: inline-flex; + flex: none; + align-items: center; + gap: 6px; + color: rgb(245 245 245 / 0.72); + font-size: 12px; +} + +.application-status svg { + width: 15px; + height: 15px; + color: #43d69a; +} + +.application-metrics { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin-top: 15px; +} + +.application-metric { + display: grid; + min-height: 89px; + align-content: center; + gap: 5px; + padding: 11px 13px; + border: 1px solid rgb(255 255 255 / 0.1); + border-radius: 7px; + background: rgb(255 255 255 / 0.025); +} + +.application-metric > span, +.application-metric small, +.application-activity-row small { + color: rgb(245 245 245 / 0.62); + font-size: 12px; +} + +.application-metric strong { + color: #f5f5f5; + font-size: 21px; + font-weight: 600; + line-height: 1.1; +} + +.application-metric:first-child small { + color: #ff8295; +} + +.application-activity { + margin-top: 15px; + padding-top: 12px; + border-top: 1px solid rgb(255 255 255 / 0.09); +} + +.application-activity-heading { + margin-bottom: 5px; + color: rgb(245 245 245 / 0.88); + font-size: 13px; +} + +.application-activity-heading > span { + color: #ff8193; + font-size: 12px; +} + +.application-activity-row { + display: flex; + min-width: 0; + align-items: center; + gap: 9px; + padding-block: 7px; + border-top: 1px solid rgb(255 255 255 / 0.055); +} + +.application-activity-row > i { + width: 7px; + height: 7px; + flex: none; + border-radius: 50%; + background: #ff3758; + box-shadow: 0 0 9px rgb(255 23 68 / 0.5); +} + +.application-activity-row > span { + display: grid; + min-width: 0; + gap: 2px; +} + +.application-activity-row strong { + overflow: hidden; + color: rgb(245 245 245 / 0.88); + font-size: 12px; + font-weight: 550; + text-overflow: ellipsis; + white-space: nowrap; +} + +.application-activity-row em { + margin-left: auto; + color: #53d69c; + font-size: 12px; + font-style: normal; +} + +.feature-icons span { + font-size: 12px; +} + +@media (min-width: 700px) and (max-width: 799px) { + .home-hero-content { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + } + + .hero-actions { + gap: 8px; + } + + .hero-actions .home-button { + padding-inline: 12px; + font-size: 14px; + } + + .javascript-showcase { + grid-template-columns: minmax(0, 1fr); + gap: 24px; + } + + .application-toolbar { + gap: 20px; + } +} + +@media (min-width: 700px) and (max-width: 1023px) { + #nd-home-layout #nd-nav ul:first-of-type a { + font-size: 14px; + } + + .code-scroll pre { + font-size: 14px; + } + + .feature-icons span { + font-size: 12px; + } +} + +@media (min-width: 700px) and (max-width: 799px) { + .home-hero { + min-height: 900px; + } + + .home-hero-content { + min-height: 680px; + } + + .hero-scene { + background-size: auto 280px; + background-position: center bottom; + } +} + +@media (max-width: 699px) { + .home-hero { + min-height: 800px; + } + + .home-hero-content { + min-height: 0; + } + + .hero-install-panel .hero-install { + gap: 8px; + padding-inline: 10px; + font-size: 13px; + } + + .hero-scene { + background-size: auto 260px; + background-position: right bottom; + } + + .code-scroll pre { + font-size: 14px; + } + + .feature-icons span { + font-size: 12px; + } + + .application-shell { + min-height: 278px; + grid-template-columns: 46px minmax(0, 1fr); + } + + .application-main { + padding: 14px 12px; + } + + .application-sidebar { + gap: 9px; + padding-inline: 5px; + } + + .application-brand-mark, + .application-nav-item { + width: 28px; + height: 28px; + } + + .application-toolbar h3 { + font-size: 16px; + } + + .application-toolbar { + flex-wrap: wrap; + } + + .application-status { + font-size: 12px; + } + + .application-metrics { + gap: 8px; + } + + .application-metric { + padding-inline: 9px; + } + + .home-cta .closing-scene { + width: 100%; + background-size: auto 230px; + background-position: 45% bottom; + background-repeat: no-repeat; + } +} + +@media (max-width: 360px) { + .hero-install-panel .hero-install { + font-size: 12px; + } +} + +.hero-feature-strip { + position: relative; + z-index: 1; + width: 100%; + border-bottom: 1px solid rgb(255 255 255 / 0.09); + background: linear-gradient(180deg, #080809, #050506); +} + +.hero-feature-list { + display: flex; + width: min(88%, 1220px); + min-height: 86px; + align-items: center; + justify-content: space-evenly; + gap: 12px; + margin: 0 auto; + padding: 12px 0; + list-style: none; +} + +.hero-feature-item { + display: flex; + min-width: 0; + align-items: center; + justify-content: center; + gap: 9px; + color: rgb(245 245 245 / 0.86); + font-size: 14px; + font-weight: 550; + white-space: nowrap; +} + +.hero-feature-item svg { + width: 17px; + height: 17px; + flex: none; + color: #ff536d; + stroke-width: 1.8; +} + +@media (max-width: 1023px) { + .hero-feature-list { + display: grid; + min-height: 0; + grid-template-columns: repeat(3, minmax(0, 1fr)); + row-gap: 18px; + padding-block: 22px; + } +} + +@media (max-width: 599px) { + .hero-feature-list { + width: calc(100% - 40px); + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .hero-feature-item { + gap: 7px; + font-size: 13px; + } + + .hero-feature-item svg { + width: 16px; + height: 16px; + } +} + +@media (min-width: 700px) { + .hero-title { + font-size: clamp(64px, 5.25vw, 76px); + } +} + +@media (min-width: 700px) and (max-width: 1099px) { + .hero-title { + font-size: clamp(54px, 6vw, 62px); + } +} + +@media (max-width: 699px) { + .hero-title { + font-size: clamp(52px, 12vw, 64px); + } +} + +@media (max-width: 360px) { + .hero-title { + font-size: 50px; + } +} diff --git a/apps/docs/app/icon.jpg b/apps/docs/app/icon.jpg new file mode 100644 index 0000000..44a2756 Binary files /dev/null and b/apps/docs/app/icon.jpg differ diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx new file mode 100644 index 0000000..0eec142 --- /dev/null +++ b/apps/docs/app/layout.tsx @@ -0,0 +1,33 @@ +import { Inter } from 'next/font/google'; +import { Provider } from '@/components/provider'; +import type { Metadata } from 'next'; +import './global.css'; + +export const metadata: Metadata = { + metadataBase: new URL('https://webview.js.org'), + title: { + default: 'WebviewJS | Native webviews for JavaScript', + template: '%s | WebviewJS', + }, + description: + 'A typed N-API binding for creating native desktop windows with Node.js, Bun, or Deno and the webview provided by the operating system.', + openGraph: { + siteName: 'WebviewJS', + type: 'website', + locale: 'en_US', + }, +}; + +const inter = Inter({ + subsets: ['latin'], +}); + +export default function Layout({ children }: LayoutProps<'/'>) { + return ( + + + {children} + + + ); +} diff --git a/apps/docs/app/llms-full.txt/route.ts b/apps/docs/app/llms-full.txt/route.ts new file mode 100644 index 0000000..c3271b0 --- /dev/null +++ b/apps/docs/app/llms-full.txt/route.ts @@ -0,0 +1,7 @@ +import { docsLlms } from '@/lib/source'; + +export const revalidate = false; + +export async function GET() { + return new Response(await docsLlms.full()); +} diff --git a/apps/docs/app/llms.mdx/docs/[[...slug]]/route.ts b/apps/docs/app/llms.mdx/docs/[[...slug]]/route.ts new file mode 100644 index 0000000..5aac2d3 --- /dev/null +++ b/apps/docs/app/llms.mdx/docs/[[...slug]]/route.ts @@ -0,0 +1,24 @@ +import { docsLlms, source } from '@/lib/source'; +import { getPageMarkdownUrl } from '@/lib/shared'; +import { notFound } from 'next/navigation'; + +export const revalidate = false; + +export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) { + const { slug } = await params; + // remove the appended "content.md" + const page = source.getPage(slug?.slice(0, -1)); + if (!page) notFound(); + + return new Response(await docsLlms.page(page), { + headers: { + 'Content-Type': 'text/markdown', + }, + }); +} + +export function generateStaticParams() { + return source.getPages().map((page) => ({ + slug: getPageMarkdownUrl(page).segments, + })); +} diff --git a/apps/docs/app/llms.txt/route.ts b/apps/docs/app/llms.txt/route.ts new file mode 100644 index 0000000..b19e4dd --- /dev/null +++ b/apps/docs/app/llms.txt/route.ts @@ -0,0 +1,7 @@ +import { docsLlms } from '@/lib/source'; + +export const revalidate = false; + +export async function GET() { + return new Response(await docsLlms.index()); +} diff --git a/apps/docs/app/og/docs/[...slug]/route.tsx b/apps/docs/app/og/docs/[...slug]/route.tsx new file mode 100644 index 0000000..9e8dcc4 --- /dev/null +++ b/apps/docs/app/og/docs/[...slug]/route.tsx @@ -0,0 +1,25 @@ +import { source } from '@/lib/source'; +import { notFound } from 'next/navigation'; +import { generateOGImage } from 'fumadocs-ui/og'; +import { appName, getPageImageUrl } from '@/lib/shared'; + +export const revalidate = false; + +export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) { + const { slug } = await params; + const page = source.getPage(slug.slice(0, -1)); + if (!page) notFound(); + + return generateOGImage({ + title: page.data.title, + description: page.data.description, + site: appName, + }); +} + +export function generateStaticParams() { + return source.getPages().map((page) => ({ + lang: page.locale, + slug: getPageImageUrl(page).segments, + })); +} diff --git a/apps/docs/components/home/code-editor.tsx b/apps/docs/components/home/code-editor.tsx new file mode 100644 index 0000000..46c815f --- /dev/null +++ b/apps/docs/components/home/code-editor.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { Check, Copy } from 'lucide-react'; +import { useState } from 'react'; + +const example = [ + "import { Application } from '@webviewjs/webview';", + '', + 'const app = new Application();', + '', + 'const window = app.createBrowserWindow({', + " title: 'My App',", + ' width: 1024,', + ' height: 768,', + '});', + '', + "window.createWebview({ url: 'https://example.com' });", + '', + 'app.run();', +].join('\n'); + +const lineClass = 'code-line'; +const numberClass = 'code-number'; + +export function CodeEditor() { + const [copied, setCopied] = useState(false); + + async function copyExample() { + try { + await navigator.clipboard.writeText(example); + setCopied(true); + window.setTimeout(() => setCopied(false), 1800); + } catch { + setCopied(false); + } + } + + return ( +
+
+ + JS + main.js + + +
+
+
+          
+            
+              1
+              import
+              {' { Application } '}
+              from '@webviewjs/webview';
+            
+            
+              2
+            
+            
+              3
+              const
+              {' app = '}
+              new
+              {' Application();'}
+            
+            
+              4
+            
+            
+              5
+              const
+              {' window = app.'}
+              createBrowserWindow({'{'}
+            
+            
+              6
+              {'  title: '}
+              'My App',
+            
+            
+              7
+              {'  width: '}
+              1024,
+            
+            
+              8
+              {'  height: '}
+              768,
+            
+            
+              9
+              {'});'}
+            
+            
+              10
+            
+            
+              11
+              {'window.'}
+              createWebview({'{ url: '}
+              'https://example.com'
+              {' });'}
+            
+            
+              12
+            
+            
+              13
+              {'app.'}
+              run();
+            
+          
+        
+
+
+ ); +} diff --git a/apps/docs/components/home/executable.tsx b/apps/docs/components/home/executable.tsx new file mode 100644 index 0000000..c321229 --- /dev/null +++ b/apps/docs/components/home/executable.tsx @@ -0,0 +1,75 @@ +import Link from 'next/link'; +import { ArrowRight, FileText } from 'lucide-react'; +import { ClosingScene } from './scenes'; + +export function Executable() { + return ( + <> +
+
+
+

+ Build it into +
+ an executable. +

+

+ Use the WebviewJS CLI with Node.js SEA, Bun compile, or Deno compile. The platform still supplies its + system webview. +

+
+
+
+ + Terminal +
+
+
+ $ + webview build src/main.ts --name my-app +
+
+
+
+
+ +
+ +
+
+

+

+

+ Build your +
+ first window. +

+

Start with a few lines of JavaScript.

+
+ + Read the quick start
+
+
+
+ + ); +} diff --git a/apps/docs/components/home/footer.tsx b/apps/docs/components/home/footer.tsx new file mode 100644 index 0000000..3f0e9b6 --- /dev/null +++ b/apps/docs/components/home/footer.tsx @@ -0,0 +1,43 @@ +import Link from 'next/link'; +import { WebviewLogo } from './webview-logo'; + +const links = [ + { label: 'Docs', href: '/getting-started/quick-start' }, + { label: 'API', href: '/api/application' }, + { label: 'Examples', href: 'https://github.com/webviewjs/webview/tree/main/apps/examples' }, + { label: 'GitHub', href: 'https://github.com/webviewjs/webview' }, + { label: 'npm', href: 'https://www.npmjs.com/package/@webviewjs/webview' }, + { label: 'MIT License', href: 'https://github.com/webviewjs/webview/blob/main/LICENSE' }, +]; + +export function Footer() { + return ( +
+
+ + + WebviewJS + + + + JavaScript / native windows + +
+
+ ); +} diff --git a/apps/docs/components/home/hero-features.tsx b/apps/docs/components/home/hero-features.tsx new file mode 100644 index 0000000..53ec2e8 --- /dev/null +++ b/apps/docs/components/home/hero-features.tsx @@ -0,0 +1,25 @@ +import { Bell, Blocks, Braces, LayoutDashboard, Menu, PanelBottom } from 'lucide-react'; + +const features = [ + { label: 'Menus', icon: Menu }, + { label: 'Tray icons', icon: PanelBottom }, + { label: 'Notifications', icon: Bell }, + { label: 'IPC', icon: Braces }, + { label: 'Browser contexts', icon: LayoutDashboard }, + { label: 'Custom protocols', icon: Blocks }, +]; + +export function HeroFeatures() { + return ( +
+
    + {features.map(({ label, icon: Icon }) => ( +
  • +
  • + ))} +
+
+ ); +} diff --git a/apps/docs/components/home/hero.tsx b/apps/docs/components/home/hero.tsx new file mode 100644 index 0000000..a8de143 --- /dev/null +++ b/apps/docs/components/home/hero.tsx @@ -0,0 +1,49 @@ +import Link from 'next/link'; +import { ArrowRight } from 'lucide-react'; +import { InstallCommand } from './install-command'; +import { HeroScene } from './scenes'; + +export function Hero() { + return ( +
+ +
+
+

+

+

+ Build native +
+ desktop apps. +

+

+ Use the system webview on Windows, macOS, and Linux, with APIs for windows, menus, tray icons, + notifications, IPC, browser contexts, and custom protocols. Works with Node.js, Bun, and Deno. +

+
+ + Get started
+ +
+
+
+ ); +} diff --git a/apps/docs/components/home/install-command.tsx b/apps/docs/components/home/install-command.tsx new file mode 100644 index 0000000..d7ce407 --- /dev/null +++ b/apps/docs/components/home/install-command.tsx @@ -0,0 +1,90 @@ +'use client'; + +import { Bot, Check, Copy, UserRound } from 'lucide-react'; +import { useState } from 'react'; + +const commands = { + humans: 'npm i @webviewjs/webview', + agents: 'npx skills add webviewjs/webview', +} as const; + +type InstallMode = keyof typeof commands; + +export function InstallCommand() { + const [mode, setMode] = useState('humans'); + const [copied, setCopied] = useState(false); + const [message, setMessage] = useState(''); + const command = commands[mode]; + + async function copyCommand() { + try { + await navigator.clipboard.writeText(command); + setCopied(true); + setMessage('Install command copied.'); + window.setTimeout(() => setCopied(false), 1800); + } catch { + setCopied(false); + setMessage('Copy unavailable. Select the command to copy it.'); + } + } + + return ( +
+
+ + +
+
+ + + {command.split(' ')[0]} {command.slice(command.indexOf(' ') + 1)} + + +
+ + {message} + +
+ ); +} diff --git a/apps/docs/components/home/javascript-native.tsx b/apps/docs/components/home/javascript-native.tsx new file mode 100644 index 0000000..94f2f3a --- /dev/null +++ b/apps/docs/components/home/javascript-native.tsx @@ -0,0 +1,123 @@ +import { Bell, Blocks, Braces, CircleCheck, LayoutDashboard, Menu, PanelBottom, Package, Settings } from 'lucide-react'; +import { CodeEditor } from './code-editor'; + +const features = [ + { label: 'Menus', Icon: Menu }, + { label: 'Tray Icons', Icon: PanelBottom }, + { label: 'Notifications', Icon: Bell }, + { label: 'IPC', Icon: Braces }, + { label: 'Custom Protocols', Icon: Package }, +]; + +const navigation = [ + { id: 'projects', Icon: Blocks }, + { id: 'activity', Icon: Bell }, + { id: 'settings', Icon: Settings }, +]; + +export function JavaScriptNative() { + return ( +
+
+
+

+ JavaScript +
+ controls the window. +

+

+ Create a window, load a page, and add menus, notifications, tray icons, IPC, browser contexts, or custom + protocols through one simple API. +

+
+
+ +
+
+
+ + My App +
+
+ +
+
+
+ Workspace +

Project overview

+
+ + +
+
+
+ Active projects + 12 + +3 this month +
+
+ Latest build + Passed + 2 minutes ago +
+
+
+
+ Recent activity + View all +
+
+