Add Lit example implementation#1
Open
oddcelot wants to merge 23 commits intoferderer:mainfrom
Open
Conversation
Mirror examples/svelte/micro layout: package.json with file:../../.. texivia-router dep + lit ^3, tsconfig pair (app/node), vite config, index.html entrypoint, shared app.css design tokens, favicon. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
app-header / app-footer mirror the Svelte+Vue micro examples. Both render in light DOM (createRenderRoot returns this) so global app.css applies and anchor clicks bubble to document for Texivia interception. main-layout slots page content between them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Seven pages mirroring the Svelte/Vue micro examples: landing, login, user-profile, about, imprint, contact, not-found. Each exports a LitElement plus a render() function that the router map will hold as the typed view value — same role component classes play in the Svelte/Vue versions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
router.ts uses Router<View> where View = (params) => TemplateResult, mirroring how Svelte/Vue store the component class itself. The render fn pattern keeps the route map declarative and avoids pulling in lit/static-html for tag-name dispatch. app-shell.ts owns the texivia event subscription and re-renders by swapping the @state-tracked view + params. Light DOM ensures global styles apply and anchor clicks reach document for interception. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Light-DOM rendering ignores <slot> elements (slots only project in shadow DOM), so children placed inside <main-layout> in the page templates were rendered above the header instead of inside <main>. Pages now build a body TemplateResult and pass it via .body, mirroring the named-snippet pattern used in the Svelte micro example. Layout keeps light DOM so the global app.css applies and anchor clicks bubble to document for Texivia's interception. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Routes without a handler complete _navigate synchronously inside start() and dispatch the texivia event before control returns. Adding the listener after start() therefore lost the initial event on direct deep-link loads (e.g. opening /en/users/42/profile would render the default Landing view). Reordering means the listener catches the very first dispatch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The root build doesn't emit dist/texivia.d.ts (vite lib mode without a dts plugin), so a strict tsc pass over the example fails with TS7016 — the same issue affects the Vue example's type-check step. Match Vue's working build-only pattern: 'build' just runs vite, 'type-check' stays available as an opt-in script. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the Svelte/Vue micro examples which both check in their lockfiles for reproducible installs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the Svelte/Vue example READMEs and documents the three Lit-specific decisions: light DOM, listener-before-start ordering, and body-as-template-prop instead of <slot>. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Lit row to the framework comparison table and a full Lit 3 section between Svelte and Vue. Covers the typed Router<View> pattern with render functions, the <app-shell> listener, and the two non-obvious gotchas: light-DOM rendering and listener-before-start ordering. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Improves discoverability for Lit/web-components users searching npm now that there's a first-party example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The layout had no state, lifecycle, or events — only a locale prop and a body prop that existed solely to work around <slot> being inert in light DOM. Replacing the LitElement with a function lets pages call mainLayout(locale, html\`...\`) directly. This removes the .body reactive prop, drops a custom-element registration, and eliminates the slot-vs-light-DOM footgun entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the body-prop note with a recommendation to use plain template functions for stateless layouts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The package.json types field has always pointed at dist/texivia.d.ts but the build only ran vite (lib mode emits no declarations), so any consumer with strict TS hit TS7016 'implicitly has an any type'. Chain 'tsc --emitDeclarationOnly' after vite — declaration is already true in tsconfig, so no other config change is needed. Adds zero runtime cost and fixes type-check failures in both example projects. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that the root build emits dist/texivia.d.ts, the type-check pass succeeds. Build runs tsc first, then vite, so type errors fail the build instead of slipping through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applies the repo's .prettierrc (semi, singleQuote, printWidth 120, trailingComma es5) to all new files in examples/lit/micro. No behavioral changes — purely whitespace, semicolons, and line wrapping to match the existing project conventions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous comment said Light DOM lets <a> clicks "bubble to document"
— but composed click events bubble through shadow boundaries fine.
The actual constraint is event.target retargeting: with Shadow DOM,
target becomes the host (<app-shell>), so the router's document-level
closest('a') returns null and skips the navigation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lit renders the app-shell before router.start()'s async chain dispatches the first texivia event, so the first paint uses whatever the @State default is. The previous hardcoded 'en' caused a brief mismatch on non-English browsers — header/footer links flickered as /en/ before the '/' handler redirect resolved to e.g. /de-DE/. Seeding from navigator.language aligns the initial paint with the redirect target. Subtags are stripped to keep the seed a plain language code; the handler still uses the full BCP-47 tag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The README's Lit 3 section claimed plain <a> tags are intercepted
automatically, but that's only true with Light DOM. With Shadow DOM
(LitElement's default), event.target retargets to the host and the
router's closest('a') misses the click.
Updates:
- Spell out the Light DOM constraint in the trailing summary so
copy-pasters don't omit createRenderRoot() and silently break
navigation.
- Reword the snippet's createRenderRoot() comment to match the example.
- Seed params.locale from navigator.language so the README snippet
doesn't render empty-locale hrefs on first paint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The '/' handler redirected to /${navigator.language}/, preserving the
full BCP-47 tag (e.g. /de-DE/). The app-shell @State seed strips the
subtag, so a de-DE browser briefly rendered links with locale=de
before the redirect landed on /de-DE/ and re-rendered with locale=de-DE.
Stripping the subtag in the handler too keeps the seed and
post-redirect params identical, eliminating the second re-render.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The form had method="POST" action="/${locale}/login" but no backend
exists. Submitting did a real navigation to a 404 — surprising for an
SPA example. Replaced with a noop @submit handler so the form stays
inert (the example demos routing, not auth).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three drifts after the recent fixes:
- Router snippet showed the bare navigator.language redirect; the
example now strips the BCP-47 subtag.
- app-shell snippet seeded params.locale with a hardcoded 'en'; the
example now seeds from navigator.language.split('-')[0].
- The Light DOM rationale claimed clicks "bubble to document" — they
do, regardless of shadow DOM, since click is composed. The actual
blocker is event.target retargeting: Texivia calls
event.target.closest('a'), which in shadow DOM walks up from the
host, not the anchor. Rewrote the paragraph and noted that a
shadow-DOM-friendly setup would need the router to read
event.composedPath() instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
README.mdwith a worked<app-shell>snippet and the Light DOM constraint that makes click interception work.dist/texivia.d.tsfrom the publish build so TypeScript users get types out of the box.litandweb-componentsto package keywords; add Lit to the framework support matrix.Implementation notes
Router<View>is parameterized over a render function:View = (params) => TemplateResult. Each route'sviewis the render fn the example swaps into a single root<app-shell>.event.target.closest('a'). Shadow DOM retargetsevent.targetto the host (<app-shell>), soclosest('a')walks past the actual anchor. The example usescreateRenderRoot() { return this }to keep<a>as the literal target. The README and in-code comments both spell this out.start(). Routes without a handler dispatch thetexiviaevent synchronously insiderouter.start(), so a late listener misses the initial event on deep-link loads.mainLayout(locale, body)is a template function, not aLitElement— avoids<slot>ceremony in light DOM and adds no state of its own.<app-shell>seedsparams.localefromnavigator.language.split('-')[0]so the first paint matches the'/' → '/<lang>/'redirect, eliminating a flash of wrong-locale links.@submit=${e => e.preventDefault()}instead of a real POST, since the example demos routing, not auth.