kempo-server 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -238,6 +238,7 @@ kempo-server --root public --config dev.config.json
238
238
  - **Configurable** - Customize behavior with a simple JSON config file
239
239
  - **Security** - Built-in protection against serving sensitive files plus security headers middleware
240
240
  - **Performance** - Smart file system caching, rescan optimization, and optional compression
241
+ - **Programmatic Rescan** - Trigger a file rescan from anywhere in the Node process without restarting
241
242
 
242
243
  ## Examples
243
244
 
@@ -313,6 +314,30 @@ export default async function(request, response) {
313
314
  }
314
315
  ```
315
316
 
317
+ ## Programmatic File Rescan
318
+
319
+ When files are added or removed at runtime (e.g., by a CMS generating static pages), you can trigger a file rescan without restarting the server:
320
+
321
+ ```javascript
322
+ import rescan from 'kempo-server/rescan';
323
+
324
+ // Returns a promise that resolves with the new file count
325
+ const fileCount = await rescan();
326
+ ```
327
+
328
+ This works from anywhere in the same Node process — route handlers, middleware, background tasks, file watchers, or any other code running alongside the server.
329
+
330
+ ```javascript
331
+ // Example: CMS generates a page and makes it immediately available
332
+ import { writeFile } from 'fs/promises';
333
+ import rescan from 'kempo-server/rescan';
334
+
335
+ const html = buildPage(theme, template, content);
336
+ await writeFile('./public/new-page.html', html);
337
+ await rescan();
338
+ // New page is now live
339
+ ```
340
+
316
341
  ## Command Line Options
317
342
 
318
343
  Kempo Server supports several command line options to customize its behavior:
@@ -367,6 +392,7 @@ See **[SPA.md](./SPA.md)** for a full walkthrough.
367
392
  - **[Caching](./docs/caching.html)** - Cache configuration and management
368
393
  - **[CLI Utilities](./docs/cli-utils.html)** - Command-line argument parsing
369
394
  - **[File System Utilities](./docs/fs-utils.html)** - File and directory operations
395
+ - **[UTILS.md](./UTILS.md)** - Utility modules including rescan, CLI, and file system helpers
370
396
  - **[Examples](./docs/examples.html)** - Interactive examples and demos
371
397
  - **[CONFIG.md](./CONFIG.md)** - Comprehensive server configuration guide
372
398
  - **[UTILS.md](./UTILS.md)** - Utility modules for Node.js projects
package/UTILS.md CHANGED
@@ -111,6 +111,44 @@ async function backupProject() {
111
111
  }
112
112
  ```
113
113
 
114
+ ## Rescan
115
+
116
+ The rescan utility lets you programmatically trigger a file rescan on the running server without restarting. This is useful when files are created or removed at runtime, such as a CMS generating static HTML pages.
117
+
118
+ ### Usage
119
+
120
+ ```javascript
121
+ import rescan from 'kempo-server/rescan';
122
+
123
+ const fileCount = await rescan();
124
+ console.log(`Server now serving ${fileCount} files`);
125
+ ```
126
+
127
+ The function returns a promise that resolves with the total number of files found in the new scan. It works from anywhere in the same Node process as the server — route handlers, middleware, scheduled tasks, file watchers, or any other code.
128
+
129
+ ### Example: CMS Page Generation
130
+
131
+ ```javascript
132
+ import { writeFile } from 'fs/promises';
133
+ import rescan from 'kempo-server/rescan';
134
+
135
+ async function publishPage(theme, template, content, slug) {
136
+ const html = buildPage(theme, template, content);
137
+ await writeFile(`./public/${slug}.html`, html);
138
+ await rescan();
139
+ // Page is immediately available at /{slug}
140
+ }
141
+ ```
142
+
143
+ ### Example: File Watcher
144
+
145
+ ```javascript
146
+ import { watch } from 'fs';
147
+ import rescan from 'kempo-server/rescan';
148
+
149
+ watch('./content', { recursive: true }, () => rescan());
150
+ ```
151
+
114
152
  ## Installation and Build
115
153
 
116
154
  These utilities are automatically built when you install kempo-server. They're available through the package's exports:
@@ -118,6 +156,7 @@ These utilities are automatically built when you install kempo-server. They're a
118
156
  ```json
119
157
  {
120
158
  "exports": {
159
+ "./rescan": "./dist/rescan.js",
121
160
  "./utils/cli": "./dist/utils/cli.js",
122
161
  "./utils/fs-utils": "./dist/utils/fs-utils.js"
123
162
  }
package/dist/rescan.js ADDED
@@ -0,0 +1 @@
1
+ import{EventEmitter}from"events";const emitter=new EventEmitter;export const onRescan=callback=>{emitter.on("rescan",callback)};export default()=>new Promise((resolve,reject)=>{emitter.emit("rescan",(error,fileCount)=>{error?reject(error):resolve(fileCount)})});
package/dist/router.js CHANGED
@@ -1 +1 @@
1
- import path from"path";import{readFile,stat,readdir}from"fs/promises";import{pathToFileURL}from"url";import defaultConfig from"./defaultConfig.js";import getFiles from"./getFiles.js";import findFile from"./findFile.js";import serveFile from"./serveFile.js";import MiddlewareRunner from"./middlewareRunner.js";import ModuleCache from"./moduleCache.js";import createRequestWrapper,{readRawBody,parseBody}from"./requestWrapper.js";import createResponseWrapper from"./responseWrapper.js";import{corsMiddleware,compressionMiddleware,rateLimitMiddleware,securityMiddleware,loggingMiddleware}from"./builtinMiddleware.js";export default async(flags,log)=>{log("Initializing router",3);const rootPath=path.isAbsolute(flags.root)?flags.root:path.join(process.cwd(),flags.root);log(`Root path: ${rootPath}`,3);let config=defaultConfig;try{const configFileName=flags.config||".config.json",configPath=path.isAbsolute(configFileName)?configFileName:path.join(rootPath,configFileName);if(log(`Config file name: ${configFileName}`,3),log(`Config path: ${configPath}`,3),path.isAbsolute(configFileName))log("Config file name is absolute, skipping validation",4);else{const relativeConfigPath=path.relative(rootPath,configPath);if(log(`Relative config path: ${relativeConfigPath}`,4),log(`Starts with '..': ${relativeConfigPath.startsWith("..")}`,4),relativeConfigPath.startsWith("..")||path.isAbsolute(relativeConfigPath))throw log("Validation failed - throwing error",4),new Error(`Config file must be within the server root directory. Config path: ${configPath}, Root path: ${rootPath}`);log("Validation passed",4)}log(`Loading config from: ${configPath}`,3);const configContent=await readFile(configPath,"utf8"),userConfig=JSON.parse(configContent);config={...defaultConfig,...userConfig,allowedMimes:{...defaultConfig.allowedMimes,...userConfig.allowedMimes||{}},middleware:{...defaultConfig.middleware,...userConfig.middleware||{}},customRoutes:{...defaultConfig.customRoutes,...userConfig.customRoutes||{}},cache:{...defaultConfig.cache,...userConfig.cache||{}}},log("User config loaded and merged with defaults",3)}catch(e){if(e.message.includes("Config file must be within the server root directory"))throw e;log("Using default config (no config file found)",3)}const dis=new Set(config.disallowedRegex);dis.add("^/\\..*"),dis.add("\\.config$"),dis.add("\\.git/"),config.disallowedRegex=[...dis],log(`Config loaded with ${config.disallowedRegex.length} disallowed patterns`,3);let files=await getFiles(rootPath,config,log);log(`Initial scan found ${files.length} files`,2);const middlewareRunner=new MiddlewareRunner;if(config.middleware?.cors?.enabled&&(middlewareRunner.use(corsMiddleware(config.middleware.cors)),log("CORS middleware enabled",3)),config.middleware?.compression?.enabled&&(middlewareRunner.use(compressionMiddleware(config.middleware.compression)),log("Compression middleware enabled",3)),config.middleware?.rateLimit?.enabled&&(middlewareRunner.use(rateLimitMiddleware(config.middleware.rateLimit)),log("Rate limit middleware enabled",3)),config.middleware?.security?.enabled&&(middlewareRunner.use(securityMiddleware(config.middleware.security)),log("Security middleware enabled",3)),config.middleware?.logging?.enabled&&(middlewareRunner.use(loggingMiddleware(config.middleware.logging,log)),log("Logging middleware enabled",3)),config.middleware?.custom&&config.middleware.custom.length>0){log(`Loading ${config.middleware.custom.length} custom middleware files`,3);for(const middlewarePath of config.middleware.custom)try{const resolvedPath=path.isAbsolute(middlewarePath)?middlewarePath:path.resolve(rootPath,middlewarePath),middlewareUrl=pathToFileURL(resolvedPath).href+`?t=${Date.now()}`,customMiddleware=(await import(middlewareUrl)).default;"function"==typeof customMiddleware?(middlewareRunner.use(customMiddleware(config.middleware)),log(`Custom middleware loaded: ${middlewarePath}`,3)):log(`Custom middleware error: ${middlewarePath} does not export a default function`,1)}catch(error){log(`Custom middleware error for ${middlewarePath}: ${error.message}`,1)}}let moduleCache=null;config.cache?.enabled&&(moduleCache=new ModuleCache(config.cache),log(`Module cache initialized: ${config.cache.maxSize} max modules, ${config.cache.maxMemoryMB}MB limit, ${config.cache.ttlMs}ms TTL`,2));const customRoutes=new Map,wildcardRoutes=new Map;if(config.customRoutes&&Object.keys(config.customRoutes).length>0){log(`Processing ${Object.keys(config.customRoutes).length} custom routes`,3);for(const[urlPath,filePath]of Object.entries(config.customRoutes))if(urlPath.includes("*")){const resolvedPath=path.isAbsolute(filePath)?filePath:path.resolve(rootPath,filePath);wildcardRoutes.set(urlPath,resolvedPath),log(`Wildcard route mapped: ${urlPath} -> ${resolvedPath}`,3)}else{const resolvedPath=path.isAbsolute(filePath)?filePath:path.resolve(rootPath,filePath);customRoutes.set(urlPath,resolvedPath),log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`,3)}}const matchWildcardRoute=(requestPath,pattern)=>{const regexPattern=pattern.replace(/\*\*/g,"(.+)").replace(/\*/g,"([^/]+)");return new RegExp(`^${regexPattern}$`).exec(requestPath)},serveStaticCustomFile=async(filePath,res)=>{const fileExtension=path.extname(filePath).toLowerCase().slice(1),mimeConfig=config.allowedMimes[fileExtension];let mimeType,encoding;"string"==typeof mimeConfig?(mimeType=mimeConfig,encoding=mimeType.startsWith("text/")?"utf8":void 0):(mimeType=mimeConfig?.mime||"application/octet-stream",encoding="utf8"===mimeConfig?.encoding?"utf8":void 0);const fileContent=await readFile(filePath,encoding);log(`Serving custom file as ${mimeType} (${fileContent.length} bytes)`,2);const contentType="utf8"===encoding&&mimeType.startsWith("text/")?`${mimeType}; charset=utf-8`:mimeType;res.writeHead(200,{"Content-Type":contentType}),res.end(fileContent)},executeRouteModule=async(filePath,req,res,params={})=>{let module;if(moduleCache&&config.cache?.enabled){const fileStats=await stat(filePath);if(module=moduleCache.get(filePath,fileStats),!module){const fileUrl=pathToFileURL(filePath).href+`?t=${Date.now()}`;module=await import(fileUrl);const estimatedSizeKB=fileStats.size/1024;moduleCache.set(filePath,module,fileStats,estimatedSizeKB)}}else{const fileUrl=pathToFileURL(filePath).href+`?t=${Date.now()}`;module=await import(fileUrl)}if("function"!=typeof module.default)return log(`Route file does not export a function: ${filePath}`,0),res.writeHead(500,{"Content-Type":"text/plain"}),void res.end("Route file does not export a function");const enhancedReq=createRequestWrapper(req,params),enhancedRes=createResponseWrapper(res),rawBody=await readRawBody(req);enhancedReq._rawBody=rawBody,enhancedReq.body=parseBody(rawBody,req.headers["content-type"]),moduleCache&&(enhancedReq._kempoCache=moduleCache),await module.default(enhancedReq,enhancedRes)},walkDynamic=async(base,segments)=>{if(0===segments.length)return{filePath:base,params:{}};const[head,...rest]=segments;let entries;try{entries=await readdir(base,{withFileTypes:!0})}catch{return null}for(const entry of entries)if(entry.name===head)if(entry.isDirectory()){const result=await walkDynamic(path.join(base,head),rest);if(result)return result}else if(entry.isFile()&&0===rest.length)return{filePath:path.join(base,head),params:{}};for(const entry of entries){if(!entry.isDirectory()||!entry.name.startsWith("[")||!entry.name.endsWith("]"))continue;const paramName=entry.name.slice(1,-1),result=await walkDynamic(path.join(base,entry.name),rest);if(result)return{filePath:result.filePath,params:{[paramName]:head,...result.params}}}return null},serveResolvedPath=async(filePath,fileStat,params,req,res)=>{if(fileStat.isDirectory()){const methodUpper=req.method.toUpperCase(),candidates=[`${methodUpper}.js`,`${methodUpper}.html`,"index.js","index.html","index.htm"];for(const candidate of candidates){const candidatePath=path.join(filePath,candidate);try{await stat(candidatePath)}catch{continue}return config.routeFiles.includes(candidate)?(log(`Executing route file: ${candidatePath}`,2),await executeRouteModule(candidatePath,req,res,params),!0):(log(`Serving index file: ${candidatePath}`,2),await serveStaticCustomFile(candidatePath,res),!0)}return null}const fileName=path.basename(filePath);return config.routeFiles.includes(fileName)?(log(`Executing route file: ${filePath}`,2),await executeRouteModule(filePath,req,res,params),!0):(await serveStaticCustomFile(filePath,res),!0)},serveCustomRoutePath=async(resolvedFilePath,req,res)=>{let fileStat;try{return fileStat=await stat(resolvedFilePath),await serveResolvedPath(resolvedFilePath,fileStat,{},req,res)}catch(e){if("ENOENT"!==e.code)throw e}let current=resolvedFilePath;const remaining=[];for(;current!==path.dirname(current);){remaining.unshift(path.basename(current)),current=path.dirname(current);try{if(!(await stat(current)).isDirectory())break;const result=await walkDynamic(current,remaining);if(!result)return null;const resolvedStat=await stat(result.filePath);return await serveResolvedPath(result.filePath,resolvedStat,result.params,req,res)}catch(e2){if("ENOENT"!==e2.code)throw e2}}return null},rescanAttempts=new Map,dynamicNoRescanPaths=new Set,shouldSkipRescan=requestPath=>config.noRescanPaths.some(pattern=>new RegExp(pattern).test(requestPath))?(log(`Skipping rescan for configured pattern: ${requestPath}`,3),!0):!!dynamicNoRescanPaths.has(requestPath)&&(log(`Skipping rescan for dynamically blacklisted path: ${requestPath}`,3),!0),handler=async(req,res)=>{if(parseInt(req.headers["content-length"]||"0",10)>config.maxBodySize)return res.writeHead(413,{"Content-Type":"text/plain"}),void res.end("Payload Too Large");const rawBody=await new Promise((resolve,reject)=>{if(["GET","HEAD"].includes(req.method)&&!req.headers["content-length"])return resolve("");let body="",size=0;req.on("data",chunk=>{if(size+=chunk.length,size>config.maxBodySize)return req.destroy(),void reject(new Error("Payload Too Large"));body+=chunk.toString()}),req.on("end",()=>resolve(body)),req.on("error",reject)}).catch(err=>{if("Payload Too Large"===err.message)return res.writeHead(413,{"Content-Type":"text/plain"}),res.end("Payload Too Large"),null;throw err});if(null===rawBody)return;req._bufferedBody=rawBody;const enhancedRequest=createRequestWrapper(req,{}),enhancedResponse=createResponseWrapper(res);enhancedRequest._rawBody=rawBody,enhancedRequest.body=parseBody(rawBody,req.headers["content-type"]),await middlewareRunner.run(enhancedRequest,enhancedResponse,async()=>{const requestPath=enhancedRequest.url.split("?")[0];log(`${enhancedRequest.method} ${requestPath}`,4),log(`customRoutes keys: ${Array.from(customRoutes.keys()).join(", ")}`,4);const normalizePath=p=>{try{let np=decodeURIComponent(p);return np.startsWith("/")||(np="/"+np),np.length>1&&np.endsWith("/")&&(np=np.slice(0,-1)),np}catch(e){log(`Warning: Failed to decode URI component "${p}": ${e.message}`,1);let np=p;return np.startsWith("/")||(np="/"+np),np.length>1&&np.endsWith("/")&&(np=np.slice(0,-1)),np}},normalizedRequestPath=normalizePath(requestPath);log(`Normalized requestPath: ${normalizedRequestPath}`,4);let matchedKey=null;for(const key of customRoutes.keys())if(normalizePath(key)===normalizedRequestPath){matchedKey=key;break}if(matchedKey){const customFilePath=customRoutes.get(matchedKey);log(`Serving custom route: ${normalizedRequestPath} -> ${customFilePath}`,3);try{if(await serveCustomRoutePath(customFilePath,req,res))return;return log(`Custom route path not found: ${customFilePath}`,1),res.writeHead(404,{"Content-Type":"text/plain"}),void res.end("Custom route file not found")}catch(error){return log(`Error serving custom route ${normalizedRequestPath}: ${error.message}`,1),res.writeHead(500,{"Content-Type":"text/plain"}),void res.end("Internal Server Error")}}const wildcardMatch=(requestPath=>{for(const[pattern,filePath]of wildcardRoutes){const matches=matchWildcardRoute(requestPath,pattern);if(matches)return{filePath:filePath,matches:matches}}return null})(requestPath);if(wildcardMatch){const resolvedFilePath=((filePath,matches)=>{let resolvedPath=filePath,matchIndex=1;for(;resolvedPath.includes("**")&&matchIndex<matches.length;)resolvedPath=resolvedPath.replace("**",matches[matchIndex]),matchIndex++;for(;resolvedPath.includes("*")&&matchIndex<matches.length;)resolvedPath=resolvedPath.replace("*",matches[matchIndex]),matchIndex++;return path.isAbsolute(resolvedPath)?resolvedPath:path.resolve(rootPath,resolvedPath)})(wildcardMatch.filePath,wildcardMatch.matches);log(`Serving wildcard route: ${requestPath} -> ${resolvedFilePath}`,3);try{if(await serveCustomRoutePath(resolvedFilePath,req,res))return;log(`Wildcard route path not found: ${requestPath}`,2)}catch(error){return log(`Error serving wildcard route ${requestPath}: ${error.message}`,1),enhancedResponse.writeHead(500,{"Content-Type":"text/plain"}),void enhancedResponse.end("Internal Server Error")}}const served=await serveFile(files,rootPath,requestPath,req.method,config,req,res,log,moduleCache);if(!served&&config.maxRescanAttempts>0&&!shouldSkipRescan(requestPath)){log("File not found, rescanning directory...",1),files=await getFiles(rootPath,config,log),log(`Rescan found ${files.length} files`,2);await serveFile(files,rootPath,requestPath,req.method,config,req,res,log,moduleCache)?rescanAttempts.delete(requestPath):((requestPath=>{const newAttempts=(rescanAttempts.get(requestPath)||0)+1;rescanAttempts.set(requestPath,newAttempts),newAttempts>config.maxRescanAttempts&&(dynamicNoRescanPaths.add(requestPath),log(`Path ${requestPath} added to dynamic blacklist after ${newAttempts} failed attempts`,1)),log(`Rescan attempt ${newAttempts}/${config.maxRescanAttempts} for: ${requestPath}`,3)})(requestPath),log(`404 - File not found after rescan: ${requestPath}`,1),enhancedResponse.writeHead(404,{"Content-Type":"text/plain"}),enhancedResponse.end("Not Found"))}else served||(shouldSkipRescan(requestPath)?log(`404 - Skipped rescan for: ${requestPath}`,2):log(`404 - File not found: ${requestPath}`,1),enhancedResponse.writeHead(404,{"Content-Type":"text/plain"}),enhancedResponse.end("Not Found"))})};return handler.moduleCache=moduleCache,handler.getStats=()=>moduleCache?.getStats()||null,handler.logCacheStats=()=>moduleCache?.logStats(log),handler.clearCache=()=>moduleCache?.clear(),handler};
1
+ import path from"path";import{readFile,stat,readdir}from"fs/promises";import{pathToFileURL}from"url";import defaultConfig from"./defaultConfig.js";import getFiles from"./getFiles.js";import findFile from"./findFile.js";import serveFile from"./serveFile.js";import MiddlewareRunner from"./middlewareRunner.js";import ModuleCache from"./moduleCache.js";import createRequestWrapper,{readRawBody,parseBody}from"./requestWrapper.js";import createResponseWrapper from"./responseWrapper.js";import{corsMiddleware,compressionMiddleware,rateLimitMiddleware,securityMiddleware,loggingMiddleware}from"./builtinMiddleware.js";import{onRescan}from"./rescan.js";export default async(flags,log)=>{log("Initializing router",3);const rootPath=path.isAbsolute(flags.root)?flags.root:path.join(process.cwd(),flags.root);log(`Root path: ${rootPath}`,3);let config=defaultConfig;try{const configFileName=flags.config||".config.json",configPath=path.isAbsolute(configFileName)?configFileName:path.join(rootPath,configFileName);if(log(`Config file name: ${configFileName}`,3),log(`Config path: ${configPath}`,3),path.isAbsolute(configFileName))log("Config file name is absolute, skipping validation",4);else{const relativeConfigPath=path.relative(rootPath,configPath);if(log(`Relative config path: ${relativeConfigPath}`,4),log(`Starts with '..': ${relativeConfigPath.startsWith("..")}`,4),relativeConfigPath.startsWith("..")||path.isAbsolute(relativeConfigPath))throw log("Validation failed - throwing error",4),new Error(`Config file must be within the server root directory. Config path: ${configPath}, Root path: ${rootPath}`);log("Validation passed",4)}log(`Loading config from: ${configPath}`,3);const configContent=await readFile(configPath,"utf8"),userConfig=JSON.parse(configContent);config={...defaultConfig,...userConfig,allowedMimes:{...defaultConfig.allowedMimes,...userConfig.allowedMimes||{}},middleware:{...defaultConfig.middleware,...userConfig.middleware||{}},customRoutes:{...defaultConfig.customRoutes,...userConfig.customRoutes||{}},cache:{...defaultConfig.cache,...userConfig.cache||{}}},log("User config loaded and merged with defaults",3)}catch(e){if(e.message.includes("Config file must be within the server root directory"))throw e;log("Using default config (no config file found)",3)}const dis=new Set(config.disallowedRegex);dis.add("^/\\..*"),dis.add("\\.config$"),dis.add("\\.git/"),config.disallowedRegex=[...dis],log(`Config loaded with ${config.disallowedRegex.length} disallowed patterns`,3);let files=await getFiles(rootPath,config,log);log(`Initial scan found ${files.length} files`,2),onRescan(async done=>{try{files=await getFiles(rootPath,config,log),log(`Rescan found ${files.length} files`,2),done(null,files.length)}catch(error){log(`Rescan failed: ${error.message}`,1),done(error)}});const middlewareRunner=new MiddlewareRunner;if(config.middleware?.cors?.enabled&&(middlewareRunner.use(corsMiddleware(config.middleware.cors)),log("CORS middleware enabled",3)),config.middleware?.compression?.enabled&&(middlewareRunner.use(compressionMiddleware(config.middleware.compression)),log("Compression middleware enabled",3)),config.middleware?.rateLimit?.enabled&&(middlewareRunner.use(rateLimitMiddleware(config.middleware.rateLimit)),log("Rate limit middleware enabled",3)),config.middleware?.security?.enabled&&(middlewareRunner.use(securityMiddleware(config.middleware.security)),log("Security middleware enabled",3)),config.middleware?.logging?.enabled&&(middlewareRunner.use(loggingMiddleware(config.middleware.logging,log)),log("Logging middleware enabled",3)),config.middleware?.custom&&config.middleware.custom.length>0){log(`Loading ${config.middleware.custom.length} custom middleware files`,3);for(const middlewarePath of config.middleware.custom)try{const resolvedPath=path.isAbsolute(middlewarePath)?middlewarePath:path.resolve(rootPath,middlewarePath),middlewareUrl=pathToFileURL(resolvedPath).href+`?t=${Date.now()}`,customMiddleware=(await import(middlewareUrl)).default;"function"==typeof customMiddleware?(middlewareRunner.use(customMiddleware(config.middleware)),log(`Custom middleware loaded: ${middlewarePath}`,3)):log(`Custom middleware error: ${middlewarePath} does not export a default function`,1)}catch(error){log(`Custom middleware error for ${middlewarePath}: ${error.message}`,1)}}let moduleCache=null;config.cache?.enabled&&(moduleCache=new ModuleCache(config.cache),log(`Module cache initialized: ${config.cache.maxSize} max modules, ${config.cache.maxMemoryMB}MB limit, ${config.cache.ttlMs}ms TTL`,2));const customRoutes=new Map,wildcardRoutes=new Map;if(config.customRoutes&&Object.keys(config.customRoutes).length>0){log(`Processing ${Object.keys(config.customRoutes).length} custom routes`,3);for(const[urlPath,filePath]of Object.entries(config.customRoutes))if(urlPath.includes("*")){const resolvedPath=path.isAbsolute(filePath)?filePath:path.resolve(rootPath,filePath);wildcardRoutes.set(urlPath,resolvedPath),log(`Wildcard route mapped: ${urlPath} -> ${resolvedPath}`,3)}else{const resolvedPath=path.isAbsolute(filePath)?filePath:path.resolve(rootPath,filePath);customRoutes.set(urlPath,resolvedPath),log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`,3)}}const matchWildcardRoute=(requestPath,pattern)=>{const regexPattern=pattern.replace(/\*\*/g,"(.+)").replace(/\*/g,"([^/]+)");return new RegExp(`^${regexPattern}$`).exec(requestPath)},serveStaticCustomFile=async(filePath,res)=>{const fileExtension=path.extname(filePath).toLowerCase().slice(1),mimeConfig=config.allowedMimes[fileExtension];let mimeType,encoding;"string"==typeof mimeConfig?(mimeType=mimeConfig,encoding=mimeType.startsWith("text/")?"utf8":void 0):(mimeType=mimeConfig?.mime||"application/octet-stream",encoding="utf8"===mimeConfig?.encoding?"utf8":void 0);const fileContent=await readFile(filePath,encoding);log(`Serving custom file as ${mimeType} (${fileContent.length} bytes)`,2);const contentType="utf8"===encoding&&mimeType.startsWith("text/")?`${mimeType}; charset=utf-8`:mimeType;res.writeHead(200,{"Content-Type":contentType}),res.end(fileContent)},executeRouteModule=async(filePath,req,res,params={})=>{let module;if(moduleCache&&config.cache?.enabled){const fileStats=await stat(filePath);if(module=moduleCache.get(filePath,fileStats),!module){const fileUrl=pathToFileURL(filePath).href+`?t=${Date.now()}`;module=await import(fileUrl);const estimatedSizeKB=fileStats.size/1024;moduleCache.set(filePath,module,fileStats,estimatedSizeKB)}}else{const fileUrl=pathToFileURL(filePath).href+`?t=${Date.now()}`;module=await import(fileUrl)}if("function"!=typeof module.default)return log(`Route file does not export a function: ${filePath}`,0),res.writeHead(500,{"Content-Type":"text/plain"}),void res.end("Route file does not export a function");const enhancedReq=createRequestWrapper(req,params),enhancedRes=createResponseWrapper(res),rawBody=await readRawBody(req);enhancedReq._rawBody=rawBody,enhancedReq.body=parseBody(rawBody,req.headers["content-type"]),moduleCache&&(enhancedReq._kempoCache=moduleCache),await module.default(enhancedReq,enhancedRes)},walkDynamic=async(base,segments)=>{if(0===segments.length)return{filePath:base,params:{}};const[head,...rest]=segments;let entries;try{entries=await readdir(base,{withFileTypes:!0})}catch{return null}for(const entry of entries)if(entry.name===head)if(entry.isDirectory()){const result=await walkDynamic(path.join(base,head),rest);if(result)return result}else if(entry.isFile()&&0===rest.length)return{filePath:path.join(base,head),params:{}};for(const entry of entries){if(!entry.isDirectory()||!entry.name.startsWith("[")||!entry.name.endsWith("]"))continue;const paramName=entry.name.slice(1,-1),result=await walkDynamic(path.join(base,entry.name),rest);if(result)return{filePath:result.filePath,params:{[paramName]:head,...result.params}}}return null},serveResolvedPath=async(filePath,fileStat,params,req,res)=>{if(fileStat.isDirectory()){const methodUpper=req.method.toUpperCase(),candidates=[`${methodUpper}.js`,`${methodUpper}.html`,"index.js","index.html","index.htm"];for(const candidate of candidates){const candidatePath=path.join(filePath,candidate);try{await stat(candidatePath)}catch{continue}return config.routeFiles.includes(candidate)?(log(`Executing route file: ${candidatePath}`,2),await executeRouteModule(candidatePath,req,res,params),!0):(log(`Serving index file: ${candidatePath}`,2),await serveStaticCustomFile(candidatePath,res),!0)}return null}const fileName=path.basename(filePath);return config.routeFiles.includes(fileName)?(log(`Executing route file: ${filePath}`,2),await executeRouteModule(filePath,req,res,params),!0):(await serveStaticCustomFile(filePath,res),!0)},serveCustomRoutePath=async(resolvedFilePath,req,res)=>{let fileStat;try{return fileStat=await stat(resolvedFilePath),await serveResolvedPath(resolvedFilePath,fileStat,{},req,res)}catch(e){if("ENOENT"!==e.code)throw e}let current=resolvedFilePath;const remaining=[];for(;current!==path.dirname(current);){remaining.unshift(path.basename(current)),current=path.dirname(current);try{if(!(await stat(current)).isDirectory())break;const result=await walkDynamic(current,remaining);if(!result)return null;const resolvedStat=await stat(result.filePath);return await serveResolvedPath(result.filePath,resolvedStat,result.params,req,res)}catch(e2){if("ENOENT"!==e2.code)throw e2}}return null},rescanAttempts=new Map,dynamicNoRescanPaths=new Set,shouldSkipRescan=requestPath=>config.noRescanPaths.some(pattern=>new RegExp(pattern).test(requestPath))?(log(`Skipping rescan for configured pattern: ${requestPath}`,3),!0):!!dynamicNoRescanPaths.has(requestPath)&&(log(`Skipping rescan for dynamically blacklisted path: ${requestPath}`,3),!0),handler=async(req,res)=>{if(parseInt(req.headers["content-length"]||"0",10)>config.maxBodySize)return res.writeHead(413,{"Content-Type":"text/plain"}),void res.end("Payload Too Large");const rawBody=await new Promise((resolve,reject)=>{if(["GET","HEAD"].includes(req.method)&&!req.headers["content-length"])return resolve("");let body="",size=0;req.on("data",chunk=>{if(size+=chunk.length,size>config.maxBodySize)return req.destroy(),void reject(new Error("Payload Too Large"));body+=chunk.toString()}),req.on("end",()=>resolve(body)),req.on("error",reject)}).catch(err=>{if("Payload Too Large"===err.message)return res.writeHead(413,{"Content-Type":"text/plain"}),res.end("Payload Too Large"),null;throw err});if(null===rawBody)return;req._bufferedBody=rawBody;const enhancedRequest=createRequestWrapper(req,{}),enhancedResponse=createResponseWrapper(res);enhancedRequest._rawBody=rawBody,enhancedRequest.body=parseBody(rawBody,req.headers["content-type"]),await middlewareRunner.run(enhancedRequest,enhancedResponse,async()=>{const requestPath=enhancedRequest.url.split("?")[0];log(`${enhancedRequest.method} ${requestPath}`,4),log(`customRoutes keys: ${Array.from(customRoutes.keys()).join(", ")}`,4);const normalizePath=p=>{try{let np=decodeURIComponent(p);return np.startsWith("/")||(np="/"+np),np.length>1&&np.endsWith("/")&&(np=np.slice(0,-1)),np}catch(e){log(`Warning: Failed to decode URI component "${p}": ${e.message}`,1);let np=p;return np.startsWith("/")||(np="/"+np),np.length>1&&np.endsWith("/")&&(np=np.slice(0,-1)),np}},normalizedRequestPath=normalizePath(requestPath);log(`Normalized requestPath: ${normalizedRequestPath}`,4);let matchedKey=null;for(const key of customRoutes.keys())if(normalizePath(key)===normalizedRequestPath){matchedKey=key;break}if(matchedKey){const customFilePath=customRoutes.get(matchedKey);log(`Serving custom route: ${normalizedRequestPath} -> ${customFilePath}`,3);try{if(await serveCustomRoutePath(customFilePath,req,res))return;return log(`Custom route path not found: ${customFilePath}`,1),res.writeHead(404,{"Content-Type":"text/plain"}),void res.end("Custom route file not found")}catch(error){return log(`Error serving custom route ${normalizedRequestPath}: ${error.message}`,1),res.writeHead(500,{"Content-Type":"text/plain"}),void res.end("Internal Server Error")}}const wildcardMatch=(requestPath=>{for(const[pattern,filePath]of wildcardRoutes){const matches=matchWildcardRoute(requestPath,pattern);if(matches)return{filePath:filePath,matches:matches}}return null})(requestPath);if(wildcardMatch){const resolvedFilePath=((filePath,matches)=>{let resolvedPath=filePath,matchIndex=1;for(;resolvedPath.includes("**")&&matchIndex<matches.length;)resolvedPath=resolvedPath.replace("**",matches[matchIndex]),matchIndex++;for(;resolvedPath.includes("*")&&matchIndex<matches.length;)resolvedPath=resolvedPath.replace("*",matches[matchIndex]),matchIndex++;return path.isAbsolute(resolvedPath)?resolvedPath:path.resolve(rootPath,resolvedPath)})(wildcardMatch.filePath,wildcardMatch.matches);log(`Serving wildcard route: ${requestPath} -> ${resolvedFilePath}`,3);try{if(await serveCustomRoutePath(resolvedFilePath,req,res))return;log(`Wildcard route path not found: ${requestPath}`,2)}catch(error){return log(`Error serving wildcard route ${requestPath}: ${error.message}`,1),enhancedResponse.writeHead(500,{"Content-Type":"text/plain"}),void enhancedResponse.end("Internal Server Error")}}const served=await serveFile(files,rootPath,requestPath,req.method,config,req,res,log,moduleCache);if(!served&&config.maxRescanAttempts>0&&!shouldSkipRescan(requestPath)){log("File not found, rescanning directory...",1),files=await getFiles(rootPath,config,log),log(`Rescan found ${files.length} files`,2);await serveFile(files,rootPath,requestPath,req.method,config,req,res,log,moduleCache)?rescanAttempts.delete(requestPath):((requestPath=>{const newAttempts=(rescanAttempts.get(requestPath)||0)+1;rescanAttempts.set(requestPath,newAttempts),newAttempts>config.maxRescanAttempts&&(dynamicNoRescanPaths.add(requestPath),log(`Path ${requestPath} added to dynamic blacklist after ${newAttempts} failed attempts`,1)),log(`Rescan attempt ${newAttempts}/${config.maxRescanAttempts} for: ${requestPath}`,3)})(requestPath),log(`404 - File not found after rescan: ${requestPath}`,1),enhancedResponse.writeHead(404,{"Content-Type":"text/plain"}),enhancedResponse.end("Not Found"))}else served||(shouldSkipRescan(requestPath)?log(`404 - Skipped rescan for: ${requestPath}`,2):log(`404 - File not found: ${requestPath}`,1),enhancedResponse.writeHead(404,{"Content-Type":"text/plain"}),enhancedResponse.end("Not Found"))})};return handler.moduleCache=moduleCache,handler.getStats=()=>moduleCache?.getStats()||null,handler.logCacheStats=()=>moduleCache?.logStats(log),handler.clearCache=()=>moduleCache?.clear(),handler};
package/docs/caching.html CHANGED
@@ -7,11 +7,13 @@
7
7
  <meta name='viewport' content='width=device-width, initial-scale=1'>
8
8
  <link rel="icon" type="image/png" sizes="48x48" href="./media/icon48.png">
9
9
  <link rel="manifest" href="./manifest.json" />
10
- <link rel="stylesheet" href="./kempo.min.css" />
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/kempo-css@2.1.3/dist/kempo.min.css" />
11
+ <link rel="stylesheet" href="./theme.css" />
12
+ <script src="./init.js"></script>
11
13
  </head>
12
14
  <body>
13
- <main>
14
- <a href="./" class="btn">Home</a>
15
+ <k-import src="./nav.inc.html"></k-import>
16
+ <k-main>
15
17
  <h1>Module Caching</h1>
16
18
  <p>Kempo Server includes an intelligent module caching system that dramatically improves performance by caching JavaScript route modules in memory.</p>
17
19
 
@@ -229,7 +231,8 @@
229
231
  <li>Extend <code>ttlMs</code> for stable route files</li>
230
232
  <li>Monitor hit rates with admin endpoints</li>
231
233
  </ul>
232
-
233
- </main>
234
+ </k-main>
235
+ <div style="height:25vh"></div>
236
+ <script type="module" src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"></script>
234
237
  </body>
235
238
  </html>
@@ -7,11 +7,13 @@
7
7
  <meta name='viewport' content='width=device-width, initial-scale=1'>
8
8
  <link rel="icon" type="image/png" sizes="48x48" href="./media/icon48.png">
9
9
  <link rel="manifest" href="./manifest.json" />
10
- <link rel="stylesheet" href="./kempo.min.css" />
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/kempo-css@2.1.3/dist/kempo.min.css" />
11
+ <link rel="stylesheet" href="./theme.css" />
12
+ <script src="./init.js"></script>
11
13
  </head>
12
14
  <body>
13
- <main>
14
- <a href="./" class="btn">Home</a>
15
+ <k-import src="./nav.inc.html"></k-import>
16
+ <k-main>
15
17
  <h1>CLI Utilities</h1>
16
18
  <p>The CLI utilities provide simple command-line argument parsing functionality for Node.js applications.</p>
17
19
 
@@ -80,8 +82,8 @@
80
82
  <li><code>promptUser(query)</code> - Prompt user for input</li>
81
83
  <li><code>promptYN(query, defaultValue)</code> - Prompt for yes/no with default</li>
82
84
  </ul>
83
- </main>
85
+ </k-main>
84
86
  <div style="height:25vh"></div>
87
+ <script type="module" src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"></script>
85
88
  </body>
86
- </html></content>
87
- <parameter name="filePath">c:\Users\dusti\dev\kempo-server\docs\cli-utils.html
89
+ </html>
@@ -6,11 +6,13 @@
6
6
  <meta name='viewport' content='width=device-width, initial-scale=1'>
7
7
  <link rel="icon" type="image/png" sizes="48x48" href="./media/icon48.png">
8
8
  <link rel="manifest" href="./manifest.json" />
9
- <link rel="stylesheet" href="./kempo.min.css" />
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/kempo-css@2.1.3/dist/kempo.min.css" />
10
+ <link rel="stylesheet" href="./theme.css" />
11
+ <script src="./init.js"></script>
10
12
  </head>
11
13
  <body>
12
- <main>
13
- <a href="./" class="btn">Home</a>
14
+ <k-import src="./nav.inc.html"></k-import>
15
+ <k-main>
14
16
  <h1>Configuration</h1>
15
17
  <p>Customize Kempo Server's behavior with a simple JSON configuration file.</p>
16
18
 
@@ -318,7 +320,8 @@
318
320
  <li>Enable file watching in development, disable in production for stability</li>
319
321
  <li>Use shorter TTL in development for faster iteration</li>
320
322
  </ul>
321
- </main>
323
+ </k-main>
322
324
  <div style="height:25vh"></div>
325
+ <script type="module" src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"></script>
323
326
  </body>
324
327
  </html>
@@ -6,11 +6,13 @@
6
6
  <meta name='viewport' content='width=device-width, initial-scale=1'>
7
7
  <link rel="icon" type="image/png" sizes="48x48" href="./media/icon48.png">
8
8
  <link rel="manifest" href="./manifest.json" />
9
- <link rel="stylesheet" href="./kempo.min.css" />
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/kempo-css@2.1.3/dist/kempo.min.css" />
10
+ <link rel="stylesheet" href="./theme.css" />
11
+ <script src="./init.js"></script>
10
12
  </head>
11
13
  <body>
12
- <main>
13
- <a href="./" class="btn">Home</a>
14
+ <k-import src="./nav.inc.html"></k-import>
15
+ <k-main>
14
16
  <h1>Examples & Demos</h1>
15
17
  <p>Explore practical examples and try interactive demos of Kempo Server features.</p>
16
18
 
@@ -129,6 +131,11 @@
129
131
  <h3>Production Configuration</h3>
130
132
  <pre><code class="hljs json">{<br /> <span class="hljs-attr">"allowedMimes"</span>: {<br /> <span class="hljs-attr">"html"</span>: <span class="hljs-string">"text/html"</span>,<br /> <span class="hljs-attr">"css"</span>: <span class="hljs-string">"text/css"</span>,<br /> <span class="hljs-attr">"js"</span>: <span class="hljs-string">"application/javascript"</span>,<br /> <span class="hljs-attr">"json"</span>: <span class="hljs-string">"application/json"</span>,<br /> <span class="hljs-attr">"png"</span>: <span class="hljs-string">"image/png"</span>,<br /> <span class="hljs-attr">"jpg"</span>: <span class="hljs-string">"image/jpeg"</span>,<br /> <span class="hljs-attr">"svg"</span>: <span class="hljs-string">"image/svg+xml"</span>,<br /> <span class="hljs-attr">"woff"</span>: <span class="hljs-string">"font/woff"</span>,<br /> <span class="hljs-attr">"woff2"</span>: <span class="hljs-string">"font/woff2"</span><br /> },<br /> <span class="hljs-attr">"disallowedRegex"</span>: [<br /> <span class="hljs-string">"^/\\..*"</span>,<br /> <span class="hljs-string">"\\.env$"</span>,<br /> <span class="hljs-string">"\\.config$"</span>,<br /> <span class="hljs-string">"password"</span>,<br /> <span class="hljs-string">"secret"</span>,<br /> <span class="hljs-string">"node_modules"</span>,<br /> <span class="hljs-string">"\\.git"</span>,<br /> <span class="hljs-string">"\\.map$"</span><br /> ],<br /> <span class="hljs-attr">"middleware"</span>: {<br /> <span class="hljs-attr">"cors"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"origin"</span>: <span class="hljs-string">"https://yourdomain.com"</span>,<br /> <span class="hljs-attr">"credentials"</span>: <span class="hljs-literal">true</span><br /> },<br /> <span class="hljs-attr">"compression"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"threshold"</span>: <span class="hljs-number">1024</span><br /> },<br /> <span class="hljs-attr">"security"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"headers"</span>: {<br /> <span class="hljs-attr">"X-Content-Type-Options"</span>: <span class="hljs-string">"nosniff"</span>,<br /> <span class="hljs-attr">"X-Frame-Options"</span>: <span class="hljs-string">"DENY"</span>,<br /> <span class="hljs-attr">"X-XSS-Protection"</span>: <span class="hljs-string">"1; mode=block"</span>,<br /> <span class="hljs-attr">"Strict-Transport-Security"</span>: <span class="hljs-string">"max-age=31536000; includeSubDomains"</span><br /> }<br /> },<br /> <span class="hljs-attr">"rateLimit"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"maxRequests"</span>: <span class="hljs-number">100</span>,<br /> <span class="hljs-attr">"windowMs"</span>: <span class="hljs-number">60000</span><br /> },<br /> <span class="hljs-attr">"custom"</span>: [<br /> <span class="hljs-string">"./middleware/auth.js"</span>,<br /> <span class="hljs-string">"./middleware/logging.js"</span>,<br /> <span class="hljs-string">"./middleware/analytics.js"</span><br /> ]<br /> }<br />}</code></pre>
131
133
 
134
+ <h3>Programmatic File Rescan</h3>
135
+ <p>When your code creates or removes files at runtime (e.g., a CMS generating static pages), you can trigger a file rescan without restarting the server. Import the <code>rescan</code> function from anywhere in the same Node process:</p>
136
+ <pre><code class="hljs javascript"><span class="hljs-comment">// api/pages/POST.js</span><br /><span class="hljs-keyword">import</span> { writeFile } <span class="hljs-keyword">from</span> <span class="hljs-string">'fs/promises'</span>;<br /><span class="hljs-keyword">import</span> rescan <span class="hljs-keyword">from</span> <span class="hljs-string">'kempo-server/rescan'</span>;<br /><br /><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> (req, res) =&gt; {<br /> <span class="hljs-keyword">const</span> { slug, html } = req.body;<br /> <span class="hljs-keyword">await</span> writeFile(<span class="hljs-string">`./pages/${slug}.html`</span>, html);<br /> <span class="hljs-keyword">const</span> fileCount = <span class="hljs-keyword">await</span> rescan();<br /> res.json({ published: <span class="hljs-literal">true</span>, fileCount });<br />};</code></pre>
137
+ <p>The <code>rescan()</code> function returns a promise that resolves with the number of files found. It works from route handlers, middleware, file watchers, scheduled tasks, or any other code running in the same Node process.</p>
138
+
132
139
  <script>
133
140
  async function fetchUser() {
134
141
  const userId = document.getElementById('userId').value || 'john';
@@ -195,7 +202,8 @@
195
202
  }
196
203
  }
197
204
  </script>
198
- </main>
205
+ </k-main>
199
206
  <div style="height:25vh"></div>
207
+ <script type="module" src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"></script>
200
208
  </body>
201
209
  </html>
@@ -7,11 +7,13 @@
7
7
  <meta name='viewport' content='width=device-width, initial-scale=1'>
8
8
  <link rel="icon" type="image/png" sizes="48x48" href="./media/icon48.png">
9
9
  <link rel="manifest" href="./manifest.json" />
10
- <link rel="stylesheet" href="./kempo.min.css" />
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/kempo-css@2.1.3/dist/kempo.min.css" />
11
+ <link rel="stylesheet" href="./theme.css" />
12
+ <script src="./init.js"></script>
11
13
  </head>
12
14
  <body>
13
- <main>
14
- <a href="./" class="btn">Home</a>
15
+ <k-import src="./nav.inc.html"></k-import>
16
+ <k-main>
15
17
  <h1>File System Utilities</h1>
16
18
  <p>The file system utilities provide common file and directory operations with Promise-based APIs for Node.js applications.</p>
17
19
 
@@ -111,8 +113,8 @@ async function backupProject() {
111
113
  <li>Functions handle errors appropriately (e.g., ensureDir ignores EEXIST)</li>
112
114
  <li>copyDir preserves directory structure recursively</li>
113
115
  </ul>
114
- </main>
116
+ </k-main>
115
117
  <div style="height:25vh"></div>
118
+ <script type="module" src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"></script>
116
119
  </body>
117
- </html></content>
118
- <parameter name="filePath">c:\Users\dusti\dev\kempo-server\docs\fs-utils.html
120
+ </html>
@@ -6,11 +6,13 @@
6
6
  <meta name='viewport' content='width=device-width, initial-scale=1'>
7
7
  <link rel="icon" type="image/png" sizes="48x48" href="./media/icon48.png">
8
8
  <link rel="manifest" href="./manifest.json" />
9
- <link rel="stylesheet" href="./kempo.min.css" />
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/kempo-css@2.1.3/dist/kempo.min.css" />
10
+ <link rel="stylesheet" href="./theme.css" />
11
+ <script src="./init.js"></script>
10
12
  </head>
11
13
  <body>
12
- <main>
13
- <a href="./" class="btn">Home</a>
14
+ <k-import src="./nav.inc.html"></k-import>
15
+ <k-main>
14
16
  <h1>Getting Started</h1>
15
17
  <p>Get up and running with Kempo Server in just a few steps.</p>
16
18
 
@@ -71,7 +73,8 @@
71
73
  <li><a href="middleware.html">Middleware</a> - Add authentication, logging, CORS, and more</li>
72
74
  <li><a href="examples.html">Examples & Demos</a> - See real-world examples in action</li>
73
75
  </ul>
74
- </main>
76
+ </k-main>
75
77
  <div style="height:25vh"></div>
78
+ <script type="module" src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"></script>
76
79
  </body>
77
80
  </html>
package/docs/index.html CHANGED
@@ -6,9 +6,9 @@
6
6
  <meta name='viewport' content='width=device-width, initial-scale=1'>
7
7
  <link rel="icon" type="image/png" sizes="48x48" href="./media/icon48.png">
8
8
  <link rel="manifest" href="./manifest.json" />
9
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/kempo-css@1.3.2/dist/kempo.min.css" />
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/kempo-css@2.1.3/dist/kempo.min.css" />
10
10
  <link rel="stylesheet" href="./theme.css" />
11
- <script>window.litDisableBundleWarning = true;</script>
11
+ <script src="./init.js"></script>
12
12
  </head>
13
13
  <body>
14
14
  <k-import src="./nav.inc.html"></k-import>
@@ -28,7 +28,7 @@
28
28
  <h4 class="mt0">Getting Started</h4>
29
29
  <ul>
30
30
  <li><a href="getting-started.html">Getting Started</a></li>
31
- <li><a href="routing.html">Routes & Routing</a></li>
31
+ <li><a href="routing.html">Routing</a></li>
32
32
  <li><a href="request-response.html">Request & Response</a></li>
33
33
  </ul>
34
34
  </div>
package/docs/init.js CHANGED
@@ -0,0 +1,2 @@
1
+ window.litDisableBundleWarning = true;
2
+ window.kempo = {pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/']};
@@ -6,11 +6,13 @@
6
6
  <meta name='viewport' content='width=device-width, initial-scale=1'>
7
7
  <link rel="icon" type="image/png" sizes="48x48" href="./media/icon48.png">
8
8
  <link rel="manifest" href="./manifest.json" />
9
- <link rel="stylesheet" href="./kempo.min.css" />
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/kempo-css@2.1.3/dist/kempo.min.css" />
10
+ <link rel="stylesheet" href="./theme.css" />
11
+ <script src="./init.js"></script>
10
12
  </head>
11
13
  <body>
12
- <main>
13
- <a href="./" class="btn">Home</a>
14
+ <k-import src="./nav.inc.html"></k-import>
15
+ <k-main>
14
16
  <h1>Middleware</h1>
15
17
  <p>Kempo Server includes a powerful middleware system that allows you to add functionality like authentication, logging, CORS, compression, and more.</p>
16
18
 
@@ -141,7 +143,8 @@
141
143
  <li>Cache expensive operations when possible</li>
142
144
  <li>Consider the performance impact of middleware order</li>
143
145
  </ul>
144
- </main>
146
+ </k-main>
145
147
  <div style="height:25vh"></div>
148
+ <script type="module" src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"></script>
146
149
  </body>
147
150
  </html>
package/docs/nav.inc.html CHANGED
@@ -1,5 +1,6 @@
1
- <nav
2
- class="d-f bg-primary fixed"
1
+ <k-nav
2
+ fixed
3
+ class="bg-primary"
3
4
  >
4
5
  <button
5
6
  id="toggleNavSideMenu"
@@ -19,23 +20,51 @@
19
20
  <a href="https://github.com/dustinpoissant/kempo-ui?tab=License-1-ov-file#creative-commons-attribution-noncommercial-sharealike-20" target="_blank"><k-icon name="license"></k-icont></a>
20
21
  <a href="https://github.com/dustinpoissant/kempo-ui" target="_blank"><k-icon name="github-mark"></k-icont></a>
21
22
  <k-theme-switcher></k-theme-switcher>
22
- </nav>
23
+ </k-nav>
23
24
  <div style="width: 100%; height: 4rem;"></div>
24
- <k-side-menu
25
+ <k-aside
25
26
  id="navSideMenu"
27
+ state="offscreen"
26
28
  >
27
29
  <menu>
28
30
  <a href="./" class="ta-center bb mb r0">
29
31
  <h1 class="tc-primary">Kempo Server</h1>
30
32
  <img src="./media/icon128.png" alt="Kempo UI Icon" />
31
33
  </a>
32
-
33
- <div class="pl mb">
34
- <a href="./">Quick Start</a>
35
- </div>
34
+ <h3>Getting Started</h3>
35
+ <a href="./" class="d-b pq pl">Quick Start</a>
36
+ <a href="./routing.html" class="d-b pq pl">Routing</a>
37
+ <a href="./request-response.html" class="d-b pq pl">Request & Response</a>
38
+ <br /><br />
39
+ <h3>Advanced Features</h3>
40
+ <a href="configuration.html" class="d-b pq pl">Configuration</a>
41
+ <a href="middleware.html" class="d-b pq pl">Middleware</a>
42
+ <a href="caching.html" class="d-b pq pl">Module Caching</a>
43
+ <a href="cli-utils.html" class="d-b pq pl">CLI Utilities</a>
44
+ <a href="fs-utils.html" class="d-b pq pl">File System Utilities</a>
45
+ <a href="examples.html" class="d-b pq pl">Examples & Demos</a>
36
46
  </menu>
37
- </k-side-menu>
38
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/SideMenu.js" type="module"></script>
39
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Icon.js" type="module"></script>
40
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/ThemeSwitcher.js" type="module"></script>
41
- <script src="./nav.inc.js" type="module"></script>
47
+ </k-aside>
48
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
49
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
50
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
51
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
52
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
53
+ <script>
54
+ document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
55
+ await window.customElements.whenDefined('k-aside');
56
+ document.getElementById('navSideMenu').toggle();
57
+ });
58
+ document.addEventListener('click', function(e) {
59
+ if (e.target.matches('a[href^="#"]')) {
60
+ e.preventDefault();
61
+ const targetId = e.target.getAttribute('href').replace('#', '');
62
+ const target = document.getElementById(targetId);
63
+ if (target) {
64
+ target.scrollIntoView({ behavior: 'smooth' });
65
+ const url = window.location.pathname + window.location.search + '#' + targetId;
66
+ history.replaceState(null, '', url);
67
+ }
68
+ }
69
+ });
70
+ </script>
@@ -6,11 +6,13 @@
6
6
  <meta name='viewport' content='width=device-width, initial-scale=1'>
7
7
  <link rel="icon" type="image/png" sizes="48x48" href="./media/icon48.png">
8
8
  <link rel="manifest" href="./manifest.json" />
9
- <link rel="stylesheet" href="./kempo.min.css" />
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/kempo-css@2.1.3/dist/kempo.min.css" />
10
+ <link rel="stylesheet" href="./theme.css" />
11
+ <script src="./init.js"></script>
10
12
  </head>
11
13
  <body>
12
- <main>
13
- <a href="./" class="btn">Home</a>
14
+ <k-import src="./nav.inc.html"></k-import>
15
+ <k-main>
14
16
  <h1>Request & Response Objects</h1>
15
17
  <p>Learn how to work with HTTP requests and responses in Kempo Server.</p>
16
18
 
@@ -104,7 +106,8 @@
104
106
 
105
107
  <h3>Request Validation</h3>
106
108
  <pre><code class="hljs javascript"><span class="hljs-comment">// api/user/POST.js</span><br /><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">request, response</span>) </span>{<br /> <span class="hljs-keyword">try</span> {<br /> <span class="hljs-keyword">const</span> userData = <span class="hljs-keyword">await</span> request.json();<br /> <br /> <span class="hljs-comment">// Validate required fields</span><br /> <span class="hljs-keyword">const</span> requiredFields = [<span class="hljs-string">'name'</span>, <span class="hljs-string">'email'</span>, <span class="hljs-string">'password'</span>];<br /> <span class="hljs-keyword">const</span> missingFields = requiredFields.filter(<span class="hljs-function"><span class="hljs-params">field</span> =></span> !userData[field]);<br /> <br /> <span class="hljs-keyword">if</span> (missingFields.length > <span class="hljs-number">0</span>) {<br /> <span class="hljs-keyword">return</span> response.status(<span class="hljs-number">400</span>).json({<br /> error: <span class="hljs-string">'Missing required fields'</span>,<br /> missingFields<br /> });<br /> }<br /> <br /> <span class="hljs-comment">// Validate email format</span><br /> <span class="hljs-keyword">const</span> emailRegex = <span class="hljs-regexp">/^[^\s@]+@[^\s@]+\.[^\s@]+$/</span>;<br /> <span class="hljs-keyword">if</span> (!emailRegex.test(userData.email)) {<br /> <span class="hljs-keyword">return</span> response.status(<span class="hljs-number">400</span>).json({<br /> error: <span class="hljs-string">'Invalid email format'</span><br /> });<br /> }<br /> <br /> <span class="hljs-comment">// Create user</span><br /> <span class="hljs-keyword">const</span> newUser = <span class="hljs-keyword">await</span> createUser(userData);<br /> <br /> response.status(<span class="hljs-number">201</span>).json({<br /> message: <span class="hljs-string">'User created successfully'</span>,<br /> user: {<br /> id: newUser.id,<br /> name: newUser.name,<br /> email: newUser.email<br /> }<br /> });<br /> } <span class="hljs-keyword">catch</span> (error) {<br /> response.status(<span class="hljs-number">400</span>).json({ error: <span class="hljs-string">'Invalid request data'</span> });<br /> }<br />}</code></pre>
107
- </main>
109
+ </k-main>
108
110
  <div style="height:25vh"></div><div style="height:25vh"></div>
111
+ <script type="module" src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"></script>
109
112
  </body>
110
113
  </html>
package/docs/routing.html CHANGED
@@ -2,16 +2,18 @@
2
2
  <head>
3
3
  <meta charset='utf-8'>
4
4
  <meta http-equiv='X-UA-Compatible' content='IE=edge'>
5
- <title>Routes & Routing - Kempo Server</title>
5
+ <title>Routing - Kempo Server</title>
6
6
  <meta name='viewport' content='width=device-width, initial-scale=1'>
7
7
  <link rel="icon" type="image/png" sizes="48x48" href="./media/icon48.png">
8
8
  <link rel="manifest" href="./manifest.json" />
9
- <link rel="stylesheet" href="./kempo.min.css" />
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/kempo-css@2.1.3/dist/kempo.min.css" />
10
+ <link rel="stylesheet" href="./theme.css" />
11
+ <script src="./init.js"></script>
10
12
  </head>
11
13
  <body>
12
- <main>
13
- <a href="./" class="btn">Home</a>
14
- <h1>Routes & Routing</h1>
14
+ <k-import src="./nav.inc.html"></k-import>
15
+ <k-main>
16
+ <h1>Routing</h1>
15
17
  <p>Learn how Kempo Server's file-based routing system works.</p>
16
18
 
17
19
  <h2>How Routes Work</h2>
@@ -81,7 +83,8 @@
81
83
  <li><code>index.html</code> / <code>index.htm</code> &mdash; served as static</li>
82
84
  </ol>
83
85
  <p>This applies to both exact and wildcard custom routes. For example, with a wildcard route <code>"/api/**": "../api/**"</code>, a request to <code>/api/auth/session</code> resolves to the <code>../api/auth/session/</code> directory and executes the appropriate route file (e.g. <code>GET.js</code> for GET requests).</p>
84
- </main>
86
+ </k-main>
85
87
  <div style="height:25vh"></div>
88
+ <script type="module" src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"></script>
86
89
  </body>
87
90
  </html>
package/docs/theme.css CHANGED
@@ -1,8 +1,6 @@
1
1
  :root {
2
- --c_primary: rgb(153, 51, 255);
3
- --c_primary__hover: rgb(119, 17, 221);
2
+ --c_primary: hsl(262, 52%, 47%);
4
3
  --c_secondary: rgb(51, 102, 255);
5
- --c_secondary__hover: rgb(17, 68, 221);
6
4
  --tc_primary: light-dark(#93f, rgb(187, 102, 255));
7
5
  --tc_secondary: light-dark(#36f, rgb(138, 180, 248));
8
6
  --c_highlight: light-dark(rgba(153, 51, 255, 0.25), rgba(153, 51, 255, 0.25));
@@ -132,6 +132,10 @@ Use `customRoutes` to redirect all page paths to the SPA entry:
132
132
  ## Utility Modules
133
133
 
134
134
  ```js
135
+ import rescan from 'kempo-server/rescan';
136
+ // rescan() → Promise<number> — triggers a file rescan on the running server, returns new file count
137
+ // Works from anywhere in the same Node process: route handlers, middleware, file watchers, scheduled tasks
138
+
135
139
  import { getArgs } from 'kempo-server/utils/cli';
136
140
  // getArgs(mapping?) → parsed argv object
137
141
 
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "kempo-server",
3
3
  "type": "module",
4
- "version": "2.1.1",
4
+ "version": "2.2.0",
5
5
  "description": "A lightweight, zero-dependency, file based routing server.",
6
6
  "exports": {
7
+ "./rescan": "./dist/rescan.js",
7
8
  "./utils/cli": "./dist/utils/cli.js",
8
9
  "./utils/fs-utils": "./dist/utils/fs-utils.js"
9
10
  },
package/scripts/build.js CHANGED
@@ -79,19 +79,6 @@ const build = async () => {
79
79
  await processJsFile(join(utilsDir, file), join(distDir, 'utils', file));
80
80
  }
81
81
 
82
- console.log('Copying kempo.min.css to docs...');
83
-
84
- // Copy kempo.min.css to docs directory
85
- const cssSource = join(rootDir, 'node_modules', 'kempo-css', 'dist', 'kempo.min.css');
86
- const cssTarget = join(docsDir, 'kempo.min.css');
87
-
88
- try {
89
- await copyFile(cssSource, cssTarget);
90
- console.log('✓ Copied kempo.min.css to docs');
91
- } catch (error) {
92
- console.warn('⚠ Could not copy kempo.min.css:', error.message);
93
- }
94
-
95
82
  console.log('Build completed successfully!');
96
83
 
97
84
  } catch (error) {
package/src/rescan.js ADDED
@@ -0,0 +1,14 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ const emitter = new EventEmitter();
4
+
5
+ export const onRescan = callback => {
6
+ emitter.on('rescan', callback);
7
+ };
8
+
9
+ export default () => new Promise((resolve, reject) => {
10
+ emitter.emit('rescan', (error, fileCount) => {
11
+ if(error) reject(error);
12
+ else resolve(fileCount);
13
+ });
14
+ });
package/src/router.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  securityMiddleware,
17
17
  loggingMiddleware
18
18
  } from './builtinMiddleware.js';
19
+ import { onRescan } from './rescan.js';
19
20
 
20
21
  export default async (flags, log) => {
21
22
  log('Initializing router', 3);
@@ -94,7 +95,18 @@ export default async (flags, log) => {
94
95
 
95
96
  let files = await getFiles(rootPath, config, log);
96
97
  log(`Initial scan found ${files.length} files`, 2);
97
-
98
+
99
+ onRescan(async done => {
100
+ try {
101
+ files = await getFiles(rootPath, config, log);
102
+ log(`Rescan found ${files.length} files`, 2);
103
+ done(null, files.length);
104
+ } catch(error) {
105
+ log(`Rescan failed: ${error.message}`, 1);
106
+ done(error);
107
+ }
108
+ });
109
+
98
110
  // Initialize middleware runner
99
111
  const middlewareRunner = new MiddlewareRunner();
100
112
 
@@ -0,0 +1,69 @@
1
+ import http from 'http';
2
+ import {withTestDir} from './utils/test-dir.js';
3
+ import {write} from './utils/file-writer.js';
4
+ import {randomPort} from './utils/port.js';
5
+ import {httpGet} from './utils/http.js';
6
+ import router from '../src/router.js';
7
+ import rescan from '../src/rescan.js';
8
+
9
+ export default {
10
+ 'rescan() triggers file rescan and returns file count': async ({pass, fail}) => {
11
+ await withTestDir(async dir => {
12
+ const prev = process.cwd();
13
+ process.chdir(dir);
14
+ const flags = {root: '.', logging: 0};
15
+ const logFn = () => {};
16
+
17
+ await write(dir, '.config.json', JSON.stringify({
18
+ maxRescanAttempts: 0
19
+ }));
20
+ await write(dir, 'index.html', '<h1>Home</h1>');
21
+
22
+ const handler = await router(flags, logFn);
23
+ const server = http.createServer(handler);
24
+ const port = randomPort();
25
+ await new Promise(r => server.listen(port, r));
26
+ await new Promise(r => setTimeout(r, 50));
27
+
28
+ const miss = await httpGet(`http://localhost:${port}/added.html`);
29
+ if(miss.res.statusCode !== 404) {
30
+ server.close();
31
+ process.chdir(prev);
32
+ return fail('should 404 before file exists');
33
+ }
34
+
35
+ await write(dir, 'added.html', '<h1>Added</h1>');
36
+
37
+ const stillMiss = await httpGet(`http://localhost:${port}/added.html`);
38
+ if(stillMiss.res.statusCode !== 404) {
39
+ server.close();
40
+ process.chdir(prev);
41
+ return fail('should still 404 with maxRescanAttempts=0');
42
+ }
43
+
44
+ const count = await rescan();
45
+ if(typeof count !== 'number' || count < 2) {
46
+ server.close();
47
+ process.chdir(prev);
48
+ return fail(`rescan should return file count, got: ${count}`);
49
+ }
50
+
51
+ const hit = await httpGet(`http://localhost:${port}/added.html`);
52
+ if(hit.res.statusCode !== 200) {
53
+ server.close();
54
+ process.chdir(prev);
55
+ return fail('should serve file after rescan()');
56
+ }
57
+
58
+ if(!hit.body.toString().includes('Added')) {
59
+ server.close();
60
+ process.chdir(prev);
61
+ return fail('should serve correct content after rescan');
62
+ }
63
+
64
+ server.close();
65
+ process.chdir(prev);
66
+ });
67
+ pass('rescan() works from imported function');
68
+ },
69
+ };
@@ -1 +0,0 @@
1
- :root{color-scheme:light;--ff_body:"Helvetica Neue",Helvetica,Arial,sans-serif;--ff_heading:"Helvetica Neue",Helvetica,Arial,sans-serif;--ff_mono:Consolas,monaco,monospace;--fs_base:16px;--fs_small:calc(0.6 * var(--fs_base));--fs_large:calc(1.5 * var(--fs_base));--fs_h6:var(--fs_base);--fs_h5:calc(1.25 * var(--fs_base));--fs_h4:calc(1.5 * var(--fs_base));--fs_h3:calc(1.75 * var(--fs_base));--fs_h2:calc(2 * var(--fs_base));--fs_h1:calc(2.5 * var(--fs_base));--fw_base:400;--fw_bold:700;--spacer:1rem;--spacer_h:calc(0.5 * var(--spacer));--spacer_q:calc(0.25 * var(--spacer));--line-height:1.35em;--container_width:90rem;--animation_ms:256ms;--radius:0.25rem;--link_decoration:underline;--input_padding:var(--spacer_h) var(--spacer);--input_border_width:1px;--btn_padding:var(--spacer_h) var(--spacer);--c_bg:light-dark(rgb(249, 249, 249), rgb(51, 51, 51));--c_bg__inv:light-dark(rgb(51, 51, 51), rgb(249, 249, 249));--c_bg__alt:light-dark(rgb(238, 238, 238), rgb(34, 34, 34));--c_overscroll:light-dark(rgb(255, 255, 255), rgb(0, 0, 0));--c_border:light-dark(rgb(204, 204, 204), rgb(119, 119, 119));--c_border__inv:light-dark(rgb(119, 119, 119), rgb(204, 204, 204));--c_primary:rgb(51, 102, 255);--c_primary__hover:rgb(17, 68, 221);--c_secondary:rgb(153, 51, 255);--c_secondary__hover:rgb(119, 17, 221);--c_success:rgb(0, 136, 0);--c_success__hover:rgb(0, 102, 0);--c_warning:rgb(255, 102, 0);--c_warning__hover:rgb(221, 68, 0);--c_danger:rgb(255, 0, 51);--c_danger__hover:rgb(221, 0, 17);--c_input_accent:rgb(51, 102, 255);--c_input_border:var(--c_border);--c_highlight:light-dark(rgba(41, 100, 210, 0.25), rgba(0, 89, 255, 0.25));--tc:light-dark(rgba(0, 0, 0, 0.93), rgba(255, 255, 255, 0.93));--tc_dark:light-dark(rgba(0, 0, 0, 0.93), rgba(0, 0, 0, 0.93));--tc_light:light-dark(rgba(255, 255, 255, 0.93), rgba(255, 255, 255, 0.93));--tc_inv:light-dark(rgba(255, 255, 255, 0.93), rgba(0, 0, 0, 0.93));--tc_muted:light-dark(rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.5));--tc_on_primary:light-dark(rgba(255, 255, 255, 0.93), rgba(255, 255, 255, 0.93));--tc_on_secondary:light-dark(rgba(255, 255, 255, 0.93), rgba(255, 255, 255, 0.93));--tc_on_success:light-dark(rgba(255, 255, 255, 0.93), rgba(255, 255, 255, 0.93));--tc_on_warning:light-dark(rgba(255, 255, 255, 0.93), rgba(255, 255, 255, 0.93));--tc_on_danger:light-dark(rgba(255, 255, 255, 0.93), rgba(255, 255, 255, 0.93));--c_overlay:rgba(0, 0, 0, 0.5);--tc_primary:light-dark(#36f, rgb(138, 180, 248));--tc_secondary:light-dark(#93f, rgb(187, 102, 255));--tc_success:light-dark(#080, rgb(102, 187, 102));--tc_warning:light-dark(#f60, rgb(255, 153, 51));--tc_danger:light-dark(#f03, rgb(255, 85, 119));--btn_box_shadow:0 0 0 transparent;--btn_box_shadow__hover:0 0 0 transparent;--btn_border:transparent;--btn_bg:light-dark(rgb(221, 221, 221), rgb(170, 170, 170));--btn_bg__hover:light-dark(rgb(204, 204, 204), rgb(187, 187, 187));--btn_tc:light-dark(rgba(0, 0, 0, 0.93), rgba(0, 0, 0, 0.93));--btn_transparent__hover:light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));--tc_link:var(--tc_primary);--tc_link__hover:var(--tc_secondary);--tc_link__inv:var(--tc_primary__inv);--tc_link__inv__hover:var(--tc_secondary__inv);--focus_shadow:0 0 2px 2px var(--c_primary);--focus_shadow_on_primary:0 0 2px 2px var(--tc_on_primary);--input_bg:light-dark(white, var(--c_bg__alt));--input_tc:light-dark(rgba(0, 0, 0, 0.93), var(--tc));--drop_shadow__light:0 0.25rem 0.5rem rgba(0, 0, 0, 0.333);--drop_shadow__dark:0 0.25rem 0.5rem rgba(0, 0, 0, 0.5);--drop_shadow:var(--drop_shadow__light);--date_picker_icon_filter:light-dark(invert(0), invert(1));--elevation_-2_bg:light-dark(rgb(215, 215, 215), rgb(25, 25, 25));--elevation_-1_bg:light-dark(rgb(232, 232, 232), rgb(38, 38, 38));--elevation_0_bg:var(--c_bg);--elevation_1_bg:light-dark(rgb(255, 255, 255), rgb(64, 64, 64));--elevation_2_bg:light-dark(rgb(255, 255, 255), rgb(77, 77, 77));--elevation_3_bg:light-dark(rgb(255, 255, 255), rgb(90, 90, 90));--elevation_-2_shadow__light:inset 0 2px 6px rgba(0, 0, 0, 0.18),inset 0 1px 3px rgba(0, 0, 0, 0.12);--elevation_-1_shadow__light:inset 0 1px 3px rgba(0, 0, 0, 0.1),inset 0 1px 2px rgba(0, 0, 0, 0.06);--elevation_0_shadow:none;--elevation_1_shadow__light:0 1px 3px rgba(0, 0, 0, 0.1),0 1px 2px rgba(0, 0, 0, 0.16);--elevation_2_shadow__light:0 3px 6px rgba(0, 0, 0, 0.12),0 2px 4px rgba(0, 0, 0, 0.1);--elevation_3_shadow__light:0 8px 16px rgba(0, 0, 0, 0.14),0 3px 6px rgba(0, 0, 0, 0.1);--elevation_-2_shadow__dark:inset 0 2px 8px rgba(0, 0, 0, 0.5),inset 0 1px 4px rgba(0, 0, 0, 0.4);--elevation_-1_shadow__dark:inset 0 1px 4px rgba(0, 0, 0, 0.35),inset 0 1px 2px rgba(0, 0, 0, 0.25);--elevation_1_shadow__dark:0 2px 6px rgba(0, 0, 0, 0.5),0 1px 3px rgba(0, 0, 0, 0.4);--elevation_2_shadow__dark:0 4px 12px rgba(0, 0, 0, 0.55),0 2px 4px rgba(0, 0, 0, 0.45);--elevation_3_shadow__dark:0 8px 20px rgba(0, 0, 0, 0.6),0 4px 8px rgba(0, 0, 0, 0.5);--elevation_-2_shadow:var(--elevation_-2_shadow__light);--elevation_-1_shadow:var(--elevation_-1_shadow__light);--elevation_1_shadow:var(--elevation_1_shadow__light);--elevation_2_shadow:var(--elevation_2_shadow__light);--elevation_3_shadow:var(--elevation_3_shadow__light)}[theme=light]{color-scheme:light}[theme=dark]{color-scheme:dark;--drop_shadow:var(--drop_shadow__dark);--elevation_-2_shadow:var(--elevation_-2_shadow__dark);--elevation_-1_shadow:var(--elevation_-1_shadow__dark);--elevation_1_shadow:var(--elevation_1_shadow__dark);--elevation_2_shadow:var(--elevation_2_shadow__dark);--elevation_3_shadow:var(--elevation_3_shadow__dark)}[theme=auto]{color-scheme:light dark}@media (prefers-color-scheme:dark){[theme=auto]{--drop_shadow:var(--drop_shadow__dark);--elevation_-2_shadow:var(--elevation_-2_shadow__dark);--elevation_-1_shadow:var(--elevation_-1_shadow__dark);--elevation_1_shadow:var(--elevation_1_shadow__dark);--elevation_2_shadow:var(--elevation_2_shadow__dark);--elevation_3_shadow:var(--elevation_3_shadow__dark)}}:root{interpolate-size:allow-keywords}*,::after,::before{font-family:inherit;box-sizing:border-box;line-height:var(--line-height)}blockquote,body,code,dd,dl,dt,h1,h2,h3,h4,h5,h6,li,ol,p,pre,ul{margin:0;padding:.1px}html{font-family:var(--ff_body);font-size:var(--fs_base);font-weight:var(--fw_base);color:var(--tc);scrollbar-gutter:stable}::selection{background:var(--c_highlight)}body{min-height:100vh;background-color:var(--c_bg);color:var(--tc);overflow-y:scroll;font-family:var(--ff_body);position:relative}body.no-scroll{overflow:hidden!important}.container,main{max-width:var(--container_width);margin-left:auto;margin-right:auto;padding-top:var(--spacer);padding-left:var(--spacer);padding-right:var(--spacer)}nav>.link,nav>a{display:inline-block;padding:var(--spacer)!important;text-decoration:none}menu{margin:0;padding:0}menu a{display:block;padding:var(--spacer_q);text-decoration:none;color:inherit}summary{cursor:pointer;margin-bottom:var(--sapcer);outline:0;box-shadow:0 0 0 transparent;transition:box-shadow var(--animation_ms);border-radius:var(--radius)}summary:focus{box-shadow:var(--focus_shadow)}.d-b{display:block!important}.d-ib{display:inline-block!important}.d-g{display:grid!important}.d-i{display:inline!important}.d-n{display:none!important}.d-f{display:flex!important;flex-wrap:wrap}.d-if{display:inline-flex!important;flex-wrap:wrap}@media (min-width:1024px){.d-d-b{display:block!important}.d-d-ib{display:inline-block!important}.d-d-g{display:grid!important}.d-d-i{display:inline!important}.d-d-n{display:none!important}.d-d-f{display:flex!important;flex-wrap:wrap}.d-d-if{display:inline-flex!important;flex-wrap:wrap}}@media (min-width:769px) and (max-width:1023px){.t-d-b{display:block!important}.t-d-ib{display:inline-block!important}.t-d-g{display:grid!important}.t-d-i{display:inline!important}.t-d-n{display:none!important}.t-d-f{display:flex!important;flex-wrap:wrap}.t-d-if{display:inline-flex!important;flex-wrap:wrap}}@media (max-width:768px){.m-d-b{display:block!important}.m-d-ib{display:inline-block!important}.m-d-g{display:grid!important}.m-d-i{display:inline!important}.m-d-n{display:none!important}.m-d-f{display:flex!important;flex-wrap:wrap}.m-d-if{display:inline-flex!important;flex-wrap:wrap}}.flex,.flex-1{flex:1 1 auto}.flex-0{flex:0 0}.flex-2{flex:2 2 auto}.flex-3{flex:3 3 auto}.flex-4{flex:4 4 auto}.flex-5{flex:5 5 auto}.flex-6{flex:6 6 auto}.flex-7{flex:7 7 auto}.flex-8{flex:8 8 auto}.flex-9{flex:9 9 auto}.flex-10{flex:10 10 auto}@media (min-width:1024px){.d-d-b{display:block!important}.d-d-ib{display:inline-block!important}.d-d-g{display:grid!important}.d-d-i{display:inline!important}.d-d-n{display:none!important}.d-d-if{display:inline-flex!important;flex-wrap:wrap}.d-d-f{display:flex!important;flex-wrap:wrap}.d-flex,.d-flex-1{flex:1 1 auto}.d-flex-0{flex:0 0}.d-flex-2{flex:2 2 auto}.d-flex-3{flex:3 3 auto}.d-flex-4{flex:4 4 auto}.d-flex-5{flex:5 5 auto}.d-flex-6{flex:6 6 auto}.d-flex-7{flex:7 7 auto}.d-flex-8{flex:8 8 auto}.d-flex-9{flex:9 9 auto}.d-flex-10{flex:10 10 auto}}@media (min-width:769px) and (max-width:1023px){.t-d-b{display:block!important}.t-d-ib{display:inline-block!important}.t-d-g{display:grid!important}.t-d-i{display:inline!important}.t-d-n{display:none!important}.t-d-if{display:inline-flex!important;flex-wrap:wrap}.t-d-f{display:flex!important;flex-wrap:wrap}.t-flex,.t-flex-1{flex:1 1 auto}.t-flex-0{flex:0 0}.t-flex-2{flex:2 2 auto}.t-flex-3{flex:3 3 auto}.t-flex-4{flex:4 4 auto}.t-flex-5{flex:5 5 auto}.t-flex-6{flex:6 6 auto}.t-flex-7{flex:7 7 auto}.t-flex-8{flex:8 8 auto}.t-flex-9{flex:9 9 auto}.t-flex-10{flex:10 10 auto}}@media (max-width:768px){.m-d-b{display:block!important}.m-d-ib{display:inline-block!important}.m-d-g{display:grid!important}.m-d-i{display:inline!important}.m-d-n{display:none!important}.m-d-if{display:inline-flex!important;flex-wrap:wrap}.m-d-f{display:flex!important;flex-wrap:wrap}.m-flex,.m-flex-1{flex:1 1 auto}.m-flex-0{flex:0 0}.m-flex-2{flex:2 2 auto}.m-flex-3{flex:3 3 auto}.m-flex-4{flex:4 4 auto}.m-flex-5{flex:5 5 auto}.m-flex-6{flex:6 6 auto}.m-flex-7{flex:7 7 auto}.m-flex-8{flex:8 8 auto}.m-flex-9{flex:9 9 auto}.m-flex-10{flex:10 10 auto}}.fixed{position:fixed;top:0;width:100%;z-index:99;box-shadow:none;transition:box-shadow var(--animation_ms)}.fixed.scrolled{box-shadow:var(--elevation_2_shadow)}.small,small{font-size:var(--fs_small)!important}.large{font-size:var(--fs_large)!important}.h1,.h2,.h3,.h4,.h5,.h6,b,h1,h2,h3,h4,h5,h6,strong{font-weight:var(--fw_bold)}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:var(--ff_heading)}.h1,h1{font-size:var(--fs_h1)}.h2,h2{font-size:var(--fs_h2)}.h3,h3{font-size:var(--fs_h3)}.h4,h4{font-size:var(--fs_h4)}.h5,h5{font-size:var(--fs_h5)}.h6,h6{font-size:var(--fs_h6)}blockquote,dd,dl,h1,h2,h3,h4,h5,h6,hr,ol,p,pre,ul{margin-bottom:var(--spacer)}dl,ol,ul{padding-left:calc(1.5 * var(--spacer))}blockquote{border-left:2px solid var(--c_border);padding:var(--spacer)}mark{background-color:var(--c_highlight);color:inherit}.ff-mono,code,pre{font-family:var(--ff_mono)}code{background-color:var(--c_bg__alt);border-radius:var(--radius);word-break:break-word;padding:calc(.125 * var(--spacer)) var(--spacer_q)}pre code{display:block;padding:var(--spacer);word-break:normal;overflow:auto;white-space:pre-wrap}output{display:block;border:1px solid var(--c_border);border-radius:var(--radius);padding:var(--spacer) var(--spacer) 0 var(--spacer)}.ta-left{text-align:left}.ta-center{text-align:center}.ta-right{text-align:right}.link,a{color:var(--tc_link);box-shadow:0 0 0 transparent;transition:color var(--animation_ms),box-shadow var(--animation_ms);outline:0;border-radius:var(--radius);text-decoration:var(--link_decoration)}.link:hover,a:hover{color:var(--tc_link__hover)}.link:focus-visible,a:focus{box-shadow:var(--focus_shadow)}.no-link,.no-link:hover{text-decoration:none;color:inherit}hr{border:none;border-top:1px solid var(--c_border)}small{font-size:.75em}dl{padding:0;margin:0}dd,dt{padding-left:var(--spacer);border-left:2px solid var(--c_border)}dt{padding-top:var(--spacer_h)}dd{padding-bottom:var(--spacer_h)}dd+dd{margin-top:calc(-1 * var(--spacer));padding-top:0}li ul{margin-bottom:0}.td-n{text-decoration:none}.p,.pt,.py{padding-top:var(--spacer)!important}.p,.pr,.px{padding-right:var(--spacer)!important}.p,.pb,.py{padding-bottom:var(--spacer)!important}.p,.pl,.px{padding-left:var(--spacer)!important}.ph,.pth,.pyh{padding-top:var(--spacer_h)!important}.ph,.prh,.pxh{padding-right:var(--spacer_h)!important}.pbh,.ph,.pyh{padding-bottom:var(--spacer_h)!important}.ph,.plh,.pxh{padding-left:var(--spacer_h)!important}.pq,.ptq,.pyq{padding-top:var(--spacer_q)!important}.pq,.prq,.pxq{padding-right:var(--spacer_q)!important}.pbq,.pq,.pyq{padding-bottom:var(--spacer_q)!important}.plq,.pq,.pxq{padding-left:var(--spacer_q)!important}.p0,.pt0,.py0{padding-top:.1px!important}.p0,.pr0,.px0{padding-right:.1px!important}.p0,.pb0,.py0{padding-bottom:.1px!important}.p0,.pl0,.px0{padding-left:.1px!important}.m,.mt,.my{margin-top:var(--spacer)!important}.m,.mr,.mx{margin-right:var(--spacer)!important}.m,.mb,.my{margin-bottom:var(--spacer)!important}.m,.ml,.mx{margin-left:var(--spacer)!important}.mh,.mth,.myh{margin-top:var(--spacer_h)!important}.mh,.mrh,.mxh{margin-right:var(--spacer_h)!important}.mbh,.mh,.myh{margin-bottom:var(--spacer_h)!important}.mh,.mlh,.mxh{margin-left:var(--spacer_h)!important}.mq,.mtq,.myq{margin-top:var(--spacer_q)!important}.mq,.mrq,.mxq{margin-right:var(--spacer_q)!important}.mbq,.mq,.myq{margin-bottom:var(--spacer_q)!important}.mlq,.mq,.mxq{margin-left:var(--spacer_q)!important}.m0,.mt0,.my0{margin-top:0!important}.m0,.mr0,.mx0{margin-right:0!important}.m0,.mb0,.my0{margin-bottom:0!important}.m0,.ml0,.mx0{margin-left:0!important}.-m,.-mt,.-my{margin-top:calc(-1 * var(--spacer))!important}.-m,.-mr,.-mx{margin-right:calc(-1 * var(--spacer))!important}.-m,.-mb,.-my{margin-bottom:calc(-1 * var(--spacer))!important}.-m,.-ml,.-mx{margin-left:calc(-1 * var(--spacer))!important}.b,.bt,.by{border-top:1px solid var(--c_border)!important}.b,.br,.bx{border-right:1px solid var(--c_border)!important}.b,.bb,.by{border-bottom:1px solid var(--c_border)!important}.b,.bl,.bx{border-left:1px solid var(--c_border)!important}.b0,.bt0,.by0{border-top:none!important}.b0,.br0,.bx0{border-right:none!important}.b0,.bb0,.by0{border-bottom:none!important}.b0,.bl0,.bx0{border-left:none!important}.r,.rl,.rt,.rtl{border-top-left-radius:var(--radius)!important}.r,.rr,.rt,.rtr{border-top-right-radius:var(--radius)!important}.r,.rb,.rbr,.rr{border-bottom-right-radius:var(--radius)!important}.r,.rb,.rbl,.rl{border-bottom-left-radius:var(--radius)!important}.r0,.rl0,.rt0,.rtl0{border-top-left-radius:0!important}.r0,.rr0,.rt0,.rtr0{border-top-right-radius:0!important}.r0,.rb0,.rbr0,.rr0{border-bottom-right-radius:0!important}.r0,.rb0,.rbl0,.rl0{border-bottom-left-radius:0!important}.round{border-radius:9999rem!important}.row{display:flex;flex-wrap:wrap}.col{flex:1 1}.span-1{min-width:8.333%;flex-basis:8.333%}.span-2{min-width:16.666%;flex-basis:16.666%}.span-3{min-width:25%;flex-basis:25%}.span-4{min-width:33.333%;flex-basis:33.333%}.span-5{min-width:41.666%;flex-basis:41.666%}.span-6{min-width:50%;flex-basis:50%}.span-7{min-width:58.333%;flex-basis:58.333%}.span-8{min-width:66.666%;flex-basis:66.666%}.span-9{min-width:75%;flex-basis:75%}.span-10{min-width:83.333%;flex-basis:83.333%}.span-11{min-width:91.333%;flex-basis:91.333%}.span-12{min-width:100%;flex-basis:100%}@media (min-width:1024px){.d-span-1{min-width:8.333%;flex-basis:8.333%}.d-span-2{min-width:16.666%;flex-basis:16.666%}.d-span-3{min-width:25%;flex-basis:25%}.d-span-4{min-width:33.333%;flex-basis:33.333%}.d-span-5{min-width:41.666%;flex-basis:41.666%}.d-span-6{min-width:50%;flex-basis:50%}.d-span-7{min-width:58.333%;flex-basis:58.333%}.d-span-8{min-width:66.666%;flex-basis:66.666%}.d-span-9{min-width:75%;flex-basis:75%}.d-span-10{min-width:83.333%;flex-basis:83.333%}.d-span-11{min-width:91.333%;flex-basis:91.333%}.d-span-12{min-width:100%;flex-basis:100%}}@media (min-width:769px) and (max-width:1023px){.t-span-1{min-width:8.333%;flex-basis:8.333%}.t-span-2{min-width:16.666%;flex-basis:16.666%}.t-span-3{min-width:25%;flex-basis:25%}.t-span-4{min-width:33.333%;flex-basis:33.333%}.t-span-5{min-width:41.666%;flex-basis:41.666%}.t-span-6{min-width:50%;flex-basis:50%}.t-span-7{min-width:58.333%;flex-basis:58.333%}.t-span-8{min-width:66.666%;flex-basis:66.666%}.t-span-9{min-width:75%;flex-basis:75%}.t-span-10{min-width:83.333%;flex-basis:83.333%}.t-span-11{min-width:91.333%;flex-basis:91.333%}.t-span-12{min-width:100%;flex-basis:100%}}@media (max-width:768px){.m-span-1{min-width:8.333%;flex-basis:8.333%}.m-span-2{min-width:16.666%;flex-basis:16.666%}.m-span-3{min-width:25%;flex-basis:25%}.m-span-4{min-width:33.333%;flex-basis:33.333%}.m-span-5{min-width:41.666%;flex-basis:41.666%}.m-span-6{min-width:50%;flex-basis:50%}.m-span-7{min-width:58.333%;flex-basis:58.333%}.m-span-8{min-width:66.666%;flex-basis:66.666%}.m-span-9{min-width:75%;flex-basis:75%}.m-span-10{min-width:83.333%;flex-basis:83.333%}.m-span-11{min-width:91.333%;flex-basis:91.333%}.m-span-12{min-width:100%;flex-basis:100%}}.cols-2{grid-template-columns:repeat(2,1fr)}.cols-3{grid-template-columns:repeat(3,1fr)}.cols-4{grid-template-columns:repeat(4,1fr)}.cols-5{grid-template-columns:repeat(5,1fr)}.cols-6{grid-template-columns:repeat(6,1fr)}.cols-7{grid-template-columns:repeat(7,1fr)}.cols-8{grid-template-columns:repeat(8,1fr)}.cols-9{grid-template-columns:repeat(9,1fr)}.cols-10{grid-template-columns:repeat(10,1fr)}@media (min-width:1024px){.d-cols-2{grid-template-columns:repeat(2,1fr)}.d-cols-3{grid-template-columns:repeat(3,1fr)}.d-cols-4{grid-template-columns:repeat(4,1fr)}.d-cols-5{grid-template-columns:repeat(5,1fr)}.d-cols-6{grid-template-columns:repeat(6,1fr)}.d-cols-7{grid-template-columns:repeat(7,1fr)}.d-cols-8{grid-template-columns:repeat(8,1fr)}.d-cols-9{grid-template-columns:repeat(9,1fr)}.d-cols-10{grid-template-columns:repeat(10,1fr)}}@media (min-width:769px) and (max-width:1023px){.t-cols-2{grid-template-columns:repeat(2,1fr)}.t-cols-3{grid-template-columns:repeat(3,1fr)}.t-cols-4{grid-template-columns:repeat(4,1fr)}.t-cols-5{grid-template-columns:repeat(5,1fr)}.t-cols-6{grid-template-columns:repeat(6,1fr)}.t-cols-7{grid-template-columns:repeat(7,1fr)}.t-cols-8{grid-template-columns:repeat(8,1fr)}.t-cols-9{grid-template-columns:repeat(9,1fr)}.t-cols-10{grid-template-columns:repeat(10,1fr)}}@media (max-width:768px){.m-cols-2{grid-template-columns:repeat(2,1fr)}.m-cols-3{grid-template-columns:repeat(3,1fr)}.m-cols-4{grid-template-columns:repeat(4,1fr)}.m-cols-5{grid-template-columns:repeat(5,1fr)}.m-cols-6{grid-template-columns:repeat(6,1fr)}.m-cols-7{grid-template-columns:repeat(7,1fr)}.m-cols-8{grid-template-columns:repeat(8,1fr)}.m-cols-9{grid-template-columns:repeat(9,1fr)}.m-cols-10{grid-template-columns:repeat(10,1fr)}}.btn,button:not(.no-btn):not(.no-style),input[type=button],input[type=reset],input[type=submit]{display:inline-block;padding:var(--btn_padding);background-color:var(--btn_bg);border:1px solid var(--btn_border);cursor:pointer;outline:0;border-radius:var(--radius);color:var(--btn_tc);transition:background-color var(--animation_ms),box-shadow var(--animation_ms);text-decoration:none;box-shadow:var(--btn_box_shadow);font-size:inherit;vertical-align:middle}.btn:hover,button:not(.no-btn):not(.no-style):hover,input[type=button]:hover,input[type=reset]:hover,input[type=submit]:hover{background-color:var(--btn_bg__hover);color:var(--btn_tc);box-shadow:var(--btn_box_shadow__hover)}.btn:focus,button:not(.no-btn):not(.no-style):focus,input[type=button]:focus,input[type=reset]:focus,input[type=submit]:focus{box-shadow:var(--btn_box_shadow__hover),var(--focus_shadow);z-index:1}.btn[disabled],button:not(.no-btn):not(.no-style):disabled,input[type=button]:disabled,input[type=reset]:disabled,input[type=submit]:disabled{opacity:.6}.btn.primary,button:not(.no-btn).primary,input[type=button].primary,input[type=reset].primary,input[type=submit].primary{background-color:var(--c_primary)!important;--btn_tc:var(--tc_on_primary)}.btn.primary:hover,button:not(.no-btn).primary:hover,input[type=button].primary:hover,input[type=reset].primary:hover,input[type=submit].primary:hover{background-color:var(--c_primary__hover)!important}.btn.secondary,button:not(.no-btn).secondary,input[type=button].secondary,input[type=reset].secondary,input[type=submit].secondary{background-color:var(--c_secondary)!important;--btn_tc:var(--tc_on_secondary)}.btn.secondary:hover,button:not(.no-btn).secondary:hover,input[type=button].secondary:hover,input[type=reset].secondary:hover,input[type=submit].secondary:hover{background-color:var(--c_secondary__hover)!important}.btn.success,button:not(.no-btn).success,input[type=button].success,input[type=reset].success,input[type=submit].success{background-color:var(--c_success)!important;--btn_tc:var(--tc_on_success)}.btn.success:hover,button:not(.no-btn).success:hover,input[type=button].success:hover,input[type=reset].success:hover,input[type=submit].success:hover{background-color:var(--c_success__hover)!important}.btn.warning,button:not(.no-btn).warning,input[type=button].warning,input[type=reset].warning,input[type=submit].warning{background-color:var(--c_warning)!important;--btn_tc:var(--tc_on_warning)}.btn.warning:hover,button:not(.no-btn).warning:hover,input[type=button].warning:hover,input[type=reset].warning:hover,input[type=submit].warning:hover{background-color:var(--c_warning__hover)!important}.btn.danger,button:not(.no-btn).danger,input[type=button].danger,input[type=reset].danger,input[type=submit].danger{background-color:var(--c_danger)!important;--btn_tc:var(--tc_on_danger)}.btn.danger:hover,button:not(.no-btn).danger:hover,input[type=button].danger:hover,input[type=reset].danger:hover,input[type=submit].danger:hover{background-color:var(--c_danger__hover)!important}.btn.link,button:not(.no-btn):not(.no-style).link,input[type=button].link,input[type=reset].link,input[type=submit].link{background-color:transparent;color:inherit;box-shadow:0 0 0 transparent;border:none;padding:.1px;font-size:inherit}.btn-grp{display:inline-flex}.btn-grp .btn:not(:first-child),.btn-grp button:not(.no-btn):not(:first-child),.btn-grp input[type=button]:not(:first-child),.btn-grp input[type=reset]:not(:first-child),.btn-grp input[type=submit]:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0;border-left:1px solid rgba(0,0,0,.25)}.btn-grp .btn:not(:last-child),.btn-grp button:not(.no-btn):not(:last-child),.btn-grp input[type=button]:not(:last-child),.btn-grp input[type=reset]:not(:last-child),.btn-grp input[type=submit]:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.no-btn{display:inline;border:none;background-color:transparent;padding:0;font-size:inherit;font-family:inherit;cursor:pointer;outline:0;box-shadow:0 0 0 transparent;transition:box-shadow var(--animation_ms);border-radius:0;text-align:left;color:inherit}.no-btn:focus{box-shadow:var(--focus_shadow)}.full{display:block;width:100%}input:not([type=button]):not([type=submit]):not([type=reset]):not([type=radio]):not([type=checkbox]),select,textarea{display:block;width:100%;background-color:var(--input_bg);color:var(--input_tc);border:var(--input_border_width) solid var(--c_input_border);padding:var(--input_padding);border-radius:var(--radius);outline:0;transition:box-shadow var(--animation_ms)}input:not([type=button]):not([type=submit]):not([type=reset]):not([type=radio]):not([type=checkbox]):focus,input[type=checkbox]:focus,input[type=radio]:focus,select:focus,textarea:focus{box-shadow:var(--focus_shadow)}input:not([type=button]):not([type=submit]):not([type=reset]):not([type=radio]):not([type=checkbox]):disabled,input[type=checkbox]:disabled,input[type=radio]:disabled,select:disabled,textarea:disabled{opacity:.6}select[multiple],textarea{resize:vertical;max-height:75vh;height:6rem;min-height:4rem}select[multiple]{height:8rem}select{cursor:pointer}label{display:block;cursor:pointer;padding-bottom:var(--spacer_h)}label.checkbox,label.radio{display:inline-block;vertical-align:middle;width:calc(100% - 2em - (2 * var(--spacer_h)) - 6px)}input[type=checkbox],input[type=radio]{display:inline-block;width:1em;height:1em;cursor:pointer;vertical-align:middle;accent-color:var(--c_input_accent);margin:var(--spacer_q) var(--spacer_h);transition:background-color var(--animation_ms),color var(--animation_ms),box-shadow var(--animation_ms)}input[type=checkbox]{width:1.75em;height:1.75em;appearance:none;-webkit-appearance:none;background-color:transparent;border:2px solid var(--c_border);border-radius:var(--radius);vertical-align:-.5em;position:relative}input[type=checkbox]::before{content:"";position:absolute;inset:0;border-radius:calc(var(--radius) - 2px);background-color:transparent;mask-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="200 -760 560 560"><path d="m424-312 282-282-56-56-226 226-114-114-56 56 170 170Z"/></svg>');-webkit-mask-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="200 -760 560 560"><path d="m424-312 282-282-56-56-226 226-114-114-56 56 170 170Z"/></svg>');mask-size:contain;-webkit-mask-size:contain;mask-position:center;-webkit-mask-position:center;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;transition:background-color var(--animation_ms)}input[type=checkbox]:focus{box-shadow:var(--focus_shadow);outline:0}input[type=checkbox]:checked{background-color:var(--c_primary);border-color:var(--c_primary)}input[type=checkbox]:checked::before{background-color:var(--tc_on_primary)}input[type=radio]{width:1.75em;height:1.75em;appearance:none;-webkit-appearance:none;background-color:transparent;border:2px solid var(--c_border);border-radius:50%;vertical-align:-.5em}input[type=radio]:focus{box-shadow:var(--focus_shadow);outline:0}input[type=radio]:checked{border-color:var(--c_primary);background:radial-gradient(circle,var(--c_primary) 40%,transparent 45%)}select option{padding:var(--spacer_h) var(--spacer);background-color:var(--input_bg);color:var(--input_tc)}select[multiple]{padding:.1px}select[multiple] option{padding:var(--spacer_h) var(--spacer)}input[type=color]{padding:0!important;height:2.35em}input[type=color]::-webkit-color-swatch-wrapper{padding:0}input[type=color]::-webkit-color-swatch{border-radius:var(--radius,.25rem);cursor:pointer}input[type=date]::-webkit-calendar-picker-indicator,input[type=month]::-webkit-calendar-picker-indicator,input[type=search]::-webkit-search-cancel-button,input[type=time]::-webkit-calendar-picker-indicator,input[type=week]::-webkit-calendar-picker-indicator{filter:var(--date_picker_icon_filter)}.table-wrapper{overflow-x:auto}table{width:100%;border-spacing:0}th{font-weight:var(--fw_bold);text-align:left;background-color:var(--c_bg__alt);border-top:1px solid var(--c_border)}td,th{padding:var(--spacer_h) var(--spacer);border-bottom:1px solid var(--c_border);border-left:1px solid var(--c_border)}td:last-child,th:last-child{border-right:1px solid var(--c_border)}th:first-child{border-top-left-radius:var(--radius)}th:last-child{border-top-right-radius:var(--radius)}tr:last-child td:first-child{border-bottom-left-radius:var(--radius)}tr:last-child td:last-child{border-bottom-right-radius:var(--radius)}.bg-default{background-color:var(--c_bg)!important;color:var(--tc)!important}.bg-alt{background-color:var(--c_bg__alt)!important;color:var(--tc)!important}.bg-inv{--c_primary:var(--c_primary__inv);--c_primary__hover:var(--c_primary__inv__hover);--c_secondary:var(--c_secondary__inv);--c_secondary__hover:var(--c_secondary__inv__hover);--c_success:var(--c_success__inv);--c_success__hover:var(--c_success__inv__hover);--c_warning:var(--c_warning__inv);--c_warning__hover:var(--c_warning__inv__hover);--c_danger:var(--c_danger__inv);--c_danger__hover:var(--c_danger__inv__hover);--tc_link:var(--tc_link__inv);--tc_link__hover:var(--tc_link__inv__hover);background-color:var(--c_bg__inv)!important;color:var(--tc_inv)!important}.bg-primary{--tc_link:var(--tc_on_primary);--tc_link__hover:var(--tc_on_primary);--c_border:var(--tc_on_primary);background-color:var(--c_primary)!important;color:var(--tc_on_primary)!important;--focus_shadow:var(--focus_shadow_on_primary)}.bg-secondary{--tc_link:var(--tc_on_secondary);--tc_link__hover:var(--tc_on_secondary);background-color:var(--c_secondary)!important;color:var(--tc_on_secondary)!important}.bg-success{--tc_link:var(--tc_on_success);--tc_link__hover:var(--tc_on_success);background-color:var(--c_success)!important;color:var(--tc_on_success)!important}.bg-warning{--tc_link:var(--tc_on_warning);--tc_link__hover:var(--tc_on_warning);background-color:var(--c_warning)!important;color:var(--tc_on_warning)!important}.bg-danger{--tc_link:var(--tc_on_danger);--tc_link__hover:var(--tc_on_danger);background-color:var(--c_danger)!important;color:var(--tc_on_danger)!important}.tc-default{color:var(--tc)!important}.tc-inv{color:var(--tc__inv)!important}.tc-primary{color:var(--tc_primary)!important}.bg-inv .tc-primary,.is-inv .tc-primary{color:var(--tc_primary__inv)!important}.tc-secondary{color:var(--tc_secondary)!important}.bg-inv .tc-secondary,.is-inv .tc-secondary{color:var(--tc_secondary__inv)!important}.tc-success{color:var(--tc_success)!important}.bg-inv .tc-success,.is-inv .tc-success{color:var(--tc_success__inv)!important}.tc-warning{color:var(--tc_warning)!important}.bg-inv .tc-warning,.is-inv .tc-warning{color:var(--tc_warning__inv)!important}.tc-danger{color:var(--tc_danger)!important}.bg-inv .tc-danger,.is-inv .tc-danger{color:var(--tc_danger__inv)!important}.tc-muted{color:var(--tc_muted)!important}.card{border:1px solid var(--c_border);border-radius:var(--radius);padding-top:var(--spacer);padding-left:var(--spacer);padding-right:var(--spacer);margin-bottom:var(--spacer)}.drop-shadow{box-shadow:var(--drop_shadow)}.elevation--2{background-color:var(--elevation_-2_bg);box-shadow:var(--elevation_-2_shadow)}.elevation--1{background-color:var(--elevation_-1_bg);box-shadow:var(--elevation_-1_shadow)}.elevation-0{background-color:var(--elevation_0_bg);box-shadow:var(--elevation_0_shadow)}.elevation-1{background-color:var(--elevation_1_bg);box-shadow:var(--elevation_1_shadow)}.elevation-2{background-color:var(--elevation_2_bg);box-shadow:var(--elevation_2_shadow)}.elevation-3{background-color:var(--elevation_3_bg);box-shadow:var(--elevation_3_shadow)}.icon{display:inline-block;width:1.35em;vertical-align:top;margin-left:auto;margin-right:auto}iframe{border:none;width:100%}
package/docs/nav.inc.js DELETED
@@ -1,16 +0,0 @@
1
- document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
2
- await window.customElements.whenDefined('k-side-menu');
3
- document.getElementById('navSideMenu').toggle();
4
- });
5
- document.addEventListener('click', function(e) {
6
- if (e.target.matches('a[href^="#"]')) {
7
- e.preventDefault();
8
- const targetId = e.target.getAttribute('href').replace('#', '');
9
- const target = document.getElementById(targetId);
10
- if (target) {
11
- target.scrollIntoView({ behavior: 'smooth' });
12
- const url = window.location.pathname + window.location.search + '#' + targetId;
13
- history.replaceState(null, '', url);
14
- }
15
- }
16
- });