From 636a112965c66120d9d2195abcb6f7f3eaa6f69f Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Sun, 11 May 2025 12:50:45 -0700 Subject: [PATCH 01/51] Switch default layout and test to use preact --- lib/build-pages/resolve-layout.js | 2 +- lib/defaults/default.root.layout.js | 45 ++++++----- package.json | 3 +- test-cases/general-features/src/blog/page.js | 28 ++++--- .../src/layouts/blog.layout.js | 40 +++++----- .../src/layouts/root.layout.js | 74 +++++++++++-------- .../general-features/src/layouts/ts.layout.ts | 69 +++++++++-------- test-cases/general-features/src/page.vars.js | 42 ++++++----- 8 files changed, 168 insertions(+), 135 deletions(-) diff --git a/lib/build-pages/resolve-layout.js b/lib/build-pages/resolve-layout.js index 2d96ffc..e343151 100644 --- a/lib/build-pages/resolve-layout.js +++ b/lib/build-pages/resolve-layout.js @@ -19,7 +19,7 @@ * @param {any} params.children - The children content, either as a string or a render function. * @param {PageInfo} params.page - Info about the current page * @param {PageData[]} params.pages - An array of info about every page - * @returns {Promise} The rendered HTML string. + * @returns {Promise | string} The rendered HTML string. */ /** diff --git a/lib/defaults/default.root.layout.js b/lib/defaults/default.root.layout.js index a14f0ef..ccaf6b0 100644 --- a/lib/defaults/default.root.layout.js +++ b/lib/defaults/default.root.layout.js @@ -1,5 +1,5 @@ -// @ts-ignore -import { html, render } from 'uhtml-isomorphic' +import { html } from 'htm/preact' +import { render } from 'preact-render-to-string' /** * @template {Record} T @@ -29,25 +29,30 @@ export default function defaultRootLayout ({ /* pages */ /* page */ }) { - return render(String, html` + return /* html */` - - - ${title ? `${title}` : ''}${title && siteName ? ' | ' : ''}${siteName} - - ${scripts - ? scripts.map(script => html``) - : null} - ${styles - ? styles.map(style => html``) - : null} - - -
- ${typeof children === 'string' ? html([children]) : children /* Support both uhtml and string children. Optional. */} -
- + ${render(html` + + + ${title ? `${title}` : ''}${title && siteName ? ' | ' : ''}${siteName} + + ${scripts + ? scripts.map(script => html``) - : null} - ${styles - ? styles.map(style => html``) - : null} + + + ${scripts?.map(script => + html``).join('\n ')} + +` +} diff --git a/examples/markdown-settings/src/style.css b/examples/markdown-settings/src/style.css new file mode 100644 index 0000000..cccce56 --- /dev/null +++ b/examples/markdown-settings/src/style.css @@ -0,0 +1,138 @@ +/* Custom warning container */ +.custom-warning { + border: 2px solid #ff9800; + border-radius: 8px; + margin: 1.5rem 0; + overflow: hidden; + background-color: #fff3e0; +} + +.custom-warning .warning-title { + background-color: #ff9800; + color: white; + padding: 0.75rem 1rem; + font-weight: bold; + font-size: 1.1rem; +} + +.custom-warning .warning-content { + padding: 1rem; + color: #e65100; +} + +/* Custom info container */ +.custom-info { + border: 2px solid #2196f3; + border-radius: 8px; + margin: 1.5rem 0; + overflow: hidden; + background-color: #e3f2fd; +} + +.custom-info .info-title { + background-color: #2196f3; + color: white; + padding: 0.75rem 1rem; + font-weight: bold; + font-size: 1.1rem; +} + +.custom-info .info-content { + padding: 1rem; + color: #0d47a1; +} + +/* Custom details container */ +details { + border: 1px solid #ddd; + border-radius: 8px; + margin: 1.5rem 0; + padding: 0; + background-color: #f5f5f5; +} + +details summary { + padding: 1rem; + cursor: pointer; + font-weight: bold; + background-color: #e0e0e0; + border-radius: 8px 8px 0 0; + user-select: none; +} + +details[open] summary { + border-bottom: 1px solid #ddd; +} + +details .details-content { + padding: 1rem; + background-color: white; + border-radius: 0 0 8px 8px; +} + +/* Custom code blocks */ +.custom-code-block { + background-color: #f8f8f8; + border: 1px solid #e0e0e0; + border-radius: 6px; + padding: 1rem; + margin: 1rem 0; + overflow-x: auto; + position: relative; +} + +.custom-code-block::before { + content: attr(data-language); + position: absolute; + top: 0; + right: 0; + padding: 0.25rem 0.75rem; + background-color: #e0e0e0; + border-radius: 0 6px 0 6px; + font-size: 0.8rem; + color: #666; +} + +.custom-code-block code { + background: none; + padding: 0; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 0.9rem; + line-height: 1.5; +} + +/* General page styling */ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + line-height: 1.6; + color: #333; + max-width: 800px; + margin: 0 auto; + padding: 2rem; +} + +h1, h2, h3 { + color: #2c3e50; + margin-top: 2rem; +} + +h1 { + border-bottom: 2px solid #2c3e50; + padding-bottom: 0.5rem; +} + +code { + background-color: #f4f4f4; + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-size: 0.9em; +} + +a { + color: #2196f3; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} \ No newline at end of file diff --git a/lib/build-pages/index.js b/lib/build-pages/index.js index c6f6077..b663f9e 100644 --- a/lib/build-pages/index.js +++ b/lib/build-pages/index.js @@ -3,6 +3,9 @@ import { join } from 'path' import pMap from 'p-map' import { cpus } from 'os' +/** + * @import { BuilderOptions } from './page-builders/page-writer.js' + */ import { keyBy } from '../helpers/key-by.js' import { resolveVars } from './resolve-vars.js' import { resolveLayout } from './resolve-layout.js' @@ -113,6 +116,8 @@ export async function buildPagesDirect (src, dest, siteData, _opts) { warnings: [], } + // Note: markdown-it settings are now passed directly to builders through builderOptions + const [ defaultVars, bareGlobalVars, @@ -146,6 +151,12 @@ export async function buildPagesDirect (src, dest, siteData, _opts) { ...bareGlobalVars, } + // Create builder options from siteData + /** @type {BuilderOptions} */ + const builderOptions = { + markdownItSettingsPath: siteData.markdownItSettings?.filepath || null + } + // Mix in resolveVars, renderInnerPage and renderFullPage methods const pages = await pMap(siteData.pages, async (pageInfo) => { const pageData = new PageData({ @@ -155,6 +166,7 @@ export async function buildPagesDirect (src, dest, siteData, _opts) { globalClient: siteData?.globalClient?.outputRelname, defaultStyle: siteData?.defaultStyle, defaultClient: siteData?.defaultClient, + builderOptions, }) try { // Resolves async vars and binds the page to a reference to its layout fn diff --git a/lib/build-pages/page-builders/md/get-md.js b/lib/build-pages/page-builders/md/get-md.js index f137d74..01b41c3 100644 --- a/lib/build-pages/page-builders/md/get-md.js +++ b/lib/build-pages/page-builders/md/get-md.js @@ -1,3 +1,5 @@ +/** + */ import markdownIt from 'markdown-it' import markdownItFootnote from 'markdown-it-footnote' import markdownItHighlightjs from 'markdown-it-highlightjs' @@ -31,7 +33,11 @@ const mdOpts = { typographer: true, } -export function getMd () { +/** + * @param {string | null | undefined} [settingsPath] - Path to the markdown-it settings file + * @returns {Promise>} + */ +export async function getMd (settingsPath = null) { const md = markdownIt(mdOpts) .use(markdownItSub) .use(markdownItSup) @@ -52,6 +58,20 @@ export function getMd () { // disable autolinking for filenames md.linkify.tlds('.md', false) // markdown + + // Apply user settings if available + if (settingsPath) { + try { + const settingsModule = await import(settingsPath) + const settingsFunction = settingsModule.default + if (typeof settingsFunction === 'function') { + return await settingsFunction(md) + } + } catch (err) { + console.error('Error loading markdown-it settings:', err) + } + } + return md } @@ -59,11 +79,12 @@ export function getMd () { * Renders markdown, and accepts an optional markdown-it instance * @param {string} mdUnparsed unparsed markdown * @param {object} vars to expose to handlebars - * @param {markdownIt} [md] an instance of markdown - * @return {string} Rendered markdown to html + * @param {markdownIt?} [md] an instance of markdown + * @param {string | null | undefined} [settingsPath] Path to the markdown-it settings file + * @return {Promise} Rendered markdown to html */ -export function renderMd (mdUnparsed, vars, md) { - if (!md) md = getMd() +export async function renderMd (mdUnparsed, vars, md, settingsPath) { + if (!md) md = await getMd(settingsPath) // @ts-ignore if (vars?.vars?.handlebars) { const template = Handlebars.compile(mdUnparsed) diff --git a/lib/build-pages/page-builders/md/index.js b/lib/build-pages/page-builders/md/index.js index 192ea4d..92c210b 100644 --- a/lib/build-pages/page-builders/md/index.js +++ b/lib/build-pages/page-builders/md/index.js @@ -1,3 +1,6 @@ +/** + * @import markdownIt from 'markdown-it' + */ import assert from 'node:assert' import { readFile } from 'fs/promises' import yaml from 'js-yaml' @@ -5,15 +8,20 @@ import * as cheerio from 'cheerio' import { getMd, renderMd } from './get-md.js' -const md = getMd() +/** @type {markdownIt | null} */ +let md = null /** * Build all of the bundles using esbuild. * @template {Record} T * @type {import('../page-writer.js').PageBuilderType} */ -export async function mdBuilder ({ pageInfo }) { +export async function mdBuilder ({ pageInfo, options }) { assert(pageInfo.type === 'md', 'md builder requires an "md" page type') + + const markdownItSettingsPath = options?.markdownItSettingsPath || null + + if (!md) md = await getMd(markdownItSettingsPath) const fileContents = await readFile(pageInfo.pageFile.filepath, 'utf8') /** @type {object} */ @@ -30,11 +38,11 @@ export async function mdBuilder ({ pageInfo }) { mdUnparsed = fileContents } - const body = renderMd(mdUnparsed, { handlebars: false, ...frontMatter }, md) + const body = await renderMd(mdUnparsed, { handlebars: false, ...frontMatter }, md, markdownItSettingsPath) const title = cheerio.load(body)('h1').first().text().trim() return { vars: Object.assign({ title }, frontMatter), - pageLayout: async (vars) => renderMd(mdUnparsed, vars, md), + pageLayout: async (vars) => await renderMd(mdUnparsed, vars, md, markdownItSettingsPath), } } diff --git a/lib/build-pages/page-builders/page-writer.js b/lib/build-pages/page-builders/page-writer.js index 35c29be..f13621b 100644 --- a/lib/build-pages/page-builders/page-writer.js +++ b/lib/build-pages/page-builders/page-writer.js @@ -3,6 +3,13 @@ import { writeFile, mkdir } from 'fs/promises' /** * @typedef {import('../../identify-pages.js').PageInfo} PageInfo + * @typedef {import('../../builder.js').SiteData} SiteData + * @typedef {import('../../identify-pages.js').PageFileAsset} PageFileAsset + */ + +/** + * @typedef {Object} BuilderOptions + * @property {string | null | undefined} [markdownItSettingsPath] - Path to the markdown-it settings file */ /** @@ -38,6 +45,7 @@ import { writeFile, mkdir } from 'fs/promises' * * @param {object} params * @param {PageInfo} params.pageInfo + * @param {BuilderOptions} [params.options] * @returns {Promise>} - The results of the build step. */ diff --git a/lib/build-pages/page-data.js b/lib/build-pages/page-data.js index 2fefbd1..c60e846 100644 --- a/lib/build-pages/page-data.js +++ b/lib/build-pages/page-data.js @@ -6,6 +6,7 @@ import pretty from 'pretty' /** * @typedef {import('../identify-pages.js').PageInfo} PageInfo * @typedef {import('../builder.js').SiteData} SiteData + * @typedef {import('./page-builders/page-writer.js').BuilderOptions} BuilderOptions */ /** @@ -30,17 +31,19 @@ export class PageData { /** @type {T?} */ #renderedPostVars = null /** @type {string?} */ #defaultStyle = null /** @type {string?} */ #defaultClient = null + /** @type {BuilderOptions} */ builderOptions /** * Creates an instance of PageData. * * @param {object} options - The options object. - * @param {PageInfo} options.pageInfo - Page-specific data. - * @param {object} options.globalVars - Global variables available to all pages. - * @param {string | undefined} options.globalStyle - Global style path. - * @param {string | undefined} options.globalClient - Global client-side script path. - * @param {string?} options.defaultStyle - Default style path. - * @param {string?} options.defaultClient - Default client-side script path. + * @param {PageInfo} options.pageInfo - Page-specific data. + * @param {object} options.globalVars - Global variables available to all pages. + * @param {string | undefined} options.globalStyle - Global style path. + * @param {string | undefined} options.globalClient - Global client-side script path. + * @param {string?} options.defaultStyle - Default style path. + * @param {string?} options.defaultClient - Default client-side script path. + * @param {BuilderOptions} options.builderOptions - Options for page builders. */ constructor ({ pageInfo, @@ -49,11 +52,13 @@ export class PageData { globalClient, defaultStyle, defaultClient, + builderOptions, }) { this.pageInfo = pageInfo this.globalVars = globalVars this.#defaultStyle = defaultStyle this.#defaultClient = defaultClient + this.builderOptions = builderOptions if (globalStyle) { this.styles.push(`/${globalStyle}`) @@ -119,7 +124,7 @@ export class PageData { }) const builder = pageBuilders[type] - const { vars: builderVars } = await builder({ pageInfo }) + const { vars: builderVars } = await builder({ pageInfo, options: this.builderOptions }) this.builderVars = builderVars /** @type {object} */ @@ -166,10 +171,10 @@ export class PageData { */ async renderInnerPage ({ pages }) { if (!this.#initialized) throw new Error('Must be initialized before rendering inner pages') - const { pageInfo, styles, scripts, vars } = this + const { pageInfo, styles, scripts, vars, builderOptions } = this if (!pageInfo) throw new Error('A page is required to render') const builder = pageBuilders[pageInfo.type] - const { pageLayout } = await builder({ pageInfo }) + const { pageLayout } = await builder({ pageInfo, options: builderOptions }) const renderedPostVars = await this.#renderPostVars({ vars, styles, scripts, pages, page: pageInfo }) // @ts-ignore return await pageLayout({ vars: renderedPostVars, styles, scripts, pages, page: pageInfo }) diff --git a/lib/helpers/dom-stack-warning.js b/lib/helpers/dom-stack-warning.js index 1198f23..778318e 100644 --- a/lib/helpers/dom-stack-warning.js +++ b/lib/helpers/dom-stack-warning.js @@ -9,6 +9,7 @@ * 'DOM_STACK_WARNING_DUPLICATE_GLOBAL_STYLE' | * 'DOM_STACK_WARNING_DUPLICATE_GLOBAL_CLIENT' | * 'DOM_STACK_WARNING_DUPLICATE_ESBUILD_SETTINGS' | + * 'DOM_STACK_WARNING_DUPLICATE_MARKDOWN_IT_SETTINGS' | * 'DOM_STACK_WARNING_DUPLICATE_GLOBAL_VARS' * } DomStackWarningCode */ diff --git a/lib/identify-pages.js b/lib/identify-pages.js index 5915fc7..1d7886f 100644 --- a/lib/identify-pages.js +++ b/lib/identify-pages.js @@ -80,6 +80,13 @@ const esbuildSettingsNames = nodeHasTS ] : ['esbuild.settings.js', 'esbuild.settings.mjs', 'esbuild.settings.cjs'] +const markdownItSettingsNames = nodeHasTS + ? [ + 'markdown-it.settings.ts', 'markdown-it.settings.mts', 'markdown-it.settings.cts', + 'markdown-it.settings.js', 'markdown-it.settings.mjs', 'markdown-it.settings.cjs' + ] + : ['markdown-it.settings.js', 'markdown-it.settings.mjs', 'markdown-it.settings.cjs'] + /** * Shape the file walker object * @@ -205,6 +212,9 @@ export async function identifyPages (src, opts = {}) { /** @type {PageFileAsset | undefined } */ let esbuildSettings + /** @type {PageFileAsset | undefined } */ + let markdownItSettings + /** @type {DomStackWarning[]} */ const warnings = [] @@ -408,6 +418,17 @@ export async function identifyPages (src, opts = {}) { esbuildSettings = fileInfo } } + + if (markdownItSettingsNames.some(name => basename(fileName) === name)) { + if (markdownItSettings) { + warnings.push({ + code: 'DOM_STACK_WARNING_DUPLICATE_MARKDOWN_IT_SETTINGS', + message: `Skipping ${fileInfo.relname}. Duplicate markdown-it settings ${fileName} to ${markdownItSettings.filepath}`, + }) + } else { + markdownItSettings = fileInfo + } + } } } @@ -452,6 +473,7 @@ export async function identifyPages (src, opts = {}) { globalClient, globalVars, esbuildSettings, + markdownItSettings, /** @type {string?} Path to a default style */ defaultStyle: null, /** @type {string?} Path to a default client */ diff --git a/test-cases/general-features/index.test.js b/test-cases/general-features/index.test.js index 350b88e..5d16b70 100644 --- a/test-cases/general-features/index.test.js +++ b/test-cases/general-features/index.test.js @@ -37,6 +37,10 @@ test.describe('general-features', () => { client: false, style: false, }, + 'md-page/markdown-settings-test.html': { + client: false, + style: false + }, 'md-page/md-no-style-client/index.html': { client: false, style: false, @@ -81,6 +85,19 @@ test.describe('general-features', () => { ? 'Generated' : 'Did not generate'} a global client`) + // Special test for markdown-it.settings.js + const mdSettingsTestPath = path.join(dest, 'md-page/markdown-settings-test.html') + try { + const mdTestContent = await readFile(mdSettingsTestPath, 'utf8') + const mdTestDoc = cheerio.load(mdTestContent) + + // Check if our custom test-box container exists - this proves markdown-it.settings.js worked + const testBox = mdTestDoc('.test-box') + assert.ok(testBox.length > 0, 'markdown-it.settings.js was applied - custom container found') + } catch (err) { + assert.fail('Failed to verify markdown-it.settings.js customization: ' + err.message) + } + for (const [filePath, assertions] of Object.entries(pages)) { try { const fullPath = path.join(dest, filePath) @@ -89,6 +106,8 @@ test.describe('general-features', () => { const contents = await readFile(fullPath, 'utf8') const doc = cheerio.load(contents) + + const headScripts = Array.from(doc('head script[type="module"]')) diff --git a/test-cases/general-features/src/markdown-it.settings.js b/test-cases/general-features/src/markdown-it.settings.js new file mode 100644 index 0000000..a3592b1 --- /dev/null +++ b/test-cases/general-features/src/markdown-it.settings.js @@ -0,0 +1,99 @@ +/** + * Custom markdown-it settings for testing + * This adds a custom container for "test-box" that will be used in our test + */ +import markdownIt from 'markdown-it' + +/** + * Customize the markdown-it instance + * @param {markdownIt} md - The markdown-it instance + * @returns {markdownIt} - The modified markdown-it instance + */ +export default async function markdownItSettingsOverride(md) { + // Add custom container for test-box + md.use(createTestBoxPlugin()) + return md +} + +/** + * Creates a plugin that adds a custom container for test-box + * This is a simplified version of markdown-it-container + */ +function createTestBoxPlugin() { + const TEST_BOX_MARKER = 'test-box' + + return (md) => { + const container = (state, startLine, endLine, silent) => { + let pos = state.bMarks[startLine] + state.tShift[startLine] + let max = state.eMarks[startLine] + + // Check if the line starts with ::: + if (state.src.charCodeAt(pos) !== 0x3A /* : */ || + state.src.charCodeAt(pos + 1) !== 0x3A /* : */ || + state.src.charCodeAt(pos + 2) !== 0x3A /* : */) { + return false + } + + // Check for TEST_BOX_MARKER after ::: + let match = state.src.slice(pos + 3, max).trim() + if (match !== TEST_BOX_MARKER) { + return false + } + + // Don't process if we're in "silent" mode + if (silent) { + return true + } + + // Find the end marker + let nextLine = startLine + let found = false + + while (nextLine < endLine) { + nextLine++ + + // End of document + if (nextLine >= state.lineMax) { + break + } + + // Get positions + pos = state.bMarks[nextLine] + state.tShift[nextLine] + max = state.eMarks[nextLine] + + // Blank line, skip + if (pos >= max) { + continue + } + + // End marker check + if (state.src.charCodeAt(pos) === 0x3A /* : */ && + state.src.charCodeAt(pos + 1) === 0x3A /* : */ && + state.src.charCodeAt(pos + 2) === 0x3A /* : */ && + state.src.slice(pos + 3, max).trim() === '') { + found = true + nextLine++ + break + } + } + + // Create tokens for the container + let token = state.push('test_box_open', 'div', 1) + token.markup = ':::' + token.block = true + token.attrs = [['class', 'test-box']] + + // Process content within the container + state.md.block.tokenize(state, startLine + 1, found ? nextLine - 1 : nextLine) + + token = state.push('test_box_close', 'div', -1) + token.markup = ':::' + token.block = true + + state.line = nextLine + return true + } + + md.block.ruler.before('fence', 'test_box', container) + } +} \ No newline at end of file diff --git a/test-cases/general-features/src/md-page/markdown-settings-test-style.css b/test-cases/general-features/src/md-page/markdown-settings-test-style.css new file mode 100644 index 0000000..f3fa7ca --- /dev/null +++ b/test-cases/general-features/src/md-page/markdown-settings-test-style.css @@ -0,0 +1,26 @@ +/* Style for the markdown settings test page */ +.test-box { + border: 2px solid #e74c3c; + border-radius: 8px; + padding: 20px; + margin: 25px 0; + background-color: #fef9e7; + position: relative; +} + +.test-box::before { + content: "Custom Test Box"; + position: absolute; + top: -12px; + left: 15px; + background-color: white; + padding: 0 10px; + color: #e74c3c; + font-weight: bold; + font-size: 14px; +} + +.test-box p { + margin: 0; + color: #7d3c98; +} \ No newline at end of file diff --git a/test-cases/general-features/src/md-page/markdown-settings-test.md b/test-cases/general-features/src/md-page/markdown-settings-test.md new file mode 100644 index 0000000..004274a --- /dev/null +++ b/test-cases/general-features/src/md-page/markdown-settings-test.md @@ -0,0 +1,34 @@ +--- +title: Markdown Settings Test +layout: root +--- + +# Markdown Settings Test Page + +This page tests custom markdown-it settings. + +## Custom Container Test + +The following is a custom container that should be rendered with a special class: + +:::test-box +This content should be inside a div with class="test-box". +The custom container was added through markdown-it.settings.js. +::: + +## Regular Markdown + +Here's some regular markdown that should still work: + +- List item 1 +- List item 2 + - Nested item + +**Bold text** and *italic text* should work too. + +```javascript +// Code blocks should work +console.log('Hello, world!'); +``` + +> Blockquotes should also render normally. \ No newline at end of file diff --git a/test-cases/general-features/src/md-page/page.vars.js b/test-cases/general-features/src/md-page/page.vars.js index 3de3297..713b89f 100644 --- a/test-cases/general-features/src/md-page/page.vars.js +++ b/test-cases/general-features/src/md-page/page.vars.js @@ -1,3 +1,5 @@ export default async () => ({ mdPageVars: true, + // Add custom styles for markdown-settings test + customStyle: true, }) diff --git a/test-cases/general-features/src/md-page/style.css b/test-cases/general-features/src/md-page/style.css index 87b51d5..4d55f5e 100644 --- a/test-cases/general-features/src/md-page/style.css +++ b/test-cases/general-features/src/md-page/style.css @@ -1,3 +1,45 @@ .md-page-style { background: yellow; } + +/* Style for the custom test-box container */ +.test-box { + border: 2px solid #3498db; + border-radius: 5px; + padding: 15px; + margin: 20px 0; + background-color: #eaf7ff; + position: relative; +} + +.test-box::before { + content: "Test Box"; + position: absolute; + top: -12px; + left: 10px; + background-color: white; + padding: 0 10px; + color: #3498db; + font-weight: bold; +} + +/* Style for the custom test-box container */ +.test-box { + border: 2px solid #3498db; + border-radius: 5px; + padding: 15px; + margin: 20px 0; + background-color: #eaf7ff; + position: relative; +} + +.test-box::before { + content: "Test Box"; + position: absolute; + top: -12px; + left: 10px; + background-color: white; + padding: 0 10px; + color: #3498db; + font-weight: bold; +} From 289719d53a69b3e3d7798794eca541628fdd59cd Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Sat, 7 Jun 2025 09:32:55 -0700 Subject: [PATCH 07/51] Switch examples over to use preact and general improvements Generally switch all examples over to use preact, add react, etc. --- examples/basic/README.md | 80 ++++ examples/basic/package.json | 4 +- examples/basic/src/README.md | 66 +++- .../basic/src/js-page/loose-assets/page.js | 29 +- examples/basic/src/js-page/page.js | 72 +++- examples/basic/src/js-page/style.css | 75 +++- examples/basic/src/layouts/child.layout.js | 11 +- examples/basic/src/layouts/root.layout.js | 45 ++- examples/basic/src/md-page/README.md | 46 ++- examples/css-modules/src/README.md | 38 +- .../css-modules/src/modules/app.module.css | 56 ++- examples/css-modules/src/modules/page.js | 40 +- examples/default-layout/README.md | 72 ++++ examples/default-layout/src/README.md | 84 ++++- examples/esbuild-settings/src/README.md | 24 +- examples/esbuild-settings/src/client.js | 39 +- .../esbuild-settings/src/esbuild.settings.js | 30 +- examples/markdown-settings/package.json | 10 +- .../src/markdown-it.settings.js | 98 +++-- examples/markdown-settings/src/page.md | 28 +- examples/markdown-settings/src/style.css | 135 ++++++- examples/nested-dest/README.md | 54 ++- examples/nested-dest/package.json | 4 +- .../package.json | 0 examples/preact-isomorphic/src/README.md | 41 +++ .../src/globals/global.client.js | 0 .../preact-isomorphic/src/globals/global.css | 341 ++++++++++++++++++ .../src/isomorphic/client.js | 217 +++++++++++ .../src/isomorphic/page.js | 0 .../preact-isomorphic/src/jsx-page/client.jsx | 120 ++++++ .../preact-isomorphic/src/jsx-page/page.html | 38 ++ .../src/layouts/root.layout.js | 0 examples/preact/src/README.md | 6 - examples/preact/src/isomorphic/client.js | 55 --- examples/preact/src/jsx-page/client.jsx | 12 - examples/preact/src/jsx-page/page.html | 4 - examples/react/package.json | 27 ++ examples/react/src/README.md | 53 +++ .../react/src/globals/esbuild.settings.ts | 43 +++ examples/react/src/globals/global.client.ts | 76 ++++ examples/react/src/globals/global.css | 119 ++++++ examples/react/src/layouts/root.layout.ts | 61 ++++ examples/react/src/react-page/client.tsx | 212 +++++++++++ examples/react/src/react-page/page.html | 27 ++ examples/react/tsconfig.json | 28 ++ examples/string-layouts/README.md | 75 ++++ examples/string-layouts/src/README.md | 74 +++- examples/string-layouts/src/root.layout.js | 37 +- examples/tailwind/src/README.md | 41 ++- .../tailwind/src/globals/esbuild.settings.js | 23 +- examples/type-stripping/src/README.md | 52 ++- .../type-stripping/src/isomorphic/client.ts | 134 +++++-- .../type-stripping/src/tsx-page/client.tsx | 125 ++++++- .../type-stripping/src/tsx-page/page.html | 30 +- examples/uhtml-isomorphic/README.md | 110 ++++++ examples/uhtml-isomorphic/package.json | 22 ++ examples/uhtml-isomorphic/src/README.md | 24 ++ .../src/globals/global.client.js | 4 + .../src/globals/global.css | 0 .../uhtml-isomorphic/src/html-mount/client.js | 12 + .../uhtml-isomorphic/src/html-mount/page.html | 4 + .../uhtml-isomorphic/src/isomorphic/client.js | 43 +++ .../uhtml-isomorphic/src/isomorphic/page.js | 17 + .../src/layouts/root.layout.js | 53 +++ package.json | 2 +- 65 files changed, 3223 insertions(+), 279 deletions(-) create mode 100644 examples/basic/README.md create mode 100644 examples/default-layout/README.md rename examples/{preact => preact-isomorphic}/package.json (100%) create mode 100644 examples/preact-isomorphic/src/README.md rename examples/{preact => preact-isomorphic}/src/globals/global.client.js (100%) create mode 100644 examples/preact-isomorphic/src/globals/global.css create mode 100644 examples/preact-isomorphic/src/isomorphic/client.js rename examples/{preact => preact-isomorphic}/src/isomorphic/page.js (100%) create mode 100644 examples/preact-isomorphic/src/jsx-page/client.jsx create mode 100644 examples/preact-isomorphic/src/jsx-page/page.html rename examples/{preact => preact-isomorphic}/src/layouts/root.layout.js (100%) delete mode 100644 examples/preact/src/README.md delete mode 100644 examples/preact/src/isomorphic/client.js delete mode 100644 examples/preact/src/jsx-page/client.jsx delete mode 100644 examples/preact/src/jsx-page/page.html create mode 100644 examples/react/package.json create mode 100644 examples/react/src/README.md create mode 100644 examples/react/src/globals/esbuild.settings.ts create mode 100644 examples/react/src/globals/global.client.ts create mode 100644 examples/react/src/globals/global.css create mode 100644 examples/react/src/layouts/root.layout.ts create mode 100644 examples/react/src/react-page/client.tsx create mode 100644 examples/react/src/react-page/page.html create mode 100644 examples/react/tsconfig.json create mode 100644 examples/string-layouts/README.md create mode 100644 examples/uhtml-isomorphic/README.md create mode 100644 examples/uhtml-isomorphic/package.json create mode 100644 examples/uhtml-isomorphic/src/README.md create mode 100644 examples/uhtml-isomorphic/src/globals/global.client.js rename examples/{preact => uhtml-isomorphic}/src/globals/global.css (100%) create mode 100644 examples/uhtml-isomorphic/src/html-mount/client.js create mode 100644 examples/uhtml-isomorphic/src/html-mount/page.html create mode 100644 examples/uhtml-isomorphic/src/isomorphic/client.js create mode 100644 examples/uhtml-isomorphic/src/isomorphic/page.js create mode 100644 examples/uhtml-isomorphic/src/layouts/root.layout.js diff --git a/examples/basic/README.md b/examples/basic/README.md new file mode 100644 index 0000000..3d76ec2 --- /dev/null +++ b/examples/basic/README.md @@ -0,0 +1,80 @@ +# Basic DOMStack Example + +This example demonstrates a fundamental website built with DOMStack, showcasing core features without advanced customization. + +## Overview + +The basic example illustrates: +- Multiple page types (Markdown, HTML, JavaScript) +- Layout system and page nesting +- Asset handling +- Client-side JavaScript integration +- CSS styling (global and page-specific) +- Variables and metadata + +## Getting Started + +### Prerequisites + +- Node.js 22.x or higher + +### Installation + +```bash +# Install dependencies +npm install +``` + +### Building the Example + +```bash +# Build the site +npm run build + +# Watch for changes during development +npm run watch +``` + +The built site will be in the `public` directory. + +## Project Structure + +``` +src/ +β”œβ”€β”€ layouts/ # Layout templates +β”‚ β”œβ”€β”€ root.layout.js # Main layout +β”‚ └── child.layout.js # Nested layout +β”œβ”€β”€ md-page/ # Markdown page examples +β”œβ”€β”€ js-page/ # JavaScript page examples +β”œβ”€β”€ html-page/ # HTML page examples +β”œβ”€β”€ global.css # Global styles +β”œβ”€β”€ global.client.js # Global client-side JavaScript +β”œβ”€β”€ global.vars.js # Global variables +└── README.md # Main content (becomes index.html) +``` + +## Key Features Demonstrated + +### Page Types +- **Markdown pages** - Simple content authoring with frontmatter +- **JavaScript pages** - Dynamic content generation with full JS capabilities +- **HTML pages** - Direct HTML control for complex layouts + +### Layouts +The example demonstrates DOMStack's layout system with nested layouts that wrap page content. + +### Assets +Static assets like images are co-located with content and automatically copied to the output directory. + +### Styling +Both global and page-specific CSS is demonstrated, showing how to scope styles appropriately. + +## Learn More + +This is one of several examples in the DOMStack repository. For more advanced features, check out the other examples like: +- css-modules +- preact +- tailwind +- and more... + +For complete documentation, visit the [DOMStack GitHub repository](https://github.com/bcomnes/domstack). \ No newline at end of file diff --git a/examples/basic/package.json b/examples/basic/package.json index cb145ea..bd7bd28 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -20,7 +20,9 @@ }, "dependencies": { "@domstack/cli": "../../.", - "uhtml-isomorphic": "^2.1.0", + "htm": "^3.1.1", + "preact": "^10.26.6", + "preact-render-to-string": "^6.5.13", "mine.css": "^9.0.1" } } diff --git a/examples/basic/src/README.md b/examples/basic/src/README.md index cfa713f..d77ba8b 100644 --- a/examples/basic/src/README.md +++ b/examples/basic/src/README.md @@ -1,16 +1,64 @@ --- +title: Basic DOMStack Example md-files: support yaml frontmatter! --- -# Minimal domstack example -This example demonstrates a example of a minimal website, no customization. +# Basic DOMStack Example -It's just a `src` folder with a few markdown files that link to each other. Markdown files can link directly to their markdown counterparts so navigation works inside GitHub's built in markdown source navigator. +This example demonstrates a complete basic website built with DOMStack, showcasing the core features without advanced customization. -- [loose-file.md](./loose-file.md) -- [nested-md](./md-page/README.md) -- [sub-page](./md-page/sub-page/README.md) +## Features Demonstrated -Also notice how the title of this document set the `title` variable for the page, and renders in the title `` properly. -Page builders can implement variable extraction based on assumptions like this for a given document type. -More automatic variable extraction is planned, like `git` metadata (first commit date, last commit date that touched the file. etc). +- Multiple page types (Markdown, HTML, JavaScript) +- Page layouts and nesting +- Asset handling +- Client-side JavaScript +- CSS styling (global and page-specific) +- Frontmatter variables + +## Project Structure + +``` +src/ +β”œβ”€β”€ layouts/ # Layout templates +β”œβ”€β”€ md-page/ # Markdown page examples +β”œβ”€β”€ js-page/ # JavaScript page examples +β”œβ”€β”€ html-page/ # HTML page examples +β”œβ”€β”€ global.css # Global styles +β”œβ”€β”€ global.client.js # Global client-side JavaScript +β”œβ”€β”€ global.vars.js # Global variables +└── README.md # This file (becomes index.html) +``` + +## Page Examples + +Navigate through different page types: + +- [Loose Markdown File](./loose-file.md) +- [Markdown Page Example](./md-page/README.md) +- [Nested Markdown Page](./md-page/sub-page/README.md) +- [JavaScript Page Example](./js-page/page.js) +- [HTML Page Example](./html-page/page.html) + +## How It Works + +- **Markdown Pages**: The title of this document (`h1`) becomes the `title` variable for the page and renders in the `` tag. +- **Layouts**: Pages use layouts defined in the `layouts` directory. +- **Assets**: Static assets are copied to the output directory. +- **Styling**: Both global and page-specific CSS is processed and included. +- **Client JS**: JavaScript bundles are created for enhanced functionality. + +## Building the Example + +Run the following commands: + +```bash +npm install +npm run build +``` + +To watch for changes during development: + +```bash +npm run watch +``` diff --git a/examples/basic/src/js-page/loose-assets/page.js b/examples/basic/src/js-page/loose-assets/page.js index f737df3..e9acf79 100644 --- a/examples/basic/src/js-page/loose-assets/page.js +++ b/examples/basic/src/js-page/loose-assets/page.js @@ -1,20 +1,23 @@ -import { html } from 'uhtml-isomorphic' +import { html } from 'htm/preact' +import { render } from 'preact-render-to-string' import sharedData from './shared-lib.js' export default async function JSPage () { - return html` - <p> - You can keep loose assets basically anywhere in the <pre>src</pre> directory. - If they are css or js files, they get included into the built website into any of the - client bundle they are imported into. - </p> - <p> - This page demonstrates that with the shared-lib.js and local-import.css files - that get imported into the page.js, client.js and style.css files for this page. - </p> - <p>${sharedData.shared}</p> - ` + return render(html` + <div> + <p> + You can keep loose assets basically anywhere in the <pre>src</pre> directory. + If they are css or js files, they get included into the built website into any of the + client bundle they are imported into. + </p> + <p> + This page demonstrates that with the shared-lib.js and local-import.css files + that get imported into the page.js, client.js and style.css files for this page. + </p> + <p>${sharedData.shared}</p> + </div> + `) } export const vars = { diff --git a/examples/basic/src/js-page/page.js b/examples/basic/src/js-page/page.js index f7b49f3..b44ef05 100644 --- a/examples/basic/src/js-page/page.js +++ b/examples/basic/src/js-page/page.js @@ -1,4 +1,5 @@ -import { html } from 'uhtml-isomorphic' +import { html } from 'htm/preact' +import { render } from 'preact-render-to-string' export default async function JSPage ({ vars: { @@ -6,26 +7,65 @@ export default async function JSPage ({ title, } }) { - return html` - <p>The js page is the only page type that can render the body with the set variables.</p> + return render(html` + <div class="js-page-example"> + <h1>JavaScript Page Example</h1> + + <section class="explanation"> + <h2>What is a JavaScript Page?</h2> + <p> + The JavaScript page type is the most powerful and flexible option in DOMStack. + It allows you to: + </p> + <ul> + <li>Access and use variables directly in your rendering logic</li> + <li>Generate dynamic content based on data or conditions</li> + <li>Use component-based architecture with Preact or other libraries</li> + <li>Return either HTML strings or component objects</li> + </ul> + </section> - <p> - All you have to do is export a default function (async or sync) that returns a string, or any - type that your layout can handle. - In this case, we are using <a href="https://ghub.io/uhtml-isomorphic"><pre>uhtml-isomorphic</pre></a>. - </p> + <section class="implementation"> + <h2>How to Implement</h2> + <p> + Export a default function (async or sync) that returns a string or any + type that your layout can handle. In this example, we're using + <a href="https://github.com/developit/htm"><code>htm/preact</code></a> for JSX-like syntax. + </p> + <div class="code-example"> + <pre><code>export default async function MyPage({ vars }) { + return render(html\`<div>Content here</div>\`) +}</code></pre> + </div> + </section> - <p>Here we access the <pre>siteName</pre> and <pre>title</pre> variables inside the page</p> + <section class="variables-demo"> + <h2>Using Variables</h2> + <p>Here we access the <code>siteName</code> and <code>title</code> variables inside the page:</p> + <div class="variable-display"> + <div><strong>Site Name:</strong> ${siteName}</div> + <div><strong>Page Title:</strong> ${title}</div> + </div> + </section> - <p>${siteName}</p> - <p>${title}</p> + <section class="additional-features"> + <h2>Additional Features</h2> + <p>JavaScript pages support:</p> + <ul> + <li>Page-scoped <code>client.js</code> for browser interactions</li> + <li>Page-scoped <code>style.css</code> for component styling</li> + <li>Page-specific variables via <code>export const vars</code></li> + <li>Async data fetching before rendering</li> + </ul> + </section> - <p>JS pages can also have a page scoped <pre>client.js</pre> and <pre>style.css</pre>. It - is an incredibly flexible page type. - </p> - ` + <a href="../" class="back-link">← Back to Home</a> + </div> + `) } +// Define page-specific variables export const vars = { - title: 'JS Page', + title: 'JavaScript Page Example', + description: 'Learn how to use JavaScript pages in DOMStack for dynamic content generation' } diff --git a/examples/basic/src/js-page/style.css b/examples/basic/src/js-page/style.css index dc48aa3..c167396 100644 --- a/examples/basic/src/js-page/style.css +++ b/examples/basic/src/js-page/style.css @@ -1,3 +1,74 @@ -.js-page-class { - background: purple; +.js-page-example { + max-width: 800px; + margin: 0 auto; + padding: 1rem; + font-family: system-ui, -apple-system, sans-serif; +} + +.js-page-example h1 { + color: #333; + border-bottom: 2px solid #6200ee; + padding-bottom: 0.5rem; +} + +.js-page-example section { + margin-bottom: 2rem; + padding: 1.5rem; + border-radius: 8px; + background-color: #f9f9f9; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.js-page-example h2 { + color: #6200ee; + margin-top: 0; + margin-bottom: 1rem; +} + +.js-page-example .code-example { + background-color: #272822; + color: #f8f8f2; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; +} + +.js-page-example .code-example pre { + margin: 0; +} + +.js-page-example code { + background-color: #eee; + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-family: monospace; + font-size: 0.9em; +} + +.js-page-example .code-example code { + background-color: transparent; + padding: 0; +} + +.js-page-example .variable-display { + background-color: #fff; + border: 1px solid #ddd; + padding: 1rem; + border-radius: 4px; + margin-top: 1rem; +} + +.js-page-example .back-link { + display: inline-block; + margin-top: 1rem; + padding: 0.5rem 1rem; + background-color: #6200ee; + color: white; + text-decoration: none; + border-radius: 4px; + transition: background-color 0.2s; +} + +.js-page-example .back-link:hover { + background-color: #3700b3; } diff --git a/examples/basic/src/layouts/child.layout.js b/examples/basic/src/layouts/child.layout.js index 4f9f622..146bfcc 100644 --- a/examples/basic/src/layouts/child.layout.js +++ b/examples/basic/src/layouts/child.layout.js @@ -1,22 +1,23 @@ -import { html } from 'uhtml-isomorphic' +import { html } from 'htm/preact' +import { render } from 'preact-render-to-string' import defaultRootLayout from './root.layout.js' export default function articleLayout (args) { const { children, ...rest } = args - const wrappedChildren = html` + const wrappedChildren = render(html` <article class="bc-article h-entry" itemscope itemtype="http://schema.org/NewsArticle"> <h1>${rest.vars.title}</h1> <section class="e-content" itemprop="articleBody"> ${typeof children === 'string' - ? html([children]) - : children /* Support both uhtml and string children. Optional. */ + ? html`<div dangerouslySetInnerHTML=${{ __html: children }}></div>` + : children /* Support both preact and string children */ } </section> </article> - ` + `) return defaultRootLayout({ children: wrappedChildren, ...rest }) } diff --git a/examples/basic/src/layouts/root.layout.js b/examples/basic/src/layouts/root.layout.js index 4a832d4..9a81217 100644 --- a/examples/basic/src/layouts/root.layout.js +++ b/examples/basic/src/layouts/root.layout.js @@ -7,7 +7,8 @@ // // All other variables are set on a page level basis, either by hand or by data extraction from the page type. -import { html, render } from 'uhtml-isomorphic' +import { html } from 'htm/preact' +import { render } from 'preact-render-to-string' export default async function RootLayout ({ vars: { @@ -18,25 +19,31 @@ export default async function RootLayout ({ styles, children, }) { - return render(String, html` + return /* html */` <!DOCTYPE html> <html> - <head> - <meta charset="utf-8"> - <title>${siteName}${title ? ` | ${title}` : ''} - - ${scripts - ? scripts.map(script => html``) - : null} - ${styles - ? styles.map(style => html``) - : null} - - -
- ${typeof children === 'string' ? html([children]) : children /* Support both uhtml and string children. Optional. */} -
- + ${render(html` + + + ${siteName}${title ? ` | ${title}` : ''} + + ${scripts + ? scripts.map(script => html``) + : null} + ${styles + ? styles.map(style => html``) + : null} + + `)} + ${render(html` + +
+ ${typeof children === 'string' + ? html`
` + : children /* Support both preact and string children */} +
+ + `)} -`) + ` } diff --git a/examples/basic/src/md-page/README.md b/examples/basic/src/md-page/README.md index 6cdaa50..05043bf 100644 --- a/examples/basic/src/md-page/README.md +++ b/examples/basic/src/md-page/README.md @@ -1,6 +1,12 @@ -# Nested Markdown +--- +title: Markdown Page Example +--- -A page folder with a `README.md` markdown file renders to an `index.html` file inside the parent folder. +# Markdown Page Example + +## How Markdown Pages Work + +A page folder with a `README.md` markdown file renders to an `index.html` file inside the corresponding output folder. For example: @@ -8,20 +14,34 @@ For example: /md-page/README.md ---> /md-page/index.html ``` -They can link to other markdown files, and the links are correctly built to their html equivalent. +Links to other markdown files are automatically transformed to their HTML equivalents during the build process, making navigation seamless between source and output. + +## Page-Scoped Resources + +Each markdown page can utilize the following page-scoped files: + +- [`client.js`](./client.js) - Client-side JavaScript specific to this page +- [`style.css`](./style.css) - CSS styles specific to this page +- [`page.vars.js`](./page.vars.js) - Variables and metadata for this page + +## Asset Management + +DOMStack copies all assets from the `src` directory to the output folder with a 1:1 mapping. This approach encourages: + +- **Co-location of assets** - Keep images near the documents that use them +- **Logical organization** - Avoid a single overcrowded global assets folder +- **Simplified references** - Maintain the same relative paths in source and output + +### Example Image Asset + +Below is an example of an image asset referenced from the page's assets folder: -Page folder markdown file can utilize the following page scoped files: +![Matrix Animation](./assets/matrix.gif) -- [`client.js`](./client.js) -- [`style.css`](./style.css) -- [`page.vars.js`](./page.vars.js) +## Navigation -All assets in the `src` directory are copied over to the `dist` folder, 1:1. -It's a good idea to co-locate images near the document they live in, otherwise -you end up with a jumble of files in one global images folder. -It's worth the overhead of copying static assets into a build directory in order to gain a simple approach to organzing assets. -In the future, asset serving may be virtualized during development. +- [Back to Home](../) +- [View Subpage](./sub-page/README.md) -![](./assets/matrix.gif) diff --git a/examples/css-modules/src/README.md b/examples/css-modules/src/README.md index f026f45..8383d7b 100644 --- a/examples/css-modules/src/README.md +++ b/examples/css-modules/src/README.md @@ -1,5 +1,37 @@ -# Preact example +# CSS Modules Example -This is a preact example. +This example demonstrates how to use CSS modules with DOMStack, providing component-scoped styling that avoids global namespace conflicts. -[Isomorphic Component Rendering](./modules/) +## What Are CSS Modules? + +CSS modules are CSS files where class names and animation names are scoped locally by default. This prevents style leakage and naming collisions in your application. + +## Features Demonstrated + +- Scoped CSS class names via the `.module.css` extension +- Isomorphic component rendering with Preact +- Integration of CSS modules with JavaScript components + +## How It Works + +1. Create a CSS file with the `.module.css` extension +2. Import the styles in your JavaScript component +3. Use the imported class names as object properties + +## Project Structure + +``` +src/ +β”œβ”€β”€ globals/ # Global styles and scripts +β”œβ”€β”€ layouts/ # Layout templates +β”œβ”€β”€ modules/ # Components with CSS modules +β”‚ β”œβ”€β”€ app.module.css # Module-scoped CSS +β”‚ β”œβ”€β”€ client.js # Client-side hydration +β”‚ β”œβ”€β”€ page.js # Server-side component +β”‚ └── style.css # Regular CSS +└── README.md # This file (becomes index.html) +``` + +## Example + +Check out the [Isomorphic Component Rendering](./modules/) example to see CSS modules in action. diff --git a/examples/css-modules/src/modules/app.module.css b/examples/css-modules/src/modules/app.module.css index aa36c98..cd1f79b 100644 --- a/examples/css-modules/src/modules/app.module.css +++ b/examples/css-modules/src/modules/app.module.css @@ -1,4 +1,58 @@ /* app.module.css */ .outerShell { - background: blue; + background: #3498db; + color: white; + padding: 1.5rem; + border-radius: 8px; + margin: 1rem 0; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.outerShell:hover { + background: #2980b9; + transform: translateY(-2px); + transition: all 0.3s ease; +} + +.button { + display: inline-block; + padding: 8px 16px; + background: #2ecc71; + color: white; + border: none; + border-radius: 4px; + font-weight: bold; + cursor: pointer; +} + +.button:hover { + background: #27ae60; +} + +/* Example of composition */ +.dangerButton { + composes: button; + background: #e74c3c; +} + +.dangerButton:hover { + background: #c0392b; +} + +/* Example of complex nesting */ +.card { + border: 1px solid #ddd; + border-radius: 4px; + padding: 1rem; + margin-bottom: 1rem; +} + +.card .title { + font-size: 1.2rem; + margin-top: 0; + color: #333; +} + +.card .content { + color: #666; } diff --git a/examples/css-modules/src/modules/page.js b/examples/css-modules/src/modules/page.js index 960bd4a..1c28bc9 100644 --- a/examples/css-modules/src/modules/page.js +++ b/examples/css-modules/src/modules/page.js @@ -1,7 +1,45 @@ import { html } from 'htm/preact' +import styles from './app.module.css' export default () => { return html` -
Loading
+
+

CSS Modules Example

+ +
+

Module-Scoped CSS

+

This example demonstrates how CSS Modules work in DOMStack.

+ +
+

This div uses the outerShell class from app.module.css

+

The class name is scoped to this component only.

+
+ +

+// How to import and use CSS modules:
+import styles from './app.module.css'
+
+// Then use them in your components:
+<div class=\${styles.outerShell}>
+  Scoped CSS!
+</div>
+          
+
+ +
+

Benefits of CSS Modules

+
    +
  • No CSS class name collisions
  • +
  • Localized styling to components
  • +
  • Explicit dependencies
  • +
  • Composition support
  • +
+
+
` } + +export const vars = { + title: 'CSS Modules Example', + description: 'Learn how to use CSS Modules for component-scoped styling in DOMStack' +} diff --git a/examples/default-layout/README.md b/examples/default-layout/README.md new file mode 100644 index 0000000..46928d5 --- /dev/null +++ b/examples/default-layout/README.md @@ -0,0 +1,72 @@ +# Default Layout Example + +This example demonstrates DOMStack's built-in default layout functionality. + +## Overview + +DOMStack provides a fallback layout system that activates when you don't explicitly define a `root.layout.js` file in your project's `src` directory. This example intentionally omits a custom layout to showcase this feature. + +## Key Concepts + +When no layout is provided, DOMStack will: +1. Display a warning message during build: `Missing a root.layout.js file. Using default layout file.` +2. Use its internal default layout, which provides a basic HTML structure +3. Properly render your content within this default layout + +## Getting Started + +### Prerequisites + +- Node.js 22.x or higher + +### Installation + +```bash +# Install dependencies +npm install +``` + +### Building the Example + +```bash +# Build the site +npm run build + +# Watch for changes during development +npm run watch +``` + +The built site will be in the `public` directory. + +## Project Structure + +``` +src/ +└── README.md # Main content (becomes index.html) +``` + +Notice the intentional absence of a `layouts` directory or any layout files. + +## Default Layout Features + +The default layout provides: +- A basic HTML5 structure +- Proper `` setup with meta tags +- Title handling +- Content insertion +- Minimal styling + +## Use Cases + +The default layout is useful for: +- Quick prototyping +- Simple content-focused websites +- Getting started with DOMStack without having to create layout files + +## Learn More + +For information about creating custom layouts, see the other examples in the DOMStack repository, particularly: +- basic +- string-layouts + +For complete documentation, visit the [DOMStack GitHub repository](https://github.com/bcomnes/domstack). \ No newline at end of file diff --git a/examples/default-layout/src/README.md b/examples/default-layout/src/README.md index c14ee7d..a4628cf 100644 --- a/examples/default-layout/src/README.md +++ b/examples/default-layout/src/README.md @@ -1,3 +1,83 @@ -# Default Layout +--- +title: Default Layout Example +--- -Domstack ships a default layout when you don't have a `root.layout.js` file your `src` directory. +# Default Layout Example + +## Overview + +DOMStack ships with a built-in default layout that automatically activates when you don't include a `root.layout.js` file in your project's `src` directory. This example intentionally omits any custom layout files to demonstrate this fallback functionality. + +## How It Works + +When no custom layout is provided: + +1. DOMStack detects the absence of a `root.layout.js` file +2. The build process shows a warning: `Missing a root.layout.js file. Using default layout file.` +3. The system automatically applies the built-in default layout +4. Your content is properly rendered within this default HTML structure + +## Default Layout Features + +The built-in default layout includes: + +- Standard HTML5 doctype and structure +- Proper metadata tags including charset and viewport settings +- Automatic title handling (using your page's H1 content) +- Responsive design considerations +- Script and stylesheet injection points +- Semantic HTML structure for content + +## Generated HTML Structure + +When using the default layout, your content will be wrapped in HTML similar to this: + +```html + + + + + Your Page Title | Your Site Name + + + + + +
+ +
+ + +``` + +## Benefits + +The default layout provides several advantages: + +- Quick start without needing to create layout files +- Basic but functional HTML5 structure with proper metadata +- Automatic title handling from your content's H1 +- Clean and simple presentation for content-focused projects +- Reduces boilerplate code in small projects + +## Try It Yourself + +To see the default layout in action: + +1. Examine the project structure - notice there's no layout file +2. Build the project and observe the warning message: + ``` + Missing a root.layout.js file. Using default layout file. + ``` +3. View the generated HTML output in the `public` directory to see how the default layout wraps the content + +## When to Use Custom Layouts + +While the default layout is convenient for getting started, you should create custom layouts when you need: + +- Custom navigation elements +- Site-specific branding and design +- Advanced layout structures +- Special metadata or analytics integrations + +For custom layouts, check the other examples in the DOMStack repository, particularly the `basic` example. diff --git a/examples/esbuild-settings/src/README.md b/examples/esbuild-settings/src/README.md index c786aff..cb5de8b 100644 --- a/examples/esbuild-settings/src/README.md +++ b/examples/esbuild-settings/src/README.md @@ -1,5 +1,23 @@ -# Esbuild settings example +# ESBuild Settings Example -This website imports some node builtins in the browser bundle. +## Overview -These get polyfilled with the esbuild.setting.js file. +This example demonstrates how to customize the bundling process in DOMStack by configuring ESBuild options. Specifically, it shows how to use Node.js built-in modules in browser-side JavaScript by applying polyfills. + +## What This Example Shows + +- How to create and use an `esbuild.settings.js` file +- How to configure ESBuild plugins (in this case, for Node.js polyfills) +- How browser code can use Node.js modules safely + +## How It Works + +1. The `client.js` file imports a Node.js built-in module (`os`) +2. The `esbuild.settings.js` file configures the `esbuild-plugin-polyfill-node` plugin +3. During the build process, DOMStack uses these settings to polyfill Node.js modules +4. The result is browser-compatible JavaScript that simulates Node.js APIs + +## Key Files + +- `esbuild.settings.js`: Contains the ESBuild configuration +- `client.js`: Demonstrates importing a Node.js module diff --git a/examples/esbuild-settings/src/client.js b/examples/esbuild-settings/src/client.js index 37f46ca..de6490e 100644 --- a/examples/esbuild-settings/src/client.js +++ b/examples/esbuild-settings/src/client.js @@ -1,4 +1,39 @@ +/** + * ESBuild Node.js Polyfill Example + * + * This file demonstrates how we can use Node.js built-in modules + * in browser-side JavaScript when proper ESBuild settings are configured. + */ + +// Import Node.js built-in modules import os from 'node:os' +import path from 'node:path' +import process from 'node:process' + +// Log a welcome message +console.log('===== Node.js Modules in Browser Demo =====') + +// Demonstrate OS module functionality +console.log('\nπŸ“Š OS Module Info:') +console.log('- Platform:', os.platform()) +console.log('- Architecture:', os.arch()) +console.log('- CPUs:', os.cpus().length) +console.log('- Total Memory:', (os.totalmem() / 1024 / 1024 / 1024).toFixed(2), 'GB') +console.log('- Free Memory:', (os.freemem() / 1024 / 1024 / 1024).toFixed(2), 'GB') + +// Demonstrate Path module functionality +console.log('\nπŸ—‚οΈ Path Module Example:') +const filePath = '/users/documents/file.txt' +console.log('- Original path:', filePath) +console.log('- Directory name:', path.dirname(filePath)) +console.log('- Base name:', path.basename(filePath)) +console.log('- Extension:', path.extname(filePath)) +console.log('- Parsed:', path.parse(filePath)) + +// Demonstrate Process module functionality +console.log('\nβš™οΈ Process Info:') +console.log('- Current working directory:', process.cwd()) +console.log('- Process platform:', process.platform) +console.log('- Node.js version:', process.version) -console.log('hello world') -console.log(os) +console.log('\nβœ… All of these Node.js APIs work in the browser thanks to ESBuild polyfills!') diff --git a/examples/esbuild-settings/src/esbuild.settings.js b/examples/esbuild-settings/src/esbuild.settings.js index a31bb8b..3e29122 100644 --- a/examples/esbuild-settings/src/esbuild.settings.js +++ b/examples/esbuild-settings/src/esbuild.settings.js @@ -1,13 +1,35 @@ -/** @import { BuildOptions } from 'esbuild' */ +/** + * ESBuild Settings Override + * + * This file demonstrates how to customize the ESBuild configuration in DOMStack. + * It allows Node.js built-in modules to be used in browser-side JavaScript by + * applying polyfills through the esbuild-plugin-polyfill-node plugin. + * + * @import { BuildOptions } from 'esbuild' + */ import { polyfillNode } from 'esbuild-plugin-polyfill-node' /** - * @param {BuildOptions} esbuildSettings - * @return {Promise} + * Configure ESBuild settings for browser-compatible Node.js modules + * + * This function receives the default ESBuild configuration and returns a + * modified version with additional plugins and settings. + * + * @param {BuildOptions} esbuildSettings - The default ESBuild configuration + * @return {Promise} - The modified ESBuild configuration */ export default async function esbuildSettingsOverride (esbuildSettings) { + // Add the Node.js polyfill plugin to enable using Node.js modules in the browser esbuildSettings.plugins = [ - polyfillNode(), + polyfillNode({ + // You can configure specific polyfills here if needed + // For example: include: ['os', 'path', 'fs'] + }), ] + + // You can also modify other ESBuild settings: + // esbuildSettings.minify = true; + // esbuildSettings.sourcemap = true; + return esbuildSettings } diff --git a/examples/markdown-settings/package.json b/examples/markdown-settings/package.json index ff1bf6a..ce57e94 100644 --- a/examples/markdown-settings/package.json +++ b/examples/markdown-settings/package.json @@ -4,11 +4,13 @@ "description": "Example demonstrating markdown-it.settings.js usage", "type": "module", "scripts": { - "build": "domstack build src dist", - "dev": "domstack dev src" + "start": "npm run dev", + "build": "npm run clean && domstack", + "clean": "rm -rf public && mkdir -p public", + "dev": "npm run clean && domstack --watch" }, - "devDependencies": { - "domstack": "workspace:*", + "dependencies": { + "@domstack/cli": "../../.", "markdown-it-container": "^4.0.0", "markdown-it-admonition": "^1.0.4" } diff --git a/examples/markdown-settings/src/markdown-it.settings.js b/examples/markdown-settings/src/markdown-it.settings.js index b75c724..fe85137 100644 --- a/examples/markdown-settings/src/markdown-it.settings.js +++ b/examples/markdown-settings/src/markdown-it.settings.js @@ -1,43 +1,72 @@ +/** + * Custom Markdown-it Configuration + * + * This file demonstrates how to extend DOMStack's markdown rendering + * capabilities by customizing the markdown-it instance. + * + * Key features demonstrated: + * 1. Adding custom container plugins (warning, info, details) + * 2. Customizing code block rendering + * 3. Applying custom CSS classes for styling + */ + import markdownItContainer from 'markdown-it-container' /** - * Customize the markdown-it instance with additional plugins - * @param {import('markdown-it')} md - The markdown-it instance - * @returns {import('markdown-it')} - The modified markdown-it instance + * Creates a custom container plugin configuration + * + * @param {string} name - Container name ('warning', 'info', etc.) + * @param {string} defaultTitle - Default title if none specified + * @param {string} cssClass - CSS class for the container + * @returns {Object} - Container plugin configuration */ -export default async function markdownItSettingsOverride (md) { - // Add custom container for warnings - md.use(markdownItContainer, 'warning', { +function createContainer(name, defaultTitle, cssClass) { + return { validate: function (params) { - return params.trim().match(/^warning\s*(.*)$/) + return params.trim().match(new RegExp(`^${name}\\s*(.*)$`)) }, - render: function (tokens, idx) { - const m = tokens[idx].info.trim().match(/^warning\s*(.*)$/) + render: function (tokens, idx, options, env, self) { + const m = tokens[idx].info.trim().match(new RegExp(`^${name}\\s*(.*)$`)) if (tokens[idx].nesting === 1) { - const title = (m && m.length > 1) ? m[1] : 'Warning' - return '
\n
' + md.utils.escapeHtml(title) + '
\n
\n' + // Opening tag + const title = (m && m.length > 1) ? m[1] : defaultTitle + return `
\n
` + + title + + `
\n
\n` } else { + // Closing tag return '
\n
\n' } } - }) + } +} - // Add custom container for info boxes - md.use(markdownItContainer, 'info', { - validate: function (params) { - return params.trim().match(/^info\s*(.*)$/) - }, - render: function (tokens, idx) { - const m = tokens[idx].info.trim().match(/^info\s*(.*)$/) - if (tokens[idx].nesting === 1) { - const title = (m && m.length > 1) ? m[1] : 'Info' - return '
\n
' + md.utils.escapeHtml(title) + '
\n
\n' - } else { - return '
\n
\n' - } - } - }) +/** + * Customize the markdown-it instance with additional plugins and renderers + * + * @param {import('markdown-it')} md - The markdown-it instance + * @returns {import('markdown-it')} - The modified markdown-it instance + */ +export default async function markdownItSettingsOverride (md) { + // ===================================================== + // CUSTOM CONTAINERS + // ===================================================== + + // Add warning container: ::: warning Title + md.use( + markdownItContainer, + 'warning', + createContainer('warning', 'Warning', 'custom-warning') + ) + + // Add info container: ::: info Title + md.use( + markdownItContainer, + 'info', + createContainer('info', 'Info', 'custom-info') + ) + // Add details/collapsible container: ::: details Title // Add custom container for collapsible sections md.use(markdownItContainer, 'details', { validate: function (params) { @@ -46,21 +75,32 @@ export default async function markdownItSettingsOverride (md) { render: function (tokens, idx) { const m = tokens[idx].info.trim().match(/^details\s+(.*)$/) if (tokens[idx].nesting === 1) { - return '
\n' + md.utils.escapeHtml(m[1]) + '\n
\n' + return '
\n' + m[1] + '\n
\n' } else { return '
\n
\n' } } }) + // ===================================================== + // CUSTOM RENDERERS + // ===================================================== + + // Customize code block rendering with enhanced styling // Customize existing renderer - add custom classes to code blocks md.renderer.rules.code_block = function (tokens, idx, options, env, renderer) { const token = tokens[idx] const content = token.content const langName = token.info || '' - return `
${md.utils.escapeHtml(content)}
\n` + return `
${renderer.utils.escapeHtml(content)}
\n` } + // You can add more customizations here: + // - Custom link renderers + // - Table formatting + // - Image processing + // - Etc. + return md } diff --git a/examples/markdown-settings/src/page.md b/examples/markdown-settings/src/page.md index 893d4be..79fb6ff 100644 --- a/examples/markdown-settings/src/page.md +++ b/examples/markdown-settings/src/page.md @@ -5,10 +5,12 @@ layout: root # Markdown-it Settings Example -This page demonstrates custom markdown-it plugins configured via `markdown-it.settings.js`. +This page demonstrates how to customize Markdown rendering in DOMStack using the `markdown-it.settings.js` configuration file. ## Custom Containers +DOMStack allows you to create specialized content containers with custom styling using the markdown-it-container plugin. + ### Warning Container ::: warning Security Notice @@ -31,6 +33,8 @@ Info boxes are great for tips and additional context. ### Details Container +The details container creates collapsible sections for content that doesn't need to be visible immediately: + ::: details Click to expand more information This content is hidden by default and can be revealed by clicking the summary. @@ -43,7 +47,7 @@ You can include any markdown content here: ::: details Advanced Configuration The `markdown-it.settings.js` file allows you to: -1. Add custom plugins +1. Add third-party plugins 2. Modify existing renderers 3. Configure parser options 4. Create entirely new markdown syntaxes @@ -51,7 +55,7 @@ The `markdown-it.settings.js` file allows you to: ## Custom Code Block Styling -The code blocks below have custom classes applied: +The renderer for code blocks has been customized to add special styling. The code blocks below demonstrate this enhanced presentation: ```javascript // This code block has a custom class @@ -67,11 +71,27 @@ def greet(name): ## How It Works -The `markdown-it.settings.js` file exports a function that receives the default markdown-it instance and returns a modified version. This allows you to: +The `markdown-it.settings.js` file exports a function that receives the default markdown-it instance and returns a modified version: + +```javascript +export default async function markdownItSettingsOverride (md) { + // Add plugins and customizations here + return md +} +``` + +This allows you to: - Add third-party plugins - Create custom containers - Override default renderers - Configure parsing options +## Implementation Steps + +1. Create a `markdown-it.settings.js` file in your project's source directory +2. Import any markdown-it plugins you want to use +3. Implement the `markdownItSettingsOverride` function +4. Use the enhanced markdown syntax in your .md files + Check out the `markdown-it.settings.js` file in this example to see how these customizations are implemented. \ No newline at end of file diff --git a/examples/markdown-settings/src/style.css b/examples/markdown-settings/src/style.css index cccce56..9afc349 100644 --- a/examples/markdown-settings/src/style.css +++ b/examples/markdown-settings/src/style.css @@ -1,3 +1,14 @@ +/** + * Markdown-it Custom Styling + * + * This stylesheet provides custom styling for the enhanced markdown-it features + * configured in markdown-it.settings.js. + */ + +/* ============================================= + CUSTOM CONTAINERS + ============================================= */ + /* Custom warning container */ .custom-warning { border: 2px solid #ff9800; @@ -5,6 +16,13 @@ margin: 1.5rem 0; overflow: hidden; background-color: #fff3e0; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; +} + +.custom-warning:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); } .custom-warning .warning-title { @@ -27,6 +45,13 @@ margin: 1.5rem 0; overflow: hidden; background-color: #e3f2fd; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; +} + +.custom-info:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); } .custom-info .info-title { @@ -42,6 +67,10 @@ color: #0d47a1; } +/* ============================================= + COLLAPSIBLE DETAILS CONTAINER + ============================================= */ + /* Custom details container */ details { border: 1px solid #ddd; @@ -49,6 +78,12 @@ details { margin: 1.5rem 0; padding: 0; background-color: #f5f5f5; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + transition: box-shadow 0.2s; +} + +details:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } details summary { @@ -58,6 +93,23 @@ details summary { background-color: #e0e0e0; border-radius: 8px 8px 0 0; user-select: none; + position: relative; + outline: none; +} + +details summary::after { + content: "β–Ό"; + position: absolute; + right: 1rem; + top: 50%; + transform: translateY(-50%); + transition: transform 0.3s; + font-size: 0.8rem; + color: #666; +} + +details[open] summary::after { + transform: translateY(-50%) rotate(180deg); } details[open] summary { @@ -70,15 +122,20 @@ details .details-content { border-radius: 0 0 8px 8px; } +/* ============================================= + CODE BLOCK STYLING + ============================================= */ + /* Custom code blocks */ .custom-code-block { - background-color: #f8f8f8; - border: 1px solid #e0e0e0; + background-color: #1e1e1e; + border: 1px solid #333; border-radius: 6px; padding: 1rem; margin: 1rem 0; overflow-x: auto; position: relative; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } .custom-code-block::before { @@ -87,10 +144,11 @@ details .details-content { top: 0; right: 0; padding: 0.25rem 0.75rem; - background-color: #e0e0e0; + background-color: #333; border-radius: 0 6px 0 6px; font-size: 0.8rem; - color: #666; + color: #fff; + opacity: 0.8; } .custom-code-block code { @@ -99,9 +157,22 @@ details .details-content { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 0.9rem; line-height: 1.5; + color: #f8f8f2; } -/* General page styling */ +/* Syntax highlighting classes */ +.custom-code-block .keyword { color: #ff79c6; } +.custom-code-block .string { color: #f1fa8c; } +.custom-code-block .comment { color: #6272a4; } +.custom-code-block .function { color: #50fa7b; } +.custom-code-block .number { color: #bd93f9; } +.custom-code-block .operator { color: #ff79c6; } + +/* ============================================= + GENERAL PAGE STYLING + ============================================= */ + +/* Base page layout */ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; line-height: 1.6; @@ -109,30 +180,82 @@ body { max-width: 800px; margin: 0 auto; padding: 2rem; + background-color: #fafafa; } +/* Typography */ h1, h2, h3 { color: #2c3e50; margin-top: 2rem; + font-weight: 600; } h1 { border-bottom: 2px solid #2c3e50; padding-bottom: 0.5rem; + font-size: 2.2rem; +} + +h2 { + font-size: 1.8rem; + border-bottom: 1px solid #eaecef; + padding-bottom: 0.3rem; +} + +h3 { + font-size: 1.4rem; +} + +p { + margin: 1rem 0; } +/* Inline code */ code { - background-color: #f4f4f4; + background-color: #f0f0f0; padding: 0.2rem 0.4rem; border-radius: 3px; font-size: 0.9em; + border: 1px solid #e0e0e0; + color: #e53935; } +/* Links */ a { color: #2196f3; text-decoration: none; + transition: color 0.2s; } a:hover { + color: #0d47a1; text-decoration: underline; +} + +/* Lists */ +ul, ol { + padding-left: 1.5rem; +} + +li { + margin: 0.5rem 0; +} + +/* Horizontal rule */ +hr { + border: 0; + border-top: 1px solid #eaecef; + margin: 2rem 0; +} + +/* Print styles */ +@media print { + body { + padding: 0; + background: white; + } + + .custom-warning, .custom-info, details { + break-inside: avoid; + } } \ No newline at end of file diff --git a/examples/nested-dest/README.md b/examples/nested-dest/README.md index da3a6aa..c1a32a2 100644 --- a/examples/nested-dest/README.md +++ b/examples/nested-dest/README.md @@ -1,12 +1,50 @@ -# Nested dest +# Nested Destination Example -One of the design goals of `domstack` was to allow you to point `domstack` at a generic -library repository, and render out all of the markdown inside of it, into a `dest` folder. +## Overview -The issue with this is that `dest` folder essentially lives inside of the `src` folder, -so it was important to support sane ignore patterns by default so you don't fall into a recursive render -loop, or try to render out `node_modules` etc. +This example demonstrates one of DOMStack's key features: the ability to build a documentation site from an existing repository structure without reorganizing it. -This example site points `domstack` at the root of the example `/`, and builds into `/public`. +## How It Works -boop beep +DOMStack can use any directory as its source, including the root of your project. This allows you to: + +1. Generate documentation directly from your project's existing markdown files +2. Keep source files in their original locations +3. Build the site into a separate destination directory + +## Key Concepts + +### Source and Destination Paths + +In this example: +- Source: The root directory (`/`) +- Destination: A subdirectory (`/public`) + +### Avoiding Recursive Processing + +When your destination folder is inside your source tree, DOMStack needs to avoid: +- Processing files in the destination folder (recursive loop) +- Processing unwanted directories like `node_modules` + +This is handled through intelligent ignore patterns: +- Default ignore patterns for common directories +- Custom ignore patterns through the `--ignore` flag + +## Example Configuration + +The build command in this example uses: + +```bash +domstack --src . --ignore ignore +``` + +This tells DOMStack to: +- Use the current directory (`.`) as the source +- Explicitly ignore the `ignore` directory +- Apply default ignore patterns for `node_modules`, etc. + +## Try It Yourself + +1. Examine this project's structure - notice how files are in the root +2. Look at the built output in `/public` to see how files are processed +3. Check `package.json` to see how the build command is configured diff --git a/examples/nested-dest/package.json b/examples/nested-dest/package.json index 0f29c56..663a3c1 100644 --- a/examples/nested-dest/package.json +++ b/examples/nested-dest/package.json @@ -5,11 +5,11 @@ "type": "module", "scripts": { "start": "npm run watch", - "build": "npm run clean && depscan --src . --ignore ignore", + "build": "npm run clean && domstack --src . --ignore ignore", "clean": "rm -rf public && mkdir -p public", "watch": "npm run clean && run-p watch:*", "watch:serve": "browser-sync start --server 'public' --files 'public'", - "watch:depscan": "npm run build -- --watch" + "watch:domstack": "npm run build -- --watch" }, "keywords": [], "author": "Bret Comnes (https://bret.io/)", diff --git a/examples/preact/package.json b/examples/preact-isomorphic/package.json similarity index 100% rename from examples/preact/package.json rename to examples/preact-isomorphic/package.json diff --git a/examples/preact-isomorphic/src/README.md b/examples/preact-isomorphic/src/README.md new file mode 100644 index 0000000..634d084 --- /dev/null +++ b/examples/preact-isomorphic/src/README.md @@ -0,0 +1,41 @@ +# Preact Isomorphic Rendering Example + +This example demonstrates how to implement isomorphic rendering with Preact in DOMStack. Isomorphic rendering means the same components can be rendered on both the server and client, providing benefits of server-side rendering (SSR) with client-side interactivity. + +## What is Isomorphic Rendering? + +Isomorphic (or universal) rendering combines: + +1. **Server-side rendering** - Components are rendered to HTML on the server first +2. **Client-side hydration** - JavaScript takes over in the browser to add interactivity +3. **Shared component code** - The same components work in both environments + +## Benefits + +- **Performance**: Faster initial page loads and time-to-content +- **SEO**: Search engines see fully rendered content +- **Accessibility**: Content is available without JavaScript +- **User Experience**: No flash of unstyled content or layout shifts + +## Examples in This Project + +- [Isomorphic Component Rendering](./isomorphic/) - Complete todo app rendered both server and client-side +- [JSX Client Mounting](./jsx-page/) - Example of client-side JSX rendering into static HTML + +## Implementation Approach + +This example uses: + +- **Preact** - A lightweight alternative to React +- **HTM** - JSX alternative using tagged template literals +- **Signals** - For reactive state management +- **preact-render-to-string** - For server-side rendering + +## How It Works + +1. The server renders components to HTML using `preact-render-to-string` +2. The HTML is sent to the browser with linked JavaScript +3. In the browser, the same components hydrate the existing HTML +4. Interactivity is enabled without replacing the DOM structure + +Learn more about these techniques in the examples! diff --git a/examples/preact/src/globals/global.client.js b/examples/preact-isomorphic/src/globals/global.client.js similarity index 100% rename from examples/preact/src/globals/global.client.js rename to examples/preact-isomorphic/src/globals/global.client.js diff --git a/examples/preact-isomorphic/src/globals/global.css b/examples/preact-isomorphic/src/globals/global.css new file mode 100644 index 0000000..cd499d1 --- /dev/null +++ b/examples/preact-isomorphic/src/globals/global.css @@ -0,0 +1,341 @@ +@import 'mine.css/dist/mine.css'; +@import 'mine.css/dist/layout.css'; +@import 'highlight.js/styles/github-dark-dimmed.css'; + +/* ===== Isomorphic Example Styles ===== */ + +.isomorphic-container { + max-width: 800px; + margin: 0 auto; + padding: 1rem; +} + +.app-header { + margin-bottom: 1.5rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid #e0e0e0; +} + +.app-header h1 { + margin-bottom: 0.5rem; +} + +.app-header .subtitle { + color: #666; + font-style: italic; +} + +.todo-app { + background-color: #fff; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 1.5rem; + margin-bottom: 2rem; +} + +.todo-form { + display: flex; + margin-bottom: 1rem; +} + +.todo-form input { + flex: 1; + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px 0 0 4px; +} + +.todo-form button { + padding: 0.5rem 1rem; + background-color: #4a8fe7; + color: white; + border: none; + border-radius: 0 4px 4px 0; + cursor: pointer; +} + +.todo-form button:hover { + background-color: #3a7fd7; +} + +.todo-list { + list-style: none; + padding: 0; + margin: 0 0 1rem 0; +} + +.todo-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem; + border-bottom: 1px solid #eee; +} + +.todo-item:last-child { + border-bottom: none; +} + +.todo-item.completed .todo-text { + color: #999; + text-decoration: line-through; +} + +.todo-label { + display: flex; + align-items: center; + gap: 0.5rem; + flex: 1; +} + +.delete-btn { + background-color: #ff5252; + color: white; + border: none; + border-radius: 4px; + padding: 0.25rem 0.5rem; + font-size: 0.8rem; + cursor: pointer; +} + +.delete-btn:hover { + background-color: #ff0000; +} + +.todo-stats { + color: #666; + font-size: 0.9rem; + margin-bottom: 1rem; +} + +.counter-widget { + background-color: #f5f5f5; + border-radius: 6px; + padding: 1rem; + margin-top: 1rem; +} + +.counter-widget h3 { + margin-top: 0; + margin-bottom: 0.5rem; + font-size: 1.1rem; +} + +.counter-display { + display: flex; + justify-content: space-between; + padding: 0.5rem; + margin-bottom: 0.5rem; + background-color: white; + border-radius: 4px; + transition: background-color 0.2s; +} + +.counter-display.even { + background-color: #e6f7ff; +} + +.counter-display.odd { + background-color: #fff2e6; +} + +.counter-controls { + display: flex; + gap: 0.5rem; +} + +.counter-controls button { + flex: 1; + padding: 0.5rem; + border: none; + border-radius: 4px; + background-color: #4a8fe7; + color: white; + cursor: pointer; +} + +.counter-controls button:hover { + background-color: #3a7fd7; +} + +.info-panel { + background-color: #f9f9f9; + border-left: 4px solid #4a8fe7; + padding: 1rem; + border-radius: 0 4px 4px 0; +} + +.info-panel h2 { + margin-top: 0; + font-size: 1.2rem; +} + +/* ===== JSX Page Styles ===== */ + +.jsx-page-container { + max-width: 800px; + margin: 0 auto; + padding: 1rem; +} + +.explanation-section, +.demo-section, +.code-reference { + margin-bottom: 2rem; +} + +.key-points { + background-color: #f5f5f5; + padding: 1rem; + border-radius: 6px; + margin: 1rem 0; +} + +.key-points h3 { + margin-top: 0; +} + +.key-points ul { + margin-bottom: 0; +} + +.jsx-demo { + background-color: #fff; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 1.5rem; + transition: background-color 0.3s, color 0.3s; +} + +.jsx-demo.dark-theme { + background-color: #222; + color: #eee; +} + +.theme-toggle { + margin-bottom: 1.5rem; + text-align: right; +} + +.theme-toggle button { + background-color: #4a8fe7; + color: white; + border: none; + border-radius: 4px; + padding: 0.5rem 1rem; + cursor: pointer; +} + +.jsx-demo.dark-theme .theme-toggle button { + background-color: #f1c40f; + color: #222; +} + +.counter-section, +.profiles-section, +.explanation { + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid #eee; +} + +.jsx-demo.dark-theme .counter-section, +.jsx-demo.dark-theme .profiles-section, +.jsx-demo.dark-theme .explanation { + border-bottom-color: #444; +} + +.counter-section h3, +.profiles-section h3, +.explanation h3 { + margin-top: 0; +} + +.profiles-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.profile-card { + background-color: #f9f9f9; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +.jsx-demo.dark-theme .profile-card { + background-color: #333; +} + +.profile-card:hover { + transform: translateY(-3px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.profile-header { + padding: 1rem; + position: relative; + display: flex; + justify-content: center; + background-color: #eee; +} + +.jsx-demo.dark-theme .profile-header { + background-color: #444; +} + +.avatar { + width: 64px; + height: 64px; + border-radius: 50%; + object-fit: cover; +} + +.status-indicator { + position: absolute; + bottom: 1rem; + right: calc(50% - 40px); + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid #fff; +} + +.status-indicator.active { + background-color: #4caf50; +} + +.status-indicator.inactive { + background-color: #ccc; +} + +.profile-info { + padding: 1rem; + text-align: center; +} + +.profile-info h3 { + margin: 0 0 0.25rem 0; + font-size: 1.1rem; +} + +.profile-info .role { + color: #666; + margin: 0; + font-size: 0.9rem; +} + +.jsx-demo.dark-theme .profile-info .role { + color: #aaa; +} + +code { + background-color: #f0f0f0; + padding: 0.1rem 0.3rem; + border-radius: 3px; + font-size: 0.9em; +} diff --git a/examples/preact-isomorphic/src/isomorphic/client.js b/examples/preact-isomorphic/src/isomorphic/client.js new file mode 100644 index 0000000..c08fac7 --- /dev/null +++ b/examples/preact-isomorphic/src/isomorphic/client.js @@ -0,0 +1,217 @@ +/** + * Preact Isomorphic Example + * + * This file demonstrates a todo application that works with both: + * 1. Server-side rendering (when imported by page.js) + * 2. Client-side hydration (when loaded in the browser) + * + * It uses the same component code for both environments. + */ +import { html, Component } from 'htm/preact' +import { render } from 'preact' +import { useCallback, useState } from 'preact/hooks' +import { useSignal, useComputed } from '@preact/signals' + +/** + * App Header Component + * Displays the title of the application + */ +const Header = ({ name, subtitle }) => html` +
+

${name}

+ ${subtitle && html`

${subtitle}

`} +
+` + +/** + * Todo Item Component + * Renders a single todo item with completion toggle + */ +const TodoItem = ({ text, completed, onToggle, onDelete }) => html` +
  • + + +
  • +` + +/** + * Counter Component using Signals + * Demonstrates Preact Signals for reactive state management + */ +const Counter = () => { + // Create a signal for the count value + const count = useSignal(0) + + // Derived state that automatically updates when count changes + const doubled = useComputed(() => count.value * 2) + const isEven = useComputed(() => count.value % 2 === 0) + + // Event handlers + const increment = useCallback(() => { count.value++ }, []) + const decrement = useCallback(() => { count.value > 0 && count.value-- }, []) + const reset = useCallback(() => { count.value = 0 }, []) + + return html` +
    +

    Signal-based Counter

    +
    + Count: ${count} + Doubled: ${doubled} +
    +
    + + + +
    +
    + ` +} + +/** + * Todo Application Component + * Manages a list of todos with add/toggle/delete functionality + */ +class TodoApp extends Component { + constructor(props) { + super(props) + // Initialize with example todos + this.state = { + todos: [ + { id: 1, text: 'Learn about SSR', completed: true }, + { id: 2, text: 'Build isomorphic apps', completed: false }, + { id: 3, text: 'Deploy to production', completed: false } + ], + newTodo: '' + } + } + + // Update the new todo input value + updateNewTodo = (e) => { + this.setState({ newTodo: e.target.value }) + } + + // Add a new todo item + addTodo = (e) => { + e.preventDefault() + const { todos, newTodo } = this.state + + if (newTodo.trim()) { + this.setState({ + todos: [ + ...todos, + { + id: Date.now(), + text: newTodo, + completed: false + } + ], + newTodo: '' + }) + } + } + + // Toggle a todo's completion status + toggleTodo = (id) => { + const { todos } = this.state + this.setState({ + todos: todos.map(todo => + todo.id === id + ? { ...todo, completed: !todo.completed } + : todo + ) + }) + } + + // Delete a todo item + deleteTodo = (id) => { + const { todos } = this.state + this.setState({ + todos: todos.filter(todo => todo.id !== id) + }) + } + + render({ title }, { todos, newTodo }) { + const remaining = todos.filter(todo => !todo.completed).length + + return html` +
    + <${Header} + name=${title || "Todo App"} + subtitle="Server + Client Rendering Example" + /> + +
    + + +
    + +
      + ${todos.map(todo => html` + <${TodoItem} + key=${todo.id} + text=${todo.text} + completed=${todo.completed} + onToggle=${() => this.toggleTodo(todo.id)} + onDelete=${() => this.deleteTodo(todo.id)} + /> + `)} +
    + +
    + ${remaining} item${remaining !== 1 ? 's' : ''} remaining +
    + + <${Counter} /> +
    + ` + } +} + +/** + * Main page export for both server and client rendering + * This is what gets rendered in both environments + */ +export const page = () => html` +
    + <${TodoApp} title="Isomorphic Todo App" /> + +
    +

    How This Works

    +

    + This page is rendered on the server first, then hydrated on the client. + The same component code runs in both environments. +

    +

    + Try adding todos and toggling them. These interactions are handled + by client-side JavaScript, but the initial HTML comes from the server. +

    +
    +
    +` + +/** + * Client-side only code + * This code only runs in the browser, not during server rendering + */ +if (typeof window !== 'undefined') { + // Find the container that was server-rendered + const renderTarget = document.querySelector('.app-main') + + // Hydrate the existing HTML with interactive components + if (renderTarget) { + render(page(), renderTarget) + console.log('βœ… Preact isomorphic app successfully hydrated') + } +} diff --git a/examples/preact/src/isomorphic/page.js b/examples/preact-isomorphic/src/isomorphic/page.js similarity index 100% rename from examples/preact/src/isomorphic/page.js rename to examples/preact-isomorphic/src/isomorphic/page.js diff --git a/examples/preact-isomorphic/src/jsx-page/client.jsx b/examples/preact-isomorphic/src/jsx-page/client.jsx new file mode 100644 index 0000000..1d92bc6 --- /dev/null +++ b/examples/preact-isomorphic/src/jsx-page/client.jsx @@ -0,0 +1,120 @@ +import { render } from 'preact' +import { useState, useEffect } from 'preact/hooks' + +/** + * Simple JSX Client-Side Component Example + * + * This demonstrates a Preact component using JSX syntax that runs + * exclusively on the client-side (browser). Unlike the isomorphic example, + * this component is not pre-rendered on the server. + */ + +// User profile card component +const ProfileCard = ({ name, role, avatar, isActive }) => ( +
    +
    + {`${name}'s + +
    +
    +

    {name}

    +

    {role}

    +
    +
    +) + +// Main application component +export const page = () => { + // State for the counter + const [count, setCount] = useState(0) + + // State for theme toggling + const [darkMode, setDarkMode] = useState(false) + + // State for user profiles + const [users, setUsers] = useState([ + { id: 1, name: "Alex Johnson", role: "Developer", isActive: true }, + { id: 2, name: "Sam Taylor", role: "Designer", isActive: false }, + { id: 3, name: "Jordan Casey", role: "Product Manager", isActive: true } + ]) + + // Effect to demonstrate client-side lifecycle + useEffect(() => { + console.log("Component mounted in the browser") + + // Update document title when count changes + document.title = `Count: ${count}` + + return () => { + console.log("Component will unmount") + } + }, [count]) + + // Toggle a user's active status + const toggleUserStatus = (userId) => { + setUsers(users.map(user => + user.id === userId + ? { ...user, isActive: !user.isActive } + : user + )) + } + + return ( +
    +

    Client-Side JSX Rendering

    + +
    + +
    + +
    +

    Interactive Counter: {count}

    +
    + + + +
    +
    + +
    +

    User Profiles

    +

    Click on a profile to toggle active status:

    +
    + {users.map(user => ( +
    toggleUserStatus(user.id)}> + +
    + ))} +
    +
    + +
    +

    How This Works

    +

    Unlike the isomorphic example, this component:

    +
      +
    • Renders entirely on the client-side
    • +
    • Uses native JSX syntax instead of HTM
    • +
    • Mounts to an empty container in the HTML
    • +
    +
    +
    + ) +} + +// Mount the component to the DOM +const renderTarget = document.querySelector('.jsx-app') +if (renderTarget) { + render(page(), renderTarget) +} diff --git a/examples/preact-isomorphic/src/jsx-page/page.html b/examples/preact-isomorphic/src/jsx-page/page.html new file mode 100644 index 0000000..28c1a88 --- /dev/null +++ b/examples/preact-isomorphic/src/jsx-page/page.html @@ -0,0 +1,38 @@ +
    +

    Client-Side JSX Rendering Example

    + +
    +

    + This example demonstrates how Preact JSX components can be mounted to static HTML pages. + Unlike isomorphic rendering, the component below is rendered entirely on the client-side. +

    + +
    +

    Key Concepts:

    +
      +
    • Static HTML page with an empty container
    • +
    • JSX syntax for component definition
    • +
    • Client-side only rendering lifecycle
    • +
    • No server-side pre-rendering
    • +
    +
    +
    + +
    +

    Live Demo:

    + +
    +
    + +
    +

    How It Works:

    +

    + The empty div.jsx-app above serves as a mount point. When the page loads, + the Preact component defined in client.jsx is rendered into this container. +

    +

    + This approach is useful when adding interactive components to otherwise static pages, + or when you want to isolate complex UI logic to the client-side only. +

    +
    +
    diff --git a/examples/preact/src/layouts/root.layout.js b/examples/preact-isomorphic/src/layouts/root.layout.js similarity index 100% rename from examples/preact/src/layouts/root.layout.js rename to examples/preact-isomorphic/src/layouts/root.layout.js diff --git a/examples/preact/src/README.md b/examples/preact/src/README.md deleted file mode 100644 index 93af148..0000000 --- a/examples/preact/src/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Preact example - -This is a preact example. - -- [Isomorphic Component Rendering](./isomorphic/) -- [JSX-page](./jsx-page/) diff --git a/examples/preact/src/isomorphic/client.js b/examples/preact/src/isomorphic/client.js deleted file mode 100644 index cbd8fed..0000000 --- a/examples/preact/src/isomorphic/client.js +++ /dev/null @@ -1,55 +0,0 @@ -import { html, Component } from 'htm/preact' -import { render } from 'preact' -import { useCallback } from 'preact/hooks' -import { useSignal, useComputed } from '@preact/signals' - -const Header = ({ name }) => html`

    ${name} List

    ` - -const Footer = props => { - const count = useSignal(0) - const double = useComputed(() => count.value * 2) - - const handleClick = useCallback(() => { - count.value++ - }, [count]) - - return html`
    - ${count} - ${double} - ${props.children} - -
    ` -} - -class App extends Component { - addTodo () { - const { todos = [] } = this.state - this.setState({ todos: todos.concat(`Item ${todos.length}`) }) - } - - render ({ page }, { todos = [] }) { - return html` -
    - <${Header} name="ToDo's (${page})" /> -
      - ${todos.map(todo => html` -
    • ${todo}
    • - `)} -
    - - <${Footer}>footer content here -
    - ` - } -} - -export const page = () => html` - <${App} page="Isomorphic"/> - <${Footer}>footer content here - <${Footer}>footer content here - ` - -if (typeof window !== 'undefined') { - const renderTarget = document.querySelector('.app-main') - render(page(), renderTarget) -} diff --git a/examples/preact/src/jsx-page/client.jsx b/examples/preact/src/jsx-page/client.jsx deleted file mode 100644 index fa47236..0000000 --- a/examples/preact/src/jsx-page/client.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import { render } from 'preact' - -export const page = () => { - return ( -
    - look ma, client side jsx! -
    - ) -} - -const renderTarget = document.querySelector('.jsx-app') -render(page(), renderTarget) diff --git a/examples/preact/src/jsx-page/page.html b/examples/preact/src/jsx-page/page.html deleted file mode 100644 index bf0074d..0000000 --- a/examples/preact/src/jsx-page/page.html +++ /dev/null @@ -1,4 +0,0 @@ -
    -

    This is an html page, with a client.jsx that mounts onto it

    -
    -
    diff --git a/examples/react/package.json b/examples/react/package.json new file mode 100644 index 0000000..452bd3f --- /dev/null +++ b/examples/react/package.json @@ -0,0 +1,27 @@ +{ + "name": "@domstack/react-typescript-example", + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "npm run watch", + "build": "npm run clean && domstack", + "clean": "rm -rf public && mkdir -p public", + "watch": "npm run clean && domstack --watch", + "typecheck": "tsc --noEmit" + }, + "author": "Bret Comnes (https://bret.io/)", + "license": "MIT", + "dependencies": { + "@domstack/cli": "../../.", + "mine.css": "^9.0.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@voxpelli/tsconfig": "^15.0.0", + "npm-run-all2": "^6.0.0", + "typescript": "~5.8.2" + } +} \ No newline at end of file diff --git a/examples/react/src/README.md b/examples/react/src/README.md new file mode 100644 index 0000000..35ff32d --- /dev/null +++ b/examples/react/src/README.md @@ -0,0 +1,53 @@ +# React with TypeScript in DOMStack + +This example demonstrates how to use React with TypeScript in DOMStack for client-side rendering. Unlike the Preact examples that come with DOMStack by default, this example shows how to override the default JSX configuration to use React with TypeScript instead. + +## What This Example Shows + +- How to configure ESBuild to use React instead of Preact for TSX +- Client-side rendering with React components written in TypeScript +- Type-safe React hooks for state management +- TypeScript interfaces and type definitions +- Integration with DOMStack's build system + +## Key Components + +1. **ESBuild Configuration**: Custom `esbuild.settings.ts` that overrides the default Preact JSX settings +2. **React Components**: Client-side components with typed hooks and state +3. **Static HTML Mount Points**: HTML pages with mount points for React components +4. **TypeScript Interfaces**: Type definitions for props, state, and functions + +## Example Structure + +- `globals/esbuild.settings.ts` - Configuration to use React instead of Preact +- `react-page/` - Client-side React component example with TypeScript +- `layouts/` - Basic layout structure + +## How It Works + +Unlike isomorphic examples with Preact, this example focuses on client-side rendering only. The workflow is: + +1. The HTML is served with empty containers +2. React components are loaded and mounted to these containers +3. All rendering happens in the browser + +## Getting Started + +Run the following commands: + +```bash +npm install +npm run build +``` + +To watch for changes during development: + +```bash +npm run watch +``` + +## React with TypeScript vs. Preact in DOMStack + +DOMStack uses Preact by default because it's smaller and has a compatible API with React. This example shows how to use React with TypeScript instead if you prefer or need specific React features with type safety. + +The key difference is in the `esbuild.settings.ts` file, which configures ESBuild to use React's TSX transformer and runtime, along with TypeScript support. \ No newline at end of file diff --git a/examples/react/src/globals/esbuild.settings.ts b/examples/react/src/globals/esbuild.settings.ts new file mode 100644 index 0000000..95b4119 --- /dev/null +++ b/examples/react/src/globals/esbuild.settings.ts @@ -0,0 +1,43 @@ +/** + * Custom ESBuild Settings for React with TypeScript + * + * This file overrides the default DOMStack ESBuild configuration + * to replace Preact with React for TSX transformation and runtime. + */ +import esbuild from 'esbuild' + +// Use the BuildOptions type from esbuild +type BuildOptions = esbuild.BuildOptions + +/** + * Configure ESBuild settings for React with TypeScript support + * + * @param esbuildSettings - The default ESBuild configuration + * @returns The modified ESBuild configuration + */ +export default async function esbuildSettingsOverride(esbuildSettings: BuildOptions): Promise { + // Override the JSX settings to use React instead of Preact + esbuildSettings.jsx = 'automatic' + esbuildSettings.jsxImportSource = 'react' + + // Enable TypeScript support + esbuildSettings.loader = { + ...esbuildSettings.loader, + '.ts': 'ts', + '.tsx': 'tsx' + } + + // Define React-specific globals if needed + esbuildSettings.define = { + ...esbuildSettings.define, + // Add any React-specific defines here if needed + } + + // Add any React-specific plugins or customizations + esbuildSettings.plugins = [ + ...(esbuildSettings.plugins || []) + // Add any additional plugins here if needed + ] + + return esbuildSettings +} diff --git a/examples/react/src/globals/global.client.ts b/examples/react/src/globals/global.client.ts new file mode 100644 index 0000000..782a44d --- /dev/null +++ b/examples/react/src/globals/global.client.ts @@ -0,0 +1,76 @@ +/** + * Global client-side TypeScript for React example + * + * This file is loaded on all pages before any page-specific TypeScript. + * It's a good place to add global event listeners, polyfills, or + * other initialization code that should run on every page. + */ + +// Define interfaces for our global utilities +interface DomstackUtils { + formatDate(date: Date): string; + getRandomItem(array: T[]): T; +} + +// Extend the Window interface to include our global utilities +interface Window { + domstackUtils: DomstackUtils; +} + +console.log('React example global client TypeScript loaded'); + +// Add a class to indicate JavaScript is enabled +document.documentElement.classList.add('js-enabled'); + +// Basic example of a global utility function +window.domstackUtils = { + /** + * Format a date in a human-readable format + * @param date - The date to format + * @returns Formatted date string + */ + formatDate: (date: Date): string => { + return new Intl.DateTimeFormat('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }).format(date); + }, + + /** + * Get a random item from an array + * @param array - The array to get a random item from + * @returns A random item from the array + */ + getRandomItem: (array: T[]): T => { + if (array.length === 0) { + throw new Error("Cannot get random item from empty array"); + } + const index = Math.floor(Math.random() * array.length); + // This assertion is safe because we've checked that array is not empty + return array[index] as T; + } +}; + +// Add dark mode detection +const prefersDarkMode: MediaQueryList = window.matchMedia('(prefers-color-scheme: dark)'); +if (prefersDarkMode.matches) { + document.body.classList.add('dark-mode-preferred'); +} + +// Listen for dark mode changes +prefersDarkMode.addEventListener('change', (e: MediaQueryListEvent): void => { + if (e.matches) { + document.body.classList.add('dark-mode-preferred'); + } else { + document.body.classList.remove('dark-mode-preferred'); + } +}); + +// Example of measuring and logging performance +const pageLoadTime: number = performance.now(); +window.addEventListener('load', (): void => { + const totalLoadTime: number = performance.now() - pageLoadTime; + console.log(`Page fully loaded in ${totalLoadTime.toFixed(2)}ms`); +}); \ No newline at end of file diff --git a/examples/react/src/globals/global.css b/examples/react/src/globals/global.css new file mode 100644 index 0000000..7f91f37 --- /dev/null +++ b/examples/react/src/globals/global.css @@ -0,0 +1,119 @@ +@import 'mine.css/dist/mine.css'; +@import 'mine.css/dist/layout.css'; + +/* Custom React Example Styles */ +:root { + --primary-color: #61dafb; + --secondary-color: #282c34; + --text-color: #333; + --background-color: #f5f5f5; + --card-background: #fff; + --success-color: #4caf50; + --warning-color: #ff9800; + --error-color: #f44336; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + color: var(--text-color); + background-color: var(--background-color); + line-height: 1.6; +} + +.app-container { + max-width: 800px; + margin: 0 auto; + padding: 2rem; +} + +.react-demo { + background-color: var(--card-background); + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 1.5rem; + margin-bottom: 2rem; +} + +.react-header { + display: flex; + align-items: center; + margin-bottom: 1.5rem; +} + +.react-logo { + animation: spin 10s linear infinite; + height: 40px; + margin-right: 1rem; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.button { + background-color: var(--primary-color); + color: var(--secondary-color); + border: none; + border-radius: 4px; + padding: 0.5rem 1rem; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.2s, transform 0.1s; +} + +.button:hover { + background-color: #4ac0e0; + transform: translateY(-1px); +} + +.button:active { + transform: translateY(1px); +} + +.button-group { + display: flex; + gap: 0.5rem; + margin: 1rem 0; +} + +.card { + border-radius: 6px; + border: 1px solid #eee; + padding: 1rem; + margin-bottom: 1rem; + transition: box-shadow 0.2s; +} + +.card:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.info-panel { + background-color: rgba(97, 218, 251, 0.1); + border-left: 4px solid var(--primary-color); + padding: 1rem; + margin: 1rem 0; + border-radius: 0 4px 4px 0; +} + +/* Form elements */ +input, select, textarea { + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; + width: 100%; + margin-bottom: 1rem; +} + +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(97, 218, 251, 0.2); +} \ No newline at end of file diff --git a/examples/react/src/layouts/root.layout.ts b/examples/react/src/layouts/root.layout.ts new file mode 100644 index 0000000..57cc2ca --- /dev/null +++ b/examples/react/src/layouts/root.layout.ts @@ -0,0 +1,61 @@ +import { html } from 'htm/preact' +import { render } from 'preact-render-to-string' + +// Define TypeScript interfaces for layout props +interface LayoutVars { + title?: string; + siteName?: string; + basePath?: string; +} + +interface LayoutProps { + vars: LayoutVars; + scripts?: string[]; + styles?: string[]; + children: string | any; +} + +/** + * Basic layout for React with TypeScript example + * + * This layout is only used for the initial HTML page structure. + * React components will be mounted client-side after the page loads. + */ +export default function rootLayout({ + vars: { + title, + siteName = 'React TypeScript Example', + basePath, + }, + scripts, + styles, + children, +}: LayoutProps): string { + return /* html */` + + + ${render(html` + + + ${title ? `${title}` : ''}${title && siteName ? ' | ' : ''}${siteName} + + + ${scripts + ? scripts.map(script => html``) - : ''} - ${styles - ? styles.map(style => /* html */``) - : ''} - - - ${children} - + + + ${siteName || ''}${title ? ` | ${title}` : ''} + + ${scripts + ? scripts.map(script => /* html */``).join('\n ') + : ''} + ${styles + ? styles.map(style => /* html */``).join('\n ') + : ''} + + +
    + ${children} +
    + -` + ` } diff --git a/examples/tailwind/src/README.md b/examples/tailwind/src/README.md index 7febf99..069373d 100644 --- a/examples/tailwind/src/README.md +++ b/examples/tailwind/src/README.md @@ -1,5 +1,40 @@ -# Preact example +# Tailwind CSS in DOMStack -This is a preact example WITH TAILWIND! +This example demonstrates how to integrate Tailwind CSS with DOMStack, providing a powerful utility-first CSS framework for your websites and applications. -[Isomorphic Component Rendering](./isomorphic/) +## What is Tailwind CSS? + +Tailwind CSS is a utility-first CSS framework that allows you to build custom designs without leaving your HTML. Instead of pre-designed components, Tailwind provides low-level utility classes that let you build completely custom designs. + +## How This Example Works + +This example shows how to: + +1. Configure ESBuild to process Tailwind CSS +2. Import Tailwind into your global CSS +3. Use Tailwind classes in Preact components +4. Create responsive, utility-based designs + +## Key Files + +- `globals/esbuild.settings.js` - Configures the Tailwind plugin +- `globals/global.css` - Imports the Tailwind framework +- `isomorphic/client.js` - Demonstrates Tailwind classes in components + +## Example Component + +The example includes a Todo application with Tailwind styling for: +- Responsive layouts +- Spacing utilities +- Flexbox components +- Colors and shadows +- Interactive states (hover effects) + +## Getting Started + +Explore the [Isomorphic Component Rendering](./isomorphic/) example to see Tailwind in action. + +## Learn More + +- [Tailwind CSS Documentation](https://tailwindcss.com/docs) +- [DOMStack Documentation](https://github.com/bcomnes/domstack) diff --git a/examples/tailwind/src/globals/esbuild.settings.js b/examples/tailwind/src/globals/esbuild.settings.js index 7c0ba4e..a7768b5 100644 --- a/examples/tailwind/src/globals/esbuild.settings.js +++ b/examples/tailwind/src/globals/esbuild.settings.js @@ -1,8 +1,29 @@ +/** + * Tailwind CSS Integration for DOMStack + * + * This file configures ESBuild to process Tailwind CSS in your project. + * It enables utility-first CSS classes that can be used directly in your HTML and components. + */ import tailwindPlugin from 'esbuild-plugin-tailwindcss' +/** + * Configure ESBuild settings to include Tailwind CSS processing + * + * @param {import('esbuild').BuildOptions} esbuildSettings - The default ESBuild configuration + * @return {Promise} - The modified ESBuild configuration + */ export default async function esbuildSettingsOverride (esbuildSettings) { + // Add the Tailwind plugin to the ESBuild configuration esbuildSettings.plugins = [ - tailwindPlugin(), + tailwindPlugin({ + // You can add Tailwind plugin options here if needed + // Example: tailwindConfig: './custom-tailwind.config.js' + }), ] + + // You can also add other ESBuild settings as needed + // esbuildSettings.minify = true; + // esbuildSettings.sourcemap = true; + return esbuildSettings } diff --git a/examples/type-stripping/src/README.md b/examples/type-stripping/src/README.md index f922c8c..52bd0b3 100644 --- a/examples/type-stripping/src/README.md +++ b/examples/type-stripping/src/README.md @@ -1,6 +1,50 @@ -# Preact example +# TypeScript Support in DOMStack -This is a preact example. +This example demonstrates how DOMStack handles TypeScript files by automatically stripping types during the build process, allowing you to use TypeScript without additional configuration. -- [Isomorphic Component Rendering](./isomorphic/) -- [tsx-client]('./isomorphic/') +## What is Type Stripping? + +Type stripping is the process of removing TypeScript type annotations during compilation to produce standard JavaScript. DOMStack performs this automatically using ESBuild, giving you: + +- Full TypeScript type checking during development +- Clean JavaScript output without runtime type overhead +- No need for separate TypeScript build steps + +## Features Demonstrated + +This example showcases: + +- `.ts` files for standard TypeScript +- `.tsx` files for JSX with TypeScript +- Type imports and exports +- Interface definitions +- Automatic handling of type annotations + +## Examples in This Project + +- [Isomorphic Component Rendering](./isomorphic/) - TypeScript with Preact +- [TSX Client Example](./tsx-page/) - TypeScript JSX components + +## How It Works + +1. Write your code using full TypeScript syntax +2. DOMStack detects `.ts` and `.tsx` file extensions +3. ESBuild automatically strips type annotations during bundling +4. Your pages and components work exactly like JavaScript versions + +## Benefits of TypeScript in DOMStack + +- **Developer Experience**: Get IDE autocompletion and type checking +- **Error Prevention**: Catch type errors before runtime +- **Documentation**: Types serve as self-documenting code +- **Zero Runtime Cost**: All types are removed in the final output + +## Getting Started with TypeScript + +To use TypeScript in your DOMStack project: + +1. Create files with `.ts` or `.tsx` extensions +2. Write TypeScript code normally +3. DOMStack will handle the rest automatically + +No additional setup required! diff --git a/examples/type-stripping/src/isomorphic/client.ts b/examples/type-stripping/src/isomorphic/client.ts index cbd8fed..371ab1d 100644 --- a/examples/type-stripping/src/isomorphic/client.ts +++ b/examples/type-stripping/src/isomorphic/client.ts @@ -3,53 +3,145 @@ import { render } from 'preact' import { useCallback } from 'preact/hooks' import { useSignal, useComputed } from '@preact/signals' -const Header = ({ name }) => html`

    ${name} List

    ` +// Type definitions demonstrate TypeScript features +interface HeaderProps { + name: string; + subtitle?: string; +} + +interface TodoItem { + id: number; + text: string; + completed: boolean; +} + +interface FooterProps { + theme?: 'light' | 'dark'; + children?: any; + onReset?: () => void; +} + +interface AppProps { + page: string; + maxItems?: number; +} + +interface AppState { + todos: TodoItem[]; + filter: 'all' | 'active' | 'completed'; +} -const Footer = props => { - const count = useSignal(0) - const double = useComputed(() => count.value * 2) +// Component with typed props +const Header = ({ name, subtitle }: HeaderProps): any => + html`
    +

    ${name} List

    + ${subtitle ? html`

    ${subtitle}

    ` : null} +
    ` - const handleClick = useCallback(() => { +// Functional component with TypeScript hooks +const Footer = (props: FooterProps) => { + const count = useSignal(0) + const double = useComputed(() => count.value * 2) + + const handleClick = useCallback((): void => { count.value++ }, [count]) return html`
    - ${count} - ${double} +
    Count: ${count}
    +
    Double: ${double}
    ${props.children} + ${props.onReset ? html`` : null}
    ` } -class App extends Component { - addTodo () { - const { todos = [] } = this.state - this.setState({ todos: todos.concat(`Item ${todos.length}`) }) +// Class component with typed props and state +class App extends Component { + // Type the state initialization + state: AppState = { + todos: [], + filter: 'all' + } + + // Typed methods + addTodo(): void { + const { todos } = this.state + const { maxItems = 10 } = this.props + + if (todos.length < maxItems) { + this.setState({ + todos: [ + ...todos, + { + id: Date.now(), + text: `Item ${todos.length}`, + completed: false + } + ] + }) + } + } + + toggleTodo(id: number): void { + this.setState({ + todos: this.state.todos.map(todo => + todo.id === id ? { ...todo, completed: !todo.completed } : todo + ) + }) + } + + // Generic utility method with type parameter + filterItems(items: T[], status: AppState['filter']): T[] { + if (status === 'active') return items.filter(i => !i.completed) + if (status === 'completed') return items.filter(i => i.completed) + return items } - render ({ page }, { todos = [] }) { + render({ page }: AppProps, { todos, filter }: AppState) { + // Use our generic method + const filteredTodos = this.filterItems(todos, filter) + return html`
    - <${Header} name="ToDo's (${page})" /> + <${Header} name="ToDo's (${page})" subtitle="TypeScript Example" /> + +
    + + + +
    +
      - ${todos.map(todo => html` -
    • ${todo}
    • + ${filteredTodos.map(todo => html` +
    • this.toggleTodo(todo.id)}> + ${todo.text} +
    • `)}
    - <${Footer}>footer content here + <${Footer} + theme="light" + onReset=${() => this.setState({ todos: [] })}> + footer content here +
    ` } } -export const page = () => html` - <${App} page="Isomorphic"/> - <${Footer}>footer content here - <${Footer}>footer content here +// Export with proper return type annotation +export const page = (): any => html` + <${App} page="Isomorphic" maxItems=${15} /> + <${Footer} theme="light">footer content here + <${Footer} theme="dark">footer content here ` if (typeof window !== 'undefined') { const renderTarget = document.querySelector('.app-main') - render(page(), renderTarget) + if (renderTarget) { + render(page(), renderTarget) + } } diff --git a/examples/type-stripping/src/tsx-page/client.tsx b/examples/type-stripping/src/tsx-page/client.tsx index 497ef69..b1c5d18 100644 --- a/examples/type-stripping/src/tsx-page/client.tsx +++ b/examples/type-stripping/src/tsx-page/client.tsx @@ -1,14 +1,133 @@ import { render } from 'preact' +import { useState, useEffect } from 'preact/hooks' +// TypeScript interfaces for our component props +interface ButtonProps { + onClick: () => void; + variant: 'primary' | 'secondary' | 'danger'; + children: any; + disabled?: boolean; +} + +interface UserCardProps { + id: number; + name: string; + email: string; + role?: string; +} + +// Styled button component with TypeScript props +const Button = ({ onClick, variant, children, disabled = false }: ButtonProps) => { + // Compute classes based on variant + const getButtonClass = (): string => { + switch (variant) { + case 'primary': + return 'bg-blue-500 hover:bg-blue-700 text-white' + case 'secondary': + return 'bg-gray-500 hover:bg-gray-700 text-white' + case 'danger': + return 'bg-red-500 hover:bg-red-700 text-white' + default: + return 'bg-blue-500 hover:bg-blue-700 text-white' + } + } + + return ( + + ) +} + +// User card component with TypeScript props +const UserCard = ({ id, name, email, role = 'User' }: UserCardProps) => ( +
    +

    {name}

    +

    ID: {id}

    +

    Email: {email}

    +

    Role: {role}

    +
    +) + +// Main application component with state management export const page = () => { + // TypeScript typed state + const [users, setUsers] = useState([ + { id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' }, + { id: 2, name: 'Jane Smith', email: 'jane@example.com' } + ]) + + const [count, setCount] = useState(0) + const [isLoading, setIsLoading] = useState(false) + + // TypeScript void return type + const incrementCounter = (): void => { + setCount(prev => prev + 1) + } + + // Add a new user with TypeScript type safety + const addUser = (): void => { + setIsLoading(true) + + // Simulate API call + setTimeout(() => { + const newUser: UserCardProps = { + id: users.length + 1, + name: `User ${users.length + 1}`, + email: `user${users.length + 1}@example.com` + } + + setUsers([...users, newUser]) + setIsLoading(false) + }, 500) + } + + // TypeScript with useEffect + useEffect((): void => { + document.title = `${count} clicks` + }, [count]) + return ( -
    - look ma, client side jsx! +
    +

    TypeScript JSX Example

    + +
    +

    Counter: {count}

    + +
    + +
    +

    Users

    + {users.map(user => ( + + ))} + +
    + +
    +
    + +
    ) } +// TypeScript DOM null check const renderTarget = document.querySelector('.jsx-app') if (renderTarget) { - render(page(), renderTarget) + render(page(), renderTarget) } diff --git a/examples/type-stripping/src/tsx-page/page.html b/examples/type-stripping/src/tsx-page/page.html index bf0074d..4557f50 100644 --- a/examples/type-stripping/src/tsx-page/page.html +++ b/examples/type-stripping/src/tsx-page/page.html @@ -1,4 +1,30 @@
    -

    This is an html page, with a client.jsx that mounts onto it

    -
    +

    TypeScript JSX Integration Example

    +

    This example demonstrates how DOMStack can use TypeScript JSX (.tsx) files for client-side rendering with automatic type stripping.

    + +
    +

    Features Demonstrated:

    +
      +
    • TypeScript interfaces for component props
    • +
    • Strongly typed state management
    • +
    • Type-safe event handlers
    • +
    • Component composition with TypeScript
    • +
    +
    + +
    +

    Live Demo:

    +
    +
    + +
    +

    How It Works:

    +

    The client.tsx file contains TypeScript JSX code that:

    +
      +
    1. Defines interfaces for component props
    2. +
    3. Creates typed state variables
    4. +
    5. Uses type-safe functions
    6. +
    7. Gets compiled to JavaScript automatically by DOMStack
    8. +
    +
    diff --git a/examples/uhtml-isomorphic/README.md b/examples/uhtml-isomorphic/README.md new file mode 100644 index 0000000..a089390 --- /dev/null +++ b/examples/uhtml-isomorphic/README.md @@ -0,0 +1,110 @@ +# uhtml-isomorphic Example + +This example demonstrates how to use [uhtml-isomorphic](https://github.com/WebReflection/uhtml-isomorphic) with DOMStack for isomorphic component rendering. + +## Overview + +uhtml-isomorphic is a lightweight library that provides the same API for both server and client rendering, making it easy to build components that work in both environments. This approach offers several benefits: + +- Write components once, use them everywhere +- Server-side rendering for fast initial page loads +- Client-side hydration for interactivity +- No JSX compilation required +- Efficient DOM updates + +## Getting Started + +### Prerequisites + +- Node.js 22.x or higher + +### Installation + +```bash +# Install dependencies +npm install +``` + +### Building the Example + +```bash +# Build the site +npm run build + +# Watch for changes during development +npm run watch +``` + +The built site will be in the `public` directory. + +## Project Structure + +``` +src/ +β”œβ”€β”€ isomorphic/ # Isomorphic component example +β”‚ β”œβ”€β”€ client.js # Client-side hydration code +β”‚ └── page.js # Server-side rendering code +β”œβ”€β”€ html-mount/ # HTML mount example +β”‚ β”œβ”€β”€ client.js # Client mounting code +β”‚ └── page.html # Static HTML template +β”œβ”€β”€ layouts/ # Layout templates +β”‚ └── root.layout.js # Root layout using uhtml-isomorphic +└── README.md # Main content (becomes index.html) +``` + +## Key Features Demonstrated + +### 1. Isomorphic Components + +The isomorphic example shows how to: +- Create components that render the same way on server and client +- Share code between environments +- Add client-side interactivity via hydration + +### 2. HTML Mounting + +The HTML mount example demonstrates: +- Starting with static HTML content +- Using uhtml-isomorphic to enhance it with dynamic client-side features +- Mounting components to specific DOM elements + +### 3. uhtml-isomorphic Layout + +The root layout shows how to: +- Build a complete HTML document structure +- Insert dynamic content +- Handle scripts and styles + +## How uhtml-isomorphic Works + +uhtml-isomorphic uses tagged template literals to define components: + +```js +import { html, render } from 'uhtml-isomorphic' + +// Create a component +const myComponent = (name) => html` +
    +

    Hello, ${name}!

    +

    Welcome to uhtml-isomorphic

    +
    +` + +// Server-side rendering +const output = render(String, myComponent('World')) + +// Client-side rendering +const container = document.querySelector('.app') +render(container, myComponent('World')) +``` + +## Learn More + +- [uhtml-isomorphic Documentation](https://github.com/WebReflection/uhtml-isomorphic) +- [DOMStack Documentation](https://github.com/bcomnes/domstack) + +## Related Examples + +Check out these other DOMStack examples: +- basic - Core features demonstration +- string-layouts - Simple template string layouts \ No newline at end of file diff --git a/examples/uhtml-isomorphic/package.json b/examples/uhtml-isomorphic/package.json new file mode 100644 index 0000000..e5a4d61 --- /dev/null +++ b/examples/uhtml-isomorphic/package.json @@ -0,0 +1,22 @@ +{ + "name": "@domstack/uhtml-isomorphic-example", + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "npm run watch", + "build": "npm run clean && domstack", + "clean": "rm -rf public && mkdir -p public", + "watch": "npm run clean && tb --watch" + }, + "author": "Bret Comnes (https://bret.io/)", + "license": "MIT", + "dependencies": { + "highlight.js": "^11.9.0", + "mine.css": "^9.0.1", + "uhtml-isomorphic": "^2.1.0", + "@domstack/cli": "../../." + }, + "devDependencies": { + "npm-run-all2": "^6.0.0" + } +} diff --git a/examples/uhtml-isomorphic/src/README.md b/examples/uhtml-isomorphic/src/README.md new file mode 100644 index 0000000..1cc6663 --- /dev/null +++ b/examples/uhtml-isomorphic/src/README.md @@ -0,0 +1,24 @@ +# uhtml-isomorphic Example + +This example demonstrates using [uhtml-isomorphic](https://github.com/WebReflection/uhtml-isomorphic) for building isomorphic components with DOMStack. + +## Features + +- Server-side rendering with the same syntax as client-side code +- Hydration of server-rendered components +- Pure JavaScript approach (no JSX required) +- Lightweight and efficient DOM updates + +## Examples + +- [Isomorphic Component Rendering](./isomorphic/) - Full isomorphic rendering with hydration +- [HTML Mount Example](./html-mount/) - Client-side mounting to HTML pages + +## How It Works + +uhtml-isomorphic provides a unified API for both server and client rendering, allowing you to write components once and use them everywhere. This example shows how to: + +1. Create components using tagged template literals +2. Render on the server with DOMStack +3. Hydrate in the browser for interactivity +4. Use the same component code in both environments diff --git a/examples/uhtml-isomorphic/src/globals/global.client.js b/examples/uhtml-isomorphic/src/globals/global.client.js new file mode 100644 index 0000000..4000282 --- /dev/null +++ b/examples/uhtml-isomorphic/src/globals/global.client.js @@ -0,0 +1,4 @@ +// @ts-ignore +import { toggleTheme } from 'mine.css' +// @ts-ignore +window.toggleTheme = toggleTheme diff --git a/examples/preact/src/globals/global.css b/examples/uhtml-isomorphic/src/globals/global.css similarity index 100% rename from examples/preact/src/globals/global.css rename to examples/uhtml-isomorphic/src/globals/global.css diff --git a/examples/uhtml-isomorphic/src/html-mount/client.js b/examples/uhtml-isomorphic/src/html-mount/client.js new file mode 100644 index 0000000..2d02d43 --- /dev/null +++ b/examples/uhtml-isomorphic/src/html-mount/client.js @@ -0,0 +1,12 @@ +import { html, render } from 'uhtml-isomorphic' + +export const page = () => { + return html` +
    + look ma, client side uhtml-isomorphic! +
    + ` +} + +const renderTarget = document.querySelector('.uhtml-app') +render(renderTarget, page()) diff --git a/examples/uhtml-isomorphic/src/html-mount/page.html b/examples/uhtml-isomorphic/src/html-mount/page.html new file mode 100644 index 0000000..f984c64 --- /dev/null +++ b/examples/uhtml-isomorphic/src/html-mount/page.html @@ -0,0 +1,4 @@ +
    +

    This is an html page, with a client.js that mounts onto it

    +
    +
    diff --git a/examples/uhtml-isomorphic/src/isomorphic/client.js b/examples/uhtml-isomorphic/src/isomorphic/client.js new file mode 100644 index 0000000..e897de9 --- /dev/null +++ b/examples/uhtml-isomorphic/src/isomorphic/client.js @@ -0,0 +1,43 @@ +import { html, render } from 'uhtml-isomorphic' + +// Simple counter state +let counter = 0 + +// Function to update counter display +function updateCounter() { + const counterElement = document.querySelector('.counter-value') + if (counterElement) { + counterElement.textContent = counter + } +} + +// Initialize client-side interactivity +function initializeCounter() { + const incrementButton = document.querySelector('.increment-button') + const decrementButton = document.querySelector('.decrement-button') + + if (incrementButton) { + incrementButton.addEventListener('click', () => { + counter++ + updateCounter() + }) + } + + if (decrementButton) { + decrementButton.addEventListener('click', () => { + counter-- + updateCounter() + }) + } +} + +// Hydrate the component when in browser +if (typeof window !== 'undefined') { + // Wait for DOM to be ready + window.addEventListener('DOMContentLoaded', () => { + // Initialize counter interactivity + initializeCounter() + + console.log('uhtml-isomorphic component hydrated!') + }) +} diff --git a/examples/uhtml-isomorphic/src/isomorphic/page.js b/examples/uhtml-isomorphic/src/isomorphic/page.js new file mode 100644 index 0000000..dab2183 --- /dev/null +++ b/examples/uhtml-isomorphic/src/isomorphic/page.js @@ -0,0 +1,17 @@ +import { html } from 'uhtml-isomorphic' + +export default () => { + return html` +
    +

    uhtml-isomorphic Example

    +

    This page is rendered using uhtml-isomorphic, which provides isomorphic rendering capabilities.

    +

    The client-side JavaScript will hydrate this component.

    +
    +

    Interactive Counter

    +

    Counter value: 0

    + + +
    +
    + ` +} diff --git a/examples/uhtml-isomorphic/src/layouts/root.layout.js b/examples/uhtml-isomorphic/src/layouts/root.layout.js new file mode 100644 index 0000000..72cf92a --- /dev/null +++ b/examples/uhtml-isomorphic/src/layouts/root.layout.js @@ -0,0 +1,53 @@ +import { html, render } from 'uhtml-isomorphic' + +/** + * @template {Record} T + * @typedef {import('../build-pages/resolve-layout.js').LayoutFunction} LayoutFunction + */ + +/** + * Build all of the bundles using esbuild. + * + * @type {LayoutFunction<{ + * title: string, + * siteName: string, + * defaultStyle: boolean, + * basePath: string + * }>} + */ +export default function defaultRootLayout ({ + vars: { + title, + siteName = 'Domstack', + basePath, + /* defaultStyle = true Set this to false in global or page to disable the default style in the default layout */ + }, + scripts, + styles, + children, + /* pages */ + /* page */ +}) { + return render(String, html` + + + + + ${title ? `${title}` : ''}${title && siteName ? ' | ' : ''}${siteName} + + ${scripts + ? scripts.map(script => html``) + : null} + ${styles + ? styles.map(style => html``) + : null} + + + ${typeof children === 'string' + ? html`
    ${html([children])}
    ` + : html`
    ${children}
    ` + } + + + `) +} diff --git a/package.json b/package.json index eb9252d..558874e 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "example:string-layouts": "cd examples/string-layouts && npm i --production && npm run build", "example:default-layout": "cd examples/default-layout && npm i --production && npm run build", "example:nested-dest": "cd examples/nested-dest && npm i --production && npm run build", - "example:preact": "cd examples/preact && npm i --production && npm run build", + "example:uhtml-isomorphic": "cd examples/uhtml-isomorphic && npm i --production && npm run build", "deps": "depcruise --exclude '^node_modules|^[a-zA-Z0-9\\_]+$' --output-type dot . | dot -T svg > dependencygraph.svg", "deps3d": "depcruise --exclude '^node_modules|^[a-zA-Z0-9\\_]+$' --output-type plugin:dependency-cruiser/sample-3d-reporter-plugin --output-to 3d-dependency-graph.html .", "start": "npm run watch" From 0d3b3ffbb4533b8b9c82d01ddb4d0410226025b7 Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Sun, 8 Jun 2025 10:03:54 -0700 Subject: [PATCH 08/51] Add page worker feature Page workers are worker entrypoints. They get bundle split with all other bundles, and a worker.json file is placed in the output directory with the page. You can load this file, get the worker filename and then create a worker from that entrypoint. This feature allows you to create workers in pages that get bundled with the rest of your site. --- README.md | 119 +++++++++++--- bin.js | 5 +- eslint.config.js | 1 + examples/esbuild-settings/src/client.js | 2 +- .../esbuild-settings/src/esbuild.settings.js | 12 +- .../src/markdown-it.settings.js | 26 +-- .../src/isomorphic/client.js | 68 ++++---- .../preact-isomorphic/src/jsx-page/client.jsx | 70 ++++---- .../tailwind/src/globals/esbuild.settings.js | 8 +- .../uhtml-isomorphic/src/isomorphic/client.js | 12 +- examples/worker-example/README.md | 121 ++++++++++++++ examples/worker-example/package.json | 22 +++ examples/worker-example/src/README.md | 83 ++++++++++ examples/worker-example/src/client.js | 110 +++++++++++++ .../worker-example/src/globals/global.css | 68 ++++++++ .../worker-example/src/globals/global.vars.js | 27 ++++ .../worker-example/src/layouts/root.layout.js | 47 ++++++ examples/worker-example/src/style.css | 100 ++++++++++++ .../worker-example/src/worker-page/client.js | 152 ++++++++++++++++++ .../src/worker-page/counter.worker.ts | 100 ++++++++++++ .../src/worker-page/fibonacci.worker.js | 89 ++++++++++ .../worker-example/src/worker-page/page.html | 90 +++++++++++ .../src/worker-page/page.vars.js | 11 ++ .../worker-example/src/worker-page/style.css | 109 +++++++++++++ lib/build-esbuild/index.js | 18 +++ lib/build-pages/page-builders/page-writer.js | 23 +++ lib/build-pages/page-data.js | 39 ++++- lib/build-pages/resolve-layout.js | 1 + lib/build-pages/resolve-vars.js | 1 + lib/helpers/generate-tree-data.js | 15 ++ lib/identify-pages.js | 24 +++ lib/identify-pages.test.js | 2 +- test-cases/general-features/index.test.js | 62 ++++++- .../src/markdown-it.settings.js | 22 +-- .../src/worker-page/client.js | 33 ++++ .../src/worker-page/counter.worker.js | 26 +++ .../general-features/src/worker-page/page.js | 27 ++++ .../src/worker-page/style.css | 45 ++++++ 38 files changed, 1640 insertions(+), 150 deletions(-) create mode 100644 examples/worker-example/README.md create mode 100644 examples/worker-example/package.json create mode 100644 examples/worker-example/src/README.md create mode 100644 examples/worker-example/src/client.js create mode 100644 examples/worker-example/src/globals/global.css create mode 100644 examples/worker-example/src/globals/global.vars.js create mode 100644 examples/worker-example/src/layouts/root.layout.js create mode 100644 examples/worker-example/src/style.css create mode 100644 examples/worker-example/src/worker-page/client.js create mode 100644 examples/worker-example/src/worker-page/counter.worker.ts create mode 100644 examples/worker-example/src/worker-page/fibonacci.worker.js create mode 100644 examples/worker-example/src/worker-page/page.html create mode 100644 examples/worker-example/src/worker-page/page.vars.js create mode 100644 examples/worker-example/src/worker-page/style.css create mode 100644 test-cases/general-features/src/worker-page/client.js create mode 100644 test-cases/general-features/src/worker-page/counter.worker.js create mode 100644 test-cases/general-features/src/worker-page/page.js create mode 100644 test-cases/general-features/src/worker-page/style.css diff --git a/README.md b/README.md index 494671d..a3c7c74 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Types in JS](https://img.shields.io/badge/types_in_js-yes-brightgreen)](https://github.com/voxpelli/types-in-js) [![Neocities][neocities-img]](https://domstack.net) -`domstack`: Build websites with actual html, md, css and js, and now ts and jsx. +`domstack`: Cut the gordian knot of modern web development and build websites with actual html, md, css, js, ts and jsx. ```console npm install @domstack/cli @@ -35,7 +35,7 @@ Usage: domstack [options] --drafts Build draft pages with the `.draft.{md,js,html}` page suffix. --target, -t comma separated target strings for esbuild --noEsbuildMeta skip writing the esbuild metafile to disk - --eject, -e eject the top bun default layout, style and client into the src flag directory + --eject, -e eject the DOMStack default layout, style and client into the src flag directory --watch, -w build, watch and serve the site build --watch-only watch and build the src folder without serving --copy path to directories to copy into dist; can be used multiple times @@ -51,7 +51,7 @@ domstack (v11.0.0) - Running `domstack --watch` or `domstack -w` will build the site and start an auto-reloading development web-server that watches for changes. - Running `domstack --eject` or `domstack -e` will extract the default layout, global styles, and client-side JavaScript into your source directory and add the necessary dependencies to your package.json. -`top-bun` is primarily a unix `bin` written for the [Node.js](https://nodejs.org) runtime that is intended to be installed from `npm` as a `devDependency` inside a `package.json` committed to a `git` repository. +`domstack` is primarily a unix `bin` written for the [Node.js](https://nodejs.org) runtime that is intended to be installed from `npm` as a `devDependency` inside a `package.json` committed to a `git` repository. It can be used outside of this context, but it works best within it. ## Core Concepts @@ -84,6 +84,11 @@ src % tree β”‚ └── page.ts # Anywhere you can use js in domstack, you can also use typescript files. They compile via speedy type stripping. β”œβ”€β”€ feeds β”‚ └── feeds.template.js # Templates let you generate any file you want from variables and page data. +β”œβ”€β”€ workers +β”‚ β”œβ”€β”€ client.ts +β”‚ └── page.ts +β”‚ β”œβ”€β”€ counter.worker.js # Web workers use a .worker.js naming convention and are auto-bundled +β”‚ └── analytics.worker.js β”œβ”€β”€ layouts # layouts can live anywhere. The inner content of your page is slotted into your layout. β”‚ β”œβ”€β”€ blog.layout.js # pages specify which layout they want by setting a `layout` page variable. β”‚ β”œβ”€β”€ blog.layout.css # layouts can define an additional layout style. @@ -127,8 +132,9 @@ Here are some additional external examples of larger domstack projects. If you have a project that uses domstack and could act as a nice example, please PR it to the list! - [Blog Example](https://github.com/bcomnes/bret.io/) -- [Isomorphic Static/Client App](https://github.com/hifiwi-fi/breadcrum.net/tree/master/packages/web/client) +- [Isomorphic Static/Client App](https://github.com/hifiwi-fi/example-app/tree/master/packages/web/client) - [Zero-Conf Markdown Docs](https://github.com/bcomnes/deploy-to-neocities/blob/70b264bcb37fca5b21e45d6cba9265f97f6bfa6f/package.json#L38) +- [Web Workers Example](https://github.com/domstack/domstack/tree/master/examples/worker-example) ## Pages @@ -167,14 +173,14 @@ An example of a `md` page: ```md --- title: A title for my markdown -favoriteBread: 'Baguette' +favoriteColor: 'Blue' --- -Just writing about baking. +Just writing about web development. -## Favorite breads +## Favorite colors -My favorite bread is \{{ vars.favoriteBread }}. +My favorite color is \{{ vars.favoriteColor }}. ``` ### `html` pages @@ -194,13 +200,13 @@ src/page-name/page.html An example `html` page: ```html -

    Favorite breads

    +

    Favorite frameworks

      -
    • French
    • -
    • Sour dough
    • -
    • Dutch crunch
    • - -
    • \{{ vars.favoriteBread }}
    • +
    • React
    • +
    • Vue
    • +
    • Svelte
    • + +
    • \{{ vars.favoriteFramework }}
    ``` @@ -358,6 +364,59 @@ It is a good idea to display something indicating the page is a draft in your te Any static assets near draft pages will still be copied because static assets are processed in parallel from page generation (to keep things fast). If you have an idea on how to relate static assets to a draft page for omission, please open a discussion issue. +## Web Workers + +DOMStack supports web workers through a simple naming convention. Any file with the pattern `{name}.worker.js` is recognized as a web worker and automatically bundled by esbuild. + +Web workers can be added to any page in your DOMStack project: + +``` +page-directory/ + β”œβ”€β”€ page.js + β”œβ”€β”€ client.js + β”œβ”€β”€ counter.worker.js # Worker with counter functionality + └── data.worker.js # Worker for data processing +``` + +During the build process, DOMStack: + +1. Bundles each worker file separately with proper cache-busting (hashed filenames) +2. Generates a `workers.json` file in each page directory that has workers +3. Maps the worker names directly to their hashed filenames in a flat structure + +To use web workers in your client code: + +```js +// First, fetch the workers.json to get worker paths +async function initializeWorkers() { + const response = await fetch('./workers.json'); + const workersData = await response.json(); + + // Initialize workers with the correct hashed filenames + const counterWorker = new Worker( + new URL(`./${workersData.counter}`, import.meta.url), + { type: 'module' } + ); + + // Use the worker + counterWorker.postMessage({ action: 'increment' }); + + counterWorker.onmessage = (e) => { + console.log(e.data); + }; + + return counterWorker; +} + +// Initialize workers when the page loads +document.addEventListener('DOMContentLoaded', async () => { + const worker = await initializeWorkers(); + // Use the worker in your page... +}); +``` + +See the [Web Workers Example](https://github.com/domstack/domstack/tree/master/examples/worker-example) for a complete implementation. + ## Layouts Layouts are "outer page templates" that pages get rendered into. @@ -392,7 +451,7 @@ A page referencing a layout name that doesn't have a matching layout file will r ### The default `root.layout.js` -A layout is a js file that `export default`'s an async or sync function that implements an outer-wrapper html template that will house the inner content from the page (`children`) being rendered. Think of the bread in a sandwich. That's a layout. πŸ₯ͺ +A layout is a js file that `export default`'s an async or sync function that implements an outer-wrapper html template that will house the inner content from the page (`children`) being rendered. Think of the frame around a picture. That's a layout. πŸ–ΌοΈ It is always passed a single object argument with the following entries: @@ -604,9 +663,9 @@ All static assets in the `src` directory are copied 1:1 to the `public` director ### `--eject` flag -The `--eject` (or `-e`) flag extracts top-bun's default layout, global CSS, and client-side JavaScript into your source directory. This allows you to fully customize these files while maintaining the same functionality. +The `--eject` (or `-e`) flag extracts DOMStack's default layout, global CSS, and client-side JavaScript into your source directory. This allows you to fully customize these files while maintaining the same functionality. -When you run `top-bun --eject`, it will: +When you run `domstack --eject`, it will: 1. Create a default root layout file at `layouts/root.layout.js` (or `.mjs` depending on your package.json type) 2. Create a default global CSS file at `globals/global.css` @@ -616,7 +675,7 @@ When you run `top-bun --eject`, it will: - uhtml-isomorphic - highlight.js -This is useful when you want to heavily customize the default theme or behavior while still leveraging top-bun's core functionality. +This is useful when you want to heavily customize the default theme or behavior while still leveraging DOMStack's core functionality. ### `--copy` directories @@ -961,9 +1020,9 @@ export default async function markdownItSettingsOverride (md) { } } }) - + md.use(markdownItPlantuml) - + return md } ``` @@ -979,10 +1038,10 @@ export default async function markdownItSettingsOverride (md) { breaks: true, // Convert \n to
    linkify: false, // Disable auto-linking }) - + // Add only the plugins you want newMd.use(myCustomPlugin) - + return newMd } ``` @@ -1198,13 +1257,13 @@ export default layout ## FAQ -Top-**Bun**? Like the JS runtime? +Why DOMStack? -: No, like the bakery from Wallace and Gromit in ["A Matter of Loaf and Death"](https://www.youtube.com/watch?v=zXBmZLmfQZ4s) +: DOMStack is named after the DOM (Document Object Model) and the concept of stacking technologies together to build websites. It represents the layering of HTML, CSS, and JavaScript in a cohesive build system. How does `domstack` relate to [`sitedown`](https://ghub.io/sitedown) -: `domstack` used to be called `siteup` which is sort of like "markup", which is related to "markdown", which inspired the project `sitedown` to which `domstack` is a spiritual off-shot of. Put a folder of web documents in your `domstack` oven, and bake a website. +: `domstack` used to be called `siteup` which is sort of like "markup", which is related to "markdown", which inspired the project `sitedown` to which `domstack` is a spiritual off-shoot of. Put a folder of web documents in your `domstack` build system, and generate a website. ## Examples @@ -1217,6 +1276,7 @@ Look at [examples](./examples/) and `domstack` [dependents](https://github.com/b - `js` and `css` is bundled with [`esbuild`](https://github.com/evanw/esbuild). - `md` is processed with [markdown-it](https://github.com/markdown-it/markdown-it). - static files are processed with [cpx2](https://github.com/bcomnes/cpx2). +- web workers are supported via special naming conventions and automatic path resolution. These tools are treated as implementation details, but they may be exposed more in the future. The idea is that they can be swapped out for better tools in the future if they don't make it. @@ -1423,6 +1483,15 @@ Some notable features are included below, see the [roadmap](https://github.com/u ## History +DOMStack started its life as `top-bun` in 2023, named after the bakery from Wallace and Gromit. The project was created to provide a simple, fast, and flexible static site generator that could handle modern web development needs while staying true to web standards. + +The project was renamed to DOMStack in version 11 to better reflect its purpose and avoid confusion with the Bun JavaScript runtime. The name DOMStack represents the layering of web technologies (HTML, CSS, JavaScript) that the tool helps developers stack together efficiently. + +Key milestones: +- **v7 (2023)**: Major rewrite and reintroduction as top-bun +- **v11 (2023)**: Renamed from top-bun to DOMStack +- **v12+**: Added full TypeScript support and improved performance +- **Current**: Added Web Workers support with automatic path resolution ## Links diff --git a/bin.js b/bin.js index 1347766..2fde7e5 100755 --- a/bin.js +++ b/bin.js @@ -1,13 +1,12 @@ #!/usr/bin/env node -// @ts-ignore import { readFile } from 'node:fs/promises' import { resolve, join, relative } from 'node:path' import { parseArgs } from 'node:util' import { printHelpText } from 'argsclopts' import readline from 'node:readline' import process from 'process' -// @ts-ignore +// @ts-expect-error import tree from 'pretty-tree' import { inspect } from 'util' import { packageDirectory } from 'pkg-dir' @@ -69,7 +68,7 @@ const options = { eject: { type: 'boolean', short: 'e', - help: 'eject the top bun default layout, style and client into the src flag directory', + help: 'eject the DOMStack default layout, style and client into the src flag directory', }, watch: { type: 'boolean', diff --git a/eslint.config.js b/eslint.config.js index 2a3cb56..caf35f0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,6 +1,7 @@ import neostandard, { resolveIgnoresFromGitignore } from 'neostandard' export default neostandard({ + env: ['browser'], ignores: [ ...resolveIgnoresFromGitignore(), 'test-cases/build-errors/src/**/*.js', diff --git a/examples/esbuild-settings/src/client.js b/examples/esbuild-settings/src/client.js index de6490e..fd48e7d 100644 --- a/examples/esbuild-settings/src/client.js +++ b/examples/esbuild-settings/src/client.js @@ -1,6 +1,6 @@ /** * ESBuild Node.js Polyfill Example - * + * * This file demonstrates how we can use Node.js built-in modules * in browser-side JavaScript when proper ESBuild settings are configured. */ diff --git a/examples/esbuild-settings/src/esbuild.settings.js b/examples/esbuild-settings/src/esbuild.settings.js index 3e29122..0abd54d 100644 --- a/examples/esbuild-settings/src/esbuild.settings.js +++ b/examples/esbuild-settings/src/esbuild.settings.js @@ -1,20 +1,20 @@ /** * ESBuild Settings Override - * + * * This file demonstrates how to customize the ESBuild configuration in DOMStack. * It allows Node.js built-in modules to be used in browser-side JavaScript by * applying polyfills through the esbuild-plugin-polyfill-node plugin. - * + * * @import { BuildOptions } from 'esbuild' */ import { polyfillNode } from 'esbuild-plugin-polyfill-node' /** * Configure ESBuild settings for browser-compatible Node.js modules - * + * * This function receives the default ESBuild configuration and returns a * modified version with additional plugins and settings. - * + * * @param {BuildOptions} esbuildSettings - The default ESBuild configuration * @return {Promise} - The modified ESBuild configuration */ @@ -26,10 +26,10 @@ export default async function esbuildSettingsOverride (esbuildSettings) { // For example: include: ['os', 'path', 'fs'] }), ] - + // You can also modify other ESBuild settings: // esbuildSettings.minify = true; // esbuildSettings.sourcemap = true; - + return esbuildSettings } diff --git a/examples/markdown-settings/src/markdown-it.settings.js b/examples/markdown-settings/src/markdown-it.settings.js index fe85137..542f4df 100644 --- a/examples/markdown-settings/src/markdown-it.settings.js +++ b/examples/markdown-settings/src/markdown-it.settings.js @@ -1,9 +1,9 @@ /** * Custom Markdown-it Configuration - * + * * This file demonstrates how to extend DOMStack's markdown rendering * capabilities by customizing the markdown-it instance. - * + * * Key features demonstrated: * 1. Adding custom container plugins (warning, info, details) * 2. Customizing code block rendering @@ -14,13 +14,13 @@ import markdownItContainer from 'markdown-it-container' /** * Creates a custom container plugin configuration - * + * * @param {string} name - Container name ('warning', 'info', etc.) * @param {string} defaultTitle - Default title if none specified * @param {string} cssClass - CSS class for the container * @returns {Object} - Container plugin configuration */ -function createContainer(name, defaultTitle, cssClass) { +function createContainer (name, defaultTitle, cssClass) { return { validate: function (params) { return params.trim().match(new RegExp(`^${name}\\s*(.*)$`)) @@ -30,8 +30,8 @@ function createContainer(name, defaultTitle, cssClass) { if (tokens[idx].nesting === 1) { // Opening tag const title = (m && m.length > 1) ? m[1] : defaultTitle - return `
    \n
    ` + - title + + return `
    \n
    ` + + title + `
    \n
    \n` } else { // Closing tag @@ -43,7 +43,7 @@ function createContainer(name, defaultTitle, cssClass) { /** * Customize the markdown-it instance with additional plugins and renderers - * + * * @param {import('markdown-it')} md - The markdown-it instance * @returns {import('markdown-it')} - The modified markdown-it instance */ @@ -51,18 +51,18 @@ export default async function markdownItSettingsOverride (md) { // ===================================================== // CUSTOM CONTAINERS // ===================================================== - + // Add warning container: ::: warning Title md.use( - markdownItContainer, - 'warning', + markdownItContainer, + 'warning', createContainer('warning', 'Warning', 'custom-warning') ) // Add info container: ::: info Title md.use( - markdownItContainer, - 'info', + markdownItContainer, + 'info', createContainer('info', 'Info', 'custom-info') ) @@ -85,7 +85,7 @@ export default async function markdownItSettingsOverride (md) { // ===================================================== // CUSTOM RENDERERS // ===================================================== - + // Customize code block rendering with enhanced styling // Customize existing renderer - add custom classes to code blocks md.renderer.rules.code_block = function (tokens, idx, options, env, renderer) { diff --git a/examples/preact-isomorphic/src/isomorphic/client.js b/examples/preact-isomorphic/src/isomorphic/client.js index c08fac7..3113def 100644 --- a/examples/preact-isomorphic/src/isomorphic/client.js +++ b/examples/preact-isomorphic/src/isomorphic/client.js @@ -1,15 +1,15 @@ /** * Preact Isomorphic Example - * + * * This file demonstrates a todo application that works with both: * 1. Server-side rendering (when imported by page.js) * 2. Client-side hydration (when loaded in the browser) - * + * * It uses the same component code for both environments. */ import { html, Component } from 'htm/preact' import { render } from 'preact' -import { useCallback, useState } from 'preact/hooks' +import { useCallback } from 'preact/hooks' import { useSignal, useComputed } from '@preact/signals' /** @@ -30,9 +30,9 @@ const Header = ({ name, subtitle }) => html` const TodoItem = ({ text, completed, onToggle, onDelete }) => html`