first draft but not working yet

This commit is contained in:
authentik Default Admin 2026-03-04 11:13:15 -08:00
commit b54c89c9c4
10 changed files with 380 additions and 0 deletions

43
README.md Normal file
View file

@ -0,0 +1,43 @@
## Architecture
* Lit-based web components
* No-build tooling (importmap only)
* Zustand-moderated state management with IndexedDB storage mechanism
## Layout
```
src/
store/
index.js ← zustand store definition
middleware/
persistence.js ← idb-keyval persist adapter
sync.js ← optional cross-tab broadcast
components/
app-root.js
feature-a/
feature-a.js ← Lit component
feature-a.css ← adopted stylesheet or constructable
controllers/
fetch.js ← ReactiveController for API calls
3d.js ← optional Three/WebGPU controller
lib/
idb.js ← thin idb-keyval wrapper/schema
index.html
importmap.json ← extracted importmap (referenced via <script src>)
```
## The Complete Mental Model
```
IndexedDB
↕ (idb-keyval adapter)
Zustand persist middleware
↕ (vanilla store)
StoreController (ReactiveController)
↕ (requestUpdate → selector)
Lit Web Components
↕ (events / store actions)
User
```

46
components/app-root.js Normal file
View file

@ -0,0 +1,46 @@
import { LitElement, html, css } from "lit";
import { store} from "../store/index.js";
import { StoreController } from "../controllers/store.js";
import "./nav-bar.js";
import "./page-home.js";
import "./page-items.js";
class AppRoot extends LitElement {
#hydrated = new StoreController(this, store, s => s._hydrated);
#user = new StoreController(this, store, s => s.user);
#route = new StoreController(this, store, s => s.route);
static styles = css`
:host {
display: block;
min-height: 100vh;
}
.loading {
display: grid;
place-items: center;
height: 100vh;
opacity: 0.4;
}
`;
render() {
if (!this.#hydrated.value) {
return html`<div class="loading">loading...</div>`;
}
return html`
<nav-bar .user=${this.#user.value}></nav-bar>
<main>${this.#renderRoute()}</main>
`;
}
#renderRoute() {
switch (this.#route.value) {
case "home": return html`<page-home></page-home>`;
case "items": return html`<page-items></page-items>`;
default: return html`<page-home></page-home>`;
}
}
}
customElements.define('app-root', AppRoot);

64
components/nav-bar.js Normal file
View file

@ -0,0 +1,64 @@
// components/nav-bar.js
import { LitElement, html, css } from 'lit'
import { store } from '../store/index.js'
import { StoreController } from '../controllers/store.js'
class NavBar extends LitElement {
#route = new StoreController(this, store, s => s.route)
static properties = {
user: { type: Object }
}
static styles = css`
:host {
display: block;
padding: 0 1.5rem;
border-bottom: 1px solid #e2e8f0;
}
nav {
display: flex;
align-items: center;
justify-content: space-between;
height: 3.5rem;
}
.links { display: flex; gap: 1.5rem; }
a {
cursor: pointer;
font-size: 0.9rem;
color: #64748b;
text-decoration: none;
}
a[active] { color: #0f172a; font-weight: 600; }
.user { font-size: 0.85rem; color: #94a3b8; }
`
#navigate(route, e) {
e.preventDefault()
store.getState().navigate(route)
}
#link(route, label) {
return html`
href="/${route}"
?active=${this.#route.value === route}
@click=${(e) => this.#navigate(route, e)}
>${label}</a>
`
}
render() {
return html`
<nav>
<div class="links">
${this.#link('home', 'Home')}
${this.#link('items', 'Items')}
</div>
<span class="user">${this.user?.name ?? 'Guest'}</span>
</nav>
`
}
}
customElements.define('nav-bar', NavBar)

44
components/page-home.js Normal file
View file

@ -0,0 +1,44 @@
// components/page-home.js
import { LitElement, html, css } from 'lit'
import { store } from '../store/index.js'
import { StoreController } from '../controllers/store.js'
class PageHome extends LitElement {
#user = new StoreController(this, store, s => s.user)
#items = new StoreController(this, store, s => s.items)
static styles = css`
:host { display: block; padding: 2rem 1.5rem; }
h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
p { color: #64748b; margin: 0 0 2rem; }
.summary {
display: flex;
gap: 1rem;
}
.stat {
padding: 1rem 1.5rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
}
.stat-value { font-size: 2rem; font-weight: 700; }
.stat-label { font-size: 0.8rem; color: #94a3b8; margin-top: 0.25rem; }
`
render() {
const name = this.#user.value?.name ?? 'there'
const count = this.#items.value.length
return html`
<h1>Hey, ${name}</h1>
<p>Here's what's going on.</p>
<div class="summary">
<div class="stat">
<div class="stat-value">${count}</div>
<div class="stat-label">Items</div>
</div>
</div>
`
}
}
customElements.define('page-home', PageHome)

111
components/page-items.js Normal file
View file

@ -0,0 +1,111 @@
// components/page-items.js
import { LitElement, html, css } from 'lit'
import { store } from '../store/index.js'
import { StoreController } from '../controllers/store.js'
class PageItems extends LitElement {
#items = new StoreController(this, store, s => s.items)
static properties = {
_draft: { type: String, state: true }
}
_draft = ''
static styles = css`
:host { display: block; padding: 2rem 1.5rem; }
h1 { font-size: 1.5rem; margin: 0 0 1.5rem; }
.add-form {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
}
input {
flex: 1;
padding: 0.5rem 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 0.9rem;
outline: none;
}
input:focus { border-color: #94a3b8; }
button {
padding: 0.5rem 1rem;
background: #0f172a;
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
cursor: pointer;
}
button:hover { background: #1e293b; }
ul { list-style: none; padding: 0; margin: 0; }
li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid #f1f5f9;
font-size: 0.9rem;
}
.remove {
background: none;
border: none;
color: #cbd5e1;
cursor: pointer;
font-size: 1rem;
padding: 0;
}
.remove:hover { color: #ef4444; }
.empty { color: #94a3b8; font-size: 0.9rem; }
`
#add() {
const name = this._draft.trim()
if (!name) return
store.getState().addItem({ id: crypto.randomUUID(), name })
this._draft = ''
}
#remove(id) {
store.getState().removeItem(id)
}
#onKeydown(e) {
if (e.key === 'Enter') this.#add()
}
render() {
const items = this.#items.value
return html`
<h1>Items</h1>
<div class="add-form">
<input
type="text"
placeholder="New item…"
.value=${this._draft}
@input=${e => this._draft = e.target.value}
@keydown=${this.#onKeydown}
/>
<button @click=${this.#add}>Add</button>
</div>
${items.length === 0
? html`<p class="empty">No items yet.</p>`
: html`
<ul>
${items.map(item => html`
<li>
<span>${item.name}</span>
<button class="remove" @click=${() => this.#remove(item.id)}></button>
</li>
`)}
</ul>
`}
`
}
}
customElements.define('page-items', PageItems)

23
controllers/store.js Normal file
View file

@ -0,0 +1,23 @@
export class StoreController {
constructor(host, store, selector) {
this.host = host
this.store = store
this.selector = selector
host.addController(this)
}
hostConnected() {
this._unsub = this.store.subscribe(
(state) => {
const next = this.selector(state)
if (next !== this.value) {
this.value = next
this.host.requestUpdate()
}
}
)
this.value = this.selector(this.store.getState())
}
hostDisconnected() { this._unsub?.() }
}

9
importmap.json Normal file
View file

@ -0,0 +1,9 @@
{
"imports": {
"lit": "https://esm.sh/lit@3",
"lit/decorators.js": "https://esm.sh/lit@3/decorators.js",
"zustand": "https://esm.sh/zustand@5/vanilla",
"zustand/middleware": "https://esm.sh/zustand@5/middleware",
"idb-keyval": "https://esm.sh/idb-keyval@6"
}
}

14
index.html Normal file
View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script type="importmap" src="importmap.json"></script>
<script src="https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js"></script>
</head>
<body>
<script type="module" src="components/app-root.js"></script>
<app-root></app-root>
</body>
</html>

18
store/index.js Normal file
View file

@ -0,0 +1,18 @@
import { createStore } from 'zustand/vanilla'
import { persist } from 'zustand/middleware'
import { idbStorage } from './middleware/persistence.js'
export const store = createStore(
persist(
(set, get) => ({
user: null,
items: [],
setUser: (user) => set({ user }),
addItem: (item) => set(s => ({ items: [...s.items, item] })),
}),
{
name: 'app-store',
storage: idbStorage, // custom adapter (see below)
}
)
)

View file

@ -0,0 +1,8 @@
import { get, set, del } from 'idb-keyval'
import { createJSONStorage } from 'zustand/middleware'
export const idbStorage = createJSONStorage(() => ({
getItem: (name) => get(name),
setItem: (name, value) => set(name, value),
removeItem: (name) => del(name),
}))