diff --git a/.github/workflows/ci-jsx-win.yml b/.github/workflows/ci-jsx-win.yml index 7e63c1e2..ebf26fe9 100644 --- a/.github/workflows/ci-jsx-win.yml +++ b/.github/workflows/ci-jsx-win.yml @@ -22,6 +22,3 @@ jobs: - name: Test run: | npm run test:jsx - - name: Build - run: | - npm run docs:build diff --git a/.github/workflows/ci-jsx.yml b/.github/workflows/ci-jsx.yml index 212cb4bc..4870ef77 100644 --- a/.github/workflows/ci-jsx.yml +++ b/.github/workflows/ci-jsx.yml @@ -22,6 +22,3 @@ jobs: - name: Test run: | npm run test:jsx - - name: Build - run: | - npm run docs:build diff --git a/.github/workflows/ci-win.yml b/.github/workflows/ci-win.yml index f9b6ba8d..d843d11d 100644 --- a/.github/workflows/ci-win.yml +++ b/.github/workflows/ci-win.yml @@ -22,6 +22,3 @@ jobs: - name: Test run: | npm test - - name: Build - run: | - npm run docs:build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aae2f752..096d30a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,10 +27,7 @@ jobs: npm run lint - name: Check Types run: | - npm run lint:types + npm run check:types - name: Test run: | npm test - - name: Build - run: | - npm run docs:build diff --git a/.gitignore b/.gitignore index ec8aa5d8..db4b0f3f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ .vscode/ coverage/ dist/ -node_modules/ \ No newline at end of file +node_modules/ +.greenwood/ +public/ \ No newline at end of file diff --git a/.ls-lint.yml b/.ls-lint.yml index 6eb6e8d6..ca20264f 100644 --- a/.ls-lint.yml +++ b/.ls-lint.yml @@ -17,4 +17,5 @@ ignore: - .git - coverage - node_modules - - dist + - public + - .greenwood diff --git a/.nvmrc b/.nvmrc index d135defb..89b93fd7 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.12.0 \ No newline at end of file +22.18.0 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 328015b9..0344567f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,8 +8,8 @@ We're excited for your interest in WCC, and maybe even your contribution! To develop for the project, you'll want to follow these steps: -1. Have [NodeJS LTS](https://nodejs.org) and / or use `nvm` (see below) 1. Clone the repository +1. Have [NodeJS LTS](https://nodejs.org) installed and / or use `nvm` (see below) 1. Run `npm ci` ### NVM @@ -31,12 +31,29 @@ The local development flow is based around building the docs website, using `wcc ### Commands -There are the main tasks, but you can see them all listed in _package.json#scripts_. +These are the main tasks, and you can see them all listed in _package.json#scripts_. -- `npm run docs:dev` - Builds the docs site for local development -- `npm start` - Builds a production version of the docs site and serves it locally -- `npm run sandbox` - Starts the sandbox app for live demos and testing +- `npm run dev` - Builds the docs site for local development - `npm test` - Run all the tests - `npm test:tdd` - Run all the tests in watch mode - `npm run lint` - Run all linters -- `npm run format` - Auto-format all files +- `npm run check:types` - Run `tsc` to validate TypeScript types +- `npm run format` - Auto-format all file + +## Website + +The website is built with [**Greenwood**](https://www.greenwoodjs.dev). To run the website locally, use one of the following commands: + +- `npm run dev` - Start the dev server +- `npm run build` - Generate a production build +- `npm run serve` - Serve a production build + +### Sandbox + +To assist in local development of WCC, there is a "sandbox" app built into the website, that can be used to validate a number of examples in the browser. (think of it as a storybook for WCC). + +After starting the dev server, visit the `/sandbox/` route in your browser. All code for the examples are in _./docs/components/sandbox/_. + +### Playground + +The website also hosts a Playground (REPL) for seeing WCC output in the browser in real time. Development happens in [this repo](https://github.com/ProjectEvergreen/playground.wcc.dev). diff --git a/README.md b/README.md index 44b8a4ac..f8e5eead 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![NodeJS compatibility](https://img.shields.io/node/v/wc-compiler.svg)](https://nodejs.org/en/about/previous-releases") [![Discord Chat](https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord)](https://www.greenwoodjs.dev/discord/) -> _Experimental Web Components compiler. It's Web Components all the way down!_ 🐢 +> _Native Web Components compiler. It's Web Components all the way down!_ 🐢 ## How It Works @@ -81,7 +81,7 @@ $ npm install wc-compiler --save-dev ## Documentation -See our [website](https://merry-caramel-524e61.netlify.app/) for API docs and examples. +See our [website](https://www.wcc.dev) for API docs and examples. ## Motivation diff --git a/build.js b/build.js deleted file mode 100644 index 10b46595..00000000 --- a/build.js +++ /dev/null @@ -1,94 +0,0 @@ -import fs from 'node:fs/promises'; -import rehypeAutolinkHeadings from 'rehype-autolink-headings'; -import rehypePrism from '@mapbox/rehype-prism'; -import rehypeSlug from 'rehype-slug'; -import rehypeStringify from 'rehype-stringify'; -import rehypeRaw from 'rehype-raw'; -import remarkParse from 'remark-parse'; -import remarkRehype from 'remark-rehype'; -import remarkToc from 'remark-toc'; -import { unified } from 'unified'; - -import { renderToString } from './src/wcc.js'; - -async function init() { - const distRoot = './dist'; - const pagesRoot = './docs/pages'; - const pages = await fs.readdir(new URL(pagesRoot, import.meta.url)); - const { html } = await renderToString(new URL('./docs/layout.js', import.meta.url)); - - await fs.rm(distRoot, { recursive: true, force: true }); - await fs.mkdir(distRoot, { recursive: true }); - await fs.mkdir(`${distRoot}/assets`, { recursive: true }); - - await fs.copyFile( - new URL('./node_modules/prismjs/themes/prism.css', import.meta.url), - new URL(`${distRoot}/prism.css`, import.meta.url), - ); - await fs.copyFile( - new URL('./node_modules/simple.css/dist/simple.min.css', import.meta.url), - new URL(`${distRoot}/simple.min.css`, import.meta.url), - ); - await fs.cp( - new URL('./docs/assets', import.meta.url), - new URL(`${distRoot}/assets`, import.meta.url), - { recursive: true }, - ); - await fs.copyFile( - new URL('./docs/assets/favicon.ico', import.meta.url), - new URL(`${distRoot}/favicon.ico`, import.meta.url), - ); - - for (const page of pages) { - const route = page.replace('.md', ''); - const outputPath = route === 'index' ? '' : `${route}/`; - const markdown = await fs.readFile(new URL(`${pagesRoot}/${page}`, import.meta.url), 'utf-8'); - const content = ( - await unified() - .use(remarkParse) - .use(remarkToc, { tight: true }) - .use(remarkRehype, { allowDangerousHtml: true }) - .use(rehypeSlug) - .use(rehypeRaw) - .use(rehypeAutolinkHeadings) - .use(rehypePrism) - .use(rehypeStringify) - .process(markdown) - ).value; - - await fs.mkdir(`./dist/${outputPath}`, { recursive: true }); - await fs.mkdir(`${distRoot}/${outputPath}`, { recursive: true }); - - await fs.writeFile( - new URL(`${distRoot}/${outputPath}/index.html`, import.meta.url), - ` - - - - - WCC - Web Components Compiler - - - - - - - - - - - - - - - - ${html.replace('', content)} - - - - `.trim(), - ); - } -} - -init(); diff --git a/docs/assets/bluesky.svg b/docs/assets/bluesky.svg new file mode 100644 index 00000000..869cc3de --- /dev/null +++ b/docs/assets/bluesky.svg @@ -0,0 +1,4 @@ + + Brand Bluesky Streamline Icon: https://streamlinehq.com + + \ No newline at end of file diff --git a/docs/assets/copy-button.svg b/docs/assets/copy-button.svg new file mode 100644 index 00000000..f5974c51 --- /dev/null +++ b/docs/assets/copy-button.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/assets/discord.svg b/docs/assets/discord.svg new file mode 100644 index 00000000..9a46ceb5 --- /dev/null +++ b/docs/assets/discord.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/assets/external-link.svg b/docs/assets/external-link.svg new file mode 100644 index 00000000..fa358922 --- /dev/null +++ b/docs/assets/external-link.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/assets/favicon.ico b/docs/assets/favicon.ico deleted file mode 100644 index b6d991a4..00000000 Binary files a/docs/assets/favicon.ico and /dev/null differ diff --git a/docs/assets/github.svg b/docs/assets/github.svg new file mode 100644 index 00000000..f34db0fb --- /dev/null +++ b/docs/assets/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/assets/html.svg b/docs/assets/html.svg new file mode 100644 index 00000000..e83c1299 --- /dev/null +++ b/docs/assets/html.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/assets/json.svg b/docs/assets/json.svg new file mode 100644 index 00000000..8103843f --- /dev/null +++ b/docs/assets/json.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/docs/assets/link.svg b/docs/assets/link.svg new file mode 100644 index 00000000..4864650a --- /dev/null +++ b/docs/assets/link.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/docs/assets/tile.svg b/docs/assets/tile.svg new file mode 100644 index 00000000..fa606599 --- /dev/null +++ b/docs/assets/tile.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/twitter-logo.svg b/docs/assets/twitter-logo.svg new file mode 100644 index 00000000..a96eb788 --- /dev/null +++ b/docs/assets/twitter-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/assets/typescript-logo.svg b/docs/assets/typescript-logo.svg new file mode 100644 index 00000000..77803046 --- /dev/null +++ b/docs/assets/typescript-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/wcc-logo-og.png b/docs/assets/wcc-logo-og.png new file mode 100644 index 00000000..36fd4713 Binary files /dev/null and b/docs/assets/wcc-logo-og.png differ diff --git a/docs/assets/wcc-logo.png b/docs/assets/wcc-logo.png deleted file mode 100644 index 27113d7f..00000000 Binary files a/docs/assets/wcc-logo.png and /dev/null differ diff --git a/docs/assets/wcc-logo.svg b/docs/assets/wcc-logo.svg new file mode 100644 index 00000000..6eef1a3f --- /dev/null +++ b/docs/assets/wcc-logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/assets/wcc-splash.png b/docs/assets/wcc-splash.png new file mode 100644 index 00000000..9a559962 Binary files /dev/null and b/docs/assets/wcc-splash.png differ diff --git a/docs/components/banner-cta/banner-cta.module.css b/docs/components/banner-cta/banner-cta.module.css new file mode 100644 index 00000000..18e90ff8 --- /dev/null +++ b/docs/components/banner-cta/banner-cta.module.css @@ -0,0 +1,69 @@ +.container { + width: 100%; + margin: var(--size-1) 0; + padding: var(--size-4); + background-color: #e7e8ea; + border-radius: var(--radius-4); + color: var(--color-primary); + font-size: var(--font-size-1); + + & p { + font-size: var(--font-size-2); + } +} + +.snippetContainer { + width: 100%; +} + +.snippet { + color: var(--color-primary); + border-radius: var(--radius-4); + border: var(--radius-1) solid #c1d3cd; + box-shadow: var(--shadow-2); + margin: var(--size-px-2) 0; + text-align: center; + padding: var(--size-2); + display: block; + + & pre { + font-size: var(--font-size-1); + } + + & wcc-ctc-button { + display: none; + } +} + +@media (min-width: 430px) { + .snippet { + & pre { + display: inline; + vertical-align: text-top; + } + + & wcc-ctc-button { + display: inline; + } + } +} + +@media (min-width: 768px) { + .container { + width: 80%; + margin: 0 auto; + padding: var(--size-4); + } +} + +@media (min-width: 1024px) { + .container { + width: 60%; + margin: 0 auto; + padding: var(--size-4); + + & p { + font-size: var(--font-size-3); + } + } +} diff --git a/docs/components/banner-cta/banner-cta.tsx b/docs/components/banner-cta/banner-cta.tsx new file mode 100644 index 00000000..6b5dcb43 --- /dev/null +++ b/docs/components/banner-cta/banner-cta.tsx @@ -0,0 +1,30 @@ +import '../ctc-button/ctc-button.tsx'; +import styles from './banner-cta.module.css'; + +export default class BannerCta extends HTMLElement { + static code = 'npm i -D wc-compiler'; + + connectedCallback() { + this.render(); + } + + render() { + return ( +
+

+ WCC (WC Compiler) is a NodeJS based package for server-rendering native Web + Components. No custom formats, no custom syntax. It's just standard JavaScript in, + standard HTML out. +

+
+
+
$ {BannerCta.code}
+ +
+
+
+ ); + } +} + +customElements.define('wcc-banner-cta', BannerCta); diff --git a/docs/components/banner-splash/banner-splash.module.css b/docs/components/banner-splash/banner-splash.module.css new file mode 100644 index 00000000..edbccf6a --- /dev/null +++ b/docs/components/banner-splash/banner-splash.module.css @@ -0,0 +1,35 @@ +.container { + width: 80%; + margin: 0 auto; + text-align: center; +} + +.splash { + display: inline-block; + background-image: url(/assets/wcc-splash.png); + background-repeat: no-repeat; + background-size: contain; + font-size: var(--size-6); + color: var(--color-white); + font-weight: bold; + width: 100px; +} + +.text { + display: inline; + font-size: var(--size-6); + height: 136px; + font-weight: bold; +} + +@media (min-width: 900px) { + .splash { + width: 297px; + height: 136px; + } + + .splash, + .text { + font-size: var(--size-10); + } +} diff --git a/docs/components/banner-splash/banner-splash.tsx b/docs/components/banner-splash/banner-splash.tsx new file mode 100644 index 00000000..6df6861f --- /dev/null +++ b/docs/components/banner-splash/banner-splash.tsx @@ -0,0 +1,18 @@ +import styles from './banner-splash.module.css'; + +export default class BannerSplash extends HTMLElement { + connectedCallback() { + this.render(); + } + + render() { + return ( +

+ SSR + for Web Components +

+ ); + } +} + +customElements.define('wcc-banner-splash', BannerSplash); diff --git a/docs/components/capability-box/capability-box.module.css b/docs/components/capability-box/capability-box.module.css new file mode 100644 index 00000000..5d9ecc26 --- /dev/null +++ b/docs/components/capability-box/capability-box.module.css @@ -0,0 +1,33 @@ +.container { + background-color: var(--color-primary); + border-radius: var(--radius-3); + padding: var(--size-4); + text-align: left; + min-height: 150px; + + & p { + color: var(--color-white); + margin: var(--size-2) 0; + font-family: var(--font-secondary); + } +} + +.heading { + font-family: var(--font-secondary); + background-color: #c1c7d3; + border-radius: var(--radius-2); + padding: var(--size-2); + font-size: var(--size-3); +} + +@media (min-width: 1024px) { + .container { + min-height: 200px; + } +} + +@media (min-width: 1200px) { + .container { + min-height: 160px; + } +} diff --git a/docs/components/capability-box/capability-box.tsx b/docs/components/capability-box/capability-box.tsx new file mode 100644 index 00000000..7d56cf51 --- /dev/null +++ b/docs/components/capability-box/capability-box.tsx @@ -0,0 +1,21 @@ +import styles from './capability-box.module.css'; + +export default class CapabilityBox extends HTMLElement { + connectedCallback() { + this.render(); + } + + render() { + const heading = this.getAttribute('heading'); + const { innerHTML } = this; + + return ( +
+ {heading} + {innerHTML} +
+ ); + } +} + +customElements.define('wcc-capability-box', CapabilityBox); diff --git a/docs/components/ctc-block/ctc-block.css b/docs/components/ctc-block/ctc-block.css new file mode 100644 index 00000000..6ad2cefc --- /dev/null +++ b/docs/components/ctc-block/ctc-block.css @@ -0,0 +1,89 @@ +.snippet-container { + & .copy-icon { + background-color: var(--color-white); + padding: 0; + border-radius: var(--radius-2); + position: relative; + top: 40px; + float: right; + right: 10px; + width: 40px; + vertical-align: middle; + cursor: copy; + } + + & .copy-icon svg { + width: 40px; + vertical-align: middle; + } + + & .heading { + display: inline-block; + background-color: var(--color-prism-bg); + color: var(--color-white); + padding: 10px; + font-size: var(--size-2); + border-radius: var(--radius-2) var(--radius-2) 0 0; + font-family: 'Geist-Mono', monospace; + font-style: italic; + text-decoration: underline; + } + + & pre { + margin: 0; + border-radius: 0 var(--radius-2) var(--radius-2) var(--radius-2); + } +} + +.shell-container { + background-color: var(--color-prism-bg); + padding: 0 var(--size-2); + border-radius: var(--radius-2); + + & pre { + border: 2px solid var(--color-white); + margin: var(--size-3) 0 var(--size-2) 0; + } + + & .copy-icon { + background-color: var(--color-white); + padding: 0; + border-radius: var(--radius-2); + position: relative; + top: 10px; + float: right; + right: 10px; + width: 40px; + vertical-align: middle; + cursor: copy; + } + + & .copy-icon svg { + width: 40px; + vertical-align: middle; + } +} + +.copy-icon svg path { + stroke: var(--color-prism-bg); +} + +.snippet-container:has(.heading) { + & .copy-icon { + top: 50px; + } +} + +@media (min-width: 768px) { + .snippet-container { + & .heading { + font-size: var(--size-4); + } + } + + .snippet-container:has(.heading) { + & .copy-icon { + top: 60px; + } + } +} diff --git a/docs/components/ctc-block/ctc-block.ts b/docs/components/ctc-block/ctc-block.ts new file mode 100644 index 00000000..b2bb344b --- /dev/null +++ b/docs/components/ctc-block/ctc-block.ts @@ -0,0 +1,86 @@ +import theme from '../../styles/theme.css' with { type: 'css' }; +import sheet from './ctc-block.css' with { type: 'css' }; +import copyIcon from '../../assets/copy-button.svg?type=raw'; + +const template = document.createElement('template'); + +export default class CopyToClipboardBlock extends HTMLElement { + selectCommandRunnerIdx; + snippetContents; + + constructor() { + super(); + this.selectCommandRunnerIdx = 0; + this.snippetContents = ''; + } + + connectedCallback() { + const variant = this.getAttribute('variant'); + + if (!this.shadowRoot && typeof window !== 'undefined') { + if (variant === 'snippet') { + const heading = this.getAttribute('heading'); + const headingHtml = heading ? `${heading}` : ''; + + this.snippetContents = this.textContent.trim(); + template.innerHTML = ` +
+
+ ${headingHtml} + + ${copyIcon} + + ${this.innerHTML} +
+
+ `; + } else if (variant === 'shell') { + this.snippetContents = this.textContent.trim(); + template.innerHTML = ` +
+ + ${copyIcon} + + ${this.innerHTML} +
+ `; + } else { + console.warn(`Unknown variant provided => ${variant}`); + } + + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + + switch (variant) { + case 'snippet': + this.shadowRoot + .querySelector('.copy-icon') + .addEventListener('click', this.copySnippetToClipboard.bind(this)); + break; + case 'shell': + this.shadowRoot + .querySelector('.copy-icon') + .addEventListener('click', this.copyShellScriptToClipboard.bind(this)); + break; + } + + this.shadowRoot.adoptedStyleSheets = [theme, sheet]; + } + } + + copySnippetToClipboard() { + const contents = this.snippetContents; + + navigator.clipboard.writeText(contents); + console.log('copying the following contents to your clipboard =>', contents); + } + + copyShellScriptToClipboard() { + const contents = this.getAttribute('paste-contents').trim(); + + navigator.clipboard.writeText(contents); + console.log('copying the following contents to your clipboard =>', contents); + } +} + +customElements.define('wcc-ctc-block', CopyToClipboardBlock); diff --git a/docs/components/ctc-button/ctc-button.css b/docs/components/ctc-button/ctc-button.css new file mode 100644 index 00000000..92cead00 --- /dev/null +++ b/docs/components/ctc-button/ctc-button.css @@ -0,0 +1,15 @@ +#icon { + padding: var(--size-2); + border: none; + cursor: copy; + border-radius: var(--radius-3); + background-color: var(--color-accent); + + & svg { + outline: var(--color-white); + } +} + +#icon svg path { + stroke: var(--color-white); +} diff --git a/docs/components/ctc-button/ctc-button.tsx b/docs/components/ctc-button/ctc-button.tsx new file mode 100644 index 00000000..29d108cb --- /dev/null +++ b/docs/components/ctc-button/ctc-button.tsx @@ -0,0 +1,39 @@ +import copy from '../../assets/copy-button.svg?type=raw'; +import sheet from './ctc-button.css' with { type: 'css' }; + +const template = document.createElement('template'); + +template.innerHTML = ` + +`; + +export default class CopyToClipboardButton extends HTMLElement { + connectedCallback() { + // bail of out of SSR entirely + if (!this.shadowRoot && typeof window !== 'undefined') { + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + + this.shadowRoot.adoptedStyleSheets = [sheet]; + + this.shadowRoot.getElementById('icon')?.addEventListener('click', () => { + const contents = this.getAttribute('content') ?? undefined; + + navigator.clipboard.writeText(contents); + console.log('copying the following contents to your clipboard =>', contents); + }); + } + } +} + +customElements.define('wcc-ctc-button', CopyToClipboardButton); + +declare global { + namespace JSX { + interface IntrinsicElements { + 'wcc-ctc-button': { + content: string; + }; + } + } +} diff --git a/docs/components/feature-box/feature-box.module.css b/docs/components/feature-box/feature-box.module.css new file mode 100644 index 00000000..5d4d6856 --- /dev/null +++ b/docs/components/feature-box/feature-box.module.css @@ -0,0 +1,58 @@ +.container { + background-color: var(--color-white); + border-radius: var(--radius-3); + color: var(--color-primary); + min-height: 300px; + padding: var(--size-2) var(--size-4); + margin: var(--size-1); + + & p { + font-size: var(--font-size-3); + margin-top: var(--size-2); + color: var(--color-black); + background-color: var(--color-background); + padding: var(--size-3); + border-radius: var(--radius-3); + } +} + +.heading { + font-size: var(--font-size-3); + background-color: var(--color-white); + display: block; + padding: var(--size-2) var(--size-6); + border-radius: var(--radius-3); + box-shadow: var(--shadow-3); + color: var(--color-primary); + margin: var(--size-4) auto; + width: fit-content; + font-weight: bolder; + text-align: center; + vertical-align: top; +} + +.icon { + & svg { + vertical-align: top; + fill: var(--color-accent); + stroke: var(--color-white); + } +} + +@media (min-width: 700px) { + .container { + min-height: fit-content; + margin: var(--size-2) 0; + padding: var(--size-4); + } +} + +@media (min-width: 1200px) { + .container { + min-height: 400px; + + & p { + min-height: 200px; + } + } +} diff --git a/docs/components/feature-box/feature-box.tsx b/docs/components/feature-box/feature-box.tsx new file mode 100644 index 00000000..7cb42e4b --- /dev/null +++ b/docs/components/feature-box/feature-box.tsx @@ -0,0 +1,34 @@ +import styles from './feature-box.module.css'; +import html from '../../assets/html.svg?type=raw'; +import json from '../../assets/json.svg?type=raw'; +import typescript from '../../assets/typescript-logo.svg?type=raw'; + +export default class FeatureBox extends HTMLElement { + static ICON_MAPPER: { [key: string]: string } = { + JSX: html, + TypeScript: typescript, + Pluggable: json, + }; + + connectedCallback() { + this.render(); + } + + render() { + const heading = this.getAttribute('heading'); + const { innerHTML } = this; + const icon = FeatureBox.ICON_MAPPER[heading]; + + return ( +
+ + {icon} + {heading} + + {innerHTML} +
+ ); + } +} + +customElements.define('wcc-feature-box', FeatureBox); diff --git a/docs/components/footer.js b/docs/components/footer.js deleted file mode 100644 index d6f64f37..00000000 --- a/docs/components/footer.js +++ /dev/null @@ -1,39 +0,0 @@ -class Footer extends HTMLElement { - connectedCallback() { - this.innerHTML = this.render(); - } - - render() { - return ` - - - - `; - } -} - -export { Footer }; - -customElements.define('wcc-footer', Footer); diff --git a/docs/components/footer/footer.module.css b/docs/components/footer/footer.module.css new file mode 100644 index 00000000..311a99ca --- /dev/null +++ b/docs/components/footer/footer.module.css @@ -0,0 +1,21 @@ +.container { + display: flex; + justify-content: center; + align-items: center; + padding: var(--size-4); + margin-top: var(--size-6); + gap: var(--size-3); + background-color: var(--color-black); +} + +.logoLink { + display: flex; + flex: 1; +} + +.socialTray { + display: flex; + flex: 2; + color: white; + justify-content: right; +} diff --git a/docs/components/footer/footer.tsx b/docs/components/footer/footer.tsx new file mode 100644 index 00000000..7d58ce9a --- /dev/null +++ b/docs/components/footer/footer.tsx @@ -0,0 +1,25 @@ +import styles from './footer.module.css'; +import '../social-tray/social-tray.tsx'; +import wccLogo from '../../assets/wcc-logo.svg?type=raw'; + +export default class Footer extends HTMLElement { + connectedCallback() { + this.render(); + } + + render() { + return ( + + ); + } +} + +customElements.define('wcc-footer', Footer); diff --git a/docs/components/header.js b/docs/components/header.js deleted file mode 100644 index 3f5be107..00000000 --- a/docs/components/header.js +++ /dev/null @@ -1,70 +0,0 @@ -import './navigation.js'; - -class Header extends HTMLElement { - connectedCallback() { - this.innerHTML = this.render(); - } - - render() { - return ` - - -
-
-
- - - - - -
- - -
-
- `; - } -} - -export { Header }; - -customElements.define('wcc-header', Header); diff --git a/docs/components/header/header.module.css b/docs/components/header/header.module.css new file mode 100644 index 00000000..abbb3816 --- /dev/null +++ b/docs/components/header/header.module.css @@ -0,0 +1,155 @@ +.container { + display: flex; + justify-content: center; + align-items: center; + padding: 0 0 var(--size-2) 0; + margin: var(--size-2) var(--size-3) var(--size-2); + border-bottom: 2px dotted var(--color-gray); + gap: var(--size-3); +} + +.logoLink { + display: flex; + flex: 1; +} + +.badgeContainer { + display: flex; + flex: 1; + align-items: center; + justify-content: right; +} + +.badge { + border: 1px solid var(--color-black); + padding: var(--size-1) var(--size-3); + border-radius: var(--radius-3); + background-color: var(--color-white); + + & img { + vertical-align: middle; + } +} + +.navBar { + display: contents; + flex: 1; + justify-content: center; +} + +.navBarMenu { + display: flex; + gap: var(--size-5); + list-style-type: none; +} + +.navBarMenuItem { + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + cursor: pointer; + font-size: var(--font-size-8); + padding: 0; +} + +.navBarMenuItem a { + text-decoration: none; + color: var(--color-primary); + padding: var(--size-1) var(--size-3); +} + +.mobileMenuContainer { + flex: 1; +} + +.mobileMenuList { + text-align: left; + margin: var(--size-4) 0 0; +} + +.mobileMenuListItem a { + color: var(--color-black); + text-decoration: none; +} + +.navBarMenuItem a.active, +.navBarMenuItem a:hover, +.navBarMenuItem a:focus { + text-decoration: underline; +} + +.navBarMenuItem a.active { + font-weight: extra-bold; + background-color: var(--color-white); + border-radius: var(--radius-3); + padding: var(--size-1) var(--size-3); +} + +.mobileMenuListItem { + list-style-type: none; + margin: 10px 0; + font-size: var(--font-size-5); +} + +.mobileMenuListItem a.active { + text-decoration: underline; +} + +.mobileMenuIcon { + display: none; + border: none; + background-color: transparent; +} + +.mobileMenuBackdrop { + height: 99vh; + width: 96vw; + margin: 0; + background-color: #f5f5f58e; + padding: var(--size-5) var(--size-6); + text-align: right; +} + +.mobileMenuCloseButton { + background: transparent; + font-size: var(--font-size-5); + cursor: pointer; + border: none; + padding: 0 12px; + color: var(--color-black); +} + +@media screen and (min-width: 480px) { + .container { + justify-content: space-between; + } +} + +@media (max-width: 600px) { + .navBar { + display: flex; + align-items: center; + gap: var(--size-2); + } + + .navBarMenu { + display: none; + } + + .mobileMenuIcon { + display: flex; + cursor: pointer; + } +} + +@media screen and (min-width: 768px) { + .navBar { + display: flex; + gap: var(--size-3); + } + + .navBar > nav { + align-self: center; + } +} diff --git a/docs/components/header/header.tsx b/docs/components/header/header.tsx new file mode 100644 index 00000000..2c3e34d8 --- /dev/null +++ b/docs/components/header/header.tsx @@ -0,0 +1,104 @@ +import { getContentByCollection } from '@greenwood/cli/src/data/client.js'; +import type { Page } from '@greenwood/cli'; +import wccLogo from '../../assets/wcc-logo.svg?type=raw'; +import mobileMenuIcon from '../../assets/tile.svg?type=raw'; +import styles from './header.module.css'; + +type NavItem = Page & { + data: { + order: number; + }; +}; + +function getNavItemsHtml(navItems: NavItem[], isMobile: boolean, currentRoute: string): string { + const itemClass = isMobile ? styles.mobileMenuListItem : styles.navBarMenuItem; + + return navItems + .map((item) => { + const { route, label } = item; + const isActiveClass = currentRoute === item.route ? 'class="active"' : ''; + + return ` +
  • + ${label} +
  • + `; + }) + .join(''); +} + +export default class Header extends HTMLElement { + currentRoute: string; + navItems: NavItem[]; + + constructor() { + super(); + this.currentRoute = ''; + this.navItems = []; + } + + async connectedCallback() { + this.currentRoute = this.getAttribute('current-route') ?? ''; + this.navItems = ((await getContentByCollection('nav')) as NavItem[]).sort((a, b) => + a.data.order > b.data.order ? 1 : -1, + ); + this.render(); + } + + render() { + const mainNavHtml = getNavItemsHtml(this.navItems, false, this.currentRoute); + const mobileNavHtml = getNavItemsHtml(this.navItems, true, this.currentRoute); + + return ( +
    + + {wccLogo} + + +
    + +
    + +
    + + WCC GitHub badge + +
    + + + +
    +
    + + + +
    +
    +
    + ); + } +} + +customElements.define('wcc-header', Header); diff --git a/docs/components/navigation.js b/docs/components/navigation.js deleted file mode 100644 index db98ad73..00000000 --- a/docs/components/navigation.js +++ /dev/null @@ -1,43 +0,0 @@ -class Navigation extends HTMLElement { - connectedCallback() { - this.innerHTML = this.render(); - } - - render() { - return ` - - - - `; - } -} - -export { Navigation }; - -customElements.define('wcc-navigation', Navigation); diff --git a/sandbox/components/card.js b/docs/components/sandbox/card.js similarity index 100% rename from sandbox/components/card.js rename to docs/components/sandbox/card.js diff --git a/sandbox/components/card.jsx b/docs/components/sandbox/card.jsx similarity index 100% rename from sandbox/components/card.jsx rename to docs/components/sandbox/card.jsx diff --git a/sandbox/components/counter-dsd.jsx b/docs/components/sandbox/counter-dsd.jsx similarity index 100% rename from sandbox/components/counter-dsd.jsx rename to docs/components/sandbox/counter-dsd.jsx diff --git a/sandbox/components/counter-dsd.tsx b/docs/components/sandbox/counter-dsd.tsx similarity index 100% rename from sandbox/components/counter-dsd.tsx rename to docs/components/sandbox/counter-dsd.tsx diff --git a/sandbox/components/counter.jsx b/docs/components/sandbox/counter.jsx similarity index 100% rename from sandbox/components/counter.jsx rename to docs/components/sandbox/counter.jsx diff --git a/sandbox/components/counter.tsx b/docs/components/sandbox/counter.tsx similarity index 100% rename from sandbox/components/counter.tsx rename to docs/components/sandbox/counter.tsx diff --git a/sandbox/components/greeting.ts b/docs/components/sandbox/greeting.ts similarity index 100% rename from sandbox/components/greeting.ts rename to docs/components/sandbox/greeting.ts diff --git a/sandbox/components/header.js b/docs/components/sandbox/header.js similarity index 100% rename from sandbox/components/header.js rename to docs/components/sandbox/header.js diff --git a/sandbox/components/header.jsx b/docs/components/sandbox/header.jsx similarity index 100% rename from sandbox/components/header.jsx rename to docs/components/sandbox/header.jsx diff --git a/sandbox/components/picture-frame.js b/docs/components/sandbox/picture-frame.js similarity index 100% rename from sandbox/components/picture-frame.js rename to docs/components/sandbox/picture-frame.js diff --git a/docs/components/sidenav/sidenav.module.css b/docs/components/sidenav/sidenav.module.css new file mode 100644 index 00000000..4f1d8988 --- /dev/null +++ b/docs/components/sidenav/sidenav.module.css @@ -0,0 +1,77 @@ +.fullMenu { + display: none; +} + +.compactMenu { + display: inline-block; +} + +.fullMenu, +.compactMenu { + & ul { + list-style-type: none; + } + + & li { + margin: var(--size-2); + } +} + +.tableOfContentHeader { + font-weight: bold; + text-decoration: underline; +} + +.compactMenuPopover { + top: 200px; + width: auto; + padding: var(--size-4); + background-color: var(--color-gray); + height: auto; + max-height: max-content; +} + +.compactMenuPopoverTrigger { + background-color: var(--color-white); + border: none; + padding: var(--size-2); + border-radius: var(--radius-2); + box-shadow: var(--shadow-1); + color: var(--color-black); + + @media (prefers-reduced-motion: no-preference) { + & #indicator { + display: inline-block; + transition: transform 1s ease; + } + } +} + +.compactMenuCloseButton { + background: transparent; + font-size: var(--font-size-5); + cursor: pointer; + border: none; + padding: 0 12px; + width: 100%; + text-align: right; + color: var(--color-black); +} + +.compactMenu:not(:has(#compact-menu:popover-open)) #indicator { + transform: rotate(0deg); +} + +.compactMenu:has(#compact-menu:popover-open) #indicator { + transform: rotate(180deg); +} + +@media (min-width: 1200px) { + .fullMenu { + display: block; + } + + .compactMenu { + display: none; + } +} diff --git a/docs/components/sidenav/sidenav.tsx b/docs/components/sidenav/sidenav.tsx new file mode 100644 index 00000000..6d820cae --- /dev/null +++ b/docs/components/sidenav/sidenav.tsx @@ -0,0 +1,74 @@ +import type { Page, Graph } from '@greenwood/cli'; +import { getContent } from '@greenwood/cli/src/data/client.js'; +import styles from './sidenav.module.css'; + +type TableOfContents = Array<{ + content: string; + slug: string; +}>; + +type DocsPage = Page & { + data?: { + tableOfContents?: TableOfContents; + }; +}; + +export default class SideNav extends HTMLElement { + route: string; + toc: TableOfContents; + heading: string; + + async connectedCallback() { + const route = this.getAttribute('route') ?? ''; + const heading = this.getAttribute('heading') ?? ''; + const page: DocsPage = (await getContent()).find((page) => page.route === route); + + this.heading = heading; + this.toc = page?.data?.tableOfContents ?? []; + + this.render(); + } + + render() { + const { heading } = this; + const tocList = this.toc + .map((item) => { + return `
  • ${item.content}
  • `; + }) + .join(''); + + return ( +
    +
    +

    Table of Contents

    + +
    + +
    + +
    + +

    Table of Contents

    +
      {tocList}
    +
    +
    +
    + ); + } +} + +customElements.define('wcc-sidenav', SideNav); diff --git a/docs/components/social-tray/social-tray.module.css b/docs/components/social-tray/social-tray.module.css new file mode 100644 index 00000000..74a7a355 --- /dev/null +++ b/docs/components/social-tray/social-tray.module.css @@ -0,0 +1,23 @@ +.socialTray { + display: flex; + gap: var(--size-3); + list-style-type: none; + background-color: var(--color-gray); + width: fit-content; + border: var(--border-size-1) solid #4d4d4d45; + border-radius: var(--radius-6); + align-items: center; + justify-content: center; + cursor: pointer; + padding: var(--size-2); +} + +.socialIcon { + display: flex; + align-items: center; + line-height: 100%; +} + +.socialIcon svg { + fill: var(--color-secondary); +} diff --git a/docs/components/social-tray/social-tray.tsx b/docs/components/social-tray/social-tray.tsx new file mode 100644 index 00000000..c5bcae0a --- /dev/null +++ b/docs/components/social-tray/social-tray.tsx @@ -0,0 +1,57 @@ +import styles from './social-tray.module.css'; +import discordIcon from '../../assets/discord.svg?type=raw'; +import githubIcon from '../../assets/github.svg?type=raw'; +import twitterIcon from '../../assets/twitter-logo.svg?type=raw'; +import blueskyIcon from '../../assets/bluesky.svg?type=raw'; + +export default class SocialTray extends HTMLElement { + connectedCallback() { + this.render(); + } + + render() { + return ( + + ); + } +} + +customElements.define('wcc-social-tray', SocialTray); + +declare global { + interface HTMLElementTagNameMap { + 'wcc-social-tray': SocialTray; + } +} diff --git a/docs/favicon.svg b/docs/favicon.svg new file mode 100644 index 00000000..b45d9523 --- /dev/null +++ b/docs/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/globals.d.ts b/docs/globals.d.ts new file mode 100644 index 00000000..282e41ef --- /dev/null +++ b/docs/globals.d.ts @@ -0,0 +1,15 @@ +declare module '*.module.css' { + const styles: Readonly>; + export default styles; +} + +declare module '*?type=raw' { + const content: string; + export default content; +} + +declare module '*.css' { + const sheet: CSSStyleSheet; + + export default sheet; +} diff --git a/docs/layout.js b/docs/layout.js deleted file mode 100644 index 1b53ca0c..00000000 --- a/docs/layout.js +++ /dev/null @@ -1,44 +0,0 @@ -import './components/footer.js'; -import './components/header.js'; - -class Layout extends HTMLElement { - connectedCallback() { - this.innerHTML = this.render(); - } - - render() { - return ` - - - - -
    - -
    - - - `; - } -} - -export default Layout; diff --git a/docs/layouts/app.html b/docs/layouts/app.html new file mode 100644 index 00000000..125c7745 --- /dev/null +++ b/docs/layouts/app.html @@ -0,0 +1,42 @@ + + + + + WCC + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + diff --git a/docs/layouts/docs.html b/docs/layouts/docs.html new file mode 100644 index 00000000..0a3b74c8 --- /dev/null +++ b/docs/layouts/docs.html @@ -0,0 +1,15 @@ + + + WCC - ${globalThis.page.title} + + + + + + + +
    + +
    + + diff --git a/docs/layouts/examples.html b/docs/layouts/examples.html new file mode 100644 index 00000000..e8c71e05 --- /dev/null +++ b/docs/layouts/examples.html @@ -0,0 +1,15 @@ + + + WCC - ${globalThis.page.title} + + + + + + + +
    + +
    + + diff --git a/docs/pages/docs.md b/docs/pages/docs.md index 03b1403e..13930b10 100644 --- a/docs/pages/docs.md +++ b/docs/pages/docs.md @@ -1,6 +1,13 @@ -# Documentation +--- +layout: docs +collection: nav +order: 2 +tocHeading: 2 +--- -## Table of contents +# Docs + +Below are the various APIs and capabilities of WCC. ## API @@ -8,116 +15,132 @@ This function takes a `URL` "entry point" to a JavaScript file that defines a custom element, and returns the static HTML output of its rendered contents. - + -```js -const { html } = await renderToString(new URL('./src/index.js', import.meta.url)); -``` + -```js -// index.js -import './components/footer.js'; -import './components/header.js'; + ```js + const { html } = await renderToString(new URL('./src/index.js', import.meta.url)); + ``` -const template = document.createElement('template'); + -template.innerHTML = ` - + - + ```js + import './components/footer.js'; + import './components/header.js'; -
    -

    My Blog Post

    -
    + const template = document.createElement('template'); - -`; + template.innerHTML = ` + + + + +
    +

    My Blog Post

    +
    -class Home extends HTMLElement { - connectedCallback() { - if (!this.shadowRoot) { - this.attachShadow({ mode: 'open' }); - this.shadowRoot.appendChild(template.content.cloneNode(true)); + + `; + + export default class Home extends HTMLElement { + connectedCallback() { + if (!this.shadowRoot) { + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } } } -} + ``` -export default Home; -``` +
    -You can also manually set `innerHTML` of Shadow Root if you don't want to use a template element + -```js -// index.js -import './components/footer.js'; -import './components/header.js'; - -class Home extends HTMLElement { - connectedCallback() { - if (!this.shadowRoot) { - this.attachShadow({ mode: 'open' }); - this.shadowRoot.innerHTML = ` - +You can also manually set the `innerHTML` of a Shadow Root if you don't want to use a template element - + -
    -

    My Website

    -
    + - - `; + ```js + import './components/footer.js'; + import './components/header.js'; + + export default class Home extends HTMLElement { + connectedCallback() { + if (!this.shadowRoot) { + this.attachShadow({ mode: 'open' }); + this.shadowRoot.innerHTML = ` + + + + +
    +

    My Website

    +
    + + + `; + } } } -} + ``` -export default Home; -``` +
    + + -> _**Note**: **WCC** will wrap or not wrap your \_entry point's HTML_ in a custom element tag if you do or do not, respectively, include a `customElements.define` in your entry point. **WCC** will use the tag name you define as the custom element tag name in the generated HTML.\_ +> _**Note**: **WCC** will wrap, or not wrap, your entry point's HTML in a custom element tag if you do or do not, respectively, include a `customElements.define` in your entry point. **WCC** will use the tag name you define as the custom element tag name in the generated HTML._ > > You can opt-out of this by passing `false` as the second parameter to `renderToString`. > -> -> > ```js > const { html } = await renderToString(new URL('...'), false); > ``` ### renderFromHTML -This function takes a string of HTML and an array of any top-level custom elements used in the HTML, and returns the static HTML output of the rendered content. +This function takes a string of HTML and an array of any top-level custom elements used in the HTML, and returns the static HTML output of the rendered content. WCC will follow and recursively render any imports from those top-level custom elements, however. - + -```js -const { html } = await renderFromHTML( - ` - - - WCC - - - -

    Home Page

    - - - -`, - [ - new URL('./src/components/footer.js', import.meta.url), - new URL('./src/components/header.js', import.meta.url), - ], -); -``` + + + ```js + const { html } = await renderFromHTML( + ` + + + WCC + + + +

    Home Page

    + + + + `, + [ + new URL('./src/components/footer.js', import.meta.url), + new URL('./src/components/header.js', import.meta.url), + ], + ); + ``` + +
    + + For example, even if `Header` or `Footer` use `import` to pull in additional custom elements, only the `Header` and `Footer custom elements used in the "entry" HTML are needed in the array. @@ -127,15 +150,23 @@ For example, even if `Header` or `Footer` use `import` to pull in additional cus So for the given HTML: -```html - + -

    Hello World

    + - -``` + ```html + -And the following conditions: +

    Hello World

    + + + ``` + +
    + + + +and the following conditions: 1. _index.js_ does not define a tag of its own, e.g. using `customElements.define` (e.g. it is just a ["layout" component](/examples/#static-sites-ssg)) 1. `` imports `` @@ -159,11 +190,19 @@ console.log({ metadata }); ## Progressive Hydration -To achieve an islands architecture implementation, if you add `hydration="true"` attribute to a custom element, e.g. +To achieve an islands architecture-like implementation, if you add the `hydration="true"` attribute to a custom element, e.g. -```html - -``` + + + + + ```html + + ``` + + + + This will be reflected in the returned `metadata` object from `renderToString`. @@ -192,179 +231,213 @@ WCC provide a couple mechanisms for data loading. Often for frameworks that might have their own needs for data loading and orchestration, a top level "constructor prop" can be provided to `renderToString` as the final param. The prop will then be passed to the custom element's `constructor` when loading the module URL. - + -```js -const request = new Request({ - /* ... */ -}); -const { html } = await renderToString(new URL(moduleUrl), false, request); -``` + + + ```js + const request = new Request({ + /* ... */ + }); + const { html } = await renderToString(new URL(moduleUrl), false, request); + ``` + + + + This pattern plays really nice with file-based routing and SSR! -```js -export default class PostPage extends HTMLElement { - constructor(request) { - super(); + - const params = new URLSearchParams(request.url.slice(request.url.indexOf('?'))); - this.postId = params.get('id'); - } + + + ```js + export default class PostPage extends HTMLElement { + constructor(request) { + super(); + + const params = new URLSearchParams(request.url.slice(request.url.indexOf('?'))); + this.postId = params.get('id'); + } + + async connectedCallback() { + const { postId } = this; + const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`).then((resp) => + resp.json(), + ); + const { title, body } = post; - async connectedCallback() { - const { postId } = this; - const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`).then((resp) => - resp.json(), - ); - const { title, body } = post; - - this.innerHTML = ` -

    ${title}

    -

    ${body}

    - `; + this.innerHTML = ` +

    ${title}

    +

    ${body}

    + `; + } } -} -``` + ``` -### Data Loader +
    + + + +### Loader To support component-level data loading and hydration scenarios, a file with a custom element definition can also export a `getData` function to inject into the custom elements constructor at build time. This can be serialized right into the component's Shadow DOM! For example, you could preload a counter component with an initial counter state, which would also come through the `constructor`. - + -```js -class Counter extends HTMLElement { - constructor(props = {}) { - super(); + - this.count = props.count; - this.render(); - } + ```js + class Counter extends HTMLElement { + constructor(props = {}) { + super(); - // setup your shadow root ... + this.count = props.count; + this.render(); + } - render() { - this.shadowRoot.innerHTML = ` - + // setup your shadow root ... -
    - - Current Count: ${this.count} - -
    - `; + render() { + this.shadowRoot.innerHTML = ` + + +
    + + Current Count: ${this.count} + +
    + `; + } } -} -export async function getData() { - return { - count: Math.floor(Math.random() * (100 - 0 + 1) + 0), - }; -} -``` + export async function getData() { + return { + count: Math.floor(Math.random() * (100 - 0 + 1) + 0), + }; + } + ``` + +
    + + ## Conventions - Make sure to define your custom elements with `customElements.define` -- Make sure to include a `export default` for your custom element base class +- Make sure to include an `export default` for your custom element base class - Avoid [touching the DOM in `constructor` methods](https://twitter.com/techytacos/status/1514029967981494280) ## TypeScript -TypeScript is supported through "type stripping", which is effectively just removing all the TypeScript and leaving only valid JavaScript, before handing off to WCC to do its compiling. +TypeScript is supported through "type stripping", which is effectively just removing all the types and leaving only valid JavaScript, before handing off to WCC to do its compiling. -```ts -interface User { - name: string; -} + -export default class Greeting extends HTMLElement { - connectedCallback() { - const user: User = { - name: this.getAttribute('name') || 'World', - }; + - this.innerHTML = ` -

    Hello ${user.name}! 👋

    - `; + ```ts + interface User { + name: string; } -} -customElements.define('wcc-greeting', Greeting); -``` + export default class Greeting extends HTMLElement { + connectedCallback() { + const user: User = { + name: this.getAttribute('name') || 'World', + }; + + this.innerHTML = ` +

    Hello ${user.name}! 👋

    + `; + } + } + + customElements.define('wcc-greeting', Greeting); + ``` + +
    + + ### Prerequisites There are of couple things you will need to do to use WCC with TypeScript parsing: -1. NodeJS version needs to be >= `22.6.0` +1. NodeJS version needs to be >= `22.18.0` 1. You will need to use the _.ts_ extension 1. You'll want to enable the [`erasableSyntaxOnly`](https://devblogs.microsoft.com/typescript/announcing-typescript-5-8/#the---erasablesyntaxonly-option) flag in your _tsconfig.json_ -> If you're feeling adventurous, you can use NodeJS **>=23.x** and omit the `--experimental-strip-types` flag. Keep an eye on this PR for when unflagged type-stripping support may come to Node LTS 22.x. 👀 - ## JSX > ⚠️ _Very Experimental!_ -Even more experimental than WCC is the option to author a rendering function for native `HTMLElements`, that can compile down to a zero run time, web ready custom element! It handles resolving event handling and `this` references and can manage some basic re-rendering lifecycles. +WCC provides the option to author a `render` function for native `HTMLElements` that can compile down to a zero run time, web ready custom element! It handles resolving event handling and `this` references and can manage some basic re-rendering lifecycles. ### Example Below is an example of what is possible right now demonstrated through a [Counter component](https://github.com/thescientist13/greenwood-counter-jsx). -```tsx -export default class Counter extends HTMLElement { - count: number; + - constructor() { - super(); - this.count = 0; - } + - connectedCallback() { - this.render(); // this is required - } + ```tsx + export default class Counter extends HTMLElement { + count: number; - increment() { - this.count += 1; - this.render(); - } + constructor() { + super(); + this.count = 0; + } - decrement() { - this.count -= 1; - this.render(); - } + connectedCallback() { + this.render(); // this is required + } - render() { - const { count } = this; - - return ( -
    - - - You have clicked {count} times - - - -
    - ); + increment() { + this.count += 1; + this.render(); + } + + decrement() { + this.count -= 1; + this.render(); + } + + render() { + const { count } = this; + + return ( +
    + + + You have clicked {count} times + + + +
    + ); + } } -} -customElements.define('wcc-counter', Counter); -``` + customElements.define('wcc-counter', Counter); + ``` + +
    + + A couple things to observe in the above example: - The `this` reference is correctly bound to the `` element's state. This works for both `this.count` and the event handler, `this.increment`. - No need for `className`! `class` just works ™️ -- The `style` attribute is just a string, no need to pass an object (e.g. `style={{ color: "red" }})`) +- The `style` attribute is just a string, no need to pass an object (e.g. `style={{ color: "red" }}`) - `this.count` will know it is a member of the ``'s state, and so will re-run `this.render` automatically in the compiled output. - Event handlers need to manage their own render function updates. @@ -377,10 +450,19 @@ There are of couple things you will need to do to use WCC with JSX: 1. NodeJS version needs to be >= `20.10.0` 1. You will need to use the _.jsx_ extension 1. Requires the `--import` flag when invoking NodeJS + + + + + ```shell $ NODE_OPTIONS="--import wc-compiler/register" node your-script.js ``` + + + + > _See our [example's page](/examples#jsx) for some usages of WCC + JSX._ 👀 ### TSX @@ -389,36 +471,49 @@ TSX (.tsx) file are also supported and your HTML will also be **type-safe**. You -```json5 -{ - "compilerOptions": { - // required options - "jsx": "preserve", - "jsxImportSource": "wc-compiler", - "lib": ["DOM"], - - // additional recommended options - "allowImportingTsExtensions": true, - "erasableSyntaxOnly": true + + + + ```json5 + { + "compilerOptions": { + // required options + "jsx": "preserve", + "jsxImportSource": "wc-compiler", + "lib": ["DOM"], + + // additional recommended options + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true + } } -} -``` + ``` + + If you create your own custom elements and use them in your TSX components, you'll need to define your own `interface` for them: -```ts -declare global { - namespace JSX { - interface IntrinsicElements { - 'my-counter': { - count?: number; - }; + + + + + ```ts + declare global { + namespace JSX { + interface IntrinsicElements { + 'my-counter': { + count?: number; + }; + } } } -} -``` + ``` + + + + ### Declarative Shadow DOM @@ -426,25 +521,33 @@ To opt-in to Declarative Shadow DOM with JSX, you will need to signal to the WCC Using, the Counter example from above, we would amend it like so: -```js -export default class Counter extends HTMLElement { - constructor() { - super(); - this.count = 0; - } + - connectedCallback() { - if (!this.shadowRoot) { - this.attachShadow({ mode: 'open' }); // this is required for DSD support - this.render(); + + + ```js + export default class Counter extends HTMLElement { + constructor() { + super(); + this.count = 0; + } + + connectedCallback() { + if (!this.shadowRoot) { + this.attachShadow({ mode: 'open' }); // this is required for DSD support + this.render(); + } } + + // ... } - // ... -} + customElements.define('wcc-counter', Counter); + ``` -customElements.define('wcc-counter', Counter); -``` + + + ### (Inferred) Attribute Observability @@ -453,32 +556,36 @@ An optional feature supported by JSX based compilation is `inferredObservability 1. an entry in the `observedAttributes` array 1. automatically handle `attributeChangedCallback` updates -So taking the above counter example, and opting in to this feature, we just need to enable the `inferredObservability` option in the component by exporting it as a `const`: +So taking the above counter example, and opting-in to this feature, we just need to enable the `inferredObservability` option in the component by exporting it as a `const`: -```jsx -export const inferredObservability = true; - -export default class Counter extends HTMLElement { - // ... - - render() { - const { count } = this; - - // note that {count} has to be wrapped in its own HTML tag - return ( -
    - - - You have clicked {count} times - - -
    - ); + + + ```jsx + export const inferredObservability = true; + + export default class Counter extends HTMLElement { + // ... + + render() { + const { count } = this; + + // note that {count} has to be wrapped in its own HTML tag + return ( +
    + + + You have clicked {count} times + + +
    + ); + } } -} -``` + ``` + +
    @@ -490,5 +597,5 @@ And so now when the attribute is set on this component, the component will re-re Some notes / limitations: -- This automatically reflects properties used in the `render` function to attributes, so [YMMV](https://dictionary.cambridge.org/us/dictionary/english/ymmv). - Please be aware of the above linked discussion and issue filter which is tracking any known bugs / feature requests / open items related to all things WCC + JSX. +- This automatically reflects properties used in the `render` function to attributes, so [YMMV](https://dictionary.cambridge.org/us/dictionary/english/ymmv). diff --git a/docs/pages/examples.md b/docs/pages/examples.md index 084234d0..06c4218e 100644 --- a/docs/pages/examples.md +++ b/docs/pages/examples.md @@ -1,121 +1,149 @@ +--- +layout: examples +collection: nav +order: 3 +tocHeading: 2 +--- + # Examples Below are some example of how **WCC** is being used right now. -## Table of contents - ## Server Rendering (SSR) For the project [**Greenwood**](https://www.greenwoodjs.dev/), **WCC** is used to provide a _Next.js_ like experience by allowing users to author [server-side routes using native custom elements](https://www.greenwoodjs.dev/docs/pages/server-rendering/#web-server-components)! ✨ -```js -import '../components/card/card.js'; - -export default class ArtistsPage extends HTMLElement { - async connectedCallback() { - if (!this.shadowRoot) { - const artists = await fetch('https://www.domain.com/api/artists').then((resp) => resp.json()); - const html = artists - .map((artist) => { - return ` - -

    ${artist.name}

    - ${artist.name} -
    - `; - }) - .join(''); - - this.attachShadow({ mode: 'open' }); - this.shadowRoot.innerHTML = html; + + + + + ```js + import '../components/card/card.js'; + + export default class ArtistsPage extends HTMLElement { + async connectedCallback() { + if (!this.shadowRoot) { + const artists = await fetch('https://www.domain.com/api/artists').then((resp) => resp.json()); + const html = artists + .map((artist) => { + return ` + +

    ${artist.name}

    + ${artist.name} +
    + `; + }) + .join(''); + + this.attachShadow({ mode: 'open' }); + this.shadowRoot.innerHTML = html; + } } } -} -``` + ``` + + + +
    ## Serverless and Edge Functions In the talk [_"Web Components at the Edge"_](https://sched.co/11loQ) for OpenJS World 2022, **WCC** was leveraged for all the AWS Lambda serverless function and Netlify Edge function demos. It also shows some clever ways to use **WCC** in more constrained runtime environments, like an edge runtime where something like `fs` might not be available. See all the [code, slides and demos in GitHub](https://github.com/thescientist13/web-components-at-the-edge). 🚀 -```js -import '../../node_modules/wc-compiler/src/dom-shim.js'; - -import Greeting from './components/greeting.js'; - -export default async function (request, context) { - const countryCode = context.geo.country.code || 'UNKNOWN'; - const countryName = context.geo.country.name || 'UNKNOWN'; - const greeting = new Greeting(countryCode, countryName); - - greeting.connectedCallback(); - - const response = new Response(` - - - - - ${greeting.getHTML({ serializableShadowRoots: true })} -
    -
    -              ${JSON.stringify(context.geo)}
    -            
    -
    -
    - - - `); - - response.headers.set('content-type', 'text/html'); - - return response; -} -``` + + + + + ```js + import '../../node_modules/wc-compiler/src/dom-shim.js'; + + import Greeting from './components/greeting.js'; + + export default async function (request, context) { + const countryCode = context.geo.country.code || 'UNKNOWN'; + const countryName = context.geo.country.name || 'UNKNOWN'; + const greeting = new Greeting(countryCode, countryName); + + greeting.connectedCallback(); + + const response = new Response(` + + + + + ${greeting.getHTML({ serializableShadowRoots: true })} +
    +
    +                ${JSON.stringify(context.geo)}
    +              
    +
    +
    + + + `); + + response.headers.set('content-type', 'text/html'); + + return response; + } + ``` + + + +
    ## Static Sites (SSG) -Using `innerHTML`, custom elements can be authored to not use Shadow DOM, which can be useful for a `Layout` or `App` component where that top level content specifically should _not_ be rendered in a shadow root, e.g. `