diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000..2f885bc --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,268 @@ +name: Python CI + +on: + push: + branches: + - main + paths: + - ".github/workflows/python-ci.yml" + - "package.json" + - "package-lock.json" + - "scripts/**" + - "src/**" + - "tests/**" + - "dist/**" + - "python/**" + - "tsconfig.json" + - "vite.config.*" + pull_request: + paths: + - ".github/workflows/python-ci.yml" + - "package.json" + - "package-lock.json" + - "scripts/**" + - "src/**" + - "tests/**" + - "dist/**" + - "python/**" + - "tsconfig.json" + - "vite.config.*" + +permissions: + contents: read + +jobs: + frontend-build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install frontend dependencies + run: npm ci + + - name: Build frontend bundles + run: npm run build + + - name: Typecheck frontend + run: npm run typecheck + + - name: Run frontend tests + run: npm test + + - name: Synchronize Python assets + run: npm run build:python-assets + + - name: Verify synchronized Python assets + run: npm run verify:python-assets + + - name: Upload synchronized Python assets + uses: actions/upload-artifact@v4 + with: + name: python-static-assets + path: python/src/olloweditor/static/olloweditor + if-no-files-found: error + + python-tests: + runs-on: ubuntu-latest + needs: frontend-build + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Download synchronized Python assets + uses: actions/download-artifact@v4 + with: + name: python-static-assets + path: python/src/olloweditor/static + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: python/pyproject.toml + + - name: Install Python package and test dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e "./python[all,test,dev]" + + - name: Run Python test suite + working-directory: python + run: python -m pytest + + - name: Run Ruff + if: matrix.python-version == '3.12' + working-directory: python + run: | + python -m ruff check . + python -m ruff format --check . + + - name: Run mypy + if: matrix.python-version == '3.12' + working-directory: python + run: python -m mypy src + + package-build: + runs-on: ubuntu-latest + needs: + - frontend-build + - python-tests + + steps: + - uses: actions/checkout@v4 + + - name: Download synchronized Python assets + uses: actions/download-artifact@v4 + with: + name: python-static-assets + path: python/src/olloweditor/static + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: python/pyproject.toml + + - name: Install Python packaging dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e "./python[all,test,dev]" + + - name: Build distributions + working-directory: python + run: | + rm -rf build dist src/*.egg-info + python -m build + + - name: Validate metadata rendering + working-directory: python + run: python -m twine check dist/* + + - name: Inspect wheel contents + working-directory: python + run: python scripts/check_wheel_contents.py dist/*.whl + + - name: Verify wheel installs cleanly + working-directory: python + run: python scripts/verify_wheel_installs.py dist/*.whl + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: python-dist + path: python/dist + if-no-files-found: error + + dependency-isolation: + runs-on: ubuntu-latest + needs: package-build + strategy: + fail-fast: false + matrix: + include: + - label: base + requirement: "olloweditor" + import_target: "olloweditor" + check_expr: | + import importlib.util + assert importlib.util.find_spec("django") is None + assert importlib.util.find_spec("rest_framework") is None + assert importlib.util.find_spec("flask") is None + assert importlib.util.find_spec("fastapi") is None + - label: django + requirement: "olloweditor[django]" + import_target: "olloweditor.integrations.django" + check_expr: | + import importlib.util + assert importlib.util.find_spec("django") is not None + - label: drf + requirement: "olloweditor[drf]" + import_target: "olloweditor.integrations.drf" + check_expr: | + import importlib.util + assert importlib.util.find_spec("django") is not None + assert importlib.util.find_spec("rest_framework") is not None + - label: flask + requirement: "olloweditor[flask]" + import_target: "olloweditor.integrations.flask" + check_expr: | + import importlib.util + assert importlib.util.find_spec("flask") is not None + - label: fastapi + requirement: "olloweditor[fastapi]" + import_target: "olloweditor.integrations.fastapi" + check_expr: | + import importlib.util + assert importlib.util.find_spec("fastapi") is not None + + steps: + - uses: actions/checkout@v4 + + - name: Download distributions + uses: actions/download-artifact@v4 + with: + name: python-dist + path: python/dist + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: python/pyproject.toml + + - name: Verify isolated install for ${{ matrix.label }} + shell: bash + run: | + WHEEL_PATH=$(find "$GITHUB_WORKSPACE/python/dist" -maxdepth 1 -name '*.whl' -print -quit) + if [ -z "$WHEEL_PATH" ]; then + echo "Wheel artifact not found in python/dist" >&2 + exit 1 + fi + python -m venv .venv-${{ matrix.label }} + . .venv-${{ matrix.label }}/bin/activate + python -m pip install --upgrade pip + python -m pip install "${{ matrix.requirement }} @ file://$WHEEL_PATH" + python -c "import ${{ matrix.import_target }}; print('ok')" + python - <<'PY' + ${{ matrix.check_expr }} + PY + + example-smoke: + runs-on: ubuntu-latest + needs: + - frontend-build + - python-tests + + steps: + - uses: actions/checkout@v4 + + - name: Download synchronized Python assets + uses: actions/download-artifact@v4 + with: + name: python-static-assets + path: python/src/olloweditor/static + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: python/pyproject.toml + + - name: Install Python package and example dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e "./python[all,test,dev]" + + - name: Run example smoke tests + working-directory: python + run: python -m pytest tests/test_examples.py diff --git a/README.md b/README.md index d0f52a1..7e3fbbf 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Ollow Editor is designed for newsroom-style writing, blog publishing, CMS forms, ## Preview -![Ollow Editor Preview](./olloweditor.png) +![Ollow Editor Preview](olloweditor.jpg) --- diff --git a/olloweditor.jpg b/olloweditor.jpg new file mode 100644 index 0000000..1a42f0b Binary files /dev/null and b/olloweditor.jpg differ diff --git a/website/css.css b/website/css.css index 329bd1b..e8343b5 100644 --- a/website/css.css +++ b/website/css.css @@ -1,261 +1,259 @@ :root { - color-scheme: light; - --page-bg: #f6f8ff; - --surface: rgba(255, 255, 255, 0.82); - --surface-strong: #ffffff; - --surface-soft: #eef4ff; - --surface-dark: #08111f; - --surface-dark-soft: #0e1830; - --text: #0f172a; - --text-soft: #516077; - --text-inverse: #ecf5ff; - --border: rgba(117, 140, 173, 0.24); - --border-strong: rgba(65, 96, 145, 0.28); - --primary: #2f7cff; - --primary-strong: #1664ef; - --cyan: #46d8ec; - --purple: #7c4dff; - --shadow: 0 18px 50px rgba(7, 17, 31, 0.1); - --shadow-soft: 0 8px 24px rgba(7, 17, 31, 0.08); - --radius-lg: 24px; - --radius-md: 16px; - --radius-sm: 10px; - --container: 1180px; - --container-padding: 24px; - --section-space: clamp(72px, 8vw, 96px); - --section-space-tight: clamp(40px, 5vw, 56px); + color-scheme: light; + --page-bg: #f6f8ff; + --surface: rgba(255, 255, 255, 0.82); + --surface-strong: #ffffff; + --surface-soft: #eef4ff; + --surface-dark: #08111f; + --surface-dark-soft: #0e1830; + --text: #0f172a; + --text-soft: #516077; + --text-inverse: #ecf5ff; + --border: rgba(117, 140, 173, 0.24); + --border-strong: rgba(65, 96, 145, 0.28); + --primary: #2f7cff; + --primary-strong: #1664ef; + --cyan: #46d8ec; + --purple: #7c4dff; + --shadow: 0 18px 50px rgba(7, 17, 31, 0.1); + --shadow-soft: 0 8px 24px rgba(7, 17, 31, 0.08); + --radius-lg: 24px; + --radius-md: 16px; + --radius-sm: 10px; + --container: 1180px; + --container-padding: 24px; + --section-space: clamp(72px, 8vw, 96px); + --section-space-tight: clamp(40px, 5vw, 56px); } *, *::before, *::after { - box-sizing: border-box; + box-sizing: border-box; } html { - scroll-behavior: smooth; + scroll-behavior: smooth; } body { - margin: 0; - width: 100%; - min-width: 320px; - background: - radial-gradient(circle at top left, rgba(70, 216, 236, 0.18), transparent 30%), + margin: 0; + width: 100%; + min-width: 320px; + background: radial-gradient(circle at top left, rgba(70, 216, 236, 0.18), transparent 30%), radial-gradient(circle at top right, rgba(124, 77, 255, 0.14), transparent 26%), linear-gradient(180deg, #fbfdff 0%, #f4f7ff 48%, #eef4ff 100%); - color: var(--text); - font-family: "Inter", system-ui, sans-serif; - overflow-x: hidden; + color: var(--text); + font-family: "Inter", system-ui, sans-serif; + overflow-x: hidden; } a { - color: inherit; - text-decoration: none; + color: inherit; + text-decoration: none; } button, input, textarea, select { - font: inherit; + font: inherit; } img { - display: block; - max-width: 100%; + display: block; + max-width: 100%; } .site-shell { - position: relative; - width: 100%; - overflow-x: clip; + position: relative; + width: 100%; + overflow-x: clip; } .container { - width: min(calc(100% - (var(--container-padding) * 2)), var(--container)); - margin: 0 auto; + width: min(calc(100% - (var(--container-padding) * 2)), var(--container)); + margin: 0 auto; } .site-header { - position: sticky; - top: 0; - z-index: 100; - border-bottom: 1px solid rgba(145, 166, 196, 0.18); - background: rgba(246, 248, 255, 0.78); - backdrop-filter: blur(18px); + position: sticky; + top: 0; + z-index: 100; + border-bottom: 1px solid rgba(145, 166, 196, 0.18); + background: rgba(246, 248, 255, 0.78); + backdrop-filter: blur(18px); } .header-inner { - display: flex; - align-items: center; - justify-content: space-between; - gap: 24px; - min-height: 76px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + min-height: 76px; } .brand { - display: inline-flex; - align-items: center; - gap: 12px; - min-width: 0; + display: inline-flex; + align-items: center; + gap: 12px; + min-width: 0; } .brand-logo { - width: 40px; - height: 40px; - object-fit: contain; + width: 40px; + height: 40px; + object-fit: contain; } .brand-text { - font-size: 1.1rem; - font-weight: 700; - white-space: nowrap; + font-size: 1.1rem; + font-weight: 700; + white-space: nowrap; } .site-nav { - display: flex; - align-items: center; - justify-content: flex-end; - flex-wrap: wrap; - gap: 14px; - min-width: 0; + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 14px; + min-width: 0; } .site-nav a { - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--text-soft); - font-size: 0.95rem; - font-weight: 600; - white-space: nowrap; - transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-soft); + font-size: 0.95rem; + font-weight: 600; + white-space: nowrap; + transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease; } .site-nav a:hover, .site-nav a:focus-visible, .footer-nav a:hover, .footer-nav a:focus-visible { - color: var(--primary-strong); + color: var(--primary-strong); } .nav-button, .primary-button, .secondary-button { - display: inline-flex; - align-items: center; - justify-content: center; - min-height: 46px; - padding: 0 20px; - border-radius: 999px; - font-size: 0.96rem; - font-weight: 700; - transition: transform 160ms ease, box-shadow 160ms ease, background-color 160ms ease, border-color 160ms ease, color 160ms ease; + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 46px; + padding: 0 20px; + border-radius: 999px; + font-size: 0.96rem; + font-weight: 700; + transition: transform 160ms ease, box-shadow 160ms ease, background-color 160ms ease, border-color 160ms ease, color 160ms ease; } .nav-button { - border: 1px solid rgba(47, 124, 255, 0.18); - background: rgba(255, 255, 255, 0.72); - color: var(--text); + border: 1px solid rgba(47, 124, 255, 0.18); + background: rgba(255, 255, 255, 0.72); + color: var(--text); } .nav-button:hover, .nav-button:focus-visible, .secondary-button:hover, .secondary-button:focus-visible { - transform: translateY(-1px); - box-shadow: 0 10px 24px rgba(47, 124, 255, 0.12); + transform: translateY(-1px); + box-shadow: 0 10px 24px rgba(47, 124, 255, 0.12); } .primary-button { - background: linear-gradient(135deg, var(--primary) 0%, var(--cyan) 100%); - color: #04101f; - box-shadow: 0 16px 32px rgba(47, 124, 255, 0.24); + background: linear-gradient(135deg, var(--primary) 0%, var(--cyan) 100%); + color: #04101f; + box-shadow: 0 16px 32px rgba(47, 124, 255, 0.24); } .primary-button:hover, .primary-button:focus-visible { - transform: translateY(-1px); - box-shadow: 0 22px 40px rgba(47, 124, 255, 0.26); + transform: translateY(-1px); + box-shadow: 0 22px 40px rgba(47, 124, 255, 0.26); } .secondary-button { - border: 1px solid rgba(156, 178, 211, 0.36); - background: rgba(255, 255, 255, 0.58); - color: var(--text); + border: 1px solid rgba(156, 178, 211, 0.36); + background: rgba(255, 255, 255, 0.58); + color: var(--text); } .menu-toggle { - display: none; - align-items: center; - justify-content: center; - width: 42px; - height: 42px; - border: 1px solid rgba(145, 166, 196, 0.3); - border-radius: 12px; - background: rgba(255, 255, 255, 0.76); - color: var(--text); + display: none; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + border: 1px solid rgba(145, 166, 196, 0.3); + border-radius: 12px; + background: rgba(255, 255, 255, 0.76); + color: var(--text); } .material-symbols-outlined { - font-variation-settings: "FILL" 0, "wght" 500, "GRAD" 0, "opsz" 24; + font-variation-settings: "FILL" 0, "wght" 500, "GRAD" 0, "opsz" 24; } .hero { - position: relative; - padding: clamp(76px, 9vw, 108px) 0 var(--section-space); + position: relative; + padding: clamp(76px, 9vw, 108px) 0 var(--section-space); } .hero-backdrop { - position: absolute; - inset: 0; - background: - radial-gradient(circle at 50% 0%, rgba(47, 124, 255, 0.15), transparent 32%), + position: absolute; + inset: 0; + background: radial-gradient(circle at 50% 0%, rgba(47, 124, 255, 0.15), transparent 32%), radial-gradient(circle at 18% 12%, rgba(70, 216, 236, 0.14), transparent 24%), radial-gradient(circle at 82% 18%, rgba(124, 77, 255, 0.14), transparent 22%); - pointer-events: none; + pointer-events: none; } .hero-inner { - position: relative; - z-index: 1; - display: flex; - justify-content: center; - text-align: center; + position: relative; + z-index: 1; + display: flex; + justify-content: center; + text-align: center; } .hero-copy { - width: 100%; - max-width: 860px; + width: 100%; + max-width: 860px; } .eyebrow { - margin: 0 0 14px; - color: var(--primary-strong); - font-size: 0.78rem; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; + margin: 0 0 14px; + color: var(--primary-strong); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; } .hero h1, .section-heading h2, .json-copy h2, .cta-panel h2 { - margin: 0; - color: var(--text); - font-family: "Inter", system-ui, sans-serif; - letter-spacing: 0; + margin: 0; + color: var(--text); + font-family: "Inter", system-ui, sans-serif; + letter-spacing: 0; } .hero h1 { - font-size: clamp(2.8rem, 6vw, 4.6rem); - line-height: 1.04; + font-size: clamp(2.8rem, 6vw, 4.6rem); + line-height: 1.04; } .hero h1 span { - color: var(--primary-strong); + color: var(--primary-strong); } .hero-text, @@ -265,354 +263,587 @@ img { .feature-card p, .api-card p, .footer-copy p { - color: var(--text-soft); - line-height: 1.7; + color: var(--text-soft); + line-height: 1.7; } .hero-text { - max-width: 720px; - margin: 24px auto 0; - font-size: 1.08rem; + max-width: 720px; + margin: 24px auto 0; + font-size: 1.08rem; +} + +.hero-badge-row { + justify-content: center; + margin-top: 22px; } .hero-actions { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 14px; - margin-top: 30px; + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 14px; + margin-top: 30px; } .feature-grid { - padding: 18px 0 var(--section-space-tight); + padding: 18px 0 var(--section-space-tight); } .feature-grid-inner { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 18px; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 18px; } .feature-card { - padding: 26px 22px; - border: 1px solid var(--border); - border-radius: var(--radius-md); - background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(243, 248, 255, 0.82)); - box-shadow: var(--shadow-soft); - backdrop-filter: blur(18px); + padding: 26px 22px; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(243, 248, 255, 0.82)); + box-shadow: var(--shadow-soft); + backdrop-filter: blur(18px); } .feature-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 48px; - height: 48px; - margin-bottom: 18px; - border-radius: 14px; - background: linear-gradient(135deg, rgba(47, 124, 255, 0.14), rgba(70, 216, 236, 0.18)); - color: var(--primary-strong); - font-size: 1.75rem; + display: inline-flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + margin-bottom: 18px; + border-radius: 14px; + background: linear-gradient(135deg, rgba(47, 124, 255, 0.14), rgba(70, 216, 236, 0.18)); + color: var(--primary-strong); + font-size: 1.75rem; } .feature-card h2, .api-card h3 { - margin: 0 0 10px; - font-size: 1.12rem; - line-height: 1.3; + margin: 0 0 10px; + font-size: 1.12rem; + line-height: 1.3; } .feature-card p, .api-card p { - margin: 0; - font-size: 0.96rem; + margin: 0; + font-size: 0.96rem; } .mern-section { - padding: 0 0 var(--section-space-tight); + padding: 0 0 var(--section-space-tight); } .mern-showcase-grid { - display: grid; - grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); - gap: 18px; - align-items: stretch; + display: grid; + grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); + gap: 18px; + align-items: stretch; } .install-command-card, .code-example-card, .mern-feature-card { - min-width: 0; - border: 1px solid var(--border); - border-radius: var(--radius-md); - box-shadow: var(--shadow-soft); - backdrop-filter: blur(18px); + min-width: 0; + border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-soft); + backdrop-filter: blur(18px); } .install-command-card, .mern-feature-card { - background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(243, 248, 255, 0.82)); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(243, 248, 255, 0.82)); } .install-command-card { - padding: 24px; + padding: 24px; } .install-command-card-header { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 18px; - color: var(--text); - font-size: 0.96rem; - font-weight: 700; + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 18px; + color: var(--text); + font-size: 0.96rem; + font-weight: 700; } .install-command-card-header .material-symbols-outlined { - color: var(--primary-strong); + color: var(--primary-strong); } .install-command-shell { - display: flex; - align-items: center; - gap: 12px; - min-width: 0; - padding: 14px; - border: 1px solid rgba(92, 118, 164, 0.24); - border-radius: 14px; - background: - linear-gradient(180deg, rgba(8, 17, 31, 0.98), rgba(12, 24, 46, 0.96)), + display: flex; + align-items: center; + gap: 12px; + min-width: 0; + padding: 14px; + border: 1px solid rgba(92, 118, 164, 0.24); + border-radius: 14px; + background: linear-gradient(180deg, rgba(8, 17, 31, 0.98), rgba(12, 24, 46, 0.96)), radial-gradient(circle at top right, rgba(70, 216, 236, 0.08), transparent 36%); - box-shadow: inset 0 1px 0 rgba(160, 188, 255, 0.08); + box-shadow: inset 0 1px 0 rgba(160, 188, 255, 0.08); } .install-command { - display: block; - min-width: 0; - flex: 1 1 auto; - overflow-x: auto; - white-space: nowrap; - color: #8fe7ff; - font-family: "JetBrains Mono", monospace; - font-size: 0.94rem; - line-height: 1.6; - scrollbar-width: thin; + display: block; + min-width: 0; + flex: 1 1 auto; + overflow-x: auto; + white-space: nowrap; + color: #8fe7ff; + font-family: "JetBrains Mono", monospace; + font-size: 0.94rem; + line-height: 1.6; + scrollbar-width: thin; } .copy-install-btn { - flex: 0 0 auto; - min-height: 44px; - padding: 0 16px; - border: 1px solid rgba(70, 216, 236, 0.38); - border-radius: 12px; - background: linear-gradient(135deg, rgba(47, 124, 255, 0.2), rgba(70, 216, 236, 0.22)); - color: var(--text-inverse); - font-size: 0.92rem; - font-weight: 700; - cursor: pointer; - transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background-color 160ms ease; + flex: 0 0 auto; + min-height: 44px; + padding: 0 16px; + border-radius: 12px; + font-size: 0.92rem; + font-weight: 700; + cursor: pointer; + transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background-color 160ms ease; + background: linear-gradient(135deg, rgb(37, 99, 235), rgb(14, 165, 233)); + color: #ffffff; + border: 1px solid rgba(125, 211, 252, 0.45); + box-shadow: 0 10px 24px rgba(14, 165, 233, 0.22); } .copy-install-btn:hover, .copy-install-btn:focus-visible { - transform: translateY(-1px); - border-color: rgba(70, 216, 236, 0.6); - box-shadow: 0 12px 28px rgba(21, 118, 255, 0.22); + transform: translateY(-1px); + border-color: rgba(70, 216, 236, 0.6); + box-shadow: 0 12px 28px rgba(21, 118, 255, 0.22); } .copy-install-btn:focus-visible { - outline: 2px solid rgba(70, 216, 236, 0.26); - outline-offset: 2px; + outline: 2px solid rgba(70, 216, 236, 0.26); + outline-offset: 2px; } .mern-feature-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 18px; - margin-top: 18px; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 18px; + margin-top: 18px; } .mern-feature-card { - padding: 24px 20px; + padding: 24px 20px; } .mern-feature-card h3 { - margin: 0 0 10px; - font-size: 1.08rem; - line-height: 1.3; + margin: 0 0 10px; + font-size: 1.08rem; + line-height: 1.3; } .mern-feature-card p { - margin: 0; - color: var(--text-soft); - font-size: 0.96rem; - line-height: 1.7; + margin: 0; + color: var(--text-soft); + font-size: 0.96rem; + line-height: 1.7; +} + +.python-section { + padding: 0 0 var(--section-space-tight); +} + +.python-showcase-grid { + display: grid; + grid-template-columns: minmax(0, 1.02fr) minmax(0, 0.98fr); + gap: 18px; + align-items: stretch; +} + +.python-stack { + display: grid; + gap: 18px; +} + +.python-install-card, +.python-code-card, +.python-flow-card, +.python-feature-card, +.python-extra-card, +.python-framework-card { + min-width: 0; + border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-soft); + backdrop-filter: blur(18px); +} + +.python-install-card, +.python-flow-card, +.python-feature-card, +.python-extra-card, +.python-framework-card { + background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(243, 248, 255, 0.82)); +} + +.python-code-card { + overflow: hidden; + background: linear-gradient(180deg, rgba(8, 17, 31, 0.98), rgba(12, 24, 46, 0.96)); +} + +.python-code-card .install-command-card-header { + margin: 0; + padding: 20px 22px 0; + color: var(--text-inverse); +} + +.python-code-card pre { + margin: 0; + padding: 20px 22px 24px; + overflow-x: auto; + font-family: "JetBrains Mono", monospace; + font-size: 0.88rem; + line-height: 1.75; + color: #8fe7ff; + scrollbar-width: thin; +} + +.python-code-card code, +.python-framework-card code { + display: block; + min-width: max-content; +} + +.python-section-text, +.python-note, +.python-security-note { + margin: 0; + color: var(--text-soft); + line-height: 1.7; +} + +.python-install-card .python-section-text { + margin-bottom: 18px; +} + +.python-badge-row { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.python-install-card .python-badge-row { + margin-bottom: 18px; +} + +.python-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 32px; + padding: 0 12px; + border: 1px solid rgba(70, 216, 236, 0.22); + border-radius: 999px; + background: linear-gradient(135deg, rgba(47, 124, 255, 0.1), rgba(70, 216, 236, 0.12)); + color: var(--primary-strong); + font-size: 0.82rem; + font-weight: 700; +} + +.python-extras-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + margin-top: 18px; +} + +.python-extra-card { + padding: 16px; +} + +.python-extra-head, +.python-framework-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.python-extra-head span { + color: var(--text); + font-size: 0.92rem; + font-weight: 700; +} + +.python-extra-card code { + display: block; + overflow-x: auto; + color: var(--primary-strong); + font-family: "JetBrains Mono", monospace; + font-size: 0.84rem; + line-height: 1.65; + scrollbar-width: thin; +} + +.python-inline-copy-btn { + min-height: 36px; + padding: 0 12px; + font-size: 0.84rem; + border-radius: 10px; +} + +.python-flow-card { + padding: 24px; +} + +.python-flow-steps { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 16px; +} + +.python-flow-steps span { + position: relative; + display: inline-flex; + align-items: center; + min-height: 38px; + padding: 0 14px; + border: 1px solid rgba(115, 143, 185, 0.24); + border-radius: 999px; + background: rgba(255, 255, 255, 0.7); + color: var(--text); + font-size: 0.88rem; + font-weight: 600; +} + +.python-framework-card { + margin: 0 22px 22px; + padding: 18px; +} + +.python-framework-card pre { + padding: 0; + color: var(--text); +} + +.python-framework-card code { + color: var(--text); + font-size: 0.82rem; + line-height: 1.7; +} + +.python-framework-badge { + display: inline-flex; + align-items: center; + min-height: 30px; + padding: 0 10px; + border-radius: 999px; + background: linear-gradient(135deg, rgba(47, 124, 255, 0.16), rgba(70, 216, 236, 0.18)); + color: var(--primary-strong); + font-size: 0.78rem; + font-weight: 700; +} + +.python-feature-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 18px; + margin-top: 18px; +} + +.python-feature-card { + padding: 24px 20px; +} + +.python-feature-card h3 { + margin: 0 0 10px; + font-size: 1.08rem; + line-height: 1.3; +} + +.python-feature-card p { + margin: 0; + color: var(--text-soft); + font-size: 0.96rem; + line-height: 1.7; +} + +.python-security-note { + margin-top: 18px; + padding: 16px 18px; + border: 1px solid rgba(70, 216, 236, 0.18); + border-radius: 14px; + background: rgba(255, 255, 255, 0.72); } .code-example-card { - background: linear-gradient(180deg, rgba(8, 17, 31, 0.98), rgba(12, 24, 46, 0.96)); - overflow: hidden; + background: linear-gradient(180deg, rgba(8, 17, 31, 0.98), rgba(12, 24, 46, 0.96)); + overflow: hidden; } .code-example-card .install-command-card-header { - margin: 0; - padding: 20px 22px 0; - color: var(--text-inverse); + margin: 0; + padding: 20px 22px 0; + color: var(--text-inverse); } .code-example-card pre { - margin: 0; - padding: 20px 22px 24px; - overflow-x: auto; - font-family: "JetBrains Mono", monospace; - font-size: 0.88rem; - line-height: 1.75; - color: #8fe7ff; - scrollbar-width: thin; + margin: 0; + padding: 20px 22px 24px; + overflow-x: auto; + font-family: "JetBrains Mono", monospace; + font-size: 0.88rem; + line-height: 1.75; + color: #8fe7ff; + scrollbar-width: thin; } .code-example-card code { - display: block; - min-width: max-content; + display: block; + min-width: max-content; } .editor-demo-section { - position: relative; - padding: var(--section-space-tight) 0 0; + position: relative; + padding: var(--section-space-tight) 0 0; } .editor-demo-band { - position: absolute; - inset: auto 0 0; - height: 52%; - background: linear-gradient(180deg, rgba(8, 17, 31, 0) 0%, rgba(8, 17, 31, 0.94) 26%, #08111f 100%); + position: absolute; + inset: auto 0 0; + height: 52%; + background: linear-gradient(180deg, rgba(8, 17, 31, 0) 0%, rgba(8, 17, 31, 0.94) 26%, #08111f 100%); } .editor-demo-layout, .json-output-layout { - position: relative; - z-index: 1; + position: relative; + z-index: 1; } .editor-demo-layout { - display: grid; - gap: 24px; + display: grid; + gap: 24px; } .section-heading { - max-width: 740px; - margin-bottom: 24px; + max-width: 740px; + margin-bottom: 24px; } .section-heading h2, .json-copy h2, .cta-panel h2 { - font-size: clamp(2rem, 4vw, 3.2rem); - line-height: 1.08; + font-size: clamp(2rem, 4vw, 3.2rem); + line-height: 1.08; } .section-heading p, .json-copy p, .cta-panel p { - margin: 14px 0 0; - font-size: 1.02rem; + margin: 14px 0 0; + font-size: 1.02rem; } .section-heading-centered { - margin: 0 auto 28px; - text-align: center; + margin: 0 auto 28px; + text-align: center; } .editor-demo-card { - width: 100%; - max-width: 100%; - overflow: visible; - border: 1px solid rgba(135, 160, 198, 0.26); - border-radius: 28px; - background: rgba(255, 255, 255, 0.94); - box-shadow: 0 28px 70px rgba(7, 17, 31, 0.24); + width: 100%; + max-width: 100%; + overflow: visible; + border: 1px solid rgba(135, 160, 198, 0.26); + border-radius: 28px; + background: rgba(255, 255, 255, 0.94); + box-shadow: 0 28px 70px rgba(7, 17, 31, 0.24); } .demo-card-topbar { - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - gap: 16px; - min-height: 62px; - padding: 0 22px; - border-bottom: 1px solid rgba(142, 165, 201, 0.2); - background: rgba(245, 249, 255, 0.92); + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 16px; + min-height: 62px; + padding: 0 22px; + border-bottom: 1px solid rgba(142, 165, 201, 0.2); + background: rgba(245, 249, 255, 0.92); } .demo-window-dots { - display: flex; - align-items: center; - gap: 8px; + display: flex; + align-items: center; + gap: 8px; } .demo-window-dots span { - width: 11px; - height: 11px; - border-radius: 999px; - background: linear-gradient(135deg, rgba(47, 124, 255, 0.3), rgba(70, 216, 236, 0.3)); + width: 11px; + height: 11px; + border-radius: 999px; + background: linear-gradient(135deg, rgba(47, 124, 255, 0.3), rgba(70, 216, 236, 0.3)); } .demo-window-title { - font-family: "JetBrains Mono", monospace; - font-size: 0.82rem; - color: #7a89a0; + font-family: "JetBrains Mono", monospace; + font-size: 0.82rem; + color: #7a89a0; } .editor-demo-shell { - width: 100%; - max-width: 100%; - padding: 28px; - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(245, 249, 255, 0.92)), + width: 100%; + max-width: 100%; + padding: 28px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(245, 249, 255, 0.92)), radial-gradient(circle at top, rgba(70, 216, 236, 0.08), transparent 28%); - overflow: visible; + overflow: visible; } .editor-demo-shell textarea { - display: none; + display: none; } .editor-demo-shell .nw-editor { - width: 100%; - max-width: none; - min-width: 0; - --ollow-bg: #f5f9ff; - --ollow-surface: #ffffff; - --ollow-surface-soft: #f4f8ff; - --ollow-surface-muted: #edf4ff; - --ollow-surface-subtle: #f6f9ff; - --ollow-fill-soft: #e6eefb; - --ollow-border: #d8e3f4; - --ollow-border-strong: #c4d4ea; - --ollow-text: #13233c; - --ollow-text-strong: #0e1a2f; - --ollow-muted: #667892; - --ollow-soft: #4d607b; - --ollow-placeholder: #96a6bc; - --ollow-link: #1864ef; - --ollow-primary: #2f7cff; - --ollow-primary-soft: #eff5ff; - --ollow-primary-border: #cfe0ff; - --ollow-active-bg: #eef5ff; - --ollow-active-border: #cfe0ff; - --ollow-active-text: #1864ef; - --ollow-focus-ring: rgba(47, 124, 255, 0.16); - --ollow-success: #10b981; - --ollow-danger: #dc2626; - --ollow-select: #2f7cff; - --ollow-select-soft: #dce9ff; - --ollow-select-text: #144fc8; - --ollow-code-bg: #0d1729; - --ollow-code-border: #1b2c45; - --ollow-code-text: #e6eefb; - --ollow-shadow: 0 8px 24px rgba(10, 20, 40, 0.08); - --ollow-shadow-float: 0 20px 44px rgba(10, 20, 40, 0.16); - --ollow-shadow-panel: 0 20px 44px rgba(10, 20, 40, 0.18); + width: 100%; + max-width: none; + min-width: 0; + --ollow-bg: #f5f9ff; + --ollow-surface: #ffffff; + --ollow-surface-soft: #f4f8ff; + --ollow-surface-muted: #edf4ff; + --ollow-surface-subtle: #f6f9ff; + --ollow-fill-soft: #e6eefb; + --ollow-border: #d8e3f4; + --ollow-border-strong: #c4d4ea; + --ollow-text: #13233c; + --ollow-text-strong: #0e1a2f; + --ollow-muted: #667892; + --ollow-soft: #4d607b; + --ollow-placeholder: #96a6bc; + --ollow-link: #1864ef; + --ollow-primary: #2f7cff; + --ollow-primary-soft: #eff5ff; + --ollow-primary-border: #cfe0ff; + --ollow-active-bg: #eef5ff; + --ollow-active-border: #cfe0ff; + --ollow-active-text: #1864ef; + --ollow-focus-ring: rgba(47, 124, 255, 0.16); + --ollow-success: #10b981; + --ollow-danger: #dc2626; + --ollow-select: #2f7cff; + --ollow-select-soft: #dce9ff; + --ollow-select-text: #144fc8; + --ollow-code-bg: #0d1729; + --ollow-code-border: #1b2c45; + --ollow-code-text: #e6eefb; + --ollow-shadow: 0 8px 24px rgba(10, 20, 40, 0.08); + --ollow-shadow-float: 0 20px 44px rgba(10, 20, 40, 0.16); + --ollow-shadow-panel: 0 20px 44px rgba(10, 20, 40, 0.18); } .editor-demo-shell .nw-editor-card, @@ -622,28 +853,28 @@ img { .editor-demo-shell .nw-editor-status, .editor-demo-shell .nw-toolbar-row, .editor-demo-shell .nw-insert-row { - min-width: 0; + min-width: 0; } .editor-demo-shell .nw-editor-toolbar { - position: sticky; - top: 0; - z-index: 12; + position: sticky; + top: 0; + z-index: 12; } .editor-demo-shell .nw-toolbar-row, .editor-demo-shell .nw-insert-row { - overflow: visible; - scrollbar-width: thin; + overflow: visible; + scrollbar-width: thin; } .editor-demo-shell .nw-editor-surface { - min-height: clamp(360px, 48vw, 620px); - overflow-x: hidden; + min-height: clamp(360px, 48vw, 620px); + overflow-x: hidden; } .editor-demo-shell .nw-editor-content { - overflow-wrap: anywhere; + overflow-wrap: anywhere; } .editor-demo-shell .nw-editor-content img, @@ -656,881 +887,926 @@ img { .editor-demo-shell .nw-editor-content .ollow-gallery-grid, .editor-demo-shell .nw-editor-content .ollow-video-wrapper, .editor-demo-shell .ollow-editor-table-scroll { - max-width: 100%; + max-width: 100%; } .editor-demo-shell .nw-editor-content iframe { - width: 100%; + width: 100%; } .editor-demo-shell .nw-editor-content pre, .editor-demo-shell .ollow-editor-table-scroll { - overflow-x: auto; + overflow-x: auto; } .editor-demo-shell .nw-editor-status { - gap: 12px; + gap: 12px; } .json-output-section { - padding: var(--section-space) 0; - background: linear-gradient(180deg, #08111f 0%, #0d1730 100%); - color: var(--text-inverse); + padding: var(--section-space) 0; + background: linear-gradient(180deg, #08111f 0%, #0d1730 100%); + color: var(--text-inverse); } .json-output-layout { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr); - gap: 34px; - align-items: center; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr); + gap: 34px; + align-items: center; } .json-copy h2, .json-copy p, .json-copy li { - color: var(--text-inverse); + color: var(--text-inverse); } .json-copy p { - opacity: 0.82; + opacity: 0.82; } .check-list { - display: grid; - gap: 14px; - margin: 24px 0 0; - padding: 0; - list-style: none; + display: grid; + gap: 14px; + margin: 24px 0 0; + padding: 0; + list-style: none; } .check-list li { - position: relative; - padding-left: 28px; - line-height: 1.6; + position: relative; + padding-left: 28px; + line-height: 1.6; } .check-list li::before { - content: ""; - position: absolute; - top: 0.62em; - left: 0; - width: 10px; - height: 10px; - border-radius: 999px; - background: linear-gradient(135deg, var(--cyan), var(--primary)); - box-shadow: 0 0 18px rgba(70, 216, 236, 0.45); + content: ""; + position: absolute; + top: 0.62em; + left: 0; + width: 10px; + height: 10px; + border-radius: 999px; + background: linear-gradient(135deg, var(--cyan), var(--primary)); + box-shadow: 0 0 18px rgba(70, 216, 236, 0.45); } .json-code-panel { - width: 100%; - min-width: 0; - border: 1px solid rgba(137, 156, 189, 0.18); - border-radius: 24px; - background: rgba(5, 12, 24, 0.78); - box-shadow: 0 30px 70px rgba(3, 8, 18, 0.38); - overflow: hidden; + width: 100%; + min-width: 0; + border: 1px solid rgba(137, 156, 189, 0.18); + border-radius: 24px; + background: rgba(5, 12, 24, 0.78); + box-shadow: 0 30px 70px rgba(3, 8, 18, 0.38); + overflow: hidden; } .json-code-panel pre { - margin: 0; - padding: 24px; - overflow-x: auto; - font-family: "JetBrains Mono", monospace; - font-size: 0.9rem; - line-height: 1.7; - color: #8fe7ff; + margin: 0; + padding: 24px; + overflow-x: auto; + font-family: "JetBrains Mono", monospace; + font-size: 0.9rem; + line-height: 1.7; + color: #8fe7ff; } .json-code-panel code { - display: block; - min-width: max-content; + display: block; + min-width: max-content; } .api-section { - padding: var(--section-space) 0; + padding: var(--section-space) 0; } .api-card-grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 18px; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 18px; } .api-card { - padding: 26px; - border: 1px solid var(--border); - border-radius: var(--radius-md); - background: rgba(255, 255, 255, 0.76); - box-shadow: var(--shadow-soft); - backdrop-filter: blur(20px); + padding: 26px; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.76); + box-shadow: var(--shadow-soft); + backdrop-filter: blur(20px); } .api-card-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - margin-bottom: 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; } .api-card-header .material-symbols-outlined { - color: var(--primary-strong); + color: var(--primary-strong); } .cta-section { - padding: 0 0 var(--section-space); + padding: 0 0 var(--section-space); } .cta-panel { - position: relative; - overflow: hidden; - padding: 44px 32px; - border: 1px solid rgba(122, 148, 187, 0.24); - border-radius: 28px; - background: - radial-gradient(circle at top right, rgba(47, 124, 255, 0.12), transparent 24%), + position: relative; + overflow: hidden; + padding: 44px 32px; + border: 1px solid rgba(122, 148, 187, 0.24); + border-radius: 28px; + background: radial-gradient(circle at top right, rgba(47, 124, 255, 0.12), transparent 24%), radial-gradient(circle at top left, rgba(124, 77, 255, 0.1), transparent 28%), rgba(255, 255, 255, 0.84); - box-shadow: var(--shadow); - text-align: center; + box-shadow: var(--shadow); + text-align: center; } .cta-panel .install-command-card { - max-width: 700px; - margin: 26px auto 0; - padding: 14px; - background: rgba(255, 255, 255, 0.72); + max-width: 700px; + margin: 26px auto 0; + padding: 14px; + background: rgba(255, 255, 255, 0.72); +} + +.cta-install-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + max-width: 860px; + margin: 26px auto 0; +} + +.cta-install-grid .install-command-card { + max-width: none; + margin: 0; } .install-command-card-compact .install-command-shell { - background: - linear-gradient(180deg, rgba(8, 17, 31, 0.96), rgba(12, 24, 46, 0.95)), + background: linear-gradient(180deg, rgba(8, 17, 31, 0.96), rgba(12, 24, 46, 0.95)), radial-gradient(circle at top right, rgba(70, 216, 236, 0.08), transparent 36%); } .cta-logo { - width: 76px; - height: 76px; - margin: 0 auto 22px; - object-fit: contain; + width: 76px; + height: 76px; + margin: 0 auto 22px; + object-fit: contain; } .site-footer { - border-top: 1px solid rgba(145, 166, 196, 0.2); - background: rgba(244, 247, 255, 0.7); + border-top: 1px solid rgba(145, 166, 196, 0.2); + background: rgba(244, 247, 255, 0.7); } .footer-inner { - display: flex; - align-items: center; - justify-content: space-between; - gap: 24px; - padding: 26px 0 34px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 26px 0 34px; } .footer-copy p { - margin: 0; - font-size: 0.94rem; + margin: 0; + font-size: 0.94rem; } .footer-nav { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 18px; + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 18px; } .footer-nav a { - color: var(--text-soft); - font-size: 0.94rem; - font-weight: 600; + color: var(--text-soft); + font-size: 0.94rem; + font-weight: 600; } @media (min-width: 1200px) { - .editor-demo-shell .nw-editor-surface { - min-height: 600px; - } + .editor-demo-shell .nw-editor-surface { + min-height: 600px; + } } @media (max-width: 1199px) { - :root { - --container: 1100px; - } - - .header-inner { - min-height: 74px; - gap: 20px; - } + :root { + --container: 1100px; + } + + .header-inner { + min-height: 74px; + gap: 20px; + } + + .site-nav { + gap: 12px; + } + + .site-nav a { + font-size: 0.92rem; + } + + .nav-button, + .primary-button, + .secondary-button { + min-height: 44px; + padding: 0 18px; + } + + .hero h1 { + font-size: clamp(2.75rem, 5.6vw, 4.15rem); + } + + .hero-text { + max-width: 680px; + font-size: 1.02rem; + } + + .feature-grid-inner { + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; + } + + .feature-card, + .api-card, + .mern-feature-card, + .install-command-card { + padding: 24px 20px; + } + + .json-output-layout { + grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr); + gap: 28px; + align-items: start; + } + + .editor-demo-shell .nw-editor-surface { + min-height: 520px; + } +} - .site-nav { - gap: 12px; - } +@media (max-width: 1024px) { + :root { + --container-padding: 20px; + } - .site-nav a { - font-size: 0.92rem; - } + .feature-grid-inner, + .api-card-grid, + .json-output-layout { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } - .nav-button, - .primary-button, - .secondary-button { - min-height: 44px; - padding: 0 18px; - } + .python-showcase-grid, + .mern-showcase-grid, + .mern-feature-grid, + .python-feature-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } - .hero h1 { - font-size: clamp(2.75rem, 5.6vw, 4.15rem); - } + .json-output-layout { + align-items: start; + } - .hero-text { - max-width: 680px; - font-size: 1.02rem; - } + .feature-grid-inner { + gap: 16px; + } - .feature-grid-inner { - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 16px; - } + .editor-demo-shell { + padding: 20px; + } - .feature-card, - .api-card, - .mern-feature-card, - .install-command-card { - padding: 24px 20px; - } + .editor-demo-shell .nw-editor-surface { + min-height: 460px; + } +} - .json-output-layout { - grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr); - gap: 28px; - align-items: start; - } +@media (max-width: 991px) { + :root { + --container-padding: 18px; + } + + .site-header { + backdrop-filter: blur(14px); + } + + .header-inner { + min-height: 70px; + gap: 16px; + } + + .brand { + gap: 10px; + } + + .brand-logo { + width: 36px; + height: 36px; + } + + .brand-text { + font-size: 1rem; + } + + .site-nav { + gap: 10px; + } + + .site-nav a { + font-size: 0.9rem; + } + + .nav-button { + min-height: 40px; + padding: 0 16px; + } + + .hero h1 { + font-size: clamp(2.45rem, 6vw, 3.45rem); + } + + .hero-text { + font-size: 1rem; + } + + .feature-grid-inner, + .api-card-grid, + .mern-feature-grid, + .python-feature-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .python-showcase-grid, + .mern-showcase-grid { + grid-template-columns: 1fr; + } + + .feature-card, + .api-card, + .mern-feature-card, + .python-feature-card { + padding: 22px 18px; + } + + .python-extras-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .json-output-layout { + grid-template-columns: 1fr; + gap: 24px; + } + + .editor-demo-shell { + padding: 18px; + } + + .editor-demo-shell .nw-editor-surface { + min-height: 400px; + } + + .demo-card-topbar { + grid-template-columns: auto 1fr; + } + + .demo-window-spacer { + display: none; + } - .editor-demo-shell .nw-editor-surface { - min-height: 520px; - } } -@media (max-width: 1024px) { - :root { - --container-padding: 20px; - } - - .feature-grid-inner, - .api-card-grid, - .json-output-layout { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } +@media (max-width: 767px) { + :root { + --container-padding: 16px; + } + + .menu-toggle { + display: inline-flex; + flex: 0 0 auto; + } + + .site-nav { + position: absolute; + top: calc(100% + 10px); + right: var(--container-padding); + left: var(--container-padding); + display: none; + flex-direction: column; + align-items: stretch; + gap: 10px; + padding: 14px; + border: 1px solid rgba(145, 166, 196, 0.24); + border-radius: 18px; + background: rgba(255, 255, 255, 0.94); + box-shadow: 0 20px 44px rgba(7, 17, 31, 0.14); + } + + .site-nav.is-open { + display: flex; + } + + .site-nav a, + .site-nav .nav-button { + width: 100%; + justify-content: center; + } + + .hero { + padding-top: 62px; + padding-bottom: 48px; + } + + .hero h1 { + font-size: clamp(2.15rem, 11vw, 3rem); + line-height: 1.08; + } + + .hero-text { + margin-top: 20px; + font-size: 0.98rem; + line-height: 1.75; + } + + .hero-actions { + margin-top: 26px; + } + + .primary-button, + .secondary-button { + width: 100%; + max-width: 320px; + } + + .feature-grid-inner, + .api-card-grid, + .mern-feature-grid, + .python-feature-grid, + .cta-install-grid, + .python-extras-grid { + grid-template-columns: 1fr; + } + + .section-heading { + margin-bottom: 20px; + } + + .section-heading h2, + .json-copy h2, + .cta-panel h2 { + font-size: clamp(1.9rem, 8vw, 2.5rem); + } + + .editor-demo-card { + border-radius: 22px; + } + + .demo-card-topbar { + min-height: 56px; + padding: 0 16px; + } + + .editor-demo-shell { + padding: 14px; + } + + .editor-demo-shell .nw-editor-toolbar { + top: 0; + } + + .editor-demo-shell .nw-editor-surface { + min-height: 340px; + } + + .editor-demo-shell .nw-editor-status { + flex-direction: column; + align-items: flex-start; + } + + .install-command-shell { + flex-direction: column; + align-items: stretch; + } + + .copy-install-btn { + width: 100%; + } + + .python-extra-head, + .python-framework-card-header { + align-items: flex-start; + flex-direction: column; + } + + .python-inline-copy-btn { + width: auto; + } + + .footer-inner { + flex-direction: column; + align-items: center; + text-align: center; + } + + .footer-nav { + justify-content: center; + } +} - .mern-showcase-grid, - .mern-feature-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } +@media (max-width: 480px) { + :root { + --container-padding: 12px; + } + + .header-inner { + min-height: 64px; + gap: 12px; + } + + .brand-logo { + width: 32px; + height: 32px; + } + + .brand-text { + font-size: 0.95rem; + } + + .menu-toggle { + width: 40px; + height: 40px; + border-radius: 10px; + } + + .hero h1 { + font-size: clamp(1.95rem, 11vw, 2.55rem); + } + + .hero-text, + .section-heading p, + .json-copy p, + .cta-panel p { + font-size: 0.95rem; + } + + .feature-card, + .api-card, + .mern-feature-card, + .python-feature-card, + .cta-panel { + padding: 20px 16px; + } + + .install-command-card { + padding: 18px 16px; + } + + .demo-card-topbar { + padding: 0 14px; + } + + .editor-demo-shell { + padding: 10px; + } + + .editor-demo-shell .nw-editor-surface { + min-height: 300px; + } + + .json-code-panel { + border-radius: 20px; + } + + .json-code-panel pre { + padding: 16px; + font-size: 0.78rem; + } + + .code-example-card .install-command-card-header, + .code-example-card pre, + .python-code-card .install-command-card-header, + .python-code-card pre { + padding-left: 16px; + padding-right: 16px; + } + + .code-example-card pre, + .python-code-card pre { + font-size: 0.8rem; + } + + .cta-logo { + width: 64px; + height: 64px; + } + + .python-flow-card, + .python-framework-card { + padding: 16px; + } + + .python-framework-card { + margin: 0 16px 16px; + } +} - .json-output-layout { - align-items: start; - } +@media (max-width: 360px) { + .hero-actions { + gap: 10px; + } - .feature-grid-inner { - gap: 16px; - } + .primary-button, + .secondary-button, + .nav-button { + min-height: 42px; + padding: 0 14px; + font-size: 0.92rem; + } - .editor-demo-shell { - padding: 20px; - } + .editor-demo-shell .nw-editor-surface { + min-height: 280px; + } +} - .editor-demo-shell .nw-editor-surface { - min-height: 460px; - } +.site-nav-link-active { + color: var(--primary-strong) !important; } -@media (max-width: 991px) { - :root { - --container-padding: 18px; - } +.docs-page { + background: #f8fbff; +} - .site-header { - backdrop-filter: blur(14px); - } +.docs-main { + padding: 32px 0 72px; +} - .header-inner { - min-height: 70px; - gap: 16px; - } +.docs-shell { + display: grid; + grid-template-columns: 260px minmax(0, 1fr) 220px; + gap: 28px; + align-items: start; +} - .brand { +.docs-mobile-toggle { + display: none; + align-items: center; + justify-content: center; gap: 10px; - } + width: 100%; + min-height: 48px; + margin-bottom: 18px; + border: 1px solid rgba(122, 148, 187, 0.24); + border-radius: 14px; + background: rgba(255, 255, 255, 0.9); + color: var(--text); + font-weight: 700; + box-shadow: var(--shadow-soft); +} - .brand-logo { - width: 36px; - height: 36px; - } - - .brand-text { - font-size: 1rem; - } - - .site-nav { - gap: 10px; - } - - .site-nav a { - font-size: 0.9rem; - } - - .nav-button { - min-height: 40px; - padding: 0 16px; - } - - .hero h1 { - font-size: clamp(2.45rem, 6vw, 3.45rem); - } - - .hero-text { - font-size: 1rem; - } - - .feature-grid-inner, - .api-card-grid, - .mern-feature-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .mern-showcase-grid { - grid-template-columns: 1fr; - } - - .feature-card, - .api-card, - .mern-feature-card { - padding: 22px 18px; - } - - .json-output-layout { - grid-template-columns: 1fr; - gap: 24px; - } - - .editor-demo-shell { - padding: 18px; - } - - .editor-demo-shell .nw-editor-surface { - min-height: 400px; - } - - .demo-card-topbar { - grid-template-columns: auto 1fr; - } - - .demo-window-spacer { - display: none; - } - -} - -@media (max-width: 767px) { - :root { - --container-padding: 16px; - } - - .menu-toggle { - display: inline-flex; - flex: 0 0 auto; - } - - .site-nav { - position: absolute; - top: calc(100% + 10px); - right: var(--container-padding); - left: var(--container-padding); - display: none; - flex-direction: column; - align-items: stretch; - gap: 10px; - padding: 14px; - border: 1px solid rgba(145, 166, 196, 0.24); - border-radius: 18px; - background: rgba(255, 255, 255, 0.94); - box-shadow: 0 20px 44px rgba(7, 17, 31, 0.14); - } - - .site-nav.is-open { - display: flex; - } - - .site-nav a, - .site-nav .nav-button { - width: 100%; - justify-content: center; - } - - .hero { - padding-top: 62px; - padding-bottom: 48px; - } - - .hero h1 { - font-size: clamp(2.15rem, 11vw, 3rem); - line-height: 1.08; - } - - .hero-text { - margin-top: 20px; - font-size: 0.98rem; - line-height: 1.75; - } - - .hero-actions { - margin-top: 26px; - } - - .primary-button, - .secondary-button { - width: 100%; - max-width: 320px; - } - - .feature-grid-inner, - .api-card-grid, - .mern-feature-grid { - grid-template-columns: 1fr; - } - - .section-heading { - margin-bottom: 20px; - } - - .section-heading h2, - .json-copy h2, - .cta-panel h2 { - font-size: clamp(1.9rem, 8vw, 2.5rem); - } - - .editor-demo-card { - border-radius: 22px; - } - - .demo-card-topbar { - min-height: 56px; - padding: 0 16px; - } - - .editor-demo-shell { - padding: 14px; - } - - .editor-demo-shell .nw-editor-toolbar { - top: 0; - } - - .editor-demo-shell .nw-editor-surface { - min-height: 340px; - } - - .editor-demo-shell .nw-editor-status { - flex-direction: column; - align-items: flex-start; - } - - .install-command-shell { - flex-direction: column; - align-items: stretch; - } - - .copy-install-btn { - width: 100%; - } - - .footer-inner { - flex-direction: column; - align-items: center; - text-align: center; - } - - .footer-nav { - justify-content: center; - } -} - -@media (max-width: 480px) { - :root { - --container-padding: 12px; - } - - .header-inner { - min-height: 64px; - gap: 12px; - } - - .brand-logo { - width: 32px; - height: 32px; - } - - .brand-text { - font-size: 0.95rem; - } - - .menu-toggle { - width: 40px; - height: 40px; - border-radius: 10px; - } - - .hero h1 { - font-size: clamp(1.95rem, 11vw, 2.55rem); - } - - .hero-text, - .section-heading p, - .json-copy p, - .cta-panel p { - font-size: 0.95rem; - } - - .feature-card, - .api-card, - .mern-feature-card, - .cta-panel { - padding: 20px 16px; - } - - .install-command-card { - padding: 18px 16px; - } - - .demo-card-topbar { - padding: 0 14px; - } - - .editor-demo-shell { - padding: 10px; - } - - .editor-demo-shell .nw-editor-surface { - min-height: 300px; - } - - .json-code-panel { - border-radius: 20px; - } - - .json-code-panel pre { - padding: 16px; - font-size: 0.78rem; - } - - .code-example-card .install-command-card-header, - .code-example-card pre { - padding-left: 16px; - padding-right: 16px; - } - - .code-example-card pre { - font-size: 0.8rem; - } - - .cta-logo { - width: 64px; - height: 64px; - } -} - -@media (max-width: 360px) { - .hero-actions { - gap: 10px; - } - - .primary-button, - .secondary-button, - .nav-button { - min-height: 42px; - padding: 0 14px; - font-size: 0.92rem; - } - - .editor-demo-shell .nw-editor-surface { - min-height: 280px; - } -} - -.site-nav-link-active { - color: var(--primary-strong) !important; -} - -.docs-page { - background: #f8fbff; -} - -.docs-main { - padding: 32px 0 72px; -} - -.docs-shell { - display: grid; - grid-template-columns: 260px minmax(0, 1fr) 220px; - gap: 28px; - align-items: start; -} - -.docs-mobile-toggle { - display: none; - align-items: center; - justify-content: center; - gap: 10px; - width: 100%; - min-height: 48px; - margin-bottom: 18px; - border: 1px solid rgba(122, 148, 187, 0.24); - border-radius: 14px; - background: rgba(255, 255, 255, 0.9); - color: var(--text); - font-weight: 700; - box-shadow: var(--shadow-soft); -} - -.docs-sidebar, -.docs-toc { - position: sticky; - top: 96px; - min-width: 0; -} +.docs-sidebar, +.docs-toc { + position: sticky; + top: 96px; + min-width: 0; +} .docs-sidebar-inner, .docs-toc-inner { - border: 1px solid rgba(122, 148, 187, 0.18); - border-radius: 20px; - background: rgba(255, 255, 255, 0.92); - box-shadow: 0 10px 30px rgba(7, 17, 31, 0.06); - backdrop-filter: blur(16px); + border: 1px solid rgba(122, 148, 187, 0.18); + border-radius: 20px; + background: rgba(255, 255, 255, 0.92); + box-shadow: 0 10px 30px rgba(7, 17, 31, 0.06); + backdrop-filter: blur(16px); } .docs-sidebar-inner { - max-height: calc(100vh - 128px); - padding: 18px 14px; - overflow-y: auto; + max-height: calc(100vh - 128px); + padding: 18px 14px; + overflow-y: auto; } .docs-sidebar-search { - position: sticky; - top: 0; - z-index: 2; - display: flex; - align-items: center; - gap: 10px; - padding: 10px 12px; - margin-bottom: 18px; - border: 1px solid rgba(122, 148, 187, 0.18); - border-radius: 14px; - background: #f7fbff; + position: sticky; + top: 0; + z-index: 2; + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + margin-bottom: 18px; + border: 1px solid rgba(122, 148, 187, 0.18); + border-radius: 14px; + background: #f7fbff; } .docs-sidebar-search input { - width: 100%; - border: 0; - outline: 0; - background: transparent; - color: var(--text); + width: 100%; + border: 0; + outline: 0; + background: transparent; + color: var(--text); } .docs-sidebar-search .material-symbols-outlined { - color: #7a8baa; - font-size: 1.1rem; + color: #7a8baa; + font-size: 1.1rem; } .docs-sidebar-group + .docs-sidebar-group { - margin-top: 18px; + margin-top: 18px; } .docs-sidebar-group-title { - margin: 0 0 8px; - padding: 0 8px; - color: #6d7f99; - font-size: 0.74rem; - font-weight: 800; - letter-spacing: 0.08em; - text-transform: uppercase; + margin: 0 0 8px; + padding: 0 8px; + color: #6d7f99; + font-size: 0.74rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; } .docs-sidebar-link, .docs-toc-links a { - display: block; - border-radius: 12px; - color: #52647e; - transition: background-color 160ms ease, color 160ms ease, box-shadow 160ms ease; + display: block; + border-radius: 12px; + color: #52647e; + transition: background-color 160ms ease, color 160ms ease, box-shadow 160ms ease; } .docs-sidebar-link { - padding: 10px 12px; - font-size: 0.94rem; - line-height: 1.4; + padding: 10px 12px; + font-size: 0.94rem; + line-height: 1.4; } .docs-sidebar-link:hover, .docs-toc-links a:hover { - color: var(--primary-strong); - background: rgba(47, 124, 255, 0.08); + color: var(--primary-strong); + background: rgba(47, 124, 255, 0.08); } .docs-sidebar-link.is-active { - background: linear-gradient(135deg, rgba(47, 124, 255, 0.14), rgba(70, 216, 236, 0.14)); - color: #0d46ae; - box-shadow: inset 0 0 0 1px rgba(47, 124, 255, 0.16); - font-weight: 700; + background: linear-gradient(135deg, rgba(47, 124, 255, 0.14), rgba(70, 216, 236, 0.14)); + color: #0d46ae; + box-shadow: inset 0 0 0 1px rgba(47, 124, 255, 0.16); + font-weight: 700; } .docs-content { - min-width: 0; + min-width: 0; } .docs-article { - padding: 8px 0 0; + padding: 8px 0 0; } .docs-hero { - margin-bottom: 36px; + margin-bottom: 36px; } .docs-kicker { - margin: 0 0 10px; - color: var(--primary-strong); - font-size: 0.78rem; - font-weight: 800; - letter-spacing: 0.08em; - text-transform: uppercase; + margin: 0 0 10px; + color: var(--primary-strong); + font-size: 0.78rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; } .docs-hero h1 { - margin: 0; - color: var(--text); - font-size: clamp(2.6rem, 4vw, 3.8rem); - line-height: 1.05; - letter-spacing: 0; + margin: 0; + color: var(--text); + font-size: clamp(2.6rem, 4vw, 3.8rem); + line-height: 1.05; + letter-spacing: 0; } .docs-lead { - max-width: 800px; - margin: 18px 0 0; - color: var(--text-soft); - font-size: 1.05rem; - line-height: 1.85; + max-width: 800px; + margin: 18px 0 0; + color: var(--text-soft); + font-size: 1.05rem; + line-height: 1.85; } .docs-meta { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin-top: 20px; + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 20px; } .docs-meta span { - display: inline-flex; - align-items: center; - min-height: 38px; - padding: 0 14px; - border: 1px solid rgba(122, 148, 187, 0.18); - border-radius: 999px; - background: rgba(255, 255, 255, 0.78); - color: #536479; - font-size: 0.92rem; - font-weight: 600; + display: inline-flex; + align-items: center; + min-height: 38px; + padding: 0 14px; + border: 1px solid rgba(122, 148, 187, 0.18); + border-radius: 999px; + background: rgba(255, 255, 255, 0.78); + color: #536479; + font-size: 0.92rem; + font-weight: 600; } .docs-badge-row { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-top: 22px; + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 22px; } .docs-badge { - display: inline-flex; - align-items: center; - min-height: 34px; - padding: 0 12px; - border: 1px solid rgba(122, 148, 187, 0.2); - border-radius: 999px; - background: rgba(255, 255, 255, 0.8); - color: #536479; - font-size: 0.84rem; - font-weight: 700; + display: inline-flex; + align-items: center; + min-height: 34px; + padding: 0 12px; + border: 1px solid rgba(122, 148, 187, 0.2); + border-radius: 999px; + background: rgba(255, 255, 255, 0.8); + color: #536479; + font-size: 0.84rem; + font-weight: 700; } .docs-install-card { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - margin-top: 22px; - padding: 18px 20px; - border: 1px solid rgba(122, 148, 187, 0.2); - border-radius: 18px; - background: - radial-gradient(circle at top right, rgba(47, 124, 255, 0.12), transparent 32%), + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-top: 22px; + padding: 18px 20px; + border: 1px solid rgba(122, 148, 187, 0.2); + border-radius: 18px; + background: radial-gradient(circle at top right, rgba(47, 124, 255, 0.12), transparent 32%), linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(243, 248, 255, 0.86)); - box-shadow: var(--shadow-soft); + box-shadow: var(--shadow-soft); } .docs-install-copy { - min-width: 0; + min-width: 0; } .docs-install-copy p { - margin: 0 0 8px; - color: var(--text-soft); - font-size: 0.88rem; - font-weight: 700; + margin: 0 0 8px; + color: var(--text-soft); + font-size: 0.88rem; + font-weight: 700; } .docs-install-copy code { - display: block; - overflow-x: auto; - white-space: nowrap; - padding: 0; - background: transparent; - color: var(--text); - font-family: "JetBrains Mono", monospace; - font-size: 0.96rem; + display: block; + overflow-x: auto; + white-space: nowrap; + padding: 0; + background: transparent; + color: var(--text); + font-family: "JetBrains Mono", monospace; + font-size: 0.96rem; } .docs-section { - scroll-margin-top: 108px; + scroll-margin-top: 108px; } .docs-section + .docs-section { - margin-top: 44px; + margin-top: 44px; } .docs-section h2, .docs-section h3 { - color: var(--text); - letter-spacing: 0; + color: var(--text); + letter-spacing: 0; } .docs-section h2 { - margin: 0 0 16px; - font-size: clamp(1.8rem, 3vw, 2.4rem); - line-height: 1.12; + margin: 0 0 16px; + font-size: clamp(1.8rem, 3vw, 2.4rem); + line-height: 1.12; } .docs-section h3 { - margin: 28px 0 12px; - font-size: 1.2rem; - line-height: 1.25; + margin: 28px 0 12px; + font-size: 1.2rem; + line-height: 1.25; } .docs-section p, .docs-section li { - color: var(--text-soft); - font-size: 1rem; - line-height: 1.8; + color: var(--text-soft); + font-size: 1rem; + line-height: 1.8; } .docs-section ul, .docs-section ol { - margin: 0; - padding-left: 20px; + margin: 0; + padding-left: 20px; } .docs-section ul + p, @@ -1541,358 +1817,448 @@ img { .docs-section .docs-note, .docs-section pre + p, .docs-section pre + ul { - margin-top: 16px; + margin-top: 16px; } .docs-section code { - padding: 0.18rem 0.42rem; - border-radius: 8px; - background: #eaf4ff; - color: #184faa; - font-family: "JetBrains Mono", monospace; - font-size: 0.9em; + padding: 0.18rem 0.42rem; + border-radius: 8px; + background: #eaf4ff; + color: #184faa; + font-family: "JetBrains Mono", monospace; + font-size: 0.9em; } .docs-code-block { - margin-top: 16px; + margin-top: 16px; } .docs-code-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 8px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; } .docs-code-toolbar span { - color: #60738f; - font-size: 0.84rem; - font-weight: 800; - letter-spacing: 0.06em; - text-transform: uppercase; + color: #60738f; + font-size: 0.84rem; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; } .docs-copy-btn { - flex: 0 0 auto; - min-height: 38px; - padding: 0 14px; - border: 1px solid rgba(70, 216, 236, 0.32); - border-radius: 12px; - background: linear-gradient(135deg, rgba(47, 124, 255, 0.14), rgba(70, 216, 236, 0.18)); - color: var(--text); - font-size: 0.88rem; - font-weight: 700; - cursor: pointer; - transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease; + flex: 0 0 auto; + min-height: 38px; + padding: 0 14px; + border: 1px solid rgba(70, 216, 236, 0.32); + border-radius: 12px; + background: linear-gradient(135deg, rgba(47, 124, 255, 0.14), rgba(70, 216, 236, 0.18)); + color: var(--text); + font-size: 0.88rem; + font-weight: 700; + cursor: pointer; + transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease; } .docs-copy-btn:hover, .docs-copy-btn:focus-visible { - transform: translateY(-1px); - border-color: rgba(70, 216, 236, 0.54); - box-shadow: 0 10px 24px rgba(47, 124, 255, 0.12); + transform: translateY(-1px); + border-color: rgba(70, 216, 236, 0.54); + box-shadow: 0 10px 24px rgba(47, 124, 255, 0.12); } .docs-copy-btn:focus-visible { - outline: 2px solid rgba(70, 216, 236, 0.22); - outline-offset: 2px; + outline: 2px solid rgba(70, 216, 236, 0.22); + outline-offset: 2px; } .docs-code { - margin: 0; - padding: 18px 20px; - border: 1px solid rgba(83, 119, 181, 0.24); - border-radius: 18px; - background: linear-gradient(180deg, #091321 0%, #10203a 100%); - color: #8fe7ff; - overflow-x: auto; - box-shadow: 0 18px 44px rgba(6, 14, 30, 0.14); + margin: 0; + padding: 18px 20px; + border: 1px solid rgba(83, 119, 181, 0.24); + border-radius: 18px; + background: linear-gradient(180deg, #091321 0%, #10203a 100%); + color: #8fe7ff; + overflow-x: auto; + box-shadow: 0 18px 44px rgba(6, 14, 30, 0.14); } .docs-code code { - display: block; - min-width: max-content; - padding: 0; - background: transparent; - color: inherit; - font-size: 0.9rem; - line-height: 1.7; + display: block; + min-width: max-content; + padding: 0; + background: transparent; + color: inherit; + font-size: 0.9rem; + line-height: 1.7; } .docs-note { - padding: 16px 18px; - border: 1px solid rgba(47, 124, 255, 0.16); - border-radius: 16px; - background: linear-gradient(135deg, rgba(47, 124, 255, 0.08), rgba(70, 216, 236, 0.08)); + padding: 16px 18px; + border: 1px solid rgba(47, 124, 255, 0.16); + border-radius: 16px; + background: linear-gradient(135deg, rgba(47, 124, 255, 0.08), rgba(70, 216, 236, 0.08)); } .docs-note strong { - color: var(--text); + color: var(--text); +} + +.docs-package-grid, +.docs-framework-grid { + display: grid; + gap: 16px; +} + +.docs-package-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.docs-package-card, +.docs-framework-card { + padding: 20px; + border: 1px solid rgba(122, 148, 187, 0.18); + border-radius: 18px; + background: rgba(255, 255, 255, 0.84); + box-shadow: var(--shadow-soft); +} + +.docs-package-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + margin-bottom: 16px; + border-radius: 14px; + background: linear-gradient(135deg, rgba(47, 124, 255, 0.14), rgba(70, 216, 236, 0.18)); + color: var(--primary-strong); +} + +.docs-package-card h3, +.docs-framework-card h3 { + margin: 0 0 10px; + color: var(--text); + font-size: 1.08rem; +} + +.docs-package-card p, +.docs-framework-card p { + margin: 0; + color: var(--text-soft); + line-height: 1.7; +} + +.docs-install-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.docs-install-grid-compact { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.docs-install-card-stacked { + align-items: flex-start; + flex-direction: column; +} + +.docs-install-card-stacked .docs-install-copy { + width: 100%; +} + +.docs-install-card-stacked .docs-copy-btn { + width: auto; } .docs-feature-grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 16px; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; } .docs-feature-tile { - padding: 20px; - border: 1px solid rgba(122, 148, 187, 0.18); - border-radius: 18px; - background: rgba(255, 255, 255, 0.84); + padding: 20px; + border: 1px solid rgba(122, 148, 187, 0.18); + border-radius: 18px; + background: rgba(255, 255, 255, 0.84); } .docs-feature-tile h3 { - margin-top: 0; + margin-top: 0; } .docs-table-wrap, .docs-table-wrapper { - width: 100%; - overflow-x: auto; + width: 100%; + overflow-x: auto; } .docs-table { - width: 100%; - min-width: 600px; - border-collapse: collapse; - border-spacing: 0; - border: 1px solid rgba(122, 148, 187, 0.18); - border-radius: 18px; - overflow: hidden; - background: rgba(255, 255, 255, 0.92); + width: 100%; + min-width: 600px; + border-collapse: collapse; + border-spacing: 0; + border: 1px solid rgba(122, 148, 187, 0.18); + border-radius: 18px; + overflow: hidden; + background: rgba(255, 255, 255, 0.92); } .docs-table th, .docs-table td { - padding: 14px 16px; - border-bottom: 1px solid rgba(122, 148, 187, 0.14); - text-align: left; - vertical-align: top; + padding: 14px 16px; + border-bottom: 1px solid rgba(122, 148, 187, 0.14); + text-align: left; + vertical-align: top; } .docs-table th { - background: #f3f8ff; - color: var(--text); - font-size: 0.9rem; - font-weight: 700; + background: #f3f8ff; + color: var(--text); + font-size: 0.9rem; + font-weight: 700; } .docs-table td { - color: var(--text-soft); - font-size: 0.94rem; - line-height: 1.65; + color: var(--text-soft); + font-size: 0.94rem; + line-height: 1.65; +} + +.docs-support-table td:nth-child(2), +.docs-support-table td:nth-child(3) { + white-space: nowrap; } .docs-table tbody tr:last-child td { - border-bottom: 0; + border-bottom: 0; } .docs-flow-card { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 10px; - padding: 18px; - border: 1px solid rgba(47, 124, 255, 0.16); - border-radius: 18px; - background: linear-gradient(135deg, rgba(47, 124, 255, 0.08), rgba(70, 216, 236, 0.08)); + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + padding: 18px; + border: 1px solid rgba(47, 124, 255, 0.16); + border-radius: 18px; + background: linear-gradient(135deg, rgba(47, 124, 255, 0.08), rgba(70, 216, 236, 0.08)); } .docs-flow-card-compact { - padding: 16px; + padding: 16px; } .docs-flow-step { - display: inline-flex; - align-items: center; - min-height: 42px; - padding: 0 14px; - border: 1px solid rgba(122, 148, 187, 0.2); - border-radius: 999px; - background: rgba(255, 255, 255, 0.82); - color: var(--text); - font-size: 0.92rem; - font-weight: 700; + display: inline-flex; + align-items: center; + min-height: 42px; + padding: 0 14px; + border: 1px solid rgba(122, 148, 187, 0.2); + border-radius: 999px; + background: rgba(255, 255, 255, 0.82); + color: var(--text); + font-size: 0.92rem; + font-weight: 700; } .docs-flow-arrow { - color: var(--primary-strong); - font-size: 1.05rem; - font-weight: 800; + color: var(--primary-strong); + font-size: 1.05rem; + font-weight: 800; +} + +.docs-code-compact { + margin-top: 0; + padding: 16px; +} + +.docs-framework-card .docs-copy-btn { + margin-top: 14px; } .docs-toc-inner { - padding: 18px 14px; + padding: 18px 14px; } .docs-toc-title { - margin: 0 0 12px; - color: var(--text); - font-size: 0.94rem; - font-weight: 800; + margin: 0 0 12px; + color: var(--text); + font-size: 0.94rem; + font-weight: 800; } .docs-toc-links { - display: grid; - gap: 4px; + display: grid; + gap: 4px; } .docs-toc-links a { - padding: 8px 10px; - font-size: 0.9rem; - line-height: 1.35; + padding: 8px 10px; + font-size: 0.9rem; + line-height: 1.35; } .docs-toc-links a.is-active { - color: #0d46ae; - background: rgba(47, 124, 255, 0.08); - font-weight: 700; + color: #0d46ae; + background: rgba(47, 124, 255, 0.08); + font-weight: 700; } @media (max-width: 1199px) { - .docs-shell { - grid-template-columns: 240px minmax(0, 1fr) 200px; - gap: 22px; - } + .docs-shell { + grid-template-columns: 240px minmax(0, 1fr) 200px; + gap: 22px; + } - .docs-main { - padding-top: 28px; - } + .docs-main { + padding-top: 28px; + } - .docs-meta span { - font-size: 0.88rem; - } + .docs-meta span { + font-size: 0.88rem; + } - .docs-install-card { - padding: 18px; - } + .docs-install-card { + padding: 18px; + } } @media (max-width: 991px) { - .docs-shell { - grid-template-columns: 240px minmax(0, 1fr); - } + .docs-shell { + grid-template-columns: 240px minmax(0, 1fr); + } - .docs-toc { - display: none; - } + .docs-toc { + display: none; + } - .docs-feature-grid { - grid-template-columns: 1fr; - } + .docs-feature-grid, + .docs-package-grid, + .docs-framework-grid, + .docs-install-grid, + .docs-install-grid-compact { + grid-template-columns: 1fr; + } - .docs-install-card { - align-items: stretch; - } + .docs-install-card { + align-items: stretch; + } - .docs-sidebar, - .docs-content { - min-width: 0; - } + .docs-sidebar, + .docs-content { + min-width: 0; + } } @media (max-width: 767px) { - .docs-main { - padding-top: 20px; - padding-bottom: 52px; - } - - .docs-shell { - grid-template-columns: 1fr; - gap: 0; - } - - .docs-mobile-toggle { - display: inline-flex; - } - - .docs-sidebar { - position: static; - top: auto; - display: none; - margin-bottom: 18px; - } - - .docs-sidebar.is-open { - display: block; - } - - .docs-sidebar-inner { - max-height: 60vh; - } - - .docs-hero { - margin-bottom: 30px; - } - - .docs-meta { - flex-direction: column; - align-items: flex-start; - } - - .docs-badge-row { - gap: 8px; - } - - .docs-meta span { - width: 100%; - justify-content: flex-start; - } - - .docs-install-card { - flex-direction: column; - align-items: stretch; - } - - .docs-copy-btn { - width: 100%; - } - - .docs-table { - min-width: 520px; - } + .docs-main { + padding-top: 20px; + padding-bottom: 52px; + } + + .docs-shell { + grid-template-columns: 1fr; + gap: 0; + } + + .docs-mobile-toggle { + display: inline-flex; + } + + .docs-sidebar { + position: static; + top: auto; + display: none; + margin-bottom: 18px; + } + + .docs-sidebar.is-open { + display: block; + } + + .docs-sidebar-inner { + max-height: 60vh; + } + + .docs-hero { + margin-bottom: 30px; + } + + .docs-meta { + flex-direction: column; + align-items: flex-start; + } + + .docs-badge-row { + gap: 8px; + } + + .docs-meta span { + width: 100%; + justify-content: flex-start; + } + + .docs-install-card { + flex-direction: column; + align-items: stretch; + } + + .docs-copy-btn { + width: 100%; + } + + .docs-install-card-stacked .docs-copy-btn { + width: 100%; + } + + .docs-table { + min-width: 520px; + } } @media (max-width: 480px) { - .docs-main { - padding-top: 16px; - } + .docs-main { + padding-top: 16px; + } - .docs-sidebar-inner, - .docs-toc-inner { - border-radius: 18px; - } + .docs-sidebar-inner, + .docs-toc-inner { + border-radius: 18px; + } - .docs-hero h1 { - font-size: clamp(2rem, 11vw, 2.75rem); - } + .docs-hero h1 { + font-size: clamp(2rem, 11vw, 2.75rem); + } - .docs-lead, - .docs-section p, - .docs-section li { - font-size: 0.96rem; - } + .docs-lead, + .docs-section p, + .docs-section li { + font-size: 0.96rem; + } - .docs-code { - padding: 16px; - border-radius: 16px; - } + .docs-code { + padding: 16px; + border-radius: 16px; + } - .docs-code-toolbar { - flex-direction: column; - align-items: stretch; - } + .docs-code-toolbar { + flex-direction: column; + align-items: stretch; + } - .docs-code code { - font-size: 0.82rem; - } + .docs-code code { + font-size: 0.82rem; + } - .docs-table { - min-width: 460px; - } + .docs-table { + min-width: 460px; + } } /* ========================================================= @@ -1918,38 +2284,38 @@ img { .editor-demo-shell .ollow-highlight-wrap, .editor-demo-shell .ollow-styles-control, .editor-demo-shell .ollow-theme-control { - overflow: visible !important; + overflow: visible !important; } .editor-demo-card { - position: relative; - z-index: 20; + position: relative; + z-index: 20; } .editor-demo-shell { - position: relative; - z-index: 30; + position: relative; + z-index: 30; } .editor-demo-shell .nw-editor-card { - position: relative; - z-index: 1; + position: relative; + z-index: 1; } .editor-demo-shell .nw-editor-toolbar { - position: relative; - z-index: 200; + position: relative; + z-index: 200; } .editor-demo-shell .nw-toolbar-row { - position: relative; - z-index: 300; + position: relative; + z-index: 300; } .editor-demo-shell .nw-insert-row { - position: relative; - z-index: 100; - overflow: visible !important; + position: relative; + z-index: 100; + overflow: visible !important; } .editor-demo-shell .ollow-font-control, @@ -1958,8 +2324,8 @@ img { .editor-demo-shell .ollow-highlight-wrap, .editor-demo-shell .ollow-styles-control, .editor-demo-shell .ollow-theme-control { - position: relative; - z-index: 500; + position: relative; + z-index: 500; } .editor-demo-shell .ollow-font-menu, @@ -1968,39 +2334,39 @@ img { .editor-demo-shell .ollow-text-color-popover, .editor-demo-shell .ollow-highlight-popover, .editor-demo-shell .ollow-theme-menu { - position: absolute !important; - top: calc(100% + 8px) !important; - left: 0 !important; - z-index: 99999 !important; - overflow-y: auto !important; - overflow-x: hidden !important; - max-height: min(420px, calc(100vh - 140px)) !important; - background: var(--ollow-surface, #ffffff) !important; - color: var(--ollow-text-strong, #111827) !important; - border: 1px solid var(--nw-border, #d9dde3) !important; - border-radius: 12px !important; - box-shadow: var(--ollow-shadow-float, 0 16px 40px rgba(15, 23, 42, 0.18)) !important; + position: absolute !important; + top: calc(100% + 8px) !important; + left: 0 !important; + z-index: 99999 !important; + overflow-y: auto !important; + overflow-x: hidden !important; + max-height: min(420px, calc(100vh - 140px)) !important; + background: var(--ollow-surface, #ffffff) !important; + color: var(--ollow-text-strong, #111827) !important; + border: 1px solid var(--nw-border, #d9dde3) !important; + border-radius: 12px !important; + box-shadow: var(--ollow-shadow-float, 0 16px 40px rgba(15, 23, 42, 0.18)) !important; } .editor-demo-shell .ollow-font-menu { - min-width: 260px !important; - max-width: min(360px, calc(100vw - 32px)) !important; + min-width: 260px !important; + max-width: min(360px, calc(100vw - 32px)) !important; } .editor-demo-shell .ollow-size-menu { - min-width: 90px !important; - max-width: 120px !important; + min-width: 90px !important; + max-width: 120px !important; } .editor-demo-shell .ollow-styles-menu { - min-width: 248px !important; - max-width: min(340px, calc(100vw - 32px)) !important; + min-width: 248px !important; + max-width: min(340px, calc(100vw - 32px)) !important; } .editor-demo-shell .ollow-text-color-popover, .editor-demo-shell .ollow-highlight-popover { - min-width: 260px !important; - max-width: min(340px, calc(100vw - 32px)) !important; + min-width: 260px !important; + max-width: min(340px, calc(100vw - 32px)) !important; } .editor-demo-shell .ollow-font-menu[hidden], @@ -2009,7 +2375,7 @@ img { .editor-demo-shell .ollow-text-color-popover[hidden], .editor-demo-shell .ollow-highlight-popover[hidden], .editor-demo-shell .ollow-theme-menu[hidden] { - display: none !important; + display: none !important; } .editor-demo-shell .ollow-font-option, @@ -2019,14 +2385,14 @@ img { .editor-demo-shell .ollow-text-color-swatch, .editor-demo-shell .ollow-highlight-reset, .editor-demo-shell .ollow-highlight-swatch { - position: relative; - z-index: 1; + position: relative; + z-index: 1; } .editor-demo-shell .nw-toolbar-button, .editor-demo-shell .nw-insert-pill { - position: relative; - z-index: 1; + position: relative; + z-index: 1; } .editor-demo-shell .ollow-font-menu::-webkit-scrollbar, @@ -2034,7 +2400,7 @@ img { .editor-demo-shell .ollow-styles-menu::-webkit-scrollbar, .editor-demo-shell .ollow-text-color-popover::-webkit-scrollbar, .editor-demo-shell .ollow-highlight-popover::-webkit-scrollbar { - width: 8px; + width: 8px; } .editor-demo-shell .ollow-font-menu::-webkit-scrollbar-thumb, @@ -2042,8 +2408,8 @@ img { .editor-demo-shell .ollow-styles-menu::-webkit-scrollbar-thumb, .editor-demo-shell .ollow-text-color-popover::-webkit-scrollbar-thumb, .editor-demo-shell .ollow-highlight-popover::-webkit-scrollbar-thumb { - background: rgba(100, 116, 139, 0.35); - border-radius: 999px; + background: rgba(100, 116, 139, 0.35); + border-radius: 999px; } .editor-demo-shell .ollow-font-menu::-webkit-scrollbar-track, @@ -2051,43 +2417,156 @@ img { .editor-demo-shell .ollow-styles-menu::-webkit-scrollbar-track, .editor-demo-shell .ollow-text-color-popover::-webkit-scrollbar-track, .editor-demo-shell .ollow-highlight-popover::-webkit-scrollbar-track { - background: transparent; + background: transparent; } @media (max-width: 640px) { - .editor-demo-shell .ollow-font-menu, - .editor-demo-shell .ollow-size-menu, - .editor-demo-shell .ollow-styles-menu, - .editor-demo-shell .ollow-text-color-popover, - .editor-demo-shell .ollow-highlight-popover, - .editor-demo-shell .ollow-theme-menu { - position: fixed !important; - left: 12px !important; - right: 12px !important; - top: auto !important; - bottom: 72px !important; - width: auto !important; - max-width: none !important; - max-height: min(70vh, 520px) !important; - z-index: 99999 !important; - } + .editor-demo-shell .ollow-font-menu, + .editor-demo-shell .ollow-size-menu, + .editor-demo-shell .ollow-styles-menu, + .editor-demo-shell .ollow-text-color-popover, + .editor-demo-shell .ollow-highlight-popover, + .editor-demo-shell .ollow-theme-menu { + position: fixed !important; + left: 12px !important; + right: 12px !important; + top: auto !important; + bottom: 72px !important; + width: auto !important; + max-width: none !important; + max-height: min(70vh, 520px) !important; + z-index: 99999 !important; + } + + .editor-demo-shell .nw-toolbar-row, + .editor-demo-shell .nw-insert-row { + overflow-x: auto !important; + overflow-y: visible !important; + } +} + +@media (max-width: 480px) { + .editor-demo-shell .ollow-font-menu, + .editor-demo-shell .ollow-size-menu, + .editor-demo-shell .ollow-styles-menu, + .editor-demo-shell .ollow-text-color-popover, + .editor-demo-shell .ollow-highlight-popover, + .editor-demo-shell .ollow-theme-menu { + left: 8px !important; + right: 8px !important; + bottom: 68px !important; + } +} + +/* ========================================================= + Editor demo section placement fix + Keeps the dark demo background self-contained after moving + the Try OllowEditor section above the npm / Python sections. + ========================================================= */ +.editor-demo-section { + position: relative; + isolation: isolate; + padding: clamp(44px, 5vw, 64px) 0 clamp(86px, 10vw, 128px); + overflow: visible; +} + +.editor-demo-band { + position: absolute; + top: clamp(230px, 30vw, 360px); + right: 0; + bottom: 0; + left: 0; + height: auto; + z-index: 0; + pointer-events: none; + background: + radial-gradient(circle at 50% 18%, rgba(47, 124, 255, 0.13), transparent 34%), + radial-gradient(circle at 24% 36%, rgba(70, 216, 236, 0.08), transparent 28%), + linear-gradient( + 180deg, + rgba(8, 17, 31, 0) 0%, + rgba(8, 17, 31, 0.78) 18%, + rgba(8, 17, 31, 0.96) 36%, + #08111f 100% + ); +} + +.editor-demo-layout { + position: relative; + z-index: 1; +} + +.editor-demo-card { + position: relative; + z-index: 2; + max-width: 1040px; + margin: 0 auto; + overflow: visible; + box-shadow: + 0 28px 70px rgba(7, 17, 31, 0.24), + 0 0 0 1px rgba(255, 255, 255, 0.48) inset; +} - .editor-demo-shell .nw-toolbar-row, - .editor-demo-shell .nw-insert-row { - overflow-x: auto !important; - overflow-y: visible !important; - } +.editor-demo-shell { + overflow: visible; +} + +.editor-demo-shell .nw-editor-card, +.editor-demo-shell .nw-editor-toolbar, +.editor-demo-shell .nw-toolbar-row, +.editor-demo-shell .nw-insert-row, +.editor-demo-shell .ollow-toolbar-top, +.editor-demo-shell .ollow-font-control, +.editor-demo-shell .ollow-size-control, +.editor-demo-shell .ollow-text-color-wrap, +.editor-demo-shell .ollow-highlight-wrap, +.editor-demo-shell .ollow-styles-control, +.editor-demo-shell .ollow-theme-control { + overflow: visible !important; +} + +.mern-section { + position: relative; + z-index: 1; + padding-top: clamp(54px, 7vw, 84px); +} + +@media (max-width: 1024px) { + .editor-demo-section { + padding-top: clamp(40px, 5vw, 56px); + padding-bottom: clamp(76px, 9vw, 108px); + } + + .editor-demo-band { + top: clamp(220px, 36vw, 330px); + } + + .editor-demo-card { + max-width: 100%; + } +} + +@media (max-width: 767px) { + .editor-demo-section { + padding-top: 40px; + padding-bottom: 76px; + } + + .editor-demo-band { + top: 250px; + } + + .mern-section { + padding-top: 56px; + } } @media (max-width: 480px) { - .editor-demo-shell .ollow-font-menu, - .editor-demo-shell .ollow-size-menu, - .editor-demo-shell .ollow-styles-menu, - .editor-demo-shell .ollow-text-color-popover, - .editor-demo-shell .ollow-highlight-popover, - .editor-demo-shell .ollow-theme-menu { - left: 8px !important; - right: 8px !important; - bottom: 68px !important; - } -} \ No newline at end of file + .editor-demo-section { + padding-bottom: 64px; + } + + .editor-demo-band { + top: 220px; + } +} diff --git a/website/documentation.html b/website/documentation.html index dbac865..b996bcd 100644 --- a/website/documentation.html +++ b/website/documentation.html @@ -3,7 +3,7 @@ - + OllowEditor Documentation @@ -12,7 +12,7 @@ - + @@ -50,68 +50,68 @@
-

Getting Started

+

Overview

Introduction - Installation - CSS Import - Quick Start + Package Options + Feature Overview + Installation Commands
-

JavaScript Usage

- Vanilla JavaScript - Vanilla TypeScript - Package Exports - API Reference +

JavaScript Editor

+ Basic HTML Setup + Editor Initialization + Toolbar Features + Image Upload + Gallery Support + YouTube Embed + Markdown Import/Export + Export HTML + Export PDF + Import DOCX + Export DOCX
-

React & Next.js

+

NPM / MERN Stack

+ NPM Installation + CSS Import + Quick Start + Vanilla JavaScript Usage + TypeScript Usage React Usage React TypeScript Usage Next.js Usage - React Props -
- -
-

MERN / Backend Integration

MERN Stack Usage - Express Image Upload - Node.js API Integration + Express Upload Example NestJS Upload Example - Backend Storage Flow -
- -
-

Editor Features

- Toolbar Features - Image Upload - Gallery Support - YouTube Embed - Table Support - Code Block - Keyboard Shortcuts - Theme Support + API Reference + React Props + Package Exports
-

Import & Export

- Overview - Markdown Import/Export - Export HTML - Export PDF - Import DOCX - Export DOCX +

Python / pip

+ pip Installation + Optional Extras + Django Integration + Django Forms + Django Admin + Django REST Framework + Flask Integration + FastAPI Integration + Static Assets + Template Helpers + Python Backend Flow + Security Notes

Reference

- OllowEditorOptions - OllowEditorCore Methods Browser Support Package Structure Local Development - Build Package + Build Publishing License
@@ -124,7 +124,7 @@

Documentation

OllowEditor Documentation

- OllowEditor is a modern, lightweight, framework-friendly rich text editor for JavaScript, TypeScript, React, MERN, Next.js, Node.js, NestJS, CMS forms, admin dashboards, and publishing workflows. + OllowEditor is a modern, lightweight, framework-friendly rich text editor for JavaScript, TypeScript, React, MERN, Next.js, Node.js, NestJS, Python, Django, Flask, FastAPI, CMS forms, admin dashboards, and publishing workflows.

JavaScript @@ -133,62 +133,439 @@

OllowEditor Documentation

Next.js MERN Stack npm package + Python + pip package + Django + Flask + FastAPI MIT License
-
-
-

Install from npm

- npm i @codefortify/olloweditor -
- -

Introduction

- OllowEditor is a lightweight rich text editor for modern JavaScript applications. It supports Vanilla JavaScript, TypeScript, React, Next.js client-side usage, and backend upload integration while keeping the editor API focused on HTML content. + This page consolidates the standalone browser editor README, the npm package README, and the Python integration README into one reference. The core editor remains a browser-based JavaScript and CSS application. The npm package wraps that editor for JavaScript, TypeScript, React, Next.js, and MERN workflows, while the Python package distributes the compiled browser assets and adds framework helpers for Django, Django REST Framework, Flask, and FastAPI.

+
+ Important: the Python package does not reimplement OllowEditor in Python. It ships the compiled browser bundle, stylesheet, and shared initializer, then integrates them into Python application patterns. +
+
+ +
+

Package Options

+
+
+ +

JavaScript Editor

+

Use the compiled ollow.js and ollow.css files directly in browser pages, CMS forms, and standalone integrations.

+
+
+ +

NPM Package

+

Use @codefortify/olloweditor for JavaScript, TypeScript, React, Next.js, MERN stack, Node.js, and NestJS workflows.

+
+
+ +

Python Package

+

Use olloweditor when a Python web app needs packaged OllowEditor assets plus helpers for Django, DRF, Flask, or FastAPI.

+
+
+
+ +
+

Feature Overview

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureJavaScript/NPMPython/pipNotes
Rich text editingYesYesPython package serves the same browser editor assets.
Toolbar formattingYesYesIncludes typography, lists, links, bookmarks, and formatting controls.
ImagesYesYesFrontend capability documented in the editor and Python READMEs.
Image upload callbackYesFramework-specific backend wiringnpm exposes uploadImage; Python apps receive submitted HTML and can host upload endpoints separately.
GalleryYesYesSupported by the frontend editor capabilities list.
YouTube embedYesYesPython package relies on the same browser bundle.
TablesYesYesSupported by the editor README and npm features list.
Code blocksYesYesSupported in all distributions through the frontend editor.
Markdown import/exportYesYesFrontend workflow; the editor still stores HTML.
Export HTMLYesYesHTML export runs in the browser editor.
Export PDFYesYesDepends on browser print support.
Import DOCXYesYesRequires a browser-compatible parser such as Mammoth at runtime.
Export DOCXYesYesDepends on the configured browser-side exporter or fallback adapter.
React wrapperYesNoProvided by @codefortify/olloweditor/react.
Next.js supportYesNoClient-side loading with ssr: false.
Django integrationBackend-independent onlyYesOllowEditorWidget, OllowEditorField, staticfiles, admin support.
Flask integrationBackend-independent onlyYesExtension, asset blueprint, and Jinja helpers.
FastAPI integrationBackend-independent onlyYesStatic mount helper and template helpers.
+
+
+ +
+

Installation Commands

+
+
+
+

npm

+ npm i @codefortify/olloweditor +
+ +
+
+
+

yarn

+ yarn add @codefortify/olloweditor +
+ +
+
+
+

pnpm

+ pnpm add @codefortify/olloweditor +
+ +
+
+
+

pip

+ pip install olloweditor +
+ +
+
+
+

Django extra

+ pip install "olloweditor[django]" +
+ +
+
+
+

Flask extra

+ pip install "olloweditor[flask]" +
+ +
+
+
+

FastAPI extra

+ pip install "olloweditor[fastapi]" +
+ +
+
+
+

All Python integrations

+ pip install "olloweditor[all]" +
+ +
+
+
+ +
+

Basic HTML Setup

- The package exposes a core factory for browser apps and a React wrapper for controlled editor state. That makes it practical for MERN stack apps, admin dashboards, CMS forms, article workflows, and publishing tools that need clean content entry without locking into a framework-specific editor architecture. + The standalone browser editor uses a synced <textarea>, the compiled stylesheet, and the compiled browser script. The textarea receives the final HTML output after editing.

-
- Package focus: use createOllowEditor in browser-driven JavaScript or TypeScript apps, and use OllowEditor from @codefortify/olloweditor/react in React-based interfaces. +
+
+ browser setup + +
+
<form method="post">
+  <textarea
+  id="ollo-editor"
+  name="content"
+  data-theme="dark"
+  data-persist-theme="true"
+  ><h2>Article title</h2>
+<p>Start writing your story...</p></textarea>
+
+  <button type="submit">Save Article</button>
+</form>
+
+<link rel="stylesheet" href="ollow.css" />
+<script src="ollow.js"></script>
-
-

Installation

-

Install OllowEditor with the package manager you already use in your JavaScript or MERN stack project.

+
+

Editor Initialization

+

+ Initialize the browser editor after the page loads. The global API is available as both OllowEditor and NationWireEditor. The editor README also documents global plugin registration with OllowEditor.registerPlugin(...). +

+
+
+ init + +
+
<script>
+  document.addEventListener("DOMContentLoaded", function () {
+    OllowEditor.init("#ollo-editor", {
+      theme: "dark",
+      persistTheme: true,
+      upload: {
+        imageUrl: "/upload/image",
+        galleryUrl: "/upload/gallery",
+        attachmentUrl: "/upload/attachment",
+        allowFallback: false
+      }
+    });
+  });
+</script>
+
+

+ Confirmed editor options in the browser README include per-editor themes with light, dark, and auto, automatic textarea synchronization, and upload adapter configuration for image, gallery, and attachment flows. +

+
+ +
+

Toolbar Features

+

+ The browser editor README documents a broad toolbar surface including typography, formatting, source mode, export tools, and media blocks. The table below combines the editor README and npm package feature list. +

+
+ + + + + + + + + + + + + + + + + + + + + +
FeatureDescription
Undo / RedoHistory controls for editing sessions.
Font family and sizeApproved typography controls with saved safe classes.
Paragraph and headingsParagraph plus heading levels including H2, H3, and H4.
Bold / Italic / Underline / StrikethroughInline text formatting actions.
Links and bookmarksHyperlink management plus internal anchor insertion.
Lists and pull quotesBullet lists, numbered lists, and quote-style blocks.
HTML modeSwitch between visual editing and sanitized source mode.
Markdown and DOCX toolsMarkdown import/export plus DOCX import/export workflows.
Images, galleries, YouTube embedsMedia blocks with upload and alignment support.
Tables and code blocksStructured content blocks for technical and editorial content.
Responsive toolbarsDesktop menu bar, tablet groups, and mobile overflow drawers.
Keyboard shortcutsShortcut set for formatting, lists, sync, and modal handling.
+
+
+
+

Image Upload

+

+ The browser editor supports local file insertion, URL-based image insertion, drag and drop, and backend upload endpoints. In the standalone editor, uploads are configured with the shared upload adapter. +

- npm - + browser upload +
-
npm install @codefortify/olloweditor
+
<textarea
+  id="ollo-editor"
+  name="body"
+  data-ollow-editor
+  data-image-upload-url="/upload/image"
+  data-gallery-upload-url="/upload/gallery"
+  data-attachment-upload-url="/upload/attachment">
+</textarea>
+
+
+ response contract + +
+
{
+  "url": "/media/editor/images/file.jpg"
+}
+
+{
+  "urls": [
+    "/media/editor/gallery/1.jpg",
+    "/media/editor/gallery/2.jpg"
+  ]
+}
+
+

+ Confirmed adapter behavior includes automatic CSRF header lookup, optional FileReader fallback, and shared handling for image, gallery, attachment, and drag-and-drop flows. +

+
+ +
+

YouTube Embed

+

The browser editor accepts standard YouTube watch, short, and embed URLs and converts them into a wrapped iframe embed.

- pnpm - + embed html +
-
pnpm add @codefortify/olloweditor
+
<figure class="ollow-media ollow-embed">
+  <div class="ollow-video-wrapper">
+    <iframe
+      src="https://www.youtube.com/embed/VIDEO_ID"
+      title="YouTube video player"
+      frameborder="0"
+      allowfullscreen
+      loading="lazy">
+    </iframe>
+  </div>
+  <figcaption>Video caption</figcaption>
+</figure>
+
+
+ +
+

Markdown Import/Export

+

+ The browser editor supports importing Markdown into the current content and exporting editor HTML back to Markdown. Confirmed supported Markdown structures include headings, paragraphs, links, images, lists, blockquotes, horizontal rules, inline code, fenced code blocks, and basic tables. +

+
+
+ markdown api + +
+
const editor = OllowEditor.get("#ollo-editor");
+
+editor.importMarkdown("## Heading\n\nParagraph text", {
+  mode: "replace"
+});
+
+const markdown = editor.exportMarkdown();
+
+
+ +
+

Export HTML

+

+ Export HTML uses sanitized editor content. The editor README confirms support for body-only export and full standalone HTML export with optional embedded styles. +

+
+
+ export html + +
+
const html = editor.exportHTML({
+  fullDocument: true,
+  includeStyles: true,
+  title: "Article Export"
+});
+
+
+ +
+

Export PDF

+

+ PDF export is browser-based. The editor generates a print-ready HTML document and relies on the browser print or save-as-PDF flow rather than producing PDF bytes directly. +

+
+
+ export pdf + +
+
editor.exportPDF({
+  title: "Article",
+  pageSize: "A4",
+  orientation: "portrait",
+  margin: "normal"
+});
+
+
+ Limitation: PDF export depends on browser print support. To suppress browser-added headers and footers, disable them in the print dialog. +
+
+ +
+

Import DOCX

+

+ DOCX import is client-side and expects an optional browser parser such as Mammoth.js to be available when that workflow is used. Supported formatting includes headings, paragraphs, inline formatting, lists, links, tables, and images when the parser supports them. +

+
+
+ import docx + +
+
OllowEditor.init("#editor", {
+  docx: {
+    enabled: true
+  }
+});
+
+editor.importDOCX(file, {
+  mode: "replace",
+  preserveFormatting: true,
+  importImages: true
+});
+
+
+ +
+

Export DOCX

+

+ True DOCX generation depends on an optional browser-compatible exporter. If that adapter is unavailable, OllowEditor can fall back to a Word-compatible HTML document flow instead of pretending to generate a native .docx. +

+
+
+ export docx + +
+
editor.exportDOCX({
+  filename: "article.docx",
+  title: "Article Title",
+  includeImages: true,
+  fallbackToDoc: true
+});
+
+
+ +
+

NPM Installation

+

The npm package ships the core editor, the React wrapper, the packaged stylesheet, and bundled TypeScript declarations.

+
+
+ npm + +
+
npm install @codefortify/olloweditor

CSS Import

-

Import the packaged stylesheet once in the entry file or page where the editor is used.

+

Import the package stylesheet once in the application entry or page where the editor is used.

style.css @@ -200,7 +577,7 @@

CSS Import

Quick Start

-

The core package mounts the editor into a selector or DOM element and returns an OllowEditorCore instance.

+

The npm package mounts into a selector or DOM element and returns an OllowEditorCore instance.

quick start @@ -219,9 +596,8 @@

Quick Start

-
-

Vanilla JavaScript

-

Use the package directly in a module script when you want an editor inside a plain JavaScript page or custom CMS view.

+
+

Vanilla JavaScript Usage

vanilla js @@ -244,96 +620,42 @@

Vanilla JavaScript

-
-

Vanilla TypeScript

-

The package ships type definitions for the core editor and React wrapper, so TypeScript can use the editor without extra ambient declarations.

+
+

TypeScript Usage

+

The npm package bundles its type declarations. No separate @types package is required.

typescript - +
-
import {
+                
import {
   createOllowEditor,
-  type OllowEditorCore,
-  type OllowEditorOptions
+  type OllowEditorOptions,
+  type OllowEditorCore
 } from "@codefortify/olloweditor";
+
 import "@codefortify/olloweditor/style.css";
 
 const options: OllowEditorOptions = {
-  initialHTML: "<p>Typed content</p>",
-  placeholder: "Start writing...",
+  initialHTML: "<p>Hello TypeScript</p>",
+  placeholder: "Write something...",
   onChange: (html: string) => {
     console.log(html);
   }
 };
 
-const editor: OllowEditorCore = createOllowEditor("#editor", options);
-editor.focus();
-
-
- -
-

Package Exports

-

Import the package based on whether you need the browser factory, the React component, or the packaged stylesheet.

-
-
- exports - -
-
import { createOllowEditor } from "@codefortify/olloweditor";
-import { OllowEditor } from "@codefortify/olloweditor/react";
-import "@codefortify/olloweditor/style.css";
+const editor: OllowEditorCore = createOllowEditor("#editor", options);
-
-

API Reference

-

The package surface is intentionally small: one core factory, one core instance type, and one React component export.

-
-
- factory - -
-
createOllowEditor(
-  selector: string | HTMLElement,
-  options?: OllowEditorOptions
-): OllowEditorCore
-
- -

OllowEditorOptions

-
-
- types - -
-
interface OllowEditorOptions {
-  initialHTML?: string;
-  placeholder?: string;
-  readOnly?: boolean;
-  className?: string;
-  onChange?: (html: string) => void;
-  uploadImage?: (file: File) => Promise<string> | string;
-}
-
- -

OllowEditorCore Methods

-
    -
  • getHTML() returns the current editor HTML.
  • -
  • setHTML(html) replaces the current editor content.
  • -
  • focus() focuses the editable surface.
  • -
  • destroy() tears down the editor instance and bindings.
  • -
-
-

React Usage

-

Use the official React wrapper for controlled state and straightforward integration inside forms, dashboards, and content management views.

react - +
-
import { useState } from "react";
+                
import { useState } from "react";
 import { OllowEditor } from "@codefortify/olloweditor/react";
 import "@codefortify/olloweditor/style.css";
 
@@ -353,21 +675,23 @@ 

React Usage

React TypeScript Usage

-

In TypeScript React apps, the wrapper props line up with the package type declarations, including an optional uploadImage callback.

react ts
import { useState } from "react";
-import { OllowEditor } from "@codefortify/olloweditor/react";
-import type { OllowEditorReactProps } from "@codefortify/olloweditor/react";
+import {
+  OllowEditor,
+  type OllowEditorReactProps
+} from "@codefortify/olloweditor/react";
+
 import "@codefortify/olloweditor/style.css";
 
-export default function EditorForm() {
+export default function App() {
   const [content, setContent] = useState<string>("");
 
-  const uploadImage: NonNullable<OllowEditorReactProps["uploadImage"]> = async (file) => {
+  const uploadImage: OllowEditorReactProps["uploadImage"] = async (file) => {
     const formData = new FormData();
     formData.append("image", file);
 
@@ -376,7 +700,7 @@ 

React TypeScript Usage

body: formData }); - const data = await response.json(); + const data: { url: string } = await response.json(); return data.url; }; @@ -394,60 +718,30 @@

React TypeScript Usage

Next.js Usage

-

- OllowEditor uses browser APIs, so in Next.js it should be loaded on the client side with dynamic import and ssr: false. -

+

OllowEditor uses browser APIs, so load it client-side with dynamic import and ssr: false.

next.js
import dynamic from "next/dynamic";
-import { useState } from "react";
 import "@codefortify/olloweditor/style.css";
 
 const OllowEditor = dynamic(
-  () => import("@codefortify/olloweditor/react").then((mod) => mod.OllowEditor),
+  () =>
+    import("@codefortify/olloweditor/react").then((mod) => mod.OllowEditor),
   { ssr: false }
 );
 
-export default function ArticleEditorPage() {
-  const [content, setContent] = useState("");
-
-  return (
-    <OllowEditor
-      value={content}
-      onChange={setContent}
-      placeholder="Write your article..."
-    />
-  );
-}
-
-
- -
-

React Props

-
-
- react props - -
-
interface OllowEditorReactProps {
-  value?: string;
-  onChange?: (html: string) => void;
-  placeholder?: string;
-  uploadImage?: (file: File) => Promise<string> | string;
-  readOnly?: boolean;
-  className?: string;
+export default function Page() {
+  return <OllowEditor placeholder="Write in Next.js..." />;
 }

MERN Stack Usage

-

- OllowEditor works well in MERN stack applications because it can run in React on the frontend and send generated HTML content to an Express, Node.js, or NestJS backend. -

+

Confirmed npm README workflow: React frontend, OllowEditor, HTML emitted through onChange, then a backend API stores and returns content for rendering in the application.

React frontend
@@ -455,19 +749,18 @@

MERN Stack Usage

onChange HTML/content
-
Express API
+
Express or Node API
-
MongoDB database
+
MongoDB/database
Render in frontend/admin panel
-
react form - +
-
import { useState } from "react";
+                
import { useState } from "react";
 import { OllowEditor } from "@codefortify/olloweditor/react";
 import "@codefortify/olloweditor/style.css";
 
@@ -502,7 +795,6 @@ 

MERN Stack Usage

); }
-
express api @@ -529,9 +821,9 @@

MERN Stack Usage

-
-

Express Image Upload

-

README-backed upload flows return URLs to the editor. A small Express endpoint is enough for local disk or proxy-based storage.

+
+

Express Upload Example

+

The npm README documents uploadImage as a backend callback contract rather than a bundled upload provider.

express upload @@ -544,21 +836,11 @@

Express Image Upload

const upload = multer({ dest: "uploads/" }); app.post("/api/uploads/image", upload.single("image"), async (req, res) => { + // Store the uploaded file and generate a public URL. const url = `/uploads/${req.file.filename}`; res.json({ url }); });
-

Connect this endpoint to OllowEditor using the uploadImage callback.

-
- -
-

Node.js API Integration

-

- The frontend editor only needs a URL back from your backend. That backend can be Express, a plain Node.js API route, NestJS, or any server that accepts uploads and returns a JSON payload with the stored file location. -

-
- Typical contract: upload the file, store it using your backend strategy, then return { url: "https://..." } so the editor can place the image into the content. -
@@ -566,9 +848,9 @@

NestJS Upload Example

nestjs - +
-
import { Controller, Post, UploadedFile, UseInterceptors } from "@nestjs/common";
+                
import { Controller, Post, UploadedFile, UseInterceptors } from "@nestjs/common";
 import { FileInterceptor } from "@nestjs/platform-express";
 
 @Controller("api/uploads")
@@ -584,170 +866,402 @@ 

NestJS Upload Example

-
-

Backend Storage Flow

-
-
Frontend editor
- -
uploadImage callback
- -
backend upload API
- -
image URL
- -
editor content
+
+

API Reference

+
+
+ factory + +
+
createOllowEditor(
+  selector: string | HTMLElement,
+  options?: OllowEditorOptions
+): OllowEditorCore
-

- OllowEditor does not force a storage provider. You can use local storage, S3, Cloudinary, Supabase Storage, DigitalOcean Spaces, or any custom backend as long as the final upload step returns a usable URL. -

+
+
+ options + +
+
interface OllowEditorOptions {
+  initialHTML?: string;
+  placeholder?: string;
+  readOnly?: boolean;
+  className?: string;
+  onChange?: (html: string) => void;
+  uploadImage?: (file: File) => Promise<string> | string;
+}
+
+
    +
  • getHTML() returns the current editor HTML.
  • +
  • setHTML(html) replaces the current editor content.
  • +
  • focus() moves focus into the editor surface.
  • +
  • destroy() removes the editor instance and listeners.
  • +
-
-

Toolbar Features

-
- - - - - - - - - - - - - - - - - - - - - - - -
FeatureDescription
Bold / Italic / Underline / StrikethroughFormat selected text for common writing workflows.
HeadingsSwitch between paragraph text and heading levels used in structured content.
ListsCreate bullet and numbered lists inside articles, admin forms, and CMS content.
LinksInsert hyperlinks and unlink content when references change.
ImagesInsert uploaded images or image URLs into the editor.
GalleryCreate a multiple-image gallery block with title and note support.
YouTube EmbedRender supported YouTube URLs inside a safe embed wrapper.
TablesInsert table content inside a scroll-safe wrapper for smaller viewports.
Code BlockAdd preformatted code blocks for technical articles and developer docs.
Markdown Import / ExportConvert between Markdown workflows and editor HTML.
Export HTMLExport sanitized HTML content for downstream publishing.
Export PDFUse the browser print workflow to create a PDF export.
DOCX Import / ExportImport Word content and export DOCX or fallback Word-compatible output.
Theme ControlsSwitch between light, dark, and auto theme modes.
+
+

React Props

+
+
+ react props + +
+
interface OllowEditorReactProps {
+  value?: string;
+  onChange?: (html: string) => void;
+  placeholder?: string;
+  uploadImage?: (file: File) => Promise<string> | string;
+  readOnly?: boolean;
+  className?: string;
+}
-
-

Image Upload

+
+

Package Exports

+
+
+ exports + +
+
import { createOllowEditor } from "@codefortify/olloweditor";
+import { OllowEditor } from "@codefortify/olloweditor/react";
+import "@codefortify/olloweditor/style.css";
+
+
+ +
+

pip Installation

+

The base Python package installs the packaged browser assets and framework-independent resource helpers only.

+
+
+ pip + +
+
pip install olloweditor
+
+
+ Current README note: production PyPI publication has not been completed yet. The Python README recommends local wheel or editable installs during current development workflows. +
+
+ +
+

Optional Extras

+

Install only the extra your Python application needs.

+
+
+
+

Django

+ pip install "olloweditor[django]" +
+ +
+
+
+

Django REST Framework

+ pip install "olloweditor[drf]" +
+ +
+
+
+

Flask

+ pip install "olloweditor[flask]" +
+ +
+
+
+

FastAPI

+ pip install "olloweditor[fastapi]" +
+ +
+
+
+

All integrations

+ pip install "olloweditor[all]" +
+ +
+
+
+ +
+

Django Integration

- The package-level upload hook is uploadImage(file). Return a string or a promise that resolves to the final image URL and OllowEditor will insert that image into the current content. + The Python package includes OllowEditorWidget, OllowEditorField, staticfiles integration, and admin support. It keeps a textarea synchronized with HTML so normal Django form processing continues to work.

- upload callback - + django field +
-
const editor = createOllowEditor("#editor", {
-  uploadImage: async (file) => {
-    const formData = new FormData();
-    formData.append("image", file);
+                
from django.db import models
+from olloweditor.integrations.django import OllowEditorField
 
-    const response = await fetch("/api/uploads/image", {
-      method: "POST",
-      body: formData
-    });
 
-    const data = await response.json();
-    return data.url;
-  }
-});
+class Article(models.Model): + title = models.CharField(max_length=255) + content = OllowEditorField()
- +
+

Django Forms

+
+
+ django form + +
+
from django import forms
+from olloweditor.integrations.django import OllowEditorWidget
 
-            
-

YouTube Embed

-

Supported YouTube links are normalized into safe iframe embeds so articles can include videos without custom rendering glue.

-
+from .models import Article -
-

Table Support

-

Tables render inside a scroll wrapper, which keeps wide tabular content usable on smaller screens and prevents page-level overflow.

-
-
-

Code Block

-

Code blocks are part of the supported block toolset and work well for engineering blogs, internal guides, and technical publishing workflows.

+class ArticleForm(forms.ModelForm): + content = forms.CharField( + widget=OllowEditorWidget( + options={ + "theme": "auto", + } + ) + ) + + class Meta: + model = Article + fields = ["title", "content"]
+
+
+
+ template + +
+
<form method="post">
+    {% csrf_token %}
+    {{ form.media }}
+    {{ form.as_p }}
+    <button type="submit">Save article</button>
+</form>
+
-
-

Keyboard Shortcuts

-
- - - - - - - - - - - - - - - - - - -
ShortcutAction
Ctrl/Cmd + BBold
Ctrl/Cmd + IItalic
Ctrl/Cmd + UUnderline
Ctrl/Cmd + KInsert or edit link
Ctrl/Cmd + Shift + 7Numbered list
Ctrl/Cmd + Shift + 8Bullet list
Ctrl/Cmd + Shift + CCode block
Ctrl/Cmd + SSync editor HTML and prevent the browser save dialog
EscClose the active modal or floating toolbar
+
+

Django Admin

+

+ Once olloweditor.apps.OllowEditorConfig is installed, OllowEditorField uses OllowEditorWidget in generated ModelForms, including standard Django admin form construction. +

+
+
+ installed apps + +
+
INSTALLED_APPS = [
+    "django.contrib.admin",
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.sessions",
+    "django.contrib.messages",
+    "django.contrib.staticfiles",
+    "olloweditor.apps.OllowEditorConfig",
+]
-
-

Theme Support

-

README-backed theme support includes light, dark, and auto modes with optional theme persistence per editor instance.

+
+

Django REST Framework

+

+ DRF does not render the browser editor for external API clients. It accepts the HTML string generated by an OllowEditor frontend through OllowEditorHTMLField. +

+
+
+ serializer + +
+
from rest_framework import serializers
+from olloweditor.integrations.drf import OllowEditorHTMLField
+
+
+class ArticleSerializer(serializers.Serializer):
+    title = serializers.CharField()
+    content = OllowEditorHTMLField(
+        allow_blank=True,
+        required=False,
+    )
+
+
+
+ sanitizer + +
+
def sanitize_article_html(value: str) -> str:
+    return trusted_html_sanitizer.clean(value)
+
+
+class ArticleSerializer(serializers.Serializer):
+    title = serializers.CharField()
+    content = OllowEditorHTMLField(
+        sanitizer=sanitize_article_html,
+    )
+
-
-

Import and Export

-

OllowEditor includes HTML, PDF, Markdown, and DOCX workflows for teams moving content between editors, admin panels, publishing systems, and offline review tools.

-
    -
  • Export HTML for clean downstream rendering.
  • -
  • Export PDF through the browser print flow.
  • -
  • Import and export Markdown for technical content workflows.
  • -
  • Import DOCX with a browser-side parser when available.
  • -
  • Export DOCX through the configured runtime exporter or fallback mode.
  • -
+
+

Flask Integration

+

The Python package exposes a Flask extension, packaged asset blueprint, and Jinja helpers.

+
+
+ flask app + +
+
from flask import Flask, render_template, request
+from olloweditor.integrations.flask import OllowEditor
+
+
+app = Flask(__name__)
+olloweditor = OllowEditor(app)
+
+
+@app.route("/", methods=["GET", "POST"])
+def index():
+    content = ""
+
+    if request.method == "POST":
+        content = request.form.get("content", "")
+
+    return render_template("index.html", content=content)
+
-
-

Markdown Import / Export

-

Markdown support is intended for conversion workflows. The editor still stores and emits HTML content as its main output.

+
+

FastAPI Integration

+

The Python package includes a static mount helper and template helper registration pattern for FastAPI applications.

+
+
+ fastapi app + +
+
from fastapi import FastAPI, Request
+from fastapi.templating import Jinja2Templates
+
+from olloweditor.integrations.fastapi import (
+    mount_olloweditor,
+    olloweditor_assets,
+)
+
+
+app = FastAPI()
+mount_olloweditor(app)
+
+templates = Jinja2Templates(directory="templates")
+templates.env.globals["olloweditor_assets"] = olloweditor_assets
+
+
+@app.get("/")
+def index(request: Request):
+    return templates.TemplateResponse(
+        request=request,
+        name="index.html",
+        context={},
+    )
+
-
-

Export HTML

-

Export HTML produces sanitized editor output so you can store or render the content directly in a frontend or backend publishing pipeline.

+
+

Static Assets

+

Installing olloweditor gives the Python package three packaged frontend assets plus resource helpers.

+
+
+ assets + +
+
olloweditor.browser.js
+olloweditor.css
+olloweditor-init.js
+
+get_static_root()
+get_asset_path(filename)
+asset_exists(filename)
+
+

+ Confirmed framework behavior: Django serves these through staticfiles, Flask through the extension blueprint, FastAPI through StaticFiles, and the base package exposes resource helpers through olloweditor.resources. +

-
-

Export PDF

-

PDF export depends on browser print support and uses the native print-to-PDF flow rather than generating PDF bytes directly inside the package.

+
+

Template Helpers

+

Flask and FastAPI both expose helper functions that inject the packaged asset tags into server-rendered templates.

+
+
+

Flask

+
<head>
+  {{ olloweditor_assets() }}
+</head>
+ +
+
+

FastAPI

+
<head>
+  {{ olloweditor_assets() }}
+</head>
+ +
+
-
-

Import DOCX

-

DOCX import expects a browser-compatible parser such as Mammoth to be available when that workflow is used.

+
+

Python Backend Flow

+

The Python package keeps the browser editor tied to normal form posts or JSON requests instead of introducing a Python-native editing engine.

+
+
Textarea
+ +
OllowEditor browser UI
+ +
synchronized HTML
+ +
normal form post or JSON payload
+ +
Python backend
+
+
+
+ submitted content + +
+
# Django
+content = request.POST.get("content", "")
+
+# Flask
+content = request.form.get("content", "")
+
+# FastAPI
+from typing import Annotated
+from fastapi import Form
+
+def create_article(
+    content: Annotated[str, Form()],
+):
+    return {"content": content}
+
-
-

Export DOCX

-

DOCX export depends on the configured exporter available to the editor at runtime. When a real DOCX exporter is not available, the editor can fall back to a Word-compatible document flow.

+
+

Security Notes

+

+ Because OllowEditor generates HTML, backend applications should validate and sanitize untrusted HTML before rendering it. The Python README is explicit that client-side cleanup is not a complete security boundary and that the package does not claim arbitrary HTML is safe automatically. +

+
    +
  • Define allowed tags, attributes, URL schemes, image sources, and embed providers.
  • +
  • Validate upload authorization, CSRF, MIME type, extension, file size, storage destination, and rate limits separately.
  • +
  • Use Django safe, Jinja |safe, Markup, or equivalent only after sanitization or explicit trust decisions.
  • +

Browser Support

+

The npm README lists the following modern browser targets:

  • Chrome
  • Edge
  • @@ -761,26 +1275,31 @@

    Browser Support

    Package Structure

    - dist + npm package
    -
    dist/
    -├── olloweditor.es.js
    -├── olloweditor.cjs
    -├── olloweditor-react.es.js
    -├── olloweditor-react.cjs
    -├── olloweditor.css
    -├── index.d.ts
    -└── react.d.ts
    +
    olloweditor/
    +├── src/
    +│   ├── core/
    +│   ├── react/
    +│   ├── styles/
    +│   └── types/
    +├── dist/
    +├── examples/
    +├── website/
    +├── package.json
    +├── vite.config.js
    +├── tsconfig.json
    +└── README.md

Local Development

-

For package development, clone the repository and install dependencies before starting the local development workflow.

+

The npm README documents the standard frontend development flow, and the Python README adds the Python-specific environment setup.

- local dev + frontend dev
git clone https://github.com/CodeFortifyCloud/olloweditor.git
@@ -788,36 +1307,69 @@ 

Local Development

npm install npm run dev
+
+
+ python dev + +
+
npm ci
+npm run build
+npm run typecheck
+npm test
+npm run build:python-assets
+npm run verify:python-assets
+
+cd python
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install --upgrade pip
+python -m pip install -e ".[all,dev,test]"
+
-
-

Build Package

+
+

Build

- build + npm build
npm run build
-

The build output should produce the distributable JavaScript, React, CSS, and type definition files shown in the package structure above.

+
+
+ python distributions + +
+
cd python
+rm -rf build dist src/*.egg-info
+python -m build
+python -m twine check dist/*
+

Publishing

- publish - + npm publish +
-
npm login
+                
npm pack --dry-run
+npm pack
+npm login
 npm publish --access public
-

Use semantic versioning for releases:

-
    -
  • patch for bug fixes
  • -
  • minor for new features
  • -
  • major for breaking changes
  • -
+
+
+ python wheel verification + +
+
pip install dist/olloweditor-<version>-py3-none-any.whl
+python scripts/check_wheel_contents.py dist/*.whl
+python scripts/verify_wheel_installs.py dist/*.whl
+
+

The Python README documents build and verification commands, but it does not publish a final PyPI release command in the same way the npm README does.

@@ -832,18 +1384,17 @@

License

On this page

diff --git a/website/index.html b/website/index.html index 3ec4d1f..4222a9d 100644 --- a/website/index.html +++ b/website/index.html @@ -46,8 +46,18 @@

Free. Lightweight. Developer-first.

A Free, Block-Style Editor with Clean JSON Output

- OllowEditor helps JavaScript and MERN stack developers build clean, structured content with a simple and powerful editing experience for modern web applications. + OllowEditor helps JavaScript, MERN, and Python developers build clean, structured content with a simple and powerful editing experience for modern web applications.

+
+ npm package + pip package + JavaScript + Python + MERN + Django + Flask + FastAPI +
Get Started View Github @@ -83,6 +93,44 @@

Developer Friendly

+
+
+
+
+

Live Demo

+

Try OllowEditor

+

Write, format, insert media, and export structured content directly from the demo.

+
+
+ +
+ +
+
+
+
+
@@ -157,44 +205,145 @@

CMS & Admin Forms

-
-
-
-
-

Live Demo

-

Try OllowEditor

-

Write, format, insert media, and export structured content directly from the demo.

+
+
+
+

Pip Package

+

OllowEditor for Python

+

Use OllowEditor in Django, Django REST Framework, Flask, and FastAPI without managing a separate npm asset pipeline.

-
- -
- + +
+
+
+
+ + Install from pip +
+

+ The Python package ships the compiled OllowEditor browser bundle, stylesheet, and shared initialization script, then adds framework helpers for forms, templates, serializer fields, and static asset serving. +

+
+ Python + Django + DRF + Flask + FastAPI + pip package +
+
+ pip install olloweditor + +
+
+
+
+ Django + +
+ pip install "olloweditor[django]" +
+
+
+ Django REST Framework + +
+ pip install "olloweditor[drf]" +
+
+
+ Flask + +
+ pip install "olloweditor[flask]" +
+
+
+ FastAPI + +
+ pip install "olloweditor[fastapi]" +
+
+
+ All integrations + +
+ pip install "olloweditor[all]" +
+
+
+ +
+
+ + Django quick start +
+
from django import forms
+from olloweditor.integrations.django import OllowEditorWidget
+from .models import Article
+
+class ArticleForm(forms.ModelForm):
+    content = forms.CharField(
+        widget=OllowEditorWidget(
+            options={
+                "theme": "auto",
+            }
+        )
+    )
+
+    class Meta:
+        model = Article
+        fields = ["title", "content"]
+ +
+
+ FastAPI helper + +
+
from fastapi import FastAPI
+from olloweditor.integrations.fastapi import mount_olloweditor
+
+app = FastAPI()
+mount_olloweditor(app)
+
+
+
+ +
+
+ +

Django Forms & Admin

+

Use OllowEditorWidget and OllowEditorField in Django forms, models, and admin workflows.

+
+
+ +

Django REST Framework

+

Accept OllowEditor-generated HTML through serializer fields and apply your own sanitizer when needed.

+
+
+ +

Flask Integration

+

Use the Flask extension, asset blueprint, and Jinja helpers to add OllowEditor to server-rendered forms.

+
+
+ +

FastAPI Helpers

+

Mount packaged static assets and use template helpers for FastAPI-powered pages.

+
+ +
@@ -285,12 +434,26 @@

Block Tunes

Start building with OllowEditor

- A clean, modern editor experience for your next JavaScript or MERN stack project. Install from npm, customize blocks, and ship structured content faster. + A clean, modern editor experience for your next JavaScript, MERN, or Python project. Install from npm or pip, customize blocks, and ship structured content faster.

+
+
+
+ npm i @codefortify/olloweditor + +
+
+
+
+ pip install olloweditor + +
+
+
diff --git a/website/script.js b/website/script.js index 0a4c41b..9dc46da 100644 --- a/website/script.js +++ b/website/script.js @@ -3,8 +3,12 @@ document.addEventListener("DOMContentLoaded", () => { if (!text) return false; if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") { - await navigator.clipboard.writeText(text); - return true; + try { + await navigator.clipboard.writeText(text); + return true; + } catch (_error) { + // Fall through to the textarea fallback on permission or browser failures. + } } const fallbackTextarea = document.createElement("textarea"); @@ -195,6 +199,7 @@ document.addEventListener("DOMContentLoaded", () => { if (!(button instanceof HTMLButtonElement)) return; const originalLabel = button.textContent ? button.textContent.trim() : "Copy"; + let resetTimeoutId = null; button.addEventListener("click", async () => { const explicitCopy = button.getAttribute("data-copy"); @@ -212,9 +217,13 @@ document.addEventListener("DOMContentLoaded", () => { const copied = await copyTextToClipboard(textToCopy); if (!copied) return; + if (resetTimeoutId) { + window.clearTimeout(resetTimeoutId); + } button.textContent = "Copied!"; - window.setTimeout(() => { + resetTimeoutId = window.setTimeout(() => { button.textContent = originalLabel; + resetTimeoutId = null; }, 1500); } catch (_error) { // Swallow clipboard failures to avoid noisy console errors on unsupported browsers.