From 433e3aeb90c1c550febaefe6c355865aa4ec3ff4 Mon Sep 17 00:00:00 2001 From: iamv4g Date: Thu, 25 Jun 2026 17:15:36 +0700 Subject: [PATCH 1/4] Align dnd_kit docs with the stable 0.4.0 family - root README status now runs through US-079 and the coordinated 0.4.0 publication - dnd_kit_jaspr README status reads stable 0.4.0 instead of a dev release - dnd_kit_flutter family table drops the "current dev adapter release" wording --- README.md | 7 +++++-- packages/dnd_kit_flutter/README.md | 2 +- packages/dnd_kit_jaspr/README.md | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3af179d..d8e2b4e 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,11 @@ workspace unification through `US-061`, and the Jaspr sortable preset through axis-aware shared-core implementation slice, and the Flutter execution-layer adoption through `US-065`, plus the Jaspr execution-layer adoption through `US-066`, plus the Jaspr example feature gallery through `US-067`, plus the -Jaspr drag overlay controller rebind fix through `US-068`, and the shared -experimental multi-container parity slice through `US-076`. +Jaspr drag overlay controller rebind fix through `US-068`, the shared +experimental multi-container parity slice through `US-076`, the +production-ready multi-container graduation and website showcase through +`US-078`, and the coordinated family stable `0.4.0` publication (`dnd_kit`, +`dnd_kit_flutter`, `dnd_kit_jaspr`) through `US-079`. The living source of truth is split from historical [SPEC.md](SPEC.md) input material into product docs, story packets, validation expectations, and decision diff --git a/packages/dnd_kit_flutter/README.md b/packages/dnd_kit_flutter/README.md index abf5273..4d099c6 100644 --- a/packages/dnd_kit_flutter/README.md +++ b/packages/dnd_kit_flutter/README.md @@ -154,5 +154,5 @@ drag runtime. | Package | Use it for | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | [`dnd_kit_flutter`](https://pub.dev/packages/dnd_kit_flutter) | Flutter apps — widgets, sensors, overlays, and sortable presets. | -| [`dnd_kit_jaspr`](https://pub.dev/packages/dnd_kit_jaspr) | Jaspr (Dart web) apps — the current dev adapter release. | +| [`dnd_kit_jaspr`](https://pub.dev/packages/dnd_kit_jaspr) | Jaspr (Dart web) apps — the web adapter over the shared engine. | | [`dnd_kit`](https://pub.dev/packages/dnd_kit) | The shared, framework-agnostic engine. | diff --git a/packages/dnd_kit_jaspr/README.md b/packages/dnd_kit_jaspr/README.md index 0344cab..252f887 100644 --- a/packages/dnd_kit_jaspr/README.md +++ b/packages/dnd_kit_jaspr/README.md @@ -8,7 +8,7 @@ Flutter behave as peer adapters over one drag runtime — the same domain model, drag lifecycle, collision logic, modifiers, sortable math, and auto-scroll curve. It depends only on `dnd_kit` and `jaspr` (no Flutter). -> Status: current public development release. This package provides `DndScope`, +> Status: stable `0.4.0` release. This package provides `DndScope`, > `DndController`, `DndDraggable`, `DndDroppable`, `DndDragHandle`, > `DndDragOverlay`, `DndAutoScroll`, `DndLiveRegion`, `SortableScope`, and > `SortableItem` over the shared runtime. Shared modifiers and sortable From 3e6dcc4260e690784f70b3e4eaa8b4724bc47374 Mon Sep 17 00:00:00 2001 From: iamv4g Date: Thu, 25 Jun 2026 17:15:46 +0700 Subject: [PATCH 2/4] Add a multi-page documentation section to the website - introduce jaspr_router with a /docs route tree that generates one static page per topic - DocsShell provides the grouped sidebar, on-this-page rail, and previous/next pager shared by every docs page - CodeTabs renders each snippet on both the Flutter and Jaspr adapter - nine core pages: overview, install, quickstart, draggable, droppable, drag overlay, sortable lists, multi-container, accessibility - point the nav and footer Docs link at the /docs route and rewrite the Pages base href on every generated page so the docs deploy under the project subpath - record the Phase 32 docs work in the release roadmap and story packet --- .github/workflows/deploy-website.yml | 8 +- docs/product/release-roadmap.md | 22 +++ .../README.md | 48 +++++ pubspec.lock | 8 + website/README.md | 4 +- website/lib/app.dart | 74 +++++++ website/lib/data/site_data.dart | 6 +- website/lib/docs/code_tabs.dart | 75 +++++++ website/lib/docs/doc_components.dart | 142 +++++++++++++ website/lib/docs/docs_nav.dart | 143 ++++++++++++++ website/lib/docs/docs_shell.dart | 162 +++++++++++++++ .../lib/docs/pages/accessibility_page.dart | 115 +++++++++++ website/lib/docs/pages/draggable_page.dart | 172 ++++++++++++++++ website/lib/docs/pages/droppable_page.dart | 133 +++++++++++++ website/lib/docs/pages/install_page.dart | 85 ++++++++ .../lib/docs/pages/multi_container_page.dart | 141 +++++++++++++ website/lib/docs/pages/overlay_page.dart | 98 +++++++++ website/lib/docs/pages/overview_page.dart | 109 ++++++++++ website/lib/docs/pages/quickstart_page.dart | 187 ++++++++++++++++++ website/lib/docs/pages/sortable_page.dart | 138 +++++++++++++ website/lib/layout/mobile_nav.dart | 5 +- website/lib/layout/nav_bar.dart | 10 +- website/lib/main.client.options.dart | 10 + website/lib/main.server.dart | 4 +- website/lib/main.server.options.dart | 12 ++ website/pubspec.yaml | 1 + 26 files changed, 1902 insertions(+), 10 deletions(-) create mode 100644 docs/stories/phase-32-website-docs-page-and-doc-alignment/README.md create mode 100644 website/lib/app.dart create mode 100644 website/lib/docs/code_tabs.dart create mode 100644 website/lib/docs/doc_components.dart create mode 100644 website/lib/docs/docs_nav.dart create mode 100644 website/lib/docs/docs_shell.dart create mode 100644 website/lib/docs/pages/accessibility_page.dart create mode 100644 website/lib/docs/pages/draggable_page.dart create mode 100644 website/lib/docs/pages/droppable_page.dart create mode 100644 website/lib/docs/pages/install_page.dart create mode 100644 website/lib/docs/pages/multi_container_page.dart create mode 100644 website/lib/docs/pages/overlay_page.dart create mode 100644 website/lib/docs/pages/overview_page.dart create mode 100644 website/lib/docs/pages/quickstart_page.dart create mode 100644 website/lib/docs/pages/sortable_page.dart diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index 1862859..cb8d43c 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -68,18 +68,22 @@ jobs: set -e ls -la test -f index.html || { echo "ERROR: index.html not found"; exit 1; } + test -f docs/index.html || { echo "ERROR: docs page not found"; exit 1; } test -f main.client.dart.js || { echo "ERROR: client bundle not found"; exit 1; } echo "Build output OK" # The project Pages site lives at the /dnd_kit/ subpath. jaspr build has no # --base-href flag and Document emits , so rewrite it here - # (asset URLs in index.html are relative and resolve against this base). + # on every generated page (relative asset and cross-page URLs resolve + # against this base). - name: Set base href for the project subpath working-directory: website/build/jaspr run: | set -e - sed -i 's|||' index.html + find . -name '*.html' -exec \ + sed -i 's|||' {} + grep -q '' index.html + grep -q '' docs/index.html touch .nojekyll - name: Upload Pages artifact diff --git a/docs/product/release-roadmap.md b/docs/product/release-roadmap.md index 98f5b2a..a3f445c 100644 --- a/docs/product/release-roadmap.md +++ b/docs/product/release-roadmap.md @@ -317,6 +317,21 @@ publication after the multi-container graduation work has soaked: Phase README: `docs/stories/phase-31-coordinated-family-stable-0-4-0-release/README.md`. +## Phase 32 - Website Docs Page And Post-0.4.0 Doc Alignment + +Close the docs/adoption gap left after the stable `0.4.0` publication: + +- `US-080`: give the website a real Getting Started docs page on its own `/docs` + route (replacing the in-page `#docs` placeholder) and align the root README + status plus the Jaspr/Flutter package READMEs with the shipped stable `0.4.0` + family; +- `US-081`: expand `/docs` into a multi-page docs section (shared sidebar shell, + right-rail TOC, prev/next pager, Flutter|Jaspr code tabs) covering the nine + core pages. + +Phase README: +`docs/stories/phase-32-website-docs-page-and-doc-alignment/README.md`. + ## Current State The repository has implemented work through `US-079`. The Flutter adapter, the @@ -360,6 +375,13 @@ packet for that line: the prepared `0.4.0-dev.1` family metadata was promoted to stable `0.4.0`, local release proof passed through the shared verifier, and the three packages published to pub.dev in dependency order on 2026-06-24: `dnd_kit 0.4.0` -> `dnd_kit_flutter 0.4.0` -> `dnd_kit_jaspr 0.4.0`. +Phase 32 then closed the post-release docs/adoption gap: `US-080` gave the +website a real Getting Started docs page on its own statically generated `/docs` +route (replacing the `#docs` placeholder) and aligned the root README plus the +Jaspr and Flutter package READMEs with the shipped stable `0.4.0` family, and +`US-081` expanded `/docs` into a multi-page docs section — a shared `DocsShell` +(grouped sidebar, right-rail TOC, prev/next pager), a Flutter|Jaspr code-tab +block, and nine core pages over the shared engine surface. Future work should keep extending this roadmap through new product docs, story packets, and decisions rather than by reviving the old umbrella/core topology from the historical specs. diff --git a/docs/stories/phase-32-website-docs-page-and-doc-alignment/README.md b/docs/stories/phase-32-website-docs-page-and-doc-alignment/README.md new file mode 100644 index 0000000..7be774a --- /dev/null +++ b/docs/stories/phase-32-website-docs-page-and-doc-alignment/README.md @@ -0,0 +1,48 @@ +# Phase 32 - Website Docs Page And Post-0.4.0 Doc Alignment + +The coordinated family stable `0.4.0` published on 2026-06-24 (Phase 31), but +public-facing docs still described the pre-release state: the website had only a +`#docs` placeholder, the root README status stopped at `US-076`, and the Jaspr +and Flutter package READMEs still carried "dev release" wording. + +This phase closes that adoption gap across two normal-lane stories: + +- `US-080` gave the website a real Getting Started docs page on its own `/docs` + route (the marketing site was previously a single page with an in-page + `#docs` anchor placeholder) and aligned the root README plus the Jaspr and + Flutter package READMEs with the shipped stable `0.4.0` family; +- `US-081` then expanded `/docs` from that single page into a dndkit.com-style + multi-page docs section: a shared `DocsShell` (grouped sidebar, right-rail + "On this page" TOC, previous/next pager), a Flutter|Jaspr `CodeTabs` block, + and nine core pages (Overview, Installation, Quickstart, Draggable, + Droppable, Drag overlay, Sortable lists, Multi-container, Accessibility). + +## Principle + +Doc-alignment and website work in this phase must: + +- introduce real on-site routing rather than another in-page anchor: the docs + page is a separate statically generated route (`docs/index.html`), reached + from the nav and footer; +- keep the static build deployable under the `/dnd_kit/` project Pages subpath — + every generated page needs its `` rewritten, and cross-page links + resolve relative to that base; +- describe only the already-shipped `0.4.0` surface; no new product behavior. + +## Delivery Sequence + +| Story | Scope | Decision | +| --- | --- | --- | +| **US-080** | Add the website `/docs` Getting Started page (jaspr_router) and align root/package READMEs + website docs link with stable `0.4.0` | No ADR (docs/adoption work under the existing website and package-topology decisions) | +| **US-081** | Expand `/docs` into a multi-page docs section: shared `DocsShell` (sidebar + TOC + pager), Flutter\|Jaspr `CodeTabs`, nine core pages | No ADR (unified single docs tree with per-snippet adapter tabs, under the existing website decisions) | + +## Validation Ladder + +- Static-site proof: `tool/styles.sh --minify` plus + `dart pub global run jaspr_cli:jaspr build` generate both the `/` and `/docs` + routes, and `website/build/jaspr/docs/index.html` is present with the + per-route `` and relative asset URLs. +- Analyzer/format proof: `fvm dart analyze` and `fvm dart format` stay clean for + the `website` package. +- Deploy proof: the deploy workflow rewrites `<base href>` on every generated + HTML file and verifies both `index.html` and `docs/index.html`. diff --git a/pubspec.lock b/pubspec.lock index dec55b3..92a7d6e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -371,6 +371,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.23.1" + jaspr_router: + dependency: transitive + description: + name: jaspr_router + sha256: "43abdfd63c19cae8faac50334c7f3065635bac3e85fd558b6076517978ade64a" + url: "https://pub.dev" + source: hosted + version: "0.8.2" jaspr_test: dependency: transitive description: diff --git a/website/README.md b/website/README.md index 037f6c8..ff91aa8 100644 --- a/website/README.md +++ b/website/README.md @@ -60,4 +60,6 @@ static host (GitHub Pages, Netlify, Cloudflare Pages, …). - GitHub: https://github.com/iamv4g/dnd_kit - pub.dev: https://pub.dev/packages/dnd_kit_jaspr -- Docs: _coming soon_ (currently a `#docs` placeholder in the nav/footer) +- Docs: the multi-page documentation section under the `/docs` routes + (`lib/docs/`), linked from the nav and footer. `lib/docs/docs_nav.dart` is the + single source of truth for the sidebar, pager, and routes. diff --git a/website/lib/app.dart b/website/lib/app.dart new file mode 100644 index 0000000..2a4b296 --- /dev/null +++ b/website/lib/app.dart @@ -0,0 +1,74 @@ +import 'package:jaspr/jaspr.dart'; +import 'package:jaspr_router/jaspr_router.dart'; + +import 'docs/pages/accessibility_page.dart'; +import 'docs/pages/draggable_page.dart'; +import 'docs/pages/droppable_page.dart'; +import 'docs/pages/install_page.dart'; +import 'docs/pages/multi_container_page.dart'; +import 'docs/pages/overlay_page.dart'; +import 'docs/pages/overview_page.dart'; +import 'docs/pages/quickstart_page.dart'; +import 'docs/pages/sortable_page.dart'; +import 'site.dart'; + +/// Top-level routing. In static (SSG) mode jaspr generates one HTML file per +/// route, so this emits `index.html` (the marketing home) plus a page under +/// `docs/` for each documentation route. +class App extends StatelessComponent { + const App({super.key}); + + @override + Component build(BuildContext context) { + return Router( + routes: [ + Route(path: '/', builder: (context, state) => const Site()), + Route( + path: '/docs', + title: 'Documentation · dnd_kit', + builder: (context, state) => const OverviewPage(), + ), + Route( + path: '/docs/install', + title: 'Installation · dnd_kit', + builder: (context, state) => const InstallPage(), + ), + Route( + path: '/docs/quickstart', + title: 'Quickstart · dnd_kit', + builder: (context, state) => const QuickstartPage(), + ), + Route( + path: '/docs/draggable', + title: 'Draggable · dnd_kit', + builder: (context, state) => const DraggablePage(), + ), + Route( + path: '/docs/droppable', + title: 'Droppable · dnd_kit', + builder: (context, state) => const DroppablePage(), + ), + Route( + path: '/docs/overlay', + title: 'Drag overlay · dnd_kit', + builder: (context, state) => const OverlayPage(), + ), + Route( + path: '/docs/sortable', + title: 'Sortable lists · dnd_kit', + builder: (context, state) => const SortablePage(), + ), + Route( + path: '/docs/multi-container', + title: 'Multi-container sortable · dnd_kit', + builder: (context, state) => const MultiContainerPage(), + ), + Route( + path: '/docs/accessibility', + title: 'Accessibility · dnd_kit', + builder: (context, state) => const AccessibilityPage(), + ), + ], + ); + } +} diff --git a/website/lib/data/site_data.dart b/website/lib/data/site_data.dart index 8bdac38..ba22082 100644 --- a/website/lib/data/site_data.dart +++ b/website/lib/data/site_data.dart @@ -11,8 +11,10 @@ class SiteLinks { static const pubFlutter = 'https://pub.dev/packages/dnd_kit_flutter'; static const pubJaspr = 'https://pub.dev/packages/dnd_kit_jaspr'; - /// Docs site is built later; placeholder for now. - static const docs = '#docs'; + /// The on-site Getting Started docs page (`/docs` route). Written relative so + /// it resolves against the document `<base href>` on both the project Pages + /// subpath (`/dnd_kit/docs/`) and a local server root (`/docs/`). + static const docs = 'docs/'; } /// In-page nav targets (also the reorderable nav pills). diff --git a/website/lib/docs/code_tabs.dart b/website/lib/docs/code_tabs.dart new file mode 100644 index 0000000..177a279 --- /dev/null +++ b/website/lib/docs/code_tabs.dart @@ -0,0 +1,75 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; + +/// A code block with a Flutter / Jaspr toggle, so one snippet shows the same +/// API on both adapters. Server-rendered with the Flutter tab active, then +/// hydrated to switch tabs on the client. +@client +class CodeTabs extends StatefulComponent { + const CodeTabs({ + required this.flutter, + required this.jaspr, + this.flutterFile = 'main.dart', + this.jasprFile = 'main.dart', + super.key, + }); + + final String flutter; + final String jaspr; + final String flutterFile; + final String jasprFile; + + @override + State<CodeTabs> createState() => _CodeTabsState(); +} + +class _CodeTabsState extends State<CodeTabs> { + int _tab = 0; // 0 = Flutter, 1 = Jaspr + + @override + Component build(BuildContext context) { + final tabs = ['Flutter', 'Jaspr']; + final code = _tab == 0 ? component.flutter : component.jaspr; + final file = _tab == 0 ? component.flutterFile : component.jasprFile; + return div( + classes: + 'overflow-hidden rounded-2xl border border-line bg-surface shadow-lift', + [ + div( + classes: + 'flex items-center gap-3 border-b border-line bg-raised px-4 py-2.5', + [ + div( + classes: 'flex items-center gap-1', + attributes: const {'role': 'tablist'}, + [ + for (var i = 0; i < tabs.length; i++) + button( + classes: + 'rounded-full px-3 py-1 font-mono text-xs transition-colors ' + '${i == _tab ? 'bg-accent text-white' : 'text-muted hover:text-ink'}', + attributes: { + 'type': 'button', + 'role': 'tab', + 'aria-selected': (i == _tab).toString(), + }, + onClick: () => setState(() => _tab = i), + [.text(tabs[i])], + ), + ], + ), + span(classes: 'ml-auto font-mono text-xs text-muted', [ + .text(file), + ]), + ], + ), + Component.element( + tag: 'pre', + classes: + 'overflow-x-auto p-5 font-mono text-sm leading-relaxed text-ink', + children: [.text(code)], + ), + ], + ); + } +} diff --git a/website/lib/docs/doc_components.dart b/website/lib/docs/doc_components.dart new file mode 100644 index 0000000..281e195 --- /dev/null +++ b/website/lib/docs/doc_components.dart @@ -0,0 +1,142 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; + +/// Shared building blocks for documentation pages: the lead paragraph, the +/// "You'll learn" callout, section wrappers, prose, bullet lists, inline code, +/// and the "Next steps" card grid. + +const _prose = 'max-w-3xl leading-relaxed text-muted'; + +/// The one-sentence summary directly under a page heading. +Component docLead(String text) { + return p(classes: 'max-w-3xl text-lg leading-relaxed text-muted', [ + .text(text), + ]); +} + +/// A short outcomes callout shown near the top of a page. +Component youWillLearn(List<String> items) { + return div( + classes: 'max-w-3xl rounded-2xl border border-line bg-surface p-5', + [ + span( + classes: 'font-mono text-xs uppercase tracking-[0.18em] text-accent', + const [.text("You'll learn")], + ), + ul(classes: 'mt-3 flex flex-col gap-1.5 text-muted', [ + for (final item in items) + li(classes: 'flex gap-2', [ + span(classes: 'text-accent', const [.text('—')]), + span([.text(item)]), + ]), + ]), + ], + ); +} + +/// A page section with an anchored `<h2>` heading. +Component docSection({ + required String id, + required String title, + required List<Component> children, +}) { + return section(id: id, classes: 'scroll-mt-24', [ + h2(classes: 'font-serif text-2xl text-ink sm:text-3xl', [.text(title)]), + div(classes: 'mt-4 flex flex-col gap-4', children), + ]); +} + +/// A prose paragraph. [spans] lets a paragraph mix text with inline code. +Component docProse(String text) => p(classes: _prose, [.text(text)]); + +/// A prose paragraph built from mixed inline content (text + inlineCode). +Component docProseRich(List<Component> spans) => p(classes: _prose, spans); + +/// Plain inline text node, for composing with [inlineCode] in [docProseRich]. +Component docText(String text) => Component.text(text); + +/// Inline `code` styling. +Component inlineCode(String text) { + return Component.element( + tag: 'code', + classes: 'rounded bg-raised px-1.5 py-0.5 font-mono text-[0.85em] text-ink', + children: [.text(text)], + ); +} + +/// A plain (non-tabbed) code block with a filename label. +Component docCodeBlock(String filename, String code) { + return div( + classes: + 'overflow-hidden rounded-2xl border border-line bg-surface shadow-lift', + [ + div( + classes: + 'flex items-center gap-3 border-b border-line bg-raised px-4 py-2.5', + [ + div(classes: 'flex items-center gap-2', [ + span(classes: 'h-2.5 w-2.5 rounded-full bg-accent/70', const []), + span(classes: 'h-2.5 w-2.5 rounded-full bg-muted/40', const []), + span(classes: 'h-2.5 w-2.5 rounded-full bg-muted/40', const []), + ]), + span(classes: 'ml-auto font-mono text-xs text-muted', [ + .text(filename), + ]), + ], + ), + Component.element( + tag: 'pre', + classes: + 'overflow-x-auto p-5 font-mono text-sm leading-relaxed text-ink', + children: [.text(code)], + ), + ], + ); +} + +/// An unordered list of prose items. +Component docBullets(List<String> items) { + return ul(classes: 'flex max-w-3xl flex-col gap-2 text-muted', [ + for (final item in items) + li(classes: 'flex gap-2', [ + span(classes: 'mt-0.5 text-accent', const [.text('•')]), + span([.text(item)]), + ]), + ]); +} + +/// A "Next steps" card linking to another doc page or external resource. +class NextStep { + const NextStep({ + required this.label, + required this.desc, + required this.href, + this.external = false, + }); + + final String label; + final String desc; + final String href; + final bool external; +} + +/// The "Next steps" card grid shown at the bottom of a page body. +Component nextSteps(List<NextStep> steps) { + return div(classes: 'mt-2 grid gap-3 sm:grid-cols-2', [ + for (final step in steps) + a( + href: step.href, + target: step.external ? Target.blank : null, + attributes: step.external ? const {'rel': 'noreferrer'} : null, + classes: + 'flex flex-col gap-1 rounded-2xl border border-line bg-surface p-4 ' + 'transition-colors hover:border-accent', + [ + span(classes: 'font-semibold text-ink', [ + .text(step.external ? '${step.label} ↗' : step.label), + ]), + span(classes: 'text-sm text-muted', [.text(step.desc)]), + ], + ), + ]); +} diff --git a/website/lib/docs/docs_nav.dart b/website/lib/docs/docs_nav.dart new file mode 100644 index 0000000..1760c19 --- /dev/null +++ b/website/lib/docs/docs_nav.dart @@ -0,0 +1,143 @@ +/// The documentation information architecture: grouped sidebar entries plus a +/// flat ordered list that drives the previous/next pager. This is the single +/// source of truth for the docs section — pages, routes, and the sidebar all +/// read from it. +library; + +/// One documentation page. +class DocEntry { + const DocEntry({ + required this.slug, + required this.navLabel, + required this.title, + required this.group, + }); + + /// URL slug under `/docs`. Empty for the docs landing (`/docs`). + final String slug; + + /// Short label shown in the sidebar. + final String navLabel; + + /// Page heading (`<h1>`). + final String title; + + /// Group label, shown as the eyebrow above the heading. + final String group; + + /// jaspr_router path for this page (e.g. `/docs` or `/docs/draggable`). + String get routePath => slug.isEmpty ? '/docs' : '/docs/$slug'; + + /// Link target, relative to the document `<base href>`. Trailing slash keeps + /// same-page anchors on the generated `index.html` instead of bouncing + /// through a Pages redirect. + String get href => slug.isEmpty ? 'docs/' : 'docs/$slug/'; + + /// A same-page anchor link to a section [id] on this page. + String anchor(String id) => '$href#$id'; +} + +/// A labelled group of sidebar entries. +class DocGroup { + const DocGroup({required this.label, required this.entries}); + + final String label; + final List<DocEntry> entries; +} + +const docGroups = <DocGroup>[ + DocGroup( + label: 'Get started', + entries: [ + DocEntry( + slug: '', + navLabel: 'Overview', + title: 'dnd_kit documentation', + group: 'Get started', + ), + DocEntry( + slug: 'install', + navLabel: 'Installation', + title: 'Installation', + group: 'Get started', + ), + DocEntry( + slug: 'quickstart', + navLabel: 'Quickstart', + title: 'Quickstart', + group: 'Get started', + ), + ], + ), + DocGroup( + label: 'Concepts', + entries: [ + DocEntry( + slug: 'draggable', + navLabel: 'Draggable', + title: 'Draggable', + group: 'Concepts', + ), + DocEntry( + slug: 'droppable', + navLabel: 'Droppable', + title: 'Droppable', + group: 'Concepts', + ), + DocEntry( + slug: 'overlay', + navLabel: 'Drag overlay', + title: 'Drag overlay', + group: 'Concepts', + ), + ], + ), + DocGroup( + label: 'Sortable', + entries: [ + DocEntry( + slug: 'sortable', + navLabel: 'Sortable lists', + title: 'Sortable lists', + group: 'Sortable', + ), + DocEntry( + slug: 'multi-container', + navLabel: 'Multi-container', + title: 'Multi-container sortable', + group: 'Sortable', + ), + ], + ), + DocGroup( + label: 'Accessibility', + entries: [ + DocEntry( + slug: 'accessibility', + navLabel: 'Accessibility', + title: 'Accessibility', + group: 'Accessibility', + ), + ], + ), +]; + +/// All entries in sidebar order — drives the previous/next pager. +final List<DocEntry> docOrder = [ + for (final group in docGroups) ...group.entries, +]; + +/// The entry immediately before [slug] in reading order, or null. +DocEntry? docPrev(String slug) { + final i = docOrder.indexWhere((e) => e.slug == slug); + return i > 0 ? docOrder[i - 1] : null; +} + +/// The entry immediately after [slug] in reading order, or null. +DocEntry? docNext(String slug) { + final i = docOrder.indexWhere((e) => e.slug == slug); + return i >= 0 && i < docOrder.length - 1 ? docOrder[i + 1] : null; +} + +/// The base-relative link for a page [slug] (for cross-page references). +String docHref(String slug) => docOrder.firstWhere((e) => e.slug == slug).href; diff --git a/website/lib/docs/docs_shell.dart b/website/lib/docs/docs_shell.dart new file mode 100644 index 0000000..7ea4414 --- /dev/null +++ b/website/lib/docs/docs_shell.dart @@ -0,0 +1,162 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; + +import '../components/ui.dart'; +import '../layout/footer.dart'; +import '../layout/nav_bar.dart'; +import 'docs_nav.dart'; + +/// The shared documentation chrome: top nav, a grouped left sidebar, the page +/// body, a right-rail "On this page" table of contents, and a previous/next +/// pager. Every docs page wraps its content in this shell. +class DocsShell extends StatelessComponent { + const DocsShell({ + required this.slug, + required this.toc, + required this.body, + super.key, + }); + + /// The current page's slug (matches a [DocEntry.slug]). + final String slug; + + /// Right-rail anchors for the sections on this page. + final List<({String id, String label})> toc; + + /// The page body — lead, callout, and sections. + final List<Component> body; + + DocEntry get _entry => docOrder.firstWhere((e) => e.slug == slug); + + @override + Component build(BuildContext context) { + final entry = _entry; + return .fragment([ + div(id: 'top', const []), + const NavBar(activeDocs: true), + Component.element( + tag: 'main', + children: [ + div( + classes: + 'mx-auto max-w-7xl gap-10 px-6 py-10 ' + 'lg:grid lg:grid-cols-[14rem_minmax(0,1fr)] ' + 'xl:grid-cols-[14rem_minmax(0,1fr)_13rem]', + [_sidebar(), _content(entry), _tocRail()], + ), + ], + ), + const Footer(), + ]); + } + + Component _sidebar() { + return Component.element( + tag: 'aside', + classes: 'hidden lg:block', + children: [ + nav( + classes: 'sticky top-24 flex flex-col gap-6', + attributes: const {'aria-label': 'Documentation'}, + [ + for (final group in docGroups) + div(classes: 'flex flex-col gap-1', [ + span( + classes: + 'mb-1 font-mono text-xs uppercase tracking-[0.18em] ' + 'text-muted', + [.text(group.label)], + ), + for (final entry in group.entries) _sidebarLink(entry), + ]), + ], + ), + ], + ); + } + + Component _sidebarLink(DocEntry entry) { + final active = entry.slug == slug; + return a( + href: entry.href, + attributes: active ? const {'aria-current': 'page'} : null, + classes: + 'rounded-lg px-3 py-1.5 text-sm transition-colors ' + '${active ? 'bg-surface font-medium text-accent' : 'text-muted hover:bg-surface hover:text-ink'}', + [.text(entry.navLabel)], + ); + } + + Component _content(DocEntry entry) { + return div(classes: 'min-w-0', [ + eyebrow(entry.group), + h1(classes: 'mt-3 font-serif text-4xl text-ink sm:text-5xl', [ + .text(entry.title), + ]), + div(classes: 'mt-6 flex flex-col gap-10', body), + _pager(), + ]); + } + + Component _tocRail() { + if (toc.isEmpty) return Component.element(tag: 'div', children: const []); + return Component.element( + tag: 'aside', + classes: 'hidden xl:block', + children: [ + nav( + classes: 'sticky top-24 flex flex-col gap-2', + attributes: const {'aria-label': 'On this page'}, + [ + span( + classes: + 'font-mono text-xs uppercase tracking-[0.18em] text-muted', + const [.text('On this page')], + ), + for (final item in toc) + a( + href: _entry.anchor(item.id), + classes: 'text-sm text-muted transition-colors hover:text-ink', + [.text(item.label)], + ), + ], + ), + ], + ); + } + + Component _pager() { + final prev = docPrev(slug); + final next = docNext(slug); + if (prev == null && next == null) { + return Component.element(tag: 'div', children: const []); + } + return div( + classes: + 'mt-12 flex items-stretch justify-between gap-4 border-t ' + 'border-line pt-6', + [ + if (prev != null) _pagerLink(prev, next: false) else div(const []), + if (next != null) _pagerLink(next, next: true) else div(const []), + ], + ); + } + + Component _pagerLink(DocEntry entry, {required bool next}) { + return a( + href: entry.href, + classes: + 'flex flex-col gap-0.5 rounded-2xl border border-line bg-surface ' + 'px-5 py-3 transition-colors hover:border-accent ' + '${next ? 'items-end text-right' : 'items-start'}', + [ + span(classes: 'font-mono text-xs text-muted', [ + .text(next ? 'Next' : 'Previous'), + ]), + span(classes: 'font-medium text-ink', [ + .text(next ? '${entry.navLabel} →' : '← ${entry.navLabel}'), + ]), + ], + ); + } +} diff --git a/website/lib/docs/pages/accessibility_page.dart b/website/lib/docs/pages/accessibility_page.dart new file mode 100644 index 0000000..19f664e --- /dev/null +++ b/website/lib/docs/pages/accessibility_page.dart @@ -0,0 +1,115 @@ +import 'package:jaspr/jaspr.dart'; + +import '../code_tabs.dart'; +import '../doc_components.dart'; +import '../docs_nav.dart'; +import '../docs_shell.dart'; + +/// `/docs/accessibility` +class AccessibilityPage extends StatelessComponent { + const AccessibilityPage({super.key}); + + @override + Component build(BuildContext context) { + return DocsShell( + slug: 'accessibility', + toc: const [ + (id: 'keyboard', label: 'Keyboard'), + (id: 'announcements', label: 'Announcements'), + (id: 'handles', label: 'Handles & labels'), + ], + body: [ + docLead( + 'Accessibility is built in, not bolted on. Every draggable is ' + 'operable from the keyboard, and a live region narrates the drag so ' + 'screen-reader users follow the same interaction as everyone else.', + ), + youWillLearn(const [ + 'How keyboard dragging works out of the box.', + 'How drag progress is announced.', + 'How to label drag handles for assistive tech.', + ]), + docSection( + id: 'keyboard', + title: 'Keyboard', + children: [ + docProse( + 'Draggables are focusable and operable without a pointer. The ' + 'keyboard sensor is active by default:', + ), + docBullets(const [ + 'Space or Enter — pick up the focused item, and drop it.', + 'Arrow keys — move the lifted item between positions or targets.', + 'Escape — cancel the drag and return the item to its origin.', + ]), + ], + ), + docSection( + id: 'announcements', + title: 'Announcements', + children: [ + docProseRich([ + docText( + 'Drag progress is announced through a polite live region. On ' + 'Jaspr, mount a ', + ), + inlineCode('DndLiveRegion'), + docText( + ' inside the scope; on Flutter, the adapter emits semantics ' + 'announcements. Both speak the same shared ', + ), + inlineCode('DndAnnouncements'), + docText(' contract — pick up, move, drop, and cancel.'), + ]), + const CodeTabs( + flutterFile: 'a11y.dart', + jasprFile: 'a11y.dart', + flutter: _flutter, + jaspr: _jaspr, + ), + ], + ), + docSection( + id: 'handles', + title: 'Handles & labels', + children: [ + docProseRich([ + docText('Give every '), + inlineCode('DndDragHandle'), + docText(' a descriptive '), + inlineCode('label'), + docText( + ' so assistive technology announces what it moves — for ' + 'example "Reorder card: Buy milk" rather than an unlabeled grip.', + ), + ]), + ], + ), + nextSteps([ + NextStep( + label: 'Sortable lists', + desc: 'Keyboard-accessible reordering by default.', + href: docHref('sortable'), + ), + NextStep( + label: 'Overview', + desc: 'Back to the documentation home.', + href: docHref(''), + ), + ]), + ], + ); + } +} + +const _flutter = '''DndScope( + // Flutter: announcements are emitted as semantics events automatically. + child: board, +)'''; + +const _jaspr = '''DndScope( + child: div([ + board, + const DndLiveRegion(), // polite live region for screen readers + ]), +)'''; diff --git a/website/lib/docs/pages/draggable_page.dart b/website/lib/docs/pages/draggable_page.dart new file mode 100644 index 0000000..2fb6aa4 --- /dev/null +++ b/website/lib/docs/pages/draggable_page.dart @@ -0,0 +1,172 @@ +import 'package:jaspr/jaspr.dart'; + +import '../code_tabs.dart'; +import '../doc_components.dart'; +import '../docs_nav.dart'; +import '../docs_shell.dart'; + +/// `/docs/draggable` +class DraggablePage extends StatelessComponent { + const DraggablePage({super.key}); + + @override + Component build(BuildContext context) { + return DocsShell( + slug: 'draggable', + toc: const [ + (id: 'usage', label: 'Usage'), + (id: 'handle', label: 'Drag handles'), + (id: 'activation', label: 'Activation'), + (id: 'events', label: 'Drag events'), + ], + body: [ + docLead( + 'A draggable is any element that can be picked up and moved. Wrap it ' + 'in DndDraggable, give it a stable DndId, and the engine handles the ' + 'pointer, keyboard, and transform.', + ), + youWillLearn(const [ + 'How to make an element draggable.', + 'How to restrict the drag surface to a handle.', + 'How to require intent before a drag begins.', + 'Which events fire over a drag lifecycle.', + ]), + docSection( + id: 'usage', + title: 'Usage', + children: [ + docProseRich([ + docText('Every draggable needs a unique '), + inlineCode('DndId'), + docText( + ' within its scope. The id is how collision results and drag ' + 'events refer back to it.', + ), + ]), + const CodeTabs( + flutterFile: 'draggable.dart', + jasprFile: 'draggable.dart', + flutter: _usageFlutter, + jaspr: _usageJaspr, + ), + ], + ), + docSection( + id: 'handle', + title: 'Drag handles', + children: [ + docProseRich([ + docText('Wrap part of the child in a '), + inlineCode('DndDragHandle'), + docText( + ' to make only that part the drag surface — useful when the ' + 'rest of the element stays interactive (links, buttons, text ' + 'selection).', + ), + ]), + const CodeTabs( + flutterFile: 'handle.dart', + jasprFile: 'handle.dart', + flutter: _handleFlutter, + jaspr: _handleJaspr, + ), + ], + ), + docSection( + id: 'activation', + title: 'Activation', + children: [ + docProseRich([ + docText('Pass a '), + inlineCode('DndSensorActivationConstraint'), + docText( + ' so a drag only starts after deliberate intent — a small ' + 'distance or a press delay — instead of on the first pixel of ' + 'movement. This keeps taps and clicks responsive.', + ), + ]), + const CodeTabs( + flutterFile: 'activation.dart', + jasprFile: 'activation.dart', + flutter: _activationFlutter, + jaspr: _activationJaspr, + ), + ], + ), + docSection( + id: 'events', + title: 'Drag events', + children: [ + docProse('A draggable reports its lifecycle through callbacks:'), + docBullets(const [ + 'onDragStart — the drag was activated and picked up.', + 'onDragUpdate — the pointer moved; the active transform changed.', + 'onDragEnd — the drag finished; read event.overId for the target ' + 'it landed on, or handle a cancel.', + ]), + ], + ), + nextSteps([ + NextStep( + label: 'Droppable', + desc: 'Define the targets a draggable can land on.', + href: docHref('droppable'), + ), + NextStep( + label: 'Drag overlay', + desc: 'Render a floating preview that follows the pointer.', + href: docHref('overlay'), + ), + ]), + ], + ); + } +} + +const _usageFlutter = '''DndDraggable( + id: const DndId('card-1'), + data: card, // optional payload carried through the drag + child: CardTile(card), +)'''; + +const _usageJaspr = '''DndDraggable( + id: const DndId('card-1'), + data: card, // optional payload carried through the drag + child: cardTile(card), +)'''; + +const _handleFlutter = '''DndDraggable( + id: const DndId('card-1'), + child: Row( + children: [ + DndDragHandle( + label: 'Reorder card', + child: const Icon(Icons.drag_indicator), + ), + const Expanded(child: Text('Card title')), + ], + ), +)'''; + +const _handleJaspr = '''DndDraggable( + id: const DndId('card-1'), + child: div([ + DndDragHandle( + label: 'Reorder card', + child: span([.text('⠿')]), + ), + span([.text('Card title')]), + ]), +)'''; + +const _activationFlutter = '''DndDraggable( + id: const DndId('card-1'), + constraint: const DndSensorActivationConstraint(distance: 6), + child: CardTile(card), +)'''; + +const _activationJaspr = '''DndDraggable( + id: const DndId('card-1'), + constraint: const DndSensorActivationConstraint(distance: 6), + child: cardTile(card), +)'''; diff --git a/website/lib/docs/pages/droppable_page.dart b/website/lib/docs/pages/droppable_page.dart new file mode 100644 index 0000000..4066757 --- /dev/null +++ b/website/lib/docs/pages/droppable_page.dart @@ -0,0 +1,133 @@ +import 'package:jaspr/jaspr.dart'; + +import '../code_tabs.dart'; +import '../doc_components.dart'; +import '../docs_nav.dart'; +import '../docs_shell.dart'; + +/// `/docs/droppable` +class DroppablePage extends StatelessComponent { + const DroppablePage({super.key}); + + @override + Component build(BuildContext context) { + return DocsShell( + slug: 'droppable', + toc: const [ + (id: 'usage', label: 'Usage'), + (id: 'collision', label: 'Collision detection'), + (id: 'feedback', label: 'Hover feedback'), + ], + body: [ + docLead( + 'A droppable is a target a draggable can land on. Mark an area with ' + 'DndDroppable and a unique id; the engine reports when a draggable ' + 'is over it and which target a drag ends on.', + ), + youWillLearn(const [ + 'How to define a drop target.', + 'How the engine decides which target is under the pointer.', + 'How to give visual feedback while a draggable hovers.', + ]), + docSection( + id: 'usage', + title: 'Usage', + children: [ + docProseRich([ + docText('Give each droppable a unique '), + inlineCode('DndId'), + docText('. On the draggable, '), + inlineCode('onDragEnd'), + docText(' reports the target id through '), + inlineCode('event.overId'), + docText('.'), + ]), + const CodeTabs( + flutterFile: 'droppable.dart', + jasprFile: 'droppable.dart', + flutter: _usageFlutter, + jaspr: _usageJaspr, + ), + ], + ), + docSection( + id: 'collision', + title: 'Collision detection', + children: [ + docProse( + 'When a draggable overlaps several droppables, a collision ' + 'strategy decides which one wins. The strategy is shared math in ' + 'the engine, so Flutter and Jaspr resolve the same target:', + ), + docBullets(const [ + 'Closest center — the target whose center is nearest the ' + 'pointer. A solid default for grids and free layouts.', + 'Largest overlap — the target the dragged element overlaps most. ' + 'Natural for list and Kanban reordering.', + ]), + ], + ), + docSection( + id: 'feedback', + title: 'Hover feedback', + children: [ + docProse( + 'Read whether a target is currently being hovered to highlight ' + 'it — the drop zones on this site light up exactly this way.', + ), + const CodeTabs( + flutterFile: 'feedback.dart', + jasprFile: 'feedback.dart', + flutter: _feedbackFlutter, + jaspr: _feedbackJaspr, + ), + ], + ), + nextSteps([ + NextStep( + label: 'Drag overlay', + desc: 'Render a floating preview that follows the pointer.', + href: docHref('overlay'), + ), + NextStep( + label: 'Sortable lists', + desc: 'Reorder a list without wiring targets by hand.', + href: docHref('sortable'), + ), + ]), + ], + ); + } +} + +const _usageFlutter = '''DndDroppable( + id: const DndId('inbox'), + child: const SizedBox(width: 240, height: 160, child: Text('Inbox')), +)'''; + +const _usageJaspr = '''DndDroppable( + id: const DndId('inbox'), + child: div([.text('Inbox')]), +)'''; + +const _feedbackFlutter = '''DndDroppable( + id: const DndId('inbox'), + builder: (context, state, child) => DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + color: state.isOver ? Colors.orange : Colors.grey, + ), + ), + child: child, + ), + child: const Text('Inbox'), +)'''; + +const _feedbackJaspr = '''DndDroppable( + id: const DndId('inbox'), + builder: (context, state, child) => div( + classes: state.isOver ? 'border-accent' : 'border-line', + [child], + ), + child: div([.text('Inbox')]), +)'''; diff --git a/website/lib/docs/pages/install_page.dart b/website/lib/docs/pages/install_page.dart new file mode 100644 index 0000000..6b0bb77 --- /dev/null +++ b/website/lib/docs/pages/install_page.dart @@ -0,0 +1,85 @@ +import 'package:jaspr/jaspr.dart'; + +import '../doc_components.dart'; +import '../docs_nav.dart'; +import '../docs_shell.dart'; + +/// `/docs/install` +class InstallPage extends StatelessComponent { + const InstallPage({super.key}); + + @override + Component build(BuildContext context) { + return DocsShell( + slug: 'install', + toc: const [ + (id: 'flutter', label: 'Flutter'), + (id: 'jaspr', label: 'Jaspr (web)'), + (id: 'engine', label: 'Shared engine'), + ], + body: [ + docLead( + 'Add one package for your platform. Each adapter depends on the ' + 'shared engine for you — you only depend on dnd_kit directly when ' + 'building a custom adapter or testing the drag math.', + ), + youWillLearn(const [ + 'Which package to add for Flutter, Jaspr, or a custom adapter.', + 'The current stable version line of the family.', + ]), + docSection( + id: 'flutter', + title: 'Flutter', + children: [ + docProseRich([ + docText('Add '), + inlineCode('dnd_kit_flutter'), + docText(' to a Flutter app:'), + ]), + docCodeBlock( + 'pubspec.yaml', + 'dependencies:\n dnd_kit_flutter: ^0.4.0', + ), + ], + ), + docSection( + id: 'jaspr', + title: 'Jaspr (web)', + children: [ + docProseRich([ + docText('Add '), + inlineCode('dnd_kit_jaspr'), + docText(' to a Jaspr app. It needs no Flutter SDK:'), + ]), + docCodeBlock( + 'pubspec.yaml', + 'dependencies:\n dnd_kit_jaspr: ^0.4.0', + ), + ], + ), + docSection( + id: 'engine', + title: 'Shared engine', + children: [ + docProseRich([ + docText('Depend on '), + inlineCode('dnd_kit'), + docText( + ' directly only for custom adapters or contract tests — the ' + 'adapters already bundle it:', + ), + ]), + docCodeBlock('pubspec.yaml', 'dependencies:\n dnd_kit: ^0.4.0'), + ], + ), + nextSteps([ + NextStep( + label: 'Quickstart', + desc: 'Wire your first drag-and-drop in three steps.', + href: docHref('quickstart'), + ), + ]), + ], + ); + } +} diff --git a/website/lib/docs/pages/multi_container_page.dart b/website/lib/docs/pages/multi_container_page.dart new file mode 100644 index 0000000..6f3b8a3 --- /dev/null +++ b/website/lib/docs/pages/multi_container_page.dart @@ -0,0 +1,141 @@ +import 'package:jaspr/jaspr.dart'; + +import '../code_tabs.dart'; +import '../doc_components.dart'; +import '../docs_nav.dart'; +import '../docs_shell.dart'; + +/// `/docs/multi-container` +class MultiContainerPage extends StatelessComponent { + const MultiContainerPage({super.key}); + + @override + Component build(BuildContext context) { + return DocsShell( + slug: 'multi-container', + toc: const [ + (id: 'anatomy', label: 'Anatomy'), + (id: 'usage', label: 'Usage'), + (id: 'state', label: 'Applying moves'), + ], + body: [ + docLead( + 'Multi-container sortable moves items within and across lists — the ' + 'shape of a Kanban board. It is a supported preset over the shared ' + 'engine, so the same code drives Flutter and the web.', + ), + youWillLearn(const [ + 'The three components that make a multi-list board.', + 'How to wire columns and cards.', + 'How to apply a cross-list move to your own state.', + ]), + docSection( + id: 'anatomy', + title: 'Anatomy', + children: [ + docBullets(const [ + 'SortableMultiScope — owns the whole board and the move policy ' + 'across all columns.', + 'SortableMultiContainerArea — one column; a drop region that ' + 'holds an ordered list of items.', + 'SortableMultiItem — one card; draggable within and between ' + 'columns.', + ]), + ], + ), + docSection( + id: 'usage', + title: 'Usage', + children: [ + docProse( + 'Nest the three components and feed them your board model — a map ' + 'of column id to the ordered card ids in that column.', + ), + const CodeTabs( + flutterFile: 'board.dart', + jasprFile: 'board.dart', + flutter: _usageFlutter, + jaspr: _usageJaspr, + ), + ], + ), + docSection( + id: 'state', + title: 'Applying moves', + children: [ + docProse( + 'A move reports the source and target column plus the indices. ' + 'You remove the card from its old column and insert it into the ' + 'new one — the board owns its data, the engine only resolves ' + 'intent. The Kanban showcase on the home page is built on exactly ' + 'this preset.', + ), + const CodeTabs( + flutterFile: 'on_move.dart', + jasprFile: 'on_move.dart', + flutter: _moveCode, + jaspr: _moveCode, + ), + ], + ), + nextSteps([ + NextStep( + label: 'Sortable lists', + desc: 'The single-list preset this builds on.', + href: docHref('sortable'), + ), + NextStep( + label: 'Accessibility', + desc: 'Keyboard moves across columns, announced.', + href: docHref('accessibility'), + ), + ]), + ], + ); + } +} + +const _usageFlutter = '''SortableMultiScope( + controller: controller, + columnIds: columns, // List<DndId> + onMove: _onMove, + child: Row( + children: [ + for (final columnId in columns) + SortableMultiContainerArea( + id: columnId, + itemIds: board[columnId]!, + child: Column( + children: [ + for (final cardId in board[columnId]!) + SortableMultiItem(id: cardId, child: CardTile(cardId)), + ], + ), + ), + ], + ), +)'''; + +const _usageJaspr = '''SortableMultiScope( + controller: controller, + columnIds: columns, // List<DndId> + onMove: _onMove, + child: div([ + for (final columnId in columns) + SortableMultiContainerArea( + id: columnId, + itemIds: board[columnId]!, + child: div([ + for (final cardId in board[columnId]!) + SortableMultiItem(id: cardId, child: cardTile(cardId)), + ]), + ), + ]), +)'''; + +const _moveCode = '''void _onMove(SortableMultiMoveDetails details) { + setState(() { + final card = board[details.fromColumn]!.removeAt(details.fromIndex); + board[details.toColumn]!.insert(details.toIndex, card); + }); +}'''; diff --git a/website/lib/docs/pages/overlay_page.dart b/website/lib/docs/pages/overlay_page.dart new file mode 100644 index 0000000..6b0dc4f --- /dev/null +++ b/website/lib/docs/pages/overlay_page.dart @@ -0,0 +1,98 @@ +import 'package:jaspr/jaspr.dart'; + +import '../code_tabs.dart'; +import '../doc_components.dart'; +import '../docs_nav.dart'; +import '../docs_shell.dart'; + +/// `/docs/overlay` +class OverlayPage extends StatelessComponent { + const OverlayPage({super.key}); + + @override + Component build(BuildContext context) { + return DocsShell( + slug: 'overlay', + toc: const [ + (id: 'why', label: 'Why an overlay'), + (id: 'usage', label: 'Usage'), + ], + body: [ + docLead( + 'A drag overlay is a floating preview that follows the pointer while ' + 'the original element keeps its place in the layout. It avoids ' + 'clipping and reflow during a drag.', + ), + youWillLearn(const [ + 'When a drag overlay helps over moving the element in place.', + 'How to render an overlay for the active draggable.', + ]), + docSection( + id: 'why', + title: 'Why an overlay', + children: [ + docProse( + 'Moving the element itself works for simple cases, but a dragged ' + 'element inside a scrollable or clipped container can be cut off, ' + 'and reordering its siblings can cause layout jumps. An overlay ' + 'renders the drag preview in a top layer instead, so it stays ' + 'crisp above everything and never reflows the list underneath.', + ), + ], + ), + docSection( + id: 'usage', + title: 'Usage', + children: [ + docProseRich([ + docText('Place a '), + inlineCode('DndDragOverlay'), + docText( + ' inside the scope. It renders its builder only while a drag is ' + 'active, positioned to follow the pointer.', + ), + ]), + const CodeTabs( + flutterFile: 'overlay.dart', + jasprFile: 'overlay.dart', + flutter: _usageFlutter, + jaspr: _usageJaspr, + ), + ], + ), + nextSteps([ + NextStep( + label: 'Sortable lists', + desc: 'Reorder a list with the sortable preset.', + href: docHref('sortable'), + ), + NextStep( + label: 'Accessibility', + desc: 'Keyboard dragging and screen-reader announcements.', + href: docHref('accessibility'), + ), + ]), + ], + ); + } +} + +const _usageFlutter = '''DndScope( + child: Stack( + children: [ + board, + DndDragOverlay( + builder: (context, activeId) => CardTile(cardFor(activeId)), + ), + ], + ), +)'''; + +const _usageJaspr = '''DndScope( + child: div([ + board, + DndDragOverlay( + builder: (context, activeId) => cardTile(cardFor(activeId)), + ), + ]), +)'''; diff --git a/website/lib/docs/pages/overview_page.dart b/website/lib/docs/pages/overview_page.dart new file mode 100644 index 0000000..b343dbe --- /dev/null +++ b/website/lib/docs/pages/overview_page.dart @@ -0,0 +1,109 @@ +import 'package:jaspr/jaspr.dart'; + +import '../../data/site_data.dart'; +import '../doc_components.dart'; +import '../docs_nav.dart'; +import '../docs_shell.dart'; + +/// `/docs` — the documentation landing page. +class OverviewPage extends StatelessComponent { + const OverviewPage({super.key}); + + @override + Component build(BuildContext context) { + return DocsShell( + slug: '', + toc: const [ + (id: 'features', label: 'Key features'), + (id: 'packages', label: 'Choose a package'), + (id: 'concepts', label: 'Core concepts'), + ], + body: [ + docLead( + 'dnd_kit is one drag-and-drop engine for Flutter and the web. A ' + 'framework-neutral core does the drag math; thin adapters render it ' + 'on each platform — so the same API moves cards on Flutter and in ' + 'the browser.', + ), + docSection( + id: 'features', + title: 'Key features', + children: [ + docBullets(const [ + 'One engine, two adapters — identical collision, modifier, and ' + 'sortable math on Flutter and the web.', + 'Batteries included — draggable, droppable, drag overlay, ' + 'sortable lists, and multi-container Kanban presets.', + 'Accessible by default — keyboard dragging and live-region ' + 'announcements are built in, not bolted on.', + 'SSR-safe on the web — components pre-render on the server and ' + 'hydrate without DOM access at import time.', + ]), + ], + ), + docSection( + id: 'packages', + title: 'Choose a package', + children: [ + docProse('Pick the package for your platform, then read on:'), + nextSteps([ + NextStep( + label: 'dnd_kit_flutter', + desc: 'Flutter apps — widgets, sensors, overlays, sortable.', + href: SiteLinks.pubFlutter, + external: true, + ), + NextStep( + label: 'dnd_kit_jaspr', + desc: + 'Jaspr (Dart web) apps — the web adapter, no Flutter SDK.', + href: SiteLinks.pubJaspr, + external: true, + ), + NextStep( + label: 'dnd_kit', + desc: 'The shared engine — custom adapters and drag math.', + href: SiteLinks.pubKit, + external: true, + ), + NextStep( + label: 'Installation', + desc: 'Add a package and start dragging.', + href: docHref('install'), + ), + ]), + ], + ), + docSection( + id: 'concepts', + title: 'Core concepts', + children: [ + docProse('The essential building blocks, smallest first:'), + nextSteps([ + NextStep( + label: 'Quickstart', + desc: 'Wire a draggable and a droppable in three steps.', + href: docHref('quickstart'), + ), + NextStep( + label: 'Draggable', + desc: 'Make any widget or element pick-up-able.', + href: docHref('draggable'), + ), + NextStep( + label: 'Droppable', + desc: 'Define the targets a draggable can land on.', + href: docHref('droppable'), + ), + NextStep( + label: 'Sortable lists', + desc: 'Reorder a list with the sortable preset.', + href: docHref('sortable'), + ), + ]), + ], + ), + ], + ); + } +} diff --git a/website/lib/docs/pages/quickstart_page.dart b/website/lib/docs/pages/quickstart_page.dart new file mode 100644 index 0000000..1bbc52b --- /dev/null +++ b/website/lib/docs/pages/quickstart_page.dart @@ -0,0 +1,187 @@ +import 'package:jaspr/jaspr.dart'; + +import '../code_tabs.dart'; +import '../doc_components.dart'; +import '../docs_nav.dart'; +import '../docs_shell.dart'; + +/// `/docs/quickstart` +class QuickstartPage extends StatelessComponent { + const QuickstartPage({super.key}); + + @override + Component build(BuildContext context) { + return DocsShell( + slug: 'quickstart', + toc: const [ + (id: 'scope', label: 'Wrap in a scope'), + (id: 'draggable', label: 'Make a draggable'), + (id: 'droppable', label: 'Add a droppable'), + (id: 'together', label: 'Put it together'), + ], + body: [ + docLead( + 'Three steps to your first drag-and-drop: wrap an area in a scope, ' + 'mark a draggable and a drop target, then react when they meet. The ' + 'API is the same on Flutter and Jaspr — switch the tab on any ' + 'snippet to compare.', + ), + youWillLearn(const [ + 'How to wrap an interactive area in a DndScope.', + 'How to make an element draggable and define a drop target.', + 'How to react to a drop with onDragEnd.', + ]), + docSection( + id: 'scope', + title: 'Wrap in a scope', + children: [ + docProseRich([ + docText('A '), + inlineCode('DndScope'), + docText( + ' owns one drag interaction. Everything draggable or droppable ' + 'lives inside it. Start by wrapping the area you want to make ' + 'interactive.', + ), + ]), + ], + ), + docSection( + id: 'draggable', + title: 'Make a draggable', + children: [ + docProseRich([ + docText('Wrap any element in a '), + inlineCode('DndDraggable'), + docText(' with a unique '), + inlineCode('DndId'), + docText('. That is all it takes to pick it up.'), + ]), + const CodeTabs( + flutterFile: 'draggable.dart', + jasprFile: 'draggable.dart', + flutter: _draggableFlutter, + jaspr: _draggableJaspr, + ), + ], + ), + docSection( + id: 'droppable', + title: 'Add a droppable', + children: [ + docProseRich([ + docText('A '), + inlineCode('DndDroppable'), + docText( + ' is a target a draggable can land on. It also takes a unique ' + 'id, which you read back when the drag ends.', + ), + ]), + const CodeTabs( + flutterFile: 'droppable.dart', + jasprFile: 'droppable.dart', + flutter: _droppableFlutter, + jaspr: _droppableJaspr, + ), + ], + ), + docSection( + id: 'together', + title: 'Put it together', + children: [ + docProseRich([ + docText('Listen to '), + inlineCode('onDragEnd'), + docText( + ' to move your data when a draggable lands on a target. You own ' + 'the state; dnd_kit reports the move.', + ), + ]), + const CodeTabs( + flutterFile: 'main.dart', + jasprFile: 'main.dart', + flutter: _togetherFlutter, + jaspr: _togetherJaspr, + ), + ], + ), + nextSteps([ + NextStep( + label: 'Draggable', + desc: 'Drag handles, activation, and drag data in depth.', + href: docHref('draggable'), + ), + NextStep( + label: 'Sortable lists', + desc: 'Reorder a list with the sortable preset.', + href: docHref('sortable'), + ), + ]), + ], + ); + } +} + +const _draggableFlutter = + '''import 'package:dnd_kit_flutter/dnd_kit_flutter.dart'; +import 'package:flutter/widgets.dart'; + +DndDraggable( + id: const DndId('card'), + child: const Text('Drag me'), +)'''; + +const _draggableJaspr = '''import 'package:dnd_kit_jaspr/dnd_kit_jaspr.dart'; + +DndDraggable( + id: const DndId('card'), + child: div([.text('Drag me')]), +)'''; + +const _droppableFlutter = '''DndDroppable( + id: const DndId('inbox'), + child: const Text('Inbox'), +)'''; + +const _droppableJaspr = '''DndDroppable( + id: const DndId('inbox'), + child: div([.text('Inbox')]), +)'''; + +const _togetherFlutter = '''DndScope( + child: Column( + children: [ + DndDraggable( + id: const DndId('card'), + onDragEnd: (event) { + if (event.overId == const DndId('inbox')) { + moveCardToInbox(); + } + }, + child: const Text('Drag me'), + ), + DndDroppable( + id: const DndId('inbox'), + child: const Text('Inbox'), + ), + ], + ), +)'''; + +const _togetherJaspr = '''DndScope( + child: div([ + DndDraggable( + id: const DndId('card'), + onDragEnd: (event) { + if (event.overId == const DndId('inbox')) { + moveCardToInbox(); + } + }, + child: div([.text('Drag me')]), + ), + DndDroppable( + id: const DndId('inbox'), + child: div([.text('Inbox')]), + ), + ]), +)'''; diff --git a/website/lib/docs/pages/sortable_page.dart b/website/lib/docs/pages/sortable_page.dart new file mode 100644 index 0000000..c69862b --- /dev/null +++ b/website/lib/docs/pages/sortable_page.dart @@ -0,0 +1,138 @@ +import 'package:jaspr/jaspr.dart'; + +import '../code_tabs.dart'; +import '../doc_components.dart'; +import '../docs_nav.dart'; +import '../docs_shell.dart'; + +/// `/docs/sortable` +class SortablePage extends StatelessComponent { + const SortablePage({super.key}); + + @override + Component build(BuildContext context) { + return DocsShell( + slug: 'sortable', + toc: const [ + (id: 'usage', label: 'Usage'), + (id: 'strategies', label: 'Strategies'), + (id: 'state', label: 'Managing state'), + ], + body: [ + docLead( + 'The sortable preset turns a list into a reorderable one without ' + 'wiring draggables and droppables by hand. Wrap the list in a ' + 'SortableScope and each item in a SortableItem.', + ), + youWillLearn(const [ + 'How to make a single list reorderable.', + 'Which layout strategy to pick for lists and grids.', + 'How to apply the reported move to your own state.', + ]), + docSection( + id: 'usage', + title: 'Usage', + children: [ + docProseRich([ + docText('A '), + inlineCode('SortableScope'), + docText(' takes the ordered '), + inlineCode('itemIds'), + docText(' and an '), + inlineCode('onMove'), + docText(' callback; each child is a '), + inlineCode('SortableItem'), + docText(' with a matching id.'), + ]), + const CodeTabs( + flutterFile: 'sortable.dart', + jasprFile: 'sortable.dart', + flutter: _usageFlutter, + jaspr: _usageJaspr, + ), + ], + ), + docSection( + id: 'strategies', + title: 'Strategies', + children: [ + docProseRich([ + docText('Pick a '), + inlineCode('SortableStrategies'), + docText(' to match your layout:'), + ]), + docBullets(const [ + 'verticalList — a stacked column of items.', + 'horizontalList — a row of items, e.g. tabs or nav pills.', + 'grid — a wrapping grid that reflows in two dimensions.', + ]), + ], + ), + docSection( + id: 'state', + title: 'Managing state', + children: [ + docProse( + 'dnd_kit reports a move as from/to indices — you own the list and ' + 'apply it. Reordering is a remove-then-insert on your data:', + ), + const CodeTabs( + flutterFile: 'on_move.dart', + jasprFile: 'on_move.dart', + flutter: _moveFlutter, + jaspr: _moveJaspr, + ), + ], + ), + nextSteps([ + NextStep( + label: 'Multi-container sortable', + desc: 'Move items across lists — a Kanban board.', + href: docHref('multi-container'), + ), + NextStep( + label: 'Accessibility', + desc: 'Keyboard reordering and announcements.', + href: docHref('accessibility'), + ), + ]), + ], + ); + } +} + +const _usageFlutter = '''SortableScope( + controller: controller, + strategy: SortableStrategies.verticalList, + itemIds: order, // List<DndId> + onMove: _onMove, + child: Column( + children: [ + for (final id in order) SortableItem(id: id, child: CardTile(id)), + ], + ), +)'''; + +const _usageJaspr = '''SortableScope( + controller: controller, + strategy: SortableStrategies.verticalList, + itemIds: order, // List<DndId> + onMove: _onMove, + child: div([ + for (final id in order) SortableItem(id: id, child: cardTile(id)), + ]), +)'''; + +const _moveFlutter = '''void _onMove(SortableMoveDetails details) { + setState(() { + final id = order.removeAt(details.fromIndex); + order.insert(details.toIndex, id); + }); +}'''; + +const _moveJaspr = '''void _onMove(SortableMoveDetails details) { + setState(() { + final id = order.removeAt(details.fromIndex); + order.insert(details.toIndex, id); + }); +}'''; diff --git a/website/lib/layout/mobile_nav.dart b/website/lib/layout/mobile_nav.dart index 662aa60..af0cc60 100644 --- a/website/lib/layout/mobile_nav.dart +++ b/website/lib/layout/mobile_nav.dart @@ -28,8 +28,11 @@ class _MobileNavState extends State<MobileNav> { if (kIsWeb) { if (href.startsWith('#')) { web.window.location.hash = href; - } else { + } else if (href.startsWith('http')) { web.window.open(href, '_blank'); + } else { + // Internal route (e.g. the docs page) — navigate in the same tab. + web.window.location.href = href; } } _close(); diff --git a/website/lib/layout/nav_bar.dart b/website/lib/layout/nav_bar.dart index acc3199..3770b14 100644 --- a/website/lib/layout/nav_bar.dart +++ b/website/lib/layout/nav_bar.dart @@ -10,7 +10,10 @@ import 'mobile_nav.dart'; /// Sticky top navigation. The in-page links are reorderable (drag a pill to /// rearrange them) while still navigating on a plain click. class NavBar extends StatelessComponent { - const NavBar({super.key}); + const NavBar({this.activeDocs = false, super.key}); + + /// Highlights the Docs pill when the docs page is the current route. + final bool activeDocs; @override Component build(BuildContext context) { @@ -43,7 +46,10 @@ class NavBar extends StatelessComponent { ), a( href: SiteLinks.docs, - classes: 'pill-link hidden sm:inline-block', + classes: + 'pill-link hidden sm:inline-block' + '${activeDocs ? ' text-accent' : ''}', + attributes: activeDocs ? const {'aria-current': 'page'} : null, [.text('Docs')], ), const ThemeToggle(), diff --git a/website/lib/main.client.options.dart b/website/lib/main.client.options.dart index 0335885..2e6ed18 100644 --- a/website/lib/main.client.options.dart +++ b/website/lib/main.client.options.dart @@ -6,6 +6,7 @@ import 'package:jaspr/client.dart'; +import 'package:dnd_kit_website/docs/code_tabs.dart' deferred as _code_tabs; import 'package:dnd_kit_website/drag/telemetry_hud.dart' deferred as _telemetry_hud; import 'package:dnd_kit_website/layout/mobile_nav.dart' deferred as _mobile_nav; @@ -39,6 +40,15 @@ import 'package:dnd_kit_website/theme/theme_toggle.dart' /// ``` ClientOptions get defaultClientOptions => ClientOptions( clients: { + 'code_tabs': ClientLoader( + (p) => _code_tabs.CodeTabs( + flutter: p['flutter'] as String, + jaspr: p['jaspr'] as String, + flutterFile: p['flutterFile'] as String, + jasprFile: p['jasprFile'] as String, + ), + loader: _code_tabs.loadLibrary, + ), 'telemetry_hud': ClientLoader( (p) => _telemetry_hud.TelemetryHud(), loader: _telemetry_hud.loadLibrary, diff --git a/website/lib/main.server.dart b/website/lib/main.server.dart index 043d722..a704993 100644 --- a/website/lib/main.server.dart +++ b/website/lib/main.server.dart @@ -1,8 +1,8 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/server.dart'; +import 'app.dart'; import 'main.server.options.dart'; -import 'site.dart'; /// Google Fonts: Newsreader (display serif), Hanken Grotesk (body), /// Geist Mono (utility/code). @@ -88,7 +88,7 @@ void main() { children: const [RawText(_noFlashScript)], ), ], - body: const Site(), + body: const App(), ), ); } diff --git a/website/lib/main.server.options.dart b/website/lib/main.server.options.dart index 39ae320..f1361fa 100644 --- a/website/lib/main.server.options.dart +++ b/website/lib/main.server.options.dart @@ -5,6 +5,7 @@ // Generated with jaspr_builder import 'package:jaspr/server.dart'; +import 'package:dnd_kit_website/docs/code_tabs.dart' as _code_tabs; import 'package:dnd_kit_website/drag/telemetry_hud.dart' as _telemetry_hud; import 'package:dnd_kit_website/layout/mobile_nav.dart' as _mobile_nav; import 'package:dnd_kit_website/layout/nav_bar.dart' as _nav_bar; @@ -35,6 +36,10 @@ import 'package:dnd_kit_website/theme/theme_toggle.dart' as _theme_toggle; ServerOptions get defaultServerOptions => ServerOptions( clientId: 'main.client.dart.js', clients: { + _code_tabs.CodeTabs: ClientTarget<_code_tabs.CodeTabs>( + 'code_tabs', + params: __code_tabsCodeTabs, + ), _telemetry_hud.TelemetryHud: ClientTarget<_telemetry_hud.TelemetryHud>( 'telemetry_hud', ), @@ -53,3 +58,10 @@ ServerOptions get defaultServerOptions => ServerOptions( ), }, ); + +Map<String, Object?> __code_tabsCodeTabs(_code_tabs.CodeTabs c) => { + 'flutter': c.flutter, + 'jaspr': c.jaspr, + 'flutterFile': c.flutterFile, + 'jasprFile': c.jasprFile, +}; diff --git a/website/pubspec.yaml b/website/pubspec.yaml index 548e55b..6812eff 100644 --- a/website/pubspec.yaml +++ b/website/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: dnd_kit_jaspr: path: ../packages/dnd_kit_jaspr jaspr: ^0.23.1 + jaspr_router: ^0.8.2 universal_web: ^1.1.1 dev_dependencies: From 548f516b2bf065a1b9edda25e96b940a3088c782 Mon Sep 17 00:00:00 2001 From: iamv4g <iamv4g.dev@gmail.com> Date: Thu, 25 Jun 2026 18:51:02 +0700 Subject: [PATCH 3/4] Complete the documentation section - add four Concepts pages written against the real engine API: collision detection, sensors & activation, modifiers, and auto-scroll - add an API reference page linking the per-package dartdoc and changelogs - add a collapsible mobile docs menu so the section is navigable below the sidebar breakpoint - record the US-082 docs completion in the roadmap and story packet --- docs/product/release-roadmap.md | 10 +- .../README.md | 8 +- website/lib/app.dart | 30 +++++ website/lib/docs/docs_nav.dart | 35 +++++ website/lib/docs/docs_shell.dart | 30 +++++ website/lib/docs/pages/autoscroll_page.dart | 107 +++++++++++++++ website/lib/docs/pages/collision_page.dart | 124 ++++++++++++++++++ website/lib/docs/pages/modifiers_page.dart | 119 +++++++++++++++++ website/lib/docs/pages/reference_page.dart | 106 +++++++++++++++ website/lib/docs/pages/sensors_page.dart | 121 +++++++++++++++++ 10 files changed, 687 insertions(+), 3 deletions(-) create mode 100644 website/lib/docs/pages/autoscroll_page.dart create mode 100644 website/lib/docs/pages/collision_page.dart create mode 100644 website/lib/docs/pages/modifiers_page.dart create mode 100644 website/lib/docs/pages/reference_page.dart create mode 100644 website/lib/docs/pages/sensors_page.dart diff --git a/docs/product/release-roadmap.md b/docs/product/release-roadmap.md index a3f445c..1159f81 100644 --- a/docs/product/release-roadmap.md +++ b/docs/product/release-roadmap.md @@ -327,7 +327,10 @@ Close the docs/adoption gap left after the stable `0.4.0` publication: family; - `US-081`: expand `/docs` into a multi-page docs section (shared sidebar shell, right-rail TOC, prev/next pager, Flutter|Jaspr code tabs) covering the nine - core pages. + core pages; +- `US-082`: complete the coverage with four more Concepts pages (collision, + sensors, modifiers, auto-scroll), an API Reference page, and a collapsible + mobile docs menu. Phase README: `docs/stories/phase-32-website-docs-page-and-doc-alignment/README.md`. @@ -381,7 +384,10 @@ route (replacing the `#docs` placeholder) and aligned the root README plus the Jaspr and Flutter package READMEs with the shipped stable `0.4.0` family, and `US-081` expanded `/docs` into a multi-page docs section — a shared `DocsShell` (grouped sidebar, right-rail TOC, prev/next pager), a Flutter|Jaspr code-tab -block, and nine core pages over the shared engine surface. +block, and nine core pages over the shared engine surface, and `US-082` +completed the coverage with four more Concepts pages (collision, sensors, +modifiers, auto-scroll), an API Reference page, and a collapsible mobile docs +menu. Future work should keep extending this roadmap through new product docs, story packets, and decisions rather than by reviving the old umbrella/core topology from the historical specs. diff --git a/docs/stories/phase-32-website-docs-page-and-doc-alignment/README.md b/docs/stories/phase-32-website-docs-page-and-doc-alignment/README.md index 7be774a..c35d108 100644 --- a/docs/stories/phase-32-website-docs-page-and-doc-alignment/README.md +++ b/docs/stories/phase-32-website-docs-page-and-doc-alignment/README.md @@ -15,7 +15,12 @@ This phase closes that adoption gap across two normal-lane stories: multi-page docs section: a shared `DocsShell` (grouped sidebar, right-rail "On this page" TOC, previous/next pager), a Flutter|Jaspr `CodeTabs` block, and nine core pages (Overview, Installation, Quickstart, Draggable, - Droppable, Drag overlay, Sortable lists, Multi-container, Accessibility). + Droppable, Drag overlay, Sortable lists, Multi-container, Accessibility); +- `US-082` completed the docs coverage: four more Concepts pages (Collision + detection, Sensors & activation, Modifiers, Auto-scroll) written against the + real engine API, an API Reference page (dartdoc + changelog links), and a + collapsible mobile docs menu so the section is navigable below the `lg` + breakpoint. ## Principle @@ -35,6 +40,7 @@ Doc-alignment and website work in this phase must: | --- | --- | --- | | **US-080** | Add the website `/docs` Getting Started page (jaspr_router) and align root/package READMEs + website docs link with stable `0.4.0` | No ADR (docs/adoption work under the existing website and package-topology decisions) | | **US-081** | Expand `/docs` into a multi-page docs section: shared `DocsShell` (sidebar + TOC + pager), Flutter\|Jaspr `CodeTabs`, nine core pages | No ADR (unified single docs tree with per-snippet adapter tabs, under the existing website decisions) | +| **US-082** | Complete docs coverage: four Concepts pages (collision, sensors, modifiers, auto-scroll), an API Reference page, and a mobile docs menu | No ADR (docs content + responsive nav under the existing website decisions) | ## Validation Ladder diff --git a/website/lib/app.dart b/website/lib/app.dart index 2a4b296..91f261c 100644 --- a/website/lib/app.dart +++ b/website/lib/app.dart @@ -2,13 +2,18 @@ import 'package:jaspr/jaspr.dart'; import 'package:jaspr_router/jaspr_router.dart'; import 'docs/pages/accessibility_page.dart'; +import 'docs/pages/autoscroll_page.dart'; +import 'docs/pages/collision_page.dart'; import 'docs/pages/draggable_page.dart'; import 'docs/pages/droppable_page.dart'; import 'docs/pages/install_page.dart'; +import 'docs/pages/modifiers_page.dart'; import 'docs/pages/multi_container_page.dart'; import 'docs/pages/overlay_page.dart'; import 'docs/pages/overview_page.dart'; import 'docs/pages/quickstart_page.dart'; +import 'docs/pages/reference_page.dart'; +import 'docs/pages/sensors_page.dart'; import 'docs/pages/sortable_page.dart'; import 'site.dart'; @@ -53,6 +58,26 @@ class App extends StatelessComponent { title: 'Drag overlay · dnd_kit', builder: (context, state) => const OverlayPage(), ), + Route( + path: '/docs/collision', + title: 'Collision detection · dnd_kit', + builder: (context, state) => const CollisionPage(), + ), + Route( + path: '/docs/sensors', + title: 'Sensors & activation · dnd_kit', + builder: (context, state) => const SensorsPage(), + ), + Route( + path: '/docs/modifiers', + title: 'Modifiers · dnd_kit', + builder: (context, state) => const ModifiersPage(), + ), + Route( + path: '/docs/auto-scroll', + title: 'Auto-scroll · dnd_kit', + builder: (context, state) => const AutoscrollPage(), + ), Route( path: '/docs/sortable', title: 'Sortable lists · dnd_kit', @@ -68,6 +93,11 @@ class App extends StatelessComponent { title: 'Accessibility · dnd_kit', builder: (context, state) => const AccessibilityPage(), ), + Route( + path: '/docs/reference', + title: 'API reference · dnd_kit', + builder: (context, state) => const ReferencePage(), + ), ], ); } diff --git a/website/lib/docs/docs_nav.dart b/website/lib/docs/docs_nav.dart index 1760c19..8559768 100644 --- a/website/lib/docs/docs_nav.dart +++ b/website/lib/docs/docs_nav.dart @@ -90,6 +90,30 @@ const docGroups = <DocGroup>[ title: 'Drag overlay', group: 'Concepts', ), + DocEntry( + slug: 'collision', + navLabel: 'Collision detection', + title: 'Collision detection', + group: 'Concepts', + ), + DocEntry( + slug: 'sensors', + navLabel: 'Sensors & activation', + title: 'Sensors & activation', + group: 'Concepts', + ), + DocEntry( + slug: 'modifiers', + navLabel: 'Modifiers', + title: 'Modifiers', + group: 'Concepts', + ), + DocEntry( + slug: 'auto-scroll', + navLabel: 'Auto-scroll', + title: 'Auto-scroll', + group: 'Concepts', + ), ], ), DocGroup( @@ -120,6 +144,17 @@ const docGroups = <DocGroup>[ ), ], ), + DocGroup( + label: 'Reference', + entries: [ + DocEntry( + slug: 'reference', + navLabel: 'API reference', + title: 'API reference', + group: 'Reference', + ), + ], + ), ]; /// All entries in sidebar order — drives the previous/next pager. diff --git a/website/lib/docs/docs_shell.dart b/website/lib/docs/docs_shell.dart index 7ea4414..565de04 100644 --- a/website/lib/docs/docs_shell.dart +++ b/website/lib/docs/docs_shell.dart @@ -87,8 +87,38 @@ class DocsShell extends StatelessComponent { ); } + /// A collapsible group menu shown below the `lg` breakpoint, where the + /// fixed sidebar is hidden. Native `<details>`, so it needs no hydration. + Component _mobileNav() { + return Component.element( + tag: 'details', + classes: 'mb-8 rounded-2xl border border-line bg-surface lg:hidden', + children: [ + Component.element( + tag: 'summary', + classes: + 'cursor-pointer select-none px-4 py-3 text-sm font-medium text-ink', + children: const [.text('Documentation menu')], + ), + div(classes: 'flex flex-col gap-5 border-t border-line px-4 py-4', [ + for (final group in docGroups) + div(classes: 'flex flex-col gap-1', [ + span( + classes: + 'mb-1 font-mono text-xs uppercase tracking-[0.18em] ' + 'text-muted', + [.text(group.label)], + ), + for (final entry in group.entries) _sidebarLink(entry), + ]), + ]), + ], + ); + } + Component _content(DocEntry entry) { return div(classes: 'min-w-0', [ + _mobileNav(), eyebrow(entry.group), h1(classes: 'mt-3 font-serif text-4xl text-ink sm:text-5xl', [ .text(entry.title), diff --git a/website/lib/docs/pages/autoscroll_page.dart b/website/lib/docs/pages/autoscroll_page.dart new file mode 100644 index 0000000..e33f37e --- /dev/null +++ b/website/lib/docs/pages/autoscroll_page.dart @@ -0,0 +1,107 @@ +import 'package:jaspr/jaspr.dart'; + +import '../code_tabs.dart'; +import '../doc_components.dart'; +import '../docs_nav.dart'; +import '../docs_shell.dart'; + +/// `/docs/auto-scroll` +class AutoscrollPage extends StatelessComponent { + const AutoscrollPage({super.key}); + + @override + Component build(BuildContext context) { + return DocsShell( + slug: 'auto-scroll', + toc: const [ + (id: 'how', label: 'How it works'), + (id: 'usage', label: 'Usage'), + (id: 'options', label: 'Options'), + ], + body: [ + docLead( + 'When a drag reaches the edge of a scrollable region, auto-scroll ' + 'moves the content to follow — so you can drag into parts of a list ' + 'or board that are off-screen.', + ), + youWillLearn(const [ + 'How edge auto-scroll is driven.', + 'How to wrap a scrollable region with DndAutoScroll.', + 'Which axis and options you can tune.', + ]), + docSection( + id: 'how', + title: 'How it works', + children: [ + docProse( + 'The scroll velocity comes from the same DOM-free math on both ' + 'adapters: the closer the pointer is to an edge, the faster the ' + 'region scrolls. Only the execution differs — Flutter drives a ' + 'Scrollable, Jaspr scrolls a DOM element.', + ), + ], + ), + docSection( + id: 'usage', + title: 'Usage', + children: [ + docProseRich([ + docText('Wrap the scrollable area in a '), + inlineCode('DndAutoScroll'), + docText(' inside the scope. On Flutter it drives the nearest '), + inlineCode('Scrollable'), + docText('; on Jaspr you make the wrapper scroll with '), + inlineCode('overflow'), + docText(' styles or classes.'), + ]), + const CodeTabs( + flutterFile: 'auto_scroll.dart', + jasprFile: 'auto_scroll.dart', + flutter: _flutterCode, + jaspr: _jasprCode, + ), + ], + ), + docSection( + id: 'options', + title: 'Options', + children: [ + docBullets(const [ + 'axis — DndScrollAxis.vertical (default) or horizontal.', + 'enabled — turn auto-scroll off without unwrapping.', + 'options — a DndAutoScrollOptions to tune edge thresholds and ' + 'speed.', + ]), + ], + ), + nextSteps([ + NextStep( + label: 'Sortable lists', + desc: 'Reorder long lists that scroll while dragging.', + href: docHref('sortable'), + ), + NextStep( + label: 'Multi-container sortable', + desc: 'Auto-scroll columns on a Kanban board.', + href: docHref('multi-container'), + ), + ]), + ], + ); + } +} + +const _flutterCode = '''DndScope( + child: DndAutoScroll( + axis: DndScrollAxis.vertical, + child: ListView(children: cards), + ), +)'''; + +const _jasprCode = '''DndScope( + child: DndAutoScroll( + axis: DndScrollAxis.vertical, + classes: 'max-h-[480px] overflow-auto', + child: div(cards), + ), +)'''; diff --git a/website/lib/docs/pages/collision_page.dart b/website/lib/docs/pages/collision_page.dart new file mode 100644 index 0000000..756fbe9 --- /dev/null +++ b/website/lib/docs/pages/collision_page.dart @@ -0,0 +1,124 @@ +import 'package:jaspr/jaspr.dart'; + +import '../code_tabs.dart'; +import '../doc_components.dart'; +import '../docs_nav.dart'; +import '../docs_shell.dart'; + +/// `/docs/collision` +class CollisionPage extends StatelessComponent { + const CollisionPage({super.key}); + + @override + Component build(BuildContext context) { + return DocsShell( + slug: 'collision', + toc: const [ + (id: 'detectors', label: 'Built-in detectors'), + (id: 'setting', label: 'Setting a detector'), + (id: 'composing', label: 'Composing'), + ], + body: [ + docLead( + 'When a draggable overlaps several droppables, a collision detector ' + 'decides which one wins. Detection is shared math in the engine, so ' + 'Flutter and Jaspr resolve the same target from the same input.', + ), + youWillLearn(const [ + 'The built-in collision detectors and when to use each.', + 'How to set a detector on a scope.', + 'How to combine detectors into a fallback chain.', + ]), + docSection( + id: 'detectors', + title: 'Built-in detectors', + children: [ + docProseRich([ + docText('All detectors live on '), + inlineCode('DndCollisionDetectors'), + docText(':'), + ]), + docBullets(const [ + 'closestCenter — the target whose center is nearest the pointer. ' + 'A solid default for grids and free layouts.', + 'closestCorners — compares corners instead of centers; steadier ' + 'when targets vary a lot in size.', + 'rectIntersection — the target the dragged rect overlaps most. ' + 'Natural for list and Kanban reordering.', + 'pointerWithin — only targets the pointer is literally inside. ' + 'Precise, but needs a pointer (not keyboard) drag.', + ]), + ], + ), + docSection( + id: 'setting', + title: 'Setting a detector', + children: [ + docProseRich([ + docText('Pass a detector to the '), + inlineCode('DndController'), + docText(' on your '), + inlineCode('DndScope'), + docText('. The default is '), + inlineCode('closestCenter'), + docText('.'), + ]), + const CodeTabs( + flutterFile: 'scope.dart', + jasprFile: 'scope.dart', + flutter: _setCode, + jaspr: _setCode, + ), + ], + ), + docSection( + id: 'composing', + title: 'Composing', + children: [ + docProseRich([ + docText('Use '), + inlineCode('compose'), + docText( + ' to try detectors in order and take the first that finds a ' + 'target — for example, prefer a precise pointer hit, then fall ' + 'back to overlap.', + ), + ]), + const CodeTabs( + flutterFile: 'scope.dart', + jasprFile: 'scope.dart', + flutter: _composeCode, + jaspr: _composeCode, + ), + ], + ), + nextSteps([ + NextStep( + label: 'Sensors & activation', + desc: 'Choose input methods and require intent.', + href: docHref('sensors'), + ), + NextStep( + label: 'Modifiers', + desc: 'Constrain the drag transform.', + href: docHref('modifiers'), + ), + ]), + ], + ); + } +} + +const _setCode = '''DndScope( + controller: DndController( + collisionDetector: DndCollisionDetectors.closestCenter, + ), + child: board, +)'''; + +const _composeCode = '''DndController( + collisionDetector: DndCollisionDetectors.compose([ + DndCollisionDetectors.pointerWithin, + DndCollisionDetectors.rectIntersection, + ]), +)'''; diff --git a/website/lib/docs/pages/modifiers_page.dart b/website/lib/docs/pages/modifiers_page.dart new file mode 100644 index 0000000..7fd9a71 --- /dev/null +++ b/website/lib/docs/pages/modifiers_page.dart @@ -0,0 +1,119 @@ +import 'package:jaspr/jaspr.dart'; + +import '../code_tabs.dart'; +import '../doc_components.dart'; +import '../docs_nav.dart'; +import '../docs_shell.dart'; + +/// `/docs/modifiers` +class ModifiersPage extends StatelessComponent { + const ModifiersPage({super.key}); + + @override + Component build(BuildContext context) { + return DocsShell( + slug: 'modifiers', + toc: const [ + (id: 'what', label: 'What modifiers do'), + (id: 'builtin', label: 'Built-in modifiers'), + (id: 'applying', label: 'Applying them'), + ], + body: [ + docLead( + 'Modifiers adjust the active drag transform before it is applied — ' + 'constrain movement to an axis, snap to a grid, or clamp within a ' + 'boundary. Each is a pure function, so they compose cleanly.', + ), + youWillLearn(const [ + 'What a modifier is.', + 'The built-in modifiers the engine ships.', + 'How to apply one or several at once.', + ]), + docSection( + id: 'what', + title: 'What modifiers do', + children: [ + docProseRich([ + docText('A modifier is a function from a '), + inlineCode('DndModifierInput'), + docText(' to a '), + inlineCode('DndTransform'), + docText( + '. The engine runs it on every frame of a drag, so the change ' + 'is live and identical on both adapters.', + ), + ]), + ], + ), + docSection( + id: 'builtin', + title: 'Built-in modifiers', + children: [ + docProseRich([ + docText('They live on '), + inlineCode('DndModifiers'), + docText(':'), + ]), + docBullets(const [ + 'restrictToHorizontalAxis — lock movement to the x axis.', + 'restrictToVerticalAxis — lock movement to the y axis.', + 'restrictToBoundary(rect) — clamp the element within a rectangle.', + 'snapToGrid(width: , height: ) — quantize movement to a grid.', + 'compose(modifiers) — apply several in order, first to last.', + ]), + ], + ), + docSection( + id: 'applying', + title: 'Applying them', + children: [ + docProseRich([ + docText('Pass modifiers to the '), + inlineCode('DndController'), + docText(' on your scope:'), + ]), + const CodeTabs( + flutterFile: 'scope.dart', + jasprFile: 'scope.dart', + flutter: _oneCode, + jaspr: _oneCode, + ), + docProse('Combine several — snap to a grid and clamp to a box:'), + const CodeTabs( + flutterFile: 'scope.dart', + jasprFile: 'scope.dart', + flutter: _manyCode, + jaspr: _manyCode, + ), + ], + ), + nextSteps([ + NextStep( + label: 'Auto-scroll', + desc: 'Scroll a container while dragging past its edge.', + href: docHref('auto-scroll'), + ), + NextStep( + label: 'Collision detection', + desc: 'Decide which target a draggable lands on.', + href: docHref('collision'), + ), + ]), + ], + ); + } +} + +const _oneCode = '''DndScope( + controller: DndController( + modifiers: [DndModifiers.restrictToHorizontalAxis], + ), + child: board, +)'''; + +const _manyCode = '''DndController( + modifiers: [ + DndModifiers.snapToGrid(width: 16, height: 16), + DndModifiers.restrictToBoundary(boardRect), + ], +)'''; diff --git a/website/lib/docs/pages/reference_page.dart b/website/lib/docs/pages/reference_page.dart new file mode 100644 index 0000000..c091ba8 --- /dev/null +++ b/website/lib/docs/pages/reference_page.dart @@ -0,0 +1,106 @@ +import 'package:jaspr/jaspr.dart'; + +import '../../data/site_data.dart'; +import '../doc_components.dart'; +import '../docs_shell.dart'; + +/// `/docs/reference` +class ReferencePage extends StatelessComponent { + const ReferencePage({super.key}); + + @override + Component build(BuildContext context) { + return DocsShell( + slug: 'reference', + toc: const [ + (id: 'api', label: 'API docs'), + (id: 'changelog', label: 'Changelog'), + (id: 'source', label: 'Source & issues'), + ], + body: [ + docLead( + 'The full generated API reference lives on pub.dev. Browse the ' + 'package you depend on, or read the engine for the shared contracts.', + ), + docSection( + id: 'api', + title: 'API docs', + children: [ + nextSteps([ + NextStep( + label: 'dnd_kit_flutter', + desc: 'Flutter adapter — widgets, sensors, overlays, sortable.', + href: _doc('dnd_kit_flutter'), + external: true, + ), + NextStep( + label: 'dnd_kit_jaspr', + desc: + 'Jaspr (web) adapter — components over the shared engine.', + href: _doc('dnd_kit_jaspr'), + external: true, + ), + NextStep( + label: 'dnd_kit', + desc: 'Shared engine — collision, modifiers, sortable math.', + href: _doc('dnd_kit'), + external: true, + ), + ]), + ], + ), + docSection( + id: 'changelog', + title: 'Changelog', + children: [ + docProse( + 'Each package publishes its changelog on pub.dev. The family ' + 'releases in lockstep — engine first, then the adapters.', + ), + nextSteps([ + NextStep( + label: 'dnd_kit_flutter changelog', + desc: 'Release notes for the Flutter adapter.', + href: _changelog('dnd_kit_flutter'), + external: true, + ), + NextStep( + label: 'dnd_kit_jaspr changelog', + desc: 'Release notes for the web adapter.', + href: _changelog('dnd_kit_jaspr'), + external: true, + ), + NextStep( + label: 'dnd_kit changelog', + desc: 'Release notes for the shared engine.', + href: _changelog('dnd_kit'), + external: true, + ), + ]), + ], + ), + docSection( + id: 'source', + title: 'Source & issues', + children: [ + docProse( + 'The whole family lives in one repository. File issues and read ' + 'the examples there.', + ), + nextSteps([ + NextStep( + label: 'GitHub repository', + desc: 'Source, examples, and issue tracker.', + href: SiteLinks.github, + external: true, + ), + ]), + ], + ), + ], + ); + } +} + +String _doc(String pkg) => 'https://pub.dev/documentation/$pkg/latest/'; +String _changelog(String pkg) => 'https://pub.dev/packages/$pkg/changelog'; diff --git a/website/lib/docs/pages/sensors_page.dart b/website/lib/docs/pages/sensors_page.dart new file mode 100644 index 0000000..cf89e09 --- /dev/null +++ b/website/lib/docs/pages/sensors_page.dart @@ -0,0 +1,121 @@ +import 'package:jaspr/jaspr.dart'; + +import '../code_tabs.dart'; +import '../doc_components.dart'; +import '../docs_nav.dart'; +import '../docs_shell.dart'; + +/// `/docs/sensors` +class SensorsPage extends StatelessComponent { + const SensorsPage({super.key}); + + @override + Component build(BuildContext context) { + return DocsShell( + slug: 'sensors', + toc: const [ + (id: 'what', label: 'What sensors do'), + (id: 'activation', label: 'Activation constraints'), + (id: 'keyboard', label: 'Keyboard'), + ], + body: [ + docLead( + 'Sensors translate raw input — pointer, keyboard — into drag intent. ' + 'The pointer sensor is active by default; activation constraints ' + 'decide how deliberate a gesture must be before a drag begins.', + ), + youWillLearn(const [ + 'How sensors turn input into drags.', + 'How to require a distance or delay before activation.', + 'How keyboard dragging fits in.', + ]), + docSection( + id: 'what', + title: 'What sensors do', + children: [ + docProseRich([ + docText('A '), + inlineCode('DndSensor'), + docText( + ' watches an input source and starts, updates, and ends ' + 'a drag on the controller. The built-in ', + ), + inlineCode('DndPointerSensor'), + docText( + ' handles mouse, touch, and pen through unified pointer ' + 'events, so the same code works across devices.', + ), + ]), + ], + ), + docSection( + id: 'activation', + title: 'Activation constraints', + children: [ + docProseRich([ + docText('A '), + inlineCode('DndSensorActivationConstraint'), + docText( + ' delays activation until the gesture clears a threshold, so a ' + 'tap or click is never mistaken for a drag. Set a small ' + 'distance for immediate-feeling drags, or a delay for ' + 'press-and-hold.', + ), + ]), + const CodeTabs( + flutterFile: 'activation.dart', + jasprFile: 'activation.dart', + flutter: _distanceCode, + jaspr: _distanceCode, + ), + docProse('A press delay instead of a distance:'), + const CodeTabs( + flutterFile: 'activation.dart', + jasprFile: 'activation.dart', + flutter: _delayCode, + jaspr: _delayCode, + ), + ], + ), + docSection( + id: 'keyboard', + title: 'Keyboard', + children: [ + docProse( + 'Keyboard dragging is built in: a focused draggable is picked up ' + 'with space or enter, moved with the arrow keys, and cancelled ' + 'with escape — no extra wiring. See the accessibility page for ' + 'the full keyboard model.', + ), + ], + ), + nextSteps([ + NextStep( + label: 'Accessibility', + desc: 'The full keyboard and announcement model.', + href: docHref('accessibility'), + ), + NextStep( + label: 'Modifiers', + desc: 'Constrain the drag transform.', + href: docHref('modifiers'), + ), + ]), + ], + ); + } +} + +const _distanceCode = '''DndDraggable( + id: const DndId('card'), + constraint: const DndSensorActivationConstraint(distance: 6), + child: card, +)'''; + +const _delayCode = '''DndDraggable( + id: const DndId('card'), + constraint: const DndSensorActivationConstraint( + delay: Duration(milliseconds: 200), + ), + child: card, +)'''; From de076b8907976cb1e64bed2b93b44e1222563d89 Mon Sep 17 00:00:00 2001 From: iamv4g <iamv4g.dev@gmail.com> Date: Thu, 25 Jun 2026 20:08:23 +0700 Subject: [PATCH 4/4] Pin the mobile docs menu while scrolling Make the mobile "Documentation menu" stick below the top nav and cap the expanded list height, so it no longer scrolls out of view on small screens. --- website/lib/docs/docs_shell.dart | 36 +++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/website/lib/docs/docs_shell.dart b/website/lib/docs/docs_shell.dart index 565de04..f35061d 100644 --- a/website/lib/docs/docs_shell.dart +++ b/website/lib/docs/docs_shell.dart @@ -92,26 +92,34 @@ class DocsShell extends StatelessComponent { Component _mobileNav() { return Component.element( tag: 'details', - classes: 'mb-8 rounded-2xl border border-line bg-surface lg:hidden', + classes: + 'sticky top-16 z-20 mb-8 rounded-2xl border border-line ' + 'bg-paper/95 shadow-sm backdrop-blur lg:hidden', children: [ Component.element( tag: 'summary', classes: - 'cursor-pointer select-none px-4 py-3 text-sm font-medium text-ink', + 'cursor-pointer select-none rounded-2xl px-4 py-3 text-sm ' + 'font-medium text-ink', children: const [.text('Documentation menu')], ), - div(classes: 'flex flex-col gap-5 border-t border-line px-4 py-4', [ - for (final group in docGroups) - div(classes: 'flex flex-col gap-1', [ - span( - classes: - 'mb-1 font-mono text-xs uppercase tracking-[0.18em] ' - 'text-muted', - [.text(group.label)], - ), - for (final entry in group.entries) _sidebarLink(entry), - ]), - ]), + div( + classes: + 'flex max-h-[70vh] flex-col gap-5 overflow-auto border-t ' + 'border-line px-4 py-4', + [ + for (final group in docGroups) + div(classes: 'flex flex-col gap-1', [ + span( + classes: + 'mb-1 font-mono text-xs uppercase tracking-[0.18em] ' + 'text-muted', + [.text(group.label)], + ), + for (final entry in group.entries) _sidebarLink(entry), + ]), + ], + ), ], ); }