diff --git a/.gitignore b/.gitignore index b93267f1..851e313f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist .vscode _test coverage +performance.js \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index f4d42173..846fd194 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/examples/simple-blog/@global/layout.css b/packages/examples/simple-blog/@global/layout.css index c80e279a..0960cb24 100644 --- a/packages/examples/simple-blog/@global/layout.css +++ b/packages/examples/simple-blog/@global/layout.css @@ -43,17 +43,18 @@ body { /* global footer */ > footer { border-top: 1px solid var(--gray-200); - justify-content: space-between; - margin-top: 6rem; - padding: 1rem 0; - display: flex; + margin-top: 8rem; - /* social icons */ - > :last-child { + nav { + padding: 1rem 0; display: flex; gap: .5em; - a:hover { - transform: scale(1.1) + + a { opacity: .8 } + a:hover { opacity: 1 } + + a:nth-child(2) { + margin-left: auto; } } } diff --git a/packages/examples/simple-blog/@global/layout.html b/packages/examples/simple-blog/@global/layout.html new file mode 100644 index 00000000..9a39bb02 --- /dev/null +++ b/packages/examples/simple-blog/@global/layout.html @@ -0,0 +1,7 @@ +
+ +
+ + \ No newline at end of file diff --git a/packages/examples/simple-blog/blog/color-strategies.md b/packages/examples/simple-blog/blog/color-strategies.md index d492356a..32c3cb48 100644 --- a/packages/examples/simple-blog/blog/color-strategies.md +++ b/packages/examples/simple-blog/blog/color-strategies.md @@ -8,12 +8,12 @@ date: 2024-05-23 Working with color is one of the most fun parts of web design. But if you don't use them right in your CSS code, colors can also tank a site's usability and accessibility. In this post, we'll explore simple, real-world tips for effectively using color in your stylesheets. - + ``` css .blue /* Tab styling */ [role=tablist] { background: rgba(0, 0, 0, .7); - background-size: 3.5em; + background-size: 3.8em; > padding: .7em 1.3em 0; > overflow: hidden; > display: flex; @@ -40,9 +40,8 @@ Choosing the right colors for a website can be tricky. Get one wrong and your en - **Consider tone and contrast** - Include light, medium and dark tones for visual interest. Test contrast for accessibility. - [image img/colors-1.png] - foo: bar + Considering the different formatting options, you can optimize CSS color usage for any need. Use hex for one-off values, RGBA where you need opacity control, HSL for color adjustments, variables for theming, and preprocessor operations for transformations. With the right format, implementing colors in CSS becomes much more flexible and manageable. diff --git a/packages/examples/simple-blog/blog/hero.html b/packages/examples/simple-blog/blog/hero.html index c96d45f5..58ddcd2d 100644 --- a/packages/examples/simple-blog/blog/hero.html +++ b/packages/examples/simple-blog/blog/hero.html @@ -1,4 +1,3 @@ -

{ title }

diff --git a/packages/examples/simple-blog/blog/scaleable-design-system.md b/packages/examples/simple-blog/blog/scaleable-design-system.md index e87ade9d..9ff41ed9 100644 --- a/packages/examples/simple-blog/blog/scaleable-design-system.md +++ b/packages/examples/simple-blog/blog/scaleable-design-system.md @@ -18,21 +18,21 @@ Design systems are key to managing code at scale, but creating one that’s flex ## Keep things minimal It's important to keep things lightweight: Write CSS that’s DRY, separating structure from skin using methodologies like BEM. Don't over-engineer - keep logic simple. -[code numbered="true"] -

- +``` html numbered +
+ - -
{{ caption }}
+-
{{ caption }}
- +

{{ caption }}

- - -
++

{{ caption }}

+ +
+``` Use gradual rollouts: Introduce changes incrementally over time. Don't rebuild everything at once. Maintain backwards compatibility. diff --git a/packages/examples/simple-blog/index.md b/packages/examples/simple-blog/index.md index 916d305a..ad097556 100644 --- a/packages/examples/simple-blog/index.md +++ b/packages/examples/simple-blog/index.md @@ -1,4 +1,3 @@ - --- include: [ gallery, motion ] content_collection: blog @@ -8,4 +7,4 @@ content_collection: blog I’m Emma Bennett, an user experience developer from Berlin. I build websites that are exceptionally well designed — inside, and outside. -[gallery] \ No newline at end of file +[page-list] \ No newline at end of file diff --git a/packages/examples/simple-blog/site.yaml b/packages/examples/simple-blog/site.yaml index b35bc66b..0bf2fb5c 100644 --- a/packages/examples/simple-blog/site.yaml +++ b/packages/examples/simple-blog/site.yaml @@ -5,6 +5,7 @@ libs: ["@library"] view_transitions: true native_css_nesting: true inline_css: true +sections: true title_template: "Emma Bennet / %s" og_image: /img/og_emma.png @@ -17,23 +18,22 @@ port: 8083 origin: https://simple-blog.nuejs.org og: /img/og.jpg -header: - navigation: + +navigation: + header: - Emma Bennet: / - Contact: /contact/ -footer: - copyright: + footer: - © Emma Bennet: / - social: - image: /img/twitter.svg url: //x.com/tipiirai alt: Twitter (X) profile size: 22 x 22 - image: /img/github.svg - url: //github.com/nuejs/ + url: //github.com/nuejs/nue/ alt: Github Projects size: 22 x 22 diff --git a/packages/glow/README.md b/packages/glow/README.md index 32814634..321a1394 100644 --- a/packages/glow/README.md +++ b/packages/glow/README.md @@ -67,4 +67,3 @@ const content = '...' const html = marked.parse(content) ``` - diff --git a/packages/glow/package.json b/packages/glow/package.json index 9aa715b9..59b584d6 100644 --- a/packages/glow/package.json +++ b/packages/glow/package.json @@ -1,8 +1,8 @@ { "name": "nue-glow", - "version": "0.2.1", - "description": "Tiny and powerful Markdown syntax highlighter", - "homepage": "https://nuejs.org/blog/introducing-glow", + "version": "0.2.2", + "description": "Markdown syntax highlighter for CSS developers", + "homepage": "https://nuejs.org/blog/introducing-glow/", "license": "MIT", "type": "module", "main": "index.js", @@ -19,7 +19,7 @@ "test": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js --runInBand" }, "devDependencies": { - "lightningcss": "^1.26.0" + "lightningcss": "^1.27.0" }, "jest": { "setupFilesAfterEnv": [ diff --git a/packages/glow/src/glow.js b/packages/glow/src/glow.js index a845b33f..91d74c91 100644 --- a/packages/glow/src/glow.js +++ b/packages/glow/src/glow.js @@ -1,5 +1,5 @@ -const MIXED_HTML = ['html', 'jsx', 'php', 'astro', 'nue', 'vue', 'svelte', 'hb'] +const MIXED_HTML = ['html', 'jsx', 'php', 'astro', 'dhtml', 'vue', 'svelte', 'hb'] const LINE_COMMENT = { clojure: ';;', lua: '--', python: '#' } const PREFIXES = { '+': 'ins', '-': 'del', '>': 'dfn' } const MARK = /(••?)([^•]+)\1/g // ALT + q @@ -186,7 +186,7 @@ function renderString(str) { // exported for testing purposes -export function renderRow(row, lang) { +export function renderRow(row, lang, mark=true) { if (!row) return '' const els = parseRow(row, lang) @@ -211,8 +211,9 @@ export function renderRow(row, lang) { } ret.push(row.substring(index)) + const res = ret.join('') - return ret.join('').replace(MARK, (_, a, b, c) => { + return !mark ? res : res.replace(MARK, (_, a, b, c) => { return elem(a[1] ? 'u' : 'mark', b) }) } @@ -221,19 +222,19 @@ export function renderRow(row, lang) { // comment start & end const COMMENT = [/(\/\*|^ *{# ||'''|=end)$/] -export function parseSyntax(str, lang) { +export function parseSyntax(lines, lang, prefix = true) { const [comm_start, comm_end] = COMMENT - const lines = [] + const html = [] // multi-line comment let comment function endComment() { - lines.push({ comment }) + html.push({ comment }) comment = null } - str.split(/\r\n|\r|\n/).forEach((line, i) => { + lines.forEach((line, i) => { if (!comment) { if (comm_start.test(line)) { comment = [line] @@ -242,13 +243,13 @@ export function parseSyntax(str, lang) { // highlighted line const c = line[0] - const wrap = isMD(lang) ? (c == '|' && 'dfn') : PREFIXES[c] + const wrap = prefix && (isMD(lang) ? (c == '|' && 'dfn') : PREFIXES[c]) if (wrap) line = (line[1] == ' ' ? ' ' : '') + line.slice(1) // escape character - if (c == '\\') line = line.slice(1) + if (!prefix && c == '\\') line = line.slice(1) - lines.push({ line, wrap }) + html.push({ line, wrap }) } } else { @@ -258,24 +259,27 @@ export function parseSyntax(str, lang) { }) - return lines + return html } // code, { language: 'js', numbered: true } -export function glow(str, opts = {}) { +export function glow(str, opts = { prefix: true, mark: true }) { if (typeof opts == 'string') opts = { language: opts } + const lines = Array.isArray(str) ? str : str.trim().split(/\r\n|\r|\n/) + + if (!lines[0]) return '' // language let lang = opts.language - if (!lang && str.trim()[0] == '<') lang = 'html' - const lines = [] + if (!lang && lines[0][0] == '<') lang = 'html' + const html = [] function push(line) { - lines.push(opts.numbered ? elem('span', line) : line) + html.push(opts.numbered ? elem('span', line) : line) } - parseSyntax(str.trim(), lang).forEach(function(block) { + parseSyntax(lines, lang, opts.prefix).forEach(function(block) { let { line, comment, wrap } = block // EOL comment @@ -283,12 +287,12 @@ export function glow(str, opts = {}) { return comment.forEach(el => push(elem('sup', encode(el)))) } else { - line = renderRow(line, lang) + line = renderRow(line, lang, opts.mark) } if (wrap) line = elem(wrap, line) push(line) }) - return `${lines.join(NL)}` + return `${html.join(NL)}` } diff --git a/packages/glow/test/glow.test.js b/packages/glow/test/glow.test.js index def80d5a..5aea4ca1 100644 --- a/packages/glow/test/glow.test.js +++ b/packages/glow/test/glow.test.js @@ -27,28 +27,34 @@ test('Emphasis', () => { /* multiline comments */ -const HTML_COMMENT = ` -
- -
` - -const JS_COMMENT = ` -/* First */ -function() { - /* - Second - */ -} -` - -test('extract comments', () => { - let blocks = parseSyntax(HTML_COMMENT.trim()) - expect(blocks[1].comment[0]).toBe(' ', '']) + expect(blocks[1].comment[0]).toBe(' +
+ + + +
+ + + + + + + + + \ No newline at end of file diff --git a/packages/nuejs.org/@global/navigation.css b/packages/nuejs.org/@global/navigation.css index 7c943aab..b291a751 100644 --- a/packages/nuejs.org/@global/navigation.css +++ b/packages/nuejs.org/@global/navigation.css @@ -12,13 +12,21 @@ nav a { body > header { margin-bottom: 5em; + .nav-home { + background: url(/img/logomark-big.png) no-repeat; + background-size: contain; + text-indent: -99em; + width: 4.5rem; + height: 1.7rem; + } + &, > * { align-items: center; display: flex; gap: 2em; } - [aria-label=Toolbar] { + nav + nav { margin-left: auto; gap: 1em; } @@ -42,8 +50,14 @@ body > header { } .social { + background: url(/icon/twitter.svg) left no-repeat; + background-size: 1.2em; + padding-left: 1.4rem; + color: var(--gray-800); + &:hover { - transform: scale(1.1); + transform: scale(1.005); + color: black; } } @@ -54,48 +68,47 @@ body > header { } /* burger menu opener */ - [popovertarget] { - background-size: 1.8em; + button { margin-left: auto; display: none; } - @media (max-width: 800px) { + @media (width < 800px) { > * { display: none } - > :first-child, > [popovertarget] { display: inline-block } + button { display: inline-block } + nav:first-child { + display: flex; + + @media (width < 650) { + gap: .75rem; + a:not(.nav-home, .pill) { display: none } + } + } } -} - -/* burger menu opener */ -button[popovertarget] { - background: url(/icon/dots.svg) center center no-repeat; - background-color: var(--gray-50); - border-radius: .2em; - padding-inline: .8em; - height: 2rem; - width: 2rem; - padding: 0; - border: 0; - - &:hover { - color: var(--gray-900); - background-color: var(--gray-100); + @media (width < 500) { + .pill { display: none } } } .pill { - background: var(--main-50) url(/icon/present.svg) .6em 50% no-repeat; + background: var(--main-50) url(/icon/cube.svg) .75em 50% no-repeat; padding: .6em 1em .6em 2.7em; background-size: 1.5em; color: var(--gray-600); font-weight: 550; border-radius: 9em; - font-size: 80%; + font-size: .8em; + + em { + font-style: normal; + color: var(--main-600); + margin-right: .3em; + font-weight: 600; + } &:hover { - background-color: var(--main-500); - background-image: url(/icon/present-light.svg); - color: white; + transform: scale(1.005); + color: var(--gray-900); } &:active { @@ -104,13 +117,6 @@ button[popovertarget] { &.github { background-image: url(/icon/github.svg); - background-color: #f4f4f4; - color: black; - - &:hover { - filter: grayscale(1) invert(1); - - } } &[aria-selected] { @@ -118,44 +124,25 @@ button[popovertarget] { } } - - /* global footer */ body > footer { - padding-block: 5em; + text-align: center; + padding: 2rem 0 5rem; + border-top: var(--border); - > :first-child { - margin-bottom: 2em; + img { + width: 50px; } - h3 { - font-size: 95%; - margin: 0 0 .5em; - } - - nav a { - font-size: 90%; - display: block; - padding: .5em 0; - font-weight: 400; - } - - a:has(img) { - padding: 0; - } - - @media (max-width: 700px) { - flex-direction: column; - gap: 3em; - } - - > div { - display: flex; - gap: 6em; + nav { + display: inline-flex; + margin-block: 1rem; + font-size: .9rem; + gap: 2rem; - @media (max-width: 400px) { + @media (width < 600) { flex-direction: column; - gap: 3em; + gap: 1.5rem; } } diff --git a/packages/nuejs.org/@global/navigation.yaml b/packages/nuejs.org/@global/navigation.yaml new file mode 100644 index 00000000..5bb393db --- /dev/null +++ b/packages/nuejs.org/@global/navigation.yaml @@ -0,0 +1,63 @@ + +# the information architecture +documentation: + Getting started: + - Why Nue: /docs/ + - How it works: /docs/how-it-works.html + - Installation: installation.html + - Command line interface: command-line-interface.html + + Building websites: + - Step-by-step tutorial: tutorial.html + - Project structure: project-structure.html + - Content: content.html + - Layout: layout.html + - Islands: islands.html + - Styling: styling.html + - Motion: motion.html + - Scripting: scripting.html + - Optimization: optimization.html + + + Reference: + - Settings and metadata: settings.html + - Content syntax: content-syntax.html + - Content tags: content-tags.html + - Custom components: custom-components.html + - Core components: core-components.html + - Template syntax: template-syntax.html + - Content collections: content-collections.html + - Syntax highlighting: syntax-highlighting.html + + +globalnav: + + # main navigation (header) + main: + - Home: / "nav-home" + - Docs: /docs/ + - Blog: /blog/ + - "*Nue 1.0 (RC)* • A better Next.js?": /blog/nue-release-candidate/ "status pill" + + + # header toolbar (complementary) + toolbar: + - Feed: //x.com/tipiirai "social" + - 6.1k: //github.com/nuejs/nue "github pill" + + # burger menu + menu: + - Home: / + - Docs: /docs/ + - Blog: /blog/ + - GitHub: //github.com/nuejs/nue + + # footer + footer: + - Why Nue?: /docs/ + - How it works: /docs/how-it-works.html + - Documentation: /docs/ + - Installation: /docs/installation.html + - Blog: /blog/ + + diff --git a/packages/nuejs.org/@global/popover.css b/packages/nuejs.org/@global/popover.css index 69053868..6ee703b8 100644 --- a/packages/nuejs.org/@global/popover.css +++ b/packages/nuejs.org/@global/popover.css @@ -6,7 +6,6 @@ opacity: 1; border: 0; - &::backdrop { background-color: #0005; backdrop-filter: blur(4px); @@ -14,7 +13,7 @@ } @starting-style { - transform: scale(.5); + transform: scale(.9); opacity: 0; &::backdrop { @@ -23,16 +22,10 @@ } } - /* close */ - [popovertarget] { - background-image: none; - color: var(--gray-600); + > .action:first-child { position: absolute; - font-weight: 400; - font-size: 1.6em; - width: 1.1em; - right: .4em; - top: .4em; + right: .5em; + top: .5em; } } @@ -57,4 +50,45 @@ dialog { display: block; z-index: 1; } -} \ No newline at end of file +} + + +.simple-compare { + max-width: 980px; + margin: 0 auto; + gap: 1.5rem; + inset: .75rem; + + &:popover-open { + display: flex; + } + + + h2 { + margin: .5em 0 0; + + p { margin-block: .3em } + a { display: none } + } + > * { + flex: 1 + } + + .action { + position: absolute; + top: .5em; + right: .5em; + figure { margin: 0 } + } + + @media(width < 900) { + max-height: 96vh; + flex-direction: column; + h3 span { display: none } + } + +} + + +#resources { + max-width: 1130px; +} diff --git a/packages/nuejs.org/@global/stack.css b/packages/nuejs.org/@global/stack.css new file mode 100644 index 00000000..2cd0446e --- /dev/null +++ b/packages/nuejs.org/@global/stack.css @@ -0,0 +1,49 @@ +.flex { + align-items: center; + display: flex; +} +.stack { + display: flex; + margin: 2em 0; + gap: 1.5em; + + > * { + flex: 1; + } + + :first-child { margin-top: 0 } + :last-child { margin-bottom: 0 } + + @media (max-width: 900px) { + flex-direction: column; + align-items: flex-start; + } +} + +/* generic card */ +.card, .of-cards > * { + + background-color: white; + border-radius: .5em; + border: var(--border); + padding: 1.5em; + font-size: 95%; + + > :first-child { margin-top: 0 } + > :last-child { margin-bottom: 0 } + + h3 { + margin-bottom: -.5em; + } +} + +.clickable > div { + cursor: pointer; + + &:hover { + box-shadow: 0 0 0 2px white, 0 0 0 4px var(--main-500); + } + &:active { + transform: scale(.99); + } +} diff --git a/packages/nuejs.org/@global/variables.css b/packages/nuejs.org/@global/variables.css new file mode 100644 index 00000000..cd538c49 --- /dev/null +++ b/packages/nuejs.org/@global/variables.css @@ -0,0 +1,49 @@ + +:root { + + /* main color */ + --main-50: #eff6ff; + --main-100: #dbeafe; + --main-200: #bfdbfe; + --main-300: #93c5fd; + --main-400: #60a5fa; + --main-500: #3b82f6; + --main-600: #2563eb; + + /* slate gray */ + --gray-50: #f8fafc; + --gray-100: #f1f5f9; + --gray-200: #e2e8f0; + --gray-300: #cbd5e1; + --gray-400: #94a3b8; + --gray-500: #64748b; + --gray-600: #475569; + --gray-700: #334155; + --gray-800: #1e293b; + --gray-900: #0f172a; + --gray-950: #020617; + + + /* accent colors */ + --red: #fb7185; + --pink: #e200ff; + --purple: #9E52FF; + --yellow: #f7ff4f; + --green: #06d4a1; + + /* syntax highlighting */ + --glow-bg-color: var(--gray-800); + --glow-padding: 2em; + + /* page padding */ + --body-padding: 1em 2.5em; + + /* shortcut */ + --border: 1px solid var(--gray-200); +} + + +@media (max-width: 600px) { + :root { --body-padding: 1.2em; } +} + diff --git a/packages/nuejs.org/@lib/compare.css b/packages/nuejs.org/@lib/compare.css new file mode 100644 index 00000000..33632f55 --- /dev/null +++ b/packages/nuejs.org/@lib/compare.css @@ -0,0 +1,79 @@ + +.compare { + background-color: var(--gray-100); + padding: 3%; + left: 1em; + max-height: calc(100vh - 2em); + + h2 { + margin: 0; + font-weight: 600; + font-size: 1.7em; + letter-spacing: -0.02em; + } + nav { + margin-left: auto; + gap: .5em; + } + + .action { + background-color: white; + &:hover { + box-shadow: 0 0 0 1px var(--gray-300); + background-color: white; + } + } + + .stack { + opacity: 1; + flex-direction: row-reverse; + + &.seeking { + transition: .2s; + opacity: .8; + } + } + + figure { + background-color: white; + border-radius: 6px; + border: var(--border); + overflow: hidden; + margin-top: 1em; + padding: 4%; + img:hover { box-shadow: none } + + &:hover { + border-color: var(--gray-400); + } + } + h3 { + font-size: 1.1em; + font-weight: 500; + gap: .3em; + } + + .github { + background: url(/icon/github.svg) 0 50% no-repeat; + color: var(--main-500); + padding-left: 1.3em; + background-size: 1em; + margin-left: auto; + margin-right: .5em; + font-size: 1rem; + + &:hover { + text-decoration: underline var(--gray-300); + } + } + + @media (width > 800px) { + overflow: hidden; + } + + @media (width < 800px) { + div:first-child h3 span { display: none } + } + + +} \ No newline at end of file diff --git a/packages/nuejs.org/@lib/compare.htm b/packages/nuejs.org/@lib/compare.htm new file mode 100644 index 00000000..57cf6bd5 --- /dev/null +++ b/packages/nuejs.org/@lib/compare.htm @@ -0,0 +1,72 @@ + + + +
+

{ step.title }

+ +
+ +
+
+

+ Tailwind/Next.js: + JS monolith + { step.src[0] } +

+
+ +
+
+ +
+

+ Nue: + { step.desc } + { step.src[1] } +

+
+ +
+
+
+ + +
\ No newline at end of file diff --git a/packages/nuejs.org/@lib/syntax-extras.css b/packages/nuejs.org/@lib/syntax-extras.css index 5071dfa5..4d93344a 100644 --- a/packages/nuejs.org/@lib/syntax-extras.css +++ b/packages/nuejs.org/@lib/syntax-extras.css @@ -61,13 +61,14 @@ flex: 1; } - figcaption { + h3 { border-bottom: 1px solid var(--gray-600); padding: 1.2em var(--glow-padding); text-align: left; margin: 0; color: var(--gray-100); font-weight: 600; + font-size: 90%; em { background-color: var(--main-500); diff --git a/packages/nuejs.org/@lib/tabs.css b/packages/nuejs.org/@lib/tabs.css new file mode 100644 index 00000000..6579642c --- /dev/null +++ b/packages/nuejs.org/@lib/tabs.css @@ -0,0 +1,28 @@ + +.tabs { + position: relative; + display: flex; + height: 150px; + gap: 1.5em; + + /* tabs */ + summary { + cursor: pointer; + font-weight: 550; + &::marker { font-size: 0 } + &:hover { color: var(--main-600) } + } + + /* tab panes */ + div { + inset: 3em 0 0 1.5em; + position: absolute; + } + + /* active tab */ + [open] summary { + pointer-events: none; + text-decoration: 3px underline var(--main-500); + text-underline-offset: 10px; + } +} \ No newline at end of file diff --git a/packages/nuejs.org/@lib/technical-content.css b/packages/nuejs.org/@lib/technical-content.css index 33341d2f..fef82e13 100644 --- a/packages/nuejs.org/@lib/technical-content.css +++ b/packages/nuejs.org/@lib/technical-content.css @@ -4,7 +4,6 @@ h1 { font-size: 2em; font-weight: 800; text-wrap: balance; - max-width: 18em; letter-spacing: -.025em; margin-bottom: .3em; @@ -22,11 +21,17 @@ table { border-collapse: collapse; color: var(--gray-600); font-size: 90%; - margin: 3em 0; + margin: 1rem 0; width: 100%; th, td { - padding: .6em 1em .6em 0; + padding: .5em 1.2em .6em 0; + line-height: 1.3; + white-space: nowrap; + + &:last-child { + white-space: inherit; + } } th, td:first-child { @@ -46,11 +51,12 @@ article { p, li { line-height: 1.75; color: var(--gray-500); - } + text-wrap: pretty; - strong { - font-weight: 550; - color: var(--gray-700); + strong { + font-weight: 550; + color: var(--gray-600); + } } /* internal links */ @@ -76,10 +82,10 @@ article { } } - p > code, li > code { - font-weight: 600; - font-size: 110%; - color: var(--gray-950); + /* not under pre */ + li > code, p > code, td > code { + font-weight: 550; + color: var(--gray-700); &:before, &:after { color: var(--main-400); @@ -179,8 +185,8 @@ blockquote { li > p > strong:first-child { - font-weight: 700; - color: var(--gray-950); + font-weight: 650; + color: var(--gray-800); } @@ -225,10 +231,10 @@ h2[id], section > h3[id] { /* images */ figure { - margin: 2em 0 3em; + margin: 1rem 0 2rem; img { - border-radius: 10px; + border-radius: 6px; } &:has(img[width]) { @@ -271,22 +277,27 @@ figure > a { .pink { background-image: linear-gradient(#e879f9, #ec4899) } .pink, .blue, .purple { - padding: 3em 0 0 3em; - border-radius: .5em; - overflow: hidden; + padding: 2.5em 0 0 2.5em; margin: 1.5em 0 2em; + border-radius: 6px; + overflow: hidden; pre, img { - border-radius: .5em 0 0 0; + border-radius: 6px 0 0 0; margin: 0; } - img { - margin-bottom: -4px; - } - @media (max-width: 500px) { - padding: 1em; + img { margin-bottom: -4px } + + &.info { + margin-top: 4rem; + figcaption { + position: absolute; + margin: -5em 0 0 -2em; + } } + + } .bordered img { @@ -316,15 +327,60 @@ pre { color: var(--main-400); } +/* "good" and "bad" code block captions */ +.good, .bad, .info { + margin-top: 2em; + + figcaption { + background: url(/icon/bad.svg) -.2em center no-repeat; + padding: .2em 0 .2em 1.8em; + background-size: 1.6em; + color: var(--gray-900); + text-align: left; + font-weight: 550; + font-size: 95%; + + &:after { + border-left: 1px solid var(--red); + position: relative; + top: 1.2em; + left: -1.2em; + content: ""; + height: 1em; + display: block; + float: left; + } + } +} + +.good figcaption { + background: url(/icon/good.svg) -1px center no-repeat; + background-size: 1.4em; + + &:after { + border-color: var(--green); + } +} + +.info figcaption { + background: url(/icon/info.svg) 0 center no-repeat; + background-size: 1.4em; + + &:after { + border-color: var(--main-400); + } +} .note { - border-left: 2px solid var(--main-500); background-color: var(--main-50); - border-radius: 0 8px 8px 0; - padding: 1.5em; - margin: 2em 0; - font-size: 95%; - max-width: 90%; + border-radius: 6px; + padding: 1em 1.5em; + font-size: 1rem; + margin: 2rem 0; + + @media(width > 1000px) { + max-width: 90%; + } > :first-child { background: url(/icon/info.svg) no-repeat; @@ -354,17 +410,17 @@ pre { } } -.stack { - display: flex; - margin: 2em 0; - gap: 1.5em; +details { + font-weight: 500; + padding: 1rem 0; + cursor: pointer; - > * { - flex: 1; + summary { + font-size: 1.1rem; + &::marker { color: var(--main-400) } } - @media (max-width: 900px) { - flex-direction: column; - align-items: flex-start; + &:not(:last-child) { + border-bottom: var(--border); } } diff --git a/packages/nuejs.org/@lib/video.css b/packages/nuejs.org/@lib/video.css index 8c550232..d4add2d4 100644 --- a/packages/nuejs.org/@lib/video.css +++ b/packages/nuejs.org/@lib/video.css @@ -3,7 +3,7 @@ border-radius: 4px; display: inline-block; position: relative; - margin: 2em 0; + margin: .5em 0; video { border: var(--border); @@ -14,28 +14,26 @@ } .play { - background: #0009 url(/icon/play.svg) no-repeat; - border: 3px solid white; + + background: rgba(0,0,0, .7) url(/icon/play.svg) 57% 50% no-repeat; transform: translate(-50%, -50%); - background-position: 62% 50%; - box-shadow: 0 0 2em #0005; + border: 3px solid white; background-size: 50%; border-radius: 99em; position: absolute; cursor: pointer; - height: 5em; - opacity: .8; - width: 5em; + height: 4.2em; + width: 4.2em; left: 50%; top: 45%; &:hover { - border-color: white; - opacity: 1; + background-color: #0009; } &:active { - border-color: black; + box-shadow: 0 0 1em black inset; + top: 45.2%; } .playing & { diff --git a/packages/nuejs.org/@lib/video.htm b/packages/nuejs.org/@lib/video.htm index 4b475041..84895437 100644 --- a/packages/nuejs.org/@lib/video.htm +++ b/packages/nuejs.org/@lib/video.htm @@ -34,17 +34,15 @@ }) } + toggle() { const { video } = this const { paused } = video paused ? video.play() : video.pause() this.root.classList.toggle('playing', paused) - - if (!this.started) { - this.root.classList.remove('splash') - this.started = true - } + this.root.parentNode.classList.remove('splash') + player = this } @@ -52,9 +50,39 @@ diff --git a/packages/nuejs.org/@lib/video.html b/packages/nuejs.org/@lib/video.html index d3c2a544..1ad54c73 100644 --- a/packages/nuejs.org/@lib/video.html +++ b/packages/nuejs.org/@lib/video.html @@ -1,6 +1,6 @@ -
+
diff --git a/packages/nuejs.org/README.md b/packages/nuejs.org/README.md index b34a458a..3c061b7b 100644 --- a/packages/nuejs.org/README.md +++ b/packages/nuejs.org/README.md @@ -3,15 +3,15 @@ The source code for https://nuejs.org - + ### Running locally 1. [Install Nue](//nuejs.org/docs/installation.html) -2. Clone this branch +2. Clone this repository 3. `cd nue/packages/nuejs.org` 4. `nue` -This should start the website at http://localhost:8080/ \ No newline at end of file +The website runs at http://localhost:8080/ \ No newline at end of file diff --git a/packages/nuejs.org/blog/index.md b/packages/nuejs.org/blog/index.md index 2647c74e..df8d9f11 100644 --- a/packages/nuejs.org/blog/index.md +++ b/packages/nuejs.org/blog/index.md @@ -21,4 +21,4 @@ exclude: [button.secondary href="/#roadmap" "Join mailing list"] -[gallery] +[page-list] diff --git a/packages/nuejs.org/blog/introducing-glow/index.md b/packages/nuejs.org/blog/introducing-glow/index.md index e13eb76c..c519dcb5 100644 --- a/packages/nuejs.org/blog/introducing-glow/index.md +++ b/packages/nuejs.org/blog/introducing-glow/index.md @@ -1,6 +1,6 @@ --- title: Introducing Glow -hero_title: "*Introducing Glow:* Beautiful, pixel-perfect Markdown code blocks" +hero_title: "*Introducing Glow:* Markdown code blocks for CSS developers" desc: Beautiful, pixel-perfect Markdown code blocks og: /img/glow-og.png date: 2024-02-13 @@ -10,17 +10,17 @@ include: [syntax-extras] Today we're launching *Glow* — a new take on syntax highlighting: [image.large] - small: /img/glow-og.png + caption: 30+ languages colored. Click for a standalone view. large: /img/glow-og-big.png - caption: 30+ languages colored. Click the image for a standalone view. - href: /glow-demo/ # TODO: fix + small: /img/glow-og.png + href: /glow-demo/ -*Glow is different*: Instead of attempting to understand language internals, Glow focuses solely on aesthetics and how your code looks. +*Glow is different*: Rather than parsing language internals, Glow focuses purely on aesthetics and the visual style of your code. -*Glow is simple*: Glow makes all languages work with your brand colors by adjusting just a handful of CSS variables. +*Glow is simple*: With just a few CSS variables, Glow adapts all languages to match your brand colors effortlessly. -*Glow is small*: Glow is orders of magnitude smaller than the mainstream alternatives. We're talking [5K](//pkg-size.dev/nue-glow) instead of [5M](//pkg-size.dev/shiki). It's by far the smallest implementation available. +*Glow is small*: Glow is significantly smaller than mainstream alternatives—around [5KB](//pkg-size.dev/nue-glow) compared to [5MB](//pkg-size.dev/shiki). It’s the most compact implementation available. [image.tall] @@ -69,58 +69,64 @@ Contrast this to grammar-aware theming systems, like Shiki and *Prism*, where a ## Unlimited possibilities Glow's unique, [classless design system](/docs/syntax-highlighting.html#html-markup) gives you line numbers, selections, error highlights, insertions, deletions, and much much more. -[code.is-dark.browser-like wrapper="gradient sky" numbered="true"] - - - - - - - - {#if pressed} -

••Yoou•• pressed and held for {duration}ms

- {/if} - - +``` .gradient.sky numbered + + + + + + + +{#if pressed} +

••Yoou•• pressed and held for {duration}ms

+{/if} + + +``` + And when I say "unlimited", it means that: -[code language="md" numbered="true" wrapper="live-code"] - # There's something about Lightning CSS - Writing future CSS today has been a massive - •productivity boost.• You'll get nesting, `color-mix()`, - variables, and whatnot. Natively, today. +``` md.live-code numbered +# There's something about Lightning CSS +Writing future CSS today has been a massive +•productivity boost.• You'll get nesting, `color-mix()`, +variables, and whatnot. Natively, today. + +![CSS, bro](/vanilla.png) - ![CSS, bro](/vanilla.png) +> •After I ditched all tooling• I was able to +> work closer to metal. Everything happened +> sub-millisecond. I entered a new planet. +``` - > •After I ditched all tooling• I was able to - > work closer to metal. Everything happened - > sub-millisecond. I entered a new planet. +## Using together with Nue +[Nue](/) is a web framework specializing on UX development. As of today, it has built-in support for Glow. You can easily extend your Markdown with stacks of code blocks or tabbed code panels. For example: -## Glow + Nue = Next level -[Nue](/) is a web framework for UX developers and other design-minded people. As of today, it has built-in support for Glow. You can do things like the following in your Markdown content: +[.codestack.larger] + ### Content *YAML* -[codeblocks.codestack.larger captions="Content *YAML* | Styling *CSS*"] + ``` md # View metadata members: title: Members @@ -136,8 +142,11 @@ And when I say "unlimited", it means that: sorting: created: Date subscribed card: Card type + ``` - --- + ### Styling *CSS* + + ``` css /* Tab styling */ [role=tablist] { background: rgba(0, 0, 0, .7); @@ -153,76 +162,13 @@ And when I say "unlimited", it means that: cursor: pointer; } } - -Or things like this: - - -[codetabs "HTML | CSS | JS" wrapper="pink" numbered="1"] - - -
-
-

{ title }

-
- - - - -
- -
-
-
- --- - dialog { - background-color: #0004; - box-shadow: 0 0 2em #9cc; - border-radius: 1em - } - - label { - display: block; - margin-bottom: 1em - input { width: 100% } - } - - footer { - margin-top: 1em; - button { width: 100%; } - } - --- - // Generic Web Component for sending data - class HTXPost extends HTMLElement { - constructor(options) { - super() - const endpoint = this.getAttribute('action') - const formData = new FormData(this) - - this.onsubmit = (e) => { - const res = await fetch(endpoint, { - headers: { AccessKey: options.access_key }, - method: 'POST' - }) - } - } - } - - customElements.define('htx-post', HTXPost, { - extends: 'form' - }) - + ``` ## Get started with Glow You can try Glow either as a standalone library or together with the Nue framework. -#### Standalone library +### Standalone library Install [nue-glow](//github.com/nuejs/nue/tree/master/packages/glow) with npm, pnpm, or bun: ```sh @@ -232,11 +178,9 @@ npm i nue-glow And follow the [Glow documentation](/docs/syntax-highlighting.html) -#### With Nue - -Nue has built-in support for Glow in Markdown fenced code blocks and offers [three new tags](/docs/tags.html#code): `[code]`, `[codeblocks]`, and `[codetabs]` for content creators. +### With Nue -You can try the tags as follows: +Try Glow with Nue as follows: ```sh # Install Bun (if not done yet) @@ -246,10 +190,10 @@ curl -fsSL https://bun.sh/install | bash bun install nuekit --global # Start a Nue project with a Glow-powered template -bun create nue@latest +nue create simple-blog ``` -Choose *"Simple blog"* on the last step and you can enjoy goodies content hot-reloading when the code blocks are edited: +Now you can enjoy goodies content hot-reloading when the code blocks are edited: [bunny-video.larger] videoId: 38caf489-74f1-416a-9f23-694baa5500bb @@ -257,7 +201,6 @@ Choose *"Simple blog"* on the last step and you can enjoy goodies content hot-re poster: thumbnail_1ca1bd66.jpg -PS: Check out [Getting started docs](/docs/#node) if you prefer Node. diff --git a/packages/nuejs.org/blog/introducing-nue-css/index.md b/packages/nuejs.org/blog/introducing-nue-css/index.md index cabe926c..de0a8e84 100644 --- a/packages/nuejs.org/blog/introducing-nue-css/index.md +++ b/packages/nuejs.org/blog/introducing-nue-css/index.md @@ -111,9 +111,9 @@ The design is separated from the HTML structure so that the system can be contro ## 2. Cleaner way to write HTML and CSS { #minimalism } -Nue has its own [CSS best practices](/docs/css-best-practices.html) for writing clear HTML and CSS. The key idea is to leave out all class names that relate to styling and let external CSS take care of the element and/or component look and feel: +Nue has its own [CSS best practices](/docs/styling.html) for writing clear HTML and CSS. The key idea is to leave out all class names that relate to styling and let external CSS take care of the element and/or component look and feel: -```html +```
+ +
+ + + +
+ - -
- - - -
diff --git a/packages/nuejs.org/blog/lib/blog-index.css b/packages/nuejs.org/blog/lib/blog-index.css index a97b4b96..5f5fc508 100644 --- a/packages/nuejs.org/blog/lib/blog-index.css +++ b/packages/nuejs.org/blog/lib/blog-index.css @@ -66,13 +66,11 @@ li { } } - a:hover p:after{ - text-decoration: underline; - } + a:hover p:after { text-decoration: underline } &.is-new h2:before { content: "NEW"; - background-color: var(--purple); + background-color: var(--main-600); margin-right: .5em; padding: .2em .4em; font-weight: 800; @@ -94,13 +92,8 @@ li { border-radius: .5em; flex: 2; - &:hover { - background-color: var(--gray-50); - } - - &:active { - transform: scale(.99); - } + &:hover { background-color: var(--gray-50) } + &:active { transform: scale(.99) } } time { @@ -126,8 +119,8 @@ li { &.is-new { time:after { - background-color: var(--purple); - border-color: var(--purple); + background-color: var(--gray-700); + border-color: var(--gray-700); } } } diff --git a/packages/nuejs.org/blog/nue-1-beta/index.md b/packages/nuejs.org/blog/nue-1-beta/index.md index 1bae0fc9..cca8db38 100644 --- a/packages/nuejs.org/blog/nue-1-beta/index.md +++ b/packages/nuejs.org/blog/nue-1-beta/index.md @@ -1,44 +1,34 @@ --- hero_title: "*Nue 1.0 (Beta)* — A web framework for UX developers" title: Nue 1.0 (Beta) -desc: A web framework for UX developers +desc: We just released a beta version of the upcoming Nue 1.0 to give you a little idea what the final product will eventually look like. date: 2024-08-15 --- -Exactly one year ago I [decided](/blog/backstory/) to create the slickest website generator for UX developers and design-focused organizations. Today this vision is becoming a reality: - -[image.larger] - small: /img/og-blue.png - large: /img/og-blue-big.png - size: 747 x 474 - caption: "Nue offers the shortest path from Figma to CSS" - -[.note] - ### The gist - Nue is simpler. What used to take a React specialist and an absurd amount of JavaScript can now be done by a UX developer and a small amount of CSS. - - -### Who is Nue for? -Nue is a great fit for the following people: +Exactly one year ago I [decided](/blog/backstory/) to create a super simple website generator targeted for the following people: 1. **UX developers**: who natively jump between **Figma** and **CSS** without a confusing [designer-developer handoff](//medium.com/design-warp/5-most-common-designer-developer-handoff-mishaps-ba96012be8a7) process in the way. +1. **Beginner web developers**: who want to skip the [redundant frontend layers](//roadmap.sh/frontend) and start building websites quickly with HTML, CSS, and JavaScript. -2. **Beginner web developers**: who want to skip the [redundant frontend layers](//roadmap.sh/frontend) and start building websites quickly with HTML, CSS, and JavaScript. - -3. **Experienced JS developers**: frustrated with the absurd amount of layers in the [React stack](//roadmap.sh/react) and looking for simpler ways to develop professional websites. +1. **Experienced JS developers**: frustrated with the absurd amount of layers in the [React stack](//roadmap.sh/react) and looking for simpler ways to develop professional websites. [.quote] > “Nue is **exactly** what I want. As a technical founder, I need easier ways to build websites. I don't want to hire devs and watch all my profits disappear in salaries.” — Alan Hemmings / [BigNameHunter](//bignamehunter.com/?refer=nuejs) / CEO -4. **Designers**: planning to learn web development, but find the JavaScript ecosystem too scary +1. **Designers**: planning to learn web development, but find the JavaScript ecosystem too scary -5. **Parents & Teachers**: who wants to educate people [how the web works](//www.websitearchitecture.co.uk/resources/examples/web-standards-model/) +1. **Parents & Teachers**: who wants to educate people [how the web works](//www.websitearchitecture.co.uk/resources/examples/web-standards-model/) +[image.larger] + small: /img/og-blue.png + large: /img/og-blue-big.png + size: 747 x 474 + - - - @@ -46,20 +36,6 @@ Nue is a great fit for the following people: **v1.0 Beta** is by far the biggest release yet with five months of work and over [500 files changed](https://github.com/nuejs/nue/pull/316). This is a breakdown of new features, updates, and breaking changes. -## Global design system -[Global design system](/docs/global-design-system.html) is the largest new feature in this release. It guarantees that you always get the same kind of markup for your pages and components, but can use CSS to achieve wildly different designs. - -[image] - small: /img/global-design-system.png - large: /img/global-design-system-big.png - caption: Same markup, but wildly different designs - href: /docs/global-design-system.html - -Think of Nue like a modern-day [CSS Zen Garden](//csszengarden.com/): A demonstration of what you can accomplish with nothing but CSS. Nue frees you from implementing page layouts and basic UI elements over and over for every new page and project. Or as **Brad Frost** [puts it][gds]: - -> Global Design System improves the quality and accessibility of the world’s web experiences, saves the world’s web designers and developers millions of hours, and makes better use of our collective human potential. *Brad Frost* - - ## Improved CSS stack Nue has a powerful CSS theming system that supports [hot-reloading](/docs/hot-reloading.html), CSS inlining, error reporting, and automatic dependency management. This version improves the system with the following features: @@ -73,8 +49,6 @@ Nue has a powerful CSS theming system that supports [hot-reloading](/docs/hot-re - - ## CSS view transitions View transitions are an important part of a seamless user experience and are a key feature in the Nue framework. They can now be enabled with this simple configuration variable: @@ -105,13 +79,13 @@ View transitions is a broad standard with tons of visual possibilities that are ## CSS best practices -Nue's new [CSS best practices](/docs/css-best-practices.html) brings out the best of modern CSS: +Nue's new [CSS best practices](/docs/styling.html) brings out the best of modern CSS: [image.larger] small: /img/blog-css-hierarchy.png large: /img/blog-css-hierarchy-big.png caption: Nue offers the shortest path from Figma to code - href: /docs/css-best-practices.html + href: /docs/styling.html These lessons focus on writing reusable CSS that is easy to read, maintain, and scale. It unpacks decades of CSS experience with a heavy focus on *clarity* and *minimalism*. In fact, all the lessons can squeezed into this one sentence: diff --git a/packages/nuejs.org/blog/nue-release-candidate/index.md b/packages/nuejs.org/blog/nue-release-candidate/index.md new file mode 100644 index 00000000..9d5de29b --- /dev/null +++ b/packages/nuejs.org/blog/nue-release-candidate/index.md @@ -0,0 +1,207 @@ + +--- +hero_title: "*Nue 1.0 (RC)* — Can it outshine Next.js?" +title: "A better Next.js? • Nue 1.0 (RC) is out" +desc: This version brings numerous improvements, bug fixes, clearer documentation, and a new Markdown parser built from scratch to strengthen Nue's vision +date: 2024-11-15 +--- + +Nue is a static site generator (SSG) built from the ground up to offer faster tooling, cleaner codebases, and better results. Today, we’re releasing the first version of Nue that earns the "release candidate" (RC) status. + +[image] + small: /img/og-dark.png + large: /img/og-dark-big.png + +This major update introduces a range of improvements while simplifying the system. The documentation has been greatly enhanced, and we've built a completely new Markdown parser from the ground up, specifically designed to bring Nue’s content-first vision to life. + +### But better than Next.js?! +I know, it might sound a bit over the top... but hear me out. Let’s break it down into three concrete points: + + +## 1. Design engineering +Nue is an ambitious engineering project designed to simplify web development through **separation of concerns** and **progressive enhancement**. This approach fundamentally redefines how websites are developed: + +[image] + small: /img/progressive-enhancement.png + large: /img/progressive-enhancement-big.png + size: 650 x 174 + +What once required a **React specialist** and a large amount of **JavaScript** can now be achieved with clean, standards-based code: + +[image.bordered] + large: /img/clean-code-big.png + small: /img/clean-code.png + size: 745 × 403 + +This means you’re no longer bogged down with debugging complex algorithms and data structures. Instead, you can put all that focus on **content**, **layout**, and **design systems**—making a natural transition from **JavaScript engineering** to **Design engineering**. + + +## 2. Tooling experience +One of Nue’s standout features is its hot-reloading mechanism, and with this version, the diff/patch system is stronger than ever. By instantly detecting changes in **content**, **data**, **layouts**, **styles**, **components**, and **islands**, Nue applies updates directly to your browser. This creates a smooth, lightning-fast feedback loop that makes the development process more exciting and fluid. + +[bunny-video] + videoId: abb2cf75-c7f9-43e6-b126-8827d0c8721e + style: "background-color: #282C30" + poster: /img/blog-content-editing-big.png + +With the new Markdown parser and the absence of unnecessary JavaScript abstractions and dependencies, Nue is **incredibly lean and fast**, consistently outperforming monolithic web frameworks: + +[table] + Framework | Next.js | Nue + NPM modules | 300+ | 10+ + Project weight | 300M+ | 10M+ + Build speed / 10 pages | 10+ seconds | 0.01+ seconds + Build speed / 100 pages | 30+ seconds | 0.1+ seconds + Hot-reload times / complex app | 1-5s | 0.05-0.3s + + +## 3. Resulting sites +Nue helps developers create **fast** and **user-friendly** websites with features like **turbolinking**, **CSS view transitions**, **interactive islands**, and **CSS inlining**: + +[bunny-video] + videoId: 383e5c79-6747-4b1a-8d7a-9da9ae721d33 + poster: /img/hero-splash.jpg + caption: "Nue templates preview. Hit **F** for fullscreen" + +Surprisingly, these rich, interactive sites remain exceptionally lightweight, comparable to text-only websites. To give you an idea, here’s how Nue [documentation area](/docs/) compares to Next.js documentation: + +[table.with-total] + Resources | Next.js | Nue | Difference + HTML document | 51kB | 10kB | 5 × smaller + CSS | 62kB | 1kB | 60 × less + JavaScript | 531kB | 7kB | 75 × less + Total | 644kB | 19kB | 30 × less + +[button.zoom popovertarget="resources" "See the difference"] + +[#resources.simple-compare popover] + + ## Nue documentation + 27kB of HTML/CSS/JS + + [! /tour/img/assets-nue.png ] + + + ## Next.js documentation + 645kB of HTML/CSS/JS + + [! /tour/img/assets-next.png ] + + [button.action popovertarget="resources"] + [image /icon/close.svg] + + + +[.note] + ### Understanding Nue + To better grasp the benefits and unique development model of Nue, be sure to explore the newly updated documents for [Why Nue?](/docs/) and [How it works](/docs/how-it-works.html). This section focuses specifically on the new release. + + + +## New Markdown parser +In earlier versions of Nue, we used the **Marked** library to handle basic formatting for our extended Markdown syntax — known as **Nuemark**. While Marked is a solid, general-purpose Markdown parser with excellent performance, it became clear that it couldn't fully support the direction we wanted for Nue, particularly with our content-first development model. + +The main issue with Marked was its lack of a mutable **abstract syntax tree** (AST). The AST is a structured representation of the Markdown content, allowing it to be easily analyzed, manipulated, and extended before rendering to HTML. With Marked, the structure was essentially immutable—meaning it wasn't possible to add new nodes, elements, or link references before rendering the document. + +Moreover, the API for creating custom extensions was overly complex, particularly for advanced features that needed to understand the document’s structure and modify parts of the syntax tree. This made implementing the more advanced components of Nue impossible. + +To overcome these limitations, we built an **entirely new parser** from scratch. This new parser implements a **mutable AST**, offering greater flexibility, performance, and control over the content structure. With this foundation, we were able to add all the required features, enabling the creation of rich, interactive content and making it easier to integrate custom elements into the rendering process. + + +```md.blue.info "A new, clutter-free syntax for accordions, tabs, and grids" +|[accordion] + + ## New features + 1. New Markdown parser + 2. New formatting options + 3. New components + + ## Improvements + 1. Web component fallbacks + 2. Markdown support + 3. Improved HMR error reporting +``` + + +### New parser features +The new parser introduces several powerful features to enhance your Markdown experience: + +- **New formatting options**: You can now use custom syntaxes like `|highlighted|` to highlight text, `•bolded•` for bold text, and `/italics/` for italics, in addition to the standard Markdown `**strong**` and `_emphasized_`. + +- **Variables and expressions**: You can now use variables and expressions within your documents, such as `{ package.name }` to display dynamic content or `{ foo.bar[0].name }` to access specific elements from complex data structures. + +- **Common Markdown extensions**: The parser now supports popular extensions like **footnotes** and **tables**, allowing you to create more interactive and well-structured documents without custom solutions. + +- **Explicit heading IDs and class names**: You can now define custom IDs and class names for headings using the syntax `## Hello { #world.epic }`, making it easier to add specific styles or create anchor links for better navigation. + +- **Automatic section splitting**: Documents will now automatically split into sections by analyzing the document’s structure. When an `h2` or `---` is encountered, a new section is created, making it easier to organize and navigate large documents. + +- **Inline components**: Custom tags can now be inlined directly within the content, like `A inlined [custom-tag] is here`. This allows you to add dynamic or reusable components within the text flow, offering more flexibility in structuring your content. + + + +### New tags + +- **New `[accordion]` tag**: The [accordion] tag allows you to create collapsible panels without the need for complex syntaxes or additional JavaScript. Using the AST, the parser automatically treats `h2` or `h3` headers as new accordion panels. This makes it easy to create interactive content sections simply by structuring your Markdown with headers. + +- **New `[define]` tag**: The [define] tag is designed for creating description lists, such as glossaries or key-value pairs. Similar to the [accordion] tag, it leverages the AST to automatically structure and render the content, offering a clean and consistent way to display terms and descriptions without requiring extra HTML markup. + +- **Updated `[tabs]`**: The `[tabs]` tags has been improved to now be based on the accordion tag and HTML ``/`
` elements. This means that tabs can be created with no JavaScript required, using the same simple, semantic structure, offering a native HTML solution for tabbed content. + +- **Generic blocks**: New generic blocks like `[.features]` allow you to easily define content blocks with nested sections. This takes advantage of the AST to create flexible layouts using native CSS features like flexbox and grid. Essentially, this provides a content-first approach to complex layouts, making it easier to organize and structure your content visually. + +- **Popovers**: You can now add popovers, overlays, and popover trigger buttons directly into your Markdown content using the `[button popovertarget]` and `[#my-overlay popover]` syntax. This allows for simple integration of interactive elements, such as tooltips or modal dialogs, without needing extra JavaScript or external libraries. + +- **`[table]` tag improvements**: The `[table]` tag has received several enhancements, including the ability to render external data and a more convenient syntax to handle tables with long columns, where traditional Markdown table syntax can be unwieldy. These updates make it easier to work with data-heavy tables and present them cleanly in your content. + + +### Performance +Marked is well-known for its performance, especially in handling basic Markdown parsing. However, Nue significantly outperforms Marked, offering around 20-50% faster processing for basic operations. As you introduce more complex features like tables, footnotes, and curly "smart" brackets, the performance gap becomes even more pronounced — with Nue being roughly 2-5 times faster in these scenarios. + +These performance improvements stem from Nue's more efficient internal architecture, designed to handle both simple and advanced Markdown features with minimal overhead. While these results come from basic, informal tests conducted on my laptop, I encourage you to test them for yourself. It would be great to see real-world benchmarks and eventually share them on this website to provide a more accurate comparison for the community. + + + +## Other improvements and changes +Full list of improvements and breaking changes on this release: + +[accordion] + + ## Improvements + + - **Web component fallbacks**: Interactive islands (client-side components) are now rendered as HTML custom elements, like ``. This allows Nue to first attempt to mount a custom component. If the custom component is not defined, it falls back to a standard Web Component, providing flexibility to choose the right technology for the task. + + - **New `.dhtml` suffix**: A new `.dhtml` file extension is introduced for interactive islands, complementing the existing `.htm` files. The deprecated `.nue` extension no longer works. This distinction clarifies the separation between interactive components and regular (server-rendered) HTML files. + + - **Markdown support**: Markdown extensions and custom components can now capture nested Markdown content using the `` element. This makes it easier to build reusable wrapper components that can enrich nested content and other components. + + - **Improved HMR error reporting**: YAML parsing errors are now displayed directly in the browser via the updated Hot Module Replacement (HMR) feature. With this improvement, developers can see parse errors related to YAML, JavaScript, CSS, and components right away, making the development process faster and more efficient. + + + + ## Breaking changes + + - **No more `[grid]` tag**: The `[grid]` tag has been removed. You can now achieve similar functionality using generic blocks like `[.features]`, and split inner blocks with `h2`, `h3`, or `---`. + + - **No more `[code]` tag**: The `[code]` tag has been removed. Extra configurations, such as line numbering, can now be specified directly after the fenced code operator. This simplifies the syntax and aligns more closely with standard Markdown. + + - **No more `[codeblocks]` tag**: The `[codeblocks]` tag has been removed as well. This functionality can now be mimicked using generic blocks, providing a cleaner and more modular approach to handling code blocks in your content. + + - **No more `[tabs]` tag**: The `[tabs]` tag is now replaced by the `[accordion]` tag. You can now use CSS to style the accordion elements and make them act as tabs, offering greater flexibility and control over the layout. + + - **Header and footer auto-generation removed**: The auto-generation of headers and footers (via the `header` and `footer` variables in `site.yaml`) has been removed due to the complexity it introduced. Instead, use regular layout modules with the `` tag to manually define these areas, giving you more control over their content and structure. + + - **`section_component` property dropped**: The `section_component` property has been dropped because it felt too "magical" and non-standard. It was inconsistent with the more straightforward, semantic approach that Nue embraces. + + - **`section_classes` property renamed**: The `section_classes` property has been renamed to `sections`. This change brings clarity and aligns with the overall goal of using clear, intuitive names for properties. + + - **"Complementary" layout module renamed to "beside"**: The "complementary" layout module has been renamed to "beside." This new name better reflects its purpose of providing additional content beside the main content area, enhancing readability and organization. + + - **`===` no longer works as a section separator**: The `===` syntax for section separation is no longer supported. Instead, use `---` for section separators, and `***` or `___` for thematic breaks (which correspond to the `
` tag in HTML). This change ensures consistency and follows standard Markdown conventions. + + - **`[image]` "srcset" removed**: The `srcset` and `sizes` parameters for the `[image]` tag have been removed due to their complexity. A simpler and more powerful system for responsive images is planned for future releases. + + - **`[video]` tag simplified**: The `[video]` tag has been simplified to only accept the `src` parameter, removing support for multiple video sources. A more robust video player and driver support system is coming in a future update, allowing for better video handling and greater flexibility. + + + diff --git a/packages/nuejs.org/blog/rethinking-reactivity/examples.nue b/packages/nuejs.org/blog/rethinking-reactivity/examples.dhtml similarity index 100% rename from packages/nuejs.org/blog/rethinking-reactivity/examples.nue rename to packages/nuejs.org/blog/rethinking-reactivity/examples.dhtml diff --git a/packages/nuejs.org/blog/rethinking-reactivity/index.md b/packages/nuejs.org/blog/rethinking-reactivity/index.md index a27c65ab..1cbb4a5a 100644 --- a/packages/nuejs.org/blog/rethinking-reactivity/index.md +++ b/packages/nuejs.org/blog/rethinking-reactivity/index.md @@ -23,7 +23,7 @@ Common questions are: "How does reactivity work"? and "How is this different fro [lobsters]: //lobste.rs/s/goxx8g/nue_react_vue_vite_astro_alternative ## HTML-based -If React is "Just JavaScript", then Nue is "Just HTML". Here's how the difference between React and Nue using an identical counter component: +If React is "just JavaScript", then Nue is "just HTML". Here's how the difference between React and Nue using an identical counter component: ### React @@ -159,7 +159,9 @@ Nue uses classes to bring the DHTML vibes back to modern component-based web dev The most notable thing is the ` + +``` +In this example, the `posts` variable is filtered to include only those entries with a specific tag (e.g., `design`). This approach lets you refine your displayed content dynamically within your custom component. \ No newline at end of file diff --git a/packages/nuejs.org/docs/content-syntax.md b/packages/nuejs.org/docs/content-syntax.md new file mode 100644 index 00000000..f5e26cbf --- /dev/null +++ b/packages/nuejs.org/docs/content-syntax.md @@ -0,0 +1,453 @@ + +# Content syntax +Nue provides an **extended Markdown syntax** for authoring rich web content. Building on top of basic Markdown, you can easily include complex elements like grids, responsive images, tables, accordions, tabbed content, and more. + + + +[image.bordered] + caption: Nue's extended Markdown syntax handles all varieties of content + small: /img/content-files.png + large: /img/content-files-big.png + size: 745 × 383 + +This allows you to rapidly create dynamic, responsive web pages without writing any additional code. Everything is managed through simple, versionable text files that can be edited directly on your file system with your favorite text editor. + + +## Basic syntax +Nue fully supports standard [Markdown](//daringfireball.net/projects/markdown/), allowing you to work with familiar formatting options like headings, quotes, lists, and fenced code blocks. Here's an example of the basic syntax: + +```md +# First level heading +A paragraph with **bold** and *italics* and `inline code` + +![An image](/path/to/image.webp) + +## Second level heading + +> Quoted text with a [Link to docs](/docs/) + +1. This here +2. is an ordered +3. list of items + +Followed with: + +- An unordered +- list of items + + +And a horizontal line: + +*** + +``` + +## Standard Markdown extensions +Nue supports common **Markdown extensions** such as **tables** and **footnotes**, allowing for more structured and informative content. + +### Tables +Tables are useful for presenting structured information. Here’s an example: + +```md +| Principle | Description | +|--------------------------|-------------------------------------------------------------------| +| Separation of Concerns | Dividing a system into distinct sections with specific roles. | +| Progressive Enhancement | Building core functionality first, then adding enhanced features. | +| Information Architecture | Structuring content for usability and navigation clarity. | +``` + +This table provides a simple breakdown of key principles. + +### Footnotes +**Footnotes** allow you to reference additional information or explanations without disrupting the main content flow. Here’s an example: + +```md +Design principles like Separation of Concerns [^1], Progressive Enhancement [^2], and Information Architecture [^3] are fundamental. + +[^1]: Separation of Concerns (SoC) is crucial for maintaining clean and maintainable code. +[^2]: Progressive Enhancement (PE) ensures that core functionality is available to all users, with enhanced features layered on. +[^3]: Information Architecture (IA) involves organizing content in a way that is intuitive and accessible for users. +``` + +Footnotes provide a way to include more detail without cluttering the main text. + + + +## Nue-specific things +Nue extends standard Markdown with additional formatting options and powerful features to make content richer and more dynamic, without needing complex HTML. + + +### No HTML allowed +In Nue, the focus is on **pure content**—free from HTML markup. This ensures that your content remains clean, semantic, and focused on structure, while design and styling are handled by CSS and layout modules. By separating content from presentation, Nue enforces the **Separation of Concerns (SoC)** principle, leading to better maintainability and a more consistent design system. + +Instead of embedding HTML, Nue provides powerful Markdown extensions like **blocks**, which let you create rich, styled content while keeping the content layer pure. + + +### More formatting optionms +Nue provides a variety of formatting options beyond standard Markdown, giving you more control over how text appears on the page. Here’s a comparison between the Markdown syntax and the corresponding HTML output: + + +[table] + Markdown | HTML | Example + ------ + `I'm **bold**` | `bold` | I'm **bold** + `I'm __bold__` | `bold` | I'm __bold__ + `I'm •bold•` | `bold` | I'm •bold• + `I'm *italic*` | `italic` | I'm *italic* + `I'm _italic_` | `italic` | I'm _italic_ + `I'm /italic/` | `italic` | I'm /italic/ + `I'm \`code\`` | `code` | I'm `code` + `I'm ~striked~` | `striked` | I'm ~striked~ + `I'm "quoted"` | `quoted` | I'm "quoted" + `I'm |marked|` | `marked` | I'm |marked| + + +This extended set of formatting options helps you achieve more **precise styling** without needing to write raw HTML. + + +### Variables +Nue allows the use of **variables** within Markdown files, enabling dynamic content based on your application data. Variables are wrapped in curly braces (`{}`) and will be replaced with their corresponding values when the page is rendered: + +```md +Package name: **{ package.name }** +Package version: **{ package.version }** +Complex value: **{ foo.bar[0].name }** +``` + +The values between curly braces are taken from the **application data** or **metadata** available on the page. This feature ensures that content can stay dynamic and up-to-date with the latest values from your site’s data and settings. + + +### Heading IDs +Enabling the `heading_ids: true` option in your configuration automatically generates anchor links for each heading. For example, a heading like: + +```md +## Less is More +``` + +is rendered as: + +```html +

+ + Less is More +

+``` + +This creates a clickable link for each heading, making it easy to navigate through your content. + +#### Explicit IDs and Class Names + +You can also define IDs and class names directly within the heading. For example: + +```md +## Less is More { #less.more } +``` + +This is rendered as: + +```html +

+ + Less is More +

+``` + +Here, the ID is set to `less`, and the class is set to `more`, providing more control over your heading's styling and link structure. + + + +### Expanded footnotes +Nue enhances the standard Markdown footnote functionality by allowing you to mark entire phrases as part of the footnote. This makes it easier to create footnotes that are more descriptive and visually clear. + +For example, instead of just marking a single digit, you can mark an entire phrase: + +```md +Design principles like [Separation of Concerns][^1], [Progressive Enhancement][^2], and [Semantic markup][^3] are fundamental. +``` + +This expanded capability allows you to reference full concepts or phrases, improving clarity in both technical and non-technical content, while maintaining the footnote's ease of use. + + +### Sections +You can split your content into sections with a triple dash `---` making your content render like this: + +``` +
+
+
+
+ ... +
+``` + +You can also generate the sections by setting `sections: true` in your configuration. This will generate a new section based on `

` headings so that each `

` tag starts a new section. + +[.note] + ### Horizontal rules + Please use `***`, `___`, or `- - -` to generate a horizontal rule (`
` tag). + +Sections are discussed in more detail in the [styling](styling.html#sections) document. + +### Blocks +**Blocks** in Nue are reusable chunks of content wrapped inside a class name, allowing you to build structured and styled sections while keeping the focus on pure content. No HTML is needed, making the content easy to manage and maintain. + +For example, here’s how to create a **"note" block**: + +```md stash +[block.note] + ### Note + Web design is 100% content and 95% typography +``` + +This generates a fully styled block while keeping the content clean and semantic. + +#### Why blocks are great +Nue’s block system promotes the **pure content** philosophy by: + +- **Keeping HTML out of Markdown**: Blocks allow you to maintain clean, readable content without the need for HTML markup, ensuring a pure content layer. +- **Promoting clean, reusable structure**: By focusing on content, blocks make it easy to reuse consistent structures across your site, supporting both scalability and a unified design. +- **Enforcing separation of concerns**: Blocks ensure that content remains focused on structure, while design and styling are applied externally via CSS, keeping the codebase clean and maintainable. + +#### HTML output +When rendered, blocks are transformed into simple `
` elements with an associated CSS class name. For example, the "note" block generates the following HTML: + +```html +
+

Note

+

Web design is 100% content and 95% typography

+
+``` + +This keeps the structure clean and semantic, while design is handled separately through CSS. + +#### Simplified syntax +You can further simplify the syntax by omitting the `block` component name and just using the class name prefixed with a dot: + +```md +[.alert] + ### Note + You should avoid inline styling like black death +``` + +### Popovers +Nue's block syntax makes it simple to create **popovers** that can be easily triggered using a [button](content-tags.html#button) tag. Popovers are a great way to present additional information without cluttering the main content flow. + +Here’s how you define a popover: + +```md +[#soc-explainer popover] + ### Separation of concerns + **Separation of Concerns (SoC)** is a core principle in software and web development that promotes dividing functionality into distinct, independent sections. In web design, this means keeping content, structure, and styling isolated. By doing this, content creators can focus purely on the message and information, while designers and engineers handle the layout and styling. This approach leads to cleaner, more maintainable codebases and a better user experience. +``` + +This generates a `dialog` element with the standard `popover` attribute, which can be opened with a button: + +```md +[button popovertarget="soc-explainer" "Learn how it works"] +``` + +This button is linked to the popover and opens it when clicked. + +#### Why this setup is great: +1. **A new creative tool for content authors**: Popovers offer a fun, engaging way to display additional information without overwhelming the reader. Imagine creating Apple-like, sleek front-page dialogs that feel immersive, but with full SEO compatibility and a clean, content-first file. Popovers let content authors introduce rich interactive elements while maintaining complete control over the content flow. + +2. **No JavaScript needed**: You can wire up app-like dialog and popover functionality directly within your content using standard HTML attributes, without writing any JavaScript. This makes your content more accessible, reliable, and easy to manage. + +3. **Standards-based approach**: Nue uses the standard [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API/Using), allowing the browser to handle the heavy lifting for opening, closing, and accessibility. This ensures that the popovers work even if JavaScript is disabled or fails to load, + + making your content resilient and SEO-friendly. + +### Complex layouts +Nue's Markdown parser automatically identifies when multiple blocks of content are placed within a single block tag and separates them using `
` tags. This makes it easy to create complex layouts directly in your content. For example, the following block: + +```md +[.stack] + ### Design + Design starts with content, ensuring a natural flow and seamless navigation. + + ### Engineering + Engineering focuses on performance, accessibility, and progressive enhancement. +``` + +Is rendered as: + +```html +
+
+

Design

+

Design starts with content, ensuring a natural flow and seamless navigation.

+
+
+

Engineering

+

Engineering focuses on performance, accessibility, and progressive enhancement.

+
+
+``` + +And when styled with CSS, it takes on a visually structured layout: + +[render] + +#### Separator +Nue automatically uses the first `h2` or `h3` tag within a block as the **separator** for the content blocks. If needed, you can use a **triple-dash** (`---`) as an explicit separator to customize content divisions. + +For example: + +```md +[.grid] + ### Design + Design blends form and function. + + --- + + ### Engineering + Code enhances the user experience while staying performant. +``` + +This allows flexible layout creation, giving you control over content structure and flow. + +#### Why this is great + +1. **Create complex layouts with pure content**: There's literally zero bloat or extra markup needed to achieve advanced layouts such as **flex** or **grid**. Your design system’s CSS components handle the layout, keeping your Markdown clean and focused on content. + +2. **Supports rich, flexible designs**: You can easily render complex, visually engaging layouts like **bento-style cards** that mix videos, images, and text—**optionally enhanced** with scripting and motion. This flexibility allows you to create stunning, content-rich sections that work seamlessly across devices, without touching a line of HTML. + +### Nesting +Blocks can be nested to form more complex layouts on your richer marketing and landing pages, giving you the flexibility to create structured, multi-layered content without ever touching HTML. For example: + +```md +[.feature] + ## Hello, World! + Let's put a nested stack here + + [.stack] + ### First item + With description + + ### Second item + With description +``` + +This creates a flexible layout where a main **feature** block contains a nested **stack**, allowing for clean organization and structure in your content. + +The possibilities are endless. You can combine blocks in creative ways, stacking sections within sections to build rich, interactive landing pages. Imagine a **hero section** that introduces key features, followed by a **grid of cards**, each with its own stacked content blocks highlighting product details, testimonials, or case studies. With the power of **nesting**, you can craft visually complex layouts while keeping your Markdown easy to read and maintain. Whether you're building product showcases, multi-section promotional pages, or detailed service breakdowns, nesting unlocks a new level of creative control over your content structure—letting design systems handle the visual complexity. + +### Code blocks +Code blocks in Nue are enclosed between triple backticks and can include an optional language hint for syntax highlighting using the [Glow syntax highlighter](blog/introducing-glow/). For example, a CSS code block would look like this: + +```md +\```css +// here is a CSS code block +:root { + --base-100: #f3f4f6; + --base-200: #e5e7eb; + --base-300: #d1d5db; + --base-400: #6b7280; +} +\``` +``` + +The language hint (`css`) enables syntax highlighting for the specified language. + +#### Line numbering +You can also apply custom class names and enable **line numbering** for your code blocks. Here's how to set it up: + +```md +\``` •.purple numbered• +function hello() { + // world +} +\``` +``` + +The above example will be rendered with **purple text** and line numbers enabled: + +``` .purple numbered +function hello() { + // world +} +``` + +### Line/region highlighting +Nue allows you to highlight specific lines or regions within your code blocks using special characters. This feature helps emphasize key parts of your code, making it easier for readers to focus on important areas. + +Here’s an example using JavaScript code with line numbering and highlights: + +```js numbered +/* Code highlighting examples */ + +>Highlight lines by prefixing them with ">" + +Here's a •highlighted region• within a single line + +// bring out errors +export ••defaultt•• interpolate() { + return "something" +} + +// prefix removed lines with - +-const html = glow(code) + +// and added lines with + ++const html = glow(code, { •numbered: true• }) +``` + +#### Highlighting options +Use the following characters to customize how code lines and regions are highlighted: + +- `>` highlights an entire line with a default blue background. +- `-` marks the line as **removed** with a red background. +- `+` marks the line as **inserted** with a green background. +- `|` highlights lines in Markdown syntax (similar to `>`). +- `\` escapes the first character to prevent special treatment. + +To highlight specific text regions within a line, use the bullet character (`•`). For example: + +```md +These •two words• are highlighted and ••these words•• are erroneous +``` + +This would be rendered as: + +These **two words** are highlighted and **these words** are erroneous. + +#### Mixing content blocks and code blocks +Here's an example combining content blocks and code blocks. Notice how clean the syntax is, avoiding excessive coding and ugly markup that often comes with complex layouts: + +```md +[.stack] + + ### CSS animation setup + + ``` .pink + @keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + + .animate { + animation: fadeIn 2s ease-in; + } + ``` + + ### CSS transition setup + + ``` .blue + .button { + transition: background-color 0.3s ease; + } + + .button:hover { + background-color: #ff4081; + } + ``` +``` + +This simple example demonstrates how you can create a **stacked layout** with content and code blocks, all within a clean, readable format. Mixing content and code blocks in this way allows you to present complex technical concepts, tutorials, or style guides without sacrificing readability or maintainability. + + +### Content tags +Nue offers a large amount of [tags](/content-tags.html) that significantly enhance your ability to create rich and interactive websites. You can add responsive images, videos, buttons, accordions, tabs, and more. + +You can also extend the syntax with [custom components](custom-components.html). + diff --git a/packages/nuejs.org/docs/content-tags.md b/packages/nuejs.org/docs/content-tags.md new file mode 100644 index 00000000..91e2bb8a --- /dev/null +++ b/packages/nuejs.org/docs/content-tags.md @@ -0,0 +1,646 @@ +--- +include: [tabs] +--- + +# Content tags +Nue offers a powerful set of built-in Markdown extensions, referred to as "tags," which significantly enhance your ability to create rich and interactive websites. + + +## Tag syntax +Tags are defined within square brackets. For example, to include an image, you would write: + +```md +[image /img/cat.png] +``` + +The tag name (in this case, "image") can be followed by various options, which can be supplied in several ways: + +### Named options + +You can specify options using named attributes. For example: + +```md +[image src="hello.png" caption="Hello, World" alt="Hello image"] +``` + +### Nested YAML + +Alternatively, you can use nested YAML to define the attributes: + +```md +[image] +| caption: Hello, World! +| large: hello-big.png +| small: hello.png +| alt: Hello Image +``` + +### Plain values + +You can also use plain values without specifying attribute names: + +```md +[image hello.png] +``` + +### ID and class name + +To set ID and class attributes, you can use the following syntax: + +```md +[image#hero.massive /home/hero.webp] +``` + +### Nested content + +Tags can also include nested content. Here’s how you can add a caption for an image: + +```md +[image explainer.png] +| This nested content is the caption for the image. +| You can add Markdown here like *emphasis* and `inline code`. +``` + +### Inline tags + +Finally, tags can be used inline as well. For instance, to add an inline SVG image, you can write: + +```md +This is an inline [svg "/icon/meow.svg"] image. +``` + +- - - + + + +## Images +To include a basic image, use: + +```md +[image hello.webp] +| This content here is the caption. Markdown *formatting* is supported. +``` + +You can also use a shortcut alias (`!`): + +```md +[! hello.webp] +``` + +### Image links + +Nue allows you to link images to specific URLs using the `href` attribute, making them interactive: + +```md +[image book.svg] + caption: View documentation + href: /docs/ +``` + +This functionality enables you to guide users to additional resources or pages while providing visual context. + +### Responsive images + +Nue supports art direction for images, allowing you to specify different sizes based on screen dimensions. This feature ensures that users receive the best possible image for their device: + +```md +[image] + large: ui-wide.png + small: ui-tall.png + caption: This is the image caption + alt: This is the alt text + loading: eager +``` + +The `large` and `small` attributes can define images with varying aspect ratios, optimizing the visual experience on different devices. + +[.options] +#### Image options + - `alt` - Alternate text for accessibility, describing the image's content. + - `src` - Source URL of the image file. + - `caption` - Provides context for the image, enhancing user understanding. + - `href` - Adds a clickable link to the image, guiding users to related content. + - `large` - Specifies the large version of the image, which can differ in aspect ratio from the small version, allowing for creative art direction. + - `loading` - Determines the loading behavior: "lazy" enables [lazy loading](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading) (default value) or "eager" for immediate loading. + - `small` - Defines the small version of the image for mobile displays, optimizing visual presentation. + - `offset` - Sets the screen size (in pixels) at which the small image switches to the large one, with a default of 750 pixels. + - `size` - A shortcut property to provide both width and height simultaneously, formatted as "width x height". + - `width` - Specifies the width of the image for precise layout control. + +### HTML output + +When rendered, images appear as `
` elements, enhancing the semantic structure of your content. For example, a simple image with a caption is rendered as: + +```html +
+ +
This content here is the caption.
+
+``` + +A more complex example with `href`, `small`, and `large` attributes looks like this: + +```html +
+ + + + + +
+``` + +### Inline SVG + +Inline SVG images can be added using the following syntax, which renders rich vector graphics directly within your content: + +```md +[svg /icon/continue.svg] +``` + +This feature allows you to incorporate sharp, scalable images into your design easily. It’s especially useful when paired with other tags, such as buttons: + +```md +[button href="/docs/"] + *Learn more* [svg /icon/chevron-right.svg] +``` + +This renders as follows, enabling smooth integration with CSS styling: + +```html + + Learn more + ... + +``` + + +## Videos +The video tag in Nue serves as a content-focused interface for the standard HTML5 video tag, allowing you to easily incorporate video content into your pages. It is used as follows: + +```md +[video /videos/hello.mp4] +``` + +You can also use a shortcut alias (`!`): + +```md +[! /videos/hello.mp4] +``` + +All standard HTML5 video attributes are supported, providing flexibility and control over video playback: + +```md +[video intro.mp4 autoplay controls muted loop] +``` + +Options can also be specified using YAML for enhanced organization: + +```md +[video.heroic] + poster: hello.png + src: hello.mp4 + width: 1000 +``` + +[.options] + #### Video options + + - `autoplay` - Starts the video when the page loads, providing an engaging experience right away. Must be used together with `muted` for autoplay to work across all browsers. + - `controls` - Displays the built-in video controls provided by the browser, allowing users to play, pause, and adjust the volume. + - `loop` - Makes the video restart automatically after it reaches the end, which is useful for continuous playback in presentations or promotional content. + - `muted` - Plays the video without sound, enabling autoplay in browsers that restrict sound playback. + - `poster` - A URL for an image that will be displayed before the video begins playing, giving users a preview of the content. + - `preload` - Provides a [hint to the browser](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#preload) about whether to load the video data before the user plays it. + - `src` - Specifies the URL to the video file, directing the browser to where it can retrieve the video content. + - `width` - Determines the width of the video player, allowing for precise control over the layout. + +### HTML output + +When rendered, videos appear as a native HTML5 video element, enabling smooth integration into your web pages. For example, the following code: + +```html + +``` + +## Buttons + +Buttons in Nue are marked with a specific tag syntax to create interactive and accessible button elements directly from Markdown. This tag makes it easy to create buttons without requiring HTML or JavaScript, allowing for clean and maintainable content. + +### Basic button + +Buttons are marked as follows: + +```md +[button label="Learn more" href="/docs/"] +``` + +The `button` tag allows you to specify various options to control the appearance and behavior of the button. In this example, the `label` and `href` attributes are used to define the button text and target link respectively. + +### Concise syntax + +Instead of using the `label` attribute, you can provide the button text directly as a plain value. This is a more concise way to define a button label: + +```md +[button "Learn more" href="/docs/"] +``` + +### Styled button + +You can include a class name to style the button differently, using a nested body to define the button label. In this example, `.secondary` indicates a secondary button style, and the button label is provided as nested content: + +```md +[button.secondary href="/docs/"] + Explore the docs +``` + +### Button with inline SVG + +Buttons can include inline SVG elements to provide additional visual cues, such as icons. In this example, the button includes a right-pointing arrow icon to indicate navigation: + +```md +[button href="/docs/"] + *Learn more* [svg /icon/chevron-right.svg] +``` + +### Button triggering a popover + +Buttons in Nue can also be used to trigger popovers, allowing for additional content or explanations without cluttering the main page: + +```md +[#info-popover popover] + ### More information + This popover provides extra details about the feature without leaving the current page. + +[button popovertarget="info-popover" "Learn more"] +``` + +This example creates a button labeled "Learn more" that opens a popover with extra information. + +[.options] + #### Button options + + - `label` The text label for the button. This can also be provided as a plain value or defined within the body content. + - `href` The target link for the button. This is the URL the button will navigate to when clicked. + - `popovertarget` The ID of the popover to trigger when the button is clicked. This allows buttons to open additional content in popovers. + + +### HTML output + +Buttons are rendered as follows: + +```html +Learn more +``` + +Buttons are essentially links (`` elements) with a `role="button"` attribute. This ensures accessibility and allows the button to be styled and function like a traditional button while keeping the implementation simple and lightweight. When used with popovers, the button is rendered as a ` +``` + + +## Tables +Nue supports standard Markdown syntax for tables, providing a straightforward way to create structured data presentations. + +### Standard tables + +Tables are created using the standard Markdown table syntax, which is simple but sometimes cumbersome when dealing with larger tables. Here is an example: + +```md +| Name | Email | Work title | +| --------------- | ------------------------ | ------------------ | +| Sarah Thompson | sarah.thompson@demo.ai | Graphic Designer | +| David Rodriguez | david.rodriguez@demo.ai | Financial Analyst | +| Jessica Lee | jessica.lee@demo.ai | Project Manager | +``` + +### Table tag + +Nue offers a specialized `[table]` tag for defining tables with less syntax noise compared to the traditional pipes and dashes. This approach improves readability and makes table creation faster and more intuitive: + +```md +[table] + Name | Email | Work title + Alice Johnson | alice.johnson@demo.ai | Marketing Manager + John Smith | john.smith@demo.ai | Software Engineer + Emily Davis | emily.davis@demo.ai | Human Resources Lead + Michael Chen | michael.chen@demo.ai | Sales Representative +``` + +### Advanced table features + +With Nue's `[table]` tag, you can add extra features such as captions, header rows, and footer rows. You can also place longer cell content on separate lines, which greatly enhances readability, especially for complex tables: + +```md +[table caption="Design Principles"] + + Principle | Acronym | Description + ----- + Separation of Concerns | SoC + Dividing a system into distinct sections with specific roles. + + Progressive Enhancement | PE + Building core functionality first, then adding enhanced features. + + Semantic Web Design | SWD + Emphasizing meaning and accessibility through proper HTML semantics. + + Content First | CF + Prioritizing content in the design and development process. + ----- + + These principles help create better and more maintainable web projects. +``` + +By placing longer descriptions on separate lines, the table becomes much easier to read and manage, especially when handling content that spans multiple lines. + +### Rendering YAML data + +You can also refer to table data available on the page by using the `:items` attribute: + +```md +[table :items="products"] +``` + +This allows you to render tables dynamically based on the structured data available in global, area, or page scopes. Here the "products" property is used to access the data. + +[.options] + #### Table options + + - `caption` - Defines a caption for the table, which appears above the table in HTML. + - `head` - Determines whether the first row should be rendered as a table head (`` elements). The default is `true`. + - `:items` - Specifies the property name for externally defined table data that should be rendered. + - `wrapper` - Wraps the table inside a parent element with a class name specified on this property, typically used for adding colored backgrounds or other design elements from your design system. + + +### HTML output +Tables are rendered as standard HTML5 tables, ensuring compatibility and accessibility across all browsers. Here is an example output: + +```html + + + + + + + + + + + + + + + + + ... + + + + + + + +
Design Principles
PrincipleAcronymDescription
Separation of ConcernsSoCDividing a system into distinct sections with specific roles.
+ These principles help create better and more maintainable web projects. +
+``` + +This structure includes a `` for the table title, a `` for the headers, a `` for the main content, and a `` for any footer notes or summaries, ensuring that your tables are fully semantic and accessible. + + + +## Accordions +Accordions in Nue make it easy to create collapsible content sections, perfect for FAQs or structured information that benefits from an expandable layout. + +The basic structure for creating an accordion is as follows: + +```md +[accordion] + ## First element + The contents of the first element + + ## Second element + The contents of the second element + + ## Third element + The contents of the third element +``` + +This Markdown generates an accordion with three entries, each with a heading and corresponding content, allowing for a clean presentation. + +### Example with styling + +You can apply a specific class for styling. Here’s an example using `.card` to create a card-like appearance: + +```md +[accordion.card] + ## First element + The contents of the first element + + ## Second element + The contents of the second element + + ## Third element + The contents of the third element +``` + +### Defining separators + +Nue uses the first heading element (`h2` or `h3`) to create new accordion entries automatically. Alternatively, use the triple-dash (`---`) separator to define new entries explicitly: + +```md +[accordion] + ## First element + The contents of the first element. + + --- + + ## Second element + The contents of the second element. +``` + +### Accordion options + +Accordions can be customized with various options to control their behavior and appearance: + +[.options] + #### Accordion options + + - `name` - Use this to name individual entries. When supplied, only one entry can be open at a time, ensuring a true accordion effect. + - `open` - Set the initial state of the accordion. Use this to open the first tab by default or provide a numeric value for a specific item to be initially opened. + + +### HTML output + +Accordions in Nue are rendered using the native HTML5 `
` and `` elements: + +```html +
+
+ First element +

The contents of the first element

+
+
+ Second element +

The contents of the second element

+
+
+ Third element +

The contents of the third element

+
+
+``` + +The `
` and `` elements ensure compatibility and accessibility across modern browsers while keeping your content performant and easy to use, without additional JavaScript. + + +## Tabs + +Tabbed panes in Nue are created using `[accordion]` elements with `name` and `open` attributes but styled to look and function like tabs: + +```md +[accordion.card.tabs name="tabs" open] + ## First element + The contents of the first element + + ## Second element + The contents of the second element + + ## Third element + The contents of the third element +``` + + +### Example rendering +The above markup generates a series of tabbed sections, with each heading acting as a tab that can be selected to reveal the associated content pane. + +[accordion.card.tabs name="tabs" open] + ## First element + The contents of the first element + + ## Second element + The contents of the second element + + ## Third element + The contents of the third element + + +### CSS for tabs + +The CSS below is used to style the above example, transforming the accordion into tabbed navigation: + +```css +.tabs { + position: relative; + display: flex; + height: 150px; + gap: 1.5em; + + /* tabs */ + summary { + cursor: pointer; + font-weight: 550; + &::marker { font-size: 0 } + &:hover { color: var(--main-600) } + } + + /* tab panes */ + div { + inset: 3em 0 0 1.5em; + position: absolute; + } + + /* active tab */ + [open] summary { + pointer-events: none; + text-decoration: 3px underline var(--main-500); + text-underline-offset: 10px; + } +} +``` +The use of `pointer-events: none;` ensures that only the active tab can be interacted with, creating a smooth user experience without extra JavaScript logic. Additionally, pseudo-elements like `&::marker` help to refine the visual style without modifying the HTML. + +### Why this is good + +- **Works Without JavaScript**: All the tab functionality is implemented purely using CSS. The tabs continue to work seamlessly even if JavaScript is disabled or not available in the browser. + +- **No Extra HTML/JS Coding**: You don't need to write new JavaScript functions or HTML structures—everything is done by simply adding styles to the existing markup. + +By creatively using CSS, Nue makes it easy to craft responsive, interactive tabbed interfaces while keeping the underlying codebase clean and maintainable—proving that CSS alone can create sophisticated UI elements. + + +## Description lists + +Description lists are standard [HTML constructs](//developer.mozilla.org/en-US/docs/Web/HTML/Element/dl) that group terms and their descriptions. They are commonly used for glossaries or key-value pair lists. + +The basic structure for creating a description list in Markdown is as follows: + +```md +[define] + ## First item + Description of the first item + + ## Second item + Description of the second item + + ## Third item + Description of the third item +``` + +### HTML output + +Description lists are rendered using the native HTML5 `
`, `
`, and `
` elements: + +```html +
+
First item
+
Description of the first item
+ +
Second item
+
Description of the second item
+ +
Third item
+
Description of the third item
+
+``` + +### Defining footnotes + +You can also use the `define` tag to create footnotes, which provides a cleaner and more readable alternative to the standard `[^label]:` syntax. For example: + +```md +[define] + ## Separation of concerns { #soc } + A strategy for clean and maintainable code + + ## Progressive Enhancement { #pe } + Setup core functionality first and enhance it later + + ## Form follows function { #fff } + Make styling follow your content +``` + +Once defined, you can refer to these descriptions like this: + +```md +Separation of concerns [^soc] is an important strategy. + +[Progressive Enhancement][^pe] is good for UX. +``` + + + + + + + + + diff --git a/packages/nuejs.org/docs/content.md b/packages/nuejs.org/docs/content.md index 4735c561..3f388df3 100644 --- a/packages/nuejs.org/docs/content.md +++ b/packages/nuejs.org/docs/content.md @@ -1,160 +1,171 @@ -# Content development -Nue has a powerful content authoring syntax for marketers, copywriters, and technical writers. You can rapidly assemble complex web pages without ever touching a single line of code or the need to set up complex cloud-based content database systems—just simple versionable text files, directly accessible on your file system, and editable with your favorite editor. +# Content-first approach in Nue -[bunny-video] - videoId: 3bf8f658-185a-449c-93b9-9bd5e1ad0d05 - poster: /img/nuemark-splash.jpg +Content is **first** in Nue. It shapes how your site is built, ensuring that design evolves around content. This approach aligns with the principle of progressive enhancement, ensuring that the structure, functionality, and design follow from the content. -## Extended Markdown -The content authoring syntax is based on **Markdown**, but extends its capabilities to make it suitable for creating rich web pages. It supports sections, content blocks, grids, stacked layouts, responsive images, videos, tabs, and more. All your content, from simple blog entries to rich landing pages is editable by non-technical people. +[image] + small: /img/content-development.png + large: /img/content-development-big.png + caption: Website development starts from content design -Thanks to universal hot-reloading, the content authors can see the results in real time as they edit the content. -Think Nue like **WordPress**, **Notion**, but the content is editable with your favorite text editor and the results are compatible with your design system. The versatile syntax allows you to build complex landing pages comparable to what you can create with online authoring tools like **Framer** or **WebFlow**. +## Structured data +Structured data in Nue includes: +- **Information architecture**: Defines how content is organized, ensuring clear navigation and user flow. +- **Content metadata**: Includes essential details like title, description, author, date, and Open Graph (OG) images for each content piece. +- **Application data**: Covers specific data used across the site, such as products, team members, language tokens, etc. +- **Content collections**: Auto-generated [content collections](content-collections.html) based on metadata, typically used for page lists, like blog posts displayed on an index page. -## Syntax -Nue offers a full [Markdown support](//daringfireball.net/projects/markdown/). That is: All the familiar things like headings, quotes, lists, and fenced code blocks are supported: +By separating structured data from the layout, your design modules stay clean and focused on **structure, semantics**, and **accessibility**. -```md -# First level heading -A paragraph with **bold** and *italics* and `inline code` +### YAML format +Nue uses **YAML** for structured data because of its simplicity, readability, and flexibility. YAML’s syntax is easy to write and understand, which makes it a popular choice for both developers and content editors. Here’s an example that defines a navigation structure in Nue: -![An image](/path/to/image.webp) +```yaml +Building websites: + - Step-by-step tutorial: tutorial.html + - Project structure: project-structure.html + - Content: content.html +``` -## Second level heading +YAML excels at handling complex structures, such as **nested arrays**, which is ideal for organizing complex datasets in Nue. Unlike alternatives like **TOML**, YAML’s expressive syntax makes it easier to represent hierarchical data. Additionally, YAML has resolved it's infamous issues, such as the **"Norway problem"**. -> Quoted text with a [Link to docs](/docs/) -1. This here -2. is an ordered -3. list of items +## Information architecture { #ia } -Followed with +**Information architecture** defines the relationships between pages, sections, and content. This hierarchy determines how users interact with your site and how content is presented. This website, for example, uses a global [navigation.yaml](//github.com/nuejs/nue/blob/dev/packages/nuejs.org/%40global/navigation.yaml) to define the site's navigation. It looks like this: -- An unordered -- list of items +```yaml +globalnav: + + # master navigation + main: + - Home: / + - Docs: /docs/ + - Blog: /blog/ + ... + + # toolbar + toolbar: + - Feed: //x.com/tipiirai + ... + + # "burger menu" + menu: + - Home: / + - Docs: /docs/ + - Blog: /blog/ + +documentation: + + Getting started: + - Why Nue: /docs/ + - How it works: /docs/how-it-works.html + - Installation: /docs/installation.html + - ... -\```js numbered -// here is a javascript code block -function hello() { - return "world" -} -\``` ``` - -### Front matter -You can pass optional [settings and metadata](settings.html) in the "front matter" section of the page. This is the first thing in your file between triple-dashed (`---`) lines taking a valid YAML format: +This data is used by [layout modules](layout.html) to generate elements like menus, sidebars, and breadcrumbs. -```yaml -\--- -title: Page title -desc: Page description for search engines -og: /img/hero-image.png -\--- -``` +## Content metadata +**Metadata** like titles, descriptions, authors, and OG properties can be defined at three levels: global, app, and page. This flexible, layered approach ensures consistency while allowing overrides when necessary. -### Sections -Just like books can be divided into chapters, your long-form articles and landing pages are often divided into sections. These sections are separated with three or more dashes (`---`) or equals (`===`) characters: +### Global data +At the **global level**, metadata is defined in the `site.yaml` file. For example, a `title_template` can dynamically format page titles: ```yaml -\--- -title: Page with sections -\--- +# In site.yaml +title_template: "%s / Nue Framework" +description: "The design engineering framework for the web" +``` + +This structure ensures that all pages follow the same title format. -# The hero section -With an epic subtitle +### Application data -\===== +At the **app level**, you can override the global metadata for specific apps, like a blog: -## A second section -With another great subtitle +```yaml +# In blog/blog.yaml +title: "Blog" +title_template: "%s / Blog" +description: "Latest news" +author: "John Doe" +og_image: "/images/blog-og.jpg" ``` +### Page data -### Tags -Nue comes with a set of [built-in tags](tags.html) for responsive images, videos, tables, code blocks, and more. The syntax takes the form of `[tagname]`. For example: +At the **page level**, metadata can be further customized via front matter in Markdown files: -```md -[video explainer.mp4] +```yaml + # In a page's front matter (Markdown) + --- + title: "Getting Started with Nue" + description: "Learn the basics of setting up your first project" + --- ``` -Tags are like WordPress short-codes, but the syntax is simpler and less verbose. +### Summary: data inheritance +As you move from the site level to the page level, the data gets extended or overridden, allowing for granular control over content and settings. +[image.gridpaper] + small: /img/data-propagation.png + large: /img/data-propagation-big.png + caption: Data inheritance in Nue -### Blocks -Blocks are chunks of content with an alternate styling. Think of highlighted content like tips, notes, and alerts. The syntax takes the form of `[.classname]`. For example: -```md -[.note] - ### Content is king - Web design is 100% content and 95% typography -``` -The "note" class must be specified on your design system and the design should be implemented in the website's CSS. +## Unstructured content -### Stacks -Block content can be divided into multi-block layouts where each item is separated with a triple-dash. For example: +Unstructured content in Nue varies in terms of its **richness and interactivity**: -```md -[.stack] - ## First item - With content +- **Technical content**: Includes detailed information with lists, tables, and code blocks. +- **Marketing content**: Visual-heavy content like landing pages and promotional sections, often including images, videos, and interactive elements. +- **Blog content**: A mix of technical and interactive content, typically starting with a hero section. +- **Formal content**: Plain text like legal documents or policies. - --- - ## Second item - With content -``` +### Extended Markdown -Again, the name "stack" must be implemented in your website CSS by the UX developer. +Nue supports an [extended Markdown syntax](content-syntax.html) that handles all these types of content seamlessly, including code blocks, tables, complex layouts, tabs, and interactive elements. It remains **SEO-friendly** and fully **accessible**. +[image.bordered] + caption: Nue's extended Markdown syntax handles all varieties of content + small: /img/content-files.png + large: /img/content-files-big.png + size: 745 × 383 -### Grids -[Grid](tags.html#grids) is a built-in tag used like the stack but is meant for more complex layouts. They have more configuration options for their visual appearance and behavior can be enhanced with JavaScript. -```md -[grid] - ## First item - With content +### Cloud storage - --- - ## Second item - With content +While the current YAML/Markdown-based editing is well-suited for technical users, we recognize the need for a cloud storage solution for non-technical users. This will allow content management directly through the website. A cloud-based backend is on our roadmap, aligning with Nue's **decoupled architecture**, where content can be fetched from the cloud before rendering. See the [roadmap](index.md#roadmap) for more details. - --- - ## Third item - With content -``` +## Content-first development +**Content-first development** is a central philosophy in Nue, ensuring both engineers and designers focus on the content, guiding the structure, functionality, and design of the site. -### Nesting -Blocks, stacks, and grids can be nested to form more complex layouts on your richer marketing/landing pages: +### For engineers: Semantic web development +For engineers, content-first means starting with **semantic HTML**, using content information to create a solid, accessible foundation. This approach emphasizes **progressive enhancement**, where functionality and design are layered on top of this foundational HTML. -```md -[.feature] - ## Hello, World! - Let's put a nested stack here +This method promotes **separation of concerns**, keeping content, layout, and behavior distinct. It improves **SEO** and **accessibility**, ensuring that content is clear to both search engines and assistive technologies. - [.stack] - ### First item - With description - --- - ### Second item - With description -``` +### For designers: Content-first design + +Designers also follow a content-first approach, where the content shapes layout and design decisions. By adhering to **form follows function**, design systems are created with a focus on the content’s needs, ensuring they are efficient and easy to maintain. +This leads to **lean** and **flexible** design systems, delivering intuitive, content-driven user experiences. +### Design engineering -### Custom tags -Your UX developer can easily [extend](custom-layouts.html#custom-md) the Markdown vocabulary with new tags that operate on the server-side, client side, or both. Ask the UX developer for the list of available extensions. +The content-first approach balances **performance**, **maintainability**, and **design**. It encourages collaboration between engineers and designers to build fast, accessible, and beautiful websites. diff --git a/packages/nuejs.org/docs/core-components.md b/packages/nuejs.org/docs/core-components.md new file mode 100644 index 00000000..4bba11d9 --- /dev/null +++ b/packages/nuejs.org/docs/core-components.md @@ -0,0 +1,177 @@ + +# Core components +This section outlines the built-in server-side components in Nue, designed to assist you with common layout tasks. + +## Navi +Navigation is a fundamental aspect of web design, including elements like global headers, footers, sidebars, and burger menus. These navigational elements serve as wrappers for links that guide users through your site. The `` tag is a useful utility for rendering these links based on the data defined in your [information architecture](content.html#ia). Here’s an example of how to create a header with navigation: + +```html +
+ + + Logo + + + + +
+``` + +### Example navigation data +Here’s how you can define the navigation data in a `site.yaml` file: + +```yaml +mastnav: + - Documentation: /docs/ + - About: /about/ + - Blog: /blog/ + - "v1.0 is out!": /blog/v1.0/ "badge" +``` + +When this data is utilized, your header will render as follows: + +```html +
+ + Logo + + +
+``` + +In this example, a quoted string ("badge") after the URL will be converted into a class name for the corresponding link. + +### Hierarchical navigation +You can also supply hierarchical data for the `` tag, which helps create structured navigation menus: + +```yaml +footer: + Product: + - Download: /download/ + - Features: /features/ + - Pricing: /pricing/ + - Docs: /docs/ + + Company: + - About us: /about/ + - Blog: /blog/ + - Careers: /careers/ + - Customers: /customers/ +``` + +When you use ``, it renders the following structure: + +```html +
+ + + +
+``` + +### Adding images and buttons +You can also include images and buttons in your navigation configuration. Here’s an example: + +```yaml +main_navigation: + - image: /img/logo.png + class: logo + url: / + + - Product: /product/ + - Pricing: /pricing/ + - FAQ: /faq/ + + - url: /get-started/ + label: Get Started + role: button +``` + +### Why this is good +Using the `` component offers several advantages: + +- **Centralized management**: It allows you to manage your information architecture from a single location, making updates easier and more consistent. +- **Clean layout modules**: Your layout modules remain simple and easy to read, as the navigation structure is automatically generated from your defined data. + +## Markdown +The `` component renders a Markdown-formatted string provided in the `content` attribute. This feature enables you to use Markdown in your metadata — typically for titles and descriptions — and then render it as HTML within your layout modules: + +```html + +``` + +## Pretty-date +The `` component displays a formatted date value provided in the `date` attribute. This is particularly useful in blogging areas and helps present dates in a more user-friendly manner: + +```html + +``` + +Here’s an example of a "hero" area for a blog entry that utilizes both the `markdown` and `pretty-date` components: + +```html +
+ + +

+ +
+ +
+
+``` + +## Table of contents +The built-in `` component automatically generates a table of contents from the current Markdown document, focusing on its second and third-level headings (h2 and h3). This tag works seamlessly for all your Markdown files without requiring additional setup: + +```html + +``` + +The HTML output will resemble the following: + +```html + +``` + + +## Page list +The built-in `` tag allows you to render a list of pages, such as blog entries or other index content, directly within your templates. This tag is ideal for creating dynamic lists that automatically update as you add new content to your site. + +To see how to collect and customize the pages in your list, refer to the [content collections](content-collections.html) documentation. + + + + diff --git a/packages/nuejs.org/docs/css-best-practices.md b/packages/nuejs.org/docs/css-best-practices.md index abd09bfe..0b75eb18 100644 --- a/packages/nuejs.org/docs/css-best-practices.md +++ b/packages/nuejs.org/docs/css-best-practices.md @@ -1,4 +1,11 @@ +# DEPRECIATED: + +See [Styling](styling.html) + +=== + + # Nue CSS best practices This document describes the best practices for writing clean CSS that is easy to maintain and scale. This is a result of decades of coding with CSS and HTML. @@ -128,25 +135,26 @@ Simple selectors make your CSS easy to read and maintain. They keep your file si ## Write clean HTML { #clean-markup } Avoid using unnecessary divs, spans and class names in your [custom layouts](custom-layouts.html): -[code.bad caption="Unnecessary divs and class names"] -
-
- -
-
-

ChitChat

-

You have a new message

-
+```html.bad "Unnecessary divs and class names" +
+
+ +
+
+

ChitChat

+

You have a new message

+
+``` Instead, you should write clean and semantic HTML: -[code.good caption="Clean, semantic markup"] -
-

ChitChat

-

You have a new message

-
- +```html.good "Clean, semantic markup" +
+

ChitChat

+

You have a new message

+
+``` Clean HTML is significantly easier to read and work with. Use a class name only on the root element and let CSS selectors do the rest. It's surprising how little class names you need with clean, semantic markup. This website, for example, has only four class names on the global scope: "grid", "card", "stack" and "note". "Global namespace pollution" is essentially a myth and is trivial to avoid. @@ -155,12 +163,12 @@ Clean HTML is significantly easier to read and work with. Use a class name only ## Create re-usable class names { #reuse } Always find ways to extract reusable pieces from your CSS code. For example, the above notification component could be written as: -[code.good caption="Using a re-usable class name"] -
-

ChitChat

-

You have a new message

-
- +```html.good "Using a re-usable class name" +
+

ChitChat

+

You have a new message

+
+``` Here, the component was broken into two pieces: A highly re-usable "card" component and a notification-specific "notification" component: @@ -191,43 +199,9 @@ Now the "card" class can be applied to any element or component you desire, redu -## Avoid inline styling { #external-css } -Don't style your components directly on the markup: -[code.bad caption="Inline styling: The styles are written directly into the markup"] -
-
- ChitChat Logo -
-
-
ChitChat
-

You have a new message

-
-
- -Instead, write clean markup and style it externally: - -[code.good caption="External styling: The styles are decoupled from the markup"] -
-

ChitChat

-

You have a new messge

-
- - -External styling is the key to professional UX development: -1. **Easier to maintain** — clean HTML and CSS are easier to read, write, teach, share and maintain. - -2. **Leaner stack** — go directly from Figma to CSS without a hefty JavaScript ecosystem on your way. - -3. **Less code to write** — external CSS leads to [maximum reuse](#reuse) and minimal duplication of code. - -4. **Central control** — external CSS is centrally controlled by UX developers, and the look and feel are dictated by the design system. - -5. **Better SEO** — With clean HTML markup your [content to markup](//www.siteguru.co/free-seo-tools/text-to-html-ratio) ratio is significantly higher. - -6. **Timeless skills and products** — CSS is a web standard. You'll learn universal skills that stand the test of time. There is zero risk for technical debt. #### Links @@ -295,7 +269,7 @@ Check [motion and reactivity](reactivity.html) for details. ## Learn modern CSS There is tons of [misinformation](/blog/tailwind-misinformation-engine/) about CSS that makes beginner developers move away from web standards and adopt the idea of inline styling. -But if you grasp the power of the global design system and see how you can accomplish the same thing with significantly less effort you begin to think why you ever bought the idea of tight coupling in the first place. +But if you grasp the power of the global design system and see how you can accomplish the same thing with significantly less effort, you begin to think why you ever bought the idea of tight coupling in the first place. Understand the power of constraints, design systems, and web standards. Become a professional UX developer and stay relevant for years to come. @@ -304,6 +278,5 @@ Understand the power of constraints, design systems, and web standards. Become a - [Ahmad Shadeed](//ishadeed.com/) - [Chris Coyier](//chriscoyier.net/) - [CSS tricks](//css-tricks.com/) -- [Josh Comeau](//www.joshwcomeau.com/) +- [Josh Comeau](//joshwcomeau.com/) - [Ryan Mulligan](//ryanmulligan.dev/blog/) - diff --git a/packages/nuejs.org/docs/css/doc-content.css b/packages/nuejs.org/docs/css/doc-content.css new file mode 100644 index 00000000..5290d216 --- /dev/null +++ b/packages/nuejs.org/docs/css/doc-content.css @@ -0,0 +1,78 @@ + +/* documentation-specific content widgets */ + +h1 + p { + text-wrap: balance; + font-size: 1.15rem; +} + +.gridpaper { + background-image: url(/img/grid-paper.png); + border: 1px solid var(--gray-200); + border-radius: 5px; + text-align: center; + + figcaption { + margin: 0; + position: relative; + line-height: 0; + top: 2em; + } + + &:has(figcaption) { + margin-bottom: 6em; + } +} + +.above { + background-color: var(--gray-600); + box-shadow: 0 0 4em #0007; + font-size: 1.2em; + position: relative; + top: -13em; + + &:hover { + background-color: var(--gray-700); + } +} + +figure:has(.above) { + margin-bottom: -1rem; +} + +.options { + h4 { + margin: 2.5em 0 1.5em .15em; + font-weight: bold; + } + + p { + font-size: 95%; + line-height: 1.4; + display: flex; + } + + code { + min-width: 10em;; + } +} + + +.apidoc article h3 { + font-family: monospace; + color: var(--main-600); + background-color: var(--main-50); + display: inline-block; + padding: .3em .6em; + margin-bottom: -.5em; + border-radius: 2px; +} + +.small { + font-size: 90%; +} + +.columns { + column-width: 15em; + column-gap: 2em; +} diff --git a/packages/nuejs.org/docs/css/layout.css b/packages/nuejs.org/docs/css/doc-layout.css similarity index 92% rename from packages/nuejs.org/docs/css/layout.css rename to packages/nuejs.org/docs/css/doc-layout.css index 194ca395..64baac85 100644 --- a/packages/nuejs.org/docs/css/layout.css +++ b/packages/nuejs.org/docs/css/doc-layout.css @@ -4,6 +4,8 @@ main { flex-direction: column; display: flex; gap: 1em; + + h1 { margin-top: 0 } } article { @@ -58,7 +60,7 @@ article + aside { body > nav { display: none } - main { margin-top: 5.5em } + main { margin-top: 5em } article { margin-left: 19rem; @@ -66,10 +68,10 @@ article + aside { } aside { + height: calc(100vh - 6.5rem); + padding: 0 0 1em .6em; position: fixed; overflow-y: auto; - height: calc(100vh - 7rem); - padding: 0 0 7em .6em; display: block; /* left sidebar */ diff --git a/packages/nuejs.org/docs/css/navigation.css b/packages/nuejs.org/docs/css/doc-navi.css similarity index 58% rename from packages/nuejs.org/docs/css/navigation.css rename to packages/nuejs.org/docs/css/doc-navi.css index 91cae152..9c7a2c36 100644 --- a/packages/nuejs.org/docs/css/navigation.css +++ b/packages/nuejs.org/docs/css/doc-navi.css @@ -1,6 +1,6 @@ -/* sub navigation in mobile */ +/* subnavigation in mobile */ body > nav { position: sticky; top: -1px; @@ -16,32 +16,17 @@ body > nav { border-width: 1px 0; padding: .75em 0; width: 100%; - - /* menu trigger */ - [popovertarget] { - background: url(/icon/menu.svg) .4em 50% no-repeat; - padding: .2em .8em .2em 2em; - background-size: 1.1em; - color: var(--gray-900); - font-family: inherit; - font-weight: 600; - font-size: .95em; - width: auto; - } } /* both sidebars (left and right) */ aside { - h3 { - font-size: 90%; - margin: 0 0 1em; - } + h3 { font-size: 90% } /* link styling */ nav > a { border-left: var(--border);; - padding: .5em 1em; + padding: .45em 1em; white-space: nowrap; display: block; font-size: 85%; @@ -59,7 +44,7 @@ aside { /* sidebar only */ aside:first-child nav { - margin-bottom: 2.5em; + margin-bottom: 1.5em; [aria-selected] { background: var(--main-500); @@ -69,56 +54,38 @@ aside:first-child nav { } /* table of contents */ -article + aside { +.toc { + margin: 1.5em 0; /* overlfow ellipsis */ - nav a { + a { max-width: 16em; display: block; overflow: hidden; text-overflow: ellipsis; } - .level-2 { - margin-top: 1em; + strong { font-weight: 550; color: var(--gray-800); } - .level-3 { - padding-left: 2rem; - font-weight: 400; - font-size: 85%; + nav { + margin-bottom: .3em; - &:has(+ .level-2) { - margin-bottom: 1.5em; + &:has(> :nth-child(2)) { + margin-bottom: 1em; } } - nav [aria-selected] { + [aria-selected] { border-color: var(--main-500); border-width: 2px; color: var(--main-500); font-weight: 550; margin-left: -1px; } - - /* h2 without children */ - .level-2:has(+ .level-2), .level-2:last-child { - border-color: transparent; - padding-left: 0; - font-weight: 400; - margin: .1em 0; - - &[aria-selected] { - font-weight: 550; - } - } - - code { - font-family: inherit; - } } @@ -126,45 +93,26 @@ article + aside { /* icons */ aside:first-child h3 { background: 0 50% no-repeat; + margin: 0 0 .6em -.55em; padding-left: 1.7em; - margin-left: -.55em; - /* Content Framework */ nav:nth-child(1) & { background-image: url(/icon/badge.svg) /* cog, badge, shield, star */ } - /* UX */ + /* websites */ nav:nth-child(2) & { - background-image: url(/icon/user.svg) /* heart */ - } - - /* Reactivity - nav:nth-child(3) & { - background-image: url(/icon/bolt.svg) + background-image: url(/icon/globe.svg) /* squares, rectangles, wrench, globe, user, heart, bolt */ } - */ - /* Content development */ + /* reference */ nav:nth-child(3) & { - background-image: url(/icon/star.svg) - } - - /* SPA */ - nav:nth-child(4) & { - background-image: url(/icon/squares.svg) - } - - /* Reference */ - nav:nth-child(5) & { - background-image: url(/icon/document.svg); + background-image: url(/icon/document.svg) /* puzzle-piece, cube, , document, */ } } - - /* zen mode */ .zen-toggle { position: fixed; diff --git a/packages/nuejs.org/docs/css/docs.css b/packages/nuejs.org/docs/css/docs.css deleted file mode 100644 index ba86b83f..00000000 --- a/packages/nuejs.org/docs/css/docs.css +++ /dev/null @@ -1,98 +0,0 @@ - -/* documentation-specific content widgets */ - -.gridpaper { - background-image: url(/img/grid-paper.png); - border: 1px solid var(--gray-200); - border-radius: 5px; - text-align: center; - - figcaption { - margin: 0; - position: relative; - line-height: 0; - top: 2em; - } - - &:has(figcaption) { - margin-bottom: 6em; - } -} - - -/* "good" and "bad" code block captions */ -.good, .bad { - margin-top: 2em; - - figcaption { - background: url(/icon/bad.svg) -.2em center no-repeat; - padding: .2em 0 .2em 1.8em; - background-size: 1.6em; - color: var(--gray-900); - text-align: left; - font-weight: 550; - font-size: 95%; - - &:after { - border-left: 1px solid var(--red); - position: relative; - top: 1.2em; - left: -1.2em; - content: ""; - height: 1em; - display: block; - float: left; - } - } -} - -.good figcaption { - background: url(/icon/good.svg) -1px center no-repeat; - background-size: 1.4em; - - &:after { - border-color: var(--green); - } -} - - -.options { - h4 { - margin: 2.5em 0 1.5em .15em; - font-weight: bold; - } - - p { - font-size: 95%; - line-height: 1.4; - display: flex; - } - - code { - min-width: 8em; - } - -} - - -/* currently: TODO badge in SPA page */ -h1 code { - background-color: var(--main-100); - color: var(--main-600); - letter-spacing: .01em; - font-family: inherit; - border-radius: .2em; - padding: .2em .4em; - font-size: 1.2rem; - float: right; -} - -.apidoc section h3 { - font-family: monospace; - color: var(--main-600); - background-color: var(--main-50); - display: inline-block; - padding: .4em .6em; - margin-bottom: -.5em; - margin-left: -.6em; -} diff --git a/packages/nuejs.org/docs/custom-components.md b/packages/nuejs.org/docs/custom-components.md new file mode 100644 index 00000000..326e322e --- /dev/null +++ b/packages/nuejs.org/docs/custom-components.md @@ -0,0 +1,739 @@ +--- +include: [demos] +--- + +# Custom components +There are four types of custom components in Nue: + +- **Markdown extensions**: These extend the Markdown syntax with custom tags, offering more flexibility in content formatting. +- **Layout modules**: These components fill the slots in the [page layout](layout.html), helping create a structured design for your pages. +- **Layout components**: These are custom server-side components used within layout modules, allowing for dynamic content rendering. +- **Interactive islands**: These are rendered on the client side (CSR) and enhance user interaction with minimal JavaScript. Here’s an example: + +### Example island +Nue makes it easy to build interactive components like this: + +[array-demo] + users: + - name: Alex Martinez + role: Lead frontend developer + img: /img/face-3.jpg + - name: Sarah Park + role: UI/UX Designer + img: /img/face-4.jpg + - name: Jamie Huang + role: JS/TS developer + img: /img/face-2.jpg + - name: Heidi Blum + role: UX developer + img: /img/face-1.jpg + - name: Adam Nattie + role: Backend developer + img: /img/face-5.jpg + - name: Mila Harrison + role: Senior frontend developer + img: /img/face-6.jpg + +- - - + +## Like React, but semantic +Nue components offer React-like functionality while focusing on semantic web design. They work seamlessly on both the server and client sides, allowing developers to enhance applications progressively without losing structure. + +Unlike React, which relies heavily on JavaScript, Nue is based on HTML. Any valid HTML in Nue is also a valid component, making it simple and accessible. + +```html +
+ + +
+``` + +### Block assembly language +Nue components are named HTML fragments that can be looped and rendered conditionally, enabling nesting within other components. Assign a component name using the `@name` attribute: + +```html +
+ + +
+``` + +Once named, components can be nested inside one another to form more complex applications and tree-like structures. For example: + +```html + +``` + +### Component libraries +Server-side components are saved with a `.html` extension, while client-side components or islands use a `.dhtml` or `.htm` extension. You can group related components together in the same file to create a cohesive component library. + +Components can be stored at three levels: globally, area-level, or page-level. These components are automatically aware of each other, allowing you to reuse them in different files without the need for explicit import statements. + +To explicitly include components from a [library folder](project-structure.html#libraries), you can use the [include](settings.html#include) statement. Components cascade similarly to CSS files, enabling a streamlined approach to component management. + +## Mounting +Once a component is available on the page, mounting it is straightforward: + +### In Markdown content +Custom components are mounted in Markdown content just like the built-in [content tags](content-tags.html), using square brackets: + +```md +[image-gallery] + +// with "heroic" styling +[image-gallery.heroic] +``` + +### In layout modules +In layout modules, components are mounted as custom HTML elements: + +```html + + + + +``` + +### Islands +If a component is not defined server-side in a `.html` file, it is rendered directly as a custom element on the client side. For example: + +```html + + +``` + +In this case, Nue will first look for an implementation of the component or island defined in a `.dhtml` file. If it is not found, a standard Web Component will be mounted as specified in a `.js` or `.ts` file. + + +## Passing data +You can pass data to your components using attributes. These attributes can either be direct values or reference data from the [unstructured data](content.html) when the attribute name starts with a colon + +#### Markdown example +In Markdown content, you can pass data as follows: + +```md +// application data (items) vs direct value (1) +[image-gallery :items="screenshots" index="1"] +``` + +In this example, `:items` references the `screenshots` array from the application data, while `index` is a direct value set to `1`. + +#### HTML component example +In layout modules and interactive islands, you can similarly pass data as arguments: + +```html + +``` + +Here, `:items` pulls from the `products` data, and `index` is set to `2`. This allows the component to dynamically render based on the provided data while keeping your templates clean and maintainable. + +#### Islands +Client-side components, such as islands, receive data through nested JSON. This enables you to encapsulate data directly within the component: + +```html + + + +``` + +In this structure, the JSON data is embedded within a ` + +``` + +### How it works +In this example: + + +- **Properties** allow you to set default values and dynamic values for rendering. For instance, `locale` is initialized to `'en-US'` and can be overridden. +- The **constructor** method runs when the component is created, allowing you to manipulate user-provided data (like `date` and `locale`) before it is processed by the template. +- **Methods** are useful for formatting and transforming data. The `pretty` method formats the date for display, while the `toIso` method converts it to a standard format. + +The scripting block helps to extract complex JavaScript logic from the [HTML template block](template-syntax.html), resulting in cleaner and more readable markup. This is particularly important for interactive islands, where the amount of scripting is typically much higher. + + +### Local scripts +You can set up local functions and variables using root-level script blocks: + +```html + + +
+

A count: { counter }

+ + +
+ +
+

B count: { counter }

+ + +
+``` + +Local variables and functions provide another way to decouple complex scripting logic from your components, allowing you to share functions and variables between components in the same file. + +Note that currently, you can only use the `import` statement in client-side components, but support for server-side imports is planned for the future. + +### Passthrough scripts +Sometimes you may want the script block to be executed directly by the browser. You can achieve this by using the `type`, `src`, or `client` attributes, which instruct the compiler to pass the scripts directly to the client. For example: + +```html + + + + + +``` + +You can also use a `client` attribute in place of the traditional `type="text/javascript"`: + +```html + +``` + +The above will be rendered as: + +```html + +``` + +## Interactive components +Interactive components in Nue are executed on the client side, directly within the user's browser. They are created and mounted using the same syntax as server-side components, but interactive components can respond to user input and re-render themselves to reflect new states. This functionality makes them ideal for a variety of applications, such as feedback forms, login forms, registration flows, account dropdowns, image galleries, or any other component that requires interactivity. + + +### Example: Image Gallery +Let’s add a simple image gallery component to this page: + +[image-gallery] + images: [tomatoes.jpg, lemons.jpg, peas.jpg, popcorn.jpg] + basedir: /img + + +```md render +[image-gallery] + images: [tomatoes.jpg, lemons.jpg, peas.jpg, popcorn.jpg] + basedir: /img +``` + + + + +Here’s the source code for the gallery component: + +```html + +``` + +Inside the component, all control flow operations, such as loops and conditionals, are reactive — they respond to user events and re-render based on the new state. Here, we have a numeric state variable `index`, which updates as the user clicks the navigational elements, automatically changing the displayed image accordingly. + +## Event handlers +In Nue, attributes starting with the `@` symbol define event handlers. These handlers are JavaScript functions that respond to user interactions, such as clicks, keypresses, or mouse movements. + +### Inline handlers +Inline handlers are defined directly within the attribute: + +```html + +``` + +Inline handlers are great for simple expressions that don’t require additional logic. + +### Method handlers +For more complex functionality, it's best to move the logic into an instance method: + +```html + + + + + +``` + +### Method calls +You can pass arguments to method calls: + +```html +
+ + + +
+``` + +### Event argument +Method handlers always receive an [Event object](https://developer.mozilla.org/en-US/docs/Web/API/Event) as the last argument, unless it is explicitly named `$event`: + +```html +
+ + + + + +
+``` + +### Event modifiers +Nue provides convenient shortcuts for common DOM event manipulation functions. For instance, `@submit.prevent` is a shortcut to call [event.preventDefault()](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault). + +```html + +
+ + + + + +
+``` + +The following modifiers are supported: + +- `.prevent`: Prevents the default behavior of the event from occurring. +- `.stop`: Prevents further [propagation](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation) of the event. +- `.self`: Only triggers the handler if `event.target` is the element itself. +- `.once`: The event will be triggered at most once. + +### Key modifiers +Key modifiers bind the event handler to specific keyboard keys: + +```html + + +``` + +You can use any valid key names from [KeyboardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values) as modifiers, converting them to kebab-case. For example, the following handler is called only if `event.key` is equal to `PageDown`: + +```html + +``` + +Nue provides aliases for commonly used keys: + +- `.enter`: Captures both "Enter" and "Return." +- `.delete`: Captures both "Delete" and "Backspace." +- `.esc`: Captures both "Esc" and "Escape." +- `.space`: Captures "Spacebar", " ", "Space Bar." +- `.tab`: Captures "Tab." +- `.up`: Captures "Up" and "ArrowUp." +- `.down`: Captures "Down" and "ArrowDown." +- `.left`: Captures "Left" and "ArrowLeft." +- `.right`: Captures "Right" and "ArrowRight." + +## Dynamic arrays +When you define a loop with the `:for` expression, Nue automatically detects if the looped array is mutated and triggers the necessary UI updates. The following [array methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) are supported: + +- `push(item)`: Adds a new item to the end of the array. +- `unshift(item)`: Adds a new item to the beginning of the array. +- `sort(fn)`: Sorts items based on the given function. +- `reverse()`: Reverses the order of items. +- `splice(start, count)`: Removes items from the array. +- `shift()`: Removes the first item from the array. +- `pop()`: Removes the last item from the array. +- `remove(item)`: A Nue-specific helper method to remove the given item from the array. + +### Replacing the array +Mutation methods modify the original array they are called on. Non-mutating methods, such as `filter()`, `concat()`, and `slice()`, return a new array. In these cases, you should replace the old array with the new one, and Nue will render the updates accordingly: + +```js +search() { + this.items = this.items.filter(item => item.text.match(/Foo/)); +} +``` + +### Example: array.push +Here’s a simple demo of using an array: + +[array-demo] + users: + - name: Alex Martinez + role: Lead frontend developer + img: /img/face-3.jpg + - name: Sarah Park + role: UI/UX Designer + img: /img/face-4.jpg + - name: Jamie Huang + role: JS/TS developer + img: /img/face-2.jpg + - name: Heidi Blum + role: UX developer + img: /img/face-1.jpg + - name: Adam Nattie + role: Backend developer + img: /img/face-5.jpg + - name: Mila Harrison + role: Senior frontend developer + img: /img/face-6.jpg + + +```md +[array-demo] + users: + - name: Alex Martinez + role: Lead frontend developer + img: /img/face-3.jpg + - name: Sarah Park + role: UI/UX Designer + img: /img/face-4.jpg + - name: Jamie Huang + role: JS/TS developer + img: /img/face-2.jpg + - name: Heidi Blum + role: UX developer + img: /img/face-1.jpg + - name: Adam Nattie + role: Backend developer + img: /img/face-5.jpg + - name: Mila Harrison + role: Senior frontend developer + img: /img/face-6.jpg +``` + +Here's the source code for the above demo: + +```html +
+ + + +
    +
  • + +

    { el.name }

    +

    { el.role }

    +
  • +
+ + + +
+``` + +Note that the transition effect is done with vanilla CSS using `@starting-style` without specialized `` elements or motion libraries. This keeps the implementation lean and clean. + + +## Lifecycle methods +Each component instance goes through a series of steps during its lifetime: first, it is created, then mounted on the page, and finally, it gets updated one or more times. Sometimes the component is removed or "unmounted" from the page. + +You can hook custom functionality to these steps by creating instance methods with specific names: + +```html + +``` + +Inside these callback functions, `this` points to the [instance API](#api), allowing access to various properties and methods related to the component. + +## Instance API +The component API is accessible via the `this` variable inside the lifecycle methods. It has the following attributes and methods: + +- `root`: The root DOM node of the component instance. +- `$el`: An alias for the root DOM node. +- `$parent`: The root DOM node of the parent instance. +- `$refs`: Access to named DOM nodes and inner components within the component. +- `mount(root: DOMElement)`: Mounts the instance to the specified root element. +- `unmount()`: Removes the component from the current component tree. +- `update(data?: Object)`: Forces the component instance to re-render itself with optional data. This is useful after fetching data from a server or during any asynchronous event. +- `mountChild(name, wrap, data)`: Mounts a new child component on a DOM element inside the current component. + +The component re-renders itself automatically after calling an event handler, but you need to call this manually if there is no clear interaction to detect. + +### References +You can obtain a handle to nested DOM elements or components via the `$refs` property: + +```html +
+ + +
+ + + + + + + + +

{ $refs.email.placeholder }

+ + +
+``` + +### Sharing code between components +You can add and import shared code within a top-level ` + + +
+
+

{ item.price }

+

{ item.amount }

+
+ + +
+ + + +``` + + +## Summary +Nue opens the door to a new way of building web applications, allowing you to harness the power of Markdown extensions, server-side components, and client-side components—all with a simple and intuitive syntax. By focusing on an HTML-based approach, you can prioritize layout, structure, semantics, and accessibility, freeing yourself from the frustration of tangled JavaScript stack traces. diff --git a/packages/nuejs.org/docs/custom-layouts.md b/packages/nuejs.org/docs/custom-layouts.md deleted file mode 100644 index a1f50d52..00000000 --- a/packages/nuejs.org/docs/custom-layouts.md +++ /dev/null @@ -1,199 +0,0 @@ - -# Custom layouts -The design system consists of various "slots" that you can fill or replace with custom template content. The slots are named as follows: - -[image.bordered /img/custom-layout.svg size="569 × 634 px"] - - -For example, if you want to add a custom banner above the global header you'd create a layout module called "banner": - -```html -
- Major update available! - Check out v2.0 -
-``` - -The bolded slot names (header, footer, and aside) don't require the `@name` attribute, because the slot is identified directly from the HTML tag name. For example, a custom `aside` tag is always placed prior to the `main` element: - -```html - -``` - -The layouts are written in a HTML-based [template language](template-syntax.html) and the template variables have access to the [project data](project-structure.html#data). - -The modules can be stored in any file with a `.html` suffix such as `layout.html` and the file can contain multiple layout components. - - - -### Area-specific layouts -You can customize the layout of all the different areas of your website like the documentation or blogging area. Think custom sidebars, blog entry "hero" layouts, or custom footers. The area-specific layouts override any existing layouts defined globally at the root level. - -This documentation area, for example, has the following documentation-specific layouts in [docs/layout.html](//github.com/nuejs/nue/blob/master/packages/nuejs.org/docs/layout.html): - - -```html - - - - - - - - - -``` - -### Leaving out layouts { #disabling } -Sometimes you want to leave out some layouts. For example, the blog index page might want to disable the layout components that are available on the actual blog entries. This happens by setting the desired layout components to `false`. For example: - -```yaml -aside: false -pagehead: false -pagefoot: false -``` - - - -### Main Layout -You can override the `main` element by re-defining it in a layout file. For example: - -```html -
-

Hello, World!

- - - -
-``` - -[.warning] - ### Warning - Overriding the main element breaks you out of the global design system. - - -### Root layout -You can go extreme and override the entire `html` element in which case you can customize everything inside the html element, including the document head: - -```html - - - - - - - - - - - -
-

{ title }

- - - -
- - - -``` - - -## Built-in helper components -You can use several built-in helper components when building your layouts. For example, the blogging area on this website takes advantage of several built-in components in the blog entry "hero" area: - -```html -
- - - - - -

- - - - -
-``` - -Here are all the helper components: - - -### `` -Renders an ARIA compatible navigational element based on the data given in the `items` attribute. The data must be formatted in a specific [YAML format](page-layout.html#yaml) which supports multiple types of navigation types: Flat, hierarchical, and more complex dropdown navigation menus. - -This website, for example, uses the `` component in the sidebar of the documentation area: - -```html - -``` -You can use an optional `label` attribute as the value for the `aria-label` HTML attribute for the generated `
``` -If React is "Just JavaScript", then Nue is "Just HTML" because any valid HTML is also valid Nue. - ### Links * [Template syntax](https://nuejs.org/docs/template-syntax.html) -* [Reactive components](https://nuejs.org/docs/reactive-components.html) +* [Creating components](https://nuejs.org/docs/custom-components.html) +* [Islands in Nue](https://nuejs.org/docs/islands.html) ### Contributing -Please see [contributing.md](/CONTRIBUTING.md) +Please see [CONTRIBUTING.md](/CONTRIBUTING.md) ### Community Please see [GitHub discussions](https://github.com/nuejs/nue/discussions) - - - diff --git a/packages/nuejs/package.json b/packages/nuejs/package.json index 30495725..d2823e09 100644 --- a/packages/nuejs/package.json +++ b/packages/nuejs/package.json @@ -1,7 +1,7 @@ { "name": "nuejs-core", - "version": "0.5.1", - "description": "HTML microlibrary for UX developers", + "version": "0.5.2", + "description": "Like React, but semantic", "homepage": "https://nuejs.org", "license": "MIT", "type": "module", diff --git a/packages/nuejs/src/fn.js b/packages/nuejs/src/fn.js index 519bda03..6dae9ef9 100644 --- a/packages/nuejs/src/fn.js +++ b/packages/nuejs/src/fn.js @@ -39,7 +39,7 @@ export function getComponentName(root) { } export function selfClose(str) { - return str.replace(/\/>/g, function(match, i) { + return str?.replace(/\/>/g, function(match, i) { const tag = str.slice(str.lastIndexOf('<', i), i) const name = /<([\w-]+)/.exec(tag) return `>` @@ -79,6 +79,7 @@ function quote(val) { } export function mkdom(src) { + if (typeof src != 'string') src = '' const dom = parseDocument(selfClose(src)) walk(dom, (el) => { if (el.type == 'comment') DomUtils.removeElement(el) }) // strip comments return dom diff --git a/packages/nuejs/src/render.js b/packages/nuejs/src/render.js index 8cec623b..7f3847aa 100644 --- a/packages/nuejs/src/render.js +++ b/packages/nuejs/src/render.js @@ -242,9 +242,13 @@ function processNode(opts) { if (name == 'slot') { if (attribs.for) { const html = exec(setContext(attribs.for), data) - if (html) DOM.replaceElement(node, mkdom(html)) + if (html && html != 'false') DOM.replaceElement(node, mkdom(html)) else removeNode(node) + // slot content provided as data + } else if (data.innerHTML) { + DOM.replaceElement(node, mkdom(data.innerHTML)) + } else if (inner) { while (inner[0]) DOM.prepend(node, inner[0]) removeElement(node) @@ -259,8 +263,8 @@ function processNode(opts) { // client side component if (is_custom && !component) { setJSONData(node, data) - node.attribs.is = name - node.name = 'div' + node.attribs.custom = name + node.name = name return // must return } diff --git a/packages/nuejs/test/client/basics.nue b/packages/nuejs/test/client/basics.dhtml similarity index 100% rename from packages/nuejs/test/client/basics.nue rename to packages/nuejs/test/client/basics.dhtml diff --git a/packages/nuejs/test/client/loops.nue b/packages/nuejs/test/client/loops.dhtml similarity index 100% rename from packages/nuejs/test/client/loops.nue rename to packages/nuejs/test/client/loops.dhtml diff --git a/packages/nuejs/test/compile.js b/packages/nuejs/test/compile.js index f5d98c4c..a531db1a 100644 --- a/packages/nuejs/test/compile.js +++ b/packages/nuejs/test/compile.js @@ -3,6 +3,6 @@ import { compileFile } from '../index.js' for (const name of ['basics', 'loops']) { const to = `dist/${name}.js` - await compileFile(`client/${name}.nue`, to) + await compileFile(`client/${name}.dhtml`, to) console.log('created', `test/${to}`) } diff --git a/packages/nuejs/test/parse.test.js b/packages/nuejs/test/parse.test.js index e1a57fe6..9d5e76d5 100644 --- a/packages/nuejs/test/parse.test.js +++ b/packages/nuejs/test/parse.test.js @@ -1,3 +1,4 @@ + import { parseFor, parseClass, setContext, parseExpr } from '../src/expr.js' diff --git a/packages/nuejs/test/render.test.js b/packages/nuejs/test/render.test.js index 6c11cf88..eb975277 100644 --- a/packages/nuejs/test/render.test.js +++ b/packages/nuejs/test/render.test.js @@ -146,7 +146,7 @@ test('Advanced', () => { // :attr (:bind works the same on server side) '
': '
', - '': '
\n \n
', + '': '\n \n', '{ val }': '1', '': '
Hello
', diff --git a/packages/nuekit/README.md b/packages/nuekit/README.md index 6749b6b2..e7fc498b 100644 --- a/packages/nuekit/README.md +++ b/packages/nuekit/README.md @@ -2,27 +2,27 @@ # Nue  [![test status](https://github.com/nuejs/nue/actions/workflows/test.yaml/badge.svg?branch=master)](https://github.com/nuejs/nue/actions/workflows/test.yaml) [![link checker status](https://github.com/nuejs/nue/actions/workflows/links.yaml/badge.svg?branch=master)](https://github.com/nuejs/nue/actions/workflows/links.yaml) - + ### What is Nue? -Nue is a web framework for UX developers. What used to take a React specialist, and an absurd amount of JavaScript can now be done by a UX developer and a small amount of CSS. +Nue is a Static Site Generator (SSG) built from scratch for faster tooling, cleaner codebases, and better results. **It makes things •fun• again** -[Learn how it works](https://nuejs.org/docs/) +[Why Nue?](https://nuejs.org/docs/) +[How it works](https://nuejs.org/docs/how-it-works.html) -### Who is it for? -Nue is designed for the following people: -1. **UX developers**: who natively jump between **Figma** and **CSS** without confusing [designer-developer handoff](https://medium.com/design-warp/5-most-common-designer-developer-handoff-mishaps-ba96012be8a7) processes in the way. +## Who is nue good for? +Nue is a great fit for: -2. **Beginner web developers**: who want to skip the [redundant frontend layers](https://roadmap.sh/frontend) and start building websites quickly with HTML, CSS, and JavaScript. +1. **Beginner web developers**: Those looking to bypass [frontend redundancy](https://roadmap.sh/frontend) and work directly with the Web Standard Model: HTML, CSS, and JavaScript. -3. **Experienced JS developers**: frustrated with the absurd amount of layers in the [React stack](https://roadmap.sh/react) and look for simpler ways to develop professional websites. +2. **Experienced JavaScript developers**: Those frustrated with the overwhelming amount of abstractions in the [React stack](https://roadmap.sh/react) and seeking simpler ways to develop professional websites. -4. **Designers**: aiming to transfer their design skills to CSS code, but find the React/JavaScript/CSS-in-JS ecosystem impossible to grasp +3. **Design-focused teams**: Those prioritizing user experience and design systems, leveraging modern CSS to create efficient, lightweight websites that enhance usability without the bloat of JS monoliths. -5. **Parents & Teachers**: who wants to educate people [how the web works](https://www.websitearchitecture.co.uk/resources/examples/web-standards-model/) +In short: if you’re looking to build beautiful and innovative websites with faster tooling and a simpler development model, **Nue is the right choice for you**. ### Installation @@ -33,8 +33,9 @@ Check out [installation docs](https://nuejs.org/docs/installation.html) ### Ultimate goal Ultimately Nue will be a ridiculously simpler alternative to **Next.js**, **Gatsby**, and **Astro** -[Learn more about the vision](https://nuejs.org/blog/perfect-web-framework/) - + + + ### Contributing @@ -44,4 +45,3 @@ Please see [CONTRIBUTING.md](/CONTRIBUTING.md) ### Community Please see [GitHub discussions](https://github.com/nuejs/nue/discussions) - diff --git a/packages/nuekit/package.json b/packages/nuekit/package.json index be49babd..0db97133 100644 --- a/packages/nuekit/package.json +++ b/packages/nuekit/package.json @@ -1,7 +1,7 @@ { "name": "nuekit", - "version": "1.0.0-beta.2", - "description": "Web Framework For UX Developers. Build the slickest websites in the world and wonder why you ever did them any other way", + "version": "1.0.0-RC.1", + "description": "The design engineering framework for the web", "homepage": "https://nuejs.org", "license": "MIT", "type": "module", @@ -17,22 +17,19 @@ "bun": ">= 1", "node": ">= 18" }, - "scripts": { - "test": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js --runInBand" - }, + "scripts": { + "test": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js --runInBand" + }, "dependencies": { "diff-dom": "^5.1.4", "es-main": "^1.3.0", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", - "lightningcss": "^1.26.0", + "lightningcss": "^1.27.0", "nue-glow": "*", "nuejs-core": "*", "nuemark": "*" }, - "devDependencies": { - "@happy-dom/global-registrator": "^14.12.3" - }, "jest": { "setupFilesAfterEnv": [ "jest-extended/all", diff --git a/packages/nuekit/src/browser/error.nue b/packages/nuekit/src/browser/error.dhtml similarity index 100% rename from packages/nuekit/src/browser/error.nue rename to packages/nuekit/src/browser/error.dhtml diff --git a/packages/nuekit/src/browser/hotreload.js b/packages/nuekit/src/browser/hotreload.js index a42cd811..342278b3 100644 --- a/packages/nuekit/src/browser/hotreload.js +++ b/packages/nuekit/src/browser/hotreload.js @@ -101,7 +101,9 @@ async function remount(path) { // save form/dialog state const data = [...document.forms].map(form => new FormData(form)) - let dialog = $('dialog[open]') + const popover = $('[popover]') + const pid = popover?.checkVisibility() && popover.id + const dialog = $('dialog[open]') // mount all await mountAll(path) @@ -109,9 +111,18 @@ async function remount(path) { // restore form data data.forEach((formdata, i) => deserialize(document.forms[i], formdata)) - // dialog found -> re-open - dialog = window[dialog?.id] - if (dialog) { dialog.close(); dialog.showModal() } + // re-open popover + if (pid) { + const el = window[pid] + el?.showPopover() + } + + // re-open dialog + if (dialog) { + const el = window[dialog.id] + if (el) { el.close(); el.showModal() } + } + } diff --git a/packages/nuekit/src/browser/mount.js b/packages/nuekit/src/browser/mount.js index 800f19d1..6ec5cc8e 100644 --- a/packages/nuekit/src/browser/mount.js +++ b/packages/nuekit/src/browser/mount.js @@ -26,14 +26,14 @@ async function importAll(hmr_path) { export async function mountAll(hmr_path) { - const els = document.querySelectorAll('[is]') + const els = document.querySelectorAll('[custom]') const lib = els[0] ? await importAll(hmr_path) : [] if (!lib[0]) return const { createApp } = await import('./nue.js') for (const node of [...els]) { - const name = node.getAttribute('is') + const name = node.getAttribute('custom') // || node.tagName.toLowerCase() const next = node.nextElementSibling const data = next?.type == 'application/json' ? JSON.parse(next.textContent) : {} const comp = lib.find(a => a.name == name) @@ -46,7 +46,7 @@ export async function mountAll(hmr_path) { // web component -> do nothing } else { - // console.error(`Component not found: "${name}"`) + console.error(`Component not defined: "${name}"`) } } } diff --git a/packages/nuekit/src/browser/view-transitions.js b/packages/nuekit/src/browser/view-transitions.js index 09600849..3b9e9d6e 100644 --- a/packages/nuekit/src/browser/view-transitions.js +++ b/packages/nuekit/src/browser/view-transitions.js @@ -40,9 +40,8 @@ export async function loadPage(path, replace_state) { const css_paths = updateStyles(dom) loadCSS(css_paths, () => { - simpleDiff($('main'), $('main', dom)) - simpleDiff($('body'), $('body2', dom)) - setActive(path) + const ignore_main = simpleDiff($('main'), $('main', dom)) + simpleDiff($('body'), $('body2', dom), ignore_main) // scroll const { hash } = location @@ -51,6 +50,12 @@ export async function loadPage(path, replace_state) { // route event dispatchEvent(new Event('route')) + + // route:app event + const [_, app ] = location.pathname.split('/') + dispatchEvent(new Event(`route:${app || 'home'}`)) + + setActive(path) }) } @@ -67,7 +72,7 @@ export function onclick(root, fn) { // event ignore if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || !path || path[0] == '#' || path?.includes('//') || path?.startsWith('mailto:') || - (name?.includes('.') && !name?.endsWith('.html')) || target == '_blank') return + (name?.includes('.') && !name?.endsWith('.html')) || !!target) return // all good if (path != location.pathname) fn(path, el) @@ -77,14 +82,24 @@ export function onclick(root, fn) { } // developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-selected +function toRelative(path) { + const curr = location.pathname + return curr.slice(0, curr.lastIndexOf('/') + 1) + path +} + export function setActive(path, attrname = 'aria-selected') { + if (path[0] != '/') path = toRelative(path) // remove old selections $$(`[${attrname}]`).forEach(el => el.removeAttribute(attrname)) // add new ones $$('a').forEach(el => { - if (el.getAttribute('href') == path) el.setAttribute(attrname, '') + if (!el.hash && el.pathname == path) { + + // set timeout needed @ nue docs area. TODO: remove this hack + setTimeout(() => el.setAttribute(attrname, ''), 50) + } }) } @@ -135,11 +150,16 @@ if (is_browser) { // primitive DOM diffing -function simpleDiff(a, b) { +function simpleDiff(a, b, ignore_main) { + a.classList.value = b.classList.value + if (a.children.length == b.children.length) { - ;[...a.children].forEach((el, i) => updateBlock(el, b.children[i])) + ;[...a.children].forEach((el, i) => { + if (!(ignore_main && el.tagName == 'MAIN')) updateBlock(el, b.children[i]) + }) + return true + } else { - a.classList.value = b.classList.value a.innerHTML = b.innerHTML } } diff --git a/packages/nuekit/src/builder.js b/packages/nuekit/src/builder.js index d97da927..38795771 100644 --- a/packages/nuekit/src/builder.js +++ b/packages/nuekit/src/builder.js @@ -6,10 +6,12 @@ import { join } from 'node:path' import { resolve } from 'import-meta-resolve' import { Features, bundleAsync } from 'lightningcss' - +let jsBuilder export async function getBuilder(is_esbuild) { + if (jsBuilder) return jsBuilder + try { - return is_esbuild ? await import(resolve('esbuild', `file://${process.cwd()}/`)) : Bun + return jsBuilder = is_esbuild ? await import(resolve('esbuild', `file://${process.cwd()}/`)) : Bun } catch { throw 'Bundler not found. Please use Bun or install esbuild' } diff --git a/packages/nuekit/src/init.js b/packages/nuekit/src/init.js index 347c5fd9..89bccfc8 100644 --- a/packages/nuekit/src/init.js +++ b/packages/nuekit/src/init.js @@ -15,7 +15,7 @@ export async function initNueDir({ dist, is_dev, esbuild, force }) { const outdir = join(cwd, dist, '@nue') // has all latest? - const latest = join(outdir, '.beta-2') + const latest = join(outdir, '.rc-1') if (force || !existsSync(latest)) { await fs.rm(outdir, { recursive: true, force: true }) @@ -61,11 +61,10 @@ async function initDir({ dist, is_dev, esbuild, cwd, srcdir, outdir }) { dot() } - // lets do it process.stdout.write(colors.green('✓') + ` Initialize ${dist}: `) - await buildPackage('nuemark/src/browser/nuemark.js', 'nuemark.js') + // await buildPackage('nuemark/src/browser/nuemark.js', 'nuemark.js') await buildPackage('nuejs-core/src/browser/nue.js', 'nue.js') await buildFile('view-transitions') await buildFile('app-router') @@ -79,7 +78,7 @@ async function initDir({ dist, is_dev, esbuild, cwd, srcdir, outdir }) { await buildPackage('diff-dom', 'diffdom.js') await buildFile('hotreload') await copy('error.css', outdir) - await nueCompile(join(fromdir, 'error.nue'), join(outdir, 'error.js')) + await nueCompile(join(fromdir, 'error.dhtml'), join(outdir, 'error.js')) } // favicon diff --git a/packages/nuekit/src/layout/components.js b/packages/nuekit/src/layout/components.js new file mode 100644 index 00000000..6cd9574d --- /dev/null +++ b/packages/nuekit/src/layout/components.js @@ -0,0 +1,145 @@ + +import { elem, parseSize, renderInline } from 'nuemark' + +export function renderPageList(data) { + const key = data.collection_name || data.content_collection + const items = key ? data[key] : data.itmes || data + + if (!items?.length) { + console.error(': no content collection data') + return '' + } + + const pages = items.map(renderPage) + return elem('ul', pages.join('\n')) +} + + +// the "main" method called by the tag +export function renderNavi(data) { + const { items } = data + + return Array.isArray(items) ? renderNav({ items }) : + typeof items == 'object' ? renderMultiNav(items, { class: data.class }) : '' +} + + +function renderTOC(data) { + const { document, attr } = data + return document.renderTOC(attr) +} + +function renderPrettyDate(date) { + if (!date) date = new Date() + if (!date.getDate) date = new Date(date) + return elem('time', { datetime: date.toISOString() }, prettyDate(date)) +} + + +// in Nue JS component format +export function getLayoutComponents() { + return [ + { name: 'navi', create: renderNavi }, + { name: 'page-list', create: renderPageList }, + { name: 'toc', create: renderTOC }, + { name: 'markdown', create: ({ content }) => renderInline(content) }, + { name: 'pretty-date', create: ({ date }) => renderPrettyDate(date) }, + ] +} + + + +/****** utilities ********/ + +export function renderPage(page) { + const { title, desc, url } = page + const thumb = toAbsolute(page.thumb, page.dir) + + // date + let date = page.date || page.pubDate || new Date() + if (!date.getDate) date = new Date(date) + + const time = date ? renderPrettyDate(date) : '' + const h2 = title ? elem('h2', renderInline(title)) : '' + const p = desc ? elem('p', renderInline(desc)) : '' + + const body = !thumb ? time + elem('a', { href: url }, h2 + p) : + + // figure + elem('a', { href: url }, elem('figure', + elem('img', { src: thumb, loading: 'lazy' }) + elem('figcaption', time + h2 + p)) + ) + + return elem('li', { class: isNew(date) && 'is-new' }, body) +} + + +function isNew(date, offset=4) { + const diff = new Date() - date + return diff < offset * 24 * 3600 * 1000 +} + +function prettyDate(date) { + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }) +} + +export function toAbsolute(path, dir) { + return path && path[0] != '/' ? `/${dir}/${path}`: path +} + +export function parseLink(item) { + if (item.image || item.label) return item + + if (typeof item == 'string') { + return item.startsWith('---') ? { separator: item } : { label: item, url: '' } + } + const [ label, url ] = Object.entries(item)[0] + return { label, ...parseClass(url) } +} + + +export function renderLink(item) { + const img = item.image ? renderImage(item) : '' + const link = parseLink(item) + const attr = { href: link.url, class: link.class, role: item.role } + return elem('a', attr, img + renderInline(link.label)) +} + +export function renderImage(data) { + const { width, height } = parseSize(data) + return elem('img', { src: data.image, width, height, alt: data.alt }) +} + +export function parseClass(url) { + const data = { url } + const i = url.indexOf('"') + if (i > 0) { + data.url = url.slice(0, i).trim() + data.class = url.slice(i + 1, -1).trim() + } + return data +} + + +export function renderNav({ items, heading='' }) { + const html = items.map(renderLink) + return elem('nav', heading + html.join('\n')) +} + + +export function renderMultiNav(data, attr={}) { + const html = [] + + for (const cat in data) { + const heading = elem('h3', cat) + const nav = renderNav({ items: data[cat], heading }) + html.push(nav) + } + + return elem('div', attr, html.join('\n')) +} + diff --git a/packages/nuekit/src/layout/gallery.js b/packages/nuekit/src/layout/gallery.js deleted file mode 100644 index 0dc6231c..00000000 --- a/packages/nuekit/src/layout/gallery.js +++ /dev/null @@ -1,64 +0,0 @@ -// content collection rendering - -import { renderInline } from 'nuemark' -import { elem, join } from 'nuemark/src/tags.js' - - -function isNew(date, offset=4) { - const diff = new Date() - date - return diff < offset * 24 * 3600 * 1000 -} - -function prettyDate(date) { - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric' - }) -} - -export function renderPrettyDate(date) { - if (!date) date = new Date() - if (!date.getDate) date = new Date(date) - return elem('time', { datetime: date.toISOString() }, prettyDate(date)) -} - -export function toAbsolute(path, dir) { - return path && path[0] != '/' ? `/${dir}/${path}`: path -} - -export function renderGalleryItem(page) { - const { title, desc, url } = page - const thumb = toAbsolute(page.thumb, page.dir) - - // date - let date = page.date || page.pubDate || new Date() - if (!date.getDate) date = new Date(date) - - const time = date ? renderPrettyDate(date) : '' - const h2 = title ? elem('h2', renderInline(title)) : '' - const p = desc ? elem('p', renderInline(desc)) : '' - - const body = !thumb ? time + elem('a', { href: url }, h2 + p) : - - // figure - elem('a', { href: url }, elem('figure', - elem('img', { src: thumb, loading: 'lazy' }) + elem('figcaption', time + h2 + p)) - ) - - return elem('li', { class: isNew(date) && 'is-new' }, body) -} - - -export function renderGallery(data) { - const key = data.collection_name || data.content_collection - const items = key ? data[key] : data.itmes || data - - if (!items?.length) { - console.error('Gallery tag: no data or content collection defined') - return '' - } - - const pages = items.map(renderGalleryItem) - return elem('ul', join(pages)) -} diff --git a/packages/nuekit/src/layout/head.js b/packages/nuekit/src/layout/head.js index 645215b5..94fe46bd 100644 --- a/packages/nuekit/src/layout/head.js +++ b/packages/nuekit/src/layout/head.js @@ -1,6 +1,7 @@ + import { extname } from 'node:path' -import { elem } from 'nuemark/src/tags.js' +import { elem } from 'nuemark' import { TYPES } from '../nueserver.js' @@ -17,18 +18,16 @@ export function renderHead(data) { viewport = 'width=device-width,initial-scale=1', charset = 'utf-8', title_template = '%s', - scripts = [], - styles = [], - inline_css = [], prefetch = [], base = '', origin = '', - components = [], favicon, title, is_prod, } = data + const { scripts=[], styles=[], inline_css=[], components=[] } = data.assets || {} + const head = [``] if (title) head.push(elem('title', title_template.replace(/%s/gi, title))) diff --git a/packages/nuekit/src/layout/navi.js b/packages/nuekit/src/layout/navi.js deleted file mode 100644 index f2931d0c..00000000 --- a/packages/nuekit/src/layout/navi.js +++ /dev/null @@ -1,120 +0,0 @@ -import { renderInline } from 'nuemark' -import { elem, join, parseSize } from 'nuemark/src/tags.js' - - -export function parseNavItem(item) { - // plain string - if (typeof item == 'string') { - return item.startsWith('---') ? { separator: item } : { text: item, url: '' } - } - - const keys = Object.keys(item) - const [char] = keys[0] - - // [text]: string | object - if (char == char.toUpperCase() && keys.length == 1) { - let [text, data] = Object.entries(item)[0] - if (typeof data == 'string') data = parseClass(data) - - if (Array.isArray(data)) data = { items: data.map(parseNavItem) } - return { text, ...data } - } - - // { ... } - const { items } = item - if (items) item.items = items.map(parseNavItem) - return item -} - - -export function renderNavItem(item) { - const { text, role, url, image, alt } = item - const html = [] - - // hr - if (item.separator) return '
' - - // image - if (image) { - const { width, height } = parseSize(item) - html.push(elem('img', { src: image, width, height, alt })) - } - - // text - if (text) { - const formatted = renderInline(text) - html.push(image ? elem('strong', formatted) : formatted) - } - - // attributes - const attr = { href: url, role } - if (item.class) attr.class = item.class - - return elem(url != null ? 'a' : 'span', attr, join(html)) -} - - -export function parseClass(url) { - const data = { url } - const i = url.indexOf('"') - if (i > 0) { - data.url = url.slice(0, i).trim() - data.class = url.slice(i + 1, -1).trim() - } - return data -} - - -export function renderNavItems(items, opts = {}) { - const { heading, label } = opts - const nav = [] - - if (heading) nav.push(elem('h3', heading)) - - items.forEach(el => { - const item = parseNavItem(el) - const { text, items } = item - if (items) { - nav.push(renderExpandable(text, items)) - } else { - nav.push(renderNavItem(item)) - } - }) - return elem('nav', label && { 'aria-label': label }, join(nav)) -} - - -export function renderExpandable(label, items) { - const nav = renderNavItems(items) - - const html = elem('a', { 'aria-expanded': 'false' }, label) + nav - return elem('span', { 'aria-haspopup': true }, html) -} - -//
-export function renderNavBlocks(data, label) { - const navs = [] - - for (const heading in data) { - const nav = renderNavItems(data[heading], { heading }) - navs.push(nav) - } - - return elem('div', { 'aria-label': label }, join(navs)) -} - - -// the "main" method called by the tag -export function renderNav({ items, label }) { - return Array.isArray(items) ? renderNavItems(items, { label }) : - typeof items == 'object' ? renderNavBlocks(items, label) : '' -} - - -export function renderTOC(data) { - - const items = data.page.headings.filter(el => [2, 3].includes(el.level)) - .map(el => elem('a', { href: '#' + el.id, class: 'level-' + el.level }, el.html)) - - return elem('nav', { 'aria-label': 'Table of contents', is: data.is }, join(items)) -} diff --git a/packages/nuekit/src/layout/page-layout.js b/packages/nuekit/src/layout/page-layout.js deleted file mode 100644 index 81837a6f..00000000 --- a/packages/nuekit/src/layout/page-layout.js +++ /dev/null @@ -1,158 +0,0 @@ -// Standardized page layout / "Modern-day CSS Zen Garden" - -import { parse as parseNue } from 'nuejs-core' -import { renderPage as nuemark, renderInline } from 'nuemark' -import { tags } from 'nuemark/src/tags.js' - -import { renderGallery, renderPrettyDate } from './gallery.js' -import { renderHead } from './head.js' -import { renderNav, renderTOC } from './navi.js' - - -const HEADER = ` -
- -
-` - -const FOOTER = ` -
- -
-` - -const MAIN = ` -
- - -
- - - -
- - -
-` - -const MENU = ` - - - - - -` - - -export function renderRootHTML(data) { - const { language = 'en-US', direction = 'ltr' } = data - const body_class = data.class ? ` class="${data.class}"` : '' - - return ` - - - - - - - - - - - - - - - - - - - - -` -} - - -export function renderSinglePage(body = '', data) { - const { language = 'en-US', direction = 'ltr' } = data - - data.layout = { head: renderHead(data) } - - return ` - - - - - - - ${body} - - -` -} - - -// system components -const system_tags = [ - { name: 'navi', create: renderNav }, - { name: 'gallery', create: renderGallery }, - { name: 'markdown', create: ({ content }) => content ? renderInline(content) : '' }, - { name: 'pretty-date', create: ({ date }) => renderPrettyDate(date) }, - { name: 'toc', create: renderTOC }, - { name: 'image', create: tags.image }, -] - -const nuemark_tags = { gallery: renderGallery, toc: renderTOC } - - -export function renderPage(data, comps) { - - const lib = [...system_tags, ...comps] - - - function renderBlock(name, html) { - - // main: false --> render default MAIN - if (name == 'main' && data.main === false) name = '' - - else if (data[name] === false || data[name.slice(1)] === false) return null - - let comp = comps.find(el => name[0] == '@' ? el.name == name.slice(1) : !el.name && el.tagName == name) - - if (!comp && html) comp = parseNue(html)[0] - - try { - return comp ? comp.render(data, lib) : '' - } catch (e) { - delete data.inline_css - console.error(`Error on <${name}> component`, e) - throw { component: name, ...e } - } - } - - data.layout = { - head: renderHead(data), - custom_head: renderBlock('head').slice(6, -7), - article: nuemark(data.page, { data, lib, tags: nuemark_tags, draw_sections: true }).html, - banner: renderBlock('@banner'), - header: renderBlock('header', data.header && HEADER), - subheader: renderBlock('@subheader'), - - footer: renderBlock('footer', data.footer && FOOTER), - - aside: renderBlock('aside'), - complementary: renderBlock('@complementary'), - - pagehead: renderBlock('@pagehead'), - pagefoot: renderBlock('@pagefoot'), - - bottom: renderBlock('@bottom'), - menu: renderBlock('@menu', data.burger_menu && MENU), - } - - data.layout.main = renderBlock('main', MAIN) - const html = renderRootHTML(data) - - return renderBlock('html', html) -} diff --git a/packages/nuekit/src/layout/page.js b/packages/nuekit/src/layout/page.js new file mode 100644 index 00000000..ea2b4a9f --- /dev/null +++ b/packages/nuekit/src/layout/page.js @@ -0,0 +1,139 @@ + +import { parse as parseNue } from 'nuejs-core' + +import { getLayoutComponents, renderPageList, } from './components.js' + +import { renderHead } from './head.js' + +const SLOTS = 'head banner header subheader pagehead pagefoot aside beside footer bottom main'.split(' ') + +const MAIN = parseNue(` +
+ + +
+ + + +
+ + +
+`)[0] + +function getPageLayout(data) { + const { language = 'en-US', direction = 'ltr' } = data + const body_class = data.class ? ` class="${data.class}"` : '' + + const html = ltrim(` + + + + + + + + + + + + + + + + + + `) + + return parseNue(html)[0] +} + +export function getSPALayout(body, data) { + const { language = 'en-US', direction = 'ltr' } = data + + return ltrim(` + + + ${ renderHead(data) } + + + + ${body} + + + `) +} + +// multiline left trim +function ltrim(str) { + return str.trim().replace(/^ {4}/gm, '') +} + + +export function findComponent(name, lib) { + return lib.find(comp => comp.name == name || !comp.name && comp.tagName == name) +} + + +export function renderSlots(data, lib) { + const slots = {} + + for (const name of SLOTS) { + const comp = findComponent(name, lib) + if (comp && data[name] !== false) { + try { + let html = comp.render(data, lib) + if (html && name == 'head') html = html.slice(6, -7) + slots[name] = html + + } catch (e) { + console.error(`Error rendering layout module: "${name}"`, e) + throw { component: name, ...e } + } + } + } + return slots +} + + +// custom components as Markdown extensions (tags) +function convertToTags(components, data) { + const tags = {} + + components.forEach(comp => { + const { name } = comp + if (name && !SLOTS.includes(name)) { + tags[name] = function(data) { + const { attr, innerHTML } = this + return comp.render({ attr, ...data, innerHTML }, components) + } + } + }) + + return tags +} + + +export function renderPage({ document, data, lib }) { + const comps = [ ...lib, ...getLayoutComponents()] + const slots = renderSlots(data, comps) + + const tags = { + ...convertToTags(comps, data), + 'page-list': renderPageList, + toc: document.renderTOC + } + + // nuemark opts: { data, sections, heading_ids, links, tags } + const { heading_ids, sections, links } = data + const content = document.render({ data, heading_ids, sections, links, tags }) + + //
...
+ if (!slots.main && data.main !== false) { + slots.main = MAIN.render({ ...slots, ...data, content }, comps) + } + + // ... + return getPageLayout(data).render({ system_head: renderHead(data), ...slots, ...data }, comps) +} + diff --git a/packages/nuekit/src/nuekit.js b/packages/nuekit/src/nuekit.js index 932409d1..b1c87486 100644 --- a/packages/nuekit/src/nuekit.js +++ b/packages/nuekit/src/nuekit.js @@ -2,16 +2,18 @@ import { promises as fs, existsSync } from 'node:fs' import { join, parse as parsePath } from 'node:path' import { parse as parseNue, compile as compileNue } from 'nuejs-core' -import { parsePage } from 'nuemark' +import { nuedoc } from 'nuemark' import { lightningCSS, buildJS } from './builder.js' -import { initNueDir } from './init.js' -import { renderPage, renderSinglePage } from './layout/page-layout.js' -import { fswatch } from './nuefs.js' import { createServer, send } from './nueserver.js' -import { createSite } from './site.js' import { printStats, categorize } from './stats.js' +import { initNueDir } from './init.js' +import { createSite } from './site.js' +import { fswatch } from './nuefs.js' + import { log, colors, getAppDir, parsePathParts, extendData } from './util.js' +import { renderPage, getSPALayout } from './layout/page.js' + // the HTML5 doctype const DOCTYPE = '\n\n' @@ -41,14 +43,15 @@ export async function createKit(args) { async function setupStyles(dir, data) { const paths = await site.getStyles(dir, data) + const { assets } = data if (data.inline_css) { - data.inline_css = await buildAllCSS(paths) - data.styles = paths.filter(path => path.includes('@nue')) + assets.inline_css = await buildAllCSS(paths) + assets.styles = paths.filter(path => path.includes('@nue')) } else { - data.inline_css = [] - data.styles = paths + assets.inline_css = [] + assets.styles = paths } } @@ -66,10 +69,11 @@ export async function createKit(args) { async function setupScripts(dir, data) { // scripts - const scripts = data.scripts = await site.getScripts(dir, data) + const { assets } = data + const scripts = assets.scripts = await site.getScripts(dir, data) // components - data.components = await site.getClientComponents(dir, data) + assets.components = await site.getClientComponents(dir, data) // system scripts function push(name) { @@ -78,8 +82,7 @@ export async function createKit(args) { } if (is_dev && data.hotreload !== false) push('hotreload') - if (data.components?.length) push('mount') - if (data.page?.isomorphic) push('nuemark') + if (assets.components?.length) push('mount') if (data.view_transitions || data.router) push('view-transitions') } @@ -88,14 +91,13 @@ export async function createKit(args) { // markdown data: meta, sections, headings, links const raw = await read(path) - const page = parsePage(raw) - const { meta } = page + const document = nuedoc(raw) + const { meta } = document const { dir } = parsePath(path) const data = await site.getData(meta.appdir || dir) - // YAML data - Object.assign(data, parsePathParts(path), { page }) + // include & exclude concatenation extendData(data, meta) // content collection @@ -107,20 +109,24 @@ export async function createKit(args) { // scripts & styling const asset_dir = meta.appdir || dir + data.use_syntax = data.syntax_highlight !== false && document.codeblocks[0] + + data.assets = {} await setupScripts(asset_dir, data) await setupStyles(asset_dir, data) - return data + return { ...data, ...parsePathParts(path), document } } // Markdown page async function renderMPA(path) { const data = await getPageData(path) + const { document } = data const file = parsePath(path) const lib = await site.getServerComponents(data.appdir || file.dir, data) - return DOCTYPE + renderPage(data, lib) + return DOCTYPE + renderPage({ document, data, lib }) } @@ -134,6 +140,7 @@ export async function createKit(args) { const data = { ...await site.getData(appdir), ...parsePathParts(index_path) } // scripts & styling + data.assets = {} await setupScripts(dir, data) await setupStyles(dir, data) @@ -145,7 +152,7 @@ export async function createKit(args) { const [ spa, ...spa_lib ] = parseNue(html) return DOCTYPE + spa.render(data, [...lib, ...spa_lib]) } - const [ spa ] = parseNue(renderSinglePage(html, data)) + const [ spa ] = parseNue(getSPALayout(html, data)) return DOCTYPE + spa.render(data) } @@ -159,8 +166,8 @@ export async function createKit(args) { await buildJS({ outdir: join(process.cwd(), dist, file.dir), path: join(process.cwd(), root, path), - esbuild, minify: is_prod, + esbuild, bundle }) @@ -216,8 +223,8 @@ export async function createKit(args) { // css if (ext == '.css') return await processCSS(file) - // reactive component (.nue, .htm) - if (file.is_nue || file.is_htm) { + // reactive component (.dhtml, .htm) + if (file.is_dhtml || file.is_htm) { const raw = await read(path) const js = await compileNue(raw) await write(js, dir, `${name}.js`) @@ -326,7 +333,7 @@ export async function createKit(args) { if (ret) send({ ...file, ...parsePathParts(file.path), ...ret }) } catch (e) { send({ error: e, ...file }) - console.error(e) + console.error(file.path, e) } // when a file/dir was removed diff --git a/packages/nuekit/src/site.js b/packages/nuekit/src/site.js index 79097309..1acfdee6 100644 --- a/packages/nuekit/src/site.js +++ b/packages/nuekit/src/site.js @@ -3,18 +3,18 @@ import { join, extname, parse as parsePath } from 'node:path' import yaml from 'js-yaml' import { parse as parseNue } from 'nuejs-core' -import { nuemark } from 'nuemark' +import { nuedoc } from 'nuemark' import { fswalk } from './nuefs.js' import { traverseDirsUp, parsePathParts, + joinRootPath, extendData, getAppDir, - log, - toPosix, sortCSS, - joinRootPath + toPosix, + log, } from './util.js' @@ -46,7 +46,9 @@ export async function createSite(args) { return yaml.load(raw) } catch (e) { if (!fileNotFound(e)) { - throw `YAML parse error in ${path}` + const { line, column } = e.mark + const err = { line, column, lineText: e.reason } + throw err } else if (path == env) throw e } } @@ -214,7 +216,7 @@ export async function createSite(args) { self.getData = async function(pagedir) { const data = { nuekit_version, ...site_data, is_prod } - for (const dir of traverseDirsUp(pagedir)) { + for (const dir of [ ...self.globals, ...traverseDirsUp(pagedir)]) { extendData(data, await readDirData(dir)) } return data @@ -241,8 +243,8 @@ export async function createSite(args) { const mds = paths.filter(el => el.endsWith('.md')).map(el => join(dir, el)) for (const path of mds) { - const raw = await read(path) - const { meta } = nuemark(raw) + const document = nuedoc(await read(path)) + const { meta } = document if (!meta.unlisted) arr.push({ ...meta, ...parsePathParts(path) }) } @@ -260,7 +262,7 @@ export async function createSite(args) { let paths = await getAssets({ dir, exts: ['css'], data }) // syntax highlighting - if (data.page?.has_code_blocks && data.syntax_highlight !== false) paths.push(`/@nue/syntax.css`) + if (data.use_syntax) paths.push(`/@nue/syntax.css`) // cascading order: globals -> area -> page sortCSS({ paths, globals: self.globals, dir }) @@ -273,7 +275,7 @@ export async function createSite(args) { } self.getClientComponents = async function(dir, data) { - return await getAssets({ dir, exts: ['nue', 'htm'], to_ext: 'js', data }) + return await getAssets({ dir, exts: ['dhtml', 'htm'], to_ext: 'js', data }) } diff --git a/packages/nuekit/src/stats.js b/packages/nuekit/src/stats.js index abce9158..423f543f 100644 --- a/packages/nuekit/src/stats.js +++ b/packages/nuekit/src/stats.js @@ -1,3 +1,8 @@ + +/* + Not in use currently +*/ + import { promises as fs } from 'node:fs' import { join, extname, parse as parsePath } from 'node:path' @@ -10,7 +15,6 @@ async function readSize(dist, path) { return raw.length } -// not currently in use. make something actually useful later export async function printStats(site, args) { if (args.dryrun) return @@ -56,7 +60,7 @@ export function categorize(paths) { ['js', 'ts'].includes(ext) ? cats.scripts : ext == 'yaml' || ext == 'html' ? misc : base == 'index.html' ? cats.spa : - ext == 'nue' || ext == 'htm' ? cats.islands : + ext == 'dhtml' || ext == 'htm' ? cats.islands : ext == 'md' ? cats.pages : cats.media diff --git a/packages/nuekit/test/component.test.js b/packages/nuekit/test/component.test.js new file mode 100644 index 00000000..9e725b61 --- /dev/null +++ b/packages/nuekit/test/component.test.js @@ -0,0 +1,81 @@ + +// tests for helper/core components +import { + renderPage, + parseClass, + parseLink, + renderNav, + renderLink, + renderMultiNav } from '../src/layout/components.js' + + +test('render page', () => { + const html = renderPage({ + desc: 'Wassup *bro*', + title: 'Yo', + url: '/bruh/' + }) + + expect(html).toStartWith('
  • ') +}) + + +test('render page with a thumb', () => { + const html = renderPage({ title: 'Yo', thumb: 'thumb.png', url: '/' }) + expect(html).toStartWith('
  • ') +}) + + +test('parse class', () => { + expect(parseClass('/foo "bar"')).toEqual({ url: "/foo", class: "bar" }) +}) + +test('parse link', () => { + expect(parseLink({ FAQ: '/faq' })).toEqual({ label: "FAQ", url: "/faq" }) + expect(parseLink({ Hey: '/ "baz"' })).toEqual({ label: "Hey", url: '/', class: 'baz' }) +}) + + +test('parse link / plain string', () => { + expect(parseLink('FAQ')).toEqual({ label: "FAQ", url: "" }) + expect(parseLink('---')).toEqual({ separator: '---' }) +}) + +test('render link', () => { + expect(renderLink({ 'Hey': '/' })).toBe('Hey') + expect(renderLink({ url: '/', label: 'Hey'})).toBe('Hey') +}) + +test('render image link', () => { + const html = renderLink({ + image: 'logo.svg', + class: 'logo', + alt: 'Nue logo', + size: '60 × 18', + url: '/', + }) + + expect(html).toStartWith('') +}) + +test('render categorized nav', () => { + const html = renderMultiNav({ + Hey: [{ Foo: '/'}], + Foo: [{ Bar: '/'}], + + }, { class: 'epic' }) + + expect(html).toStartWith('
    ') + expect(html).toEndWith('
    ') +}) + diff --git a/packages/nuekit/test/kit-init.test.js b/packages/nuekit/test/kit-init.test.js index 65a0a7e8..bffd35ea 100644 --- a/packages/nuekit/test/kit-init.test.js +++ b/packages/nuekit/test/kit-init.test.js @@ -19,11 +19,11 @@ afterAll(async () => await fs.rm(dist, { recursive: true, force: true })) test('bun init', async () => { await initNueDir({ dist, is_dev: true }) const names = await fs.readdir(join(dist, '@nue')) - expect(names.length).toBe(11) + expect(names.length).toBe(10) }) test('esbuild init', async () => { await initNueDir({ dist, is_dev: true, esbuild: true }) const names = await fs.readdir(join(dist, '@nue')) - expect(names.length).toBe(11) + expect(names.length).toBe(10) }) diff --git a/packages/nuekit/test/layout.test.js b/packages/nuekit/test/layout.test.js index c8f57601..31fd5478 100644 --- a/packages/nuekit/test/layout.test.js +++ b/packages/nuekit/test/layout.test.js @@ -1,35 +1,7 @@ -// content collection rendering -import { renderGalleryItem } from '../src/layout/gallery.js' +// content collection rendering +import { findComponent, renderSlots, renderPage, getSPALayout } from '../src/layout/page.js' import { renderHead } from '../src/layout/head.js' -import { - parseClass, - renderExpandable, - parseNavItem, - renderNavItem, - renderNavItems, - renderNavBlocks, -} from '../src/layout/navi.js' - - -test('gallery item', () => { - const html = renderGalleryItem({ - desc: 'Wassup *bro*', - title: 'Yo', - url: '/bruh/' - }) - - expect(html).toStartWith('
  • ') -}) - - -test('gallery with thumbs', () => { - const html = renderGalleryItem({ title: 'Yo', thumb: 'thumb.png', url: '/' }) - expect(html).toStartWith('
  • ') -}) test('', () => { @@ -46,87 +18,45 @@ test('prefetch', () => { expect(head).toInclude('') }) +test('findComponent', () => { + const lib = [ + { name: 'beside' }, + { name: 'hero', tagName: 'header' }, + { tagName: 'header' }, + ] -/***** Navigation tests ****/ - - -test('navi: class shortcut', () => { - expect(parseClass('/foo "bar"')).toEqual({ url: "/foo", class: "bar" }) -}) - -test('navi: plain string', () => { - expect(parseNavItem('FAQ')).toEqual({ text: "FAQ", url: "" }) - expect(parseNavItem('---')).toEqual({ separator: '---' }) -}) - -test('navi: object', () => { - expect(parseNavItem({ FAQ: '/en/faq' })).toEqual({ text: "FAQ", url: "/en/faq" }) - expect(parseNavItem({ FAQ: { foo: 1, bar: 'baz' }})).toEqual({ text: "FAQ", foo: 1, bar: 'baz' }) -}) - -test('navi: array', () => { - const item = parseNavItem({ Company: ['About', 'Blog'] }) - - expect(item.text).toBe('Company') - - expect(item.items).toEqual([ - { text: "About", url: "", }, - { text: "Blog", url: "", } - ]) -}) - -test('render item', () => { - const item = { text: 'FAQ', url: '/faq' } - expect(renderNavItem(item)).toBe('FAQ') -}) + expect(findComponent('beside', lib).name).toBe('beside') + expect(findComponent('header', lib).name).toBeUndefined() -test('render image', () => { - const item = { url: '/foo', image: 'book.jpg', size: '1 x 1' } - const html = renderNavItem(item) - expect(html).toBe('') }) +test('renderSlots', () => { + const lib = [ + { name: 'head', render: () => '' }, + { name: 'banner', render: () => '' }, + { name: 'beside', render: () => '