M4 Calendar: publish public .ics links, subscriptions, multi-calendar settings
Calendar (builtin plugin):
- Publish read-only public .ics feed per calendar via vault-sealed token
(full-detail or free/busy-only); unauthenticated GET /public/{token},
404 on bad/absent token. Migration calendar0004 adds publish_token,
publish_busy, subscription_url + partial unique index.
- Subscribe to external iCal (.ics) URLs (SSRF-guarded) as read-only
calendars; CSRF-guarded POST /subscriptions.
- Settings > Calendars: create/edit/delete, publish toggles with live
shareable link + copy, Add-account stubs (Google/iCloud/CalDAV, deferred),
Subscriptions section; default calendar + default reminders; auto-detected
time zone; color-dropdown contrast fix; 3x-larger edit arrow.
- Left panel: shared Mail-parity sidebar template (.wm-side-*), calendar
search panel, font/spacing tuning, per-section "+" create shortcut.
Frontend: CalendarOut publish fields, api publish/subscribe, settings keys.
Backend: settings default_calendar + reminder lists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U72FZiF1W1pb8XxyWBTREA
This commit is contained in:
@@ -10,8 +10,19 @@ type Importer = (url: string) => Promise<unknown>;
|
||||
|
||||
const _cache = new Map<string, Promise<PluginMount>>();
|
||||
|
||||
// In dev, bust the browser's HTTP/module cache so an edited plugin bundle is picked
|
||||
// up on reload. The backend sends `no-cache`, but a copy the browser cached *before*
|
||||
// that header existed can still be served as "fresh" without revalidating — a unique
|
||||
// per-page-load query sidesteps it. Computed once per module load so a bundle is still
|
||||
// imported only once within a session (and the cache key stays the clean URL). Prod
|
||||
// keeps the clean, cacheable URL.
|
||||
const DEV_CACHE_BUST = import.meta.env.DEV ? `v=${Date.now()}` : "";
|
||||
|
||||
// @vite-ignore keeps Vite from trying to resolve/transform the runtime URL.
|
||||
const defaultImporter: Importer = (url) => import(/* @vite-ignore */ url);
|
||||
const defaultImporter: Importer = (url) => {
|
||||
const bustable = DEV_CACHE_BUST ? `${url}${url.includes("?") ? "&" : "?"}${DEV_CACHE_BUST}` : url;
|
||||
return import(/* @vite-ignore */ bustable);
|
||||
};
|
||||
|
||||
export function loadBundle(url: string, importer: Importer = defaultImporter): Promise<PluginMount> {
|
||||
let pending = _cache.get(url);
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
*/
|
||||
|
||||
.wm-root {
|
||||
font: 15px system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
font-family: "Lato", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 15px;
|
||||
color: rgb(var(--app-fg));
|
||||
}
|
||||
|
||||
@@ -126,3 +127,93 @@
|
||||
font-size: 14px;
|
||||
color: rgb(var(--app-muted));
|
||||
}
|
||||
|
||||
/* ── Sidebar shell ─────────────────────────────────────────────────────────
|
||||
* Shared left-panel template. Each class @applies the EXACT Tailwind utilities
|
||||
* Mail's sidebar uses (frontend/src/routes/MailLayout.tsx), so the compiled CSS
|
||||
* is byte-identical to Mail's — no hand-transcribed values that can drift.
|
||||
* `.wm-side` also pins the base font context (Lato, 16px, line-height 1.5) to
|
||||
* Mail's <body>, because a plugin bundle's own root can change the inherited
|
||||
* font size/family for anything that doesn't set its own (e.g. the theme glyph).
|
||||
* An app supplies only: switcher label + menu, primary button, the scrollable
|
||||
* body content (.wm-side-body), and the foot links. */
|
||||
.wm-side {
|
||||
@apply flex flex-col border-r border-app-border bg-app-bg;
|
||||
height: 100%;
|
||||
font-family: "Lato", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.wm-side-head {
|
||||
@apply flex items-center justify-between pt-3 px-3 pb-1;
|
||||
}
|
||||
.wm-side-switch {
|
||||
@apply relative;
|
||||
}
|
||||
.wm-side-switch > button {
|
||||
@apply flex items-center gap-1.5 rounded px-1.5 py-1 text-[15px] font-semibold text-app-fg hover:bg-app-hover;
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.wm-side-menu {
|
||||
@apply absolute left-0 top-full z-20 mt-1 min-w-[160px] rounded-lg border border-app-border bg-app-elevated py-1 shadow-lg;
|
||||
}
|
||||
.wm-side-menu button {
|
||||
@apply flex w-full items-center px-3 py-1.5 text-left text-sm text-app-muted hover:bg-app-hover;
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.wm-side-headicons {
|
||||
@apply flex items-center gap-1;
|
||||
}
|
||||
.wm-side-iconbtn {
|
||||
@apply inline-flex rounded p-1.5 text-app-muted hover:bg-app-hover hover:text-app-fg;
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.wm-side-themebtn {
|
||||
@apply p-2;
|
||||
}
|
||||
.wm-side-primary {
|
||||
@apply px-3 pb-2;
|
||||
}
|
||||
.wm-side-primary > button {
|
||||
@apply flex w-full items-center justify-center gap-2 rounded-lg bg-app-accent py-2 text-[15px] font-medium text-app-accent-fg shadow-sm hover:opacity-90;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.wm-side-body {
|
||||
@apply flex-1 overflow-y-auto px-2 py-1 text-[15px];
|
||||
min-height: 0;
|
||||
}
|
||||
.wm-side-section {
|
||||
@apply px-3 pt-3 pb-1 text-[12px] font-semibold uppercase tracking-wide text-app-faint;
|
||||
}
|
||||
.wm-side-nav {
|
||||
@apply flex flex-col;
|
||||
gap: 2px;
|
||||
}
|
||||
.wm-side-item {
|
||||
@apply flex w-full items-center gap-1.5 rounded px-2.5 py-1 text-left text-app-fg hover:bg-app-hover hover:text-app-fg;
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.wm-side-foot {
|
||||
@apply border-t border-app-border p-2 text-[15px];
|
||||
}
|
||||
.wm-side-foot button,
|
||||
.wm-side-foot a {
|
||||
@apply flex w-full items-center gap-2.5 rounded-lg py-2 px-3 text-app-muted hover:bg-app-hover hover:text-app-fg;
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
}
|
||||
.wm-side-quota {
|
||||
@apply mt-1 border-t border-app-border px-3 pt-2 pb-1 text-[13px] text-app-faint;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user