From 4b8d974eb9c25c399229e15d6271f2bdabcb4a0c Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 23 Feb 2026 10:41:20 +0100 Subject: [PATCH 1/8] ci: auto-release workflow on merge to main Rewrite .github/workflows/release.yml to trigger on every push to main instead of manual tag pushes. The workflow now: - Computes version as 0.YYYYMMDD.GITHUB_RUN_NUMBER - Builds aarch64-apple-darwin and x86_64-apple-darwin in parallel - Merges them into a universal binary using lipo - Creates a universal .dmg and signed updater tarball - Generates latest.json with per-arch and universal platform entries - Publishes a GitHub Release with auto-generated release notes - Updates a GitHub Pages release history site (gh-pages branch) Product decisions: - Universal binary approach: copy arm64 .app as base, lipo the main executable, keep everything else from arm64 (shared frameworks are architecture-independent). This is the standard Tauri pattern. - Per-arch updater tarballs are also uploaded so the Tauri updater can download the correct arch-specific build (smaller download). - Release notes are auto-generated from git log since last tag. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release.yml | 438 ++++++++++++++++++++++++++++++++-- 1 file changed, 416 insertions(+), 22 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21d540e1..1935746b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,24 +2,50 @@ name: Release on: push: - tags: - - 'v*' + branches: + - main + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: true + +env: + APP_NAME: Laputa + BUNDLE_ID: com.tauri.dev jobs: - release: - name: Build & Release (macOS) - permissions: - contents: write + # ───────────────────────────────────────────────────────────── + # Phase 1: Compute the version string once + # ───────────────────────────────────────────────────────────── + version: + name: Compute version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.ver.outputs.version }} + tag: ${{ steps.ver.outputs.tag }} + steps: + - id: ver + run: | + VERSION="0.$(date -u +%Y%m%d).${GITHUB_RUN_NUMBER}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" + echo "### Version: \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY" + + # ───────────────────────────────────────────────────────────── + # Phase 2: Build each architecture in parallel + # ───────────────────────────────────────────────────────────── + build: + name: Build (${{ matrix.arch }}) + needs: version + runs-on: macos-latest strategy: + fail-fast: true matrix: include: - - target: aarch64-apple-darwin - runner: macos-latest - - target: x86_64-apple-darwin - runner: macos-latest - - runs-on: ${{ matrix.runner }} - + - arch: aarch64 + target: aarch64-apple-darwin + - arch: x86_64 + target: x86_64-apple-darwin steps: - uses: actions/checkout@v4 @@ -39,19 +65,387 @@ jobs: with: targets: ${{ matrix.target }} - - name: Install dependencies + - name: Rust cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + + - name: Install frontend dependencies run: pnpm install --frozen-lockfile - - name: Build and release - uses: tauri-apps/tauri-action@v0 + # Stamp the computed version into tauri.conf.json and Cargo.toml + - name: Set version + run: | + VERSION="${{ needs.version.outputs.version }}" + # tauri.conf.json + jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json + # Cargo.toml + sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml + + - name: Build frontend + run: pnpm build + + - name: Build Tauri app env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + run: | + pnpm tauri build --target ${{ matrix.target }} + + # Upload the .app bundle so the merge job can lipo them together. + # Also upload the per-arch .tar.gz updater artifact and its .sig. + - name: Upload arch artifacts + uses: actions/upload-artifact@v4 with: - tagName: ${{ github.ref_name }} - releaseName: 'Laputa v__VERSION__' - releaseBody: 'See the assets below to download and install this version.' - releaseDraft: false + name: app-${{ matrix.arch }} + path: | + src-tauri/target/${{ matrix.target }}/release/bundle/macos/${{ env.APP_NAME }}.app + retention-days: 1 + + - name: Upload updater artifacts + uses: actions/upload-artifact@v4 + with: + name: updater-${{ matrix.arch }} + path: | + src-tauri/target/${{ matrix.target }}/release/bundle/macos/${{ env.APP_NAME }}.app.tar.gz + src-tauri/target/${{ matrix.target }}/release/bundle/macos/${{ env.APP_NAME }}.app.tar.gz.sig + retention-days: 1 + + - name: Upload dmg artifact + uses: actions/upload-artifact@v4 + with: + name: dmg-${{ matrix.arch }} + path: | + src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg + retention-days: 1 + + # ───────────────────────────────────────────────────────────── + # Phase 3: Merge into universal binary, re-package, and release + # ───────────────────────────────────────────────────────────── + release: + name: Universal binary & GitHub Release + needs: [version, build] + runs-on: macos-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # for release notes generation + + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Create universal binary + run: | + set -euo pipefail + mkdir -p universal + + ARM_APP="app-aarch64/${{ env.APP_NAME }}.app" + X86_APP="app-x86_64/${{ env.APP_NAME }}.app" + + # Copy the aarch64 .app as base (preserves bundle structure) + cp -R "$ARM_APP" "universal/${{ env.APP_NAME }}.app" + + # Find the main executable inside the .app bundle + EXEC_NAME="${{ env.APP_NAME }}" + # Tauri uses lowercase binary name + EXEC_NAME_LOWER=$(echo "$EXEC_NAME" | tr '[:upper:]' '[:lower:]') + ARM_BIN="$ARM_APP/Contents/MacOS/$EXEC_NAME_LOWER" + X86_BIN="$X86_APP/Contents/MacOS/$EXEC_NAME_LOWER" + UNI_BIN="universal/${{ env.APP_NAME }}.app/Contents/MacOS/$EXEC_NAME_LOWER" + + # If the binary is the product name (case-sensitive), try that too + if [ ! -f "$ARM_BIN" ]; then + ARM_BIN="$ARM_APP/Contents/MacOS/$EXEC_NAME" + X86_BIN="$X86_APP/Contents/MacOS/$EXEC_NAME" + UNI_BIN="universal/${{ env.APP_NAME }}.app/Contents/MacOS/$EXEC_NAME" + fi + + echo "Merging: $ARM_BIN + $X86_BIN → $UNI_BIN" + lipo -create "$ARM_BIN" "$X86_BIN" -output "$UNI_BIN" + lipo -info "$UNI_BIN" + + - name: Create universal .dmg + run: | + set -euo pipefail + VERSION="${{ needs.version.outputs.version }}" + DMG_NAME="${{ env.APP_NAME }}_${VERSION}_universal.dmg" + + hdiutil create -volname "${{ env.APP_NAME }}" \ + -srcfolder "universal/${{ env.APP_NAME }}.app" \ + -ov -format UDZO \ + "universal/$DMG_NAME" + + echo "DMG_PATH=universal/$DMG_NAME" >> "$GITHUB_ENV" + echo "DMG_NAME=$DMG_NAME" >> "$GITHUB_ENV" + + - name: Create universal updater tarball and sign + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + run: | + set -euo pipefail + + # Create tar.gz of the universal .app + TARBALL="universal/${{ env.APP_NAME }}.app.tar.gz" + tar -czf "$TARBALL" -C universal "${{ env.APP_NAME }}.app" + + # Install tauri-cli for signing + npm install -g @tauri-apps/cli + + # Sign the tarball using Tauri's signer + tauri signer sign "$TARBALL" --private-key "$TAURI_SIGNING_PRIVATE_KEY" --password "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" + + echo "TARBALL_PATH=$TARBALL" >> "$GITHUB_ENV" + echo "SIG_PATH=${TARBALL}.sig" >> "$GITHUB_ENV" + + - name: Generate release notes + id: notes + run: | + set -euo pipefail + + # Find the previous release tag + PREV_TAG=$(git tag --sort=-version:refname | head -n 1 || echo "") + + if [ -z "$PREV_TAG" ]; then + # No previous tag — use all commits on main + NOTES=$(git log --oneline --no-merges -20) + else + NOTES=$(git log --oneline --no-merges "${PREV_TAG}..HEAD") + fi + + # Write to file for the release body + { + echo "## What's Changed" + echo "" + echo "$NOTES" | while IFS= read -r line; do + echo "- $line" + done + echo "" + echo "---" + echo "*Built from \`$(git rev-parse --short HEAD)\` on $(date -u +%Y-%m-%d)*" + } > release_notes.md + + cat release_notes.md + + - name: Build latest.json + run: | + set -euo pipefail + VERSION="${{ needs.version.outputs.version }}" + TAG="${{ needs.version.outputs.tag }}" + REPO="refactoringhq/laputa-app" + + # Read signatures + UNI_SIG=$(cat "${SIG_PATH}") + ARM_SIG=$(cat updater-aarch64/${{ env.APP_NAME }}.app.tar.gz.sig) + X86_SIG=$(cat updater-x86_64/${{ env.APP_NAME }}.app.tar.gz.sig) + + # The universal tarball URL + UNI_URL="https://github.com/${REPO}/releases/download/${TAG}/${{ env.APP_NAME }}.app.tar.gz" + + # Per-arch URLs (fallback) + ARM_URL="https://github.com/${REPO}/releases/download/${TAG}/${{ env.APP_NAME }}_aarch64.app.tar.gz" + X86_URL="https://github.com/${REPO}/releases/download/${TAG}/${{ env.APP_NAME }}_x86_64.app.tar.gz" + + cat > latest.json << MANIFEST + { + "version": "${VERSION}", + "notes": "See https://refactoringhq.github.io/laputa-app/ for full release notes.", + "pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "platforms": { + "darwin-aarch64": { + "signature": "${ARM_SIG}", + "url": "${ARM_URL}" + }, + "darwin-x86_64": { + "signature": "${X86_SIG}", + "url": "${X86_URL}" + }, + "darwin-universal": { + "signature": "${UNI_SIG}", + "url": "${UNI_URL}" + } + } + } + MANIFEST + + echo "latest.json:" + cat latest.json + + - name: Rename per-arch updater artifacts + run: | + cp "updater-aarch64/${{ env.APP_NAME }}.app.tar.gz" "universal/${{ env.APP_NAME }}_aarch64.app.tar.gz" + cp "updater-aarch64/${{ env.APP_NAME }}.app.tar.gz.sig" "universal/${{ env.APP_NAME }}_aarch64.app.tar.gz.sig" + cp "updater-x86_64/${{ env.APP_NAME }}.app.tar.gz" "universal/${{ env.APP_NAME }}_x86_64.app.tar.gz" + cp "updater-x86_64/${{ env.APP_NAME }}.app.tar.gz.sig" "universal/${{ env.APP_NAME }}_x86_64.app.tar.gz.sig" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.version.outputs.tag }} + name: Laputa ${{ needs.version.outputs.version }} + body_path: release_notes.md + draft: false prerelease: false - args: --target ${{ matrix.target }} + files: | + ${{ env.DMG_PATH }} + ${{ env.TARBALL_PATH }} + ${{ env.SIG_PATH }} + universal/${{ env.APP_NAME }}_aarch64.app.tar.gz + universal/${{ env.APP_NAME }}_aarch64.app.tar.gz.sig + universal/${{ env.APP_NAME }}_x86_64.app.tar.gz + universal/${{ env.APP_NAME }}_x86_64.app.tar.gz.sig + latest.json + dmg-aarch64/*.dmg + dmg-x86_64/*.dmg + + # ───────────────────────────────────────────────────────────── + # Phase 4: Update GitHub Pages with release history + # ───────────────────────────────────────────────────────────── + pages: + name: Update release history page + needs: [version, release] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: gh-pages + fetch-depth: 0 + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Initialize gh-pages if needed + run: | + if [ ! -f index.html ]; then + echo "Initializing gh-pages branch" + fi + + - name: Fetch release data and rebuild page + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + # Fetch all releases as JSON + gh api repos/${{ github.repository }}/releases --paginate > releases.json + + # Build the HTML page + cat > index.html << 'HTMLEOF' + + + + + + Laputa — Release History + + + +

Laputa Release History

+

Auto-updated on every release

+
+ + + + HTMLEOF2 + + - name: Commit and push + run: | + git add index.html + git diff --cached --quiet && echo "No changes" && exit 0 + git commit -m "Update release history for ${{ needs.version.outputs.tag }}" + git push From 62c52c72661790241b03620c87eac3c86f205be1 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 23 Feb 2026 10:42:15 +0100 Subject: [PATCH 2/8] ci: github pages with release history Use peaceiris/actions-gh-pages@v4 to deploy a release history site. The page fetches releases.json (also deployed) and renders each release with date, notes, and download links for .dmg files. This handles the gh-pages branch creation automatically on first run. The page is available at https://refactoringhq.github.io/laputa-app/ Product decision: used fetch() to load releases.json at runtime instead of inlining it, which is cleaner and avoids shell escaping issues with release note content. The releases.json is deployed alongside index.html. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release.yml | 120 ++++++++++++---------------------- 1 file changed, 41 insertions(+), 79 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1935746b..f5d22eb7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -311,32 +311,19 @@ jobs: contents: write steps: - uses: actions/checkout@v4 - with: - ref: gh-pages - fetch-depth: 0 - - name: Configure git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Initialize gh-pages if needed - run: | - if [ ! -f index.html ]; then - echo "Initializing gh-pages branch" - fi - - - name: Fetch release data and rebuild page + - name: Build release history page env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail + mkdir -p _site # Fetch all releases as JSON - gh api repos/${{ github.repository }}/releases --paginate > releases.json + gh api repos/${{ github.repository }}/releases --paginate > _site/releases.json # Build the HTML page - cat > index.html << 'HTMLEOF' + cat > _site/index.html << 'HTMLEOF' @@ -354,15 +341,8 @@ jobs: max-width: 720px; margin: 0 auto; } - h1 { - font-size: 1.75rem; - font-weight: 600; - margin-bottom: 0.5rem; - } - .subtitle { - color: #787774; - margin-bottom: 2rem; - } + h1 { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.5rem; } + .subtitle { color: #787774; margin-bottom: 2rem; } .release { background: #fff; border: 1px solid #E9E9E7; @@ -370,36 +350,19 @@ jobs: padding: 1.25rem 1.5rem; margin-bottom: 1rem; } - .release h2 { - font-size: 1.125rem; - font-weight: 600; - margin-bottom: 0.25rem; - } - .release .meta { - font-size: 0.8125rem; - color: #787774; - margin-bottom: 0.75rem; - } - .release .body { - font-size: 0.875rem; - white-space: pre-wrap; - } - .release .body ul { padding-left: 1.25rem; } + .release h2 { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.25rem; } + .release .meta { font-size: 0.8125rem; color: #787774; margin-bottom: 0.75rem; } + .release .body { font-size: 0.875rem; white-space: pre-wrap; } .release .downloads { margin-top: 0.75rem; - display: flex; - gap: 0.5rem; - flex-wrap: wrap; + display: flex; gap: 0.5rem; flex-wrap: wrap; } .release .downloads a { display: inline-block; padding: 0.375rem 0.75rem; - background: #155DFF; - color: #fff; - border-radius: 6px; - text-decoration: none; - font-size: 0.8125rem; - font-weight: 500; + background: #155DFF; color: #fff; + border-radius: 6px; text-decoration: none; + font-size: 0.8125rem; font-weight: 500; } .release .downloads a:hover { background: #1248CC; } .empty { color: #787774; text-align: center; padding: 3rem; } @@ -410,42 +373,41 @@ jobs:

Auto-updated on every release

- HTMLEOF2 + HTMLEOF - - name: Commit and push - run: | - git add index.html - git diff --cached --quiet && echo "No changes" && exit 0 - git commit -m "Update release history for ${{ needs.version.outputs.tag }}" - git push + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./_site + commit_message: "Update release history for ${{ needs.version.outputs.tag }}" From 4106bea6700a7c6657d8bfd8062060e17b5dadb2 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 23 Feb 2026 10:44:31 +0100 Subject: [PATCH 3/8] feat: in-app update notification UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the old window.confirm updater with a proper React-based update notification system: - useUpdater hook now exposes state machine (idle → available → downloading → ready) and actions (startDownload, openReleaseNotes, dismiss) - UpdateBanner component renders at the top of the app shell: - "Available" state: shows version, Release Notes link, Update Now button, dismiss X - "Downloading" state: animated spinner, progress bar with percentage - "Ready" state: Restart Now button to apply the update - Silently checks on startup after 3s delay; fails silently on network errors or 404 - Release Notes link opens the GitHub Pages release history site Product decisions: - Banner at top of app (not a modal) — non-intrusive, visible but not blocking. User can dismiss and continue working. - Progress bar shows during download so user knows it's working. - Separate "Restart Now" state after download so user controls when the app restarts (they may have unsaved work). Co-Authored-By: Claude Opus 4.6 --- src/App.css | 5 ++ src/App.tsx | 4 +- src/components/UpdateBanner.tsx | 145 ++++++++++++++++++++++++++++++++ src/hooks/useUpdater.ts | 110 +++++++++++++++++++----- 4 files changed, 240 insertions(+), 24 deletions(-) create mode 100644 src/components/UpdateBanner.tsx diff --git a/src/App.css b/src/App.css index b6e63a44..e9c59281 100644 --- a/src/App.css +++ b/src/App.css @@ -46,3 +46,8 @@ .app__editor > * { flex: 1; } + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} diff --git a/src/App.tsx b/src/App.tsx index ba0aa874..59a75529 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import { useEntryActions } from './hooks/useEntryActions' import { isTauri } from './mock-tauri' import { useKeyboardNavigation } from './hooks/useKeyboardNavigation' import { useUpdater } from './hooks/useUpdater' +import { UpdateBanner } from './components/UpdateBanner' import { setApiKey } from './utils/ai-chat' import type { SidebarSelection, GitCommit } from './types' import type { VaultOption } from './components/StatusBar' @@ -139,7 +140,7 @@ function App() { handleCloseTabRef: notes.handleCloseTabRef, }) - useUpdater() + const { status: updateStatus, actions: updateActions } = useUpdater() useKeyboardNavigation({ tabs: notes.tabs, @@ -168,6 +169,7 @@ function App() { return (
+
setShowCommitDialog(true)} /> diff --git a/src/components/UpdateBanner.tsx b/src/components/UpdateBanner.tsx new file mode 100644 index 00000000..64359ec8 --- /dev/null +++ b/src/components/UpdateBanner.tsx @@ -0,0 +1,145 @@ +import { Download, ExternalLink, RefreshCw, X } from 'lucide-react' +import type { UpdateStatus, UpdateActions } from '../hooks/useUpdater' +import { restartApp } from '../hooks/useUpdater' + +interface UpdateBannerProps { + status: UpdateStatus + actions: UpdateActions +} + +export function UpdateBanner({ status, actions }: UpdateBannerProps) { + if (status.state === 'idle' || status.state === 'error') return null + + return ( +
+ {status.state === 'available' && ( + <> + + + Laputa {status.version} is available + + + + + + )} + + {status.state === 'downloading' && ( + <> + + Downloading Laputa {status.version}... +
+
+
+ + {Math.round(status.progress * 100)}% + + + )} + + {status.state === 'ready' && ( + <> + + + Laputa {status.version} is ready — restart to apply + + + + )} +
+ ) +} diff --git a/src/hooks/useUpdater.ts b/src/hooks/useUpdater.ts index 4f63f4b4..2f92ba66 100644 --- a/src/hooks/useUpdater.ts +++ b/src/hooks/useUpdater.ts @@ -1,40 +1,104 @@ -import { useEffect } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { isTauri } from '../mock-tauri' -/** - * Checks for OTA updates on app startup (Tauri only). - * If an update is available, shows a native confirm dialog and - * downloads + installs + relaunches if the user accepts. - */ -export function useUpdater() { +const RELEASE_NOTES_URL = 'https://refactoringhq.github.io/laputa-app/' + +export type UpdateStatus = + | { state: 'idle' } + | { state: 'available'; version: string; notes: string | undefined } + | { state: 'downloading'; version: string; progress: number } + | { state: 'ready'; version: string } + | { state: 'error' } + +export interface UpdateActions { + startDownload: () => void + openReleaseNotes: () => void + dismiss: () => void +} + +export function useUpdater(): { status: UpdateStatus; actions: UpdateActions } { + const [status, setStatus] = useState({ state: 'idle' }) + const updateRef = useRef(null) + useEffect(() => { if (!isTauri()) return const checkForUpdates = async () => { try { const { check } = await import('@tauri-apps/plugin-updater') - const { relaunch } = await import('@tauri-apps/plugin-process') - const update = await check() - if (!update) return + if (!update) return // up to date - const yes = window.confirm( - `A new version (${update.version}) is available.\n\n` + - (update.body ? `${update.body}\n\n` : '') + - 'Do you want to update and restart now?' - ) - if (!yes) return - - await update.downloadAndInstall() - await relaunch() - } catch (err) { - // Silently log — update check failures should never block the app - console.warn('[updater] Failed to check for updates:', err) + updateRef.current = update + setStatus({ + state: 'available', + version: update.version, + notes: update.body ?? undefined, + }) + } catch { + // Network error or 404 — fail silently + console.warn('[updater] Failed to check for updates') } } - // Delay slightly so the app can render first + // Delay so the app can render first const timer = setTimeout(checkForUpdates, 3000) return () => clearTimeout(timer) }, []) + + const startDownload = useCallback(async () => { + const update = updateRef.current as { + version: string + downloadAndInstall: (cb: (event: { event: string; data?: { contentLength?: number; chunkLength?: number } }) => void) => Promise + } | null + if (!update) return + + let totalBytes = 0 + let downloadedBytes = 0 + + setStatus({ state: 'downloading', version: update.version, progress: 0 }) + + try { + await update.downloadAndInstall((event) => { + if (event.event === 'Started' && event.data?.contentLength) { + totalBytes = event.data.contentLength + } else if (event.event === 'Progress' && event.data?.chunkLength) { + downloadedBytes += event.data.chunkLength + const progress = totalBytes > 0 ? Math.min(downloadedBytes / totalBytes, 1) : 0 + setStatus({ state: 'downloading', version: update.version, progress }) + } else if (event.event === 'Finished') { + setStatus({ state: 'ready', version: update.version }) + } + }) + + // If Finished wasn't emitted via callback, set ready after await resolves + setStatus((prev) => (prev.state === 'downloading' ? { state: 'ready', version: update.version } : prev)) + } catch { + console.warn('[updater] Download failed') + setStatus({ state: 'error' }) + } + }, []) + + const openReleaseNotes = useCallback(() => { + window.open(RELEASE_NOTES_URL, '_blank') + }, []) + + const dismiss = useCallback(() => { + setStatus({ state: 'idle' }) + }, []) + + return { status, actions: { startDownload, openReleaseNotes, dismiss } } +} + +/** + * Trigger app restart after an update has been downloaded. + * Separated so the component can call it on button click. + */ +export async function restartApp(): Promise { + try { + const { relaunch } = await import('@tauri-apps/plugin-process') + await relaunch() + } catch { + console.warn('[updater] Failed to relaunch') + } } From ca7e1da7bf21d2d8e201bcdc7a82606beee813ea Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 23 Feb 2026 10:45:40 +0100 Subject: [PATCH 4/8] test: updater component tests Rewrite useUpdater hook tests and add UpdateBanner component tests: Hook tests (10 cases): - Starts in idle state - Does nothing when not in Tauri - Checks for updates after 3s delay - Stays idle when no update available - Transitions to available when update found - Handles missing release body gracefully - Stays idle on network error (fails silently) - Dismiss returns to idle - openReleaseNotes opens correct URL - startDownload transitions through downloading to ready Component tests (10 cases): - Renders nothing when idle - Renders nothing on error - Shows version and buttons when available - Update Now calls startDownload - Release Notes calls openReleaseNotes - Dismiss button works - Shows progress bar during download - Shows 0% at start of download - Shows restart button when ready - Restart button calls restartApp All 457 tests pass. Co-Authored-By: Claude Opus 4.6 --- src/components/UpdateBanner.test.tsx | 114 ++++++++++++++++++++ src/hooks/useUpdater.test.ts | 151 +++++++++++++++++++++------ 2 files changed, 235 insertions(+), 30 deletions(-) create mode 100644 src/components/UpdateBanner.test.tsx diff --git a/src/components/UpdateBanner.test.tsx b/src/components/UpdateBanner.test.tsx new file mode 100644 index 00000000..f8d4e3e0 --- /dev/null +++ b/src/components/UpdateBanner.test.tsx @@ -0,0 +1,114 @@ +import { render, screen, fireEvent } from '@testing-library/react' +import { describe, it, expect, vi } from 'vitest' +import { UpdateBanner } from './UpdateBanner' +import type { UpdateStatus, UpdateActions } from '../hooks/useUpdater' + +// Mock restartApp to prevent dynamic import issues in tests +vi.mock('../hooks/useUpdater', async () => { + const actual = await vi.importActual('../hooks/useUpdater') + return { + ...actual, + restartApp: vi.fn(), + } +}) + +function makeActions(overrides?: Partial): UpdateActions { + return { + startDownload: vi.fn(), + openReleaseNotes: vi.fn(), + dismiss: vi.fn(), + ...overrides, + } +} + +describe('UpdateBanner', () => { + it('renders nothing when idle', () => { + const status: UpdateStatus = { state: 'idle' } + const { container } = render() + expect(container.innerHTML).toBe('') + }) + + it('renders nothing on error state', () => { + const status: UpdateStatus = { state: 'error' } + const { container } = render() + expect(container.innerHTML).toBe('') + }) + + it('shows version and action buttons when update is available', () => { + const status: UpdateStatus = { state: 'available', version: '1.5.0', notes: 'Bug fixes' } + const actions = makeActions() + render() + + expect(screen.getByTestId('update-banner')).toBeTruthy() + expect(screen.getByText(/Laputa 1\.5\.0/)).toBeTruthy() + expect(screen.getByText('is available')).toBeTruthy() + expect(screen.getByTestId('update-now-btn')).toBeTruthy() + expect(screen.getByTestId('update-release-notes')).toBeTruthy() + expect(screen.getByTestId('update-dismiss')).toBeTruthy() + }) + + it('"Update Now" calls startDownload', () => { + const status: UpdateStatus = { state: 'available', version: '1.5.0', notes: undefined } + const actions = makeActions() + render() + + fireEvent.click(screen.getByTestId('update-now-btn')) + expect(actions.startDownload).toHaveBeenCalledOnce() + }) + + it('"Release Notes" link calls openReleaseNotes', () => { + const status: UpdateStatus = { state: 'available', version: '1.5.0', notes: undefined } + const actions = makeActions() + render() + + fireEvent.click(screen.getByTestId('update-release-notes')) + expect(actions.openReleaseNotes).toHaveBeenCalledOnce() + }) + + it('dismiss button calls dismiss action', () => { + const status: UpdateStatus = { state: 'available', version: '1.5.0', notes: undefined } + const actions = makeActions() + render() + + fireEvent.click(screen.getByTestId('update-dismiss')) + expect(actions.dismiss).toHaveBeenCalledOnce() + }) + + it('shows progress bar during download', () => { + const status: UpdateStatus = { state: 'downloading', version: '1.5.0', progress: 0.65 } + render() + + expect(screen.getByText(/Downloading Laputa 1\.5\.0/)).toBeTruthy() + expect(screen.getByText('65%')).toBeTruthy() + + const progressBar = screen.getByTestId('update-progress') + expect(progressBar.style.width).toBe('65%') + }) + + it('shows 0% at start of download', () => { + const status: UpdateStatus = { state: 'downloading', version: '2.0.0', progress: 0 } + render() + + expect(screen.getByText('0%')).toBeTruthy() + const progressBar = screen.getByTestId('update-progress') + expect(progressBar.style.width).toBe('0%') + }) + + it('shows restart button when update is ready', () => { + const status: UpdateStatus = { state: 'ready', version: '1.5.0' } + render() + + expect(screen.getByText(/Laputa 1\.5\.0/)).toBeTruthy() + expect(screen.getByText(/restart to apply/)).toBeTruthy() + expect(screen.getByTestId('update-restart-btn')).toBeTruthy() + }) + + it('restart button calls restartApp', async () => { + const { restartApp } = await import('../hooks/useUpdater') + const status: UpdateStatus = { state: 'ready', version: '1.5.0' } + render() + + fireEvent.click(screen.getByTestId('update-restart-btn')) + expect(restartApp).toHaveBeenCalled() + }) +}) diff --git a/src/hooks/useUpdater.test.ts b/src/hooks/useUpdater.test.ts index d63d83df..05308826 100644 --- a/src/hooks/useUpdater.test.ts +++ b/src/hooks/useUpdater.test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react' +import { renderHook, act } from '@testing-library/react' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { useUpdater } from './useUpdater' @@ -25,7 +25,6 @@ describe('useUpdater', () => { beforeEach(() => { vi.useFakeTimers() vi.clearAllMocks() - vi.spyOn(window, 'confirm').mockReturnValue(false) vi.spyOn(console, 'warn').mockImplementation(() => {}) }) @@ -34,74 +33,166 @@ describe('useUpdater', () => { vi.restoreAllMocks() }) - it('does nothing when not in Tauri', () => { + it('starts in idle state', () => { + vi.mocked(isTauri).mockReturnValue(false) + const { result } = renderHook(() => useUpdater()) + expect(result.current.status).toEqual({ state: 'idle' }) + }) + + it('does nothing when not in Tauri', async () => { vi.mocked(isTauri).mockReturnValue(false) renderHook(() => useUpdater()) - vi.advanceTimersByTime(5000) + await vi.advanceTimersByTimeAsync(5000) expect(mockCheck).not.toHaveBeenCalled() }) - it('checks for updates after delay when in Tauri', async () => { + it('checks for updates after 3s delay when in Tauri', async () => { vi.mocked(isTauri).mockReturnValue(true) mockCheck.mockResolvedValue(null) // no update renderHook(() => useUpdater()) expect(mockCheck).not.toHaveBeenCalled() - // Advance past the 3s delay, then flush microtasks for dynamic imports await vi.advanceTimersByTimeAsync(3500) - // Dynamic imports are resolved by the mock, but need microtask flush await vi.waitFor(() => { expect(mockCheck).toHaveBeenCalledOnce() }) }) - it('shows confirm dialog when update is available', async () => { + it('stays idle when no update is available', async () => { + vi.mocked(isTauri).mockReturnValue(true) + mockCheck.mockResolvedValue(null) + + const { result } = renderHook(() => useUpdater()) + await vi.advanceTimersByTimeAsync(3500) + + await vi.waitFor(() => { + expect(mockCheck).toHaveBeenCalled() + }) + expect(result.current.status).toEqual({ state: 'idle' }) + }) + + it('transitions to available when update is found', async () => { vi.mocked(isTauri).mockReturnValue(true) mockCheck.mockResolvedValue({ version: '1.2.0', body: 'Bug fixes and improvements', - downloadAndInstall: vi.fn().mockResolvedValue(undefined), + downloadAndInstall: vi.fn(), }) - vi.spyOn(window, 'confirm').mockReturnValue(false) - renderHook(() => useUpdater()) + const { result } = renderHook(() => useUpdater()) await vi.advanceTimersByTimeAsync(3500) - expect(window.confirm).toHaveBeenCalledWith( - expect.stringContaining('1.2.0') - ) - expect(mockRelaunch).not.toHaveBeenCalled() + await vi.waitFor(() => { + expect(result.current.status).toEqual({ + state: 'available', + version: '1.2.0', + notes: 'Bug fixes and improvements', + }) + }) }) - it('downloads, installs, and relaunches when user accepts', async () => { + it('handles missing body gracefully', async () => { vi.mocked(isTauri).mockReturnValue(true) - const mockDownloadAndInstall = vi.fn().mockResolvedValue(undefined) mockCheck.mockResolvedValue({ - version: '1.2.0', - body: '', - downloadAndInstall: mockDownloadAndInstall, + version: '2.0.0', + body: null, + downloadAndInstall: vi.fn(), }) - vi.spyOn(window, 'confirm').mockReturnValue(true) - mockRelaunch.mockResolvedValue(undefined) - renderHook(() => useUpdater()) + const { result } = renderHook(() => useUpdater()) await vi.advanceTimersByTimeAsync(3500) - expect(mockDownloadAndInstall).toHaveBeenCalled() - expect(mockRelaunch).toHaveBeenCalled() + await vi.waitFor(() => { + expect(result.current.status).toEqual({ + state: 'available', + version: '2.0.0', + notes: undefined, + }) + }) }) - it('logs warning on check failure without crashing', async () => { + it('stays idle on network error (fails silently)', async () => { vi.mocked(isTauri).mockReturnValue(true) mockCheck.mockRejectedValue(new Error('Network error')) - renderHook(() => useUpdater()) + const { result } = renderHook(() => useUpdater()) await vi.advanceTimersByTimeAsync(3500) - expect(console.warn).toHaveBeenCalledWith( - '[updater] Failed to check for updates:', - expect.any(Error) + await vi.waitFor(() => { + expect(console.warn).toHaveBeenCalledWith( + '[updater] Failed to check for updates' + ) + }) + expect(result.current.status).toEqual({ state: 'idle' }) + }) + + it('dismiss returns to idle from available', async () => { + vi.mocked(isTauri).mockReturnValue(true) + mockCheck.mockResolvedValue({ + version: '1.2.0', + body: 'Notes', + downloadAndInstall: vi.fn(), + }) + + const { result } = renderHook(() => useUpdater()) + await vi.advanceTimersByTimeAsync(3500) + + await vi.waitFor(() => { + expect(result.current.status.state).toBe('available') + }) + + act(() => { + result.current.actions.dismiss() + }) + + expect(result.current.status).toEqual({ state: 'idle' }) + }) + + it('openReleaseNotes opens the release notes URL', async () => { + vi.mocked(isTauri).mockReturnValue(false) + const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null) + + const { result } = renderHook(() => useUpdater()) + + act(() => { + result.current.actions.openReleaseNotes() + }) + + expect(openSpy).toHaveBeenCalledWith( + 'https://refactoringhq.github.io/laputa-app/', + '_blank' ) }) + + it('startDownload transitions through downloading to ready', async () => { + vi.mocked(isTauri).mockReturnValue(true) + + const mockDownload = vi.fn(async (callback: (event: { event: string; data?: Record }) => void) => { + callback({ event: 'Started', data: { contentLength: 1000 } }) + callback({ event: 'Progress', data: { chunkLength: 500 } }) + callback({ event: 'Progress', data: { chunkLength: 500 } }) + callback({ event: 'Finished' }) + }) + + mockCheck.mockResolvedValue({ + version: '1.2.0', + body: 'Notes', + downloadAndInstall: mockDownload, + }) + + const { result } = renderHook(() => useUpdater()) + await vi.advanceTimersByTimeAsync(3500) + + await vi.waitFor(() => { + expect(result.current.status.state).toBe('available') + }) + + await act(async () => { + await result.current.actions.startDownload() + }) + + expect(result.current.status).toEqual({ state: 'ready', version: '1.2.0' }) + expect(mockDownload).toHaveBeenCalled() + }) }) From 9df9130b32a964a8e26060bacbb4f5e3d48b60d0 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 23 Feb 2026 10:46:27 +0100 Subject: [PATCH 5/8] design: auto-build-release wireframes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy ui-design.pen as base. Frames to be added for: 1. Update notification banner (visible state) — horizontal bar at top of app shell with version text, Release Notes link, Update Now button, and dismiss X 2. Update download progress state — spinner icon, progress bar with percentage, downloading text 3. "Restart to apply" state — green accent, version text, Restart Now button Note: Pencil editor was not available during this session. The base design file is committed; frames will be added when the editor is accessible. The implemented component (UpdateBanner.tsx) serves as the source of truth for the design. Co-Authored-By: Claude Opus 4.6 --- design/auto-build-release.pen | 14536 ++++++++++++++++++++++++++++++++ 1 file changed, 14536 insertions(+) create mode 100644 design/auto-build-release.pen diff --git a/design/auto-build-release.pen b/design/auto-build-release.pen new file mode 100644 index 00000000..2a16c03d --- /dev/null +++ b/design/auto-build-release.pen @@ -0,0 +1,14536 @@ +{ + "version": "2.8", + "children": [ + { + "type": "frame", + "id": "qHhaj", + "x": 68, + "y": 3385, + "name": "Laputa App — Full Layout (Light)", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 1440, + "height": 900, + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "yVQFF", + "name": "macOS Title Bar", + "width": "fill_container", + "height": 38, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "0MCME", + "name": "trafficLights", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "c2AU6", + "name": "closeBtn", + "fill": "#FF5F57", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "h0zbh", + "name": "minimizeBtn", + "fill": "#FEBC2E", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "tcqbu", + "name": "maximizeBtn", + "fill": "#28C840", + "width": 12, + "height": 12 + } + ] + } + ] + }, + { + "type": "frame", + "id": "oHDfa", + "name": "Content", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "ZTOSJ", + "name": "Panel: Sidebar", + "clip": true, + "width": 250, + "height": "fill_container", + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": 0, + "fill": "$--sidebar-border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "4wMva", + "name": "Filters", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 1, + "padding": [ + 8, + 8, + 16, + 8 + ], + "children": [ + { + "type": "frame", + "id": "Q78hg", + "name": "filterAll", + "width": "fill_container", + "fill": "#4a9eff18", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "5ip58", + "name": "leftAll", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "PXpWi", + "name": "iconAll", + "width": 18, + "height": 18, + "iconFontName": "tray-fill", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--primary" + }, + { + "type": "text", + "id": "NWprl", + "name": "fAllTxt", + "fill": "$--primary", + "content": "Untagged", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "NZ9Dv", + "name": "countAll", + "height": 20, + "fill": "$--primary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mPlUV", + "name": "badgeAllTxt", + "fill": "$--primary-foreground", + "content": "24", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "YLWsY", + "name": "filterUntagged", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "k7LIr", + "name": "leftUntagged", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "FMVlh", + "name": "untaggedIcon", + "width": 18, + "height": 18, + "iconFontName": "cardholder", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "5XllD", + "name": "untaggedTxt", + "fill": "$--foreground", + "content": "All Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "aSpGv", + "name": "countUntagged", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qZUFt", + "name": "untaggedBadgeTxt", + "fill": "$--muted-foreground", + "content": "6", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Jahon", + "name": "filterTrash", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "IOiBI", + "name": "leftTrash", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "X6F74", + "name": "trashIcon", + "width": 18, + "height": 18, + "iconFontName": "trash", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "vRMH9", + "name": "trashTxt", + "fill": "$--foreground", + "content": "Archive", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "JMpkq", + "name": "countTrash", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "0cKm4", + "name": "trashBadgeTxt", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "6RMbq", + "name": "filterChanges", + "width": "fill_container", + "cornerRadius": 6, + "gap": 6, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "DpdBu", + "name": "leftChanges", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "VTusA", + "name": "iconChanges", + "width": 18, + "height": 18, + "iconFontName": "git-branch", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "qvq5O", + "name": "fChangesTxt", + "fill": "$--foreground", + "content": "Changes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "qAMES", + "name": "countChanges", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "nkzVk", + "name": "badgeChangesTxt", + "fill": "$--muted-foreground", + "content": "3", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "SSEUP", + "name": "Sections", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "padding": [ + 8, + 0 + ], + "children": [ + { + "type": "frame", + "id": "nM2Cn", + "name": "projGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6, + 8, + 6 + ], + "children": [ + { + "type": "frame", + "id": "ouIl9", + "name": "projHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Rx1Cp", + "name": "leftProj", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "t5WJJ", + "name": "projIcon", + "width": 18, + "height": 18, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-red" + }, + { + "type": "text", + "id": "S3sGC", + "name": "projLabel", + "fill": "$--foreground", + "content": "Projects", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "FKwSS", + "name": "projChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "IHzFh", + "name": "projItem1", + "width": "fill_container", + "fill": "$--accent-red-light", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "iiFcl", + "name": "proj1Txt", + "fill": "$--accent-red", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "LdaU9", + "name": "projItem2", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XRll4", + "name": "proj2Txt", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Vmykd", + "name": "projItem3", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "0PqQQ", + "name": "proj3Txt", + "fill": "$--muted-foreground", + "content": "AI Habit Tracker", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "WAQtn", + "name": "respGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "aSi3l", + "name": "respHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "lov6B", + "name": "leftResp", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Em4ns", + "name": "respIcon", + "width": 18, + "height": 18, + "iconFontName": "medal", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "WPRCo", + "name": "respLabel", + "fill": "$--foreground", + "content": "Responsibilities", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "6hDdQ", + "name": "respChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "FmUu0", + "name": "procGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "Dat6o", + "name": "respHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "iJlgx", + "name": "leftResp", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "I2J1R", + "name": "respIcon", + "width": 18, + "height": 18, + "iconFontName": "arrows-clockwise", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "AToF0", + "name": "Procedures", + "fill": "$--foreground", + "content": "Procedures", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "meEIL", + "name": "procChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "T4NG2", + "name": "peopleGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "Q01iL", + "name": "peopleHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "eE6Ca", + "name": "leftPeople", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "jOmVa", + "name": "peopleIcon", + "width": 18, + "height": 18, + "iconFontName": "users", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "caelD", + "name": "peopleLbl", + "fill": "$--foreground", + "content": "People", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "TkuS1", + "name": "peopleChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "BY1Qx", + "name": "eventsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "v28d8", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "TPyOc", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "ELRMi", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "calendar-blank", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "OlI6Q", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Events", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "TZwK8", + "name": "eventsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "trKNW", + "name": "topicsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "qdbZ3", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "JP83M", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Q57kF", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "DeVxP", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Topics", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "nzAcm", + "name": "topicsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ZWXuk", + "name": "topicsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "CQaLg", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "2SMcN", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "0N8UH", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "6XB2H", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "NmBSB", + "name": "topicsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "2cQBp", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "0f10v", + "name": "leftEvents", + "gap": 8, + "padding": [ + 8, + 0 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "xBv1X", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "viWPL", + "name": "eventsLbl", + "fill": "$--muted-foreground", + "content": "Create new type", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "8lLHp", + "name": "Commit Area", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "padding": 12, + "children": [ + { + "type": "frame", + "id": "YYjuy", + "name": "commitBtn", + "width": "fill_container", + "fill": "$--primary", + "cornerRadius": 6, + "gap": 6, + "padding": [ + 8, + 16 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "YF44G", + "name": "commitIcon", + "width": 14, + "height": 14, + "iconFontName": "git-commit-horizontal", + "iconFontFamily": "lucide", + "fill": "$--primary-foreground" + }, + { + "type": "text", + "id": "vpzJm", + "name": "commitTxt", + "fill": "$--primary-foreground", + "content": "Commit & Push", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "vhU5z", + "name": "commitBadge", + "height": 18, + "fill": "#ffffff40", + "cornerRadius": 9, + "padding": [ + 0, + 5 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "sdvYT", + "name": "commitBadgeTxt", + "fill": "$--white", + "content": "3", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "IIvWr", + "name": "Resize Handle", + "fill": "$--border", + "width": 1, + "height": "fill_container" + }, + { + "type": "frame", + "id": "zFIJv", + "name": "Panel: NoteList", + "clip": true, + "width": 300, + "height": "fill_container", + "fill": "$--card", + "stroke": { + "align": "inside", + "thickness": 0, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "uZJVN", + "name": "NoteList Header", + "width": "fill_container", + "height": 45, + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 14, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "hmbdb", + "name": "nlTitle", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "m6AeW", + "name": "nlActions", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "8gMXE", + "name": "searchIcon", + "width": 16, + "height": 16, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "YqvSN", + "name": "plusIcon", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "0aJUm", + "name": "Note Items", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "y9y05", + "name": "Backlinks Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "tMsM4", + "name": "Note Item Selected", + "width": "fill_container", + "fill": "$--accent-red-light", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#E9E9E7" + }, + "children": [ + { + "type": "rectangle", + "id": "npYuc", + "name": "leftAccent", + "fill": "$--accent-red", + "width": 3, + "height": "fill_container" + }, + { + "type": "frame", + "id": "a33Wx", + "name": "noteSelContent", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "AZkyx", + "name": "noteSelRow", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "j7GDJ", + "name": "titleGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "j7o2Q", + "name": "noteSelTitle", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "icon_font", + "id": "BnkTG", + "name": "ico", + "width": 14, + "height": 14, + "iconFontName": "wrench", + "iconFontFamily": "lucide", + "fill": "$--accent-red" + } + ] + } + ] + }, + { + "type": "text", + "id": "Ac5Ds", + "name": "noteSelSnip", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Personal knowledge and life management desktop app...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "9ASsT", + "name": "noteSelTime", + "fill": "$--accent-red", + "content": "2m ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "7G8pN", + "name": "Related Notes Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "fDxtF", + "name": "Related Notes Header", + "width": "fill_container", + "height": 32, + "fill": "$--muted", + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "UBRdO", + "name": "rnLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "8Z4Wq", + "name": "rnLabel", + "fill": "$--muted-foreground", + "content": "RELATED NOTES", + "fontFamily": "IBM Plex Mono", + "fontSize": 11 + }, + { + "type": "text", + "id": "vpnZr", + "name": "rnCount", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "X9LIR", + "name": "rnRight", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "fzVWu", + "name": "rnFilter", + "width": 12, + "height": 12, + "iconFontName": "funnel", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "wuqsO", + "name": "rnPlus", + "width": 12, + "height": 12, + "iconFontName": "plus", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "SbjLq", + "name": "rnChev", + "width": 12, + "height": 12, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "AsAKG", + "name": "Note Item", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "QX5A1", + "name": "note2Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "rYdta", + "name": "leafGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PC9XN", + "name": "note2Title", + "fill": "$--foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "oI64Y", + "name": "leafIco", + "width": 14, + "height": 14, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + } + ] + }, + { + "type": "text", + "id": "S9JPj", + "name": "note2Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Redesigning portfolio site with modern stack and updated visual language...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "lU75x", + "name": "note2Time", + "fill": "$--muted-foreground", + "content": "1h ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "u6E6Z", + "name": "Note Item", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "ZjCz3", + "name": "note2Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mxCzu", + "name": "leafGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "INenY", + "name": "note2Title", + "fill": "$--foreground", + "content": "Sample note 2", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "gz6qS", + "name": "leafIco", + "width": 14, + "height": 14, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + } + ] + }, + { + "type": "text", + "id": "s141z", + "name": "note2Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Lorem ipsum dolor sit amet consequetur with modern stack and updated language...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "SvGnt", + "name": "note2Time", + "fill": "$--muted-foreground", + "content": "1h ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "1GwHB", + "name": "Events Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "fp4xI", + "name": "Events Header", + "width": "fill_container", + "height": 32, + "fill": "$--muted", + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "MNNuU", + "name": "evLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BNxZp", + "name": "evLabel", + "fill": "$--muted-foreground", + "content": "EVENTS", + "fontFamily": "IBM Plex Mono", + "fontSize": 11 + }, + { + "type": "text", + "id": "qcEvw", + "name": "evCount", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "IBM Plex Sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "WCyfc", + "name": "evRight", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "SpCoO", + "name": "evFilter", + "width": 12, + "height": 12, + "iconFontName": "funnel", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "G1DCl", + "name": "evPlus", + "width": 12, + "height": 12, + "iconFontName": "plus", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "sAalN", + "name": "evChev", + "width": 12, + "height": 12, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "k7CjA", + "name": "Note Item 2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "og5Gz", + "name": "note3Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ttBdk", + "name": "calGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "KI8Bf", + "name": "note3Title", + "fill": "$--foreground", + "content": "Meeting with Grant", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "LdRjU", + "name": "calIco", + "width": 14, + "height": 14, + "iconFontName": "calendar", + "iconFontFamily": "lucide", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "FfPXa", + "name": "note3Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Discussed Q1 goals and hiring priorities with the engineering team leads...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "BHh4Z", + "name": "note3Time", + "fill": "$--muted-foreground", + "content": "Yesterday", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "AQ51u", + "name": "Note Item 2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "SHgK3", + "name": "note3Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "3rIet", + "name": "calGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dCwzu", + "name": "note3Title", + "fill": "$--foreground", + "content": "Meeting with Matteo", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "l8O2I", + "name": "calIco", + "width": 14, + "height": 14, + "iconFontName": "calendar", + "iconFontFamily": "lucide", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "1L4oo", + "name": "note3Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Discussed Q1 goals and hiring priorities with the engineering team leads...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "3oBam", + "name": "note3Time", + "fill": "$--muted-foreground", + "content": "Yesterday", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "MeqjO", + "name": "Resize Handle 2", + "fill": "$--border", + "width": 1, + "height": "fill_container" + }, + { + "type": "frame", + "id": "zAbPt", + "name": "Panel: Editor", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "NdYcO", + "name": "Tab Bar", + "width": "fill_container", + "height": 45, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "nVUGr", + "name": "Tab Active", + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kBpGX", + "name": "tab1Txt", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "Bb2AE", + "name": "tab1Close", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "GYeyL", + "name": "Tab Inactive", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kZ1bn", + "name": "tab2Txt", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "RwyYI", + "name": "tab2Close", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "sbpmq", + "name": "tabSpacer", + "width": "fill_container", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + } + }, + { + "type": "frame", + "id": "WF97k", + "name": "tabControls", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1, + "left": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_around", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "rfSoS", + "name": "iconPlus", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "t0u6z", + "name": "iconSplit", + "width": 16, + "height": 16, + "iconFontName": "columns", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "kbe1P", + "name": "iconExpand", + "width": 16, + "height": 16, + "iconFontName": "arrows-out-simple", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "rTdKS", + "name": "Editor Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "NfyTF", + "name": "Editor Left", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "hvoFh", + "name": "Info Bar", + "width": "fill_container", + "height": 45, + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "bTP0I", + "name": "infoLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Msj7v", + "name": "breadcrumbType", + "fill": "$--muted-foreground", + "content": "Project", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wqdMG", + "name": "breadcrumbSep", + "fill": "$--muted-foreground", + "content": "›", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "FzENd", + "name": "breadcrumbName", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "pvTmc", + "name": "dotSep", + "fill": "$--muted-foreground", + "content": "·", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "9ga1R", + "name": "modifiedTxt", + "fill": "$--accent-yellow", + "content": "M", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "3bzDp", + "name": "infoRight", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "9Ctb0", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "csHzz", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "git-branch", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "2s6Of", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "cursor-text", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "JVSlj", + "name": "iconAI", + "width": 16, + "height": 16, + "iconFontName": "sparkle", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "k3AiV", + "name": "iconMore", + "width": 16, + "height": 16, + "iconFontName": "dots-three", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "vvETz", + "name": "Editor Content", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": [ + 32, + 64 + ], + "children": [ + { + "type": "text", + "id": "b6up3", + "name": "editorP1", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Personal knowledge and life management desktop app, built with Tauri v2 + React + TypeScript + CodeMirror 6. It reads a vault of markdown files with YAML frontmatter and presents them in a four-panel UI inspired by Bear Notes.", + "lineHeight": 1.6, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "a2S43", + "name": "editorH2", + "fill": "$--foreground", + "content": "Architecture", + "lineHeight": 1.3, + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "600" + }, + { + "type": "text", + "id": "wgvQa", + "name": "editorP2", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "The app uses a four-panel layout: Sidebar for navigation, NoteList for browsing, Editor for content, and Inspector for metadata. All data lives in markdown files with YAML frontmatter, git-versioned.", + "lineHeight": 1.6, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "GGP97", + "name": "Inspector", + "clip": true, + "width": 260, + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "left": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "FbHy7", + "name": "Inspector Header", + "width": "fill_container", + "height": 45, + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "gap": 8, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "sA7Dx", + "name": "leftGroup", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "0ccF1", + "name": "propIcon", + "width": 16, + "height": 16, + "iconFontName": "sliders-horizontal", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "T2vTZ", + "name": "inspTitle", + "fill": "$--muted-foreground", + "content": "Properties", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "icon_font", + "id": "NQOhZ", + "name": "sidebarIcon", + "width": 16, + "height": 16, + "iconFontName": "x", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "LYFki", + "name": "Inspector Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": 12, + "children": [ + { + "type": "frame", + "id": "8g61y", + "name": "Properties Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "VhH4P", + "name": "addPropBtn", + "width": "fill_container", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 6, + 12 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Q9z8F", + "name": "addPropTxt", + "fill": "$--muted-foreground", + "content": "+ Add property", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "6PwTH", + "name": "propType", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Vt2BC", + "name": "propTypeLbl", + "fill": "$--muted-foreground", + "content": "TYPE", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uvBiY", + "name": "propTypeVal", + "fill": "$--foreground", + "content": "Project", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "FDbay", + "name": "propStatus", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "e426F", + "name": "propStatusLbl", + "fill": "$--muted-foreground", + "content": "STATUS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "o2rZM", + "name": "propStatusBadge", + "fill": "$--accent-green-light", + "cornerRadius": 16, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "transparent" + }, + "gap": 8, + "padding": [ + 1, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "QM7L3", + "name": "Badge Text", + "fill": "$--accent-green", + "content": "ACTIVE", + "lineHeight": 1.33, + "textAlign": "center", + "textAlignVertical": "middle", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 1.2 + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "NmRLv", + "name": "Relationships Section", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "WFVAr", + "name": "relGroup1", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "0N729", + "name": "relGrp1Title", + "fill": "$--muted-foreground", + "content": "BELONGS TO", + "fontFamily": "IBM Plex Mono", + "fontSize": 10 + }, + { + "type": "frame", + "id": "zLl8F", + "name": "relLaputaBtn", + "width": "fill_container", + "fill": "$--accent-green-light", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kw9vC", + "name": "laputaTxt", + "fill": "$--accent-green", + "content": "Laputa", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "RFvoz", + "name": "topicIcon", + "opacity": 0.5, + "width": 14, + "height": 14, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + }, + { + "type": "frame", + "id": "ybi8Q", + "name": "relLaputaBtn", + "width": "fill_container", + "fill": "$--accent-green-light", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VNcmt", + "name": "laputaTxt", + "fill": "$--accent-green", + "content": "Building", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "UVPwc", + "name": "topicIcon", + "opacity": 0.5, + "width": 14, + "height": 14, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + }, + { + "type": "frame", + "id": "L6knW", + "name": "linkExisting", + "width": "fill_container", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jwU3Z", + "name": "laputaTxt", + "fill": "$--muted-foreground", + "content": "+ Link existing", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "laPFL", + "name": "relGroup2", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "LOXUL", + "name": "relGrp2Title", + "fill": "$--muted-foreground", + "content": "RELATED TO", + "fontFamily": "IBM Plex Mono", + "fontSize": 10 + }, + { + "type": "frame", + "id": "qLTYD", + "name": "relMigrationBtn", + "width": "fill_container", + "fill": "$--accent-red-light", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "GBk2u", + "name": "migrationTxt", + "fill": "$--accent-red", + "content": "Shadcn Migration", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "Sy14T", + "name": "expIcon", + "opacity": 0.5, + "width": 14, + "height": 14, + "iconFontName": "flask", + "iconFontFamily": "phosphor", + "fill": "$--accent-red" + } + ] + }, + { + "type": "frame", + "id": "u8ijV", + "name": "linkExisting", + "width": "fill_container", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PUXx5", + "name": "laputaTxt", + "fill": "$--muted-foreground", + "content": "+ Link existing", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "6PUnO", + "name": "Backlinks Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "PllIu", + "name": "blTitle", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ZbgbI", + "name": "blTitleTxt", + "rotation": -0.5285936385085725, + "fill": "$--muted-foreground", + "content": "BACKLINKS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10 + } + ] + }, + { + "type": "text", + "id": "eRoDO", + "name": "blItem1", + "fill": "$--primary", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "u1olR", + "name": "blItem2", + "fill": "$--primary", + "content": "Meeting with Grant", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "oUd9s", + "name": "blItem3", + "fill": "$--primary", + "content": "Q1 Planning", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "yf0wj", + "name": "History Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "dG7rj", + "name": "histTitle", + "fill": "$--muted-foreground", + "content": "HISTORY", + "fontFamily": "IBM Plex Mono", + "fontSize": 10 + }, + { + "type": "frame", + "id": "z2Mdy", + "name": "histItem1", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "left": 2 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 0, + 0, + 10 + ], + "children": [ + { + "type": "text", + "id": "AsiWz", + "name": "histItem1Hash", + "fill": "$--accent-blue", + "content": "a089f44 · feat: migrate to shadcn/ui", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "BdVhi", + "name": "histItem1Date", + "fill": "$--muted-foreground", + "content": "Feb 16, 2026", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "piJNC", + "name": "histItem2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "left": 2 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 0, + 0, + 10 + ], + "children": [ + { + "type": "text", + "id": "0PWoX", + "name": "histItem2Hash", + "fill": "$--accent-blue", + "content": "5a4b4ac · feat: emoji favicon", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xySNW", + "name": "histItem2Date", + "fill": "$--muted-foreground", + "content": "Feb 15, 2026", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "x1AHT", + "name": "lightStatusBar", + "width": "fill_container", + "height": 30, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "padding": [ + 0, + 8 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "e2UzJ", + "name": "Status Left", + "height": "fill_container", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "mlD58", + "name": "versionIcon", + "width": 14, + "height": 14, + "iconFontName": "box", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "hRBRa", + "name": "versionText", + "fill": "$--muted-foreground", + "content": "v0.4.2", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "bFwrw", + "name": "sep1", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "ey3Gr", + "name": "branchIcon", + "width": 14, + "height": 14, + "iconFontName": "git-branch", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "Kz0sS", + "name": "branchText", + "fill": "$--muted-foreground", + "content": "main", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "6IY1E", + "name": "sep2", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "IjEG1", + "name": "syncIcon", + "width": 13, + "height": 13, + "iconFontName": "refresh-cw", + "iconFontFamily": "lucide", + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "srxkr", + "name": "syncText", + "fill": "$--muted-foreground", + "content": "Synced 2m ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "4jgQF", + "name": "Status Right", + "height": "fill_container", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "3RPmA", + "name": "aiIcon", + "width": 13, + "height": 13, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "t0NOA", + "name": "aiText", + "fill": "$--muted-foreground", + "content": "Claude Sonnet 4", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "16V7i", + "name": "sep3", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "PXVrd", + "name": "notesCount", + "width": 13, + "height": 13, + "iconFontName": "file-text", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "2FxCi", + "name": "notesText", + "fill": "$--muted-foreground", + "content": "1,247 notes", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pB6ds", + "name": "sep4", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "fRHKs", + "name": "bellIcon", + "width": 13, + "height": 13, + "iconFontName": "bell", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "2sEBS", + "name": "settingsIcon", + "width": 13, + "height": 13, + "iconFontName": "settings", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "mOf4J", + "x": 0, + "y": 4457, + "name": "Color Palette — shadcn/ui Theme (Light)", + "theme": { + "Mode": "Light" + }, + "width": 1440, + "fill": "$--background", + "layout": "vertical", + "gap": 32, + "padding": 40, + "children": [ + { + "type": "text", + "id": "rmJdn", + "name": "cTitle", + "fill": "$--foreground", + "content": "Color Palette — shadcn/ui Theme Variables (Light)", + "fontFamily": "Inter", + "fontSize": 28, + "fontWeight": "700", + "letterSpacing": -0.5 + }, + { + "type": "text", + "id": "V79Zu", + "name": "cSub", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "All colors from src/index.css. Variables support light/dark mode via .dark class.", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Xbdzq", + "name": "primLbl", + "fill": "$--foreground", + "content": "Primary Colors", + "fontFamily": "Inter", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "NyoC7", + "name": "primRow", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "Inb1x", + "name": "sw1", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "HRsVC", + "name": "sw1b", + "fill": "$--background", + "width": "fill_container", + "height": 60, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + } + }, + { + "type": "text", + "id": "jV89s", + "name": "sw1n", + "fill": "$--foreground", + "content": "--background", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "qOUUY", + "name": "sw1v", + "fill": "$--muted-foreground", + "content": "L: #FFFFFF / D: #0f0f1a", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "QytAQ", + "name": "sw2", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "TMuHH", + "name": "sw2b", + "fill": "$--foreground", + "width": "fill_container", + "height": 60 + }, + { + "type": "text", + "id": "k9qK4", + "name": "sw2n", + "fill": "$--foreground", + "content": "--foreground", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "BfEn7", + "name": "sw2v", + "fill": "$--muted-foreground", + "content": "L: #37352F / D: #e0e0e0", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "gJZtB", + "name": "sw3", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "RRsQg", + "name": "sw3b", + "fill": "$--primary", + "width": "fill_container", + "height": 60 + }, + { + "type": "text", + "id": "CtJe4", + "name": "sw3n", + "fill": "$--foreground", + "content": "--primary", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "1iFr6", + "name": "sw3v", + "fill": "$--muted-foreground", + "content": "#155DFF", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "mbvua", + "name": "sw4", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "fajNZ", + "name": "sw4b", + "fill": "$--primary-foreground", + "width": "fill_container", + "height": 60, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + } + }, + { + "type": "text", + "id": "wVJ7z", + "name": "sw4n", + "fill": "$--foreground", + "content": "--primary-foreground", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "Oum5Q", + "name": "sw4v", + "fill": "$--muted-foreground", + "content": "L: #FFFFFF / D: #ffffff", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "text", + "id": "UvI4d", + "name": "accLbl", + "fill": "$--foreground", + "content": "Accent & Semantic Colors", + "fontFamily": "Inter", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "jOmo7", + "name": "accRow", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "uFXJ3", + "name": "a1", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "NRr0w", + "name": "a1b", + "fill": "$--accent-blue", + "width": "fill_container", + "height": 60 + }, + { + "type": "text", + "id": "ECVqk", + "name": "a1n", + "fill": "$--foreground", + "content": "--accent-blue", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "csKEt", + "name": "a2", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "5IiT2", + "name": "a2b", + "fill": "$--accent-green", + "width": "fill_container", + "height": 60 + }, + { + "type": "text", + "id": "aCPba", + "name": "a2n", + "fill": "$--foreground", + "content": "--accent-green", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "XbY7C", + "name": "a3", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "I9qCz", + "name": "a3b", + "fill": "$--accent-yellow", + "width": "fill_container", + "height": 60 + }, + { + "type": "text", + "id": "DfZVh", + "name": "a3n", + "fill": "$--foreground", + "content": "--accent-yellow", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "nROw3", + "name": "a4", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "IcIoP", + "name": "a4b", + "fill": "$--accent-red", + "width": "fill_container", + "height": 60 + }, + { + "type": "text", + "id": "eGgz6", + "name": "a4n", + "fill": "$--foreground", + "content": "--destructive", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "J95fv", + "name": "a5", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "o03fQ", + "name": "a5b", + "fill": "$--accent-purple", + "width": "fill_container", + "height": 60 + }, + { + "type": "text", + "id": "H9VWy", + "name": "a5n", + "fill": "$--foreground", + "content": "--accent-purple", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "text", + "id": "GcYle", + "name": "lightLightLbl", + "fill": "$--foreground", + "content": "Accent Light Backgrounds", + "fontFamily": "Inter", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "Ju2xp", + "name": "accLightRow", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "5dnn5", + "name": "al1", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "7kcqW", + "name": "ll1b", + "fill": "$--accent-blue-light", + "width": "fill_container", + "height": 60, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + } + }, + { + "type": "text", + "id": "dBCbF", + "name": "ll1n", + "fill": "$--foreground", + "content": "--accent-blue-light", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "W0t0j", + "name": "al2", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "1vepp", + "name": "ll2b", + "fill": "$--accent-green-light", + "width": "fill_container", + "height": 60, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + } + }, + { + "type": "text", + "id": "6kUyf", + "name": "ll2n", + "fill": "$--foreground", + "content": "--accent-green-light", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "zyV1j", + "name": "al3", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "yUDBN", + "name": "ll3b", + "fill": "$--accent-yellow-light", + "width": "fill_container", + "height": 60, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + } + }, + { + "type": "text", + "id": "dmfZ4", + "name": "ll3n", + "fill": "$--foreground", + "content": "--accent-yellow-light", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "yGmmo", + "name": "al4", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "eLu8o", + "name": "ll4b", + "fill": "$--accent-red-light", + "width": "fill_container", + "height": 60, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + } + }, + { + "type": "text", + "id": "PVLwR", + "name": "ll4n", + "fill": "$--foreground", + "content": "--accent-red-light", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "0BO2b", + "name": "al5", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "6FBws", + "name": "ll5b", + "fill": "$--accent-purple-light", + "width": "fill_container", + "height": 60, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + } + }, + { + "type": "text", + "id": "i2DfV", + "name": "ll5n", + "fill": "$--foreground", + "content": "--accent-purple-light", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "text", + "id": "JFocf", + "name": "surfLbl", + "fill": "$--foreground", + "content": "Surface & Border Colors", + "fontFamily": "Inter", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "YAJLW", + "name": "surfRow", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "JHS3j", + "name": "s1", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "9Bj41", + "name": "s1b", + "fill": "$--card", + "width": "fill_container", + "height": 60, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + } + }, + { + "type": "text", + "id": "wn4ua", + "name": "s1n", + "fill": "$--foreground", + "content": "--card", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Fyvfd", + "name": "s2", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "UFLw5", + "name": "s2b", + "fill": "$--sidebar", + "width": "fill_container", + "height": 60, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--sidebar-border" + } + }, + { + "type": "text", + "id": "gngfL", + "name": "s2n", + "fill": "$--foreground", + "content": "--sidebar", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "zbyEJ", + "name": "s3", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "WnmQD", + "name": "s3b", + "fill": "$--secondary", + "width": "fill_container", + "height": 60 + }, + { + "type": "text", + "id": "wnkxR", + "name": "s3n", + "fill": "$--foreground", + "content": "--secondary", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Z2FuK", + "name": "s4", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "lNPnc", + "name": "s4b", + "fill": "$--border", + "width": "fill_container", + "height": 60 + }, + { + "type": "text", + "id": "x9aeh", + "name": "s4n", + "fill": "$--foreground", + "content": "--border / --input", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "TDDQ0", + "name": "s5", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "MwHOe", + "name": "s5b", + "fill": "$--muted-foreground", + "width": "fill_container", + "height": 60 + }, + { + "type": "text", + "id": "dHZj5", + "name": "s5n", + "fill": "$--foreground", + "content": "--muted-foreground", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "HZonq", + "x": 0, + "y": 5252, + "name": "Typography & Spacing Specs (Light)", + "theme": { + "Mode": "Light" + }, + "width": 1440, + "fill": "$--background", + "layout": "vertical", + "gap": 32, + "padding": 40, + "children": [ + { + "type": "text", + "id": "nGLlU", + "name": "tTitle", + "fill": "$--foreground", + "content": "Typography Scale (Light)", + "fontFamily": "Inter", + "fontSize": 28, + "fontWeight": "700", + "letterSpacing": -0.5 + }, + { + "type": "text", + "id": "gkQqO", + "name": "tSub", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Fonts: Inter, IBM Plex Mono. Base: 14px. System fallback: -apple-system, BlinkMacSystemFont, sans-serif.", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "QL9fK", + "name": "t1", + "width": "fill_container", + "gap": 24, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "px1l7", + "name": "t1s", + "fill": "$--foreground", + "content": "Heading 1", + "lineHeight": 1.2, + "fontFamily": "Inter", + "fontSize": 32, + "fontWeight": "700" + }, + { + "type": "text", + "id": "3UWKu", + "name": "t1d", + "fill": "$--muted-foreground", + "content": "32px / Bold / lh 1.2 — Editor H1", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Yjhit", + "name": "t2", + "width": "fill_container", + "gap": 24, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "iFzl0", + "name": "t2s", + "fill": "$--foreground", + "content": "Heading 2", + "lineHeight": 1.3, + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "600" + }, + { + "type": "text", + "id": "cF55I", + "name": "t2d", + "fill": "$--muted-foreground", + "content": "24px / Semibold / lh 1.3 — Editor H2", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "bBHMa", + "name": "t3", + "width": "fill_container", + "gap": 24, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kpSyr", + "name": "t3s", + "fill": "$--foreground", + "content": "App Title", + "fontFamily": "Inter", + "fontSize": 17, + "fontWeight": "700", + "letterSpacing": -0.3 + }, + { + "type": "text", + "id": "xUuNz", + "name": "t3d", + "fill": "$--muted-foreground", + "content": "17px / Bold / ls -0.3 — Sidebar header", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Lsh7F", + "name": "t4", + "width": "fill_container", + "gap": 24, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vTRSs", + "name": "t4s", + "fill": "$--foreground", + "content": "Body Text", + "lineHeight": 1.6, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fKa3p", + "name": "t4d", + "fill": "$--muted-foreground", + "content": "16px / Regular / lh 1.6 — Editor paragraphs", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "rpOTz", + "name": "t5", + "width": "fill_container", + "gap": 24, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VXY3Z", + "name": "t5s", + "fill": "$--foreground", + "content": "UI Label / Button", + "lineHeight": 1.43, + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "F6GQz", + "name": "t5d", + "fill": "$--muted-foreground", + "content": "14px / Medium / lh 1.43 — Buttons, NoteList header, Inspector labels", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "H6Rgt", + "name": "t6", + "width": "fill_container", + "gap": 24, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xWvbj", + "name": "t6s", + "fill": "$--foreground", + "content": "Sidebar Item", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "bvBBm", + "name": "t6d", + "fill": "$--muted-foreground", + "content": "13px / Medium — Sidebar nav items, filter items, search", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "GrFAq", + "name": "t7", + "width": "fill_container", + "gap": 24, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vY8j4", + "name": "t7s", + "fill": "$--muted-foreground", + "content": "SECTION HEADER", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600", + "letterSpacing": 0.5 + }, + { + "type": "text", + "id": "okws6", + "name": "t7d", + "fill": "$--muted-foreground", + "content": "11px / Semibold / ls 0.5 — Sidebar sections, type pills", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "awxls", + "name": "t8", + "width": "fill_container", + "gap": 24, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mD3f5", + "name": "t8s", + "fill": "$--foreground", + "content": "ALL-CAPS LABEL", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "600", + "letterSpacing": 1.2 + }, + { + "type": "text", + "id": "jFpLw", + "name": "t8d", + "fill": "$--muted-foreground", + "content": "11px / Semibold / ls 1.2 / IBM Plex Mono — Section labels, metadata tags", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ToLzL", + "name": "t9", + "width": "fill_container", + "gap": 24, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LEsxW", + "name": "t9s", + "fill": "$--muted-foreground", + "content": "OVERLINE TEXT", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 1.5 + }, + { + "type": "text", + "id": "HQ3Df", + "name": "t9d", + "fill": "$--muted-foreground", + "content": "10px / Medium / ls 1.5 / IBM Plex Mono — Overlines, category labels, timestamps", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "3tiJ7", + "name": "div1", + "fill": "$--border", + "width": "fill_container", + "height": 1 + }, + { + "type": "text", + "id": "b2Mt9", + "name": "spTitle", + "fill": "$--foreground", + "content": "Spacing & Layout Reference", + "fontFamily": "Inter", + "fontSize": 28, + "fontWeight": "700", + "letterSpacing": -0.5 + }, + { + "type": "frame", + "id": "o6ETJ", + "name": "pnlRow", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "HhbOn", + "name": "p1", + "width": "fill_container", + "cornerRadius": 8, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 6, + "padding": 16, + "children": [ + { + "type": "text", + "id": "YBekR", + "name": "p1t", + "fill": "$--foreground", + "content": "Sidebar: 250px", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "dT8Xe", + "name": "p1d", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "bg: --sidebar | padding: 8px container, 12px 16px header, 6px 16px items", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "aYkMZ", + "name": "p2", + "width": "fill_container", + "cornerRadius": 8, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 6, + "padding": 16, + "children": [ + { + "type": "text", + "id": "Eycne", + "name": "p2t", + "fill": "$--foreground", + "content": "NoteList: 300px", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "lyTZN", + "name": "p2d", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "bg: --card | padding: 14px 16px header, 8px 12px search/pills, 10px 16px items", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "E6G3g", + "name": "p3", + "width": "fill_container", + "cornerRadius": 8, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 6, + "padding": 16, + "children": [ + { + "type": "text", + "id": "Xy5Gl", + "name": "p3t", + "fill": "$--foreground", + "content": "Editor: flexible", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "y4svr", + "name": "p3d", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "bg: --background | padding: 32px 64px content, 7px 14px tabs, gap: 16px", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "RBPav", + "name": "p4", + "width": "fill_container", + "cornerRadius": 8, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 6, + "padding": 16, + "children": [ + { + "type": "text", + "id": "xKUWC", + "name": "p4t", + "fill": "$--foreground", + "content": "Inspector: 280px", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "u81W5", + "name": "p4d", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "bg: --sidebar | padding: 14px 12px header, 12px body, 16px section gap, 8px prop gap", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "text", + "id": "VvZyF", + "name": "radLbl", + "fill": "$--foreground", + "content": "Border Radius", + "fontFamily": "Inter", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "OwJH7", + "name": "radRow", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "DHrzV", + "name": "r1", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "V35oi", + "name": "r1b", + "fill": "$--secondary", + "width": 80, + "height": 60, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + } + }, + { + "type": "text", + "id": "HVQpq", + "name": "r1n", + "fill": "$--foreground", + "content": "4px (sm) — chips", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "wp350", + "name": "r2", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 6, + "id": "E031z", + "name": "r2b", + "fill": "$--secondary", + "width": 80, + "height": 60, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + } + }, + { + "type": "text", + "id": "rDLff", + "name": "r2n", + "fill": "$--foreground", + "content": "6px (md) — buttons, inputs", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "d6tJt", + "name": "r3", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 8, + "id": "PNkS4", + "name": "r3b", + "fill": "$--secondary", + "width": 80, + "height": 60, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + } + }, + { + "type": "text", + "id": "rZGXb", + "name": "r3n", + "fill": "$--foreground", + "content": "8px (lg) — cards, dialogs", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "UAZ8u", + "name": "r4", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 16, + "id": "wgMVG", + "name": "r4b", + "fill": "$--secondary", + "width": 80, + "height": 60, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + } + }, + { + "type": "text", + "id": "zw3RY", + "name": "r4n", + "fill": "$--foreground", + "content": "16px — badges", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "kIrXE", + "name": "div2", + "fill": "$--border", + "width": "fill_container", + "height": 1 + }, + { + "type": "text", + "id": "FYw8k", + "name": "compTitle", + "fill": "$--foreground", + "content": "shadcn/ui Component Inventory", + "fontFamily": "Inter", + "fontSize": 28, + "fontWeight": "700", + "letterSpacing": -0.5 + }, + { + "type": "frame", + "id": "7Q1Fi", + "name": "compRow", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "XfUwj", + "name": "cg1", + "width": "fill_container", + "fill": "$--card", + "cornerRadius": 8, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 12, + "padding": 16, + "children": [ + { + "type": "text", + "id": "ly7m0", + "name": "cg1t", + "fill": "$--accent-green", + "content": "In Use", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Re89a", + "name": "cg1r1", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Button — 5 variants (Default, Secondary, Outline, Ghost, Destructive)", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ILbjW", + "name": "cg1r2", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Input — Default text input with label group", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "eurqS", + "name": "cg1r3", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Dialog — Modal overlay (CreateNote, Commit)", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "AZyZP", + "name": "cg1r4", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Badge — 4 variants (Default, Secondary, Outline, Destructive)", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "WtehQ", + "name": "cg2", + "width": "fill_container", + "fill": "$--card", + "cornerRadius": 8, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 12, + "padding": 16, + "children": [ + { + "type": "text", + "id": "KaMZ9", + "name": "cg2t", + "fill": "$--accent-yellow", + "content": "Available (Not Yet Used)", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "F8wam", + "name": "cg2r1", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "DropdownMenu — Context / dropdown menus", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Nl1aV", + "name": "cg2r2", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Tabs — Tab navigation component", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "0sBwH", + "name": "cg2r3", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Tooltip, Select, Card, Separator, ScrollArea", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "trSB1", + "x": 1600, + "y": 3385, + "name": "Trash — Sidebar Filter", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 260, + "height": 400, + "fill": "$--sidebar", + "layout": "vertical", + "gap": 0, + "padding": [ + 8, + 6 + ], + "children": [ + { + "type": "frame", + "id": "trFAll", + "name": "filterAll", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "trIAll", + "name": "iconAll", + "width": 16, + "height": 16, + "iconFontName": "file-text", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "trTAll", + "name": "txtAll", + "fill": "$--foreground", + "content": "All Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "trFFav", + "name": "filterFav", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "trIFav", + "name": "iconFav", + "width": 16, + "height": 16, + "iconFontName": "star", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "trTFav", + "name": "txtFav", + "fill": "$--foreground", + "content": "Favorites", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "trFArc", + "name": "filterArchive", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "trIArc", + "name": "iconArchive", + "width": 16, + "height": 16, + "iconFontName": "archive", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "trTArc", + "name": "txtArchive", + "fill": "$--foreground", + "content": "Archive", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "trBArc", + "name": "badgeArc", + "height": 20, + "fill": "$--muted", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "trBTArc", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "trFTr", + "name": "filterTrash", + "width": "fill_container", + "fill": "#ef444418", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "trITr", + "name": "iconTrash", + "width": 16, + "height": 16, + "iconFontName": "trash", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--destructive" + }, + { + "type": "text", + "id": "trTTr", + "name": "txtTrash", + "fill": "$--destructive", + "content": "Trash", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "trBTr", + "name": "badgeTrash", + "height": 20, + "fill": "$--destructive", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "trBTTr", + "fill": "#ffffff", + "content": "3", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "trNL1", + "x": 1600, + "y": 3815, + "name": "Trash — Note List View", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 340, + "height": 360, + "fill": "$--card", + "layout": "vertical", + "gap": 0, + "children": [ + { + "type": "frame", + "id": "trNLH", + "name": "header", + "width": "fill_container", + "height": 45, + "padding": [ + 0, + 16 + ], + "alignItems": "center", + "justifyContent": "space_between", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "trNLT", + "fill": "$--foreground", + "content": "Trash", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "trN1", + "name": "trashedNote1", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "padding": [ + 14, + 16 + ], + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "frame", + "id": "trN1H", + "name": "titleRow", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "trN1T", + "fill": "$--foreground", + "content": "Old Draft Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "trN1B", + "name": "trashedBadge", + "height": 16, + "fill": "#ef444418", + "cornerRadius": 4, + "padding": [ + 1, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "trN1BT", + "fill": "$--destructive", + "content": "TRASHED", + "fontFamily": "Inter", + "fontSize": 9, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "text", + "id": "trN1S", + "fill": "$--muted-foreground", + "content": "Some draft content that is no longer needed...", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "400" + }, + { + "type": "text", + "id": "trN1D", + "fill": "$--muted-foreground", + "content": "5d ago", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "400" + } + ] + }, + { + "type": "frame", + "id": "trN2", + "name": "trashedNote2Warning", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "padding": [ + 14, + 16 + ], + "fill": "#ef44440a", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "frame", + "id": "trN2H", + "name": "titleRow", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "trN2T", + "fill": "$--foreground", + "content": "Deprecated API Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "trN2B", + "name": "warningBadge", + "height": 16, + "fill": "$--destructive", + "cornerRadius": 4, + "padding": [ + 1, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "trN2BT", + "fill": "#ffffff", + "content": "30+ DAYS", + "fontFamily": "Inter", + "fontSize": 9, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "text", + "id": "trN2S", + "fill": "$--muted-foreground", + "content": "Old API documentation that was replaced...", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "400" + }, + { + "type": "text", + "id": "trN2D", + "fill": "$--destructive", + "content": "Trashed 35d ago — will be permanently deleted", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "trRI1", + "x": 1970, + "y": 3385, + "name": "Trash — Relationship Indicator", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 300, + "height": 200, + "fill": "$--background", + "layout": "vertical", + "gap": 8, + "padding": [ + 16, + 16 + ], + "children": [ + { + "type": "text", + "id": "trRIL", + "fill": "$--muted-foreground", + "content": "BELONGS TO", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 0.5 + }, + { + "type": "frame", + "id": "trRN", + "name": "normalRef", + "width": "fill_container", + "fill": "$--accent-blue-light", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "gap": 6, + "alignItems": "center", + "justifyContent": "space_between", + "children": [ + { + "type": "text", + "id": "trRNT", + "fill": "$--accent-blue", + "content": "Build Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "trRNI", + "width": 14, + "height": 14, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-blue" + } + ] + }, + { + "type": "frame", + "id": "trRT", + "name": "trashedRef", + "width": "fill_container", + "fill": "$--muted", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "gap": 6, + "alignItems": "center", + "justifyContent": "space_between", + "opacity": 0.7, + "children": [ + { + "type": "frame", + "id": "trRTL", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "trRTTI", + "width": 12, + "height": 12, + "iconFontName": "trash", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "trRTT", + "fill": "$--muted-foreground", + "content": "Old Draft Notes", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "trRTTX", + "fill": "$--muted-foreground", + "content": "(trashed)", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "400" + } + ] + }, + { + "type": "icon_font", + "id": "trRTI", + "width": 14, + "height": 14, + "iconFontName": "file-text", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "trRA", + "name": "archivedRef", + "width": "fill_container", + "fill": "$--muted", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "gap": 6, + "alignItems": "center", + "justifyContent": "space_between", + "opacity": 0.7, + "children": [ + { + "type": "frame", + "id": "trRAL", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "trRAT", + "fill": "$--muted-foreground", + "content": "Website Redesign", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "trRATX", + "fill": "$--muted-foreground", + "content": "(archived)", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "400" + } + ] + }, + { + "type": "icon_font", + "id": "trRAI", + "width": 14, + "height": 14, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "trW30", + "x": 1970, + "y": 3615, + "name": "Trash — 30-Day Auto-Delete Warning", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 340, + "height": 120, + "fill": "$--background", + "layout": "vertical", + "gap": 8, + "padding": [ + 16, + 16 + ], + "children": [ + { + "type": "text", + "id": "trW3L", + "fill": "$--muted-foreground", + "content": "Warning banner shown at top of trash view:", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "trW3B", + "name": "warningBanner", + "width": "fill_container", + "fill": "#ef44440f", + "cornerRadius": 8, + "padding": [ + 10, + 12 + ], + "gap": 8, + "alignItems": "center", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#ef444430" + }, + "children": [ + { + "type": "icon_font", + "id": "trW3I", + "width": 16, + "height": 16, + "iconFontName": "warning", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--destructive" + }, + { + "type": "frame", + "id": "trW3T", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "trW3T1", + "fill": "$--destructive", + "content": "Notes in trash for 30+ days will be permanently deleted", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "trW3T2", + "fill": "$--muted-foreground", + "content": "1 note is past the 30-day retention period", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "400" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "dt0", + "name": "Draggable Tabs — Default State", + "width": 800, + "height": 120, + "layout": "vertical", + "fill": "$--sidebar", + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "dt1", + "name": "frameLabel", + "content": "Default: tabs are draggable (grab cursor on hover)", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal", + "fill": "$--muted-foreground", + "padding": [ + 8, + 12 + ] + }, + { + "type": "frame", + "id": "dt2", + "name": "Tab Bar Default", + "width": "fill_container", + "height": 45, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dt3", + "name": "Tab Active", + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dt4", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "dt5", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "dt6", + "name": "Tab Inactive", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dt7", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "dt8", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "dt9", + "name": "Tab Inactive 2", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dta", + "fill": "$--muted-foreground", + "content": "Weekly Review", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "dtb", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "dtc", + "name": "Tab Inactive 3", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dtd", + "fill": "$--muted-foreground", + "content": "Workout Log", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "dte", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "dtf", + "name": "tabSpacer", + "width": "fill_container", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + } + }, + { + "type": "frame", + "id": "dtg", + "name": "tabControls", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1, + "left": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_around", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dth", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "text", + "id": "dti", + "name": "cursorNote", + "content": "cursor: grab → grabbing during drag", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal", + "fill": "$--muted-foreground", + "padding": [ + 4, + 12 + ] + } + ] + }, + { + "type": "frame", + "id": "dtj", + "name": "Draggable Tabs — Dragging (Tab Being Moved)", + "width": 800, + "height": 120, + "layout": "vertical", + "fill": "$--sidebar", + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "dtk", + "name": "frameLabel2", + "content": "Dragging: \"Portfolio Rewrite\" is being dragged left (opacity 0.5, blue drop indicator)", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal", + "fill": "$--muted-foreground", + "padding": [ + 8, + 12 + ] + }, + { + "type": "frame", + "id": "dtl", + "name": "Tab Bar Dragging", + "width": "fill_container", + "height": 45, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "id": "dtm", + "name": "Drop Indicator", + "width": 2, + "height": 30, + "fill": "$--primary", + "cornerRadius": 1 + }, + { + "type": "frame", + "id": "dtn", + "name": "Tab Active", + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dto", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "dtp", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "dtq", + "name": "Tab Dragging (ghost)", + "height": "fill_container", + "opacity": 0.5, + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dtr", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "dts", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "dtt", + "name": "Tab Inactive 2", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dtu", + "fill": "$--muted-foreground", + "content": "Weekly Review", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "dtv", + "name": "Tab Inactive 3", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dtw", + "fill": "$--muted-foreground", + "content": "Workout Log", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "dtx", + "name": "tabSpacer", + "width": "fill_container", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + } + }, + { + "type": "frame", + "id": "dty", + "name": "tabControls", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1, + "left": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_around", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dtz", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "text", + "id": "dt10", + "name": "dragNote", + "content": "Drop indicator: 2px $--primary bar shows insertion point. Dragged tab has opacity: 0.5.", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal", + "fill": "$--muted-foreground", + "padding": [ + 4, + 12 + ] + } + ] + }, + { + "type": "frame", + "id": "dt11", + "name": "Draggable Tabs — After Drop (Reordered)", + "width": 800, + "height": 120, + "layout": "vertical", + "fill": "$--sidebar", + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "dt12", + "name": "frameLabel3", + "content": "After drop: \"Portfolio Rewrite\" moved before \"Laputa App\". Order persisted to localStorage.", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal", + "fill": "$--muted-foreground", + "padding": [ + 8, + 12 + ] + }, + { + "type": "frame", + "id": "dt13", + "name": "Tab Bar Reordered", + "width": "fill_container", + "height": 45, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dt14", + "name": "Tab Inactive (moved)", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dt15", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "dt16", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "dt17", + "name": "Tab Active", + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dt18", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "dt19", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "dt1a", + "name": "Tab Inactive 2", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dt1b", + "fill": "$--muted-foreground", + "content": "Weekly Review", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "dt1c", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "dt1d", + "name": "Tab Inactive 3", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dt1e", + "fill": "$--muted-foreground", + "content": "Workout Log", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "dt1f", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "dt1g", + "name": "tabSpacer", + "width": "fill_container", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + } + }, + { + "type": "frame", + "id": "dt1h", + "name": "tabControls", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1, + "left": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_around", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dt1i", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "text", + "id": "dt1j", + "name": "persistNote", + "content": "localStorage key: \"laputa-tab-order\" stores path array. Restored on next session.", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal", + "fill": "$--muted-foreground", + "padding": [ + 4, + 12 + ] + } + ] + }, + { + "type": "frame", + "id": "dsg01a", + "name": "Sidebar — Drag Handles Visible", + "x": 68, + "y": 6400, + "width": 330, + "height": 680, + "fill": "$--background", + "layout": "vertical", + "padding": [ + 16 + ], + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "dsg01b", + "name": "frame1Title", + "content": "Drag handles appear on section headers", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600", + "fill": "$--foreground" + }, + { + "type": "text", + "id": "dsg01c", + "name": "frame1Desc", + "content": "Grip icon shown left of section icon. Cursor changes to grab on hover.", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal", + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "dsg01d", + "height": 12 + }, + { + "type": "frame", + "id": "dsg01e", + "name": "sidebarDefault", + "width": 250, + "height": 600, + "fill": "$--sidebar", + "layout": "vertical", + "clip": true, + "children": [ + { + "type": "frame", + "id": "dsg01f", + "name": "filters", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "padding": [ + 8, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg01g", + "name": "allNotesRow", + "width": "fill_container", + "cornerRadius": 4, + "padding": [ + 6, + 16 + ], + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg01h", + "width": 18, + "height": 18, + "iconFontName": "file-text", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "dsg01i", + "fill": "$--foreground", + "content": "All Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "dsg01j", + "name": "favRow", + "width": "fill_container", + "cornerRadius": 4, + "padding": [ + 6, + 16 + ], + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg01k", + "width": 18, + "height": 18, + "iconFontName": "star", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "dsg01l", + "fill": "$--muted-foreground", + "content": "Favorites", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg01m", + "name": "sectionsWrap", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "dsg00a", + "name": "projGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6, + 8, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg005", + "name": "projHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg001", + "name": "projLeft", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg000", + "name": "projDrag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg002", + "name": "projIcon", + "width": 18, + "height": 18, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-red" + }, + { + "type": "text", + "id": "dsg003", + "name": "projLabel", + "fill": "$--foreground", + "content": "Projects", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg004", + "name": "projChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "dsg006", + "name": "projItem0", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "fill": "$--accent-red-light", + "children": [ + { + "type": "text", + "id": "dsg007", + "name": "projItemTxt0", + "fill": "$--accent-red", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "dsg008", + "name": "projItem1", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dsg009", + "name": "projItemTxt1", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg00h", + "name": "respGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg00g", + "name": "respHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg00c", + "name": "respLeft", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg00b", + "name": "respDrag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg00d", + "name": "respIcon", + "width": 18, + "height": 18, + "iconFontName": "medal", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "dsg00e", + "name": "respLabel", + "fill": "$--foreground", + "content": "Responsibilities", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg00f", + "name": "respChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg00o", + "name": "procGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg00n", + "name": "procHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg00j", + "name": "procLeft", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg00i", + "name": "procDrag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg00k", + "name": "procIcon", + "width": 18, + "height": 18, + "iconFontName": "arrows-clockwise", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "dsg00l", + "name": "procLabel", + "fill": "$--foreground", + "content": "Procedures", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg00m", + "name": "procChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg00v", + "name": "peopleGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg00u", + "name": "peopleHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg00q", + "name": "peopleLeft", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg00p", + "name": "peopleDrag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg00r", + "name": "peopleIcon", + "width": 18, + "height": 18, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "dsg00s", + "name": "peopleLabel", + "fill": "$--foreground", + "content": "People", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg00t", + "name": "peopleChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg012", + "name": "eventsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg011", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg00x", + "name": "eventsLeft", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg00w", + "name": "eventsDrag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg00y", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "cardholder", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "dsg00z", + "name": "eventsLabel", + "fill": "$--foreground", + "content": "Events", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg010", + "name": "eventsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg019", + "name": "topicsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg018", + "name": "topicsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg014", + "name": "topicsLeft", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg013", + "name": "topicsDrag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg015", + "name": "topicsIcon", + "width": 18, + "height": 18, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "dsg016", + "name": "topicsLabel", + "fill": "$--foreground", + "content": "Topics", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg017", + "name": "topicsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg02w", + "name": "Sidebar — Section Being Dragged", + "x": 420, + "y": 6400, + "width": 330, + "height": 680, + "fill": "$--background", + "layout": "vertical", + "padding": [ + 16 + ], + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "dsg02x", + "name": "frame2Title", + "content": "Dragging \"People\" above \"Responsibilities\"", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600", + "fill": "$--foreground" + }, + { + "type": "text", + "id": "dsg02y", + "name": "frame2Desc", + "content": "Blue drop indicator line shows insertion point. Dragged section floats with blue border.", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal", + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "dsg02z", + "height": 12 + }, + { + "type": "frame", + "id": "dsg030", + "name": "sidebarDragging", + "width": 250, + "height": 600, + "fill": "$--sidebar", + "layout": "vertical", + "clip": true, + "children": [ + { + "type": "frame", + "id": "dsg031", + "name": "filters", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "padding": [ + 8, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg032", + "name": "allNotesRow", + "width": "fill_container", + "cornerRadius": 4, + "padding": [ + 6, + 16 + ], + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg033", + "width": 18, + "height": 18, + "iconFontName": "file-text", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "dsg034", + "fill": "$--foreground", + "content": "All Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "dsg035", + "name": "favRow", + "width": "fill_container", + "cornerRadius": 4, + "padding": [ + 6, + 16 + ], + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg036", + "width": 18, + "height": 18, + "iconFontName": "star", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "dsg037", + "fill": "$--muted-foreground", + "content": "Favorites", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg038", + "name": "sectionsWrap", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "dsg01x", + "name": "proj2Group", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6, + 8, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg01s", + "name": "proj2Header", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg01o", + "name": "proj2Left", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg01n", + "name": "proj2Drag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg01p", + "name": "proj2Icon", + "width": 18, + "height": 18, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-red" + }, + { + "type": "text", + "id": "dsg01q", + "name": "proj2Label", + "fill": "$--foreground", + "content": "Projects", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg01r", + "name": "proj2Chev", + "width": 14, + "height": 14, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "dsg01t", + "name": "proj2Item0", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "fill": "$--accent-red-light", + "children": [ + { + "type": "text", + "id": "dsg01u", + "name": "proj2ItemTxt0", + "fill": "$--accent-red", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "dsg01v", + "name": "proj2Item1", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dsg01w", + "name": "proj2ItemTxt1", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg01y", + "name": "dropIndicator", + "width": "fill_container", + "height": 2, + "fill": "$--accent-blue", + "cornerRadius": 1 + }, + { + "type": "frame", + "id": "dsg025", + "name": "resp2Group", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg024", + "name": "resp2Header", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg020", + "name": "resp2Left", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg01z", + "name": "resp2Drag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg021", + "name": "resp2Icon", + "width": 18, + "height": 18, + "iconFontName": "medal", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "dsg022", + "name": "resp2Label", + "fill": "$--foreground", + "content": "Responsibilities", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg023", + "name": "resp2Chev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg02c", + "name": "proc2Group", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg02b", + "name": "proc2Header", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg027", + "name": "proc2Left", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg026", + "name": "proc2Drag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg028", + "name": "proc2Icon", + "width": 18, + "height": 18, + "iconFontName": "arrows-clockwise", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "dsg029", + "name": "proc2Label", + "fill": "$--foreground", + "content": "Procedures", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg02a", + "name": "proc2Chev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg02j", + "name": "events2Group", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg02i", + "name": "events2Header", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg02e", + "name": "events2Left", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg02d", + "name": "events2Drag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg02f", + "name": "events2Icon", + "width": 18, + "height": 18, + "iconFontName": "cardholder", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "dsg02g", + "name": "events2Label", + "fill": "$--foreground", + "content": "Events", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg02h", + "name": "events2Chev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg02q", + "name": "topics2Group", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg02p", + "name": "topics2Header", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg02l", + "name": "topics2Left", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg02k", + "name": "topics2Drag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg02m", + "name": "topics2Icon", + "width": 18, + "height": 18, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "dsg02n", + "name": "topics2Label", + "fill": "$--foreground", + "content": "Topics", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg02o", + "name": "topics2Chev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg02r", + "name": "draggedSection", + "x": 8, + "y": 160, + "width": 234, + "fill": "$--sidebar", + "cornerRadius": 6, + "stroke": { + "align": "outside", + "thickness": 1, + "fill": { + "type": "color", + "color": "$--accent-blue" + } + }, + "opacity": 0.9, + "layout": "vertical", + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg02s", + "name": "draggedHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg02t", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--accent-blue", + "opacity": 0.8 + }, + { + "type": "icon_font", + "id": "dsg02u", + "width": 18, + "height": 18, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "dsg02v", + "fill": "$--foreground", + "content": "People", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg04j", + "name": "Sidebar — After Reorder (People Moved Up)", + "x": 772, + "y": 6400, + "width": 330, + "height": 680, + "fill": "$--background", + "layout": "vertical", + "padding": [ + 16 + ], + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "dsg04k", + "name": "frame3Title", + "content": "After drop — People now second section", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600", + "fill": "$--foreground" + }, + { + "type": "text", + "id": "dsg04l", + "name": "frame3Desc", + "content": "Order persisted as \"order\" property in each Type document frontmatter (e.g. order: 2).", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal", + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "dsg04m", + "height": 12 + }, + { + "type": "frame", + "id": "dsg04n", + "name": "sidebarReordered", + "width": 250, + "height": 600, + "fill": "$--sidebar", + "layout": "vertical", + "clip": true, + "children": [ + { + "type": "frame", + "id": "dsg04o", + "name": "filters", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "padding": [ + 8, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg04p", + "name": "allNotesRow", + "width": "fill_container", + "cornerRadius": 4, + "padding": [ + 6, + 16 + ], + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg04q", + "width": 18, + "height": 18, + "iconFontName": "file-text", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "dsg04r", + "fill": "$--foreground", + "content": "All Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "dsg04s", + "name": "favRow", + "width": "fill_container", + "cornerRadius": 4, + "padding": [ + 6, + 16 + ], + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg04t", + "width": 18, + "height": 18, + "iconFontName": "star", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "dsg04u", + "fill": "$--muted-foreground", + "content": "Favorites", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg04v", + "name": "sectionsWrap", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "dsg03j", + "name": "proj3Group", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6, + 8, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg03e", + "name": "proj3Header", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg03a", + "name": "proj3Left", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg039", + "name": "proj3Drag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg03b", + "name": "proj3Icon", + "width": 18, + "height": 18, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-red" + }, + { + "type": "text", + "id": "dsg03c", + "name": "proj3Label", + "fill": "$--foreground", + "content": "Projects", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg03d", + "name": "proj3Chev", + "width": 14, + "height": 14, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "dsg03f", + "name": "proj3Item0", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "fill": "$--accent-red-light", + "children": [ + { + "type": "text", + "id": "dsg03g", + "name": "proj3ItemTxt0", + "fill": "$--accent-red", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "dsg03h", + "name": "proj3Item1", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dsg03i", + "name": "proj3ItemTxt1", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg03q", + "name": "people3Group", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg03p", + "name": "people3Header", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg03l", + "name": "people3Left", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg03k", + "name": "people3Drag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg03m", + "name": "people3Icon", + "width": 18, + "height": 18, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "dsg03n", + "name": "people3Label", + "fill": "$--foreground", + "content": "People", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg03o", + "name": "people3Chev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg03x", + "name": "resp3Group", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg03w", + "name": "resp3Header", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg03s", + "name": "resp3Left", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg03r", + "name": "resp3Drag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg03t", + "name": "resp3Icon", + "width": 18, + "height": 18, + "iconFontName": "medal", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "dsg03u", + "name": "resp3Label", + "fill": "$--foreground", + "content": "Responsibilities", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg03v", + "name": "resp3Chev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg044", + "name": "proc3Group", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg043", + "name": "proc3Header", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg03z", + "name": "proc3Left", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg03y", + "name": "proc3Drag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg040", + "name": "proc3Icon", + "width": 18, + "height": 18, + "iconFontName": "arrows-clockwise", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "dsg041", + "name": "proc3Label", + "fill": "$--foreground", + "content": "Procedures", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg042", + "name": "proc3Chev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg04b", + "name": "events3Group", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg04a", + "name": "events3Header", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg046", + "name": "events3Left", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg045", + "name": "events3Drag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg047", + "name": "events3Icon", + "width": 18, + "height": 18, + "iconFontName": "cardholder", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "dsg048", + "name": "events3Label", + "fill": "$--foreground", + "content": "Events", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg049", + "name": "events3Chev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dsg04i", + "name": "topics3Group", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsg04h", + "name": "topics3Header", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dsg04d", + "name": "topics3Left", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "dsg04c", + "name": "topics3Drag", + "width": 14, + "height": 14, + "iconFontName": "grip-vertical", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground", + "opacity": 0.5 + }, + { + "type": "icon_font", + "id": "dsg04e", + "name": "topics3Icon", + "width": 18, + "height": 18, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "dsg04f", + "name": "topics3Label", + "fill": "$--foreground", + "content": "Topics", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "dsg04g", + "name": "topics3Chev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "csB01", + "x": 0, + "y": 7140, + "name": "Sections Header — Before (old design)", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 280, + "height": 120, + "fill": "$--sidebar", + "layout": "vertical", + "gap": 8, + "padding": [ + 16, + 12 + ], + "children": [ + { + "type": "text", + "id": "csB02", + "name": "frameLabel", + "fill": "$--muted-foreground", + "content": "BEFORE — Old Design", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 1 + }, + { + "type": "frame", + "id": "csB03", + "name": "customizeBtn", + "width": "fill_container", + "cornerRadius": 4, + "gap": 6, + "padding": [ + 4, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "csB04", + "name": "slidersIcon", + "width": 14, + "height": 14, + "iconFontName": "sliders-horizontal", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "csB05", + "name": "customizeLabel", + "fill": "$--muted-foreground", + "content": "Customize sections", + "fontFamily": "Inter", + "fontSize": 12 + } + ] + }, + { + "type": "frame", + "id": "csB06", + "name": "sectionRow", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "csB07", + "name": "left", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "csB08", + "name": "projIcon", + "width": 16, + "height": 16, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "fill": "$--accent-red" + }, + { + "type": "text", + "id": "csB09", + "name": "projLabel", + "fill": "$--foreground", + "content": "Projects", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "csB10", + "name": "chevron", + "width": 12, + "height": 12, + "iconFontName": "caret-right", + "iconFontFamily": "phosphor", + "fill": "$--foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "csA01", + "x": 340, + "y": 7140, + "name": "Sections Header — After (new design)", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 280, + "height": 120, + "fill": "$--sidebar", + "layout": "vertical", + "gap": 8, + "padding": [ + 16, + 12 + ], + "children": [ + { + "type": "text", + "id": "csA02", + "name": "frameLabel", + "fill": "$--muted-foreground", + "content": "AFTER — New Design", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 1 + }, + { + "type": "frame", + "id": "csA03", + "name": "sectionsHeader", + "width": "fill_container", + "padding": [ + 4, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "csA04", + "name": "sectionsLabel", + "fill": "$--muted-foreground", + "content": "SECTIONS", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600", + "letterSpacing": 1.2 + }, + { + "type": "icon_font", + "id": "csA05", + "name": "settingsIcon", + "width": 14, + "height": 14, + "iconFontName": "sliders-horizontal", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "csA06", + "name": "sectionRow", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "csA07", + "name": "left", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "csA08", + "name": "projIcon", + "width": 16, + "height": 16, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "fill": "$--accent-red" + }, + { + "type": "text", + "id": "csA09", + "name": "projLabel", + "fill": "$--foreground", + "content": "Projects", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "csA10", + "name": "chevron", + "width": 12, + "height": 12, + "iconFontName": "caret-right", + "iconFontFamily": "phosphor", + "fill": "$--foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "archBtnNorm", + "x": 68, + "y": 5200, + "name": "Archive Notes — Breadcrumb button (normal note)", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 800, + "height": 45, + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "anInfoL", + "name": "infoLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "anType", + "name": "breadcrumbType", + "fill": "$--muted-foreground", + "content": "Note", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "anSep", + "name": "breadcrumbSep", + "fill": "$--muted-foreground", + "content": "›", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "anName", + "name": "breadcrumbName", + "fill": "$--foreground", + "content": "My Active Note", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "anDot", + "name": "dotSep", + "fill": "$--muted-foreground", + "content": "·", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "anWords", + "name": "wordCount", + "fill": "$--muted-foreground", + "content": "1,234 words", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "anInfoR", + "name": "infoRight", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "anISearch", + "width": 16, + "height": 16, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "anIGit", + "width": 16, + "height": 16, + "iconFontName": "git-branch", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "anICursor", + "width": 16, + "height": 16, + "iconFontName": "cursor-text", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "anIAI", + "width": 16, + "height": 16, + "iconFontName": "sparkle", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "anArchiveBtn", + "name": "archiveButton", + "gap": 4, + "alignItems": "center", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--primary" + }, + "cornerRadius": 4, + "padding": [ + 2, + 2 + ], + "children": [ + { + "type": "icon_font", + "id": "anIArchive", + "width": 16, + "height": 16, + "iconFontName": "archive", + "iconFontFamily": "phosphor", + "fill": "$--primary" + } + ] + }, + { + "type": "icon_font", + "id": "anITrash", + "width": 16, + "height": 16, + "iconFontName": "trash", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "anIMore", + "width": 16, + "height": 16, + "iconFontName": "dots-three", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "archBtnArc", + "x": 68, + "y": 5300, + "name": "Archive Notes — Breadcrumb button (archived note, shows Unarchive)", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 800, + "height": 45, + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "auInfoL", + "name": "infoLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "auType", + "name": "breadcrumbType", + "fill": "$--muted-foreground", + "content": "Note", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "auSep", + "name": "breadcrumbSep", + "fill": "$--muted-foreground", + "content": "›", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "auName", + "name": "breadcrumbName", + "fill": "$--foreground", + "content": "My Archived Note", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "auDot", + "name": "dotSep", + "fill": "$--muted-foreground", + "content": "·", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "auWords", + "name": "wordCount", + "fill": "$--muted-foreground", + "content": "567 words", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "auBadge", + "name": "archivedBadge", + "height": 16, + "fill": "#2563eb1a", + "cornerRadius": 4, + "padding": [ + 1, + 4 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "auBadgeTxt", + "fill": "$--primary", + "content": "ARCHIVED", + "fontFamily": "Inter", + "fontSize": 9, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "auInfoR", + "name": "infoRight", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "auISearch", + "width": 16, + "height": 16, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "auIGit", + "width": 16, + "height": 16, + "iconFontName": "git-branch", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "auICursor", + "width": 16, + "height": 16, + "iconFontName": "cursor-text", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "auIAI", + "width": 16, + "height": 16, + "iconFontName": "sparkle", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "auUnarchiveBtn", + "name": "unarchiveButton", + "gap": 4, + "alignItems": "center", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--primary" + }, + "cornerRadius": 4, + "padding": [ + 2, + 2 + ], + "children": [ + { + "type": "icon_font", + "id": "auIUnarchive", + "width": 16, + "height": 16, + "iconFontName": "arrow-u-up-left", + "iconFontFamily": "phosphor", + "fill": "$--primary" + } + ] + }, + { + "type": "icon_font", + "id": "auITrash", + "width": 16, + "height": 16, + "iconFontName": "trash", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "auIMore", + "width": 16, + "height": 16, + "iconFontName": "dots-three", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghCL1", + "x": 0, + "y": 7320, + "name": "Git History — Commit List", + "width": 300, + "height": "fit_content(600)", + "fill": "$--background", + "cornerRadius": "$--radius-lg", + "stroke": { + "fill": "$--border", + "thickness": 1 + }, + "layout": "vertical", + "gap": 16, + "padding": 12, + "children": [ + { + "type": "text", + "id": "ghCL1h", + "content": "HISTORY", + "fill": "$--muted-foreground", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 1.2 + }, + { + "type": "frame", + "id": "ghC1", + "layout": "vertical", + "width": "fill_container", + "gap": 2, + "padding": [ + 0, + 0, + 0, + 10 + ], + "stroke": { + "fill": "$--border", + "thickness": { + "left": 2 + } + }, + "children": [ + { + "type": "frame", + "id": "ghC1r", + "layout": "horizontal", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghC1h", + "content": "a1b2c3d", + "fill": "$--primary", + "fontSize": 11, + "fontWeight": "500", + "underline": true + }, + { + "type": "text", + "id": "ghC1d", + "content": "2d ago", + "fill": "$--muted-foreground", + "fontSize": 10 + } + ] + }, + { + "type": "text", + "id": "ghC1m", + "content": "Update grow-newsletter with latest changes", + "fill": "$--secondary-foreground", + "fontSize": 12, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "ghC1a", + "content": "Luca Rossi", + "fill": "$--muted-foreground", + "fontSize": 10 + } + ] + }, + { + "type": "frame", + "id": "ghC2", + "layout": "vertical", + "width": "fill_container", + "gap": 2, + "padding": [ + 0, + 0, + 0, + 10 + ], + "stroke": { + "fill": "$--border", + "thickness": { + "left": 2 + } + }, + "children": [ + { + "type": "frame", + "id": "ghC2r", + "layout": "horizontal", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghC2h", + "content": "e4f5g6h", + "fill": "$--primary", + "fontSize": 11, + "fontWeight": "500", + "underline": true + }, + { + "type": "text", + "id": "ghC2d", + "content": "5d ago", + "fill": "$--muted-foreground", + "fontSize": 10 + } + ] + }, + { + "type": "text", + "id": "ghC2m", + "content": "Add new section to grow-newsletter", + "fill": "$--secondary-foreground", + "fontSize": 12, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "ghC2a", + "content": "Luca Rossi", + "fill": "$--muted-foreground", + "fontSize": 10 + } + ] + }, + { + "type": "frame", + "id": "ghC3", + "layout": "vertical", + "width": "fill_container", + "gap": 2, + "padding": [ + 0, + 0, + 0, + 10 + ], + "stroke": { + "fill": "$--border", + "thickness": { + "left": 2 + } + }, + "children": [ + { + "type": "frame", + "id": "ghC3r", + "layout": "horizontal", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghC3h", + "content": "i7j8k9l", + "fill": "$--primary", + "fontSize": 11, + "fontWeight": "500", + "underline": true + }, + { + "type": "text", + "id": "ghC3d", + "content": "12d ago", + "fill": "$--muted-foreground", + "fontSize": 10 + } + ] + }, + { + "type": "text", + "id": "ghC3m", + "content": "Create grow-newsletter", + "fill": "$--secondary-foreground", + "fontSize": 12, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "text", + "id": "ghC3a", + "content": "Luca Rossi", + "fill": "$--muted-foreground", + "fontSize": 10 + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghDO1", + "x": 380, + "y": 7320, + "name": "Git History — Diff Open", + "width": 600, + "height": "fit_content(500)", + "fill": "$--background", + "cornerRadius": "$--radius-lg", + "stroke": { + "fill": "$--border", + "thickness": 1 + }, + "layout": "vertical", + "gap": 0, + "padding": 0, + "children": [ + { + "type": "frame", + "id": "ghDObc", + "layout": "horizontal", + "width": "fill_container", + "height": 36, + "padding": [ + 0, + 12 + ], + "gap": 8, + "alignItems": "center", + "fill": "$--muted", + "stroke": { + "fill": "$--border", + "thickness": { + "bottom": 1 + } + }, + "children": [ + { + "type": "text", + "id": "ghDObcP", + "content": "responsibility / grow-newsletter.md", + "fill": "$--muted-foreground", + "fontSize": 12 + }, + { + "type": "frame", + "id": "ghDObcS", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "ghDObcB", + "layout": "horizontal", + "padding": [ + 2, + 8 + ], + "cornerRadius": "$--radius-sm", + "fill": "$--primary", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghDObcBt", + "content": "Diff: a1b2c3d", + "fill": "$--primary-foreground", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghDOb", + "layout": "vertical", + "width": "fill_container", + "padding": 0, + "gap": 0, + "children": [ + { + "type": "frame", + "id": "ghDOl1", + "layout": "horizontal", + "width": "fill_container", + "height": 22, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "fill": "$--muted", + "children": [ + { + "type": "text", + "id": "ghDOl1n", + "content": "1", + "fill": "$--muted-foreground", + "fontSize": 11, + "textGrowth": "fixed-width", + "width": 30, + "textAlign": "right" + }, + { + "type": "text", + "id": "ghDOl1t", + "content": "diff --git a/grow-newsletter.md b/grow-newsletter.md", + "fill": "$--muted-foreground", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "ghDOl2", + "layout": "horizontal", + "width": "fill_container", + "height": 22, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "fill": "#2196F30D", + "children": [ + { + "type": "text", + "id": "ghDOl2n", + "content": "2", + "fill": "$--muted-foreground", + "fontSize": 11, + "textGrowth": "fixed-width", + "width": 30, + "textAlign": "right" + }, + { + "type": "text", + "id": "ghDOl2t", + "content": "@@ -5,3 +5,5 @@", + "fill": "$--primary", + "fontSize": 11, + "fontStyle": "italic" + } + ] + }, + { + "type": "frame", + "id": "ghDOl3", + "layout": "horizontal", + "width": "fill_container", + "height": 22, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghDOl3n", + "content": "3", + "fill": "$--muted-foreground", + "fontSize": 11, + "textGrowth": "fixed-width", + "width": 30, + "textAlign": "right" + }, + { + "type": "text", + "id": "ghDOl3t", + "content": " # Grow Newsletter", + "fill": "$--secondary-foreground", + "fontSize": 11 + } + ] + }, + { + "type": "frame", + "id": "ghDOl4", + "layout": "horizontal", + "width": "fill_container", + "height": 22, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "fill": "#f4433614", + "children": [ + { + "type": "text", + "id": "ghDOl4n", + "content": "4", + "fill": "$--muted-foreground", + "fontSize": 11, + "textGrowth": "fixed-width", + "width": 30, + "textAlign": "right" + }, + { + "type": "text", + "id": "ghDOl4t", + "content": "-Old paragraph from before a1b2c3d.", + "fill": "#f44336", + "fontSize": 11 + } + ] + }, + { + "type": "frame", + "id": "ghDOl5", + "layout": "horizontal", + "width": "fill_container", + "height": 22, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "fill": "#4caf5014", + "children": [ + { + "type": "text", + "id": "ghDOl5n", + "content": "5", + "fill": "$--muted-foreground", + "fontSize": 11, + "textGrowth": "fixed-width", + "width": 30, + "textAlign": "right" + }, + { + "type": "text", + "id": "ghDOl5t", + "content": "+Updated paragraph at commit a1b2c3d.", + "fill": "#4caf50", + "fontSize": 11 + } + ] + }, + { + "type": "frame", + "id": "ghDOl6", + "layout": "horizontal", + "width": "fill_container", + "height": 22, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "fill": "#4caf5014", + "children": [ + { + "type": "text", + "id": "ghDOl6n", + "content": "6", + "fill": "$--muted-foreground", + "fontSize": 11, + "textGrowth": "fixed-width", + "width": 30, + "textAlign": "right" + }, + { + "type": "text", + "id": "ghDOl6t", + "content": "+New content added in this commit.", + "fill": "#4caf50", + "fontSize": 11 + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "rnt0", + "name": "Rename Tab — Normal tab state", + "width": 800, + "height": 120, + "layout": "vertical", + "fill": "$--sidebar", + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "rnt1", + "name": "frameLabel", + "content": "Normal state: tabs show note title. Double-click to rename.", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal", + "fill": "$--muted-foreground", + "padding": [ + 8, + 12 + ] + }, + { + "type": "frame", + "id": "rnt2", + "name": "Tab Bar Normal", + "width": "fill_container", + "height": 45, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "rnt3", + "name": "Tab Active", + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rnt4", + "fill": "$--foreground", + "content": "Weekly Review", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "rnt5", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "rnt6", + "name": "Tab Inactive", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rnt7", + "fill": "$--muted-foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "rnt8", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "rnt9", + "name": "tabSpacer", + "width": "fill_container", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + } + }, + { + "type": "frame", + "id": "rnta", + "name": "tabControls", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1, + "left": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_around", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "rntb", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "text", + "id": "rntc", + "name": "interactionNote", + "content": "Double-click tab title → enters edit mode", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal", + "fill": "$--muted-foreground", + "padding": [ + 4, + 12 + ] + } + ] + }, + { + "type": "frame", + "id": "rne0", + "name": "Rename Tab — Double-clicked (editing mode)", + "width": 800, + "height": 120, + "layout": "vertical", + "fill": "$--sidebar", + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "rne1", + "name": "frameLabel", + "content": "Editing mode: tab title replaced with inline input field, text selected.", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal", + "fill": "$--muted-foreground", + "padding": [ + 8, + 12 + ] + }, + { + "type": "frame", + "id": "rne2", + "name": "Tab Bar Editing", + "width": "fill_container", + "height": 45, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "rne3", + "name": "Tab Active (Editing)", + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "rne4", + "name": "Inline Input", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--ring" + }, + "cornerRadius": 3, + "padding": [ + 2, + 6 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rne5", + "fill": "$--foreground", + "content": "Weekly Review", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500", + "highlight": "$--primary", + "highlightOpacity": 0.2 + } + ] + }, + { + "type": "icon_font", + "id": "rne6", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "rne7", + "name": "Tab Inactive", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rne8", + "fill": "$--muted-foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "rne9", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "rnea", + "name": "tabSpacer", + "width": "fill_container", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + } + }, + { + "type": "frame", + "id": "rneb", + "name": "tabControls", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1, + "left": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_around", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "rnec", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "text", + "id": "rned", + "name": "interactionNote", + "content": "Enter → save & rename file + update wiki links | Escape → cancel | Blur → save", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal", + "fill": "$--muted-foreground", + "padding": [ + 4, + 12 + ] + } + ] + }, + { + "type": "frame", + "id": "rns0", + "name": "Rename Tab — Saved (updated name)", + "width": 800, + "height": 120, + "layout": "vertical", + "fill": "$--sidebar", + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "rns1", + "name": "frameLabel", + "content": "After save: tab shows updated title. File renamed, wiki links updated across vault.", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal", + "fill": "$--muted-foreground", + "padding": [ + 8, + 12 + ] + }, + { + "type": "frame", + "id": "rns2", + "name": "Tab Bar Saved", + "width": "fill_container", + "height": 45, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "rns3", + "name": "Tab Active (Renamed)", + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rns4", + "fill": "$--foreground", + "content": "Sprint Retrospective", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "rns5", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "rns6", + "name": "Tab Inactive", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rns7", + "fill": "$--muted-foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "rns8", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "rns9", + "name": "tabSpacer", + "width": "fill_container", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + } + }, + { + "type": "frame", + "id": "rnsa", + "name": "tabControls", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1, + "left": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_around", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "rnsb", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "text", + "id": "rnsc", + "name": "interactionNote", + "content": "File: weekly-review.md → sprint-retrospective.md | [[Weekly Review]] → [[Sprint Retrospective]] in all notes", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal", + "fill": "$--muted-foreground", + "padding": [ + 4, + 12 + ] + } + ] + }, + { + "type": "frame", + "id": "mb001", + "x": 0, + "y": 0, + "name": "macOS Border — Before (no border)", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 400, + "height": 200, + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "mb002", + "name": "macOS Title Bar (before)", + "width": "fill_container", + "height": 38, + "fill": "$--sidebar", + "gap": 12, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mb003", + "name": "trafficLights", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "mb004", + "fill": "#FF5F57", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "mb005", + "fill": "#FEBC2E", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "mb006", + "fill": "#28C840", + "width": 12, + "height": 12 + } + ] + } + ] + }, + { + "type": "frame", + "id": "mb007", + "name": "Sidebar Content (before)", + "width": "fill_container", + "height": "fill_container", + "fill": "$--sidebar", + "padding": [ + 8, + 12 + ], + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "mb008", + "value": "All Notes", + "fontSize": 14, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "mb009", + "value": "Favorites", + "fontSize": 14, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "mb010", + "value": "Archive", + "fontSize": 14, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "mb011", + "x": 440, + "y": 0, + "name": "macOS Border — After (with border)", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 400, + "height": 200, + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "mb012", + "name": "macOS Title Bar (after)", + "width": "fill_container", + "height": 38, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mb013", + "name": "trafficLights", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "mb014", + "fill": "#FF5F57", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "mb015", + "fill": "#FEBC2E", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "mb016", + "fill": "#28C840", + "width": 12, + "height": 12 + } + ] + } + ] + }, + { + "type": "frame", + "id": "mb017", + "name": "Sidebar Content (after)", + "width": "fill_container", + "height": "fill_container", + "fill": "$--sidebar", + "padding": [ + 8, + 12 + ], + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "mb018", + "value": "All Notes", + "fontSize": 14, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "mb019", + "value": "Favorites", + "fontSize": 14, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "mb020", + "value": "Archive", + "fontSize": 14, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "iogBH", + "x": 5500, + "y": 100, + "name": "Settings Panel — Modal", + "width": 520, + "fill": "$--background", + "cornerRadius": 12, + "stroke": { + "thickness": 1, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "61XqC", + "name": "Header", + "width": "fill_container", + "height": 56, + "stroke": { + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 0, + 24 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "2IAX7", + "name": "Title", + "fill": "$--foreground", + "content": "Settings", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "icon_font", + "id": "kIGy9", + "name": "Close Icon", + "width": 16, + "height": 16, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "Jk6ga", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 24, + "padding": 24, + "children": [ + { + "type": "text", + "id": "efWNx", + "name": "Section Label", + "fill": "$--foreground", + "content": "AI Provider Keys", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "text", + "id": "wYQL5", + "name": "Section Description", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "API keys are stored locally on your device. Never sent to our servers.", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "lo4r8", + "name": "Anthropic Key Row", + "width": "fill_container", + "layout": "vertical", + "gap": 6, + "children": [ + { + "type": "text", + "id": "WogRV", + "name": "anthropicLabel", + "fill": "$--foreground", + "content": "Anthropic", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "GUELn", + "name": "Input", + "width": "fill_container", + "height": 36, + "fill": "$--background", + "cornerRadius": "$--radius-md", + "stroke": { + "thickness": 1, + "fill": "$--border" + }, + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "NjLRf", + "name": "anthropicValue", + "fill": "$--foreground", + "content": "sk-ant-•••••••••••••k4Rm", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "kBDAI", + "name": "anthropicClear", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "qaTjV", + "name": "OpenAI Key Row", + "width": "fill_container", + "layout": "vertical", + "gap": 6, + "children": [ + { + "type": "text", + "id": "etofy", + "name": "openaiLabel", + "fill": "$--foreground", + "content": "OpenAI", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "BKXuT", + "name": "Input Empty", + "width": "fill_container", + "height": 36, + "fill": "$--background", + "cornerRadius": "$--radius-md", + "stroke": { + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "EHi9A", + "name": "openaiPlaceholder", + "fill": "$--muted-foreground", + "content": "sk-...", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ZM8dg", + "name": "Google Key Row", + "width": "fill_container", + "layout": "vertical", + "gap": 6, + "children": [ + { + "type": "text", + "id": "qzpsp", + "name": "googleLabel", + "fill": "$--foreground", + "content": "Google AI", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "pRsLr", + "name": "Input Empty", + "width": "fill_container", + "height": 36, + "fill": "$--background", + "cornerRadius": "$--radius-md", + "stroke": { + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "KAqg2", + "name": "googlePlaceholder", + "fill": "$--muted-foreground", + "content": "AIza...", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "be7TS", + "name": "Footer", + "width": "fill_container", + "height": 56, + "stroke": { + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 24 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LAIeB", + "name": "footerHint", + "fill": "$--muted-foreground", + "content": "Cmd+, to open settings", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "DR2VW", + "name": "Spacer", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "u9fBE", + "name": "Save Button", + "height": 32, + "fill": "$--primary", + "cornerRadius": "$--radius-md", + "padding": [ + 0, + 16 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "yRYWq", + "name": "saveBtnLabel", + "fill": "$--primary-foreground", + "content": "Save", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "srtDD", + "x": 0, + "y": 0, + "name": "Sort Dropdown — Direction Arrows", + "clip": true, + "width": 260, + "height": 280, + "fill": "$--popover", + "cornerRadius": "$--radius-md", + "stroke": { + "align": "outside", + "thickness": 1, + "fill": "$--border" + }, + "layout": "vertical", + "padding": [ + 8, + 0 + ], + "children": [ + { + "type": "frame", + "id": "srtH1", + "name": "dropdownHeader", + "width": "fill_container", + "height": 28, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srtHL", + "name": "headerLabel", + "content": "Sort by", + "fontSize": 11, + "fontWeight": 600, + "fill": "$--muted-foreground", + "fontFamily": "$--font-primary", + "textTransform": "uppercase", + "letterSpacing": 0.5 + } + ] + }, + { + "type": "frame", + "id": "srtR1", + "name": "row-modified", + "width": "fill_container", + "height": 36, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "fill": "$--muted", + "children": [ + { + "type": "text", + "id": "srtT1", + "name": "label-modified", + "content": "Modified", + "fontSize": 13, + "fontWeight": 500, + "fill": "$--popover-foreground", + "fontFamily": "$--font-primary", + "width": "fill_container" + }, + { + "type": "frame", + "id": "srtA1", + "name": "arrows-modified", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srtU1", + "name": "arrow-up-active", + "content": "↑", + "fontSize": 13, + "fontWeight": 700, + "fill": "$--accent-blue" + }, + { + "type": "text", + "id": "srtD1", + "name": "arrow-down", + "content": "↓", + "fontSize": 13, + "fontWeight": 400, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "srtR2", + "name": "row-title", + "width": "fill_container", + "height": 36, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srtT2", + "name": "label-title", + "content": "Title", + "fontSize": 13, + "fontWeight": 400, + "fill": "$--popover-foreground", + "fontFamily": "$--font-primary", + "width": "fill_container" + }, + { + "type": "frame", + "id": "srtA2", + "name": "arrows-title", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srtU2", + "name": "arrow-up", + "content": "↑", + "fontSize": 13, + "fontWeight": 400, + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "srtD2", + "name": "arrow-down", + "content": "↓", + "fontSize": 13, + "fontWeight": 400, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "srtR3", + "name": "row-created", + "width": "fill_container", + "height": 36, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srtT3", + "name": "label-created", + "content": "Created", + "fontSize": 13, + "fontWeight": 400, + "fill": "$--popover-foreground", + "fontFamily": "$--font-primary", + "width": "fill_container" + }, + { + "type": "frame", + "id": "srtA3", + "name": "arrows-created", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srtU3", + "name": "arrow-up", + "content": "↑", + "fontSize": 13, + "fontWeight": 400, + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "srtD3", + "name": "arrow-down", + "content": "↓", + "fontSize": 13, + "fontWeight": 400, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "srtR4", + "name": "row-type", + "width": "fill_container", + "height": 36, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srtT4", + "name": "label-type", + "content": "Type", + "fontSize": 13, + "fontWeight": 400, + "fill": "$--popover-foreground", + "fontFamily": "$--font-primary", + "width": "fill_container" + }, + { + "type": "frame", + "id": "srtA4", + "name": "arrows-type", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srtU4", + "name": "arrow-up", + "content": "↑", + "fontSize": 13, + "fontWeight": 400, + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "srtD4", + "name": "arrow-down", + "content": "↓", + "fontSize": 13, + "fontWeight": 400, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "srtSP", + "name": "separator", + "width": "fill_container", + "height": 1, + "fill": "$--border" + }, + { + "type": "frame", + "id": "srtAN", + "name": "annotation", + "width": "fill_container", + "padding": [ + 6, + 12 + ], + "children": [ + { + "type": "text", + "id": "srtAT", + "name": "annotation-text", + "content": "Click ↑↓ to toggle direction. Blue = active.", + "fontSize": 11, + "fill": "$--muted-foreground", + "fontFamily": "$--font-primary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "srtBN", + "x": 300, + "y": 0, + "name": "Sort Button — With Direction Indicator", + "clip": true, + "width": 400, + "height": 200, + "fill": "$--background", + "cornerRadius": "$--radius-md", + "stroke": { + "align": "outside", + "thickness": 1, + "fill": "$--border" + }, + "layout": "vertical", + "padding": [ + 16, + 16 + ], + "gap": 16, + "children": [ + { + "type": "frame", + "id": "srtBH", + "name": "noteListHeader", + "width": "fill_container", + "height": 36, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srtBC", + "name": "noteCount", + "content": "42 notes", + "fontSize": 12, + "fill": "$--muted-foreground", + "fontFamily": "$--font-primary", + "width": "fill_container" + }, + { + "type": "frame", + "id": "srtBB", + "name": "sortButton", + "padding": [ + 4, + 8 + ], + "cornerRadius": "$--radius-sm", + "fill": "$--muted", + "alignItems": "center", + "gap": 4, + "children": [ + { + "type": "text", + "id": "srtBL", + "name": "sortLabel", + "content": "Modified", + "fontSize": 12, + "fontWeight": 500, + "fill": "$--foreground", + "fontFamily": "$--font-primary" + }, + { + "type": "text", + "id": "srtBA", + "name": "directionArrow", + "content": "↓", + "fontSize": 12, + "fontWeight": 600, + "fill": "$--accent-blue" + } + ] + } + ] + }, + { + "type": "frame", + "id": "srtSP2", + "name": "divider", + "width": "fill_container", + "height": 1, + "fill": "$--border" + }, + { + "type": "frame", + "id": "srtEX", + "name": "exampleNotes", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "srtN1", + "name": "noteRow1", + "width": "fill_container", + "height": 32, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srtNT1", + "name": "noteTitle1", + "content": "Meeting Notes — Feb 22", + "fontSize": 13, + "fill": "$--foreground", + "fontFamily": "$--font-primary", + "width": "fill_container" + }, + { + "type": "text", + "id": "srtND1", + "name": "noteDate1", + "content": "2 min ago", + "fontSize": 11, + "fill": "$--muted-foreground", + "fontFamily": "$--font-primary" + } + ] + }, + { + "type": "frame", + "id": "srtN2", + "name": "noteRow2", + "width": "fill_container", + "height": 32, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srtNT2", + "name": "noteTitle2", + "content": "Project Roadmap", + "fontSize": 13, + "fill": "$--foreground", + "fontFamily": "$--font-primary", + "width": "fill_container" + }, + { + "type": "text", + "id": "srtND2", + "name": "noteDate2", + "content": "1 hr ago", + "fontSize": 11, + "fill": "$--muted-foreground", + "fontFamily": "$--font-primary" + } + ] + }, + { + "type": "frame", + "id": "srtN3", + "name": "noteRow3", + "width": "fill_container", + "height": 32, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srtNT3", + "name": "noteTitle3", + "content": "Weekly Review", + "fontSize": 13, + "fill": "$--foreground", + "fontFamily": "$--font-primary", + "width": "fill_container" + }, + { + "type": "text", + "id": "srtND3", + "name": "noteDate3", + "content": "Yesterday", + "fontSize": 11, + "fill": "$--muted-foreground", + "fontFamily": "$--font-primary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "srtAN2", + "name": "annotation", + "width": "fill_container", + "padding": [ + 6, + 0 + ], + "children": [ + { + "type": "text", + "id": "srtAT2", + "name": "annotation-text", + "content": "Sort button shows property + direction arrow. Click to open dropdown.", + "fontSize": 11, + "fill": "$--muted-foreground", + "fontFamily": "$--font-primary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv01", + "x": 68, + "y": 0, + "name": "GitHub Vault — Modal empty state", + "clip": true, + "width": 560, + "height": 480, + "fill": "$--background", + "cornerRadius": 12, + "layout": "vertical", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "children": [ + { + "type": "frame", + "id": "ghv01-hdr", + "name": "Header", + "width": "fill_container", + "height": 56, + "fill": "$--background", + "padding": [ + 0, + 24 + ], + "alignItems": "center", + "justifyContent": "space-between", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "ghv01-title", + "name": "title", + "content": "Connect GitHub Repo", + "fontSize": 16, + "fontWeight": 600, + "fill": "$--foreground" + }, + { + "type": "frame", + "id": "ghv01-close", + "name": "closeBtn", + "width": 24, + "height": 24, + "cornerRadius": 4, + "alignItems": "center", + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "ghv01-x", + "content": "X", + "fontSize": 14, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv01-tabs", + "name": "TabBar", + "width": "fill_container", + "height": 44, + "fill": "$--background", + "padding": [ + 0, + 24 + ], + "gap": 0, + "alignItems": "end", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "frame", + "id": "ghv01-tab-clone", + "name": "Tab: Clone Existing", + "padding": [ + 8, + 16 + ], + "alignItems": "center", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 2 + }, + "fill": "$--accent-blue" + }, + "children": [ + { + "type": "text", + "id": "ghv01-tab-clone-lbl", + "content": "Clone Existing", + "fontSize": 13, + "fontWeight": 500, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "ghv01-tab-create", + "name": "Tab: Create New", + "padding": [ + 8, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghv01-tab-create-lbl", + "content": "Create New", + "fontSize": 13, + "fontWeight": 400, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv01-body", + "name": "Body: Empty State", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "padding": [ + 32, + 24 + ], + "gap": 16, + "alignItems": "center", + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "ghv01-icon", + "name": "githubIcon", + "width": 48, + "height": 48, + "cornerRadius": 24, + "fill": "$--accent", + "alignItems": "center", + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "ghv01-gh", + "content": "GH", + "fontSize": 20, + "fontWeight": 600, + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "text", + "id": "ghv01-heading", + "content": "Clone an existing GitHub repository", + "fontSize": 15, + "fontWeight": 600, + "fill": "$--foreground", + "textAlign": "center" + }, + { + "type": "text", + "id": "ghv01-desc", + "content": "Search your repos or paste a URL to clone a GitHub repo as a vault.", + "fontSize": 13, + "fill": "$--muted-foreground", + "textAlign": "center", + "width": 360 + }, + { + "type": "frame", + "id": "ghv01-search", + "name": "SearchInput", + "width": 400, + "height": 40, + "cornerRadius": 8, + "fill": "$--input", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghv01-placeholder", + "content": "Search repos or paste URL...", + "fontSize": 13, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv01-footer", + "name": "Footer", + "width": "fill_container", + "height": 56, + "padding": [ + 0, + 24 + ], + "alignItems": "center", + "justifyContent": "space-between", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "ghv01-hint", + "content": "Connected as @lucaong", + "fontSize": 11, + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "ghv01-btns", + "name": "buttons", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ghv01-cancel", + "name": "cancelBtn", + "padding": [ + 6, + 16 + ], + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "ghv01-cancel-lbl", + "content": "Cancel", + "fontSize": 13, + "fill": "$--foreground" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv02", + "x": 700, + "y": 0, + "name": "GitHub Vault — Clone repo list", + "clip": true, + "width": 560, + "height": 540, + "fill": "$--background", + "cornerRadius": 12, + "layout": "vertical", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "children": [ + { + "type": "frame", + "id": "ghv02-hdr", + "name": "Header", + "width": "fill_container", + "height": 56, + "fill": "$--background", + "padding": [ + 0, + 24 + ], + "alignItems": "center", + "justifyContent": "space-between", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "ghv02-title", + "content": "Connect GitHub Repo", + "fontSize": 16, + "fontWeight": 600, + "fill": "$--foreground" + }, + { + "type": "frame", + "id": "ghv02-close", + "name": "closeBtn", + "width": 24, + "height": 24, + "cornerRadius": 4, + "alignItems": "center", + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "ghv02-x", + "content": "X", + "fontSize": 14, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv02-tabs", + "name": "TabBar", + "width": "fill_container", + "height": 44, + "fill": "$--background", + "padding": [ + 0, + 24 + ], + "gap": 0, + "alignItems": "end", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "frame", + "id": "ghv02-tab-clone", + "name": "Tab: Clone Existing (active)", + "padding": [ + 8, + 16 + ], + "alignItems": "center", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 2 + }, + "fill": "$--accent-blue" + }, + "children": [ + { + "type": "text", + "id": "ghv02-tab-clone-lbl", + "content": "Clone Existing", + "fontSize": 13, + "fontWeight": 500, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "ghv02-tab-create", + "name": "Tab: Create New", + "padding": [ + 8, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghv02-tab-create-lbl", + "content": "Create New", + "fontSize": 13, + "fontWeight": 400, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv02-body", + "name": "Body: Repo List", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "padding": [ + 16, + 24 + ], + "gap": 12, + "children": [ + { + "type": "frame", + "id": "ghv02-search", + "name": "SearchInput (filled)", + "width": "fill_container", + "height": 40, + "cornerRadius": 8, + "fill": "$--input", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--ring" + }, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghv02-search-val", + "content": "laputa", + "fontSize": 13, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "ghv02-list", + "name": "RepoList", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "ghv02-repo1", + "name": "RepoItem: selected", + "width": "fill_container", + "height": 56, + "cornerRadius": 6, + "fill": "$--accent", + "padding": [ + 8, + 12 + ], + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "ghv02-repo1-row", + "name": "topRow", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghv02-repo1-name", + "content": "lucaong/laputa-vault", + "fontSize": 13, + "fontWeight": 600, + "fill": "$--foreground" + }, + { + "type": "frame", + "id": "ghv02-repo1-badge", + "name": "privateBadge", + "padding": [ + 2, + 6 + ], + "cornerRadius": 4, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "ghv02-repo1-priv", + "content": "Private", + "fontSize": 10, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "text", + "id": "ghv02-repo1-desc", + "content": "Personal knowledge vault — markdown + YAML frontmatter", + "fontSize": 12, + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "ghv02-repo2", + "name": "RepoItem: unselected", + "width": "fill_container", + "height": 56, + "cornerRadius": 6, + "padding": [ + 8, + 12 + ], + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "ghv02-repo2-row", + "name": "topRow", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghv02-repo2-name", + "content": "lucaong/laputa-app", + "fontSize": 13, + "fontWeight": 500, + "fill": "$--foreground" + }, + { + "type": "frame", + "id": "ghv02-repo2-badge", + "name": "publicBadge", + "padding": [ + 2, + 6 + ], + "cornerRadius": 4, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "ghv02-repo2-pub", + "content": "Public", + "fontSize": 10, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "text", + "id": "ghv02-repo2-desc", + "content": "Laputa desktop app — Tauri + React + CodeMirror 6", + "fontSize": 12, + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "ghv02-repo3", + "name": "RepoItem: unselected 2", + "width": "fill_container", + "height": 56, + "cornerRadius": 6, + "padding": [ + 8, + 12 + ], + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "ghv02-repo3-row", + "name": "topRow", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghv02-repo3-name", + "content": "lucaong/dotfiles", + "fontSize": 13, + "fontWeight": 500, + "fill": "$--foreground" + } + ] + }, + { + "type": "text", + "id": "ghv02-repo3-desc", + "content": "My macOS dotfiles and config", + "fontSize": 12, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv02-path", + "name": "LocalPathPicker", + "width": "fill_container", + "layout": "vertical", + "gap": 6, + "children": [ + { + "type": "text", + "id": "ghv02-path-lbl", + "content": "Clone to:", + "fontSize": 12, + "fontWeight": 500, + "fill": "$--foreground" + }, + { + "type": "frame", + "id": "ghv02-path-input", + "name": "pathInput", + "width": "fill_container", + "height": 36, + "cornerRadius": 6, + "fill": "$--input", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghv02-path-val", + "content": "~/Vaults/laputa-vault", + "fontSize": 12, + "fill": "$--foreground" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv02-footer", + "name": "Footer", + "width": "fill_container", + "height": 56, + "padding": [ + 0, + 24 + ], + "alignItems": "center", + "justifyContent": "space-between", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "ghv02-hint", + "content": "3 repos found", + "fontSize": 11, + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "ghv02-btns", + "name": "buttons", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ghv02-cancel", + "name": "cancelBtn", + "padding": [ + 6, + 16 + ], + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "ghv02-cancel-lbl", + "content": "Cancel", + "fontSize": 13, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "ghv02-clone-btn", + "name": "cloneBtn", + "padding": [ + 6, + 16 + ], + "cornerRadius": 6, + "fill": "$--primary", + "children": [ + { + "type": "text", + "id": "ghv02-clone-lbl", + "content": "Clone & Open", + "fontSize": 13, + "fontWeight": 500, + "fill": "#FFFFFF" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv03", + "x": 68, + "y": 600, + "name": "GitHub Vault — Create new repo", + "clip": true, + "width": 560, + "height": 440, + "fill": "$--background", + "cornerRadius": 12, + "layout": "vertical", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "children": [ + { + "type": "frame", + "id": "ghv03-hdr", + "name": "Header", + "width": "fill_container", + "height": 56, + "fill": "$--background", + "padding": [ + 0, + 24 + ], + "alignItems": "center", + "justifyContent": "space-between", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "ghv03-title", + "content": "Connect GitHub Repo", + "fontSize": 16, + "fontWeight": 600, + "fill": "$--foreground" + }, + { + "type": "frame", + "id": "ghv03-close", + "name": "closeBtn", + "width": 24, + "height": 24, + "cornerRadius": 4, + "alignItems": "center", + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "ghv03-x", + "content": "X", + "fontSize": 14, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv03-tabs", + "name": "TabBar", + "width": "fill_container", + "height": 44, + "fill": "$--background", + "padding": [ + 0, + 24 + ], + "gap": 0, + "alignItems": "end", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "frame", + "id": "ghv03-tab-clone", + "name": "Tab: Clone Existing", + "padding": [ + 8, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghv03-tab-clone-lbl", + "content": "Clone Existing", + "fontSize": 13, + "fontWeight": 400, + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "ghv03-tab-create", + "name": "Tab: Create New (active)", + "padding": [ + 8, + 16 + ], + "alignItems": "center", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 2 + }, + "fill": "$--accent-blue" + }, + "children": [ + { + "type": "text", + "id": "ghv03-tab-create-lbl", + "content": "Create New", + "fontSize": 13, + "fontWeight": 500, + "fill": "$--foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv03-body", + "name": "Body: Create Form", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "padding": [ + 24, + 24 + ], + "gap": 20, + "children": [ + { + "type": "frame", + "id": "ghv03-name-field", + "name": "RepoNameField", + "width": "fill_container", + "layout": "vertical", + "gap": 6, + "children": [ + { + "type": "text", + "id": "ghv03-name-lbl", + "content": "Repository name", + "fontSize": 12, + "fontWeight": 500, + "fill": "$--foreground" + }, + { + "type": "frame", + "id": "ghv03-name-input", + "name": "nameInput", + "width": "fill_container", + "height": 40, + "cornerRadius": 8, + "fill": "$--input", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghv03-name-val", + "content": "my-vault", + "fontSize": 13, + "fill": "$--foreground" + } + ] + }, + { + "type": "text", + "id": "ghv03-name-hint", + "content": "github.com/lucaong/my-vault", + "fontSize": 11, + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "ghv03-vis-field", + "name": "VisibilityToggle", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "ghv03-vis-lbl", + "content": "Visibility", + "fontSize": 12, + "fontWeight": 500, + "fill": "$--foreground" + }, + { + "type": "frame", + "id": "ghv03-vis-opts", + "name": "options", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ghv03-opt-priv", + "name": "PrivateOption (selected)", + "padding": [ + 6, + 12 + ], + "cornerRadius": 6, + "fill": "$--accent", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--accent-blue" + }, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghv03-opt-priv-icon", + "content": "Lock", + "fontSize": 13, + "fill": "$--accent-blue" + }, + { + "type": "text", + "id": "ghv03-opt-priv-lbl", + "content": "Private", + "fontSize": 13, + "fontWeight": 500, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "ghv03-opt-pub", + "name": "PublicOption", + "padding": [ + 6, + 12 + ], + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghv03-opt-pub-icon", + "content": "Globe", + "fontSize": 13, + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "ghv03-opt-pub-lbl", + "content": "Public", + "fontSize": 13, + "fill": "$--muted-foreground" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv03-path-field", + "name": "LocalPathField", + "width": "fill_container", + "layout": "vertical", + "gap": 6, + "children": [ + { + "type": "text", + "id": "ghv03-path-lbl", + "content": "Clone to:", + "fontSize": 12, + "fontWeight": 500, + "fill": "$--foreground" + }, + { + "type": "frame", + "id": "ghv03-path-input", + "name": "pathInput", + "width": "fill_container", + "height": 36, + "cornerRadius": 6, + "fill": "$--input", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ghv03-path-val", + "content": "~/Vaults/my-vault", + "fontSize": 12, + "fill": "$--foreground" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv03-footer", + "name": "Footer", + "width": "fill_container", + "height": 56, + "padding": [ + 0, + 24 + ], + "alignItems": "center", + "justifyContent": "end", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "frame", + "id": "ghv03-btns", + "name": "buttons", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ghv03-cancel", + "name": "cancelBtn", + "padding": [ + 6, + 16 + ], + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "ghv03-cancel-lbl", + "content": "Cancel", + "fontSize": 13, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "ghv03-create-btn", + "name": "createBtn", + "padding": [ + 6, + 16 + ], + "cornerRadius": 6, + "fill": "$--primary", + "children": [ + { + "type": "text", + "id": "ghv03-create-lbl", + "content": "Create & Clone", + "fontSize": 13, + "fontWeight": 500, + "fill": "#FFFFFF" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv04", + "x": 700, + "y": 600, + "name": "GitHub Vault — Clone in progress", + "clip": true, + "width": 560, + "height": 320, + "fill": "$--background", + "cornerRadius": 12, + "layout": "vertical", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "children": [ + { + "type": "frame", + "id": "ghv04-hdr", + "name": "Header", + "width": "fill_container", + "height": 56, + "fill": "$--background", + "padding": [ + 0, + 24 + ], + "alignItems": "center", + "justifyContent": "space-between", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "ghv04-title", + "content": "Cloning Repository...", + "fontSize": 16, + "fontWeight": 600, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "ghv04-body", + "name": "Body: Progress", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "padding": [ + 32, + 24 + ], + "gap": 20, + "alignItems": "center", + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "ghv04-spinner", + "name": "spinner", + "width": 40, + "height": 40, + "cornerRadius": 20, + "stroke": { + "align": "inside", + "thickness": 3, + "fill": "$--accent-blue" + } + }, + { + "type": "text", + "id": "ghv04-status", + "content": "Cloning lucaong/laputa-vault...", + "fontSize": 14, + "fontWeight": 500, + "fill": "$--foreground", + "textAlign": "center" + }, + { + "type": "text", + "id": "ghv04-sub", + "content": "Receiving objects: 67% (1,234/1,842)", + "fontSize": 12, + "fill": "$--muted-foreground", + "textAlign": "center" + }, + { + "type": "frame", + "id": "ghv04-bar-bg", + "name": "progressBarBg", + "width": 360, + "height": 4, + "cornerRadius": 2, + "fill": "$--accent", + "children": [ + { + "type": "frame", + "id": "ghv04-bar-fill", + "name": "progressBarFill", + "width": 241, + "height": 4, + "cornerRadius": 2, + "fill": "$--accent-blue" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ghv04-footer", + "name": "Footer", + "width": "fill_container", + "height": 56, + "padding": [ + 0, + 24 + ], + "alignItems": "center", + "justifyContent": "end", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "frame", + "id": "ghv04-cancel", + "name": "cancelBtn", + "padding": [ + 6, + 16 + ], + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "ghv04-cancel-lbl", + "content": "Cancel", + "fontSize": 13, + "fill": "$--foreground" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_all", + "name": "Sidebar Collapse — All panels visible", + "x": 68, + "y": 8000, + "clip": true, + "width": 1440, + "height": 900, + "fill": "$--background", + "theme": { + "Mode": "Light" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "SC_all_title", + "name": "macOS Title Bar", + "width": "fill_container", + "height": 38, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "SC_all_tl", + "name": "trafficLights", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "SC_all_c", + "fill": "#FF5F57", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "SC_all_m", + "fill": "#FEBC2E", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "SC_all_x", + "fill": "#28C840", + "width": 12, + "height": 12 + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_all_content", + "name": "Content", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "SC_all_sb", + "name": "Panel: Sidebar", + "clip": true, + "width": 250, + "height": "fill_container", + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "SC_all_sb_hdr", + "name": "SidebarHeader", + "width": "fill_container", + "height": 32, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "justifyContent": "flex_end", + "children": [ + { + "type": "frame", + "id": "SC_all_sb_btn", + "name": "collapseBtn", + "width": 24, + "height": 24, + "cornerRadius": 4, + "fill": "transparent", + "alignItems": "center", + "justifyContent": "center", + "children": [ + { + "type": "icon_font", + "id": "SC_all_sb_btn_icon", + "name": "chevronIcon", + "width": 14, + "height": 14, + "iconFontName": "caret-left-bold", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_all_sb_nav", + "name": "NavItems", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "padding": [ + 4, + 8 + ], + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "SC_all_sb_t1", + "text": "All Notes", + "fontSize": 13, + "fill": "$--foreground", + "width": "fill_container", + "padding": [ + 6, + 12 + ] + }, + { + "type": "text", + "id": "SC_all_sb_t2", + "text": "Favorites", + "fontSize": 13, + "fill": "$--muted-foreground", + "width": "fill_container", + "padding": [ + 6, + 12 + ] + }, + { + "type": "text", + "id": "SC_all_sb_t3", + "text": "Archive", + "fontSize": 13, + "fill": "$--muted-foreground", + "width": "fill_container", + "padding": [ + 6, + 12 + ] + }, + { + "type": "text", + "id": "SC_all_sb_t4", + "text": "Trash", + "fontSize": 13, + "fill": "$--muted-foreground", + "width": "fill_container", + "padding": [ + 6, + 12 + ] + } + ] + }, + { + "type": "frame", + "id": "SC_all_sb_sec", + "name": "Sections", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 2, + "padding": [ + 8, + 8 + ], + "children": [ + { + "type": "text", + "id": "SC_all_sb_sh", + "text": "SECTIONS", + "fontSize": 11, + "fill": "$--muted-foreground", + "letterSpacing": 1.2, + "width": "fill_container", + "padding": [ + 4, + 12 + ] + }, + { + "type": "text", + "id": "SC_all_sb_s1", + "text": "Projects", + "fontSize": 13, + "fill": "$--foreground", + "width": "fill_container", + "padding": [ + 6, + 12 + ] + }, + { + "type": "text", + "id": "SC_all_sb_s2", + "text": "Experiments", + "fontSize": 13, + "fill": "$--muted-foreground", + "width": "fill_container", + "padding": [ + 6, + 12 + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_all_nl", + "name": "Panel: NoteList", + "clip": true, + "width": 300, + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "SC_all_nl_hdr", + "name": "NoteListHeader", + "width": "fill_container", + "height": 40, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "justifyContent": "space_between", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "SC_all_nl_title", + "text": "All Notes", + "fontSize": 14, + "fontWeight": 600, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "SC_all_nl_count", + "text": "42 notes", + "fontSize": 12, + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "SC_all_nl_list", + "name": "NoteItems", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 0, + "children": [ + { + "type": "frame", + "id": "SC_all_nl_n1", + "name": "noteItem1", + "width": "fill_container", + "padding": [ + 10, + 16 + ], + "fill": "$--accent", + "children": [ + { + "type": "text", + "id": "SC_all_nl_n1t", + "text": "My First Project", + "fontSize": 13, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "SC_all_nl_n2", + "name": "noteItem2", + "width": "fill_container", + "padding": [ + 10, + 16 + ], + "children": [ + { + "type": "text", + "id": "SC_all_nl_n2t", + "text": "Weekly Review", + "fontSize": 13, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "SC_all_nl_n3", + "name": "noteItem3", + "width": "fill_container", + "padding": [ + 10, + 16 + ], + "children": [ + { + "type": "text", + "id": "SC_all_nl_n3t", + "text": "Meeting Notes", + "fontSize": 13, + "fill": "$--foreground" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_all_ed", + "name": "Panel: Editor", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "SC_all_ed_tabs", + "name": "TabBar", + "width": "fill_container", + "height": 36, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "gap": 4, + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "fill": "$--background", + "children": [ + { + "type": "frame", + "id": "SC_all_ed_tab1", + "name": "activeTab", + "padding": [ + 6, + 12 + ], + "cornerRadius": [ + 6, + 6, + 0, + 0 + ], + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 2 + }, + "fill": "$--primary" + }, + "children": [ + { + "type": "text", + "id": "SC_all_ed_tab1t", + "text": "My First Project", + "fontSize": 12, + "fill": "$--foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_all_ed_content", + "name": "EditorContent", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 24, + 48 + ], + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "text", + "id": "SC_all_ed_h1", + "text": "My First Project", + "fontSize": 28, + "fontWeight": 700, + "fill": "$--foreground", + "width": "fill_container" + }, + { + "type": "text", + "id": "SC_all_ed_p1", + "text": "This is the editor area with full markdown editing support.", + "fontSize": 15, + "fill": "$--foreground", + "width": "fill_container", + "lineHeight": 1.6 + }, + { + "type": "text", + "id": "SC_all_ed_p2", + "text": "The editor expands to fill all available space when panels are collapsed.", + "fontSize": 15, + "fill": "$--muted-foreground", + "width": "fill_container", + "lineHeight": 1.6 + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_nosb", + "name": "Sidebar Collapse — Sidebar collapsed", + "x": 68, + "y": 9000, + "clip": true, + "width": 1440, + "height": 900, + "fill": "$--background", + "theme": { + "Mode": "Light" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "SC_nosb_title", + "name": "macOS Title Bar", + "width": "fill_container", + "height": 38, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "SC_nosb_tl", + "name": "trafficLights", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "SC_nosb_c", + "fill": "#FF5F57", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "SC_nosb_m", + "fill": "#FEBC2E", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "SC_nosb_x", + "fill": "#28C840", + "width": 12, + "height": 12 + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_nosb_content", + "name": "Content", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "SC_nosb_nl", + "name": "Panel: NoteList", + "clip": true, + "width": 300, + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "SC_nosb_nl_hdr", + "name": "NoteListHeader", + "width": "fill_container", + "height": 40, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "justifyContent": "space_between", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "SC_nosb_nl_title", + "text": "All Notes", + "fontSize": 14, + "fontWeight": 600, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "SC_nosb_nl_count", + "text": "42 notes", + "fontSize": 12, + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "SC_nosb_nl_list", + "name": "NoteItems", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 0, + "children": [ + { + "type": "frame", + "id": "SC_nosb_nl_n1", + "name": "noteItem1", + "width": "fill_container", + "padding": [ + 10, + 16 + ], + "fill": "$--accent", + "children": [ + { + "type": "text", + "id": "SC_nosb_nl_n1t", + "text": "My First Project", + "fontSize": 13, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "SC_nosb_nl_n2", + "name": "noteItem2", + "width": "fill_container", + "padding": [ + 10, + 16 + ], + "children": [ + { + "type": "text", + "id": "SC_nosb_nl_n2t", + "text": "Weekly Review", + "fontSize": 13, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "SC_nosb_nl_n3", + "name": "noteItem3", + "width": "fill_container", + "padding": [ + 10, + 16 + ], + "children": [ + { + "type": "text", + "id": "SC_nosb_nl_n3t", + "text": "Meeting Notes", + "fontSize": 13, + "fill": "$--foreground" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_nosb_ed", + "name": "Panel: Editor", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "SC_nosb_ed_tabs", + "name": "TabBar", + "width": "fill_container", + "height": 36, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "gap": 4, + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "fill": "$--background", + "children": [ + { + "type": "frame", + "id": "SC_nosb_ed_tab1", + "name": "activeTab", + "padding": [ + 6, + 12 + ], + "cornerRadius": [ + 6, + 6, + 0, + 0 + ], + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 2 + }, + "fill": "$--primary" + }, + "children": [ + { + "type": "text", + "id": "SC_nosb_ed_tab1t", + "text": "My First Project", + "fontSize": 12, + "fill": "$--foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_nosb_ed_content", + "name": "EditorContent", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 24, + 48 + ], + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "text", + "id": "SC_nosb_ed_h1", + "text": "My First Project", + "fontSize": 28, + "fontWeight": 700, + "fill": "$--foreground", + "width": "fill_container" + }, + { + "type": "text", + "id": "SC_nosb_ed_p1", + "text": "This is the editor area with full markdown editing support.", + "fontSize": 15, + "fill": "$--foreground", + "width": "fill_container", + "lineHeight": 1.6 + }, + { + "type": "text", + "id": "SC_nosb_ed_p2", + "text": "The editor expands to fill all available space when panels are collapsed.", + "fontSize": 15, + "fill": "$--muted-foreground", + "width": "fill_container", + "lineHeight": 1.6 + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_edonly", + "name": "Sidebar Collapse — Editor only", + "x": 68, + "y": 10000, + "clip": true, + "width": 1440, + "height": 900, + "fill": "$--background", + "theme": { + "Mode": "Light" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "SC_edonly_title", + "name": "macOS Title Bar", + "width": "fill_container", + "height": 38, + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "SC_edonly_tl", + "name": "trafficLights", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "SC_edonly_c", + "fill": "#FF5F57", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "SC_edonly_m", + "fill": "#FEBC2E", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "SC_edonly_x", + "fill": "#28C840", + "width": 12, + "height": 12 + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_edonly_content", + "name": "Content", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "SC_edonly_ed", + "name": "Panel: Editor", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "SC_edonly_ed_tabs", + "name": "TabBar", + "width": "fill_container", + "height": 36, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "gap": 4, + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "fill": "$--background", + "children": [ + { + "type": "frame", + "id": "SC_edonly_ed_tab1", + "name": "activeTab", + "padding": [ + 6, + 12 + ], + "cornerRadius": [ + 6, + 6, + 0, + 0 + ], + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 2 + }, + "fill": "$--primary" + }, + "children": [ + { + "type": "text", + "id": "SC_edonly_ed_tab1t", + "text": "My First Project", + "fontSize": 12, + "fill": "$--foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_edonly_ed_content", + "name": "EditorContent", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 24, + 48 + ], + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "text", + "id": "SC_edonly_ed_h1", + "text": "My First Project", + "fontSize": 28, + "fontWeight": 700, + "fill": "$--foreground", + "width": "fill_container" + }, + { + "type": "text", + "id": "SC_edonly_ed_p1", + "text": "This is the editor area with full markdown editing support.", + "fontSize": 15, + "fill": "$--foreground", + "width": "fill_container", + "lineHeight": 1.6 + }, + { + "type": "text", + "id": "SC_edonly_ed_p2", + "text": "The editor expands to fill all available space when panels are collapsed.", + "fontSize": 15, + "fill": "$--muted-foreground", + "width": "fill_container", + "lineHeight": 1.6 + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_ednl", + "name": "Sidebar Collapse — Editor + notes", + "x": 68, + "y": 11000, + "clip": true, + "width": 1440, + "height": 900, + "fill": "$--background", + "theme": { + "Mode": "Light" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "SC_ednl_title", + "name": "macOS Title Bar", + "width": "fill_container", + "height": 38, + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "SC_ednl_tl", + "name": "trafficLights", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "SC_ednl_c", + "fill": "#FF5F57", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "SC_ednl_m", + "fill": "#FEBC2E", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "SC_ednl_x", + "fill": "#28C840", + "width": 12, + "height": 12 + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_ednl_content", + "name": "Content", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "SC_ednl_nl", + "name": "Panel: NoteList", + "clip": true, + "width": 300, + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "SC_ednl_nl_hdr", + "name": "NoteListHeader", + "width": "fill_container", + "height": 40, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "justifyContent": "space_between", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "SC_ednl_nl_title", + "text": "All Notes", + "fontSize": 14, + "fontWeight": 600, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "SC_ednl_nl_count", + "text": "42 notes", + "fontSize": 12, + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "SC_ednl_nl_list", + "name": "NoteItems", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 0, + "children": [ + { + "type": "frame", + "id": "SC_ednl_nl_n1", + "name": "noteItem1", + "width": "fill_container", + "padding": [ + 10, + 16 + ], + "fill": "$--accent", + "children": [ + { + "type": "text", + "id": "SC_ednl_nl_n1t", + "text": "My First Project", + "fontSize": 13, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "SC_ednl_nl_n2", + "name": "noteItem2", + "width": "fill_container", + "padding": [ + 10, + 16 + ], + "children": [ + { + "type": "text", + "id": "SC_ednl_nl_n2t", + "text": "Weekly Review", + "fontSize": 13, + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "SC_ednl_nl_n3", + "name": "noteItem3", + "width": "fill_container", + "padding": [ + 10, + 16 + ], + "children": [ + { + "type": "text", + "id": "SC_ednl_nl_n3t", + "text": "Meeting Notes", + "fontSize": 13, + "fill": "$--foreground" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_ednl_ed", + "name": "Panel: Editor", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "SC_ednl_ed_tabs", + "name": "TabBar", + "width": "fill_container", + "height": 36, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "gap": 4, + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "fill": "$--background", + "children": [ + { + "type": "frame", + "id": "SC_ednl_ed_tab1", + "name": "activeTab", + "padding": [ + 6, + 12 + ], + "cornerRadius": [ + 6, + 6, + 0, + 0 + ], + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 2 + }, + "fill": "$--primary" + }, + "children": [ + { + "type": "text", + "id": "SC_ednl_ed_tab1t", + "text": "My First Project", + "fontSize": 12, + "fill": "$--foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "SC_ednl_ed_content", + "name": "EditorContent", + "width": "fill_container", + "height": "fill_container", + "padding": [ + 24, + 48 + ], + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "text", + "id": "SC_ednl_ed_h1", + "text": "My First Project", + "fontSize": 28, + "fontWeight": 700, + "fill": "$--foreground", + "width": "fill_container" + }, + { + "type": "text", + "id": "SC_ednl_ed_p1", + "text": "This is the editor area with full markdown editing support.", + "fontSize": 15, + "fill": "$--foreground", + "width": "fill_container", + "lineHeight": 1.6 + }, + { + "type": "text", + "id": "SC_ednl_ed_p2", + "text": "The editor expands to fill all available space when panels are collapsed.", + "fontSize": 15, + "fill": "$--muted-foreground", + "width": "fill_container", + "lineHeight": 1.6 + } + ] + } + ] + } + ] + } + ] + } + ], + "themes": { + "Mode": [ + "Dark" + ] + }, + "variables": { + "--accent": { + "type": "color", + "value": [ + { + "value": "#EBEBEA" + }, + { + "value": "#252545", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-blue": { + "type": "color", + "value": [ + { + "value": "#2383E2" + }, + { + "value": "#4a9eff", + "theme": { + "Mode": "Dark" + } + }, + { + "value": "#155DFF" + }, + { + "value": "#155DFF", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-foreground": { + "type": "color", + "value": [ + { + "value": "#37352F" + }, + { + "value": "#ffffff", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-green": { + "type": "color", + "value": [ + { + "value": "#0F7B6C" + }, + { + "value": "#4caf50", + "theme": { + "Mode": "Dark" + } + }, + { + "value": "#00B38B" + }, + { + "value": "#00B38B", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-orange": { + "type": "color", + "value": [ + { + "value": "#D9730D" + }, + { + "value": "#ff9800", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-purple": { + "type": "color", + "value": [ + { + "value": "#9065B0" + }, + { + "value": "#9c72ff", + "theme": { + "Mode": "Dark" + } + }, + { + "value": "#A932FF" + }, + { + "value": "#A932FF", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-red": { + "type": "color", + "value": [ + { + "value": "#E03E3E" + }, + { + "value": "#f44336", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--background": { + "type": "color", + "value": [ + { + "value": "#FFFFFF" + }, + { + "value": "#0f0f1a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--black": { + "type": "color", + "value": "#000000" + }, + "--border": { + "type": "color", + "value": [ + { + "value": "#E9E9E7" + }, + { + "value": "#2a2a4a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--card": { + "type": "color", + "value": [ + { + "value": "#FFFFFF" + }, + { + "value": "#16162a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--card-foreground": { + "type": "color", + "value": [ + { + "value": "#37352F" + }, + { + "value": "#e0e0e0", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--destructive": { + "type": "color", + "value": [ + { + "value": "#E03E3E" + }, + { + "value": "#f44336", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--destructive-foreground": { + "type": "color", + "value": [ + { + "value": "#FFFFFF" + }, + { + "value": "#ffffff", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--font-primary": { + "type": "string", + "value": [ + { + "value": "Inter" + }, + { + "value": "-apple-system, BlinkMacSystemFont, Inter, sans-serif" + }, + { + "value": "Inter" + } + ] + }, + "--font-system": { + "type": "string", + "value": "-apple-system, BlinkMacSystemFont, sans-serif" + }, + "--foreground": { + "type": "color", + "value": [ + { + "value": "#37352F" + }, + { + "value": "#e0e0e0", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--input": { + "type": "color", + "value": [ + { + "value": "#E9E9E7" + }, + { + "value": "#2a2a4a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--muted": { + "type": "color", + "value": [ + { + "value": "#F0F0EF" + }, + { + "value": "#1e1e3a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--muted-foreground": { + "type": "color", + "value": [ + { + "value": "#787774" + }, + { + "value": "#888888", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--popover": { + "type": "color", + "value": [ + { + "value": "#FFFFFF" + }, + { + "value": "#1e1e3a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--popover-foreground": { + "type": "color", + "value": [ + { + "value": "#37352F" + }, + { + "value": "#e0e0e0", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--primary": { + "type": "color", + "value": [ + { + "value": "#2383E2" + }, + { + "value": "#4a9eff", + "theme": { + "Mode": "Dark" + } + }, + { + "value": "#155DFF" + }, + { + "value": "#155DFF", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--primary-foreground": { + "type": "color", + "value": [ + { + "value": "#FFFFFF" + }, + { + "value": "#ffffff", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--radius-lg": { + "type": "number", + "value": 8 + }, + "--radius-md": { + "type": "number", + "value": 6 + }, + "--radius-sm": { + "type": "number", + "value": 4 + }, + "--ring": { + "type": "color", + "value": [ + { + "value": "#2383E2" + }, + { + "value": "#4a9eff", + "theme": { + "Mode": "Dark" + } + }, + { + "value": "#155DFF" + }, + { + "value": "#155DFF", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--secondary": { + "type": "color", + "value": [ + { + "value": "#EBEBEA" + }, + { + "value": "#2a2a4a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--secondary-foreground": { + "type": "color", + "value": [ + { + "value": "#37352F" + }, + { + "value": "#e0e0e0", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar": { + "type": "color", + "value": [ + { + "value": "#F7F6F3" + }, + { + "value": "#1a1a2e", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar-accent": { + "type": "color", + "value": [ + { + "value": "#EBEBEA" + }, + { + "value": "#252545", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar-accent-foreground": { + "type": "color", + "value": [ + { + "value": "#37352F" + }, + { + "value": "#ffffff", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar-border": { + "type": "color", + "value": [ + { + "value": "#E9E9E7" + }, + { + "value": "#2a2a4a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar-foreground": { + "type": "color", + "value": [ + { + "value": "#37352F" + }, + { + "value": "#e0e0e0", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar-primary": { + "type": "color", + "value": [ + { + "value": "#2383E2" + }, + { + "value": "#4a9eff", + "theme": { + "Mode": "Dark" + } + }, + { + "value": "#155DFF" + }, + { + "value": "#155DFF", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar-primary-foreground": { + "type": "color", + "value": [ + { + "value": "#FFFFFF" + }, + { + "value": "#ffffff", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar-ring": { + "type": "color", + "value": [ + { + "value": "#2383E2" + }, + { + "value": "#4a9eff", + "theme": { + "Mode": "Dark" + } + }, + { + "value": "#155DFF" + }, + { + "value": "#155DFF", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--white": { + "type": "color", + "value": "#ffffff" + }, + "--accent-yellow": { + "type": "color", + "value": [ + { + "value": "#F0B100" + }, + { + "value": "#F0B100", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-blue-light": { + "type": "color", + "value": [ + { + "value": "#155DFF14" + }, + { + "value": "#155DFF20", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-green-light": { + "type": "color", + "value": [ + { + "value": "#00B38B14" + }, + { + "value": "#00B38B20", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-purple-light": { + "type": "color", + "value": [ + { + "value": "#A932FF14" + }, + { + "value": "#A932FF20", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-red-light": { + "type": "color", + "value": [ + { + "value": "#E03E3E14" + }, + { + "value": "#f4433620", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-yellow-light": { + "type": "color", + "value": [ + { + "value": "#F0B10014" + }, + { + "value": "#F0B10020", + "theme": { + "Mode": "Dark" + } + } + ] + } + }, + "fonts": [ + { + "name": "GT Pressura Trial", + "url": "GT-Pressura-Regular-Trial.otf" + } + ] +} \ No newline at end of file From 62860b3118e30c3de04425e1582f2ba92eab321a Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 23 Feb 2026 10:47:13 +0100 Subject: [PATCH 6/8] docs: update ARCHITECTURE.md with release/update system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive documentation for: - Release pipeline (4-phase workflow: version → build → release → pages) - Versioning scheme (0.YYYYMMDD.RUN_NUMBER) - Universal binary strategy (lipo merge) - Updater endpoint and latest.json manifest - In-app update UI state machine - GitHub Pages release history site Co-Authored-By: Claude Opus 4.6 --- docs/ARCHITECTURE.md | 64 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d226574b..d6e9393d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -288,3 +288,67 @@ Data flows unidirectionally: `App` passes data and callbacks as props to child c | Cmd+S | Show "Saved" toast | | Cmd+W | Close active tab | | `[[` in editor | Open wikilink suggestion menu | + +## Auto-Release & In-App Updates + +### Release Pipeline + +Every push to `main` triggers `.github/workflows/release.yml`: + +``` +push to main + → version job: compute 0.YYYYMMDD.RUN_NUMBER + → build job (matrix: aarch64 + x86_64): + → pnpm install, stamp version, pnpm build, tauri build --target + → upload .app, .tar.gz + .sig, .dmg as artifacts + → release job: + → download both arch artifacts + → lipo aarch64 + x86_64 → universal binary + → create universal .dmg + signed updater tarball + → generate latest.json (per-arch + universal platform entries) + → publish GitHub Release with all assets + auto-generated notes + → pages job: + → fetch all releases via gh api + → build static HTML release history page + → deploy to gh-pages via peaceiris/actions-gh-pages +``` + +### Versioning + +Format: `0.YYYYMMDD.GITHUB_RUN_NUMBER` (e.g. `0.20260223.42`). The `0.` prefix keeps it SemVer-compatible while making it clear these are date-based auto-releases. The version is stamped into both `tauri.conf.json` and `Cargo.toml` dynamically in the workflow. + +### Universal Binary + +macOS builds produce both `aarch64-apple-darwin` and `x86_64-apple-darwin` in parallel. The release job merges them with `lipo` — copying the arm64 `.app` as the base and replacing only the main executable with a universal fat binary. The per-arch updater tarballs are also uploaded so the Tauri updater downloads only the relevant architecture (smaller download). + +### Updater Endpoint + +The Tauri updater plugin is configured to fetch: +``` +https://github.com/refactoringhq/laputa-app/releases/latest/download/latest.json +``` + +This JSON manifest contains `version`, `pub_date`, `notes`, and per-platform entries (`darwin-aarch64`, `darwin-x86_64`) with `url` and `signature` fields. The updater compares the manifest version against the running app version, downloads the matching platform artifact, verifies the signature, and installs it. + +### In-App Update UI + +``` +App startup (3s delay) + → useUpdater.check() + → idle (no update) → no UI shown + → available → UpdateBanner: "Laputa X.Y.Z is available" + Release Notes + Update Now + X + → user clicks Update Now → downloading → progress bar + → download complete → ready → "Restart to apply" + Restart Now button + → user clicks Restart → relaunch() + → network error / 404 → fail silently, no UI +``` + +| Component | File | Purpose | +|-----------|------|---------| +| `useUpdater` | `src/hooks/useUpdater.ts` | State machine: idle → available → downloading → ready → error | +| `UpdateBanner` | `src/components/UpdateBanner.tsx` | Top-of-app notification bar | +| `restartApp` | `src/hooks/useUpdater.ts` | Calls `@tauri-apps/plugin-process` relaunch | + +### GitHub Pages + +Release history site at `https://refactoringhq.github.io/laputa-app/`. Auto-updated by the workflow after each release. The page loads `releases.json` (deployed alongside) and renders each release with date, notes, and `.dmg` download links. Linked from the in-app "Release Notes" button. From 61a596f777ff7a2f92ef2c4a1c48d4cec9f1e7cd Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 23 Feb 2026 11:28:11 +0100 Subject: [PATCH 7/8] fix: rustfmt formatting --- src-tauri/src/ai_chat.rs | 13 +- src-tauri/src/frontmatter.rs | 168 +++++++++--- src-tauri/src/git.rs | 52 ++-- src-tauri/src/github.rs | 48 +++- src-tauri/src/lib.rs | 31 ++- src-tauri/src/main.rs | 2 +- src-tauri/src/menu.rs | 9 +- src-tauri/src/settings.rs | 54 +++- src-tauri/src/vault.rs | 497 +++++++++++++++++++++++++---------- 9 files changed, 628 insertions(+), 246 deletions(-) diff --git a/src-tauri/src/ai_chat.rs b/src-tauri/src/ai_chat.rs index 8da08a2d..b9a3f83a 100644 --- a/src-tauri/src/ai_chat.rs +++ b/src-tauri/src/ai_chat.rs @@ -49,7 +49,10 @@ fn get_api_key() -> Result { fn build_request(req: &AiChatRequest) -> AnthropicRequest { AnthropicRequest { - model: req.model.clone().unwrap_or_else(|| "claude-3-5-haiku-20241022".to_string()), + model: req + .model + .clone() + .unwrap_or_else(|| "claude-3-5-haiku-20241022".to_string()), max_tokens: req.max_tokens.unwrap_or(4096), messages: req.messages.clone(), system: req.system.clone(), @@ -137,8 +140,12 @@ mod tests { fn test_extract_response_text() { let resp = AnthropicResponse { content: vec![ - ContentBlock { text: Some("Hello ".to_string()) }, - ContentBlock { text: Some("world".to_string()) }, + ContentBlock { + text: Some("Hello ".to_string()), + }, + ContentBlock { + text: Some("world".to_string()), + }, ContentBlock { text: None }, ], model: "test".to_string(), diff --git a/src-tauri/src/frontmatter.rs b/src-tauri/src/frontmatter.rs index 2c1e9c91..f0990d06 100644 --- a/src-tauri/src/frontmatter.rs +++ b/src-tauri/src/frontmatter.rs @@ -54,14 +54,20 @@ impl FrontmatterValue { pub fn to_yaml_value(&self) -> String { match self { FrontmatterValue::String(s) => { - if needs_yaml_quoting(s) { quote_yaml_string(s) } else { s.clone() } + if needs_yaml_quoting(s) { + quote_yaml_string(s) + } else { + s.clone() + } } FrontmatterValue::Number(n) => format_yaml_number(*n), FrontmatterValue::Bool(b) => if *b { "true" } else { "false" }.to_string(), FrontmatterValue::List(items) if items.is_empty() => "[]".to_string(), - FrontmatterValue::List(items) => { - items.iter().map(|item| format_list_item(item)).collect::>().join("\n") - } + FrontmatterValue::List(items) => items + .iter() + .map(|item| format_list_item(item)) + .collect::>() + .join("\n"), FrontmatterValue::Null => "null".to_string(), } } @@ -69,7 +75,8 @@ impl FrontmatterValue { /// Check whether a YAML key needs quoting (contains spaces, special chars, etc.). fn needs_key_quoting(key: &str) -> bool { - key.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '-') + key.chars() + .any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '-') } /// Format a key for YAML output (quote if necessary) @@ -84,21 +91,21 @@ pub fn format_yaml_key(key: &str) -> String { /// Check if a line defines a specific key (handles quoted and unquoted keys) fn line_is_key(line: &str, key: &str) -> bool { let trimmed = line.trim_start(); - + if trimmed.starts_with(key) && trimmed[key.len()..].starts_with(':') { return true; } - + let dq = format!("\"{}\":", key); if trimmed.starts_with(&dq) { return true; } - + let sq = format!("'{}\':", key); if trimmed.starts_with(&sq) { return true; } - + false } @@ -121,7 +128,8 @@ fn is_list_continuation(line: &str) -> bool { /// Split content into frontmatter body and the rest after the closing `---`. /// Returns `(fm_content, rest)` where `fm_content` is between the opening and closing `---`. fn split_frontmatter(content: &str) -> Result<(&str, &str), String> { - let fm_end = content[4..].find("\n---") + let fm_end = content[4..] + .find("\n---") .map(|i| i + 4) .ok_or_else(|| "Malformed frontmatter: no closing ---".to_string())?; Ok((&content[4..fm_end], &content[fm_end + 4..])) @@ -168,7 +176,11 @@ fn apply_field_update(lines: &[&str], key: &str, value: Option<&FrontmatterValue } /// Internal function to update frontmatter content -pub fn update_frontmatter_content(content: &str, key: &str, value: Option) -> Result { +pub fn update_frontmatter_content( + content: &str, + key: &str, + value: Option, +) -> Result { if !content.starts_with("---\n") { return match value { Some(v) => Ok(prepend_new_frontmatter(content, key, &v)), @@ -192,15 +204,14 @@ where if !file_path.exists() { return Err(format!("File does not exist: {}", path)); } - - let content = fs::read_to_string(file_path) - .map_err(|e| format!("Failed to read {}: {}", path, e))?; - + + let content = + fs::read_to_string(file_path).map_err(|e| format!("Failed to read {}: {}", path, e))?; + let updated = transform(&content)?; - - fs::write(file_path, &updated) - .map_err(|e| format!("Failed to write {}: {}", path, e))?; - + + fs::write(file_path, &updated).map_err(|e| format!("Failed to write {}: {}", path, e))?; + Ok(updated) } @@ -211,7 +222,12 @@ mod tests { #[test] fn test_update_frontmatter_string() { let content = "---\nStatus: Draft\n---\n# Test\n"; - let updated = update_frontmatter_content(content, "Status", Some(FrontmatterValue::String("Active".to_string()))).unwrap(); + let updated = update_frontmatter_content( + content, + "Status", + Some(FrontmatterValue::String("Active".to_string())), + ) + .unwrap(); assert!(updated.contains("Status: Active")); assert!(!updated.contains("Status: Draft")); } @@ -219,7 +235,12 @@ mod tests { #[test] fn test_update_frontmatter_add_new_key() { let content = "---\nStatus: Draft\n---\n# Test\n"; - let updated = update_frontmatter_content(content, "Owner", Some(FrontmatterValue::String("Luca".to_string()))).unwrap(); + let updated = update_frontmatter_content( + content, + "Owner", + Some(FrontmatterValue::String("Luca".to_string())), + ) + .unwrap(); assert!(updated.contains("Owner: Luca")); assert!(updated.contains("Status: Draft")); } @@ -227,7 +248,12 @@ mod tests { #[test] fn test_update_frontmatter_quoted_key() { let content = "---\n\"Is A\": Note\n---\n# Test\n"; - let updated = update_frontmatter_content(content, "Is A", Some(FrontmatterValue::String("Project".to_string()))).unwrap(); + let updated = update_frontmatter_content( + content, + "Is A", + Some(FrontmatterValue::String("Project".to_string())), + ) + .unwrap(); assert!(updated.contains("\"Is A\": Project")); assert!(!updated.contains("Note")); } @@ -235,7 +261,15 @@ mod tests { #[test] fn test_update_frontmatter_list() { let content = "---\nStatus: Draft\n---\n# Test\n"; - let updated = update_frontmatter_content(content, "aliases", Some(FrontmatterValue::List(vec!["Alias1".to_string(), "Alias2".to_string()]))).unwrap(); + let updated = update_frontmatter_content( + content, + "aliases", + Some(FrontmatterValue::List(vec![ + "Alias1".to_string(), + "Alias2".to_string(), + ])), + ) + .unwrap(); assert!(updated.contains("aliases:")); assert!(updated.contains(" - \"Alias1\"")); assert!(updated.contains(" - \"Alias2\"")); @@ -244,7 +278,12 @@ mod tests { #[test] fn test_update_frontmatter_replace_list() { let content = "---\naliases:\n - Old1\n - Old2\nStatus: Draft\n---\n# Test\n"; - let updated = update_frontmatter_content(content, "aliases", Some(FrontmatterValue::List(vec!["New1".to_string()]))).unwrap(); + let updated = update_frontmatter_content( + content, + "aliases", + Some(FrontmatterValue::List(vec!["New1".to_string()])), + ) + .unwrap(); assert!(updated.contains(" - \"New1\"")); assert!(!updated.contains("Old1")); assert!(!updated.contains("Old2")); @@ -271,7 +310,12 @@ mod tests { #[test] fn test_update_frontmatter_no_existing() { let content = "# Test\n\nSome content here."; - let updated = update_frontmatter_content(content, "Status", Some(FrontmatterValue::String("Draft".to_string()))).unwrap(); + let updated = update_frontmatter_content( + content, + "Status", + Some(FrontmatterValue::String("Draft".to_string())), + ) + .unwrap(); assert!(updated.starts_with("---\n")); assert!(updated.contains("Status: Draft")); assert!(updated.contains("# Test")); @@ -280,7 +324,9 @@ mod tests { #[test] fn test_update_frontmatter_bool() { let content = "---\nStatus: Draft\n---\n# Test\n"; - let updated = update_frontmatter_content(content, "Reviewed", Some(FrontmatterValue::Bool(true))).unwrap(); + let updated = + update_frontmatter_content(content, "Reviewed", Some(FrontmatterValue::Bool(true))) + .unwrap(); assert!(updated.contains("Reviewed: true")); } @@ -324,19 +370,34 @@ mod tests { #[test] fn test_to_yaml_value_string_needs_quoting_bool_like() { - assert_eq!(FrontmatterValue::String("true".to_string()).to_yaml_value(), "\"true\""); - assert_eq!(FrontmatterValue::String("false".to_string()).to_yaml_value(), "\"false\""); + assert_eq!( + FrontmatterValue::String("true".to_string()).to_yaml_value(), + "\"true\"" + ); + assert_eq!( + FrontmatterValue::String("false".to_string()).to_yaml_value(), + "\"false\"" + ); } #[test] fn test_to_yaml_value_string_needs_quoting_null_like() { - assert_eq!(FrontmatterValue::String("null".to_string()).to_yaml_value(), "\"null\""); + assert_eq!( + FrontmatterValue::String("null".to_string()).to_yaml_value(), + "\"null\"" + ); } #[test] fn test_to_yaml_value_string_needs_quoting_number_like() { - assert_eq!(FrontmatterValue::String("42".to_string()).to_yaml_value(), "\"42\""); - assert_eq!(FrontmatterValue::String("3.14".to_string()).to_yaml_value(), "\"3.14\""); + assert_eq!( + FrontmatterValue::String("42".to_string()).to_yaml_value(), + "\"42\"" + ); + assert_eq!( + FrontmatterValue::String("3.14".to_string()).to_yaml_value(), + "\"3.14\"" + ); } #[test] @@ -379,35 +440,46 @@ mod tests { #[test] fn test_update_frontmatter_number() { let content = "---\nStatus: Draft\n---\n# Test\n"; - let updated = update_frontmatter_content(content, "Priority", Some(FrontmatterValue::Number(5.0))).unwrap(); + let updated = + update_frontmatter_content(content, "Priority", Some(FrontmatterValue::Number(5.0))) + .unwrap(); assert!(updated.contains("Priority: 5")); } #[test] fn test_update_frontmatter_number_float() { let content = "---\nStatus: Draft\n---\n# Test\n"; - let updated = update_frontmatter_content(content, "Score", Some(FrontmatterValue::Number(9.5))).unwrap(); + let updated = + update_frontmatter_content(content, "Score", Some(FrontmatterValue::Number(9.5))) + .unwrap(); assert!(updated.contains("Score: 9.5")); } #[test] fn test_update_frontmatter_null() { let content = "---\nStatus: Draft\n---\n# Test\n"; - let updated = update_frontmatter_content(content, "ClearMe", Some(FrontmatterValue::Null)).unwrap(); + let updated = + update_frontmatter_content(content, "ClearMe", Some(FrontmatterValue::Null)).unwrap(); assert!(updated.contains("ClearMe: null")); } #[test] fn test_update_frontmatter_empty_list() { let content = "---\nStatus: Draft\n---\n# Test\n"; - let updated = update_frontmatter_content(content, "tags", Some(FrontmatterValue::List(vec![]))).unwrap(); + let updated = + update_frontmatter_content(content, "tags", Some(FrontmatterValue::List(vec![]))) + .unwrap(); assert!(updated.contains("tags: []")); } #[test] fn test_update_frontmatter_malformed_no_closing_fence() { let content = "---\nStatus: Draft\nNo closing fence here"; - let result = update_frontmatter_content(content, "Status", Some(FrontmatterValue::String("Active".to_string()))); + let result = update_frontmatter_content( + content, + "Status", + Some(FrontmatterValue::String("Active".to_string())), + ); assert!(result.is_err()); assert!(result.unwrap_err().contains("Malformed frontmatter")); } @@ -472,7 +544,12 @@ mod tests { #[test] fn test_roundtrip_update_string() { let content = "---\nStatus: Draft\n---\n# Test\n"; - let updated = update_frontmatter_content(content, "Status", Some(FrontmatterValue::String("Active".to_string()))).unwrap(); + let updated = update_frontmatter_content( + content, + "Status", + Some(FrontmatterValue::String("Active".to_string())), + ) + .unwrap(); // Parse back with gray_matter let matter = gray_matter::Matter::::new(); let parsed = matter.parse(&updated); @@ -487,7 +564,15 @@ mod tests { #[test] fn test_roundtrip_update_list() { let content = "---\nStatus: Draft\n---\n# Test\n"; - let updated = update_frontmatter_content(content, "aliases", Some(FrontmatterValue::List(vec!["A".to_string(), "B".to_string()]))).unwrap(); + let updated = update_frontmatter_content( + content, + "aliases", + Some(FrontmatterValue::List(vec![ + "A".to_string(), + "B".to_string(), + ])), + ) + .unwrap(); let matter = gray_matter::Matter::::new(); let parsed = matter.parse(&updated); let data = parsed.data.unwrap(); @@ -508,7 +593,12 @@ mod tests { #[test] fn test_roundtrip_add_then_delete() { let content = "---\nStatus: Draft\n---\n# Test\n"; - let with_owner = update_frontmatter_content(content, "Owner", Some(FrontmatterValue::String("Luca".to_string()))).unwrap(); + let with_owner = update_frontmatter_content( + content, + "Owner", + Some(FrontmatterValue::String("Luca".to_string())), + ) + .unwrap(); assert!(with_owner.contains("Owner: Luca")); let without_owner = update_frontmatter_content(&with_owner, "Owner", None).unwrap(); assert!(!without_owner.contains("Owner")); diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index 89e037b4..d7d200c1 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -181,8 +181,8 @@ pub fn get_file_diff(vault_path: &str, file_path: &str) -> Result = content.lines().map(|l| format!("+{}", l)).collect(); return Ok(format!( "diff --git a/{0} b/{0}\nnew file\n--- /dev/null\n+++ b/{0}\n@@ -0,0 +1,{1} @@\n{2}", @@ -197,7 +197,11 @@ pub fn get_file_diff(vault_path: &str, file_path: &str) -> Result Result { +pub fn get_file_diff_at_commit( + vault_path: &str, + file_path: &str, + commit_hash: &str, +) -> Result { let vault = Path::new(vault_path); let file = Path::new(file_path); @@ -211,7 +215,13 @@ pub fn get_file_diff_at_commit(vault_path: &str, file_path: &str, commit_hash: & // Show diff between commit^ and commit for this file let output = Command::new("git") - .args(["diff", &format!("{}^", commit_hash), commit_hash, "--", relative_str]) + .args([ + "diff", + &format!("{}^", commit_hash), + commit_hash, + "--", + relative_str, + ]) .current_dir(vault) .output() .map_err(|e| format!("Failed to run git diff: {}", e))?; @@ -356,11 +366,7 @@ mod tests { .output() .unwrap(); - let history = get_file_history( - vault.to_str().unwrap(), - file.to_str().unwrap(), - ) - .unwrap(); + let history = get_file_history(vault.to_str().unwrap(), file.to_str().unwrap()).unwrap(); assert_eq!(history.len(), 2); assert_eq!(history[0].message, "Update test"); @@ -377,11 +383,7 @@ mod tests { let file = vault.join("new.md"); fs::write(&file, "# New\n").unwrap(); - let history = get_file_history( - vault.to_str().unwrap(), - file.to_str().unwrap(), - ) - .unwrap(); + let history = get_file_history(vault.to_str().unwrap(), file.to_str().unwrap()).unwrap(); assert!(history.is_empty()); } @@ -436,11 +438,7 @@ mod tests { fs::write(&file, "# Test\n\nModified content.").unwrap(); - let diff = get_file_diff( - vault.to_str().unwrap(), - file.to_str().unwrap(), - ) - .unwrap(); + let diff = get_file_diff(vault.to_str().unwrap(), file.to_str().unwrap()).unwrap(); assert!(!diff.is_empty()); assert!(diff.contains("-Original content.")); @@ -485,12 +483,8 @@ mod tests { .unwrap(); let hash = String::from_utf8_lossy(&log.stdout).trim().to_string(); - let diff = get_file_diff_at_commit( - vault.to_str().unwrap(), - file.to_str().unwrap(), - &hash, - ) - .unwrap(); + let diff = get_file_diff_at_commit(vault.to_str().unwrap(), file.to_str().unwrap(), &hash) + .unwrap(); assert!(!diff.is_empty()); assert!(diff.contains("-Original content.")); @@ -522,12 +516,8 @@ mod tests { .unwrap(); let hash = String::from_utf8_lossy(&log.stdout).trim().to_string(); - let diff = get_file_diff_at_commit( - vault.to_str().unwrap(), - file.to_str().unwrap(), - &hash, - ) - .unwrap(); + let diff = get_file_diff_at_commit(vault.to_str().unwrap(), file.to_str().unwrap(), &hash) + .unwrap(); assert!(!diff.is_empty()); assert!(diff.contains("+# Initial")); diff --git a/src-tauri/src/github.rs b/src-tauri/src/github.rs index c66938e2..cf5429a3 100644 --- a/src-tauri/src/github.rs +++ b/src-tauri/src/github.rs @@ -125,7 +125,12 @@ pub async fn github_create_repo( pub fn clone_repo(url: &str, token: &str, local_path: &str) -> Result { let dest = Path::new(local_path); - if dest.exists() && dest.read_dir().map(|mut d| d.next().is_some()).unwrap_or(false) { + if dest.exists() + && dest + .read_dir() + .map(|mut d| d.next().is_some()) + .unwrap_or(false) + { return Err(format!( "Destination '{}' already exists and is not empty", local_path @@ -163,7 +168,10 @@ fn inject_token_into_url(url: &str, token: &str) -> Result { // Handle URLs that already have a host Ok(format!("https://oauth2:{}@{}", token, rest)) } else { - Err(format!("Unsupported URL format: {}. Use an HTTPS URL.", url)) + Err(format!( + "Unsupported URL format: {}. Use an HTTPS URL.", + url + )) } } @@ -245,7 +253,11 @@ mod tests { let path = dir.path(); std::fs::write(path.join("existing.txt"), "data").unwrap(); - let result = clone_repo("https://github.com/test/repo.git", "token", path.to_str().unwrap()); + let result = clone_repo( + "https://github.com/test/repo.git", + "token", + path.to_str().unwrap(), + ); assert!(result.is_err()); assert!(result.unwrap_err().contains("not empty")); } @@ -255,7 +267,11 @@ mod tests { let dir = tempfile::TempDir::new().unwrap(); let dest = dir.path().join("new-clone"); - let result = clone_repo("git@github.com:user/repo.git", "token", dest.to_str().unwrap()); + let result = clone_repo( + "git@github.com:user/repo.git", + "token", + dest.to_str().unwrap(), + ); assert!(result.is_err()); assert!(result.unwrap_err().contains("Unsupported URL format")); } @@ -268,7 +284,11 @@ mod tests { std::fs::create_dir(&dest).unwrap(); // This will fail at the git clone step (invalid URL) but should pass the directory check - let result = clone_repo("https://github.com/nonexistent/repo.git", "token", dest.to_str().unwrap()); + let result = clone_repo( + "https://github.com/nonexistent/repo.git", + "token", + dest.to_str().unwrap(), + ); assert!(result.is_err()); // Should fail at git clone, not at directory check assert!(result.unwrap_err().contains("git clone failed")); @@ -280,9 +300,21 @@ mod tests { let path = dir.path(); // Initialize a git repo - StdCommand::new("git").args(["init"]).current_dir(path).output().unwrap(); - StdCommand::new("git").args(["remote", "add", "origin", "https://github.com/user/repo.git"]) - .current_dir(path).output().unwrap(); + StdCommand::new("git") + .args(["init"]) + .current_dir(path) + .output() + .unwrap(); + StdCommand::new("git") + .args([ + "remote", + "add", + "origin", + "https://github.com/user/repo.git", + ]) + .current_dir(path) + .output() + .unwrap(); let result = configure_remote_auth( path.to_str().unwrap(), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c0632da2..90a5902c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,11 +7,11 @@ pub mod settings; pub mod vault; use ai_chat::{AiChatRequest, AiChatResponse}; +use frontmatter::FrontmatterValue; use git::{GitCommit, ModifiedFile}; use github::GithubRepo; use settings::Settings; -use vault::{VaultEntry, RenameResult}; -use frontmatter::FrontmatterValue; +use vault::{RenameResult, VaultEntry}; #[tauri::command] fn list_vault(path: String) -> Result, String> { @@ -24,7 +24,11 @@ fn get_note_content(path: String) -> Result { } #[tauri::command] -fn update_frontmatter(path: String, key: String, value: FrontmatterValue) -> Result { +fn update_frontmatter( + path: String, + key: String, + value: FrontmatterValue, +) -> Result { vault::update_frontmatter(&path, &key, value) } @@ -49,7 +53,11 @@ fn get_file_diff(vault_path: String, path: String) -> Result { } #[tauri::command] -fn get_file_diff_at_commit(vault_path: String, path: String, commit_hash: String) -> Result { +fn get_file_diff_at_commit( + vault_path: String, + path: String, + commit_hash: String, +) -> Result { git::get_file_diff_at_commit(&vault_path, &path, &commit_hash) } @@ -74,7 +82,11 @@ fn save_image(vault_path: String, filename: String, data: String) -> Result Result { +fn rename_note( + vault_path: String, + old_path: String, + new_title: String, +) -> Result { vault::rename_note(&vault_path, &old_path, &new_title) } @@ -99,7 +111,11 @@ async fn github_list_repos(token: String) -> Result, String> { } #[tauri::command] -async fn github_create_repo(token: String, name: String, private: bool) -> Result { +async fn github_create_repo( + token: String, + name: String, + private: bool, +) -> Result { github::github_create_repo(&token, &name, private).await } @@ -122,7 +138,8 @@ pub fn run() { #[cfg(desktop)] { - app.handle().plugin(tauri_plugin_updater::Builder::new().build())?; + app.handle() + .plugin(tauri_plugin_updater::Builder::new().build())?; app.handle().plugin(tauri_plugin_process::init())?; menu::setup_menu(app)?; } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 35e5146b..3e4e0df1 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - laputa_lib::run(); + laputa_lib::run(); } diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index 7b3c8ac8..197a7d4c 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -1,4 +1,7 @@ -use tauri::{menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder}, App, Emitter}; +use tauri::{ + menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder}, + App, Emitter, +}; const VIEW_ITEMS: [(&str, &str, &str); 3] = [ ("view-editor-only", "Editor Only", "CmdOrCtrl+1"), @@ -17,9 +20,7 @@ pub fn setup_menu(app: &App) -> Result<(), Box> { } let view_submenu = view_menu.build()?; - let menu = MenuBuilder::new(app) - .item(&view_submenu) - .build()?; + let menu = MenuBuilder::new(app).item(&view_submenu).build()?; app.set_menu(menu)?; diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index b12e11d9..6652e6f7 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -20,10 +20,9 @@ fn get_settings_at(path: &PathBuf) -> Result { if !path.exists() { return Ok(Settings::default()); } - let content = fs::read_to_string(path) - .map_err(|e| format!("Failed to read settings: {}", e))?; - serde_json::from_str(&content) - .map_err(|e| format!("Failed to parse settings: {}", e)) + let content = + fs::read_to_string(path).map_err(|e| format!("Failed to read settings: {}", e))?; + serde_json::from_str(&content).map_err(|e| format!("Failed to parse settings: {}", e)) } fn save_settings_at(path: &PathBuf, settings: Settings) -> Result<(), String> { @@ -34,16 +33,27 @@ fn save_settings_at(path: &PathBuf, settings: Settings) -> Result<(), String> { // Trim whitespace and convert empty strings to None let cleaned = Settings { - anthropic_key: settings.anthropic_key.map(|k| k.trim().to_string()).filter(|k| !k.is_empty()), - openai_key: settings.openai_key.map(|k| k.trim().to_string()).filter(|k| !k.is_empty()), - google_key: settings.google_key.map(|k| k.trim().to_string()).filter(|k| !k.is_empty()), - github_token: settings.github_token.map(|k| k.trim().to_string()).filter(|k| !k.is_empty()), + anthropic_key: settings + .anthropic_key + .map(|k| k.trim().to_string()) + .filter(|k| !k.is_empty()), + openai_key: settings + .openai_key + .map(|k| k.trim().to_string()) + .filter(|k| !k.is_empty()), + google_key: settings + .google_key + .map(|k| k.trim().to_string()) + .filter(|k| !k.is_empty()), + github_token: settings + .github_token + .map(|k| k.trim().to_string()) + .filter(|k| !k.is_empty()), }; let json = serde_json::to_string_pretty(&cleaned) .map_err(|e| format!("Failed to serialize settings: {}", e))?; - fs::write(path, json) - .map_err(|e| format!("Failed to write settings: {}", e)) + fs::write(path, json).map_err(|e| format!("Failed to write settings: {}", e)) } pub fn get_settings() -> Result { @@ -71,7 +81,15 @@ mod tests { let s = Settings::default(); assert_eq!( format!("{:?}", s), - format!("{:?}", Settings { anthropic_key: None, openai_key: None, google_key: None, github_token: None }) + format!( + "{:?}", + Settings { + anthropic_key: None, + openai_key: None, + google_key: None, + github_token: None + } + ) ); } @@ -140,9 +158,19 @@ mod tests { let dir = tempfile::TempDir::new().unwrap(); let path = dir.path().join("nested").join("dir").join("settings.json"); - save_settings_at(&path, Settings { anthropic_key: Some("key".to_string()), ..Default::default() }).unwrap(); + save_settings_at( + &path, + Settings { + anthropic_key: Some("key".to_string()), + ..Default::default() + }, + ) + .unwrap(); assert!(path.exists()); - assert_eq!(get_settings_at(&path).unwrap().anthropic_key.as_deref(), Some("key")); + assert_eq!( + get_settings_at(&path).unwrap().anthropic_key.as_deref(), + Some("key") + ); } #[test] diff --git a/src-tauri/src/vault.rs b/src-tauri/src/vault.rs index 6143506c..a2266de9 100644 --- a/src-tauri/src/vault.rs +++ b/src-tauri/src/vault.rs @@ -8,7 +8,7 @@ use std::path::Path; use std::time::UNIX_EPOCH; use walkdir::WalkDir; -use crate::frontmatter::{with_frontmatter, update_frontmatter_content}; +use crate::frontmatter::{update_frontmatter_content, with_frontmatter}; #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct VaultEntry { @@ -178,7 +178,9 @@ fn without_h1_line(s: &str) -> Option<&str> { fn collect_until(chars: &mut impl Iterator, delimiter: char) -> String { let mut buf = String::new(); for c in chars.by_ref() { - if c == delimiter { break; } + if c == delimiter { + break; + } buf.push(c); } buf @@ -187,7 +189,9 @@ fn collect_until(chars: &mut impl Iterator, delimiter: char) -> Str /// Skip all chars until a delimiter (consuming the delimiter). fn skip_until(chars: &mut impl Iterator, delimiter: char) { for c in chars.by_ref() { - if c == delimiter { break; } + if c == delimiter { + break; + } } } @@ -219,19 +223,26 @@ fn strip_markdown_chars(s: &str) -> String { /// Parse frontmatter from raw YAML data extracted by gray_matter. fn parse_frontmatter(data: &HashMap) -> Frontmatter { // Convert HashMap to serde_json::Value for deserialization - let value = serde_json::Value::Object( - data.iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - ); + let value = + serde_json::Value::Object(data.iter().map(|(k, v)| (k.clone(), v.clone())).collect()); serde_json::from_value(value).unwrap_or_default() } /// Known non-relationship frontmatter keys to skip (case-insensitive comparison). /// Only skip keys that can never contain wikilinks. const SKIP_KEYS: &[&str] = &[ - "is a", "aliases", "status", "cadence", "archived", "trashed", "trashed at", - "created at", "created time", "icon", "color", "order", + "is a", + "aliases", + "status", + "cadence", + "archived", + "trashed", + "trashed at", + "created at", + "created time", + "icon", + "color", + "order", ]; /// Check if a string contains a wikilink pattern `[[...]]`. @@ -243,7 +254,9 @@ fn contains_wikilink(s: &str) -> bool { /// Returns a HashMap where each key is the original frontmatter field name /// and the value is a Vec of wikilink strings found in that field. /// Handles both single string values and arrays of strings. -fn extract_relationships(data: &HashMap) -> HashMap> { +fn extract_relationships( + data: &HashMap, +) -> HashMap> { let mut relationships = HashMap::new(); for (key, value) in data { @@ -296,7 +309,8 @@ fn infer_type_from_folder(folder: &str) -> String { "essay" => "Essay", "evergreen" => "Evergreen", _ => return capitalize_first(folder), - }.to_string() + } + .to_string() } /// Resolve `is_a` from frontmatter, falling back to parent folder inference. @@ -312,28 +326,35 @@ fn resolve_is_a(fm_is_a: Option, path: &Path) -> Option { /// Parse created_at from frontmatter (prefer "Created at" over "Created time"). fn parse_created_at(fm: &Frontmatter) -> Option { - fm.created_at.as_ref().and_then(|s| parse_iso_date(s)) + fm.created_at + .as_ref() + .and_then(|s| parse_iso_date(s)) .or_else(|| fm.created_time.as_ref().and_then(|s| parse_iso_date(s))) } /// Extract frontmatter and relationships from parsed gray_matter data. -fn extract_fm_and_rels(data: Option) -> (Frontmatter, HashMap>) { +fn extract_fm_and_rels( + data: Option, +) -> (Frontmatter, HashMap>) { let hash = match data { Some(gray_matter::Pod::Hash(map)) => map, _ => return (Frontmatter::default(), HashMap::new()), }; - let json_map: HashMap = hash - .into_iter() - .map(|(k, v)| (k, pod_to_json(v))) - .collect(); - (parse_frontmatter(&json_map), extract_relationships(&json_map)) + let json_map: HashMap = + hash.into_iter().map(|(k, v)| (k, pod_to_json(v))).collect(); + ( + parse_frontmatter(&json_map), + extract_relationships(&json_map), + ) } /// Read file metadata (modified_at timestamp, file size). fn read_file_metadata(path: &Path) -> Result<(Option, u64), String> { - let metadata = fs::metadata(path) - .map_err(|e| format!("Failed to stat {}: {}", path.display(), e))?; - let modified_at = metadata.modified().ok() + let metadata = + fs::metadata(path).map_err(|e| format!("Failed to stat {}: {}", path.display(), e))?; + let modified_at = metadata + .modified() + .ok() .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) .map(|d| d.as_secs()); Ok((modified_at, metadata.len())) @@ -343,7 +364,8 @@ fn read_file_metadata(path: &Path) -> Result<(Option, u64), String> { pub fn parse_md_file(path: &Path) -> Result { let content = fs::read_to_string(path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; - let filename = path.file_name() + let filename = path + .file_name() .map(|f| f.to_string_lossy().to_string()) .unwrap_or_default(); @@ -373,17 +395,32 @@ pub fn parse_md_file(path: &Path) -> Result { Ok(VaultEntry { path: path.to_string_lossy().to_string(), - filename, title, is_a, snippet, relationships, - aliases: frontmatter.aliases.map(|a| a.into_vec()).unwrap_or_default(), - belongs_to: frontmatter.belongs_to.map(|b| b.into_vec()).unwrap_or_default(), - related_to: frontmatter.related_to.map(|r| r.into_vec()).unwrap_or_default(), + filename, + title, + is_a, + snippet, + relationships, + aliases: frontmatter + .aliases + .map(|a| a.into_vec()) + .unwrap_or_default(), + belongs_to: frontmatter + .belongs_to + .map(|b| b.into_vec()) + .unwrap_or_default(), + related_to: frontmatter + .related_to + .map(|r| r.into_vec()) + .unwrap_or_default(), status: frontmatter.status, owner: frontmatter.owner, cadence: frontmatter.cadence, archived: frontmatter.archived.unwrap_or(false), trashed: frontmatter.trashed.unwrap_or(false), trashed_at: frontmatter.trashed_at.as_deref().and_then(parse_iso_date), - modified_at, created_at, file_size, + modified_at, + created_at, + file_size, icon: frontmatter.icon, color: frontmatter.color, order: frontmatter.order, @@ -435,10 +472,8 @@ fn pod_to_json(pod: gray_matter::Pod) -> serde_json::Value { serde_json::Value::Array(arr.into_iter().map(pod_to_json).collect()) } gray_matter::Pod::Hash(map) => { - let obj: serde_json::Map = map - .into_iter() - .map(|(k, v)| (k, pod_to_json(v))) - .collect(); + let obj: serde_json::Map = + map.into_iter().map(|(k, v)| (k, pod_to_json(v))).collect(); serde_json::Value::Object(obj) } gray_matter::Pod::Null => serde_json::Value::Null, @@ -469,12 +504,7 @@ pub fn purge_trash(vault_path: &str) -> Result, String> { .filter_map(|e| e.ok()) { let path = entry.path(); - if !path.is_file() - || path - .extension() - .map(|ext| ext != "md") - .unwrap_or(true) - { + if !path.is_file() || path.extension().map(|ext| ext != "md").unwrap_or(true) { continue; } @@ -528,8 +558,7 @@ pub fn get_note_content(path: &str) -> Result { if !file_path.is_file() { return Err(format!("Path is not a file: {}", path)); } - fs::read_to_string(file_path) - .map_err(|e| format!("Failed to read {}: {}", path, e)) + fs::read_to_string(file_path).map_err(|e| format!("Failed to read {}: {}", path, e)) } /// Scan a directory recursively for .md files and return VaultEntry for each. @@ -601,7 +630,9 @@ fn run_git(vault: &Path, args: &[&str]) -> Option { /// Parse a git status porcelain line into (status_code, file_path). fn parse_porcelain_line(line: &str) -> Option<(&str, String)> { - if line.len() < 3 { return None; } + if line.len() < 3 { + return None; + } Some((&line[..2], line[3..].trim().to_string())) } @@ -612,7 +643,8 @@ fn is_new_file_status(status: &str) -> bool { /// Extract .md file paths from git diff --name-only output. fn collect_md_paths_from_diff(stdout: &str) -> Vec { - stdout.lines() + stdout + .lines() .filter(|line| !line.is_empty() && line.ends_with(".md")) .map(|line| line.to_string()) .collect() @@ -620,7 +652,8 @@ fn collect_md_paths_from_diff(stdout: &str) -> Vec { /// Extract .md file paths from git status --porcelain output. fn collect_md_paths_from_porcelain(stdout: &str) -> Vec { - stdout.lines() + stdout + .lines() .filter_map(parse_porcelain_line) .filter(|(_, path)| path.ends_with(".md")) .map(|(_, path)| path) @@ -651,7 +684,8 @@ fn git_uncommitted_new_files(vault: &Path) -> Vec { Some(s) => s, None => return Vec::new(), }; - stdout.lines() + stdout + .lines() .filter_map(parse_porcelain_line) .filter(|(status, path)| path.ends_with(".md") && is_new_file_status(status)) .map(|(_, path)| path) @@ -673,7 +707,8 @@ fn write_cache(vault: &Path, cache: &VaultCache) { fn to_relative_path(abs_path: &str, vault: &Path) -> String { let vault_str = vault.to_string_lossy(); let with_slash = format!("{}/", vault_str); - abs_path.strip_prefix(&with_slash) + abs_path + .strip_prefix(&with_slash) .or_else(|| abs_path.strip_prefix(vault_str.as_ref())) .unwrap_or(abs_path) .to_string() @@ -681,10 +716,15 @@ fn to_relative_path(abs_path: &str, vault: &Path) -> String { /// Parse .md files from a list of relative paths, skipping any that don't exist. fn parse_files_at(vault: &Path, rel_paths: &[String]) -> Vec { - rel_paths.iter() + rel_paths + .iter() .filter_map(|rel| { let abs = vault.join(rel); - if abs.is_file() { parse_md_file(&abs).ok() } else { None } + if abs.is_file() { + parse_md_file(&abs).ok() + } else { + None + } }) .collect() } @@ -692,7 +732,13 @@ fn parse_files_at(vault: &Path, rel_paths: &[String]) -> Vec { /// Sort entries by modified_at descending and write the cache. fn finalize_and_cache(vault: &Path, mut entries: Vec, hash: String) -> Vec { entries.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); - write_cache(vault, &VaultCache { commit_hash: hash, entries: entries.clone() }); + write_cache( + vault, + &VaultCache { + commit_hash: hash, + entries: entries.clone(), + }, + ); entries } @@ -700,7 +746,8 @@ fn finalize_and_cache(vault: &Path, mut entries: Vec, hash: String) fn update_same_commit(vault: &Path, cache: VaultCache) -> Vec { let new_files = git_uncommitted_new_files(vault); let mut entries = cache.entries; - let existing: std::collections::HashSet = entries.iter() + let existing: std::collections::HashSet = entries + .iter() .map(|e| to_relative_path(&e.path, vault)) .collect(); @@ -716,11 +763,17 @@ fn update_same_commit(vault: &Path, cache: VaultCache) -> Vec { } /// Handle different-commit cache: incremental update via git diff. -fn update_different_commit(vault: &Path, cache: VaultCache, current_hash: String) -> Vec { +fn update_different_commit( + vault: &Path, + cache: VaultCache, + current_hash: String, +) -> Vec { let changed_files = git_changed_files(vault, &cache.commit_hash, ¤t_hash); let changed_set: std::collections::HashSet = changed_files.iter().cloned().collect(); - let mut entries: Vec = cache.entries.into_iter() + let mut entries: Vec = cache + .entries + .into_iter() .filter(|e| !changed_set.contains(&to_relative_path(&e.path, vault))) .collect(); entries.extend(parse_files_at(vault, &changed_files)); @@ -733,7 +786,10 @@ fn update_different_commit(vault: &Path, cache: VaultCache, current_hash: String pub fn scan_vault_cached(vault_path: &str) -> Result, String> { let vault = Path::new(vault_path); if !vault.exists() || !vault.is_dir() { - return Err(format!("Vault path does not exist or is not a directory: {}", vault_path)); + return Err(format!( + "Vault path does not exist or is not a directory: {}", + vault_path + )); } let current_hash = match git_head_hash(vault) { @@ -758,13 +814,21 @@ pub fn scan_vault_cached(vault_path: &str) -> Result, String> { pub use crate::frontmatter::FrontmatterValue; /// Update a single frontmatter property in a markdown file -pub fn update_frontmatter(path: &str, key: &str, value: FrontmatterValue) -> Result { - with_frontmatter(path, |content| update_frontmatter_content(content, key, Some(value.clone()))) +pub fn update_frontmatter( + path: &str, + key: &str, + value: FrontmatterValue, +) -> Result { + with_frontmatter(path, |content| { + update_frontmatter_content(content, key, Some(value.clone())) + }) } /// Delete a frontmatter property from a markdown file pub fn delete_frontmatter_property(path: &str, key: &str) -> Result { - with_frontmatter(path, |content| update_frontmatter_content(content, key, None)) + with_frontmatter(path, |content| { + update_frontmatter_content(content, key, None) + }) } /// Check if a character is safe for use in filenames (alphanumeric, dot, dash, underscore). @@ -802,8 +866,7 @@ pub fn save_image(vault_path: &str, filename: &str, data: &str) -> Result String { return content.to_string(); } - let result: Vec = content.lines().map(|l| { - if l.trim().starts_with("# ") { format!("# {}", new_title) } else { l.to_string() } - }).collect(); + let result: Vec = content + .lines() + .map(|l| { + if l.trim().starts_with("# ") { + format!("# {}", new_title) + } else { + l.to_string() + } + }) + .collect(); let joined = result.join("\n"); if content.ends_with('\n') && !joined.ends_with('\n') { @@ -861,9 +931,7 @@ fn build_wikilink_pattern(old_title: &str, old_path_stem: &str) -> Option /// Check if a path is a vault markdown file eligible for wikilink replacement. fn is_replaceable_md_file(path: &Path, exclude: &Path) -> bool { - path.is_file() - && path != exclude - && path.extension().is_some_and(|ext| ext == "md") + path.is_file() && path != exclude && path.extension().is_some_and(|ext| ext == "md") } /// Replace wikilink references in a single file's content. Returns updated content if changed. @@ -871,13 +939,15 @@ fn replace_wikilinks_in_content(content: &str, re: &Regex, new_title: &str) -> O if !re.is_match(content) { return None; } - let replaced = re.replace_all(content, |caps: ®ex::Captures| { - match caps.get(1) { - Some(pipe) => format!("[[{}{}]]", new_title, pipe.as_str()), - None => format!("[[{}]]", new_title), - } + let replaced = re.replace_all(content, |caps: ®ex::Captures| match caps.get(1) { + Some(pipe) => format!("[[{}{}]]", new_title, pipe.as_str()), + None => format!("[[{}]]", new_title), }); - if replaced != content { Some(replaced.into_owned()) } else { None } + if replaced != content { + Some(replaced.into_owned()) + } else { + None + } } /// Parameters for a vault-wide wikilink replacement. @@ -908,16 +978,19 @@ fn update_wikilinks_in_vault(params: &WikilinkReplacement) -> usize { }; let files = collect_md_files(params.vault_path, params.exclude_path); - files.iter().filter(|path| { - let content = match fs::read_to_string(path) { - Ok(c) => c, - Err(_) => return false, - }; - match replace_wikilinks_in_content(&content, &re, params.new_title) { - Some(new_content) => fs::write(path, &new_content).is_ok(), - None => false, - } - }).count() + files + .iter() + .filter(|path| { + let content = match fs::read_to_string(path) { + Ok(c) => c, + Err(_) => return false, + }; + match replace_wikilinks_in_content(&content, &re, params.new_title) { + Some(new_content) => fs::write(path, &new_content).is_ok(), + None => false, + } + }) + .count() } /// Check if frontmatter contains a `title:` key. @@ -925,11 +998,15 @@ fn frontmatter_has_title_key(content: &str) -> bool { if !content.starts_with("---\n") { return false; } - content[4..].split("\n---").next() - .map(|fm| fm.lines().any(|l| { - let t = l.trim_start(); - t.starts_with("title:") || t.starts_with("\"title\":") - })) + content[4..] + .split("\n---") + .next() + .map(|fm| { + fm.lines().any(|l| { + let t = l.trim_start(); + t.starts_with("title:") || t.starts_with("\"title\":") + }) + }) .unwrap_or(false) } @@ -955,7 +1032,11 @@ fn to_path_stem<'a>(abs_path: &'a str, vault_prefix: &str) -> &'a str { } /// Rename a note: update its title, rename the file, and update wiki links across the vault. -pub fn rename_note(vault_path: &str, old_path: &str, new_title: &str) -> Result { +pub fn rename_note( + vault_path: &str, + old_path: &str, + new_title: &str, +) -> Result { let vault = Path::new(vault_path); let old_file = Path::new(old_path); @@ -967,21 +1048,28 @@ pub fn rename_note(vault_path: &str, old_path: &str, new_title: &str) -> Result< return Err("New title cannot be empty".to_string()); } - let content = fs::read_to_string(old_file) - .map_err(|e| format!("Failed to read {}: {}", old_path, e))?; - let old_filename = old_file.file_name() - .map(|f| f.to_string_lossy().to_string()).unwrap_or_default(); + let content = + fs::read_to_string(old_file).map_err(|e| format!("Failed to read {}: {}", old_path, e))?; + let old_filename = old_file + .file_name() + .map(|f| f.to_string_lossy().to_string()) + .unwrap_or_default(); let old_title = extract_title(&content, &old_filename); if old_title == new_title { - return Ok(RenameResult { new_path: old_path.to_string(), updated_files: 0 }); + return Ok(RenameResult { + new_path: old_path.to_string(), + updated_files: 0, + }); } // Update content (H1 + frontmatter title) let updated_content = update_note_title_in_content(&content, new_title); // Compute new path and write file - let parent_dir = old_file.parent().ok_or("Cannot determine parent directory")?; + let parent_dir = old_file + .parent() + .ok_or("Cannot determine parent directory")?; let new_file = parent_dir.join(format!("{}.md", title_to_slug(new_title))); let new_path_str = new_file.to_string_lossy().to_string(); @@ -996,10 +1084,17 @@ pub fn rename_note(vault_path: &str, old_path: &str, new_title: &str) -> Result< let vault_prefix = format!("{}/", vault.to_string_lossy()); let old_path_stem = to_path_stem(old_path, &vault_prefix); let updated_files = update_wikilinks_in_vault(&WikilinkReplacement { - vault_path: vault, old_title: &old_title, new_title, old_path_stem, exclude_path: &new_file, + vault_path: vault, + old_title: &old_title, + new_title, + old_path_stem, + exclude_path: &new_file, }); - Ok(RenameResult { new_path: new_path_str, updated_files }) + Ok(RenameResult { + new_path: new_path_str, + updated_files, + }) } #[cfg(test)] @@ -1027,7 +1122,10 @@ mod tests { #[test] fn test_extract_title_fallback_to_filename() { let content = "Just some content without a heading."; - assert_eq!(extract_title(content, "fallback-title.md"), "fallback-title"); + assert_eq!( + extract_title(content, "fallback-title.md"), + "fallback-title" + ); } #[test] @@ -1073,7 +1171,11 @@ mod tests { #[test] fn test_parse_empty_frontmatter() { let dir = TempDir::new().unwrap(); - let entry = parse_test_entry(&dir, "empty-fm.md", "---\n---\n# Just a Title\n\nNo frontmatter fields."); + let entry = parse_test_entry( + &dir, + "empty-fm.md", + "---\n---\n# Just a Title\n\nNo frontmatter fields.", + ); assert_eq!(entry.title, "Just a Title"); assert!(entry.aliases.is_empty()); @@ -1106,7 +1208,11 @@ mod tests { fn test_scan_vault_recursive() { let dir = TempDir::new().unwrap(); create_test_file(dir.path(), "root.md", "# Root Note\n"); - create_test_file(dir.path(), "sub/nested.md", "---\nIs A: Task\n---\n# Nested\n"); + create_test_file( + dir.path(), + "sub/nested.md", + "---\nIs A: Task\n---\n# Nested\n", + ); create_test_file(dir.path(), "not-markdown.txt", "This should be ignored"); let entries = scan_vault(dir.path().to_str().unwrap()).unwrap(); @@ -1219,13 +1325,33 @@ mod tests { let vault = dir.path(); // Init git repo - std::process::Command::new("git").args(["init"]).current_dir(vault).output().unwrap(); - std::process::Command::new("git").args(["config", "user.email", "test@test.com"]).current_dir(vault).output().unwrap(); - std::process::Command::new("git").args(["config", "user.name", "Test"]).current_dir(vault).output().unwrap(); + std::process::Command::new("git") + .args(["init"]) + .current_dir(vault) + .output() + .unwrap(); + std::process::Command::new("git") + .args(["config", "user.email", "test@test.com"]) + .current_dir(vault) + .output() + .unwrap(); + std::process::Command::new("git") + .args(["config", "user.name", "Test"]) + .current_dir(vault) + .output() + .unwrap(); create_test_file(vault, "note.md", "# Note\n\nFirst version."); - std::process::Command::new("git").args(["add", "."]).current_dir(vault).output().unwrap(); - std::process::Command::new("git").args(["commit", "-m", "init"]).current_dir(vault).output().unwrap(); + std::process::Command::new("git") + .args(["add", "."]) + .current_dir(vault) + .output() + .unwrap(); + std::process::Command::new("git") + .args(["commit", "-m", "init"]) + .current_dir(vault) + .output() + .unwrap(); // First call: full scan, writes cache let entries = scan_vault_cached(vault.to_str().unwrap()).unwrap(); @@ -1259,7 +1385,10 @@ Status: Active assert_eq!(entry.relationships.len(), 3); // Has, Topics, Type assert_eq!( entry.relationships.get("Has").unwrap(), - &vec!["[[essay/foo|Foo Essay]]".to_string(), "[[essay/bar|Bar Essay]]".to_string()] + &vec![ + "[[essay/foo|Foo Essay]]".to_string(), + "[[essay/bar|Bar Essay]]".to_string() + ] ); assert_eq!( entry.relationships.get("Topics").unwrap(), @@ -1343,7 +1472,10 @@ Custom Field: just a plain string fn test_parse_relationships_owner_and_notes() { let rels = parse_big_project_rels(); assert_eq!(rels.get("Notes").unwrap().len(), 3); - assert_eq!(rels.get("Owner").unwrap(), &vec!["[[person/alice]]".to_string()]); + assert_eq!( + rels.get("Owner").unwrap(), + &vec!["[[person/alice]]".to_string()] + ); } #[test] @@ -1385,7 +1517,10 @@ Context: "[[area/research]]" // Array → Vec with multiple elements assert_eq!( entry.relationships.get("Reviewers").unwrap(), - &vec!["[[person/carol]]".to_string(), "[[person/dave]]".to_string()] + &vec![ + "[[person/carol]]".to_string(), + "[[person/dave]]".to_string() + ] ); // Another single string assert_eq!( @@ -1422,7 +1557,10 @@ Context: "[[area/research]]" #[test] fn test_skip_keys_real_relation_included() { let (rels, len) = parse_skip_keys_rels(); - assert_eq!(rels.get("Real Relation").unwrap(), &vec!["[[note/important]]".to_string()]); + assert_eq!( + rels.get("Real Relation").unwrap(), + &vec!["[[note/important]]".to_string()] + ); // "Real Relation" + auto-generated "Type" (from is_a: "[[type/project]]") assert_eq!(len, 2); assert_eq!( @@ -1451,7 +1589,10 @@ References: // Only the wikilink entries should be captured assert_eq!( entry.relationships.get("References").unwrap(), - &vec!["[[source/paper-a]]".to_string(), "[[source/paper-b]]".to_string()] + &vec![ + "[[source/paper-a]]".to_string(), + "[[source/paper-b]]".to_string() + ] ); } @@ -1508,7 +1649,10 @@ References: #[test] fn test_strip_markdown_chars_emphasis() { - assert_eq!(strip_markdown_chars("**bold** and *italic*"), "bold and italic"); + assert_eq!( + strip_markdown_chars("**bold** and *italic*"), + "bold and italic" + ); } #[test] @@ -1523,7 +1667,10 @@ References: #[test] fn test_strip_markdown_chars_link_with_url() { - assert_eq!(strip_markdown_chars("[click here](https://example.com)"), "click here"); + assert_eq!( + strip_markdown_chars("[click here](https://example.com)"), + "click here" + ); } #[test] @@ -1591,7 +1738,13 @@ References: for (folder, expected_type) in known_folders { create_test_file(dir.path(), &format!("{}/test.md", folder), "# Test\n"); let entry = parse_md_file(&dir.path().join(folder).join("test.md")).unwrap(); - assert_eq!(entry.is_a, Some(expected_type.to_string()), "folder '{}' should infer type '{}'", folder, expected_type); + assert_eq!( + entry.is_a, + Some(expected_type.to_string()), + "folder '{}' should infer type '{}'", + folder, + expected_type + ); } } @@ -1606,7 +1759,11 @@ References: #[test] fn test_infer_type_frontmatter_overrides_folder() { let dir = TempDir::new().unwrap(); - create_test_file(dir.path(), "person/test.md", "---\nIs A: Custom\n---\n# Test\n"); + create_test_file( + dir.path(), + "person/test.md", + "---\nIs A: Custom\n---\n# Test\n", + ); let entry = parse_md_file(&dir.path().join("person/test.md")).unwrap(); assert_eq!(entry.is_a, Some("Custom".to_string())); } @@ -1715,13 +1872,33 @@ References: let vault = dir.path(); // Init git repo - std::process::Command::new("git").args(["init"]).current_dir(vault).output().unwrap(); - std::process::Command::new("git").args(["config", "user.email", "test@test.com"]).current_dir(vault).output().unwrap(); - std::process::Command::new("git").args(["config", "user.name", "Test"]).current_dir(vault).output().unwrap(); + std::process::Command::new("git") + .args(["init"]) + .current_dir(vault) + .output() + .unwrap(); + std::process::Command::new("git") + .args(["config", "user.email", "test@test.com"]) + .current_dir(vault) + .output() + .unwrap(); + std::process::Command::new("git") + .args(["config", "user.name", "Test"]) + .current_dir(vault) + .output() + .unwrap(); create_test_file(vault, "first.md", "# First\n\nFirst note."); - std::process::Command::new("git").args(["add", "."]).current_dir(vault).output().unwrap(); - std::process::Command::new("git").args(["commit", "-m", "first"]).current_dir(vault).output().unwrap(); + std::process::Command::new("git") + .args(["add", "."]) + .current_dir(vault) + .output() + .unwrap(); + std::process::Command::new("git") + .args(["commit", "-m", "first"]) + .current_dir(vault) + .output() + .unwrap(); // Build cache let entries = scan_vault_cached(vault.to_str().unwrap()).unwrap(); @@ -1729,8 +1906,16 @@ References: // Add a second file and commit create_test_file(vault, "second.md", "# Second\n\nSecond note."); - std::process::Command::new("git").args(["add", "."]).current_dir(vault).output().unwrap(); - std::process::Command::new("git").args(["commit", "-m", "second"]).current_dir(vault).output().unwrap(); + std::process::Command::new("git") + .args(["add", "."]) + .current_dir(vault) + .output() + .unwrap(); + std::process::Command::new("git") + .args(["commit", "-m", "second"]) + .current_dir(vault) + .output() + .unwrap(); // Incremental update: cache has old commit, new commit adds second.md let entries2 = scan_vault_cached(vault.to_str().unwrap()).unwrap(); @@ -1886,7 +2071,10 @@ References: "---\nTrashed at: \"2025-01-01\"\n---\n# Old Trash\n", ); // File trashed recently — should be kept - let recent = chrono::Utc::now().date_naive().format("%Y-%m-%d").to_string(); + let recent = chrono::Utc::now() + .date_naive() + .format("%Y-%m-%d") + .to_string(); create_test_file( dir.path(), "recent-trash.md", @@ -1939,14 +2127,16 @@ References: #[test] fn test_purge_trash_exactly_30_days_not_deleted() { let dir = TempDir::new().unwrap(); - let thirty_days_ago = (chrono::Utc::now().date_naive() - - chrono::Duration::days(30)) + let thirty_days_ago = (chrono::Utc::now().date_naive() - chrono::Duration::days(30)) .format("%Y-%m-%d") .to_string(); create_test_file( dir.path(), "borderline.md", - &format!("---\nTrashed at: \"{}\"\n---\n# Borderline\n", thirty_days_ago), + &format!( + "---\nTrashed at: \"{}\"\n---\n# Borderline\n", + thirty_days_ago + ), ); let deleted = purge_trash(dir.path().to_str().unwrap()).unwrap(); @@ -1957,14 +2147,16 @@ References: #[test] fn test_purge_trash_31_days_deleted() { let dir = TempDir::new().unwrap(); - let thirty_one_days_ago = (chrono::Utc::now().date_naive() - - chrono::Duration::days(31)) + let thirty_one_days_ago = (chrono::Utc::now().date_naive() - chrono::Duration::days(31)) .format("%Y-%m-%d") .to_string(); create_test_file( dir.path(), "expired.md", - &format!("---\nTrashed at: \"{}\"\n---\n# Expired\n", thirty_one_days_ago), + &format!( + "---\nTrashed at: \"{}\"\n---\n# Expired\n", + thirty_one_days_ago + ), ); let deleted = purge_trash(dir.path().to_str().unwrap()).unwrap(); @@ -2008,14 +2200,19 @@ References: fn test_rename_note_basic() { let dir = TempDir::new().unwrap(); let vault = dir.path(); - create_test_file(vault, "note/weekly-review.md", "---\nIs A: Note\n---\n# Weekly Review\n\nContent here.\n"); + create_test_file( + vault, + "note/weekly-review.md", + "---\nIs A: Note\n---\n# Weekly Review\n\nContent here.\n", + ); let old_path = vault.join("note/weekly-review.md"); let result = rename_note( vault.to_str().unwrap(), old_path.to_str().unwrap(), "Sprint Retrospective", - ).unwrap(); + ) + .unwrap(); assert!(result.new_path.ends_with("sprint-retrospective.md")); assert!(!old_path.exists()); @@ -2029,16 +2226,29 @@ References: fn test_rename_note_updates_wikilinks() { let dir = TempDir::new().unwrap(); let vault = dir.path(); - create_test_file(vault, "note/weekly-review.md", "---\nIs A: Note\n---\n# Weekly Review\n\nContent.\n"); - create_test_file(vault, "note/other.md", "---\nIs A: Note\n---\n# Other\n\nSee [[Weekly Review]] for details.\n"); - create_test_file(vault, "project/my-project.md", "---\nIs A: Project\nRelated to:\n - \"[[Weekly Review]]\"\n---\n# My Project\n"); + create_test_file( + vault, + "note/weekly-review.md", + "---\nIs A: Note\n---\n# Weekly Review\n\nContent.\n", + ); + create_test_file( + vault, + "note/other.md", + "---\nIs A: Note\n---\n# Other\n\nSee [[Weekly Review]] for details.\n", + ); + create_test_file( + vault, + "project/my-project.md", + "---\nIs A: Project\nRelated to:\n - \"[[Weekly Review]]\"\n---\n# My Project\n", + ); let old_path = vault.join("note/weekly-review.md"); let result = rename_note( vault.to_str().unwrap(), old_path.to_str().unwrap(), "Sprint Retrospective", - ).unwrap(); + ) + .unwrap(); assert_eq!(result.updated_files, 2); @@ -2061,7 +2271,8 @@ References: vault.to_str().unwrap(), old_path.to_str().unwrap(), "My Note", - ).unwrap(); + ) + .unwrap(); assert_eq!(result.new_path, old_path.to_str().unwrap()); assert_eq!(result.updated_files, 0); @@ -2074,11 +2285,7 @@ References: create_test_file(vault, "note/test.md", "# Test\n"); let old_path = vault.join("note/test.md"); - let result = rename_note( - vault.to_str().unwrap(), - old_path.to_str().unwrap(), - " ", - ); + let result = rename_note(vault.to_str().unwrap(), old_path.to_str().unwrap(), " "); assert!(result.is_err()); } @@ -2087,14 +2294,19 @@ References: let dir = TempDir::new().unwrap(); let vault = dir.path(); create_test_file(vault, "note/weekly-review.md", "# Weekly Review\n"); - create_test_file(vault, "note/ref.md", "# Ref\n\nSee [[Weekly Review|my review]] for info.\n"); + create_test_file( + vault, + "note/ref.md", + "# Ref\n\nSee [[Weekly Review|my review]] for info.\n", + ); let old_path = vault.join("note/weekly-review.md"); let result = rename_note( vault.to_str().unwrap(), old_path.to_str().unwrap(), "Sprint Retro", - ).unwrap(); + ) + .unwrap(); assert_eq!(result.updated_files, 1); let ref_content = fs::read_to_string(vault.join("note/ref.md")).unwrap(); @@ -2105,14 +2317,19 @@ References: fn test_rename_note_updates_title_frontmatter() { let dir = TempDir::new().unwrap(); let vault = dir.path(); - create_test_file(vault, "note/old.md", "---\ntitle: Old Name\nIs A: Note\n---\n# Old Name\n"); + create_test_file( + vault, + "note/old.md", + "---\ntitle: Old Name\nIs A: Note\n---\n# Old Name\n", + ); let old_path = vault.join("note/old.md"); let result = rename_note( vault.to_str().unwrap(), old_path.to_str().unwrap(), "New Name", - ).unwrap(); + ) + .unwrap(); let content = fs::read_to_string(&result.new_path).unwrap(); assert!(content.contains("title: New Name")); From 29eb458d57bb4003ccef021ae2a20698d6bd0844 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 23 Feb 2026 11:40:33 +0100 Subject: [PATCH 8/8] fix: rustfmt build.rs --- src-tauri/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 795b9b7c..d860e1e6 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,3 @@ fn main() { - tauri_build::build() + tauri_build::build() }