--- // Offcanvas behavior script lives in BaseLayout (after global scripts). import FormText from '@ui/FormText.astro' import Icon from '@ui/Icon.astro' import Illustration from '@ui/Illustration.astro' import Subheader from '@ui/Subheader.astro' import { site } from '@shared/lib/site' import { themeDefaults, themeSections, type ThemeKey, type ThemePreset } from '@shared/lib/theme-config' const lastSection = themeSections.length - 1 // Thumbnails, drawn as inline SVG on the theme's own custom properties so they // follow the color mode, the gray base and the accent color. The preset tiles // pin light and dark on purpose (they depict a mode); the layout tiles follow // the page. All share one 96×64 frame. type Mode = 'light' | 'dark' const shade: Record = { light: 'var(--tblr-gray-50)', dark: 'var(--tblr-gray-900)' } const bar: Record = { light: 'var(--tblr-white)', dark: 'var(--tblr-gray-800)', primary: 'var(--tblr-primary)' } const lineOn: Record = { light: 'var(--tblr-gray-300)', dark: 'var(--tblr-gray-700)' } // mode-aware palette for the layout tiles const page = 'var(--tblr-bg-surface-secondary)' const surface = 'var(--tblr-bg-surface)' const border = 'var(--tblr-border-color)' const primary = 'var(--tblr-primary)' const svg = (inner: string, attrs = '') => `` const frame = (fill: string) => `` const outline = `` const topBar = (fill: string) => `` const rail = (w: number, fill: string) => `` const lines = (x: number, y: number, widths: number[], fill: string, step = 11) => widths.map((w, i) => ``).join('') const dots = (x: number, y: number, count: number, fill: string) => Array.from({ length: count }, (_, i) => ``).join('') const pageHalf = (mode: Mode, right: boolean) => (right ? `` : ``) const barHalf = (fill: string, right: boolean) => (right ? `` : ``) /** A preset tile: page halves and navigation bar halves in the preset's modes. */ const presetSvg = (values: Partial>) => { const theme = values.theme ?? 'light' const nav = values['navbar-theme'] ?? 'default' const [first, second]: [Mode, Mode] = theme === 'auto' ? ['light', 'dark'] : [theme as Mode, theme as Mode] const barFill = (mode: Mode) => (nav === 'primary' ? bar.primary : nav === 'dark' ? bar.dark : bar[mode]) return svg(pageHalf(first, false) + pageHalf(second, true) + barHalf(barFill(first), false) + barHalf(barFill(second), true) + lines(12, 30, [46, 72, 30], lineOn[first]) + outline) } /** Marks drawn inside an accent-colored navigation shape. */ const onAccent = 'rgba(255, 255, 255, 0.7)' /** A content card: the page's own surface with a border, lines inside. */ const card = (x: number, y: number, w: number, h: number) => `` + lines(x + 6, y + 7, [w * 0.6, w * 0.8, w * 0.4].map(Math.round), border, 9) /** A scrollbar thumb on the right edge, at the top or scrolled down. */ const thumb = (y: number) => `` /** A tile for one value of one setting. */ const tileSvg = (key: string, value: string): string => { switch (key) { case 'navbar-position': // The navigation is the accent-colored shape: a bar across the top or a rail down the start side. return value === 'vertical' ? svg(frame(page) + rail(30, primary) + lines(7, 10, [16, 16, 16, 16], onAccent, 10) + lines(40, 12, [40, 48, 28], border) + outline) : svg(frame(page) + topBar(primary) + [8, 26, 44].map((x) => ``).join('') + lines(12, 30, [46, 72, 30], border) + outline) case 'layout': // Only the content width changes, so the navigation stays out of these tiles. // The surround is the inverted surface, so it stays dark on a light page and light on a dark one. if (value === 'boxed') return svg(`` + card(18, 16, 60, 32) + outline) return value === 'fluid' ? svg(frame(page) + card(4, 8, 88, 48) + outline) : svg(frame(page) + card(22, 8, 52, 48) + outline) case 'navbar': // Sticky: the bar keeps its place while the content has scrolled up behind it. return value === 'sticky' ? svg(frame(page) + lines(12, 4, [46, 72, 30, 60], border) + thumb(44) + `` + topBar(primary) + outline) : svg(frame(page) + topBar(primary) + lines(12, 30, [46, 72, 30], border) + thumb(24) + outline) case 'navbar-style': // Floating: the bar keeps a gap from the edges and gets its own corners. return value === 'floating' ? svg(frame(page) + `` + [11, 29, 47].map((x) => ``).join('') + lines(12, 30, [46, 72, 30], border) + outline) : svg(frame(page) + topBar(primary) + [8, 26, 44].map((x) => ``).join('') + lines(12, 30, [46, 72, 30], border) + outline) case 'offcanvas-style': // A panel open on the end side, over the dimmed page: flush with the edges, or a card with a gap. return value === 'floating' ? svg(frame(page) + lines(10, 12, [40, 48, 28], border) + `` + lines(59, 12, [20, 26, 14], border) + outline) : svg(frame(page) + lines(10, 12, [40, 48, 28], border) + `` + lines(64, 12, [20, 26, 14], border) + outline) case 'sidebar': if (value === 'folded') return svg(frame(page) + rail(16, primary) + dots(8, 12, 4, onAccent) + lines(26, 12, [50, 60, 34], border) + outline) if (value === 'folded-hover') return svg(frame(page) + rail(16, primary) + dots(8, 12, 4, onAccent) + lines(26, 12, [50, 60, 34], border) + `` + lines(21, 9.5, [18, 18, 18, 18], border, 11) + outline) return svg(frame(page) + rail(30, primary) + lines(7, 10, [16, 16, 16, 16], onAccent, 10) + lines(40, 12, [40, 48, 28], border) + outline) case 'theme-base': // The base's own gray scale: the attribute swaps the --tblr-gray-* values for this tile only. return svg(frame(surface) + [100, 300, 500, 700, 900].map((shadeValue, i) => ``).join(''), ` data-bs-theme-base="${value}"`) case 'theme-radius': { // One shape carries the whole message: a square at 0, a circle at 2. const r = Number(value) * 10 return svg(frame(page) + ``) } default: return svg(frame(page) + outline) } } ---