diff --git a/apps/mana/apps/web/src/hooks.server.ts b/apps/mana/apps/web/src/hooks.server.ts index 906453ecf..ff0316bce 100644 --- a/apps/mana/apps/web/src/hooks.server.ts +++ b/apps/mana/apps/web/src/hooks.server.ts @@ -76,10 +76,30 @@ const APP_SUBDOMAINS = new Set([ ]); export const handle: Handle = async ({ event, resolve }) => { + // Force HTTPS in production. Cloudflare forwards HTTP requests to the + // origin without rewriting the scheme, so without this redirect a user + // who types `mana.how` (no scheme → HTTP default) loads the page over + // HTTP and the browser then sends `Origin: http://mana.how` on every + // fetch. mana-auth's CORS only allows `https://mana.how`, so all auth + // requests fail and the loader hangs forever. + const host = event.request.headers.get('host') || ''; + const cfVisitor = event.request.headers.get('cf-visitor'); // {"scheme":"http"|"https"} + const xfProto = event.request.headers.get('x-forwarded-proto'); + const isHttp = + (cfVisitor && cfVisitor.includes('"scheme":"http"')) || + xfProto === 'http' || + (event.url.protocol === 'http:' && !cfVisitor && !xfProto); + const isLocal = host.startsWith('localhost') || host.startsWith('127.'); + if (isHttp && !isLocal) { + return new Response(null, { + status: 301, + headers: { Location: `https://${host}${event.url.pathname}${event.url.search}` }, + }); + } + // Redirect app subdomains to their path equivalent // e.g. todo.mana.how → mana.how/todo - const host = event.request.headers.get('host') || ''; - const subdomain = host.split('.')[0]; + const subdomain = host.split('.')[0]; // host already declared above if (APP_SUBDOMAINS.has(subdomain) && event.url.pathname === '/') { return new Response(null, { status: 302, diff --git a/apps/mana/apps/web/src/lib/components/page-carousel/PageShell.svelte b/apps/mana/apps/web/src/lib/components/page-carousel/PageShell.svelte index 70227d77d..2792cde93 100644 --- a/apps/mana/apps/web/src/lib/components/page-carousel/PageShell.svelte +++ b/apps/mana/apps/web/src/lib/components/page-carousel/PageShell.svelte @@ -250,6 +250,38 @@ animation: fadeIn 0.25s ease-out; overflow: hidden; position: relative; + /* Establish a blend-mode stacking context so the grain overlay + only blends within this card, not through to the workbench + background behind it. */ + isolation: isolate; + } + + /* Per-theme paper-grain overlay. CSS variables come from + applyThemeToDocument() in @mana/shared-theme — swap one line in + THEME_DEFINITIONS to change the texture for a whole theme. */ + .page-shell::before { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + z-index: 0; + background-image: var(--paper-texture, none); + background-size: var(--paper-size, 240px 240px); + background-repeat: repeat; + mix-blend-mode: var(--paper-blend-mode, multiply); + opacity: var(--paper-opacity, 0); + transition: opacity 0.25s ease; + } + /* Make sure the header, body and resize handle sit above the grain */ + .page-shell > * { + position: relative; + z-index: 1; + } + /* A11y: let users who asked for reduced transparency/contrast opt out */ + @media (prefers-contrast: more) { + .page-shell::before { + opacity: 0; + } } .page-shell.resizing { box-shadow: diff --git a/apps/mana/apps/web/static/textures/paper/LICENSE.txt b/apps/mana/apps/web/static/textures/paper/LICENSE.txt new file mode 100644 index 000000000..4a4968914 --- /dev/null +++ b/apps/mana/apps/web/static/textures/paper/LICENSE.txt @@ -0,0 +1,20 @@ +Paper textures used for the per-theme workbench paper-grain overlay. + +Source: ambientCG (https://ambientcg.com) +License: CC0 1.0 Universal (Public Domain) + https://creativecommons.org/publicdomain/zero/1.0/ + +No attribution required for commercial or private use. The original 1K +PBR material sets were downloaded from ambientCG, only the diffuse / +Color map was extracted, then downscaled to 512×512 via `sips` and +re-encoded as JPEG (quality 70) to keep web payloads tiny. + +Original asset → local filename: + Paper001 → paper-001.jpg + Paper002 → paper-002.jpg + Paper003 → paper-003.jpg + Paper004 → paper-004.jpg + Paper005 → paper-005.jpg + Paper006 → paper-006.jpg + Cardboard001 → cardboard-001.jpg + Cardboard002 → cardboard-002.jpg diff --git a/apps/mana/apps/web/static/textures/paper/cardboard-001.jpg b/apps/mana/apps/web/static/textures/paper/cardboard-001.jpg new file mode 100644 index 000000000..97adf03a5 Binary files /dev/null and b/apps/mana/apps/web/static/textures/paper/cardboard-001.jpg differ diff --git a/apps/mana/apps/web/static/textures/paper/cardboard-002.jpg b/apps/mana/apps/web/static/textures/paper/cardboard-002.jpg new file mode 100644 index 000000000..6ea259898 Binary files /dev/null and b/apps/mana/apps/web/static/textures/paper/cardboard-002.jpg differ diff --git a/apps/mana/apps/web/static/textures/paper/paper-001.jpg b/apps/mana/apps/web/static/textures/paper/paper-001.jpg new file mode 100644 index 000000000..7f0f70496 Binary files /dev/null and b/apps/mana/apps/web/static/textures/paper/paper-001.jpg differ diff --git a/apps/mana/apps/web/static/textures/paper/paper-002.jpg b/apps/mana/apps/web/static/textures/paper/paper-002.jpg new file mode 100644 index 000000000..088260305 Binary files /dev/null and b/apps/mana/apps/web/static/textures/paper/paper-002.jpg differ diff --git a/apps/mana/apps/web/static/textures/paper/paper-003.jpg b/apps/mana/apps/web/static/textures/paper/paper-003.jpg new file mode 100644 index 000000000..bb13072c4 Binary files /dev/null and b/apps/mana/apps/web/static/textures/paper/paper-003.jpg differ diff --git a/apps/mana/apps/web/static/textures/paper/paper-004.jpg b/apps/mana/apps/web/static/textures/paper/paper-004.jpg new file mode 100644 index 000000000..f96169249 Binary files /dev/null and b/apps/mana/apps/web/static/textures/paper/paper-004.jpg differ diff --git a/apps/mana/apps/web/static/textures/paper/paper-005.jpg b/apps/mana/apps/web/static/textures/paper/paper-005.jpg new file mode 100644 index 000000000..278a67195 Binary files /dev/null and b/apps/mana/apps/web/static/textures/paper/paper-005.jpg differ diff --git a/apps/mana/apps/web/static/textures/paper/paper-006.jpg b/apps/mana/apps/web/static/textures/paper/paper-006.jpg new file mode 100644 index 000000000..097d97a4f Binary files /dev/null and b/apps/mana/apps/web/static/textures/paper/paper-006.jpg differ diff --git a/packages/shared-theme/src/constants.ts b/packages/shared-theme/src/constants.ts index d3fb7ab2c..bd09277b6 100644 --- a/packages/shared-theme/src/constants.ts +++ b/packages/shared-theme/src/constants.ts @@ -372,6 +372,12 @@ const lavenderDark: ThemeColors = { /** * Complete theme variant definitions + * + * Each theme can also carry a `paper` descriptor — a tileable texture + * the workbench page-shell uses as a grain overlay. Swap the filename + * here to change the texture for a whole theme in one place. Assets + * live under `apps//static/textures/paper/` and are CC0-licensed + * (see that directory's LICENSE.txt for provenance). */ export const THEME_DEFINITIONS: Record = { lume: { @@ -382,6 +388,12 @@ export const THEME_DEFINITIONS: Record = { hue: 47, light: lumeLight, dark: lumeDark, + paper: { + url: '/textures/paper/paper-001.jpg', + blendMode: 'multiply', + opacityLight: 0.4, + opacityDark: 0.18, + }, }, nature: { name: 'nature', @@ -391,6 +403,12 @@ export const THEME_DEFINITIONS: Record = { hue: 122, light: natureLight, dark: natureDark, + paper: { + url: '/textures/paper/cardboard-001.jpg', + blendMode: 'multiply', + opacityLight: 0.32, + opacityDark: 0.14, + }, }, stone: { name: 'stone', @@ -400,6 +418,12 @@ export const THEME_DEFINITIONS: Record = { hue: 200, light: stoneLight, dark: stoneDark, + paper: { + url: '/textures/paper/paper-005.jpg', + blendMode: 'multiply', + opacityLight: 0.35, + opacityDark: 0.15, + }, }, ocean: { name: 'ocean', @@ -409,6 +433,12 @@ export const THEME_DEFINITIONS: Record = { hue: 199, light: oceanLight, dark: oceanDark, + paper: { + url: '/textures/paper/paper-003.jpg', + blendMode: 'multiply', + opacityLight: 0.3, + opacityDark: 0.12, + }, }, // Extended themes (not in PillNav by default, can be pinned) sunset: { @@ -419,6 +449,12 @@ export const THEME_DEFINITIONS: Record = { hue: 15, light: sunsetLight, dark: sunsetDark, + paper: { + url: '/textures/paper/paper-006.jpg', + blendMode: 'multiply', + opacityLight: 0.38, + opacityDark: 0.16, + }, }, midnight: { name: 'midnight', @@ -428,6 +464,12 @@ export const THEME_DEFINITIONS: Record = { hue: 260, light: midnightLight, dark: midnightDark, + paper: { + url: '/textures/paper/paper-004.jpg', + blendMode: 'overlay', + opacityLight: 0.35, + opacityDark: 0.22, + }, }, rose: { name: 'rose', @@ -437,6 +479,12 @@ export const THEME_DEFINITIONS: Record = { hue: 340, light: roseLight, dark: roseDark, + paper: { + url: '/textures/paper/paper-002.jpg', + blendMode: 'multiply', + opacityLight: 0.32, + opacityDark: 0.14, + }, }, lavender: { name: 'lavender', @@ -446,6 +494,12 @@ export const THEME_DEFINITIONS: Record = { hue: 270, light: lavenderLight, dark: lavenderDark, + paper: { + url: '/textures/paper/cardboard-002.jpg', + blendMode: 'soft-light', + opacityLight: 0.4, + opacityDark: 0.18, + }, }, }; diff --git a/packages/shared-theme/src/types.ts b/packages/shared-theme/src/types.ts index 3103b6e06..4235d87b1 100644 --- a/packages/shared-theme/src/types.ts +++ b/packages/shared-theme/src/types.ts @@ -104,6 +104,33 @@ export interface ThemeVariantDefinition { hue: number; light: ThemeColors; dark: ThemeColors; + /** + * Optional "paper grain" overlay for workbench page surfaces. + * Each theme can ship its own tileable texture to give pages a + * distinct tactile character. The consuming app is responsible for + * serving the asset at the given URL (typically under `/textures/`). + * When undefined, no paper overlay is applied for this theme. + */ + paper?: { + /** URL / absolute path to a seamless tileable texture */ + url: string; + /** CSS blend-mode for the overlay vs. the underlying card bg */ + blendMode?: + | 'multiply' + | 'overlay' + | 'soft-light' + | 'hard-light' + | 'screen' + | 'darken' + | 'lighten' + | 'color-burn'; + /** Opacity of the paper layer in light mode (0..1, default 0.35) */ + opacityLight?: number; + /** Opacity of the paper layer in dark mode (0..1, default 0.15) */ + opacityDark?: number; + /** CSS background-size (default "240px 240px") */ + size?: string; + }; } /** diff --git a/packages/shared-theme/src/utils.ts b/packages/shared-theme/src/utils.ts index 94abff341..87b83ea54 100644 --- a/packages/shared-theme/src/utils.ts +++ b/packages/shared-theme/src/utils.ts @@ -112,6 +112,25 @@ export function applyThemeToDocument( root.style.setProperty(key, value); }); + // Set per-theme paper-grain CSS variables (consumed by PageShell). + // Unset the vars for themes without a paper config so they don't + // leak across theme switches. + const paper = THEME_DEFINITIONS[variant]?.paper; + if (paper) { + root.style.setProperty('--paper-texture', `url("${paper.url}")`); + root.style.setProperty('--paper-blend-mode', paper.blendMode ?? 'multiply'); + root.style.setProperty( + '--paper-opacity', + String(effectiveMode === 'dark' ? (paper.opacityDark ?? 0.15) : (paper.opacityLight ?? 0.35)) + ); + root.style.setProperty('--paper-size', paper.size ?? '240px 240px'); + } else { + root.style.removeProperty('--paper-texture'); + root.style.removeProperty('--paper-blend-mode'); + root.style.removeProperty('--paper-opacity'); + root.style.removeProperty('--paper-size'); + } + // Set data-theme attribute root.setAttribute('data-theme', variant);