a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

build: setup demo build

+108 -15
+1
.gitignore
··· 10 10 node_modules 11 11 dist 12 12 dist-ssr 13 + dist-demo 13 14 *.local 14 15 15 16 # Editor directories and files
+3
lib/package.json
··· 28 28 "build:css:min": "postcss src/styles/index.css -o dist/voltx.min.css --env production", 29 29 "build:finalize": "node scripts/build-finalize.js", 30 30 "build:jsr": "pnpm build:lib && pnpm build:lib:min", 31 + "build:demo": "vite build --mode demo", 32 + "build:demo:min": "vite build --mode demo:min", 31 33 "preview": "vite preview", 34 + "preview:demo": "vite preview --outDir dist-demo", 32 35 "test": "vitest", 33 36 "test:run": "vitest run", 34 37 "coverage": "vitest run --coverage",
+21 -5
lib/src/demo/index.ts
··· 6 6 7 7 import { charge } from "$core/charge"; 8 8 import { isSignal } from "$core/shared"; 9 - import { initNavigationListener } from "$plugins/navigate"; 9 + import { initNavigationListener, setRouterMode } from "$plugins/navigate"; 10 10 import { persistPlugin } from "$plugins/persist"; 11 11 import { scrollPlugin } from "$plugins/scroll"; 12 12 import { shiftPlugin } from "$plugins/shift"; ··· 22 22 import { createPluginsSection } from "./sections/plugins"; 23 23 import { createReactivitySection } from "./sections/reactivity"; 24 24 import * as dom from "./utils"; 25 + 26 + setRouterMode(import.meta.env.DEV ? "history" : "hash"); 25 27 26 28 registerPlugin("persist", persistPlugin); 27 29 registerPlugin("scroll", scrollPlugin); ··· 88 90 ); 89 91 90 92 /** 91 - * Get the current page from the URL pathname 93 + * Get the current page from the URL (supports both hash and history routing) 92 94 */ 93 95 function getCurrentPageFromPath(): string { 96 + if (globalThis.location.hash) { 97 + const hash = globalThis.location.hash.slice(1); 98 + if (hash === "/" || hash === "") return "home"; 99 + return hash.slice(1); 100 + } 101 + 94 102 const path = globalThis.location.pathname; 95 103 if (path === "/" || path === "") return "home"; 96 104 return path.slice(1); ··· 197 205 return; 198 206 } 199 207 200 - // Add helper functions to scope (not serializable, so added after charge) 201 208 rootScope.$helpers = helpers; 202 209 203 210 const handleNavigate = (event: Event) => { 204 211 const customEvent = event as CustomEvent; 205 - const url = customEvent.detail?.url || globalThis.location.pathname; 206 - const page = url === "/" || url === "" ? "home" : url.slice(1); 212 + const url = customEvent.detail?.url || ""; 213 + 214 + let page = "home"; 215 + if (url.startsWith("#")) { 216 + const hash = url.slice(1); 217 + page = hash === "/" || hash === "" ? "home" : hash.slice(1); 218 + } else if (url) { 219 + page = url === "/" || url === "" ? "home" : url.slice(1); 220 + } else { 221 + page = getCurrentPageFromPath(); 222 + } 207 223 208 224 const currentPageSignal = rootScope.currentPage as Signal<string>; 209 225 if (currentPageSignal && isSignal(currentPageSignal)) {
+1 -1
lib/src/index.ts
··· 53 53 supportsViewTransitions, 54 54 withViewTransition, 55 55 } from "$core/view-transitions"; 56 - export { goBack, goForward, initNavigationListener, navigate, redirect } from "$plugins/navigate"; 56 + export { goBack, goForward, getRouterMode, initNavigationListener, navigate, redirect, setRouterMode } from "$plugins/navigate"; 57 57 export { persistPlugin, registerStorageAdapter } from "$plugins/persist"; 58 58 export { scrollPlugin } from "$plugins/scroll"; 59 59 export {
+72 -9
lib/src/plugins/navigate.ts
··· 14 14 15 15 type NavigationOpts = { replace?: boolean; transition?: boolean; transitionName?: string }; 16 16 17 + type RouterMode = "history" | "hash"; 18 + 17 19 const scrollPositions = new Map<string, { x: number; y: number }>(); 18 20 const focusSelectors = new Map<string, string>(); 19 21 20 22 /** 23 + * Current router mode (history or hash) 24 + * Defaults to history mode for backwards compatibility 25 + */ 26 + let currentRouterMode: RouterMode = "history"; 27 + 28 + /** 29 + * Set the router mode for navigation 30 + * 31 + * @param mode - Router mode to use ("history" or "hash") 32 + * 33 + * @example 34 + * ```typescript 35 + * import { setRouterMode } from 'voltx.js'; 36 + * 37 + * // Use hash routing in production 38 + * setRouterMode(import.meta.env.DEV ? 'history' : 'hash'); 39 + * ``` 40 + */ 41 + export function setRouterMode(mode: RouterMode): void { 42 + currentRouterMode = mode; 43 + } 44 + 45 + /** 46 + * Get the current router mode 47 + */ 48 + export function getRouterMode(): RouterMode { 49 + return currentRouterMode; 50 + } 51 + 52 + /** 53 + * Get the current location key for storing scroll positions 54 + */ 55 + function getLocationKey(): string { 56 + if (currentRouterMode === "hash") { 57 + return globalThis.location.hash.slice(1) || "/"; 58 + } 59 + return `${globalThis.location.pathname}${globalThis.location.search}`; 60 + } 61 + 62 + /** 21 63 * Navigate directive handler for client-side navigation 22 64 * 23 65 * Syntax: data-volt-navigate[.modifiers]="url" or data-volt-navigate[.modifiers] (uses href) ··· 110 152 111 153 async function navigateTo(url: string, options: NavigationOpts = {}): Promise<void> { 112 154 const { replace = false, transition = true, transitionName = "page-transition" } = options; 113 - const currentKey = `${globalThis.location.pathname}${globalThis.location.search}`; 155 + const currentKey = getLocationKey(); 114 156 scrollPositions.set(currentKey, { x: window.scrollX, y: window.scrollY }); 115 157 116 158 const activeElement = document.activeElement; ··· 128 170 }; 129 171 130 172 const performNavigation = async () => { 131 - if (replace) { 132 - globalThis.history.replaceState(state, "", url); 173 + if (currentRouterMode === "hash") { 174 + const hash = url.startsWith("#") ? url : `#${url}`; 175 + if (replace) { 176 + globalThis.history.replaceState(state, "", hash); 177 + } else { 178 + globalThis.location.hash = hash; 179 + } 133 180 } else { 134 - globalThis.history.pushState(state, "", url); 181 + if (replace) { 182 + globalThis.history.replaceState(state, "", url); 183 + } else { 184 + globalThis.history.pushState(state, "", url); 185 + } 135 186 } 136 187 137 188 globalThis.dispatchEvent( ··· 278 329 } 279 330 280 331 /** 281 - * Initialize popstate listener for back/forward navigation 332 + * Initialize navigation listeners for back/forward navigation 282 333 * Should be called once on app initialization 334 + * Handles both popstate (history mode) and hashchange (hash mode) events 283 335 */ 284 336 export function initNavigationListener(): () => void { 285 - const handlePopState = (event: PopStateEvent) => { 286 - const state = event.state as NavigationState | null; 287 - 288 - const key = `${globalThis.location.pathname}${globalThis.location.search}`; 337 + const handleNavigation = (state: NavigationState | null = null) => { 338 + const key = getLocationKey(); 289 339 const savedPosition = scrollPositions.get(key); 290 340 const savedFocus = focusSelectors.get(key); 291 341 ··· 306 356 globalThis.dispatchEvent(new CustomEvent("volt:popstate", { detail: { state }, bubbles: true, cancelable: false })); 307 357 }; 308 358 359 + const handlePopState = (event: PopStateEvent) => { 360 + const state = event.state as NavigationState | null; 361 + handleNavigation(state); 362 + }; 363 + 364 + const handleHashChange = () => { 365 + if (currentRouterMode === "hash") { 366 + handleNavigation(); 367 + } 368 + }; 369 + 309 370 globalThis.addEventListener("popstate", handlePopState); 371 + globalThis.addEventListener("hashchange", handleHashChange); 310 372 311 373 return () => { 312 374 globalThis.removeEventListener("popstate", handlePopState); 375 + globalThis.removeEventListener("hashchange", handleHashChange); 313 376 }; 314 377 } 315 378
+9
lib/vite.config.ts
··· 24 24 const buildOptions = (mode: string): BuildEnvironmentOptions => { 25 25 const [baseMode, ...flags] = mode.split(":"); 26 26 const isLibBuild = baseMode === "lib"; 27 + const isDemoBuild = baseMode === "demo"; 27 28 const shouldMinify = flags.includes("min"); 28 29 const target = flags.find((flag) => flag !== "min") ?? "all"; 30 + 31 + if (isDemoBuild) { 32 + return { 33 + minify: shouldMinify ? "oxc" : false, 34 + outDir: path.resolve(__dirname, "dist-demo"), 35 + rollupOptions: { input: path.resolve(__dirname, "index.html") }, 36 + }; 37 + } 29 38 30 39 if (!isLibBuild) return { minify: shouldMinify ? "oxc" : false }; 31 40
+1
package.json
··· 6 6 "scripts": { 7 7 "dev": "pnpm --filter voltx.js dev", 8 8 "build": "pnpm -r build", 9 + "build:demo": "pnpm --filter voltx.js build:demo", 9 10 "preview": "pnpm --filter voltx.js preview", 10 11 "test": "pnpm -r test", 11 12 "test:ui": "pnpm --filter voltx.js test:ui",