diff --git a/.gitignore b/.gitignore index e170cbd..4e7ad72 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .databricks/ -build/ +/build/ +docs/build/ dist/ __pycache__/ *.egg-info diff --git a/VERSION b/VERSION index 8c20c52..759e855 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.20.0 \ No newline at end of file +v0.21.0 diff --git a/docs/Makefile b/docs/Makefile index 21073bb..4fc15a1 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -35,7 +35,7 @@ spelling: html-multiversion: @./scripts/build_versioned_docs.sh -# Local preview build including all local branches. +# Local preview build: current branch as local-branch-preview (current stays main). html-multiversion-preview: @./scripts/build_versioned_docs.sh --preview diff --git a/docs/README.md b/docs/README.md index b8b6da4..a6ba15d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,7 +9,7 @@ This folder contains the Sphinx documentation source and build tooling for local - [Multiversion Implementation (No sphinx-multiversion)](#multiversion-implementation-no-sphinx-multiversion) - [What Gets Built](#what-gets-built) - [Version Selection Rules](#version-selection-rules) - - [How the Sidebar Switcher Works](#how-the-sidebar-switcher-works) + - [How the Version Dropdown Works](#how-the-version-dropdown-works) - [Purge and Rebuild Behavior](#purge-and-rebuild-behavior) - [Local Build and Testing](#local-build-and-testing) - [GitHub Pages Deployment](#github-pages-deployment) @@ -40,7 +40,7 @@ The `docs/Makefile` supports these explicit targets: - `make html-multiversion` - Builds versioned docs without `sphinx-multiversion` using `scripts/build_versioned_docs.sh`. - `make html-multiversion-preview` - - Same as above, plus local branches for preview/testing. + - Same as above, plus the current git branch as `local-branch-preview`. Catch-all behavior is enabled: @@ -59,6 +59,10 @@ Versioned docs are generated by: - `main` branch is always built and published as `current`. - Selected release tags are built from git tags. +- **Docs IA cutover at v0.21.0:** + - **`current` and tags `>= v0.21.0`** use **main's** `docs/conf.py` with that ref's `docs/source` (sphinx-immaterial / section-folder IA). + - **Tags `<= v0.20.x`** use **`v0.20.0`'s** `docs/conf.py` (RTD / flat layout) with each tag's own `docs/source`. +- **`local-branch-preview`** (optional) uses the current branch's own conf + source (pre-merge validation). - Output layout: - `docs/build/html/current/index.html` - `docs/build/html/vX.Y.Z/index.html` @@ -70,9 +74,10 @@ Versioned docs are generated by: `scripts/select_versions.py` applies these rules: 1. Identify the highest major version present in tags. -2. Include the latest patch release for the last 5 minor series in that major. -3. Include the latest available release for each of the last 3 majors. -4. De-duplicate and sort descending (newest first). +2. For major `0`, include the latest patch release for every minor from the current minor down to `0.12` (inclusive). +3. For other majors, include the latest patch release for the last 5 minor series in that major. +4. Include the latest available release for each of the last 3 majors. +5. De-duplicate and sort descending (newest first). Example ordering: @@ -82,13 +87,27 @@ Example ordering: - `v0.14.0` - `v0.13.0` - `v0.12.1` +- … through `v0.12.x` when older minors exist -### How the Sidebar Switcher Works +### How the Version Dropdown Works -- `scripts/build_versioned_docs.py` writes `versions.json` with `name`, `url`, and `is_latest`. -- `docs/conf.py` loads this manifest via `DOCS_VERSIONS_FILE`. -- `docs/_templates/versions.html` renders the version list in the RTD sidebar. -- Links target `.../index.html` explicitly to avoid directory-listing behavior in local `file://` browsing. +- `scripts/build_versioned_docs.py` writes a **superset** `versions.json` with: + - mike / sphinx-immaterial fields: `version`, `title`, `aliases` + - legacy RTD fields: `name`, `display_version`, `url`, `status`, `is_latest`, `release_date` +- RTD-era refs load the manifest at build time via `DOCS_VERSIONS_FILE` into `versions.html`. +- Immaterial refs enable `version_dropdown` and fetch the parent `../versions.json` over HTTP (`version_json` in `conf.py`). +- Use a local HTTP server to test the immaterial switcher (see below). + +### Preview builds + +```bash +make -C docs html-multiversion-preview +``` + +- Still publishes `current` from `main`. +- Also builds the **current git branch** once as `local-branch-preview` (skipped if you are on `main` or detached HEAD). +- To review a feature branch before merge, check it out, run preview, then select `local-branch-preview` in the version menu. +- Preview builds from the **working tree** (uncommitted conf/theme/source edits are included). ## Purge and Rebuild Behavior @@ -122,7 +141,7 @@ make -C docs spelling make -C docs html-multiversion ``` -1. If you need to test branch previews in the version menu: +1. If you need to preview the current branch against `main` / release tags: ```bash make -C docs html-multiversion-preview @@ -132,7 +151,7 @@ Practical rule of thumb: - Doc text/layout change only: `html` + `spelling` - Versioning/switcher/build-script change: `html` + `spelling` + `html-multiversion` -- Branch preview verification: add `html-multiversion-preview` +- Branch theme/IA preview: `html-multiversion-preview`, then select `local-branch-preview` ### Open docs @@ -145,7 +164,9 @@ cd docs/build/html python3 -m http.server 8000 ``` -Then open [http://localhost:8000](http://localhost:8000). +Then open [http://localhost:8000/current/index.html](http://localhost:8000/current/index.html) to exercise the version dropdown. + +Note: `make html` builds a single-version tree without a parent-level `versions.json`; use `html-multiversion` to test version switching. ## GitHub Pages Deployment diff --git a/docs/conf.py b/docs/conf.py index afb2892..7015d14 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,18 +5,27 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -import json import os -import subprocess import sys -from datetime import datetime sys.path.append(os.path.abspath(".")) # Ensure the script is discoverable from custom_markdown_builder import CustomMarkdownTranslator +# Ensure sphinx-immaterial registers domain synopses before env init. +from sphinx.domains.python import PythonDomain +import sphinx.domains.std + +PythonDomain.initial_data.setdefault("synopses", {}) +sphinx.domains.std.StandardDomain.initial_data.setdefault("synopses", {}) +import sphinx_immaterial.apidoc.python.synopses # noqa: F401 +import sphinx_immaterial.apidoc.generic_synopses # noqa: F401 + project = 'Lakeflow Framework' copyright = '2026, Databricks' author = 'Erik Seefeld, Haille Woldegebriel' +# Site title in the header / — project name only (no version / "documentation"). +html_title = project + # Read version from the VERSION file at the repo root so conf.py never # needs a manual update when a release is cut. _here = os.path.dirname(os.path.abspath(__file__)) @@ -36,144 +45,463 @@ 'sphinx_design', 'myst_parser', 'sphinx_tabs.tabs', + 'sphinx_copybutton', 'custom_markdown_builder', "sphinxcontrib.spelling", + # Theme directives (md-mermaid, task-list) register via html_theme setup, + # which does not run for the spelling builder — load them explicitly. + "sphinx_immaterial.mermaid_diagrams", + "sphinx_immaterial.task_lists", ] autosectionlabel_prefix_document = True +# MyST — task-list checkboxes (Quick Start prerequisites) and fenced directives +myst_enable_extensions = [ + "colon_fence", + "tasklist", +] +myst_enable_checkboxes = True + +# sphinx-immaterial ``.. task-list::`` (RST) — interactive like MyST checkboxes +clickable_checkbox = True + templates_path = ['_templates', 'source/_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +# Code blocks: when :linenos: is set, use Cursor/VS Code-style gutter (table layout). +html_codeblock_linenos_style = 'table' + +# Copy button — WAF-style command snippets only (not dark spec/code panels) +copybutton_selector = "div.lf-command-block div.highlight pre" +copybutton_prompt_is_regexp = True +copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,6}\.\.\. | {5,8}: " +copybutton_only_copy_prompt_lines = False +copybutton_remove_prompts = True + # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -import sphinx_rtd_theme intersphinx_mapping = { 'rtd': ('https://docs.readthedocs.io/en/stable/', None), 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), } -html_theme = "sphinx_rtd_theme" +html_theme = "sphinx_immaterial" html_baseurl = "https://databricks-solutions.github.io/lakeflow_framework/" +html_logo = "source/_static/lff-logo.png" +html_favicon = "source/_static/lff-logo.png" -# Inject the version switcher into the sidebar. -html_sidebars = { - '**': [ - 'versions.html', - 'globaltoc.html', - 'relations.html', - 'sourcelink.html', - 'searchbox.html', +html_theme_options = { + "site_url": html_baseurl, + "repo_url": "https://github.com/databricks-solutions/lakeflow_framework", + "repo_name": "lakeflow_framework", + "icon": { + "repo": "fontawesome/brands/github", + }, + "features": [ + "navigation.instant", + "navigation.top", + "navigation.tabs", + "navigation.tabs.sticky", + "toc.integrate", + "toc.follow", + "search.suggest", + "content.code.copy", + ], + # Version dropdown needs parent-level versions.json (written by html-multiversion). + # Flat ``make html`` builds omit it; fetching a missing manifest breaks search init. + "version_dropdown": os.environ.get("DOCS_CURRENT_VERSION") is not None, + # Parent of each version folder (mike default). Plain "versions.json" + # resolves inside the version dir and 404s in multiversion layouts. + "version_json": "../versions.json", + "palette": [ + { + "media": "(prefers-color-scheme: light)", + "scheme": "default", + "primary": "red", + "accent": "blue-grey", + "toggle": { + "icon": "material/brightness-7", + "name": "Switch to dark mode", + }, + }, + { + "media": "(prefers-color-scheme: dark)", + "scheme": "slate", + "primary": "red", + "accent": "blue-grey", + "toggle": { + "icon": "material/brightness-4", + "name": "Switch to light mode", + }, + }, ], + "font": { + "text": "Roboto", + "code": "Roboto Mono", + }, } html_static_path = ['source/_static'] html_css_files = [ + 'databricks-theme.css', 'custom.css', ] +html_js_files = [ + 'mermaid-zoom.js', +] -# Suppress generic Sphinx "Last updated" text; custom version metadata is shown. +# Suppress generic Sphinx "Last updated" text. html_last_updated_fmt = None +html_context = { + "docs_current_version": os.environ.get("DOCS_CURRENT_VERSION", "current"), +} + -def _head_release_date() -> str: - try: - return subprocess.run( - ["git", "log", "-1", "--format=%cs"], - cwd=_here, - capture_output=True, - text=True, - check=True, - ).stdout.strip() - except Exception: - return "" - - -def _format_release_date(date_str: str) -> str: - if not date_str: - return "" - try: - dt = datetime.strptime(date_str, "%Y-%m-%d") - return dt.strftime("%B %d, %Y") - except ValueError: - return date_str - - -def _fallback_versions() -> list[dict[str, str]]: - head_release_date = _head_release_date() - return [ - { - "name": "current", - "display_version": release, - "url": "../current/index.html", - "is_latest": True, - "status": "current", - "release_date": head_release_date, - "release_date_human": _format_release_date(head_release_date), - } +def _patch_landing_nav(app, pagename, templatename, context, doctree): + """Use index.html as the Home landing page; WAF-style section nav.""" + nav = context.get("nav") + if nav is None: + return + + from sphinx_immaterial.nav_adapt import MkdocsNavEntry + + pathto = context["pathto"] + master = app.config.master_doc + home_url = pathto(master) + is_home = pagename == master + + # Section index pages: non-selectable sidebar title, tab lands on first child. + # Architecture / Deploy / Samples / Features / Build / Contributors / Get Started + # are real top-level hub links. + section_index_pages = frozenset({ + "build/spec-reference/index", + "deploy/framework/index", + "features/metadata/index", + "features/authoring/index", + "features/configuration/index", + "features/sources-targets/index", + "features/platform/index", + "features/python/index", + "features/data-quality/index", + "features/environments/index", + "features/migrations/index", + }) + + def _docname_from_url(url): + if not url or url == "#": + return None + path = url.split("#", 1)[0] + if path.endswith(".html"): + return path[:-5] + return path + + def apply_section_captions(entries): + for entry in entries: + if entry.children: + apply_section_captions(entry.children) + docname = _docname_from_url(entry.url) + if docname in section_index_pages and entry.children: + first_child = entry.children[0] + if first_child.url: + entry.caption_only = True + entry.url = first_child.url + + apply_section_captions(nav) + + def rewrite_home_urls(entries): + for entry in entries: + if entry.url: + path = entry.url.split("#", 1)[0] + if path == "home.html" or path.endswith("/home.html"): + fragment = entry.url[len(path):] + entry.url = home_url + fragment + rewrite_home_urls(entry.children) + + rewrite_home_urls(nav) + + nav[:] = [ + entry + for entry in nav + if not (entry.url and entry.url.split("#", 1)[0] == home_url and "Home" in entry.title) ] + nav.insert( + 0, + MkdocsNavEntry( + title_text="Home", + url=home_url, + children=[], + active=is_home, + current=is_home, + active_or_section_within_active=is_home, + caption_only=False, + ), + ) -def _load_versions() -> list[dict[str, str]]: - versions_file = os.environ.get("DOCS_VERSIONS_FILE") - if not versions_file: - default_versions_file = os.path.join(_here, "build", "html", "versions.json") - if os.path.exists(default_versions_file): - versions_file = default_versions_file - - if not versions_file: - return _fallback_versions() - - try: - with open(versions_file, encoding="utf-8") as f: - versions = json.load(f) - except FileNotFoundError: - return _fallback_versions() - - # For per-version pages, switch from site-root links (e.g. "v1.2.3/") to - # sibling paths (e.g. "../v1.2.3/"), which work under GitHub project pages. - adapted = [] - for item in versions: - adapted.append( - { - "name": item["name"], - "display_version": item.get("display_version", item["name"].lstrip("v")), - "url": f"../{item['name']}/index.html", - "is_latest": item.get("is_latest", False), - "status": item.get("status", "release"), - "release_date": item.get("release_date", ""), - "release_date_human": _format_release_date(item.get("release_date", "")), - } - ) - return adapted - - -def _current_version_meta(versions: list[dict[str, str]], current: str) -> dict[str, str]: - for item in versions: - if item["name"] == current: - return item - return { - "name": current, - "display_version": current.lstrip("v"), - "url": f"../{current}/index.html", - "is_latest": current == "current", - "status": "current" if current == "current" else "release", - "release_date": "", - "release_date_human": "", + if is_home: + for entry in nav[1:]: + entry.active = False + entry.current = False + entry.active_or_section_within_active = False + + page = context.get("page") + if page is not None: + if "hide" not in page["meta"]: + page["meta"]["hide"] = [] + page["meta"]["hide"].append("navigation") + page["meta"]["hide"].append("toc") + + context["nav"] = nav + + +def _flatten_mermaid_diagrams(app, doctree): + """Emit plain diagram source for md-mermaid (no Pygments wrapper inside <pre>).""" + from sphinx_immaterial.mermaid_diagrams import mermaid_node + + for diagram in doctree.findall(mermaid_node): + content = diagram.get('content', '') + if not content: + for child in diagram.children: + if child.tagname == 'literal_block': + content = child.astext() + break + if content: + diagram['content'] = content + # Drop child nodes — content lives on the node attribute. Replacing + # children with nodes.Text() without append() orphans text nodes that + # the spelling builder treats as prose (<unknown>:None: br, subgraph, …). + diagram.children[:] = [] + + +def _ignore_spelling_in_nonprose(app, doctree): + """Skip diagram markup and raw HTML during sphinxcontrib-spelling checks.""" + from docutils import nodes + + from sphinx_immaterial.mermaid_diagrams import mermaid_node + + skip_tags = { + 'literal_block', + 'raw', + 'fixed_text_block', + 'math_block', + mermaid_node.__name__, } + for node in doctree.findall(nodes.Element): + classes = set(node.get('classes', [])) + if node.tagname in skip_tags or classes.intersection( + {'mermaid-diagram', 'lf-mermaid-source', 'lf-arch'} + ): + for text in node.findall(nodes.Text): + text.spellingIgnore = True -_docs_current_version = os.environ.get("DOCS_CURRENT_VERSION", "current") -_docs_versions = _load_versions() -html_context = { - "docs_current_version": _docs_current_version, - "docs_versions": _docs_versions, - "docs_current_version_meta": _current_version_meta(_docs_versions, _docs_current_version), -} +def _embed_mermaid_source_templates(app, doctree): + """Persist diagram source beside the rendered host (survives pre→div swap).""" + import html + + from docutils import nodes + + from sphinx_immaterial.mermaid_diagrams import mermaid_node + + for diagram in doctree.findall(mermaid_node): + content = diagram.get('content') or diagram.astext() + if not content: + continue + + parent = diagram.parent + if parent is None: + continue + + if any( + isinstance(child, nodes.raw) + and 'lf-mermaid-source' in child.astext() + for child in parent.children + ): + continue + + template = nodes.raw( + '', + f'<template class="lf-mermaid-source">{html.escape(content)}</template>', + format='html', + ) + parent.insert(parent.children.index(diagram), template) + + +def _table_column_count(table) -> int: + from docutils import nodes + + for tgroup in table.findall(nodes.tgroup): + cols = tgroup.get('cols') + if cols: + return int(cols) + return 0 + + +def _is_page_metadata_table(table) -> bool: + """Feature-page metadata strip: 2 cols, Applies To / Configuration Scope rows.""" + from docutils import nodes + + if _table_column_count(table) != 2: + return False + + rows = list(table.findall(nodes.row)) + if not rows: + return False + + entries = list(rows[0].findall(nodes.entry)) + if not entries: + return False + + first_cell = entries[0].astext() + return ( + 'Applies To' in first_cell + or 'Configuration Scope' in first_cell + or 'Databricks Docs' in first_cell + ) + + +def _mark_content_tables(app, doctree, docname=None): + """Tag body list-tables for theme CSS (excludes h1 metadata strips).""" + from docutils import nodes + + for table in doctree.findall(nodes.table): + if _is_page_metadata_table(table): + continue + + classes = list(table.get('classes', [])) + if 'lf-content-table' not in classes: + classes.append('lf-content-table') + if 'data' not in classes: + classes.append('data') + + col_count = _table_column_count(table) + col_class = f'lf-table-cols-{col_count}' + if 2 <= col_count <= 6 and col_class not in classes: + classes.append(col_class) + + table['classes'] = classes + + +def _upgrade_mermaid_dist(app, env=None): + """Replace theme mermaid (11.12) with vendored 11.16 for treeView-beta support.""" + from pathlib import Path + import shutil + + import sphinx_immaterial + from sphinx.builders.html import StandaloneHTMLBuilder + + if not isinstance(app.builder, StandaloneHTMLBuilder): + return + + if env is None: + env = app.env + dst_dir = Path(app.outdir) / "_static" / "mermaid" + dst = dst_dir / "mermaid.min.js" + theme_mermaid = Path(sphinx_immaterial.__file__).parent / "bundles" / "mermaid" + + needs_mermaid = getattr(env, "sphinx_immaterial_copy_mermaid_dist", False) + if not needs_mermaid and not dst.is_file(): + return + + # Theme copy runs on env-check-consistency (skipped on no-op incremental + # builds). Ensure the dist exists before overriding with the vendored build. + if not dst.is_file() and theme_mermaid.is_dir(): + dst_dir.mkdir(parents=True, exist_ok=True) + shutil.copytree(theme_mermaid, dst_dir, dirs_exist_ok=True) + + vendor = Path(_here) / "source" / "_static" / "vendor" / "mermaid.min.js" + if vendor.is_file(): + dst_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(vendor, dst) + + +def _upgrade_mermaid_dist_on_finish(app, exception): + if exception is not None: + return + _upgrade_mermaid_dist(app) + + +def _fix_language_data_for_immaterial_search(app, exception): + """Sphinx 8+ emits stopwords as a Set; immaterial search expects an array.""" + if exception is not None: + return + + from pathlib import Path + import re + + path = Path(app.outdir) / "_static" / "language_data.js" + if not path.is_file(): + return + + text = path.read_text(encoding="utf-8") + if "new Set(" not in text: + return + + updated, count = re.subn( + r"const stopwords = new Set\((\[[\s\S]*?\])\);\s*\nwindow\.stopwords = stopwords;", + r"var stopwords = \1;\nwindow.stopwords = stopwords;", + text, + count=1, + ) + if count: + path.write_text(updated, encoding="utf-8") + + +def _override_mermaid_pre_class(app): + """Use lf-mermaid-src so sphinx-immaterial does not shadow-DOM render diagrams.""" + from sphinx_immaterial.mermaid_diagrams import ( + depart_mermaid_node_html, + mermaid_node, + ) + + def visit_mermaid_node_html_lf(self, node): + attributes = {'class': 'lf-mermaid-src'} + self.body.append(self.starttag(node, 'pre', **attributes)) + + app.add_node( + mermaid_node, + override=True, + html=(visit_mermaid_node_html_lf, depart_mermaid_node_html), + ) + + +def _patch_mermaid_pre_tags(app, exception): + """Ensure theme does not shadow-render our diagrams (class lf-mermaid-src).""" + if exception is not None: + return + + from pathlib import Path + + outdir = Path(app.outdir) + for html_path in outdir.rglob('*.html'): + text = html_path.read_text(encoding='utf-8') + updated = text.replace('<pre class="mermaid">', '<pre class="lf-mermaid-src">') + if updated != text: + html_path.write_text(updated, encoding='utf-8') + def setup(app): app.set_translator("markdown", CustomMarkdownTranslator) - app.add_css_file('custom.css') + + def _init_domain_synopses(app): + for domain_name in ("py", "std"): + domain = app.env.get_domain(domain_name) + if "synopses" not in domain.data: + domain.data["synopses"] = {} + + app.connect("builder-inited", _init_domain_synopses) + _override_mermaid_pre_class(app) + app.connect("doctree-read", _flatten_mermaid_diagrams) + app.connect("doctree-read", _ignore_spelling_in_nonprose) + app.connect("doctree-read", _embed_mermaid_source_templates) + app.connect("doctree-read", _mark_content_tables) + app.connect("env-check-consistency", _upgrade_mermaid_dist, priority=1000) + app.connect("build-finished", _upgrade_mermaid_dist_on_finish, priority=1000) + app.connect("build-finished", _fix_language_data_for_immaterial_search) + app.connect("build-finished", _patch_mermaid_pre_tags) + app.connect("html-page-context", _patch_landing_nav, priority=999) #app.add_builder(MarkdownBuilder) diff --git a/docs/decisions/0001-bundle-src-layout.md b/docs/decisions/0001-bundle-src-layout.md index 9aa1af0..ab17607 100644 --- a/docs/decisions/0001-bundle-src-layout.md +++ b/docs/decisions/0001-bundle-src-layout.md @@ -13,7 +13,7 @@ modules and lifecycle scripts. This caused: - No separation between "code that gets installed on the cluster" and "code that spec references by module path". -- No clear, fork-safe area for customer customisation. +- No clear, fork-safe area for customer customization. - Ambiguity between `extensions/` (sys.path) and `extensions/libraries/` (a variant that was never released to `main`). @@ -69,6 +69,6 @@ user-facing documentation to avoid confusion with callback-registration APIs. code. It is **not** required if libraries are sourced from PyPI, UC Volumes, or an artifact repository. - New bundles should place spec-referenced Python in `src/python/` and cluster-install - artefacts in `src/libraries/` (if bundled). + artifacts in `src/libraries/` (if bundled). - The `pipeline_bundle_template/` and samples are migrated to this layout in this PR. - Existing bundles using `extensions/` continue to work until `v1.0.0` (see ADR-0002). diff --git a/docs/decisions/0002-extensions-deprecation.md b/docs/decisions/0002-extensions-deprecation.md index f8d4dae..a2e7074 100644 --- a/docs/decisions/0002-extensions-deprecation.md +++ b/docs/decisions/0002-extensions-deprecation.md @@ -22,12 +22,12 @@ The new canonical path (`src/python/`) was introduced in ADR-0001. ## Decision -| Legacy behaviour | Released? | Action in v0.13.0 | Removed in | +| Legacy behavior | Released? | Action in v0.13.0 | Removed in | |-----------------|-----------|-----------------|------------| | Flat `extensions/` on `sys.path` | Yes | Emit `DeprecationWarning` + structured log | **v1.0.0** | | `extensions/libraries/` on `sys.path` | No | Drop immediately, no deprecation window | **v0.13.0** (this PR) | -**One-minor deprecation window:** deprecated behaviour is removed in the first minor +**One-minor deprecation window:** deprecated behavior is removed in the first minor version after the deprecation warning lands — giving consumers one release cycle to migrate before the path disappears. diff --git a/docs/decisions/0004-mirror-to-stdout-default.md b/docs/decisions/0004-mirror-to-stdout-default.md index 2eb3118..e0bd432 100644 --- a/docs/decisions/0004-mirror-to-stdout-default.md +++ b/docs/decisions/0004-mirror-to-stdout-default.md @@ -62,7 +62,7 @@ default logger is returned unchanged. - The `lakeflowframework` `logging.Logger` instance is modified in-place when silenced. Any code that obtained a direct `logging.getLogger("lakeflowframework")` reference before logger resolution will stop producing output — this is the - intended behaviour. + intended behavior. - `mirror_to_stdout: false` is the breaking change relative to the previous hard-coded stdout logger. Teams relying on plain-text stdout output and also enabling a custom logger must set `mirror_to_stdout: true` explicitly. diff --git a/docs/decisions/0005-syspath-registration-decoupled-from-logger.md b/docs/decisions/0005-syspath-registration-decoupled-from-logger.md index 33ebe02..360a77e 100644 --- a/docs/decisions/0005-syspath-registration-decoupled-from-logger.md +++ b/docs/decisions/0005-syspath-registration-decoupled-from-logger.md @@ -1,4 +1,4 @@ -# ADR-0005: `sys.path` registration decoupled from logger initialisation +# ADR-0005: `sys.path` registration decoupled from logger initialization **Date:** 2026-05-17 **Status:** Accepted diff --git a/docs/decisions/0006-local-config-sparse-overlay.md b/docs/decisions/0006-local-config-sparse-overlay.md index d21baea..07d67da 100644 --- a/docs/decisions/0006-local-config-sparse-overlay.md +++ b/docs/decisions/0006-local-config-sparse-overlay.md @@ -30,7 +30,7 @@ The original framework config mechanism offered a binary choice: use resolution like `GLOBAL_CONFIG`). The `src/local/` fork-safe area introduced in ADR-0001 provides a natural home -for customer customisations. Config overrides should follow the same pattern. +for customer customizations. Config overrides should follow the same pattern. ## Decision @@ -47,7 +47,7 @@ Override detection and deprecation warnings are the **caller's responsibility**. active, stores the resolved root in `self._active_config_path`, and passes it to `load_framework_config`. `self._active_config_path` is reused by `_setup_operational_metadata` so the override detection only runs once per - pipeline initialisation. + pipeline initialization. - **`load_framework_logger_config`** independently checks `config/override/logger.json`, emits its own `DeprecationWarning` if active, @@ -66,7 +66,7 @@ load_framework_config( ) -> Dict ``` -Behaviour: +Behavior: 1. When `name` is a **sequence** (e.g. `FrameworkPaths.GLOBAL_CONFIG = ("global.json", "global.yaml", "global.yml")`): diff --git a/docs/decisions/0007-scripted-versioned-docs-and-ui-scope.md b/docs/decisions/0007-scripted-versioned-docs-and-ui-scope.md index 13363cd..17bfe02 100644 --- a/docs/decisions/0007-scripted-versioned-docs-and-ui-scope.md +++ b/docs/decisions/0007-scripted-versioned-docs-and-ui-scope.md @@ -32,16 +32,21 @@ The pipeline builds: - `main` as `current` - selected release tags +- optional `local-branch-preview` (current branch) with `--preview` - `docs/build/html/versions.json` - root redirect `docs/build/html/index.html -> current/index.html` -### 2) Keep selector UI scope minimal and theme-aligned +Each published version (`current` and release tags) is built with **main's** +`docs/conf.py` + templates and that ref's `docs/source`, so the RTD version +selector stays available on historical docs. `--preview` adds +`local-branch-preview` using the current branch's own conf/source. -- Keep the RTD selector at the bottom (`docs/_templates/versions.html`). -- Keep breadcrumb version/release-date metadata (`docs/_templates/breadcrumbs.html`). -- Do **not** keep a second custom header selector path. +### 2) Shared superset `versions.json` + theme-native selectors -This limits ongoing CSS/template overrides and reduces regressions from theme changes. +- Write one `versions.json` with both mike fields (`version`, `title`, `aliases`) and legacy RTD fields (`name`, `display_version`, `url`, …). +- RTD refs (`current` / tags, via main's conf) consume it at build time via `DOCS_VERSIONS_FILE` / `versions.html`. +- Immaterial preview refs enable `version_dropdown` and load the same file over HTTP. +- Do **not** use a feature-branch conf for historical tag builds — that drops the RTD selector on older tags. ### 3) Place docs tooling under `docs/scripts/` diff --git a/docs/decisions/0008-lakeflow-framework-package.md b/docs/decisions/0008-lakeflow-framework-package.md index 1fcb79c..2b2e0ab 100644 --- a/docs/decisions/0008-lakeflow-framework-package.md +++ b/docs/decisions/0008-lakeflow-framework-package.md @@ -137,7 +137,7 @@ module is added. risk with customer code. - Existing flat-deploy customers are unaffected: the `src/` shims preserve backward compatibility, and `framework.sourcePath` + Workspace Files-first - resolution (ADR-0009) means behaviour is identical to pre-v0.20.0. + resolution (ADR-0009) means behavior is identical to pre-v0.20.0. - Editable installs (`pip install -e ".[contrib]"`) are supported for local development; the `src/` layout ensures the installed package and the source tree are the same directory. diff --git a/docs/decisions/0009-strategy-b-workspace-files-first-resolver.md b/docs/decisions/0009-strategy-b-workspace-files-first-resolver.md index dbd8512..71e9cdd 100644 --- a/docs/decisions/0009-strategy-b-workspace-files-first-resolver.md +++ b/docs/decisions/0009-strategy-b-workspace-files-first-resolver.md @@ -32,7 +32,7 @@ first when both a Workspace Files path and the wheel package data are potentiall - `+` Simple: the wheel is always the canonical source of defaults. - `−` **Breaking for existing flat-deploy customers.** Their defaults live in Workspace Files; if the wheel shadow-contains a newer (or different) version of a default - file, behaviour silently changes on upgrade. + file, behavior silently changes on upgrade. - `−` Contradicts the "explicit wins" principle: a customer who set `framework.sourcePath` explicitly expects Workspace Files to be authoritative. @@ -91,7 +91,7 @@ similar validators. - **Zero-change upgrade for flat-deploy customers.** Their `framework.sourcePath` points to the deployed `src/` directory in Workspace Files; those files are - found at step 1 and the wheel is never consulted. Behaviour is identical to + found at step 1 and the wheel is never consulted. Behavior is identical to pre-v0.20.0. - **Explicit wins.** Setting `framework.sourcePath` is a deliberate act. Workspace Files-first honours that intent; package-first would silently override it. @@ -108,7 +108,7 @@ similar validators. config and schema reads; call sites that previously used `os.path.join(framework_path, FrameworkPaths.CONFIG_PATH, name)` are migrated to it. -- Flat-deploy customers see **no behaviour change** on upgrade to v0.20.0. +- Flat-deploy customers see **no behavior change** on upgrade to v0.20.0. - Wheel-install customers get defaults from the wheel with no `framework.sourcePath` required. - The `src/local/config/` sparse overlay (ADR-0006) continues to work in all diff --git a/docs/redirects/architecture.yaml b/docs/redirects/architecture.yaml new file mode 100644 index 0000000..d7bbf4b --- /dev/null +++ b/docs/redirects/architecture.yaml @@ -0,0 +1,3 @@ +redirects: +- from: concepts.html + to: architecture/index.html diff --git a/docs/redirects/build.yaml b/docs/redirects/build.yaml new file mode 100644 index 0000000..8e1ed39 --- /dev/null +++ b/docs/redirects/build.yaml @@ -0,0 +1,45 @@ +redirects: +- from: build_pipeline_bundle.html + to: build/index.html +- from: build_pipeline_bundle_steps.html + to: build/bundle-steps.html +- from: build_pipeline_bundle_structure.html + to: build/bundle-structure.html +- from: dataflow_spec_ref_cdc.html + to: build/spec-reference/cdc.html +- from: dataflow_spec_ref_data_quality.html + to: build/spec-reference/data-quality.html +- from: dataflow_spec_ref_main_flows.html + to: build/spec-reference/flows.html +- from: dataflow_spec_ref_main_materialized_views.html + to: build/spec-reference/materialized-views.html +- from: dataflow_spec_ref_main_standard.html + to: build/spec-reference/standard.html +- from: dataflow_spec_ref_source_details.html + to: build/spec-reference/source-details.html +- from: dataflow_spec_ref_table_migration.html + to: build/spec-reference/table-migration.html +- from: dataflow_spec_ref_target_details.html + to: build/spec-reference/target-details.html +- from: dataflow_spec_reference.html + to: build/spec-reference/index.html +- from: patterns.html + to: build/patterns/index.html +- from: patterns_streaming_basic_1_1.html + to: build/patterns/basic-1-1.html +- from: build/patterns/silver-1-1.html + to: build/patterns/basic-1-1.html +- from: build/patterns/common-medallion-path.html + to: build/patterns/base-patterns.html +- from: patterns_streaming_cdc_stream_from_snapshot.html + to: build/patterns/cdc-stream-from-snapshot.html +- from: patterns_streaming_flow_groups.html + to: build/patterns/flow-groups.html +- from: patterns_streaming_multi_source_streaming.html + to: build/patterns/multi-source-streaming.html +- from: patterns_streaming_stream_static_basic.html + to: build/patterns/stream-static-basic.html +- from: patterns_streaming_stream_static_streaming_dwh.html + to: build/patterns/stream-static-streaming-dwh.html +- from: splitting_dataflow_spec.html + to: build/spec-reference/splitting-dataflow-spec.html diff --git a/docs/redirects/contributors.yaml b/docs/redirects/contributors.yaml new file mode 100644 index 0000000..046b187 --- /dev/null +++ b/docs/redirects/contributors.yaml @@ -0,0 +1,15 @@ +redirects: +- from: contributor.html + to: contributors/index.html +- from: contributor_contrib.html + to: contributors/contrib.html +- from: contributor_dev_docs.html + to: contributors/dev-docs.html +- from: contributor_dev_env.html + to: contributors/dev-env.html +- from: contributor_dev_git.html + to: contributors/dev-git.html +- from: contributor_dev_steps.html + to: contributors/dev-steps.html +- from: contributor_imports.html + to: contributors/imports.html diff --git a/docs/redirects/deploy.yaml b/docs/redirects/deploy.yaml new file mode 100644 index 0000000..1bd2a48 --- /dev/null +++ b/docs/redirects/deploy.yaml @@ -0,0 +1,17 @@ +redirects: +- from: deploy.html + to: deploy/index.html +- from: deploy_before_you_deploy.html + to: deploy/before-you-deploy.html +- from: deploy_ci_cd.html + to: deploy/ci-cd.html +- from: deploy_framework.html + to: deploy/framework/index.html +- from: deploy_framework_options.html + to: deploy/framework/options.html +- from: deploy_local_framework.html + to: deploy/framework/local-framework.html +- from: deploy_local_pipeline_bundle.html + to: deploy/pipeline-bundle/local.html +- from: deploy_wheel.html + to: deploy/framework/wheel.html diff --git a/docs/redirects/features.yaml b/docs/redirects/features.yaml new file mode 100644 index 0000000..456f84d --- /dev/null +++ b/docs/redirects/features.yaml @@ -0,0 +1,93 @@ +redirects: +- from: feature_a_z.html + to: features/a-z.html +- from: feature_authoring.html + to: features/authoring/index.html +- from: feature_auto_complete.html + to: features/authoring/auto-complete.html +- from: feature_builder_parallelization.html + to: features/configuration/builder-parallelization.html +- from: feature_cdc.html + to: features/platform/cdc.html +- from: feature_cdf.html + to: features/platform/cdf.html +- from: feature_configuration.html + to: features/configuration/index.html +- from: feature_data_quality.html + to: features/data-quality/index.html +- from: feature_data_quality_expectations.html + to: features/data-quality/expectations.html +- from: feature_data_quality_quarantine.html + to: features/data-quality/quarantine.html +- from: feature_databricks_schema_features.html + to: features/platform/databricks-schema-features.html +- from: feature_environments.html + to: features/environments/index.html +- from: feature_framework_configuration.html + to: features/configuration/framework-configuration.html +- from: feature_liquid_clustering.html + to: features/platform/liquid-clustering.html +- from: feature_logging.html + to: features/configuration/logging.html +- from: feature_logical_environment.html + to: features/environments/logical-environments.html +- from: feature_mandatory_table_properties.html + to: features/configuration/mandatory-table-properties.html +- from: feature_materialized_views.html + to: features/platform/materialized-views.html +- from: feature_metadata.html + to: features/metadata/index.html +- from: feature_migrations.html + to: features/migrations/index.html +- from: feature_multi_source_streaming.html + to: features/platform/multi-source-streaming.html +- from: feature_operational_metadata.html + to: features/configuration/operational-metadata.html +- from: feature_platform.html + to: features/platform/index.html +- from: feature_python.html + to: features/python/index.html +- from: feature_python_dependency_management.html + to: features/python/dependency-management.html +- from: feature_python_extensions.html + to: features/python/extensions.html +- from: feature_python_functions.html + to: features/python/functions.html +- from: feature_python_source.html + to: features/python/source.html +- from: feature_schemas.html + to: features/metadata/schema-management.html +- from: feature_secrets.html + to: features/configuration/secrets-management.html +- from: feature_soft_deletes.html + to: features/platform/soft-deletes.html +- from: feature_source_types.html + to: features/sources-targets/source-types.html +- from: feature_sources_targets.html + to: features/sources-targets/index.html +- from: feature_spark_configuration.html + to: features/configuration/spark-configuration.html +- from: feature_spec_format.html + to: features/metadata/spec-format.html +- from: feature_sql_source.html + to: features/sources-targets/sql-source.html +- from: feature_substitutions.html + to: features/metadata/substitutions.html +- from: feature_table_migration.html + to: features/migrations/table-migration.html +- from: feature_target_catalog_schema.html + to: features/platform/target-catalog-schema.html +- from: feature_target_types.html + to: features/sources-targets/target-types.html +- from: feature_templates.html + to: features/metadata/templates.html +- from: feature_ui_integration.html + to: features/authoring/ui-integration.html +- from: feature_validation.html + to: features/metadata/validation.html +- from: feature_versioning_dataflow_spec.html + to: features/environments/versioning-dataflow-specs.html +- from: feature_versioning_framework.html + to: features/environments/versioning-framework.html +- from: features.html + to: features/index.html diff --git a/docs/redirects/get-started.yaml b/docs/redirects/get-started.yaml new file mode 100644 index 0000000..26c8d6f --- /dev/null +++ b/docs/redirects/get-started.yaml @@ -0,0 +1,7 @@ +redirects: +- from: get_started.html + to: get-started/index.html +- from: quick_start.html + to: get-started/quick-start.html +- from: what_is_lakeflow_framework.html + to: get-started/what-is-lakeflow-framework.html diff --git a/docs/redirects/legacy.yaml b/docs/redirects/legacy.yaml new file mode 100644 index 0000000..765000a --- /dev/null +++ b/docs/redirects/legacy.yaml @@ -0,0 +1,17 @@ +redirects: +- from: deploy_framework_bundle.html + to: deploy/framework/index.html +- from: deploy_framework_overview.html + to: deploy/framework/options.html +- from: deploy_pipeline_bundle.html + to: deploy/pipeline-bundle/local.html +- from: feature_direct_publishing_mode.html + to: features/platform/target-catalog-schema.html +- from: getting_started.html + to: get-started/index.html +- from: home.html + to: index.html +- from: orchestration.html + to: build/index.html +- from: pipeline_execution.html + to: build/index.html diff --git a/docs/redirects/samples.yaml b/docs/redirects/samples.yaml new file mode 100644 index 0000000..4e8518e --- /dev/null +++ b/docs/redirects/samples.yaml @@ -0,0 +1,3 @@ +redirects: +- from: deploy_samples.html + to: samples/index.html diff --git a/docs/scripts/build_versioned_docs.py b/docs/scripts/build_versioned_docs.py index dc57dac..2eaa43e 100755 --- a/docs/scripts/build_versioned_docs.py +++ b/docs/scripts/build_versioned_docs.py @@ -2,10 +2,17 @@ """Build versioned docs without sphinx-multiversion. Build strategy: -- Always build `main` as the default development docs, published as `current`. +- Always build ``main`` as ``current``. - Build selected release tags from docs/scripts/select_versions.py. +- **Docs IA cutover at v0.21.0:** ``current`` and tags ``>= v0.21.0`` use + **main's** ``docs/conf.py`` with that ref's ``docs/source`` (immaterial / + section-folder IA). Tags ``<= v0.20.x`` use **v0.20.0's** ``docs/conf.py`` + (RTD / flat layout) with each tag's own ``docs/source``. +- ``--preview`` builds the current branch as ``local-branch-preview`` using + **that branch's** conf + source (pre-merge IA validation). - Write per-version output under docs/build/html/<version>/. -- Generate docs/build/html/versions.json for the sidebar switcher. +- Generate docs/build/html/versions.json as a **superset** manifest consumed by + both sphinx-immaterial (mike fields) and legacy RTD ``versions.html``. """ from __future__ import annotations @@ -13,12 +20,25 @@ import argparse import json import os +import re import shutil import subprocess import sys from pathlib import Path +_SCRIPTS_DIR = Path(__file__).resolve().parent +if str(_SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(_SCRIPTS_DIR)) + DOCS_BASEURL = "https://databricks-solutions.github.io/lakeflow_framework/" +PREVIEW_VERSION_NAME = "local-branch-preview" +MAIN_CONF_WORKTREE = "_conf_main" +LEGACY_CONF_REF = "v0.20.0" +LEGACY_CONF_WORKTREE = "_conf_legacy" +# Tags at or above this semver use main conf; older tags use LEGACY_CONF_REF conf. +DOCS_CONF_CUTOVER = (0, 21, 0) +REDIRECT_VERSIONS = frozenset({"current", PREVIEW_VERSION_NAME}) +_TAG_RE = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)$") def _run(command: list[str], *, cwd: Path | None = None, env: dict[str, str] | None = None) -> None: @@ -39,13 +59,11 @@ def _selected_tags(repo_root: Path) -> list[str]: return json.loads(output) if output else [] -def _list_preview_branches(repo_root: Path) -> list[str]: - raw = _run_capture( - ["git", "for-each-ref", "--format=%(refname:short)", "refs/heads"], - cwd=repo_root, - ) - branches = [line.strip() for line in raw.splitlines() if line.strip()] - return [b for b in branches if b != "main"] +def _current_branch(repo_root: Path) -> str: + try: + return _run_capture(["git", "branch", "--show-current"], cwd=repo_root) + except subprocess.CalledProcessError: + return "" def _safe_remove(path: Path) -> None: @@ -53,10 +71,56 @@ def _safe_remove(path: Path) -> None: shutil.rmtree(path) +def _cleanup_docs_worktrees(repo_root: Path, worktrees_root: Path) -> None: + """Unregister git worktrees under ``worktrees_root``, then delete the dir. + + ``shutil.rmtree`` alone leaves stale registrations ("missing but already + registered") that break the next ``git worktree add``. + """ + if worktrees_root.exists(): + for child in sorted(worktrees_root.iterdir()): + if not child.is_dir(): + continue + subprocess.run( + ["git", "worktree", "remove", "--force", str(child)], + cwd=repo_root, + check=False, + capture_output=True, + ) + shutil.rmtree(worktrees_root, ignore_errors=True) + subprocess.run( + ["git", "worktree", "prune"], + cwd=repo_root, + check=False, + capture_output=True, + ) + + def _normalize_name(ref: str) -> str: return ref.replace("/", "-") +def _parse_release_tag(tag: str) -> tuple[int, int, int] | None: + match = _TAG_RE.match(tag) + if not match: + return None + return int(match.group(1)), int(match.group(2)), int(match.group(3)) + + +def _conf_ref_for(version_name: str) -> str | None: + """Git ref for ``docs/conf.py``, or ``None`` for the working tree (preview).""" + if version_name == PREVIEW_VERSION_NAME: + return None + if version_name == "current": + return "main" + parsed = _parse_release_tag(version_name) + if parsed is None: + return "main" + if parsed >= DOCS_CONF_CUTOVER: + return "main" + return LEGACY_CONF_REF + + def _release_date_for_ref(repo_root: Path, ref: str) -> str: # ISO-style date (YYYY-MM-DD) for deterministic display in docs headers. return _run_capture(["git", "log", "-1", "--format=%cs", ref], cwd=repo_root) @@ -72,43 +136,174 @@ def _version_for_ref(repo_root: Path, ref: str, fallback: str) -> str: return fallback +def _superset_versions(links: list[dict[str, str]]) -> list[dict[str, object]]: + """Build a versions.json entry set readable by immaterial and legacy RTD. + + Immaterial / mike fields: ``version``, ``title``, ``aliases`` + Legacy RTD fields: ``name``, ``display_version``, ``url``, ``is_latest``, + ``status``, ``release_date`` + """ + payload: list[dict[str, object]] = [] + for item in links: + name = item["name"] + display = item["display_version"] + is_current = item.get("status") == "current" + title = f"{display} (current)" if is_current else display + aliases: list[str] = ["latest"] if is_current else [] + payload.append( + { + # mike / sphinx-immaterial + "version": name, + "title": title, + "aliases": aliases, + # legacy RTD versions.html (via conf.py _load_versions) + "name": name, + "display_version": display, + "url": f"{name}/index.html", + "is_latest": is_current, + "status": "current" if is_current else item.get("status", "release"), + "release_date": item.get("release_date", ""), + } + ) + return payload + + +def _ensure_worktree( + *, + repo_root: Path, + worktrees_root: Path, + name: str, + ref: str, +) -> Path: + path = worktrees_root / name + if path.exists(): + return path + try: + _run(["git", "worktree", "add", "--detach", str(path), ref], cwd=repo_root) + except subprocess.CalledProcessError: + # Recover from a previous run that deleted the dir without unregistering. + subprocess.run( + ["git", "worktree", "prune"], + cwd=repo_root, + check=False, + capture_output=True, + ) + _run( + ["git", "worktree", "add", "--force", "--detach", str(path), ref], + cwd=repo_root, + ) + return path + + +def _sphinx_build( + *, + repo_root: Path, + conf_dir: Path, + source_dir: Path, + out_dir: Path, + version_name: str, + versions_file: Path, +) -> None: + out_dir.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["DOCS_CURRENT_VERSION"] = version_name + # Legacy RTD conf.py loads the shared manifest at build time. + env["DOCS_VERSIONS_FILE"] = str(versions_file) + _run( + [ + sys.executable, + "-m", + "sphinx", + "-b", + "html", + "-c", + str(conf_dir), + str(source_dir), + str(out_dir), + ], + cwd=repo_root, + env=env, + ) + + def _build_ref( *, repo_root: Path, - docs_dir: Path, worktrees_root: Path, output_root: Path, + versions_file: Path, ref: str, version_name: str, + conf_ref: str | None, ) -> None: - worktree_path = worktrees_root / _normalize_name(version_name) - _run(["git", "worktree", "add", "--detach", str(worktree_path), ref], cwd=repo_root) - try: - out_dir = output_root / version_name - out_dir.mkdir(parents=True, exist_ok=True) - env = os.environ.copy() - env["DOCS_CURRENT_VERSION"] = version_name - env["DOCS_VERSIONS_FILE"] = str(output_root / "versions.json") - _run( - [ - "sphinx-build", - "-b", - "html", - "-c", - str(docs_dir), - str(worktree_path / "docs" / "source"), - str(out_dir), - ], - cwd=repo_root, - env=env, + """Build one version folder. + + ``conf_ref`` selects which git ref supplies ``docs/conf.py``: + ``main`` (modern IA), ``v0.20.0`` (legacy RTD), or ``None`` (working tree). + Source always comes from ``ref`` (except preview, which uses the working tree). + """ + if conf_ref is None: + # Preview: pick up local conf.py / theme / source edits immediately. + source_wt = repo_root + conf_dir = repo_root / "docs" + conf_label = f"{ref} (working tree)" + else: + source_wt = _ensure_worktree( + repo_root=repo_root, + worktrees_root=worktrees_root, + name=_normalize_name(version_name), + ref=ref, + ) + conf_worktree_name = ( + MAIN_CONF_WORKTREE if conf_ref == "main" else LEGACY_CONF_WORKTREE ) - finally: - _run(["git", "worktree", "remove", "--force", str(worktree_path)], cwd=repo_root) + conf_wt = _ensure_worktree( + repo_root=repo_root, + worktrees_root=worktrees_root, + name=conf_worktree_name, + ref=conf_ref, + ) + conf_dir = conf_wt / "docs" + conf_label = conf_ref + + source_dir = source_wt / "docs" / "source" + print( + f"Building docs for {ref} -> {version_name} (conf={conf_label})", + file=sys.stderr, + ) + out_dir = output_root / version_name + _sphinx_build( + repo_root=repo_root, + conf_dir=conf_dir, + source_dir=source_dir, + out_dir=out_dir, + version_name=version_name, + versions_file=versions_file, + ) + + if version_name in REDIRECT_VERSIONS: + from write_redirects import write_redirects + + count = write_redirects( + out_dir, + version_name=version_name, + docs_dir=repo_root / "docs", + ) + print(f"Wrote {count} redirect stub(s) for {version_name}", file=sys.stderr) def main() -> None: parser = argparse.ArgumentParser() - parser.add_argument("--preview", action="store_true", help="Include local branches in build.") + parser.add_argument( + "--preview", + action="store_true", + help=( + "Also build the current git branch as ``local-branch-preview`` " + "using that branch's own conf/source. " + "``current`` uses main; tags >= v0.21.0 use main conf; " + f"older tags use {LEGACY_CONF_REF} conf." + ), + ) args = parser.parse_args() docs_dir = Path(__file__).resolve().parent.parent @@ -116,17 +311,30 @@ def main() -> None: build_root = docs_dir / "build" output_root = build_root / "html" worktrees_root = build_root / ".worktrees" + versions_file = output_root / "versions.json" _safe_remove(output_root) - _safe_remove(worktrees_root) + _cleanup_docs_worktrees(repo_root, worktrees_root) output_root.mkdir(parents=True, exist_ok=True) worktrees_root.mkdir(parents=True, exist_ok=True) tags = _selected_tags(repo_root) - versions = [{"name": "current", "ref": "main"}] + [{"name": t, "ref": t} for t in tags] + versions: list[dict[str, str]] = [{"name": "current", "ref": "main"}] + versions.extend({"name": t, "ref": t} for t in tags) if args.preview: - for branch in _list_preview_branches(repo_root): - versions.append({"name": _normalize_name(branch), "ref": branch}) + preview_branch = _current_branch(repo_root) + if not preview_branch: + print( + "Warning: --preview ignored (detached HEAD; no current branch).", + file=sys.stderr, + ) + elif preview_branch == "main": + print( + "Warning: --preview ignored (already on main; current covers it).", + file=sys.stderr, + ) + else: + versions.append({"name": PREVIEW_VERSION_NAME, "ref": preview_branch}) # De-duplicate while preserving order. deduped: list[dict[str, str]] = [] @@ -140,7 +348,11 @@ def main() -> None: links = [] for item in deduped: is_current = item["name"] == "current" - display_version = _version_for_ref(repo_root, item["ref"], item["name"].lstrip("v")) + is_preview = item["name"] == PREVIEW_VERSION_NAME + if is_preview: + display_version = f"{PREVIEW_VERSION_NAME} ({item['ref']})" + else: + display_version = _version_for_ref(repo_root, item["ref"], item["name"].lstrip("v")) links.append( { "name": item["name"], @@ -149,6 +361,7 @@ def main() -> None: "is_latest": is_current, "status": "current" if is_current else "release", "release_date": _release_date_for_ref(repo_root, item["ref"]), + "ref": item["ref"], } ) @@ -162,19 +375,30 @@ def main() -> None: if item["name"] == "current" or item["display_version"] != current_version ] - (output_root / "versions.json").write_text(json.dumps(links, indent=2) + "\n", encoding="utf-8") + versions_file.write_text( + json.dumps(_superset_versions(links), indent=2) + "\n", + encoding="utf-8", + ) for item in deduped: - print(f"Building docs for {item['ref']} -> {item['name']}", file=sys.stderr) _build_ref( repo_root=repo_root, - docs_dir=docs_dir, worktrees_root=worktrees_root, output_root=output_root, + versions_file=versions_file, ref=item["ref"], version_name=item["name"], + conf_ref=_conf_ref_for(item["name"]), ) + # Immaterial's mike client resolves version_json relative to the version + # folder. Copy the site-root manifest into each version dir so both + # ``versions.json`` and ``../versions.json`` resolve successfully. + for item in deduped: + dest = output_root / item["name"] / "versions.json" + if (output_root / item["name"]).is_dir(): + shutil.copy2(versions_file, dest) + # Root redirect for GitHub Pages. index_html = """<!DOCTYPE html> <html> @@ -219,8 +443,13 @@ def main() -> None: ) (output_root / "robots.txt").write_text(robots_txt, encoding="utf-8") - _safe_remove(worktrees_root) - _run(["git", "worktree", "prune"], cwd=repo_root) + from write_redirects import write_root_mirror_redirects + + root_count = write_root_mirror_redirects(output_root, docs_dir=repo_root / "docs") + if root_count: + print(f"Wrote {root_count} root-level redirect mirror(s)", file=sys.stderr) + + _cleanup_docs_worktrees(repo_root, worktrees_root) if __name__ == "__main__": diff --git a/docs/scripts/select_versions.py b/docs/scripts/select_versions.py index 6a4ea33..03a5cd0 100755 --- a/docs/scripts/select_versions.py +++ b/docs/scripts/select_versions.py @@ -5,6 +5,8 @@ -------- * For the current release major (highest major present), include the latest patch release for the last 5 minor series. +* For major ``0``, include the latest patch for every minor from the current + minor down to ``0.12`` (inclusive), not capped at five minors. * Also include the latest available release for each of the last 3 major versions. @@ -27,6 +29,9 @@ TAG_RE = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)$") +# For 0.x releases, publish docs back to this minor (latest patch per minor). +MIN_MINOR_MAJOR_0 = 12 + def _get_tags() -> list[str]: result = subprocess.run( @@ -75,19 +80,27 @@ def select_versions(tags: list[str]) -> list[str]: majors_desc = sorted(best_per_major.keys(), reverse=True) current_major = majors_desc[0] - # Last 5 minor series for current major (latest patch in each minor). + # Latest patch for each selected minor in the current major. current_major_minors = sorted( [minor for (major, minor) in best_per_minor if major == current_major], reverse=True, ) - current_major_last_5 = { - best_per_minor[(current_major, minor)][1] for minor in current_major_minors[:5] - } + if current_major == 0: + current_major_selected = { + best_per_minor[(0, minor)][1] + for minor in current_major_minors + if minor >= MIN_MINOR_MAJOR_0 + } + else: + current_major_selected = { + best_per_minor[(current_major, minor)][1] + for minor in current_major_minors[:5] + } # Last 3 major versions (latest release per major). last_3_major_latest = {best_per_major[major][2] for major in majors_desc[:3]} - return _sort_tags_desc(current_major_last_5 | last_3_major_latest) + return _sort_tags_desc(current_major_selected | last_3_major_latest) def tags_to_regex(tags: list[str]) -> str: diff --git a/docs/scripts/write_redirects.py b/docs/scripts/write_redirects.py new file mode 100644 index 0000000..c7c130c --- /dev/null +++ b/docs/scripts/write_redirects.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Write HTML redirect stubs from docs/redirects.yaml and docs/redirects/*.yaml.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +try: + import yaml +except ImportError as exc: # pragma: no cover + raise SystemExit("PyYAML is required: pip install pyyaml") from exc + +DOCS_BASEURL = "https://databricks-solutions.github.io/lakeflow_framework/" + +_STUB_MARKER = "docs-redirect-stub" + + +def _docs_dir() -> Path: + return Path(__file__).resolve().parent.parent + + +def load_redirects(docs_dir: Path | None = None) -> list[dict[str, str]]: + """Load and merge redirect entries from the central manifest.""" + docs_dir = docs_dir or _docs_dir() + entries: list[dict[str, str]] = [] + + single = docs_dir / "redirects.yaml" + if single.is_file(): + data = yaml.safe_load(single.read_text(encoding="utf-8")) or {} + entries.extend(data.get("redirects", [])) + + redirects_dir = docs_dir / "redirects" + if redirects_dir.is_dir(): + for path in sorted(redirects_dir.glob("*.yaml")): + data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + entries.extend(data.get("redirects", [])) + + seen: set[str] = set() + deduped: list[dict[str, str]] = [] + for entry in entries: + src = entry.get("from", "").strip() + dst = entry.get("to", "").strip() + if not src or not dst or src in seen: + continue + seen.add(src) + deduped.append({"from": src, "to": dst}) + return deduped + + +def _is_redirect_stub(path: Path) -> bool: + if not path.is_file(): + return False + try: + return _STUB_MARKER in path.read_text(encoding="utf-8") + except OSError: + return False + + +def _redirect_html(*, target: str, canonical: str) -> str: + return f"""<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="{_STUB_MARKER}" content="true"> + <title>Page moved + + + + +

This page has moved to {target}.

+ + +""" + + +def write_redirects( + version_outdir: Path, + *, + version_name: str, + baseurl: str = DOCS_BASEURL, + docs_dir: Path | None = None, +) -> int: + """Write redirect stubs into a built version directory. Returns count written.""" + version_outdir = version_outdir.resolve() + written = 0 + + for entry in load_redirects(docs_dir): + from_rel = entry["from"] + to_rel = entry["to"] + stub_path = version_outdir / from_rel + target_path = version_outdir / to_rel + + if stub_path.exists() and stub_path.is_dir(): + print(f"skip redirect (directory exists): {from_rel}", file=sys.stderr) + continue + if stub_path.exists() and not _is_redirect_stub(stub_path): + print(f"skip redirect (real page exists): {from_rel}", file=sys.stderr) + continue + if not target_path.exists(): + print(f"warning: redirect target missing: {to_rel}", file=sys.stderr) + + stub_path.parent.mkdir(parents=True, exist_ok=True) + canonical = f"{baseurl.rstrip('/')}/{version_name}/{to_rel}" + stub_path.write_text( + _redirect_html(target=to_rel, canonical=canonical), + encoding="utf-8", + ) + written += 1 + + return written + + +def write_root_mirror_redirects( + output_root: Path, + *, + version_name: str = "current", + baseurl: str = DOCS_BASEURL, + docs_dir: Path | None = None, +) -> int: + """Mirror redirect stubs at site root for links that omit the version prefix.""" + output_root = output_root.resolve() + version_dir = output_root / version_name + if not version_dir.is_dir(): + return 0 + + written = 0 + for entry in load_redirects(docs_dir): + from_rel = entry["from"] + to_rel = entry["to"] + stub_path = output_root / from_rel + target_path = version_dir / to_rel + + if stub_path.exists(): + continue + if not target_path.exists(): + continue + + stub_path.parent.mkdir(parents=True, exist_ok=True) + target_url = f"{version_name}/{to_rel}" + canonical = f"{baseurl.rstrip('/')}/{target_url}" + stub_path.write_text( + _redirect_html(target=target_url, canonical=canonical), + encoding="utf-8", + ) + written += 1 + + return written + + +def main() -> None: + parser = argparse.ArgumentParser(description="Write HTML redirect stubs for docs.") + parser.add_argument( + "version_outdir", + type=Path, + help="Built version directory (e.g. docs/build/html/current)", + ) + parser.add_argument( + "--version-name", + default="current", + help="Version folder name for canonical URLs (default: current)", + ) + parser.add_argument( + "--baseurl", + default=DOCS_BASEURL, + help="Site base URL for canonical links", + ) + args = parser.parse_args() + + count = write_redirects( + args.version_outdir, + version_name=args.version_name, + baseurl=args.baseurl, + ) + print(f"Wrote {count} redirect stub(s) to {args.version_outdir}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/docs/source/_landing.rst b/docs/source/_landing.rst new file mode 100644 index 0000000..29e8925 --- /dev/null +++ b/docs/source/_landing.rst @@ -0,0 +1,105 @@ +.. raw:: html + +
+

The Lakeflow Framework (LFF)

+

Metadata-driven pipelines for Databricks Lakeflow SDP

+

+ Configure once. Reuse everywhere. LFF transforms declarative metadata into production-ready + Lakeflow Spark Declarative Pipelines (SDP) using reusable patterns and Databricks + Declarative Automation Bundles (DABs). +

+
+ + Get Started + + + + + View on GitHub + +
+
+ +.. raw:: html + +
+

One framework. Any operating model.

+

+ Built to fit the way your organization delivers data. Whether your + teams operate through a centralized platform, a hybrid model, or + federated domain ownership, LFF adapts to your operating model. + Use consistent implementation patterns to build medallion pipelines, + domain-owned data products, or architectures tailored to your business. +

+
+ +.. include:: _lff_architecture_diagram.rst + +.. container:: lf-landing-onboarding + + New to LFF? Start at :doc:`/get-started/index` — product overview, Quick Start, or straight into the docs. + +.. raw:: html + +
+
+
+ +

Architecture

+
+
+

Operating models, Framework and Pipeline Bundles, data flow specs, and DABs on Lakeflow SDP.

+ + + Explore architecture + +
+
+
+ +

Samples

+
+
+

Feature samples, pattern samples, and the end-to-end TPCH reference warehouse.

+ + + Browse feature & pattern samples + +
+
+
+ +

Build

+
+
+

Bundle structure, pipeline bundle steps, data flow spec reference, and medallion patterns.

+ + + Author pipeline bundles & specs + +
+
+
+ +

Deploy

+
+
+

Deploy the Framework Bundle and Pipeline Bundles — flat DAB deploy, pip wheel, local CLI, or CI/CD.

+ + + Deploy framework & pipelines + +
+
+
+ +

Features

+
+
+

Metadata-driven specs, configuration, Python extensions, data quality, sources and targets, and platform features.

+ + + Browse features by category + +
+
diff --git a/docs/source/_lff_architecture_diagram.rst b/docs/source/_lff_architecture_diagram.rst new file mode 100644 index 0000000..3264d9a --- /dev/null +++ b/docs/source/_lff_architecture_diagram.rst @@ -0,0 +1,84 @@ +.. raw:: html + +
+ + + + + + + + + + + Framework Bundle + + + + + + + + Abstraction Layer + Data Flow Spec · Patterns · Features + + + + + + + + + + + + + + + + SDP Wrapper + Spark Declarative Pipelines APIs + + + + + + + + + + + + + Pipeline Bundle + + + + + + + Resource definitions + Pipelines · Jobs + + + + + + + + Data Flow Specs + Metadata Per Data Flow · src/dataflows/ + + + + + + + + + + DABS Foundation + Declarative Automation Bundles + + +
diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css index e2a0e8c..3c62957 100644 --- a/docs/source/_static/custom.css +++ b/docs/source/_static/custom.css @@ -47,51 +47,30 @@ vertical-align: baseline; } -.docs-breadcrumb-version-meta { - font-size: 0.9em; - color: #6f6f6f; -} - -.docs-breadcrumb-version-meta strong { - color: inherit; - font-weight: 600; -} - -.docs-breadcrumb-version-meta .docs-current-badge { - margin: 0 0.45em 0 0.25em; -} - /* markdown styles for raw html */ .highlight-row { background-color: green !important; color: white !important; } -/* Custom styling for sphinx-tabs */ +/* Custom styling for sphinx-tabs — layout/structure only; colours in databricks-theme.css */ .sphinx-tabs { - margin-bottom: 1rem; + margin-bottom: 0.75rem; } .sphinx-tabs-tab { - background-color: #f3f6f6; - color: #404040; - padding: 0.5rem 1rem; - border: 1px solid #e1e4e5; + padding: 0.35rem 0.75rem; border-bottom: none; font-weight: 500; - font-size: 0.9rem; + font-size: 0.75rem; + line-height: 1.25; } .sphinx-tabs-tab[aria-selected="true"] { - background-color: #fff; - color: #2980b9; - border-bottom: 2px solid #2980b9; + margin: 0; } .sphinx-tabs-panel { - background-color: #f8f8f8 !important; - border: 1px solid #e1e4e5; - border-top: none; padding: 0; margin-bottom: 0; } @@ -147,17 +126,3 @@ .sphinx-tabs-panel .highlight:last-child { margin-bottom: 0; } - -.sphinx-tabs-panel .highlight pre { - padding-bottom: 12px; -} - -/* Keep RTD footer versions stacked one per line. */ -.rst-versions .rst-other-versions dd { - float: none; - width: 100%; -} - -.rst-versions .rst-other-versions dd a { - display: block; -} diff --git a/docs/source/_static/databricks-theme.css b/docs/source/_static/databricks-theme.css new file mode 100644 index 0000000..dcf43ba --- /dev/null +++ b/docs/source/_static/databricks-theme.css @@ -0,0 +1,1933 @@ +/* + * Databricks header CTA ("Try Databricks") — bg-cta-red from databricks.com + * Base: #EB1600 Hover: #BD2B26 Active/light: #FF5F46 + */ +:root, +[data-md-color-scheme="default"] { + --md-primary-fg-color: #eb1600; + --md-primary-fg-color--light: #ff5f46; + --md-primary-fg-color--dark: #bd2b26; + --md-accent-fg-color: #1b3139; + + /* sphinx-design alignment */ + --sd-color-primary: #eb1600; + --sd-color-primary-highlight: #bd2b26; + --sd-color-primary-text: #fff; + --sd-color-tabs-label-active: #eb1600; + --sd-color-tabs-label-hover: #bd2b26; + --sd-color-tabs-underline-active: #eb1600; + --sd-color-card-border-hover: #eb1600; + + /* sphinx-tabs (JSON/YAML format switchers) */ + --lf-tabs-divider: #dce0e2; + --lf-tabs-border: #dce0e2; + --lf-tabs-inactive-bg: #f3f6f6; + --lf-tabs-inactive-fg: #566268; + --lf-tabs-active-bg: #fff; + --lf-tabs-active-fg: #eb1600; + --lf-tabs-active-indicator: #eb1600; + + /* Command snippets (.lf-command-block) */ + --lf-command-bg: #f5f5f5; + --lf-command-fg: #364152; + --lf-command-copy: #8b949e; + --lf-command-copy-hover-bg: rgba(255, 255, 255, 0.65); + --lf-command-copy-hover-fg: #57606a; + + /* Feature metadata strip badges (shared with header search) */ + --lf-badge-bg: rgba(235, 22, 0, 0.1); + --lf-badge-border: rgba(235, 22, 0, 0.22); + --lf-badge-fg: #c41200; +} + +[data-md-color-scheme="slate"] { + --md-primary-fg-color: #eb1600; + --md-primary-fg-color--light: #ff5f46; + --md-primary-fg-color--dark: #bd2b26; + --md-accent-fg-color: #dce0e2; + + --sd-color-primary: #eb1600; + --sd-color-primary-highlight: #ff5f46; + --sd-color-primary-text: #fff; + --sd-color-tabs-label-active: #ff5f46; + --sd-color-tabs-label-hover: #ff5f46; + --sd-color-tabs-underline-active: #ff5f46; + --sd-color-card-border-hover: #ff5f46; + + --lf-tabs-divider: #3d484d; + --lf-tabs-border: #3d484d; + --lf-tabs-inactive-bg: rgba(255, 255, 255, 0.06); + --lf-tabs-inactive-fg: #a9b3b8; + --lf-tabs-active-bg: #243036; + --lf-tabs-active-fg: #ff5f46; + --lf-tabs-active-indicator: #ff5f46; + + --lf-command-bg: #2a3438; + --lf-command-fg: #e6edf3; + --lf-command-copy: #8b949e; + --lf-command-copy-hover-bg: rgba(255, 255, 255, 0.08); + --lf-command-copy-hover-fg: #e6edf3; + + --lf-badge-bg: rgba(255, 95, 70, 0.16); + --lf-badge-border: rgba(255, 95, 70, 0.28); + --lf-badge-fg: #ff8a75; +} + +/* Landing hero — WAF-style centered light panel */ +.lf-hero { + margin: 0 0 2.5rem; + padding: 2.5rem 1.5rem 2rem; + text-align: center; + background: transparent; + color: var(--md-default-fg-color, #1b3139); +} + +.lf-hero__title { + margin: 0 0 0.5rem; + color: #eb1600; + font-size: 3rem; + font-weight: 700; + line-height: 1.15; + letter-spacing: -0.02em; +} + +/* Beat sphinx-immaterial .md-typeset h1 (light gray, 2em) */ +.md-typeset .lf-hero h1.lf-hero__title { + margin: 0 0 0.5rem; + color: #eb1600; + font-size: 3rem; + font-weight: 700; + line-height: 1.15; + letter-spacing: -0.02em; +} + +/* Hero subtitle — same colour as lead; larger semibold type */ +.md-typeset .lf-hero p.lf-hero__subtitle, +.md-typeset .lf-landing-blurb p.lf-hero__subtitle { + max-width: 46rem; + margin: 0 auto 1.25rem; + color: var(--md-default-fg-color, #1b3139); + font-family: var(--md-text-font, "Roboto"), sans-serif; + font-size: 1.75rem; + font-weight: 600; + line-height: 1.35; + letter-spacing: -0.015em; + opacity: 0.92; +} + +.md-typeset .lf-hero p.lf-hero__lead, +.md-typeset .lf-landing-blurb p.lf-hero__lead { + max-width: 46rem; + margin: 0 auto 1.75rem; + font-size: 1.05rem; + line-height: 1.65; + color: var(--md-default-fg-color, #1b3139); + opacity: 0.92; +} + +.lf-landing-blurb { + max-width: 46rem; + margin: 0 auto 1.5rem; + text-align: center; +} + +/* + * LFF architecture diagram — inline SVG themed for Material light/slate. + * Accents stay; surfaces and text follow the docs color scheme. + */ +.lf-arch { + --lf-arch-fg: #1b3139; + --lf-arch-muted: #445861; + --lf-arch-card-bg: #ffffff; + --lf-arch-fw: #2b7bb9; + --lf-arch-fw-header: #e6f2f8; + --lf-arch-fw-panel: #f3f9fc; + /* Pipeline Bundle — former DABS green */ + --lf-arch-pl: #3d9a5f; + --lf-arch-pl-header: #e8f5ec; + --lf-arch-pl-panel: #f2faf5; + /* DABS Foundation — Option 1 (CTA red) in light mode */ + --lf-arch-dabs: #eb1600; + --lf-arch-dabs-bg: rgba(235, 22, 0, 0.16); + --lf-arch-dabs-border: #eb1600; + + display: block; + max-width: 46rem; + margin: 0 auto 1.75rem; + text-align: center; +} + +[data-md-color-scheme="slate"] .lf-arch { + --lf-arch-fg: #e8eef0; + --lf-arch-muted: #b0bec3; + --lf-arch-card-bg: #1e2629; + --lf-arch-fw: #5dade2; + --lf-arch-fw-header: rgba(93, 173, 226, 0.22); + --lf-arch-fw-panel: rgba(93, 173, 226, 0.1); + --lf-arch-pl: #6fcf97; + --lf-arch-pl-header: rgba(111, 207, 151, 0.22); + --lf-arch-pl-panel: rgba(111, 207, 151, 0.1); + /* DABS — Option 4 (slate bump) */ + --lf-arch-dabs: #eb1600; + --lf-arch-dabs-bg: rgba(235, 22, 0, 0.2); + --lf-arch-dabs-border: #ff5f46; +} + +.lf-arch__svg { + display: block; + width: 100%; + max-width: 840px; + height: auto; + margin: 0 auto; +} + +.lf-arch__caption { + margin: 0.5rem 0 0; + font-size: 0.78rem; + color: var(--lf-arch-muted); +} + +.lf-arch__title, +.lf-arch__card-title { + font-family: var(--md-text-font, "Roboto"), sans-serif; + font-weight: 700; + fill: var(--lf-arch-fg); +} + +.lf-arch__card-sub { + font-family: var(--md-text-font, "Roboto"), sans-serif; + font-weight: 400; + fill: var(--lf-arch-muted); +} + +.lf-arch__panel--fw { + fill: var(--lf-arch-fw-panel); + stroke: none; +} + +.lf-arch__panel--pl { + fill: var(--lf-arch-pl-panel); + stroke: none; +} + +.lf-arch__panel--dabs { + fill: var(--lf-arch-dabs-bg); + stroke: none; +} + +/* Outer frames drawn last so header fills never cover the border */ +.lf-arch__frame { + fill: none; + stroke-width: 2.75; + pointer-events: none; +} + +.lf-arch__frame--fw { + stroke: var(--lf-arch-fw); +} + +.lf-arch__frame--pl { + stroke: var(--lf-arch-pl); +} + +.lf-arch__frame--dabs { + stroke: var(--lf-arch-dabs-border, var(--lf-arch-dabs)); +} + +.lf-arch__header--fw { + fill: var(--lf-arch-fw-header); +} + +.lf-arch__header--pl { + fill: var(--lf-arch-pl-header); +} + +.lf-arch__card { + fill: var(--lf-arch-card-bg); + stroke-width: 2; +} + +.lf-arch__card--fw { + stroke: var(--lf-arch-fw); +} + +.lf-arch__card--pl { + stroke: var(--lf-arch-pl); +} + +.lf-arch__arrow-line { + stroke: var(--lf-arch-fw); + stroke-width: 2.5; +} + +.lf-arch__arrow-head { + fill: var(--lf-arch-fw); + stroke: none; +} + +.lf-arch__icon--fw { + stroke: var(--lf-arch-fw); + color: var(--lf-arch-fw); + fill: none; + stroke-linecap: round; + stroke-linejoin: round; +} + +.lf-arch__icon--pl { + stroke: var(--lf-arch-pl); + color: var(--lf-arch-pl); + fill: none; + stroke-linecap: round; + stroke-linejoin: round; +} + +.lf-arch__icon--dabs { + stroke: var(--lf-arch-dabs); + color: var(--lf-arch-dabs); + fill: none; + stroke-linecap: round; + stroke-linejoin: round; +} + +.lf-arch__icon--layers { + stroke-width: 2.4; +} + +.lf-arch__icon--package { + stroke-width: 2; +} + +.lf-arch__icon--window, +.lf-arch__icon--flow, +.lf-arch__icon--doc { + stroke-width: 2; +} + +.lf-arch__icon--gear { + stroke-width: 1.85; +} + +.lf-arch__icon-dot { + fill: currentColor; + stroke: none; +} + +.lf-arch__icon--databricks { + fill: var(--lf-arch-dabs); + stroke: none; +} + +/* Secondary blurb below hero, above feature cards */ +.md-typeset section:has(> .lf-hero) > p, +.md-typeset .lf-hero + p { + max-width: 46rem; + margin: 0 auto 2rem; + text-align: center; + font-size: 1.05rem; + line-height: 1.65; + color: var(--md-default-fg-color, #1b3139); +} + +/* Onboarding line — directly above home cards, left-aligned */ +.md-typeset section:has(> .lf-hero) .lf-landing-onboarding, +.md-typeset .lf-landing-onboarding { + max-width: none; + margin: 0 0 1.75rem; + padding: 0; + text-align: left; + font-size: 1.05rem; + line-height: 1.65; + color: var(--md-default-fg-color, #1b3139); +} + +.md-typeset .lf-landing-onboarding > p { + margin: 0; + max-width: none; + text-align: left; +} + +/* Home — Why LFF (positioning) between hero blurb and capability cards */ +.md-typeset section:has(> .lf-hero) > h2, +.md-typeset .lf-hero ~ h2 { + max-width: 46rem; + margin: 0 auto 1rem; + text-align: center; + font-size: 1.5rem; +} + +.md-typeset section:has(> .lf-hero) > h2 ~ p, +.md-typeset .lf-hero ~ h2 ~ p { + max-width: 46rem; + margin-left: auto; + margin-right: auto; +} + +.md-typeset section:has(> .lf-hero) > h2 ~ ul, +.md-typeset .lf-hero ~ h2 ~ ul { + max-width: 46rem; + margin: 1rem auto 2rem; +} + +/* Landing page — full-width content column (no sidebar gutter) */ +body:has(.lf-hero) .md-main__inner { + display: block; + margin-top: 0.75rem; +} + +body:has(.lf-hero) .md-main__inner .md-sidebar[hidden] { + display: none !important; +} + +body:has(.lf-hero) .md-content { + width: 100%; + max-width: none; +} + +body:has(.lf-hero) .md-content__inner { + max-width: none; + margin: 0 auto; + padding: 0 clamp(0.75rem, 2vw, 1.5rem) 2rem; +} + +.lf-feature-grid { + display: grid; + grid-template-columns: repeat(1, minmax(0, 1fr)); + gap: 1rem; + margin: 0 0 2rem; +} + +/* Features hub category cards — slightly denser than home marketing cards */ +.lf-hub-grid { + margin-top: 1.25rem; +} + +.lf-hub-grid .lf-feature-card { + padding: 1.25rem; +} + +.lf-hub-grid .lf-feature-card__body { + font-size: 0.9rem; + line-height: 1.55; +} + +@media (min-width: 48em) { + .lf-feature-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (min-width: 64em) { + .lf-feature-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +.lf-feature-card { + display: flex; + flex-direction: column; + margin: 0; + padding: 1.5rem; + background: #fff; + border: 1px solid #e5e5e5; + border-top: 3px solid #eb1600; + border-radius: 0; + box-shadow: none; +} + +.lf-feature-card__header { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0 0 0.75rem; +} + +.lf-feature-card__icon { + width: 1.25rem; + height: 1.25rem; + flex-shrink: 0; + color: #1b3139; +} + +.lf-feature-card__title { + margin: 0; + color: #1b3139; + font-size: 1rem; + font-weight: 700; + line-height: 1.35; +} + +.md-typeset .lf-feature-card h3.lf-feature-card__title { + margin: 0; + color: #1b3139; + font-size: 1rem; + font-weight: 700; + line-height: 1.35; +} + +.lf-feature-card__divider { + width: 100%; + height: 0; + margin: 0 0 1rem; + border: none; + border-top: 1px solid #e5e5e5; +} + +.lf-feature-card__body { + flex: 1; + margin: 0 0 1.25rem; + color: var(--md-default-fg-color, #1b3139); + font-size: 0.95rem; + line-height: 1.65; +} + +.md-typeset .lf-feature-card p.lf-feature-card__body { + margin: 0 0 1.25rem; + font-size: 0.95rem; + line-height: 1.65; +} + +.lf-feature-card__link { + display: inline-flex; + align-items: center; + gap: 0.35rem; + margin-top: auto; + color: #eb1600; + font-size: 0.9rem; + font-weight: 500; + text-decoration: none; +} + +.lf-feature-card__link:hover { + color: #bd2b26; + text-decoration: none; +} + +.lf-feature-card__link-icon { + width: 1rem; + height: 1rem; + flex-shrink: 0; +} + +[data-md-color-scheme="slate"] .lf-feature-card { + background: var(--md-default-bg-color, #1e2128); + border-color: #3a3f4b; + border-top-color: #eb1600; +} + +[data-md-color-scheme="slate"] .lf-feature-card__icon, +[data-md-color-scheme="slate"] .lf-feature-card__title, +[data-md-color-scheme="slate"] .md-typeset .lf-feature-card h3.lf-feature-card__title { + color: #dce0e2; +} + +[data-md-color-scheme="slate"] .lf-feature-card__divider { + border-top-color: #3a3f4b; +} + +.lf-hero-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 0; + justify-content: center; +} + +.lf-hero-actions .lf-hero-btn { + display: inline-flex; + align-items: center; + gap: 0.4rem; +} + +.lf-hero-actions .lf-btn-icon { + width: 1.1em; + height: 1.1em; + flex-shrink: 0; +} + +.lf-hero-actions .lf-btn-icon--after { + margin-left: 0.15rem; +} + +.lf-hero-actions .lf-btn-icon--before { + margin-right: 0.05rem; +} + +.lf-hero-actions .md-button--primary { + background-color: #eb1600; + border-color: #eb1600; + color: #fff; +} + +.lf-hero-actions .md-button--primary:hover { + background-color: #bd2b26; + border-color: #bd2b26; +} + +.lf-hero-actions .md-button--secondary { + background-color: #fff; + border: 1px solid #eb1600; + color: #eb1600; +} + +.lf-hero-actions .md-button--secondary:hover { + background-color: rgba(235, 22, 0, 0.06); + border-color: #bd2b26; + color: #bd2b26; +} + +/* Hide duplicate RST section title when landing hero is present */ +.md-typeset section:has(> .lf-hero) > h1, +.md-content__inner > h1:first-child:has(+ .document .lf-hero), +.md-content__inner > h1:first-child:has(+ .lf-hero), +.md-typeset > h1:first-child:has(+ .lf-hero) { + display: none; +} + +.md-header { + background-color: #eb1600; +} +.md-header .md-header__title, +.md-header .md-header__ellipsis, +.md-header .md-header__button, +.md-header .md-icon { + color: #fff; +} + +.md-header .md-header__button.md-logo img { + height: 1.75rem; + width: auto; +} + +.md-header .md-header__button.md-logo svg { + display: none; +} + +/* + * Header scroll behaviour (toc.follow): keep "Lakeflow Framework" and the + * version selector visible. Theme default hides the first .md-header__topic + * (where .md-version is injected) when md-header__title--active is set. + */ +.md-header__title, +.md-header__ellipsis, +.md-header__topic { + overflow: visible; +} + +.md-header__title--active .md-header__topic { + opacity: 1; + pointer-events: auto; + transform: none; + z-index: 0; +} + +.md-header__title--active .md-header__topic + .md-header__topic { + opacity: 0; + pointer-events: none; + transform: translateX(1.25rem); + z-index: -1; +} + +.md-header__title--active .md-version { + opacity: 1; + pointer-events: auto; + visibility: visible; +} + +.md-header__source, +.md-source, +.md-source__repository, +.md-source__fact, +.md-source__fact--version { + color: rgba(255, 255, 255, 0.92) !important; +} + +.md-source__icon, +.md-source__icon svg { + color: #fff !important; + fill: currentColor !important; +} + +.md-tabs { + background-color: #eb1600; + color: #fff; +} + +.md-tabs__list { + align-items: center; + margin: 0 0 0 0.2rem; + padding: 0; +} + +.md-tabs__item { + display: flex; + align-items: center; + height: 2.4rem; + padding-left: 0.6rem; + padding-right: 0.6rem; +} + +.md-tabs__link { + display: flex; + align-items: center; + height: 100%; + margin-top: 0; + padding: 0; + font-size: 0.7rem; + font-weight: 400; + line-height: 1.2; + box-sizing: border-box; +} + +.md-tabs__link span { + display: inline-block; + line-height: 1.2; + vertical-align: middle; +} + +/* Nav tabs — WAF-style: active bright white, inactive muted */ +.md-tabs__link { + color: rgba(255, 255, 255, 0.62) !important; + font-weight: 400; + opacity: 1 !important; + transition: color 0.15s ease; +} + +.md-tabs__link:hover, +.md-tabs__link:focus { + color: rgba(255, 255, 255, 0.82) !important; +} + +.md-tabs__link:visited { + color: rgba(255, 255, 255, 0.62) !important; +} + +.md-tabs__item--active .md-tabs__link, +.md-tabs__link--active { + color: #fff !important; + font-weight: 400; + opacity: 1 !important; +} + +.md-tabs__item--active .md-tabs__link:hover, +.md-tabs__item--active .md-tabs__link:focus, +.md-tabs__item--active .md-tabs__link:visited { + color: #fff !important; +} + +/* Search dialog — badge-tint input on red header; results use page colors */ +.md-search[data-md-component="search"] .md-search__input { + color: var(--md-default-fg-color, #1b3139); + background-color: var(--lf-badge-bg); + border: 1px solid var(--lf-badge-border); +} + +.md-search[data-md-component="search"] .md-search__input::placeholder { + color: rgba(255, 255, 255, 0.72); +} + +.md-search[data-md-component="search"] .md-search__icon { + color: rgba(255, 255, 255, 0.85); +} + +.md-search[data-md-component="search"] .md-search-result__meta, +.md-search[data-md-component="search"] .md-search-result__item { + color: var(--md-default-fg-color, #1b3139); +} + +[data-md-color-scheme="slate"] .md-search[data-md-component="search"] .md-search__input { + color: #fff; + background-color: var(--lf-badge-bg); + border-color: var(--lf-badge-border); +} + +[data-md-color-scheme="slate"] .md-search[data-md-component="search"] .md-search__input::placeholder, +[data-md-color-scheme="slate"] .md-search[data-md-component="search"] .md-search__icon { + color: rgba(255, 255, 255, 0.72); +} +[data-md-color-scheme="slate"] .md-search[data-md-component="search"] .md-search-result__meta, +[data-md-color-scheme="slate"] .md-search[data-md-component="search"] .md-search-result__item { + color: var(--md-default-fg-color--lighter, #b0bec5); +} + +/* Primary sidebar — WAF-style: grey links, red active, generous row padding */ +.md-nav--primary .md-nav__link[href] { + color: var(--md-default-fg-color, #1b3139); +} + +.md-nav--primary .md-nav__link[href]:visited { + color: var(--md-default-fg-color, #1b3139); +} + +.md-nav--primary .md-nav__item .md-nav__link--active, +.md-nav--primary .md-nav__item--active > .md-nav__link.md-nav__link--active { + color: var(--md-primary-fg-color, #eb1600); +} + +.md-nav--primary .md-nav__item .md-nav__link--active:visited, +.md-nav--primary .md-nav__item--active > .md-nav__link.md-nav__link--active:visited { + color: var(--md-primary-fg-color, #eb1600); +} + +.md-nav--primary .md-nav__link[href]:hover, +.md-nav--primary .md-nav__link[href]:focus-visible { + color: var(--md-primary-fg-color, #eb1600); +} + +.md-nav--primary .md-nav__title { + color: var(--md-default-fg-color, #1b3139); + font-weight: 700; +} + +.md-nav--primary .md-nav__link { + margin-top: 0; + padding: 0.35rem 0.75rem; + scroll-snap-align: none; +} + +.md-nav--primary .md-nav__link > .md-nav__link { + padding: 0; +} + +/* + * Sidebar scroll — use one scroll container (scrollwrap), not nested nav lists. + * Lifted tabs pin nested section nav to a 1fr grid row, which clips long lists + * (e.g. Features) before the scrollbar reaches the last items. + * + * Only unlock the level-1 section shell — do NOT force every nested .md-nav to + * display:block/height:auto (that leaves collapsed siblings taking vertical + * space → large empty gaps between items like Tables & Quality). + */ +.md-nav--primary .md-nav__title ~ .md-nav__list { + overflow: visible; + scroll-snap-type: none; + flex: none; +} + +@media screen and (min-width: 76.25em) { + .md-header--lifted ~ .md-container .md-sidebar--primary .md-sidebar__scrollwrap { + max-height: calc(100vh - 4.8rem - 1rem); + margin: 0.5rem 0 1rem; + overflow-y: auto; + overscroll-behavior: contain; + } + + .md-sidebar--primary .md-sidebar__inner { + padding-bottom: 2rem; + } + + .md-nav--lifted.md-nav--primary > .md-nav__list > .md-nav__item > .md-nav, + .md-nav--lifted .md-nav[data-md-level="1"] { + position: static; + height: auto; + display: block; + grid-template-rows: none; + } +} + +/* + * Global primary nav: active-path only, no expand/collapse toggles. + * + * Nested children show only when an ancestor has .md-nav__item--active. + * Do not clip overflow on the active path — the theme draws the red edge as + * .md-nav__link--in-viewport::before outside the link (right: 100% + .3rem). + */ +.md-nav--primary .md-nav__item--nested > .md-nav__container > label.md-nav__link { + display: none !important; +} + +.md-nav--primary .md-nav__item--nested > .md-nav__container > .md-nav__link[href] { + width: 100%; + margin-right: 0; +} + +/* Hub pages with an in-page TOC render label[for=__toc] + link (no container). + * Hide the toggle label so the section title appears once (same as container hubs). */ +.md-nav--primary .md-nav__item--nested > label.md-nav__link[for] { + display: none !important; +} + +.md-nav--primary .md-nav__item--nested .md-nav__icon { + display: none !important; +} + +.md-nav--primary .md-nav__item--nested > .md-nav__toggle { + display: none !important; +} + +.md-nav--primary .md-nav__item:not(.md-nav__item--active) > .md-nav { + display: none !important; + height: 0 !important; + max-height: 0 !important; + overflow: hidden !important; + visibility: hidden; +} + +.md-nav--primary .md-nav__item--active > .md-nav { + display: block !important; + height: auto !important; + max-height: none !important; + overflow: visible !important; + visibility: visible !important; +} + +/* + * Red edge is theme .md-nav__link--in-viewport::before (in-page scroll spy). + * Do not draw it on top-level section/nav links (cross-page hrefs) — only on + * in-page TOC anchors (#overview, #configuration, …). + */ +.md-nav--primary .md-nav__link--in-viewport:not([href^="#"])::before { + content: none !important; + display: none !important; + width: 0 !important; + background: none !important; +} + +/* + * Content tables (Option A) — RST list-tables tagged lf-content-table in conf.py. + * h1 metadata strips (no thead) are excluded by the build hook. + */ +.md-typeset table.lf-content-table { + display: table !important; + width: 100% !important; + max-width: 100% !important; + margin: 0 0 1.25rem 0 !important; + margin-left: 0 !important; + margin-right: auto !important; + border-collapse: collapse; + table-layout: fixed; + font-size: 0.78rem; + line-height: 1.5; + border: 1px solid rgba(27, 49, 57, 0.12); + border-radius: 0.2rem; + background-color: var(--md-default-bg-color); + overflow-x: auto; +} + +.md-typeset table.lf-content-table > thead > tr > th { + background-color: #f4f6f8; + color: var(--md-default-fg-color, #1b3139); + font-weight: 700; + text-align: left; + vertical-align: top; + padding: 0.55rem 0.85rem; + border-bottom: 1px solid rgba(27, 49, 57, 0.14); + white-space: normal; + word-break: normal; +} + +/* Pattern sample tables from raw HTML (flat tr/th/td, no thead/tbody). */ +.md-typeset table.lf-content-table > tr:first-child > th { + background-color: #f4f6f8; + color: var(--md-default-fg-color, #1b3139); + font-weight: 700; + text-align: left; + vertical-align: top; + padding: 0.55rem 0.85rem; + border-bottom: 1px solid rgba(27, 49, 57, 0.14); + white-space: normal; + word-break: normal; +} + +.md-typeset table.lf-content-table > tbody > tr > td { + text-align: left; + vertical-align: top; + padding: 0.5rem 0.85rem; + border-top: 1px solid rgba(27, 49, 57, 0.1); + white-space: normal; + word-break: normal; + overflow-wrap: break-word; +} + +.md-typeset table.lf-content-table > tr:not(:first-child) > td { + text-align: left; + vertical-align: top; + padding: 0.5rem 0.85rem; + border-top: 1px solid rgba(27, 49, 57, 0.1); + white-space: normal; + word-break: normal; + overflow-wrap: break-word; +} + +.md-typeset table.lf-content-table > tbody > tr:hover, +.md-typeset table.lf-content-table > tr:not(:first-child):hover > td { + background-color: rgba(27, 49, 57, 0.03); +} + +/* Parameter-style 5-column tables */ +.md-typeset table.lf-table-cols-5 > thead > tr > :nth-child(1), +.md-typeset table.lf-table-cols-5 > tbody > tr > :nth-child(1) { + width: 22%; +} + +.md-typeset table.lf-table-cols-5 > thead > tr > :nth-child(2), +.md-typeset table.lf-table-cols-5 > tbody > tr > :nth-child(2) { + width: 9%; +} + +.md-typeset table.lf-table-cols-5 > thead > tr > :nth-child(3), +.md-typeset table.lf-table-cols-5 > tbody > tr > :nth-child(3) { + width: 9%; +} + +.md-typeset table.lf-table-cols-5 > thead > tr > :nth-child(4), +.md-typeset table.lf-table-cols-5 > tbody > tr > :nth-child(4) { + width: 14%; +} + +.md-typeset table.lf-table-cols-5 > thead > tr > :nth-child(5), +.md-typeset table.lf-table-cols-5 > tbody > tr > :nth-child(5) { + width: 46%; +} + +/* Sphinx basic.css sets span.pre { white-space: nowrap } — force wrap in tables */ +.md-typeset table.lf-content-table > tbody > tr > :nth-child(1) { + overflow-wrap: anywhere; + word-break: break-word; +} + +.md-typeset table.lf-content-table > tbody > tr > :nth-child(1) p, +.md-typeset table.lf-content-table > tbody > tr > :nth-child(1) code, +.md-typeset table.lf-content-table > tbody > tr > :nth-child(1) span.pre { + white-space: normal !important; + word-break: break-word; + overflow-wrap: anywhere; +} + +.md-typeset table.lf-table-cols-5 > thead > tr > :nth-child(2), +.md-typeset table.lf-table-cols-5 > tbody > tr > :nth-child(2), +.md-typeset table.lf-table-cols-5 > thead > tr > :nth-child(3), +.md-typeset table.lf-table-cols-5 > tbody > tr > :nth-child(3) { + white-space: nowrap; +} + +.md-typeset table.lf-table-cols-4 > thead > tr > :nth-child(1), +.md-typeset table.lf-table-cols-4 > tbody > tr > :nth-child(1) { + width: 16%; +} + +.md-typeset table.lf-table-cols-4 > thead > tr > :nth-child(2), +.md-typeset table.lf-table-cols-4 > tbody > tr > :nth-child(2) { + width: 14%; +} + +.md-typeset table.lf-table-cols-4 > thead > tr > :nth-child(3), +.md-typeset table.lf-table-cols-4 > tbody > tr > :nth-child(3) { + width: 14%; +} + +.md-typeset table.lf-table-cols-4 > thead > tr > :nth-child(4), +.md-typeset table.lf-table-cols-4 > tbody > tr > :nth-child(4) { + width: 56%; +} + +.md-typeset table.lf-table-cols-3 > thead > tr > :nth-child(1), +.md-typeset table.lf-table-cols-3 > tbody > tr > :nth-child(1) { + width: 20%; +} + +.md-typeset table.lf-table-cols-3 > thead > tr > :nth-child(2), +.md-typeset table.lf-table-cols-3 > tbody > tr > :nth-child(2) { + width: 18%; +} + +.md-typeset table.lf-table-cols-3 > thead > tr > :nth-child(3), +.md-typeset table.lf-table-cols-3 > tbody > tr > :nth-child(3) { + width: 62%; +} + +.md-typeset table.lf-table-cols-2 > thead > tr > :nth-child(1), +.md-typeset table.lf-table-cols-2 > tbody > tr > :nth-child(1) { + width: 28%; +} + +.md-typeset table.lf-table-cols-2 > thead > tr > :nth-child(2), +.md-typeset table.lf-table-cols-2 > tbody > tr > :nth-child(2) { + width: 72%; +} + +.md-typeset table.lf-content-table td p { + margin: 0 0 0.35em 0; +} + +.md-typeset table.lf-content-table td p:last-child { + margin-bottom: 0; +} + +.md-typeset table.lf-content-table td ul, +.md-typeset table.lf-content-table td ol { + margin: 0.25em 0 0; + padding-left: 1.2em; +} + +.md-typeset table.lf-content-table td li { + margin: 0; + padding: 0; +} + +.md-typeset table.lf-content-table td li p { + margin: 0; + padding: 0.1em 0; +} + +.md-typeset table.lf-content-table tr.highlight-row td, +.md-typeset .highlight-cell { + background-color: green !important; + color: white !important; +} + +[data-md-color-scheme="slate"] .md-typeset table.lf-content-table { + border-color: rgba(255, 255, 255, 0.12); +} + +[data-md-color-scheme="slate"] .md-typeset table.lf-content-table > thead > tr > th { + background-color: rgba(255, 255, 255, 0.06); +} + +[data-md-color-scheme="slate"] .md-typeset table.lf-content-table > tr:first-child > th { + background-color: rgba(255, 255, 255, 0.06); +} + +[data-md-color-scheme="slate"] .md-typeset table.lf-content-table > tbody > tr > td { + border-top-color: rgba(255, 255, 255, 0.1); +} + +[data-md-color-scheme="slate"] .md-typeset table.lf-content-table > tr:not(:first-child) > td { + border-top-color: rgba(255, 255, 255, 0.1); +} + +/* + * Feature metadata table (Applies To / Configuration Scope / Databricks Docs). + * Minimal definition rows + soft brand badges (Databricks red tint, square corners). + */ +.md-typeset section > h1 + table.docutils { + display: table; + width: fit-content; + max-width: 100%; + margin: 0 0 1.35rem 0; + border-collapse: separate; + border-spacing: 0; + font-size: 0.72rem; + line-height: 1.45; + background: none; + border: none; + border-radius: 0; + border-bottom: 1px solid rgba(27, 49, 57, 0.12); + padding-bottom: 0.75rem; + overflow: visible; +} + +.md-typeset section > h1 + table.docutils td { + text-align: left !important; + vertical-align: top; + padding: 0.45rem 0; + border: none; + background: transparent; +} + +.md-typeset section > h1 + table.docutils tr + tr td { + border-top: 1px solid rgba(27, 49, 57, 0.1); +} + +.md-typeset section > h1 + table.docutils td:first-child { + width: 1%; + white-space: nowrap; + color: var(--md-default-fg-color--light, #526770); + font-weight: 500; + border-right: none; + padding-right: 1.5rem; + vertical-align: top; +} + +.md-typeset section > h1 + table.docutils td p { + margin: 0; + text-align: left; +} + +.md-typeset section > h1 + table.docutils td ul { + margin: 0; + padding-left: 1.15em; +} + +.md-typeset section > h1 + table.docutils td li { + margin: 0; + padding: 0; +} + +.md-typeset section > h1 + table.docutils td li p { + margin: 0; + padding: 0.05em 0; +} + +[data-md-color-scheme="slate"] .md-typeset section > h1 + table.docutils { + border-bottom-color: rgba(255, 255, 255, 0.12); +} + +[data-md-color-scheme="slate"] .md-typeset section > h1 + table.docutils tr + tr td { + border-top-color: rgba(255, 255, 255, 0.1); +} + +/* Soft brand badges — overrides solid Bootstrap info/success; RST :bdg-* unchanged */ +.md-typeset .sd-badge.sd-bg-info, +.md-typeset .sd-badge.sd-bg-success { + background-color: var(--lf-badge-bg) !important; + color: var(--lf-badge-fg) !important; + border: 1px solid var(--lf-badge-border); + border-radius: 0.15rem; + font-weight: 600; + padding: 0.28em 0.55em; +} + +[data-md-color-scheme="slate"] .md-typeset .sd-badge.sd-bg-info, +[data-md-color-scheme="slate"] .md-typeset .sd-badge.sd-bg-success { + background-color: var(--lf-badge-bg) !important; + color: var(--lf-badge-fg) !important; + border-color: var(--lf-badge-border); +} + +/* Fenced / highlighted code blocks - Cursor editor panel */ +.md-typeset div[class*="highlight-"].notranslate { + display: block; + width: 100%; + margin: 0.75em 0; + border-radius: 0.375rem; + overflow: hidden; + background-color: #1a1a1a; +} + +.md-typeset div[class*="highlight-"].notranslate .highlight { + background-color: #1a1a1a !important; + margin: 0; +} + +/* Beat pygments.css default white background */ +.md-typeset div[class*="highlight-"] .highlight { + background: #1a1a1a !important; +} + +.md-typeset div[class*="highlight-"] .highlight, +.md-typeset div[class*="highlight-"] .highlighttable, +.md-typeset .highlighttable .code { + --lf-code-font-size: 0.62rem; + --md-code-bg-color: #1a1a1a; + --md-code-fg-color: #d4d4d4; + --md-code-hl-keyword-color: #9cdcfe; + --md-code-hl-string-color: #ce5cce; + --md-code-hl-number-color: #b5cea8; + --md-code-hl-special-color: #ce5cce; + --md-code-hl-function-color: #dcdcaa; + --md-code-hl-constant-color: #d4d4d4; + --md-code-hl-name-color: #d4d4d4; + --md-code-hl-operator-color: #d4d4d4; + --md-code-hl-punctuation-color: #ffd700; + --md-code-hl-comment-color: #6a9955; + --md-code-hl-generic-color: #858585; + --md-code-hl-variable-color: #9cdcfe; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: var(--lf-code-font-size); + line-height: 1.35; + background-color: #1a1a1a !important; +} + +.md-typeset .highlight .highlighttable { + display: flow-root; + width: 100%; + margin: 0; + border: none; + border-radius: 0; + background-color: #1a1a1a; +} + +.md-typeset .highlight .highlighttable tbody, +.md-typeset .highlight .highlighttable tr { + display: flex; + flex-direction: row; + width: 100%; +} + +.md-typeset .highlight .highlighttable td { + display: block; + padding: 0; + border: none; + vertical-align: top; +} + +.md-typeset .highlight .highlighttable td.linenos { + flex: 0 0 auto; + min-width: 2.75em; + background-color: #1a1a1a; + border-right: 1px solid #2d2d2d; + padding: 0.5em 0.55em 0.5em 0.65em; + user-select: none; + text-align: right; +} + +.md-typeset .highlight .highlighttable td.linenos pre, +.md-typeset .highlight .highlighttable td.linenos .linenodiv pre { + margin: 0; + padding: 0; + background: transparent; + color: #6e7681; + font-size: var(--lf-code-font-size, 0.62rem); + line-height: 1.35; + font-family: inherit; +} + +.md-typeset .highlight .highlighttable td.linenos .normal { + color: #6e7681; +} + +.md-typeset .highlight .highlighttable td.code { + flex: 1 1 auto; + min-width: 0; + overflow-x: auto; +} + +.md-typeset .highlight .highlighttable td.code pre { + margin: 0; + padding: 0.5em 0.75em; + background: transparent; + color: #d4d4d4; + font-size: var(--lf-code-font-size, 0.62rem); + line-height: 1.35; + font-family: inherit; +} + +.md-typeset div[class*="highlight-"] pre, +.md-typeset div[class*="highlight-"] .highlight pre { + margin: 0; + padding: 0.5em 0.75em; + font-size: var(--lf-code-font-size, 0.62rem); + line-height: 1.35; + background-color: #1a1a1a; + color: #d4d4d4; + font-family: inherit; +} + +.md-typeset div[class*="highlight-"] pre span, +.md-typeset .highlighttable td.code pre span { + font-size: inherit; + line-height: inherit; + font-family: inherit; +} + +.md-typeset pre > code { + font-size: inherit; + padding: 0.5em 0.75em; + background-color: transparent; + color: inherit; + font-family: inherit; +} + +.md-typeset div[class*="highlight-"] [data-linenos]:before { + color: #6e7681 !important; + box-shadow: inset -1px 0 #2d2d2d !important; + background-color: #1a1a1a !important; + font-size: var(--lf-code-font-size, 0.62rem); +} + +.md-typeset div[class*="highlight-"] .highlight .nt, +.md-typeset .highlighttable .highlight .nt { + color: #9cdcfe; +} + +.md-typeset div[class*="highlight-"] .highlight .s2, +.md-typeset div[class*="highlight-"] .highlight .s1, +.md-typeset div[class*="highlight-"] .highlight .ss, +.md-typeset .highlighttable .highlight .s2, +.md-typeset .highlighttable .highlight .s1, +.md-typeset .highlighttable .highlight .ss { + color: #ce5cce; +} + +.md-typeset div[class*="highlight-"] .highlight .kc, +.md-typeset .highlighttable .highlight .kc { + color: #d4d4d4; +} + +.md-typeset div[class*="highlight-"] .highlight .mi, +.md-typeset div[class*="highlight-"] .highlight .mf, +.md-typeset .highlighttable .highlight .mi, +.md-typeset .highlighttable .highlight .mf { + color: #b5cea8; +} + +.md-typeset div[class*="highlight-"] .highlight .p, +.md-typeset .highlighttable .highlight .p { + color: #ffd700; +} + +.md-typeset div[class*="highlight-"] .highlight .w, +.md-typeset .highlighttable .highlight .w { + color: transparent; +} + +.md-typeset .highlight span.filename { + font-size: 0.62rem; + background-color: #252526; + color: #cccccc; + border-bottom-color: #2d2d2d; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; +} + +/* + * Command snippets (Quick Start) — WAF / MkDocs Material-style light panels. + * Scoped via .lf-command-block on the code block (explicit in RST/MD): + * RST: .. code-block:: console + * :class: lf-command-block + * MyST: ```{code-block} console + * :class: lf-command-block + * Dark Cursor panels stay default elsewhere. + */ +.md-typeset div.lf-command-block[class*="highlight-"].notranslate { + display: block; + width: 100%; + margin: 0.35em 0 0.5em; + padding: 0; + border: none; + border-radius: 0.2rem; + overflow: hidden; + background-color: var(--lf-command-bg); + box-shadow: none; +} + +.md-typeset ol li div.lf-command-block[class*="highlight-"].notranslate, +.md-typeset ul li div.lf-command-block[class*="highlight-"].notranslate { + margin-top: 0.25em; +} + +.md-typeset div.lf-command-block[class*="highlight-"].notranslate .highlight, +.md-typeset div.lf-command-block .highlighttable, +.md-typeset div.lf-command-block .highlighttable .code { + --lf-command-font-size: 0.75rem; + --md-code-bg-color: var(--lf-command-bg); + --md-code-fg-color: var(--lf-command-fg); + background-color: var(--lf-command-bg) !important; + font-family: var(--md-code-font, "Roboto Mono"), ui-monospace, SFMono-Regular, Menlo, Monaco, + Consolas, monospace; + font-size: var(--lf-command-font-size); + line-height: 1.4; +} + +.md-typeset div.lf-command-block .highlight .highlighttable { + display: block; + width: 100%; + margin: 0; + border: none; + border-radius: 0; + background-color: var(--lf-command-bg); +} + +.md-typeset div.lf-command-block .highlight .highlighttable tbody, +.md-typeset div.lf-command-block .highlight .highlighttable tr { + display: block; + width: 100%; +} + +.md-typeset div.lf-command-block .highlight .highlighttable td.linenos { + display: none !important; +} + +.md-typeset div.lf-command-block .highlight .highlighttable td.code { + display: block; + width: 100%; + overflow-x: auto; +} + +.md-typeset div.lf-command-block pre, +.md-typeset div.lf-command-block .highlight pre, +.md-typeset div.lf-command-block .highlight .go, +.md-typeset div.lf-command-block .highlight span { + margin: 0; + padding: 0; + background: transparent !important; + color: var(--lf-command-fg) !important; + font-size: var(--lf-command-font-size, 0.75rem); + line-height: 1.4; + font-family: inherit; + font-weight: 400; + white-space: pre; +} + +.md-typeset div.lf-command-block pre, +.md-typeset div.lf-command-block .highlight pre { + padding: 0.55em 0.75em; +} + +.md-typeset div.lf-command-block .highlight { + position: relative; + margin: 0; + background: var(--lf-command-bg) !important; + border: none; + box-shadow: none; +} + +/* One copy control — hide immaterial duplicate on command snippets */ +.md-typeset div.lf-command-block .md-clipboard { + display: none !important; +} + +/* Copy button — WAF-style: borderless icon, always visible */ +.md-typeset div.lf-command-block .highlight button.copybtn { + opacity: 0.65; + top: 0.3em; + right: 0.3em; + width: 1.5em; + height: 1.5em; + padding: 0; + border: none; + border-radius: 0.2rem; + background-color: transparent; + color: var(--lf-command-copy); + box-shadow: none; + transition: opacity 0.15s ease, color 0.15s ease, background-color 0.15s ease; +} + +.md-typeset div.lf-command-block .highlight:hover button.copybtn { + opacity: 0.85; +} + +.md-typeset div.lf-command-block .highlight button.copybtn:hover { + opacity: 1; + background-color: var(--lf-command-copy-hover-bg); + color: var(--lf-command-copy-hover-fg); +} + +.md-typeset div.lf-command-block .highlight button.copybtn.success { + opacity: 1; + background-color: transparent; + border: none; + color: var(--md-primary-fg-color, #eb1600); +} + +/* Task lists — WAF-style prerequisite checkboxes (Quick Start) */ +.md-typeset ul.contains-task-list { + margin-left: 0; + padding-left: 0; + list-style: none; +} + +.md-typeset .task-list-item { + margin-left: 0; + padding-left: 0; + list-style-type: none; +} + +.md-typeset .task-list-item-checkbox { + accent-color: var(--md-primary-fg-color, #eb1600); + margin-right: 0.5rem; + vertical-align: middle; +} + +/* Sphinx-tabs — compact sizing + Databricks colour scheme */ +.md-typeset .sphinx-tabs { + margin-bottom: 0.75rem; +} + +.md-typeset [role="tablist"] { + display: flex; + flex-wrap: wrap; + gap: 0.125rem; + align-items: flex-end; + border-bottom: 1px solid var(--lf-tabs-divider); +} + +.md-typeset .sphinx-tabs-tab { + font-size: 0.75rem; + line-height: 1.25; + padding: 0.35rem 0.75rem; + border-radius: 4px 4px 0 0; + color: var(--lf-tabs-inactive-fg); + background-color: var(--lf-tabs-inactive-bg); + border: 1px solid var(--lf-tabs-border); + border-bottom: none; + font-weight: 500; + transition: color 0.15s ease, background-color 0.15s ease; +} + +.md-typeset .sphinx-tabs-tab:hover { + color: var(--md-primary-fg-color--dark, #bd2b26); +} + +.md-typeset .sphinx-tabs-tab[aria-selected="true"] { + margin: 0; + color: var(--lf-tabs-active-fg); + background-color: var(--lf-tabs-active-bg); + border-color: var(--lf-tabs-border); + border-bottom: 2px solid var(--lf-tabs-active-indicator); + font-weight: 600; +} + +.md-typeset .sphinx-tabs-tab:focus-visible { + outline: 2px solid var(--md-primary-fg-color); + outline-offset: 1px; + z-index: 1; +} + +.md-typeset .sphinx-tabs-panel { + padding: 0; + margin: 0; + border: 1px solid var(--lf-tabs-border); + border-top: none; + border-radius: 0 0 4px 4px; + background: #1a1a1a; + overflow: hidden; +} + +.md-typeset .sphinx-tabs-panel.code-tab { + padding: 0; +} + +/* Code blocks inside tabs — flush with panel, no outer gutter */ +.md-typeset .sphinx-tabs-panel div[class*="highlight-"].notranslate { + margin: 0; + border-radius: 0; + width: 100%; +} + +.md-typeset .sphinx-tabs-panel div[class*="highlight-"].notranslate .highlight, +.md-typeset .sphinx-tabs-panel .highlighttable { + border-radius: 0; +} + +/* + * Version selector: open on click/focus, not hover. + * + * With navigation.tabs.sticky the tab bar is rendered inside .md-header as a + * sibling of .md-header__inner. Theme CSS sets .md-tabs { z-index: 3 } while + * the inner bar stays at auto, so the dropdown list paints behind tab labels. + */ +.md-header--lifted:has(.md-version:focus-within) .md-header__inner { + position: relative; + z-index: 4; +} + +.md-header .md-version { + position: relative; +} + +.md-header .md-version__list { + z-index: 1; +} + +.md-header .md-version:hover .md-version__list { + max-height: 0; + opacity: 0; + animation: none; +} + +.md-header .md-version:focus-within .md-version__list { + max-height: 18rem; + opacity: 1; + overflow: auto; + animation: none; +} + +/* Dropdown links — beat theme accent + browser :visited purple in dark mode */ +.md-header .md-version__link { + color: var(--md-default-fg-color); +} + +.md-header .md-version__link:visited { + color: var(--md-default-fg-color); +} + +.md-header .md-version__link:hover, +.md-header .md-version__link:focus { + color: var(--md-primary-fg-color, #eb1600); + background-color: var(--md-default-fg-color--lightest); +} + +.md-header .md-version__link:visited:hover, +.md-header .md-version__link:visited:focus { + color: var(--md-primary-fg-color, #eb1600); +} + +[data-md-color-scheme="slate"] .md-header .md-version__link:hover, +[data-md-color-scheme="slate"] .md-header .md-version__link:focus, +[data-md-color-scheme="slate"] .md-header .md-version__link:visited:hover, +[data-md-color-scheme="slate"] .md-header .md-version__link:visited:focus { + color: var(--md-primary-fg-color--light, #ff5f46); +} + +/* Mermaid — lf-arch palette (matches home-page architecture diagram) */ +.md-typeset .mermaid-diagram, +.lf-mermaid, +.lf-mermaid-shell, +.lf-mermaid-dialog__canvas { + --lf-mermaid-fg: #1b3139; + --lf-mermaid-muted: #445861; + --lf-mermaid-card-bg: #ffffff; + --lf-mermaid-fw: #2b7bb9; + --lf-mermaid-fw-header: #e6f2f8; + --lf-mermaid-fw-panel: #f3f9fc; + --lf-mermaid-edge: var(--lf-mermaid-fw); +} + +[data-md-color-scheme="slate"] .md-typeset .mermaid-diagram, +[data-md-color-scheme="slate"] .lf-mermaid, +[data-md-color-scheme="slate"] .lf-mermaid-shell, +[data-md-color-scheme="slate"] .lf-mermaid-dialog__canvas { + --lf-mermaid-fg: #e8eef0; + --lf-mermaid-muted: #b0bec3; + --lf-mermaid-card-bg: #1e2629; + --lf-mermaid-fw: #5dade2; + --lf-mermaid-fw-header: rgba(93, 173, 226, 0.22); + --lf-mermaid-fw-panel: rgba(93, 173, 226, 0.1); + --lf-mermaid-edge: var(--lf-mermaid-fw); +} + +/* Hide raw source until our renderer produces the SVG */ +.md-typeset .mermaid-diagram > pre.lf-mermaid-src, +.md-typeset .mermaid-diagram > pre.mermaid { + overflow: hidden; + visibility: hidden; + height: 0; + margin: 0; + padding: 0; + border: 0; +} + +.md-typeset .mermaid-diagram:has(> pre.lf-mermaid-src):not(:has(> .lf-mermaid-rendered)), +.md-typeset .mermaid-diagram:has(> pre.mermaid):not(:has(> div.mermaid)):not(:has(> .lf-mermaid-rendered)) { + min-height: 4rem; + margin: 1rem 0 1.25rem; + border: 2px solid var(--lf-mermaid-fw, #2b7bb9); + border-radius: 12px; + background: var(--lf-mermaid-fw-panel, #f3f9fc); +} + +.md-typeset .mermaid-diagram:has(> pre.lf-mermaid-src):not(:has(> .lf-mermaid-rendered))::before, +.md-typeset .mermaid-diagram:has(> pre.mermaid):not(:has(> div.mermaid)):not(:has(> .lf-mermaid-rendered))::before { + content: "Loading diagram…"; + display: block; + padding: 1rem 1.1rem; + color: var(--lf-mermaid-muted, rgba(27, 49, 57, 0.55)); + font-size: 0.82rem; +} + +.md-typeset .mermaid-diagram > .lf-mermaid-rendered, +.md-typeset .mermaid-diagram > div.mermaid { + display: block; + min-width: min(100%, 48rem); +} + +.md-typeset .lf-mermaid-rendered svg.lf-mermaid-svg { + display: block; + max-width: none; + height: auto; + margin: 0 auto; +} + +/* lf-arch SVG shapes — document stylesheet (inherits CSS vars from .mermaid-diagram) */ +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .node rect, +.lf-mermaid svg.lf-mermaid-svg .node rect, +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .node polygon, +.lf-mermaid svg.lf-mermaid-svg .node polygon, +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .node path, +.lf-mermaid svg.lf-mermaid-svg .node path, +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .node circle, +.lf-mermaid svg.lf-mermaid-svg .node circle, +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .node ellipse, +.lf-mermaid svg.lf-mermaid-svg .node ellipse { + fill: var(--lf-mermaid-card-bg) !important; + stroke: var(--lf-mermaid-fw) !important; + stroke-width: 2px !important; +} + +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .edgePath .path, +.lf-mermaid svg.lf-mermaid-svg .edgePath .path, +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .flowchart-link, +.lf-mermaid svg.lf-mermaid-svg .flowchart-link { + stroke: var(--lf-mermaid-edge) !important; + stroke-width: 2px !important; +} + +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .edgePath .arrowheadPath, +.lf-mermaid svg.lf-mermaid-svg .edgePath .arrowheadPath, +.md-typeset .mermaid-diagram svg.lf-mermaid-svg marker path, +.lf-mermaid svg.lf-mermaid-svg marker path, +.md-typeset .mermaid-diagram svg.lf-mermaid-svg marker polygon, +.lf-mermaid svg.lf-mermaid-svg marker polygon { + fill: var(--lf-mermaid-edge) !important; + stroke: none !important; +} + +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .cluster rect, +.lf-mermaid svg.lf-mermaid-svg .cluster rect { + fill: var(--lf-mermaid-fw-panel) !important; + stroke: var(--lf-mermaid-fw) !important; + stroke-width: 2.75px !important; +} + +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .label, +.lf-mermaid svg.lf-mermaid-svg .label, +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .nodeLabel, +.lf-mermaid svg.lf-mermaid-svg .nodeLabel, +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .label div, +.lf-mermaid svg.lf-mermaid-svg .label div, +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .nodeLabel p, +.lf-mermaid svg.lf-mermaid-svg .nodeLabel p { + color: var(--lf-mermaid-fg) !important; + fill: var(--lf-mermaid-fg) !important; +} + +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .edgeLabel, +.lf-mermaid svg.lf-mermaid-svg .edgeLabel, +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .edgeLabel p, +.lf-mermaid svg.lf-mermaid-svg .edgeLabel p { + color: var(--lf-mermaid-muted) !important; + fill: var(--lf-mermaid-muted) !important; +} + +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .cluster-label, +.lf-mermaid svg.lf-mermaid-svg .cluster-label, +.md-typeset .mermaid-diagram svg.lf-mermaid-svg .cluster .label span, +.lf-mermaid svg.lf-mermaid-svg .cluster .label span { + color: var(--lf-mermaid-fg) !important; + fill: var(--lf-mermaid-fg) !important; + font-weight: 700 !important; +} + +/* Mermaid — scrollable inline + click-to-expand modal */ +.md-typeset .lf-mermaid-shell { + margin: 1rem 0 1.25rem; + border: 2px solid var(--lf-mermaid-fw); + border-radius: 12px; + background: var(--lf-mermaid-fw-panel); + overflow: auto; + max-width: 100%; + cursor: zoom-in; +} + +.md-typeset .lf-mermaid-shell .lf-mermaid-rendered, +.md-typeset .lf-mermaid-shell .mermaid { + display: block; + min-width: min(100%, 48rem); + padding: 0.75rem 1rem 0.25rem; +} + +.md-typeset .lf-mermaid-shell .lf-mermaid-rendered svg, +.md-typeset .lf-mermaid-shell .mermaid svg { + display: block; + max-width: none; + height: auto; + margin: 0 auto; +} + +.md-typeset .lf-mermaid-hint { + margin: 0; + padding: 0.45rem 0.85rem; + border-top: 1px solid rgba(27, 49, 57, 0.1); + color: rgba(27, 49, 57, 0.62); + font-size: 0.72rem; + line-height: 1.35; + cursor: default; +} + +.lf-mermaid-dialog { + width: min(96vw, 72rem); + max-width: 96vw; + max-height: 92vh; + padding: 0; + border: 1px solid rgba(27, 49, 57, 0.18); + border-radius: 10px; + box-shadow: 0 18px 48px rgba(27, 49, 57, 0.22); +} + +.lf-mermaid-dialog::backdrop { + background: rgba(15, 23, 28, 0.55); +} + +.lf-mermaid-dialog__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.65rem 0.85rem; + border-bottom: 1px solid rgba(27, 49, 57, 0.12); + background: rgba(27, 49, 57, 0.03); +} + +.lf-mermaid-dialog__title { + font-size: 0.82rem; + font-weight: 600; + color: var(--md-default-fg-color, #1b3139); +} + +.lf-mermaid-dialog__tools { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.lf-mermaid-dialog__btn { + border: 1px solid rgba(27, 49, 57, 0.18); + border-radius: 6px; + background: #fff; + color: var(--md-default-fg-color, #1b3139); + font-size: 0.78rem; + line-height: 1; + padding: 0.35rem 0.55rem; + cursor: pointer; +} + +.lf-mermaid-dialog__btn:hover { + background: rgba(27, 49, 57, 0.05); +} + +.lf-mermaid-dialog__stage { + overflow: hidden; + height: min(68vh, 44rem); + background: #fff; + touch-action: none; +} + +.lf-mermaid-dialog__stage--dragging { + cursor: grabbing; +} + +.lf-mermaid-dialog__canvas { + transform-origin: top left; + padding: 1rem; + cursor: grab; +} + +.lf-mermaid-dialog__loading { + margin: 0; + padding: 1.5rem; + color: rgba(27, 49, 57, 0.62); + font-size: 0.82rem; +} + +.lf-mermaid-dialog__hint { + margin: 0; + padding: 0.45rem 0.85rem 0.65rem; + color: rgba(27, 49, 57, 0.62); + font-size: 0.72rem; +} + +[data-md-color-scheme="slate"] .md-typeset .mermaid-diagram:has(> pre.lf-mermaid-src):not(:has(> .lf-mermaid-rendered)), +[data-md-color-scheme="slate"] .md-typeset .mermaid-diagram:has(> pre.mermaid):not(:has(> div.mermaid)):not(:has(> .lf-mermaid-rendered)) { + border-color: var(--lf-mermaid-fw); + background: var(--lf-mermaid-fw-panel); +} + +[data-md-color-scheme="slate"] .md-typeset .lf-mermaid-shell { + border-color: var(--lf-mermaid-fw); + background: var(--lf-mermaid-fw-panel); +} + +[data-md-color-scheme="slate"] .md-typeset .lf-mermaid-hint, +[data-md-color-scheme="slate"] .lf-mermaid-dialog__hint { + color: rgba(255, 255, 255, 0.62); +} + +[data-md-color-scheme="slate"] .lf-mermaid-dialog__btn { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.18); + color: var(--md-default-fg-color, #fff); +} + +[data-md-color-scheme="slate"] .lf-mermaid-dialog__stage { + background: var(--md-default-bg-color, #1b3139); +} diff --git a/docs/source/_static/lff-logo.png b/docs/source/_static/lff-logo.png new file mode 100644 index 0000000..04c064b Binary files /dev/null and b/docs/source/_static/lff-logo.png differ diff --git a/docs/source/_static/mermaid-zoom.js b/docs/source/_static/mermaid-zoom.js new file mode 100644 index 0000000..6e79b91 --- /dev/null +++ b/docs/source/_static/mermaid-zoom.js @@ -0,0 +1,562 @@ +/** + * Lakeflow Mermaid: lf-arch styling, inline render (no shadow DOM), click-to-expand modal. + * + * Diagram source lives in a sibling