From 3b8a64dc0c1b3eb69706186eaf551b30b3c75afa Mon Sep 17 00:00:00 2001 From: Anthony Vitacco Date: Sat, 22 Aug 2026 12:11:54 -0400 Subject: [PATCH 1/2] Adding a new code block component I have added a new code block component that handles most of the languages this community is likely to use without having to bring in a framework to do it. This commit also updates all of the existing documentation to use the new code block component. Assisted-by: Claude Signed-off-by: Anthony Vitacco --- README.md | 2 +- docs/_data/sidebar.yml | 1 + docs/components/accordion/index.md | 14 +- docs/components/alert/index.md | 10 +- docs/components/avatar/index.md | 8 +- docs/components/badge/index.md | 14 +- docs/components/billboard/index.md | 14 +- docs/components/breadcrumbs/index.md | 14 +- docs/components/button/index.md | 34 +-- docs/components/calendar-tile/index.md | 6 +- docs/components/callout/index.md | 20 +- docs/components/card/index.md | 34 +-- docs/components/checkbox/index.md | 8 +- docs/components/code-block/index.md | 89 ++++++ docs/components/cta-band/index.md | 18 +- docs/components/cta/index.md | 6 +- docs/components/dialog/index.md | 20 +- docs/components/disclosure/index.md | 10 +- docs/components/dropdown/index.md | 16 +- docs/components/empty-state/index.md | 12 +- docs/components/file-input/index.md | 8 +- docs/components/footer/index.md | 18 +- docs/components/grid/index.md | 14 +- docs/components/header/index.md | 16 +- docs/components/hero/index.md | 10 +- docs/components/icon/index.md | 36 +-- docs/components/input-group/index.md | 12 +- docs/components/link-hub/index.md | 14 +- docs/components/list/index.md | 10 +- docs/components/loader/index.md | 6 +- docs/components/pagination/index.md | 14 +- docs/components/prose/index.md | 16 +- docs/components/quote/index.md | 8 +- docs/components/radio/index.md | 12 +- docs/components/select/index.md | 14 +- docs/components/series-nav/index.md | 8 +- docs/components/sidenav/index.md | 20 +- docs/components/sponsor/index.md | 24 +- docs/components/stat/index.md | 6 +- docs/components/step-indicator/index.md | 14 +- docs/components/subnav/index.md | 12 +- docs/components/switch/index.md | 6 +- docs/components/table/index.md | 34 +-- docs/components/tabs/index.md | 16 +- docs/components/text-input/index.md | 8 +- docs/components/textarea/index.md | 8 +- docs/components/theme-toggle/index.md | 14 +- docs/components/timeline/index.md | 12 +- docs/components/toc/index.md | 20 +- docs/guide/colors/index.md | 12 +- docs/guide/design-tokens/index.md | 5 +- docs/guide/getting-started/index.md | 44 +-- docs/guide/utilities/index.md | 52 ++-- docs/icons/index.md | 6 +- docs/layouts/app-shell/index.md | 132 ++++---- docs/layouts/article/index.md | 106 +++---- docs/layouts/basic/index.md | 94 +++--- docs/layouts/dashboard/index.md | 98 +++--- docs/layouts/docs-sidenav/index.md | 118 +++---- docs/layouts/marketing/index.md | 144 ++++----- docs/src/site.css | 54 ---- src/components/code-block/languages.ts | 259 ++++++++++++++++ .../code-block/vox-code-block.stories.ts | 99 ++++++ src/components/code-block/vox-code-block.ts | 288 ++++++++++++++++++ src/index.ts | 2 + src/tokens/tokens.css | 17 ++ 66 files changed, 1496 insertions(+), 794 deletions(-) create mode 100644 docs/components/code-block/index.md create mode 100644 src/components/code-block/languages.ts create mode 100644 src/components/code-block/vox-code-block.stories.ts create mode 100644 src/components/code-block/vox-code-block.ts diff --git a/README.md b/README.md index db8a068..e72611a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The component catalog, by category: | Images | `vox-avatar`, `vox-billboard`, `vox-icon` — 79 icons ([full set](https://openvoxproject.github.io/voxblocks/icons/)): shared UI baseline, OpenVox marketing, module registry (Forge replacement), fleet console (Puppet Enterprise replacement). ``/`` show one automatically; ``/``/``/`` accept one via an `icon` slot | | Navigation | `vox-breadcrumbs`, `vox-header` (collapses to a menu button below 768px), `vox-pagination`, `vox-series-nav`, `vox-sidenav` (collapses to a menu button below 768px, same as `vox-header`), `vox-subnav`, `vox-tabs`, `vox-toc` | | Overlays | `vox-dialog`, `vox-disclosure`, `vox-dropdown` | -| Page content | `vox-accordion`, `vox-alert`, `vox-badge`, `vox-calendar-tile`, `vox-callout`, `vox-card`, `vox-cta-band`, `vox-empty-state`, `vox-footer`, `vox-grid`, `vox-hero`, `vox-link-hub`, `vox-loader`, `vox-quote`, `vox-sponsor-tier`/`vox-sponsor`, `vox-stat`, `vox-step-indicator`, `vox-timeline` | +| Page content | `vox-accordion`, `vox-alert`, `vox-badge`, `vox-calendar-tile`, `vox-callout`, `vox-card`, `vox-code-block` (built-in syntax highlighting, no Prism/highlight.js/Shiki needed), `vox-cta-band`, `vox-empty-state`, `vox-footer`, `vox-grid`, `vox-hero`, `vox-link-hub`, `vox-loader`, `vox-quote`, `vox-sponsor-tier`/`vox-sponsor`, `vox-stat`, `vox-step-indicator`, `vox-timeline` | | Utilities | Utility classes: spacing (`vox-m-*`/`vox-p-*`), a `vox-container` layout wrapper (1280px, centered, matches `vox-header`/`vox-footer`), typography (`vox-ts-*`, `vox-text-*`), color (`vox-color-*`, `vox-bg-*`, plus an extended `vox-color-{hue}-{100-900}`/`vox-bg-{hue}-{100-900}` palette across 7 hues), border, display, flex, gap, shadow, visibility (incl. `vox-sr-only` and responsive hiding), width, z-index — plus `.vox-table`/`.vox-table-wrap`, `.vox-list`, and `.vox-prose` | Form controls are form-associated custom elements (ElementInternals): they submit values, files, and validity with a plain `
` like native inputs. diff --git a/docs/_data/sidebar.yml b/docs/_data/sidebar.yml index a512215..683edb7 100644 --- a/docs/_data/sidebar.yml +++ b/docs/_data/sidebar.yml @@ -67,6 +67,7 @@ components: - { text: Calendar Tile, link: /components/calendar-tile/ } - { text: Callout, link: /components/callout/ } - { text: Card, link: /components/card/ } + - { text: Code Block, link: /components/code-block/ } - { text: CTA Band, link: /components/cta-band/ } - { text: Empty State, link: /components/empty-state/ } - { text: Footer, link: /components/footer/ } diff --git a/docs/components/accordion/index.md b/docs/components/accordion/index.md index f821c2b..7ef7673 100644 --- a/docs/components/accordion/index.md +++ b/docs/components/accordion/index.md @@ -20,13 +20,13 @@ title: "Accordion" -```html - - -

Yes — it's a drop-in replacement.

-
-
-``` + +<vox-accordion single> + <vox-accordion-item heading="Is OpenVox compatible with Puppet?" open> + <p>Yes — it's a drop-in replacement.</p> + </vox-accordion-item> +</vox-accordion> + ## API diff --git a/docs/components/alert/index.md b/docs/components/alert/index.md index c55531a..a5556d8 100644 --- a/docs/components/alert/index.md +++ b/docs/components/alert/index.md @@ -13,11 +13,11 @@ title: "Alert"

Check the CI logs for details. This one is dismissible.

-```html - -

The Forge API will be read-only on Saturday.

-
-``` + +<vox-alert variant="warning" heading="Maintenance window" dismissible> + <p>The Forge API will be read-only on Saturday.</p> +</vox-alert> + ## API diff --git a/docs/components/avatar/index.md b/docs/components/avatar/index.md index 3b5e7d0..c656aed 100644 --- a/docs/components/avatar/index.md +++ b/docs/components/avatar/index.md @@ -13,10 +13,10 @@ title: "Avatar" -```html - - -``` + +<vox-avatar src="/img/user.png" alt="K. Mensah" size="lg"></vox-avatar> +<vox-avatar initials="KM" alt="K. Mensah"></vox-avatar> + ## API diff --git a/docs/components/badge/index.md b/docs/components/badge/index.md index aa138a4..9a88469 100644 --- a/docs/components/badge/index.md +++ b/docs/components/badge/index.md @@ -16,13 +16,13 @@ title: "Badge" Draft -```html -v8.1.0 -Stable -Deprecated -Breaking -Draft -``` + +<vox-badge variant="brand">v8.1.0</vox-badge> +<vox-badge variant="tip">Stable</vox-badge> +<vox-badge variant="warning">Deprecated</vox-badge> +<vox-badge variant="danger">Breaking</vox-badge> +<vox-badge variant="neutral">Draft</vox-badge> + ## API diff --git a/docs/components/billboard/index.md b/docs/components/billboard/index.md index f3cdbef..e7e9bb3 100644 --- a/docs/components/billboard/index.md +++ b/docs/components/billboard/index.md @@ -15,13 +15,13 @@ title: "Billboard" -```html - - -

Hundreds of modules, tools, and gems…

- Browse modules -
-``` + +<vox-billboard heading="Community-maintained modules" reverse> + <img slot="media" src="/img/community.png" alt="" /> + <p>Hundreds of modules, tools, and gems…</p> + <vox-button slot="actions">Browse modules</vox-button> +</vox-billboard> + ## API diff --git a/docs/components/breadcrumbs/index.md b/docs/components/breadcrumbs/index.md index 99e7b70..9d92ecf 100644 --- a/docs/components/breadcrumbs/index.md +++ b/docs/components/breadcrumbs/index.md @@ -14,10 +14,10 @@ title: "Breadcrumbs" -```html - - Home - Modules - puppet-nginx - -``` + +<vox-breadcrumbs> + <a href="/">Home</a> + <a href="/modules">Modules</a> + <span aria-current="page">puppet-nginx</span> +</vox-breadcrumbs> + diff --git a/docs/components/button/index.md b/docs/components/button/index.md index 92c76a6..4ab7c6a 100644 --- a/docs/components/button/index.md +++ b/docs/components/button/index.md @@ -15,12 +15,12 @@ title: Button Ghost -```html -Brand -Alt -Danger -Ghost -``` + +<vox-button variant="brand">Brand</vox-button> +<vox-button variant="alt">Alt</vox-button> +<vox-button variant="danger">Danger</vox-button> +<vox-button variant="ghost">Ghost</vox-button> + ## Sizes @@ -30,11 +30,11 @@ title: Button Large -```html -Small -Medium -Large -``` + +<vox-button size="sm">Small</vox-button> +<vox-button size="md">Medium</vox-button> +<vox-button size="lg">Large</vox-button> + ## As a link @@ -42,9 +42,9 @@ title: Button Visit OpenVox -```html -Visit OpenVox -``` + +<vox-button href="https://voxpupuli.org/openvox/" target="_blank">Visit OpenVox</vox-button> + ## Disabled @@ -52,9 +52,9 @@ title: Button Disabled -```html -Disabled -``` + +<vox-button disabled>Disabled</vox-button> + ## API diff --git a/docs/components/calendar-tile/index.md b/docs/components/calendar-tile/index.md index a772895..f7d2d04 100644 --- a/docs/components/calendar-tile/index.md +++ b/docs/components/calendar-tile/index.md @@ -14,9 +14,9 @@ title: "Calendar Tile" -```html - -``` + +<vox-calendar-tile date="2026-09-18"></vox-calendar-tile> + ## API diff --git a/docs/components/callout/index.md b/docs/components/callout/index.md index f4f06cf..6da5eed 100644 --- a/docs/components/callout/index.md +++ b/docs/components/callout/index.md @@ -15,11 +15,11 @@ title: "Callout"

A destructive or breaking consequence.

-```html - -

A helpful suggestion.

-
-``` + +<vox-callout variant="tip"> + <p>A helpful suggestion.</p> +</vox-callout> + ## Custom heading @@ -29,11 +29,11 @@ title: "Callout" -```html - -

Use openvox-agent instead.

-
-``` + +<vox-callout variant="warning" heading="Deprecated since 8.0"> + <p>Use <code>openvox-agent</code> instead.</p> +</vox-callout> + ## API diff --git a/docs/components/card/index.md b/docs/components/card/index.md index e7be048..96c1656 100644 --- a/docs/components/card/index.md +++ b/docs/components/card/index.md @@ -22,12 +22,12 @@ title: "Card" -```html - - + +<vox-card heading="OpenVox Server" href="https://docs.openvoxproject.org"> + <span slot="icon"><svg width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="13" y="6" width="22" height="11" rx="2.5"/><line x1="13" y1="11.5" x2="35" y2="11.5" stroke-width="1.4"/><path d="M24 17 L14 34 M24 17 L24 34 M24 17 L34 34"/><circle cx="14" cy="36" r="2.4" fill="currentColor" stroke="none"/><circle cx="24" cy="36" r="2.4" fill="currentColor" stroke="none"/><circle cx="34" cy="36" r="2.4" fill="currentColor" stroke="none"/></svg></span> Installation and configuration of the server components. - -``` +</vox-card> + ## Badge @@ -45,13 +45,13 @@ Use the `badge` slot to place a badge or label in the top-right corner of the ca -```html - - - Pro + +<vox-card heading="Design Patterns"> + <span slot="icon"><vox-icon name="blocks" size="lg"></vox-icon></span> + <vox-badge slot="badge" variant="danger">Pro</vox-badge> Grab-and-go patterns for common site, app, and online product workflows. - -``` +</vox-card> + ## Footer @@ -72,13 +72,13 @@ same row line up their footers even when body length varies. -```html - - 8.1.0 + +<vox-card heading="puppetdb"> + <span slot="badge">8.1.0</span> Installs PostgreSQL and PuppetDB, sets up the connection to Puppet master. - 313,347,865 downloads - -``` + <span slot="footer">313,347,865 downloads</span> +</vox-card> + ## API diff --git a/docs/components/checkbox/index.md b/docs/components/checkbox/index.md index 36f246c..f62b8a7 100644 --- a/docs/components/checkbox/index.md +++ b/docs/components/checkbox/index.md @@ -12,11 +12,11 @@ title: "Checkbox" Unavailable option -```html - + +<vox-checkbox name="subscribe" checked> Subscribe to release announcements - -``` +</vox-checkbox> + ## API diff --git a/docs/components/code-block/index.md b/docs/components/code-block/index.md new file mode 100644 index 0000000..ae504b7 --- /dev/null +++ b/docs/components/code-block/index.md @@ -0,0 +1,89 @@ +--- +title: "Code Block" +--- + +# Code Block + +`` is a syntax-highlighted code block with a copy button. Highlighting is built in — no Prism, highlight.js, or Shiki to set up. + +It recognizes `bash`/`sh`/`shell`/`zsh`, `yaml`/`yml`, `json`, `javascript`/`js`, `typescript`/`ts`, `css`, `html`/`xml`, `ruby`/`rb`, and `puppet`/`pp`. Any other (or omitted) `language` renders as plain, unhighlighted text. + +
+ + class profile::base ( + String $version = '1.0', + ) { + package { 'openssl': + ensure => installed, + } + + file { '/etc/motd': + ensure => file, + content => "Managed by Puppet ${version}", + } + } + +
+ + +<vox-code-block language="puppet" filename="profile/base.pp"> + class profile::base ( + String $version = '1.0', + ) { + package { 'openssl': + ensure => installed, + } + } +</vox-code-block> + + +The code is the element's default slot content, taken as plain text — escape `<` and `&` the way you would inside any `` block. A shared leading indent (from formatting the HTML source itself) is stripped automatically, so the snippet renders flush left regardless of how it's indented in your markup. + +## Line numbers + +Add `line-numbers` for a numbered gutter — useful when prose elsewhere on the page refers to "line 3." + +
+ + Facter.add(:os_release) do + setcode do + if File.exist?('/etc/os-release') + File.read('/etc/os-release') + end + end + end + +
+ + +<vox-code-block language="ruby" line-numbers> + Facter.add(:os_release) do + setcode { File.read('/etc/os-release') } + end +</vox-code-block> + + +## Without a copy button + +
+ + echo "read-only example output" + +
+ + +<vox-code-block language="bash" no-copy> + echo "read-only example output" +</vox-code-block> + + +## API + +| Attribute | Type | Default | Description | +| --- | --- | --- | --- | +| `language` | `string` | `''` | One of the recognized language ids above, or any other string to label the block without highlighting it | +| `filename` | `string` | `''` | Shown at the start of the header, e.g. `profile/base.pp` | +| `line-numbers` | `boolean` | `false` | Adds a numbered gutter | +| `no-copy` | `boolean` | `false` | Hides the copy-to-clipboard button | + +The copy button copies the raw, un-highlighted code via the Clipboard API (falling back to `execCommand` where unavailable) and announces success to screen readers through a live region. The block itself is keyboard-scrollable (`tabindex="0"`) when its content overflows horizontally. diff --git a/docs/components/cta-band/index.md b/docs/components/cta-band/index.md index 2e021b6..25a8920 100644 --- a/docs/components/cta-band/index.md +++ b/docs/components/cta-band/index.md @@ -16,15 +16,15 @@ title: "CTA Band" -```html - -

Install OpenVox in minutes and bring your existing Puppet code with you.

-
- Installation Guide - Join the Community -
-
-``` + +<vox-cta-band heading="Your infrastructure. Your community. Your platform."> + <p>Install OpenVox in minutes and bring your existing Puppet code with you.</p> + <div slot="actions"> + <vox-button href="#" variant="brand">Installation Guide</vox-button> + <vox-button href="#" variant="alt">Join the Community</vox-button> + </div> +</vox-cta-band> + ## API diff --git a/docs/components/cta/index.md b/docs/components/cta/index.md index 6040c25..72f7e87 100644 --- a/docs/components/cta/index.md +++ b/docs/components/cta/index.md @@ -10,9 +10,9 @@ title: "Call to Action" Explore the module catalog -```html -Explore the module catalog -``` + +<vox-cta href="/modules">Explore the module catalog</vox-cta> + ## API diff --git a/docs/components/dialog/index.md b/docs/components/dialog/index.md index 2c3f6e1..33ea8f4 100644 --- a/docs/components/dialog/index.md +++ b/docs/components/dialog/index.md @@ -15,17 +15,17 @@ title: "Dialog" -```html - -

This removes puppet-nginx from your namespace.

- Cancel - Delete -
- - -``` +</script> +
## API diff --git a/docs/components/disclosure/index.md b/docs/components/disclosure/index.md index 8658e1b..daea501 100644 --- a/docs/components/disclosure/index.md +++ b/docs/components/disclosure/index.md @@ -12,11 +12,11 @@ title: "Disclosure" -```html - -

Environment caching, code manager settings…

-
-``` + +<vox-disclosure summary="Show advanced options"> + <p>Environment caching, code manager settings…</p> +</vox-disclosure> + ## API diff --git a/docs/components/dropdown/index.md b/docs/components/dropdown/index.md index 303ac4d..4045528 100644 --- a/docs/components/dropdown/index.md +++ b/docs/components/dropdown/index.md @@ -15,14 +15,14 @@ title: "Dropdown" -```html - - Documentation - Module catalog -
- -
-``` + +<vox-dropdown label="Resources"> + <a href="/docs">Documentation</a> + <a href="/modules">Module catalog</a> + <hr /> + <button type="button">Copy install command</button> +</vox-dropdown> + Arrow keys move through entries; Escape and outside clicks close the menu. diff --git a/docs/components/empty-state/index.md b/docs/components/empty-state/index.md index 4646d0d..4cc018b 100644 --- a/docs/components/empty-state/index.md +++ b/docs/components/empty-state/index.md @@ -14,13 +14,13 @@ title: "Empty State" -```html - - + +<vox-empty-state heading="No modules yet"> + <span slot="icon"><vox-icon name="module" size="xl"></vox-icon></span> You haven't published any modules yet. - Publish a module - -``` + <vox-button slot="actions" size="sm">Publish a module</vox-button> +</vox-empty-state> + ## API diff --git a/docs/components/file-input/index.md b/docs/components/file-input/index.md index 98f510c..7d2a498 100644 --- a/docs/components/file-input/index.md +++ b/docs/components/file-input/index.md @@ -10,14 +10,14 @@ title: "File Input" -```html - +<vox-file-input label="Module tarball" name="tarball" accept=".tar.gz" button-label="Choose a file" -> -``` +></vox-file-input> + ## API diff --git a/docs/components/footer/index.md b/docs/components/footer/index.md index aa7c4ff..aa3467c 100644 --- a/docs/components/footer/index.md +++ b/docs/components/footer/index.md @@ -26,12 +26,12 @@ title: "Footer" -```html - - - Modules - Documentation - - © OpenVox. Apache-2.0 licensed. - -``` + +<vox-footer> + <vox-footer-column heading="Project"> + <a href="/modules">Modules</a> + <a href="/docs">Documentation</a> + </vox-footer-column> + <span slot="bottom">© OpenVox. Apache-2.0 licensed.</span> +</vox-footer> + diff --git a/docs/components/grid/index.md b/docs/components/grid/index.md index a14bb90..8687ae8 100644 --- a/docs/components/grid/index.md +++ b/docs/components/grid/index.md @@ -14,14 +14,14 @@ title: "Grid" -```html - - - - + +<vox-grid min="240px" gap="lg"> + <vox-card heading="…">…</vox-card> + <vox-card heading="…">…</vox-card> +</vox-grid> - -``` +<vox-grid cols="2">…</vox-grid> + ## API diff --git a/docs/components/header/index.md b/docs/components/header/index.md index a22cbfd..1860926 100644 --- a/docs/components/header/index.md +++ b/docs/components/header/index.md @@ -15,14 +15,14 @@ title: "Header" -```html - - - Modules - Docs - Sponsor - -``` + +<vox-header site-title="OpenVox" href="/"> + <img slot="logo" src="/img/logo.svg" alt="" /> + <a href="/modules" aria-current="page">Modules</a> + <a href="/docs">Docs</a> + <vox-button slot="actions" size="sm" variant="alt">Sponsor</vox-button> +</vox-header> + ## API diff --git a/docs/components/hero/index.md b/docs/components/hero/index.md index 50783d1..3165f7e 100644 --- a/docs/components/hero/index.md +++ b/docs/components/hero/index.md @@ -14,12 +14,12 @@ title: "Hero" -```html - + +<vox-hero eyebrow="Documentation" heading="OpenVox Server"> Everything you need to install, configure, and operate… - Get started - -``` + <vox-button slot="actions">Get started</vox-button> +</vox-hero> + ## API diff --git a/docs/components/icon/index.md b/docs/components/icon/index.md index 8fc6d5e..265cfc3 100644 --- a/docs/components/icon/index.md +++ b/docs/components/icon/index.md @@ -12,10 +12,10 @@ A handful of components use this set internally too — `` and ` - -``` + +<vox-icon name="search"></vox-icon> +<vox-icon name="chevron-down"></vox-icon> + ## Sizes @@ -28,10 +28,10 @@ Browse the [full icon set](../../icons/) to see every available name. -```html - - -``` + +<vox-icon name="search" size="sm"></vox-icon> +<vox-icon name="search" size="xl"></vox-icon> + ## Color @@ -43,11 +43,11 @@ Icons inherit `color` from their context — no separate color prop. Pair with t -```html - - - -``` + +<span class="vox-color-warning"> + <vox-icon name="warning"></vox-icon> +</span> + ## Accessible icon-only controls @@ -59,11 +59,11 @@ Icons render `aria-hidden="true"` by default, since they're normally paired with -```html - - - -``` + +<vox-button size="sm"> + <vox-icon name="delete" label="Delete"></vox-icon> +</vox-button> + ## API diff --git a/docs/components/input-group/index.md b/docs/components/input-group/index.md index 292f7c9..520c339 100644 --- a/docs/components/input-group/index.md +++ b/docs/components/input-group/index.md @@ -17,12 +17,12 @@ title: "Input Group" -```html - - - Search - -``` + +<vox-input-group> + <vox-input aria-label="Search modules" placeholder="Search modules…"></vox-input> + <vox-button>Search</vox-button> +</vox-input-group> + Inside a group, give inputs an `aria-label` instead of a visible `label` so the row stays aligned. diff --git a/docs/components/link-hub/index.md b/docs/components/link-hub/index.md index 1522f6f..965e4fb 100644 --- a/docs/components/link-hub/index.md +++ b/docs/components/link-hub/index.md @@ -23,14 +23,14 @@ title: "Link Hub" -```html - - - + +<vox-link-hub> + <vox-link-hub-item href="/start" heading="Getting started"> + <vox-icon slot="icon" name="book" size="sm"></vox-icon> Install OpenVox and apply your first manifest. - - -``` + </vox-link-hub-item> +</vox-link-hub> + ## API diff --git a/docs/components/list/index.md b/docs/components/list/index.md index 1b12828..f1f04ff 100644 --- a/docs/components/list/index.md +++ b/docs/components/list/index.md @@ -17,8 +17,8 @@ Lists are styled with light-DOM utility classes (shadow DOM can't reach into slo
  • items
  • -```html -
    -
    -
    -``` + +<ul class="vox-list">…</ul> +<ul class="vox-list vox-list--plain">…</ul> <!-- no bullets --> +<ul class="vox-list vox-list--inline">…</ul> <!-- horizontal --> + diff --git a/docs/components/loader/index.md b/docs/components/loader/index.md index ad351a1..1d86418 100644 --- a/docs/components/loader/index.md +++ b/docs/components/loader/index.md @@ -12,9 +12,9 @@ title: "Loading Indicator" -```html - -``` + +<vox-loader size="lg" label="Fetching modules"></vox-loader> + ## API diff --git a/docs/components/pagination/index.md b/docs/components/pagination/index.md index 9af39d2..360f2fa 100644 --- a/docs/components/pagination/index.md +++ b/docs/components/pagination/index.md @@ -17,10 +17,10 @@ title: "Pagination" -```html - - 1 - 2 - 3 - -``` + +<vox-pagination> + <a href="?page=1">1</a> + <a href="?page=2" aria-current="page">2</a> + <a href="?page=3">3</a> +</vox-pagination> + diff --git a/docs/components/prose/index.md b/docs/components/prose/index.md index 26a2649..e8caf73 100644 --- a/docs/components/prose/index.md +++ b/docs/components/prose/index.md @@ -22,11 +22,11 @@ Stack multiple blocks and they space themselves apart automatically:

    Second block, spaced apart from the first.

    -```html -
    -

    OpenVox began as a soft fork…

    -
    -
    -

    The real difference is who steers it.

    -
    -``` + +<div class="vox-prose"> + <p>OpenVox began as a soft fork…</p> +</div> +<div class="vox-prose"> + <p>The real difference is who steers it.</p> +</div> + diff --git a/docs/components/quote/index.md b/docs/components/quote/index.md index 83f5d04..ea35c23 100644 --- a/docs/components/quote/index.md +++ b/docs/components/quote/index.md @@ -12,11 +12,11 @@ title: "Quote" -```html - + +<vox-quote attribution="A happy operator" detail="Somewhere in production"> Migrating to OpenVox was the least dramatic change we made all year. - -``` +</vox-quote> + ## API diff --git a/docs/components/radio/index.md b/docs/components/radio/index.md index e4905e8..8d8624b 100644 --- a/docs/components/radio/index.md +++ b/docs/components/radio/index.md @@ -14,12 +14,12 @@ title: "Radio Input" -```html - - System packages - Ruby gem - -``` + +<vox-radio-group label="Install method" name="method" value="pkg"> + <vox-radio value="pkg">System packages</vox-radio> + <vox-radio value="gem">Ruby gem</vox-radio> +</vox-radio-group> + ## API diff --git a/docs/components/select/index.md b/docs/components/select/index.md index 95314ad..592d3cc 100644 --- a/docs/components/select/index.md +++ b/docs/components/select/index.md @@ -15,13 +15,13 @@ title: "Select Input" -```html - - - - - -``` + +<vox-select label="Operating system" name="os"> + <option value="">Choose one…</option> + <option value="deb">Debian / Ubuntu</option> + <option value="rpm">RHEL / Fedora</option> +</vox-select> + ## API diff --git a/docs/components/series-nav/index.md b/docs/components/series-nav/index.md index ac0e814..b521244 100644 --- a/docs/components/series-nav/index.md +++ b/docs/components/series-nav/index.md @@ -15,14 +15,14 @@ title: "Series Nav" > -```html - +<vox-series-nav previous-href="/install" previous-label="Installing the server" next-href="/agents" next-label="Configuring agents" -> -``` +></vox-series-nav> + ## API diff --git a/docs/components/sidenav/index.md b/docs/components/sidenav/index.md index 8a09cab..fb8392f 100644 --- a/docs/components/sidenav/index.md +++ b/docs/components/sidenav/index.md @@ -23,17 +23,17 @@ title: "Sidenav" -```html - - - + +<vox-sidenav label="Server docs"> + <vox-sidenav-item href="/server" current> + <vox-icon slot="icon" name="dashboard" size="sm"></vox-icon> Overview - - - Debian / Ubuntu - - -``` + </vox-sidenav-item> + <vox-sidenav-group heading="Installation" open> + <vox-sidenav-item href="/server/deb">Debian / Ubuntu</vox-sidenav-item> + </vox-sidenav-group> +</vox-sidenav> + Icons are optional and per-item — use them for top-level entries, skip them inside groups, whatever the nav actually needs. diff --git a/docs/components/sponsor/index.md b/docs/components/sponsor/index.md index efc55af..4c33e26 100644 --- a/docs/components/sponsor/index.md +++ b/docs/components/sponsor/index.md @@ -19,15 +19,15 @@ the sponsor's site. -```html - - Ongoing monthly or annual support. - + +<vox-sponsor-tier heading="Current Sponsors"> + <span slot="description">Ongoing monthly or annual support.</span> + <vox-sponsor name="Example Cloud" href="https://example.com"> $500/month · CI infrastructure - - $50/month - -``` + </vox-sponsor> + <vox-sponsor name="Anonymous">$50/month</vox-sponsor> +</vox-sponsor-tier> + ## Logo @@ -44,11 +44,11 @@ Set `logo` to show an image instead of just the sponsor name. -```html - + +<vox-sponsor name="OpenVox" href="https://example.com" logo="/logos/openvox.svg"> $1000/month · Dedicated servers - -``` +</vox-sponsor> + ## Multiple tiers diff --git a/docs/components/stat/index.md b/docs/components/stat/index.md index 256694e..957387c 100644 --- a/docs/components/stat/index.md +++ b/docs/components/stat/index.md @@ -12,9 +12,9 @@ title: "Stat" Since the OpenVox fork -```html -Community-maintained -``` + +<vox-stat value="450+" label="Modules">Community-maintained</vox-stat> + ## API diff --git a/docs/components/step-indicator/index.md b/docs/components/step-indicator/index.md index 3a2b9ee..5694528 100644 --- a/docs/components/step-indicator/index.md +++ b/docs/components/step-indicator/index.md @@ -15,13 +15,13 @@ title: "Step Indicator" -```html - - - - - -``` + +<vox-step-indicator> + <vox-step label="Choose plan" state="complete"></vox-step> + <vox-step label="Configure" state="current"></vox-step> + <vox-step label="Review"></vox-step> +</vox-step-indicator> + ## API diff --git a/docs/components/subnav/index.md b/docs/components/subnav/index.md index 28f1d5f..80b6761 100644 --- a/docs/components/subnav/index.md +++ b/docs/components/subnav/index.md @@ -15,12 +15,12 @@ title: "Subnav" -```html - - Overview - Installation - -``` + +<vox-subnav label="Server docs"> + <a href="/server" aria-current="page">Overview</a> + <a href="/server/install">Installation</a> +</vox-subnav> + ## API diff --git a/docs/components/switch/index.md b/docs/components/switch/index.md index cd83461..49bc2e8 100644 --- a/docs/components/switch/index.md +++ b/docs/components/switch/index.md @@ -11,9 +11,9 @@ title: "Switch" Send anonymous usage stats -```html -Enable dark mode -``` + +<vox-switch name="dark" checked>Enable dark mode</vox-switch> + ## API diff --git a/docs/components/table/index.md b/docs/components/table/index.md index 0b96826..6686e80 100644 --- a/docs/components/table/index.md +++ b/docs/components/table/index.md @@ -17,11 +17,11 @@ Tables are styled with light-DOM utility classes on native `` elements (s
    -```html -…
    -…
    -…
    -``` + +<table class="vox-table">…</table> +<table class="vox-table vox-table--striped">…</table> +<table class="vox-table vox-table--compact">…</table> + ## Comparison tables @@ -41,15 +41,15 @@ background as column headers automatically. -```html -
    - - - - - - - -
    DimensionOpenVoxAnsible
    ModelDeclarativeProcedural
    -
    -``` + +<div class="vox-table-wrap"> + <table class="vox-table"> + <thead> + <tr><th scope="col">Dimension</th><th scope="col">OpenVox</th><th scope="col">Ansible</th></tr> + </thead> + <tbody> + <tr><th scope="row">Model</th><td>Declarative</td><td>Procedural</td></tr> + </tbody> + </table> +</div> + diff --git a/docs/components/tabs/index.md b/docs/components/tabs/index.md index 938366e..c1aef32 100644 --- a/docs/components/tabs/index.md +++ b/docs/components/tabs/index.md @@ -15,14 +15,14 @@ title: "Tabs" -```html - - Debian / Ubuntu - RHEL / Fedora - apt install openvox-agent - dnf install openvox-agent - -``` + +<vox-tabs> + <vox-tab slot="tab" panel="deb" selected>Debian / Ubuntu</vox-tab> + <vox-tab slot="tab" panel="rpm">RHEL / Fedora</vox-tab> + <vox-tab-panel name="deb">apt install openvox-agent</vox-tab-panel> + <vox-tab-panel name="rpm">dnf install openvox-agent</vox-tab-panel> +</vox-tabs> + Left/Right arrows switch tabs. Fires `vox-tab-change` with `detail.panel`. diff --git a/docs/components/text-input/index.md b/docs/components/text-input/index.md index a9e382c..fb835b2 100644 --- a/docs/components/text-input/index.md +++ b/docs/components/text-input/index.md @@ -12,14 +12,14 @@ title: "Text Input" -```html - +<vox-input label="Module name" name="module" placeholder="puppet-nginx" note="Lowercase, hyphen-separated." -> -``` +></vox-input> + ## API diff --git a/docs/components/textarea/index.md b/docs/components/textarea/index.md index 31714f0..be5edb4 100644 --- a/docs/components/textarea/index.md +++ b/docs/components/textarea/index.md @@ -10,14 +10,14 @@ title: "Textarea" -```html - +<vox-textarea label="Description" name="description" rows="4" note="Markdown is supported." -> -``` +></vox-textarea> + ## API diff --git a/docs/components/theme-toggle/index.md b/docs/components/theme-toggle/index.md index c0c3662..9b110de 100644 --- a/docs/components/theme-toggle/index.md +++ b/docs/components/theme-toggle/index.md @@ -10,24 +10,24 @@ title: "Theme Toggle" -```html - -``` + +<vox-theme-toggle></vox-theme-toggle> + ## Required: a blocking script in `` A custom element can't upgrade until the browser parses it, which is well after first paint — too late to prevent a flash of the wrong theme. Every page still needs its own small blocking script in ``, *before* any stylesheet, to apply the stored (or system) theme immediately: -```html - -``` +</script> + `` picks up whatever this script already applied — it doesn't set an initial theme itself, only reflects and toggles it. diff --git a/docs/components/timeline/index.md b/docs/components/timeline/index.md index a84871e..feb3078 100644 --- a/docs/components/timeline/index.md +++ b/docs/components/timeline/index.md @@ -20,13 +20,13 @@ title: "Timeline" -```html - - + +<vox-timeline> + <vox-timeline-item date="Jan 2025" heading="First release"> OpenVox 8.x packages published. - - -``` + </vox-timeline-item> +</vox-timeline> + ## API diff --git a/docs/components/toc/index.md b/docs/components/toc/index.md index fa54ab2..0405820 100644 --- a/docs/components/toc/index.md +++ b/docs/components/toc/index.md @@ -18,17 +18,17 @@ title: "Table of Contents" -```html - - Overview - + +<vox-toc heading="On this page"> + <vox-toc-item href="#overview">Overview</vox-toc-item> + <vox-toc-item href="#palette" current> Palette - Soft badge - Solid fill - - Usage - -``` + <vox-toc-item slot="children" href="#soft-badge">Soft badge</vox-toc-item> + <vox-toc-item slot="children" href="#solid-fill">Solid fill</vox-toc-item> + </vox-toc-item> + <vox-toc-item href="#usage">Usage</vox-toc-item> +</vox-toc> + `vox-toc` doesn't read the page's headings for you — build `items` from whatever heading data your framework already tracks (e.g. VitePress's `useSidebar()`/route data) and re-render on scroll to keep `current` in sync. diff --git a/docs/guide/colors/index.md b/docs/guide/colors/index.md index a266267..111df0f 100644 --- a/docs/guide/colors/index.md +++ b/docs/guide/colors/index.md @@ -306,18 +306,18 @@ Lower numbers are lighter tints, higher numbers are darker shades. `500` is each Every swatch is a `--vox-palette-{hue}-{step}` custom property, defined once in `src/tokens/palette.css` (`:root` only — no dark-mode override): -```css + --vox-palette-teal-500: #41d2c9; -``` + ## Utility classes `vox-color-{hue}-{step}` sets text color, `vox-bg-{hue}-{step}` sets background: -```html -New -Beta -``` + +<span class="vox-bg-teal-100 vox-color-teal-900">New</span> +<span class="vox-bg-purple-600" style="color: white;">Beta</span> + ## Pairing for contrast diff --git a/docs/guide/design-tokens/index.md b/docs/guide/design-tokens/index.md index 31bc976..cb5649e 100644 --- a/docs/guide/design-tokens/index.md +++ b/docs/guide/design-tokens/index.md @@ -22,6 +22,7 @@ The default palette, "Deep Teal," is an OpenVox-specific identity rather than a | `--vox-color-bg`, `-alt`, `-soft`, `-elv` | Surfaces | | `--vox-color-text-1/2/3` | Text emphasis levels | | `--vox-color-divider`, `--vox-color-border` | Lines | +| `--vox-code-comment/keyword/string/number/function/property/tag` | [``](../../components/code-block/) syntax highlighting — one token role per palette hue | ## Typography, shape, spacing @@ -36,14 +37,14 @@ The default palette, "Deep Teal," is an OpenVox-specific identity rather than a ## Overriding -```css + :root { /* Give buttons and links your site's accent color */ --vox-color-brand-1: #067f56; --vox-color-brand-2: #08976a; --vox-color-brand-3: #0aa877; } -``` + ## Dark mode diff --git a/docs/guide/getting-started/index.md b/docs/guide/getting-started/index.md index 42bb388..a3395ec 100644 --- a/docs/guide/getting-started/index.md +++ b/docs/guide/getting-started/index.md @@ -8,52 +8,52 @@ VoxBlocks is a set of [Web Components](https://developer.mozilla.org/en-US/docs/ ## With a bundler (npm) -```sh + npm install @voxpupuli/voxblocks -``` + Import once in your entry point — this registers every `` element and pulls in the design tokens: -```js + import '@voxpupuli/voxblocks'; import '@voxpupuli/voxblocks/voxblocks.css'; -``` + ## Without a build step (CDN / static sites) The CDN build bundles Lit, so a single module script is enough: -```html - - -``` + +<link rel="stylesheet" href="https://unpkg.com/@voxpupuli/voxblocks/dist/cdn/voxblocks.css"> +<script type="module" src="https://unpkg.com/@voxpupuli/voxblocks/dist/cdn/voxblocks.js"></script> + ## Use the components -```html -Visit OpenVox + +<vox-button href="https://voxpupuli.org/openvox/">Visit OpenVox</vox-button> - -

    Components are just HTML tags — no framework required.

    -
    -``` +<vox-callout variant="tip"> + <p>Components are just HTML tags — no framework required.</p> +</vox-callout> +
    ## Dark mode Tokens switch automatically when an ancestor element (usually ``) has the class `dark` — the VitePress convention used by the OpenVox docs — or the attribute `data-vox-theme="dark"`: -```html - -``` + +<html data-vox-theme="dark"> + ## Fonts VoxBlocks doesn't bundle font files — it names Inter (body), Source Serif 4 (headings), and JetBrains Mono (code) in its font stacks and falls back to system fonts if they aren't loaded, the same way it's always handled Inter. Load them yourself for the intended look, e.g.: -```html - - - -``` + +<link rel="preconnect" href="https://fonts.googleapis.com"> +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> +<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,wght@0,600;1,500&family=Inter:wght@400;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> + All three are open licensed (Source Serif 4 and JetBrains Mono included) and safe to self-host instead if a site doesn't want the Google Fonts request. diff --git a/docs/guide/utilities/index.md b/docs/guide/utilities/index.md index 1d5216d..d04c518 100644 --- a/docs/guide/utilities/index.md +++ b/docs/guide/utilities/index.md @@ -14,20 +14,20 @@ Included in `voxblocks.css`; nothing extra to load. `vox-container` centers content at 1280px with responsive horizontal padding — the same measure `vox-header` and `vox-footer` already use internally. Wrap other page sections (`vox-hero`, `vox-grid`, prose) in it so they line up with the header and footer instead of running edge-to-edge. -```html -
    - -
    -``` + +<div class="vox-container"> + <vox-hero heading="…">…</vox-hero> +</div> + ## Spacing Margin and padding: `vox-{m|p}-{side}-{size}` where side is `top`, `right`, `bottom`, `left`, `x`, `y`, or `all`. -```html -
    -
    centered column
    -``` + +<div class="vox-m-top-lg vox-p-all-md">…</div> +<div class="vox-m-x-auto vox-max-width-md">centered column</div> + `vox-m-x-auto` centers block elements horizontally. @@ -39,10 +39,10 @@ Type scale `vox-ts-{xs|sm|md|lg|xl|xxl|3xl}` (12–36px), weight `vox-text-{regu

    Small uppercase label

    Monospace text

    -```html -

    Extra-large bold text

    -

    Small uppercase label

    -``` + +<p class="vox-ts-xl vox-text-bold">Extra-large bold text</p> +<p class="vox-ts-sm vox-text-uppercase vox-color-text-3">Small uppercase label</p> + ## Color @@ -57,9 +57,9 @@ Background: `vox-bg`, `vox-bg-{alt|soft|elv}`, `vox-bg-brand`, `vox-bg-{brand|ti danger-soft -```html -brand-soft -``` + +<span class="vox-bg-brand-soft vox-color-brand">brand-soft</span> + An extended, non-themed palette is also available — `vox-color-{hue}-{100-900}` / `vox-bg-{hue}-{100-900}` across seven hues (gray, teal, blue, green, gold, red, purple). See [Colors](../colors/) for the full swatch reference and contrast pairing guidance. @@ -69,9 +69,9 @@ An extended, non-themed palette is also available — `vox-color-{hue}-{100-900}
    Bordered, rounded box
    -```html -
    -``` + +<div class="vox-border-all vox-radius-lg vox-p-all-md">…</div> + ## Display @@ -87,9 +87,9 @@ Direction `vox-flex-{row|column}`, wrapping `vox-flex-{wrap|nowrap}`, main axis Right -```html -
    -``` + +<div class="vox-display-flex vox-justify-between vox-items-center">…</div> + ## Gap @@ -110,10 +110,10 @@ Direction `vox-flex-{row|column}`, wrapping `vox-flex-{wrap|nowrap}`, main axis - `vox-sr-only` — visually hidden, still announced by screen readers. - Responsive: `vox-hide-{sm|md|lg}-down` hides at and below the breakpoint, `vox-hide-{sm|md|lg}-up` at and above. Breakpoints: `sm` 480px, `md` 768px, `lg` 1024px. -```html -Only shown on wider screens -Screen-reader-only label -``` + +<span class="vox-hide-md-down">Only shown on wider screens</span> +<span class="vox-sr-only">Screen-reader-only label</span> + ## Width diff --git a/docs/icons/index.md b/docs/icons/index.md index 8db5529..a0c4672 100644 --- a/docs/icons/index.md +++ b/docs/icons/index.md @@ -8,9 +8,9 @@ title: "Icons" Use any icon by name via [``](../components/icon/): -```html - -``` + +<vox-icon name="search"></vox-icon> + ## Navigation & search diff --git a/docs/layouts/app-shell/index.md b/docs/layouts/app-shell/index.md index ca1b64a..150e94b 100644 --- a/docs/layouts/app-shell/index.md +++ b/docs/layouts/app-shell/index.md @@ -62,83 +62,83 @@ A logged-in application screen: persistent sidebar navigation, a data table as t ## Full HTML -```html - - - - - - Modules — Forge Admin - - - - - - - + +<!doctype html> +<html lang="en"> +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Modules — Forge Admin</title> + <link rel="stylesheet" href="https://unpkg.com/@openvoxproject/voxblocks/dist/cdn/voxblocks.css" /> + <script type="module" src="https://unpkg.com/@openvoxproject/voxblocks/dist/cdn/voxblocks.js"></script> +</head> +<body> + <vox-header site-title="Forge Admin" href="/"> + <vox-avatar slot="actions" size="sm" initials="DR" alt="D. Ramírez"></vox-avatar> + </vox-header> -
    -
    - - - + <div class="vox-display-flex vox-items-start vox-gap-xl vox-p-all-lg"> + <div style="flex: 0 0 13rem;"> + <vox-sidenav label="Admin"> + <vox-sidenav-item href="/admin/modules" current> + <vox-icon slot="icon" name="module" size="sm"></vox-icon> Modules - - - + </vox-sidenav-item> + <vox-sidenav-item href="/admin/maintainers"> + <vox-icon slot="icon" name="people" size="sm"></vox-icon> Maintainers - - - + </vox-sidenav-item> + <vox-sidenav-item href="/admin/reports"> + <vox-icon slot="icon" name="report" size="sm"></vox-icon> Reports - - - + </vox-sidenav-item> + <vox-sidenav-item href="/admin/settings"> + <vox-icon slot="icon" name="settings" size="sm"></vox-icon> Settings - - -
    + </vox-sidenav-item> + </vox-sidenav> + </div> -
    -
    -

    Modules

    - New module -
    + <div style="flex: 1 1 auto; min-width: 0;"> + <div class="vox-display-flex vox-justify-between vox-items-center vox-m-bottom-md"> + <h1 class="vox-ts-xl vox-m-y-none">Modules</h1> + <vox-button size="sm">New module</vox-button> + </div> -
    - - - - - - - - - -
    ModuleVersionStatusDownloads
    puppet-nginx6.0.1Published1.2M
    puppet-systemd8.1.0Published3.4M
    puppet-postgresql10.5.0-rc1Pre-release2.1M
    -
    + <div class="vox-table-wrap"> + <table class="vox-table vox-table--striped"> + <thead> + <tr><th>Module</th><th>Version</th><th>Status</th><th>Downloads</th></tr> + </thead> + <tbody> + <tr><td>puppet-nginx</td><td>6.0.1</td><td><vox-badge variant="tip">Published</vox-badge></td><td>1.2M</td></tr> + <tr><td>puppet-systemd</td><td>8.1.0</td><td><vox-badge variant="tip">Published</vox-badge></td><td>3.4M</td></tr> + <tr><td>puppet-postgresql</td><td>10.5.0-rc1</td><td><vox-badge variant="warning">Pre-release</vox-badge></td><td>2.1M</td></tr> + </tbody> + </table> + </div> - - - 1 - 2 - 3 - - -
    -
    - - -``` + <vox-pagination class="vox-m-top-md"> + <a href="?page=1">←</a> + <a href="?page=1" aria-current="page">1</a> + <a href="?page=2">2</a> + <a href="?page=3">3</a> + <a href="?page=2">→</a> + </vox-pagination> + </div> + </div> +</body> +</html> +
    ## Empty state When there's nothing to show yet (a fresh account, a cleared filter), swap the table for `` rather than rendering an empty table: -```html - - + +<vox-empty-state heading="No modules yet"> + <span slot="icon"><vox-icon name="module" size="xl"></vox-icon></span> You haven't published any modules. Your first one is easier than you think. - Publish a module - -``` + <vox-button slot="actions" size="sm">Publish a module</vox-button> +</vox-empty-state> +
    diff --git a/docs/layouts/article/index.md b/docs/layouts/article/index.md index 5dd64b5..be1d588 100644 --- a/docs/layouts/article/index.md +++ b/docs/layouts/article/index.md @@ -55,67 +55,67 @@ A single blog post: title, byline, body copy, a pull quote, and a set of related ## Full HTML -```html - - - - - - Why we forked, and what happens next — OpenVox Blog - - - - - - Blog - Modules - + +<!doctype html> +<html lang="en"> +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Why we forked, and what happens next — OpenVox Blog</title> + <link rel="stylesheet" href="https://unpkg.com/@openvoxproject/voxblocks/dist/cdn/voxblocks.css" /> + <script type="module" src="https://unpkg.com/@openvoxproject/voxblocks/dist/cdn/voxblocks.js"></script> +</head> +<body> + <vox-header site-title="OpenVox Blog" href="/"> + <a href="/blog" aria-current="page">Blog</a> + <a href="/modules">Modules</a> + </vox-header> -
    - - Blog - Why we forked - + <div class="vox-p-all-lg vox-max-width-md vox-m-x-auto"> + <vox-breadcrumbs> + <a href="/blog">Blog</a> + <span aria-current="page">Why we forked</span> + </vox-breadcrumbs> -

    Why we forked, and what happens next

    + <h1 class="vox-m-top-md vox-m-bottom-sm">Why we forked, and what happens next</h1> -
    - - S. Almeida · Nov 12, 2024 · 6 min read -
    + <div class="vox-display-flex vox-items-center vox-gap-sm vox-m-bottom-lg"> + <vox-avatar size="sm" initials="SA" alt="S. Almeida"></vox-avatar> + <span class="vox-color-text-2 vox-ts-sm">S. Almeida · Nov 12, 2024 · 6 min read</span> + </div> -
    -

    When the license terms changed, the community had a choice: adapt in place, or take the code and keep going on our own terms. We chose the second one.

    -

    OpenVox exists because the alternative — hoping a vendor's incentives stay aligned with ours indefinitely — isn't a plan, it's a bet.

    + <div class="vox-prose"> + <p>When the license terms changed, the community had a choice: adapt in place, or take the code and keep going on our own terms. We chose the second one.</p> + <p>OpenVox exists because the alternative — hoping a vendor's incentives stay aligned with ours indefinitely — isn't a plan, it's a bet.</p> -

    What actually changes for you

    -

    If you're already running Puppet, nothing breaks. Manifests, modules, and the agent protocol are unchanged. What's different is who's steering.

    -
    + <h2>What actually changes for you</h2> + <p>If you're already running Puppet, nothing breaks. Manifests, modules, and the agent protocol are unchanged. What's different is who's steering.</p> + </div> -
    - + <div class="vox-m-y-xl vox-display-flex vox-justify-center"> + <vox-quote attribution="S. Almeida"> We didn't fork to be different. We forked to stay the same, on our own terms. - -
    + </vox-quote> + </div> -

    Related posts

    - - + <h2 class="vox-m-bottom-md">Related posts</h2> + <vox-link-hub> + <vox-link-hub-item href="/blog/governance-one-year-in" heading="Governance, one year in"> What worked, what didn't, and what we'd change. - - + </vox-link-hub-item> + <vox-link-hub-item href="/blog/migrating-without-downtime" heading="Migrating without downtime"> A field report from three production upgrades. - - -
    + </vox-link-hub-item> + </vox-link-hub> + </div> - - - All posts - RSS - - © OpenVox. Apache-2.0 licensed. - - - -``` + <vox-footer> + <vox-footer-column heading="Blog"> + <a href="/blog">All posts</a> + <a href="/blog/rss.xml">RSS</a> + </vox-footer-column> + <span slot="bottom">© OpenVox. Apache-2.0 licensed.</span> + </vox-footer> +</body> +</html> +
    diff --git a/docs/layouts/basic/index.md b/docs/layouts/basic/index.md index 24c44c2..50e47f0 100644 --- a/docs/layouts/basic/index.md +++ b/docs/layouts/basic/index.md @@ -44,56 +44,56 @@ The simplest starting point: a header, a single readable column of content, and ## Full HTML -```html - - - - - - Governance — OpenVox - - - - - - Modules - Governance - Blog - + +<!doctype html> +<html lang="en"> +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Governance — OpenVox</title> + <link rel="stylesheet" href="https://unpkg.com/@openvoxproject/voxblocks/dist/cdn/voxblocks.css" /> + <script type="module" src="https://unpkg.com/@openvoxproject/voxblocks/dist/cdn/voxblocks.js"></script> +</head> +<body> + <vox-header site-title="OpenVox" href="/"> + <a href="/modules">Modules</a> + <a href="/governance" aria-current="page">Governance</a> + <a href="/blog">Blog</a> + </vox-header> -
    - - Home - Governance - + <div class="vox-p-all-lg vox-max-width-lg vox-m-x-auto"> + <vox-breadcrumbs> + <a href="/">Home</a> + <span aria-current="page">Governance</span> + </vox-breadcrumbs> -
    -

    How OpenVox makes decisions

    -

    OpenVox is a community-run project. No single company or person controls the code, the infrastructure, or the roadmap — decisions are made in the open, by the people doing the work.

    + <div class="vox-prose vox-m-top-lg"> + <h1>How OpenVox makes decisions</h1> + <p>OpenVox is a community-run project. No single company or person controls the code, the infrastructure, or the roadmap — decisions are made in the open, by the people doing the work.</p> - -

    Want a vote? Show up. Voting rights follow contribution, not tenure.

    -
    + <vox-callout variant="tip"> + <p>Want a vote? Show up. Voting rights follow contribution, not tenure.</p> + </vox-callout> -

    Maintainership

    -

    Anyone who merges a pull request more than a couple of times becomes a candidate for commit access. There's no formal application — an existing maintainer just proposes you.

    + <h2>Maintainership</h2> + <p>Anyone who merges a pull request more than a couple of times becomes a candidate for commit access. There's no formal application — an existing maintainer just proposes you.</p> -

    Conflict resolution

    -

    Most disagreements get settled in the issue tracker. When they don't, they go to the monthly community call, and failing that, to a vote of active maintainers.

    -
    -
    + <h2>Conflict resolution</h2> + <p>Most disagreements get settled in the issue tracker. When they don't, they go to the monthly community call, and failing that, to a vote of active maintainers.</p> + </div> + </div> - - - Modules - Documentation - - - GitHub - Slack - - © OpenVox. Apache-2.0 licensed. - - - -``` + <vox-footer> + <vox-footer-column heading="Project"> + <a href="/modules">Modules</a> + <a href="/docs">Documentation</a> + </vox-footer-column> + <vox-footer-column heading="Community"> + <a href="https://github.com/OpenVoxProject">GitHub</a> + <a href="#">Slack</a> + </vox-footer-column> + <span slot="bottom">© OpenVox. Apache-2.0 licensed.</span> + </vox-footer> +</body> +</html> +
    diff --git a/docs/layouts/dashboard/index.md b/docs/layouts/dashboard/index.md index 579f34b..6db91d8 100644 --- a/docs/layouts/dashboard/index.md +++ b/docs/layouts/dashboard/index.md @@ -52,59 +52,59 @@ Each module card's category pill (Database, Web Server, Networking) uses the [ex ## Full HTML -```html - - - - - - Overview — Forge Dashboard - - - - - - Overview - Modules - Team - - + +<!doctype html> +<html lang="en"> +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Overview — Forge Dashboard</title> + <link rel="stylesheet" href="https://unpkg.com/@openvoxproject/voxblocks/dist/cdn/voxblocks.css" /> + <script type="module" src="https://unpkg.com/@openvoxproject/voxblocks/dist/cdn/voxblocks.js"></script> +</head> +<body> + <vox-header site-title="Forge Dashboard" href="/"> + <a href="/overview" aria-current="page">Overview</a> + <a href="/modules">Modules</a> + <a href="/team">Team</a> + <vox-avatar slot="actions" size="sm" initials="DR" alt="D. Ramírez"></vox-avatar> + </vox-header> -
    -

    Overview

    + <div class="vox-p-all-lg"> + <h1 class="vox-ts-xl vox-m-bottom-md">Overview</h1> - -

    puppet-legacy-app and puppet-old-tool haven't had a release in over a year.

    -
    + <vox-alert variant="warning" heading="Two modules need a maintainer" class="vox-m-bottom-lg"> + <p>puppet-legacy-app and puppet-old-tool haven't had a release in over a year.</p> + </vox-alert> - - Community-maintained - Awaiting review - This week - Across all modules - + <vox-grid min="160px" gap="lg" class="vox-m-bottom-lg"> + <vox-stat value="450+" label="Modules">Community-maintained</vox-stat> + <vox-stat value="12" label="Pending PRs">Awaiting review</vox-stat> + <vox-stat value="3" label="Releases">This week</vox-stat> + <vox-stat value="98%" label="CI passing">Across all modules</vox-stat> + </vox-grid> - - - 8.1.0 - Database + <vox-grid min="240px" gap="md"> + <vox-card heading="puppetdb"> + <span slot="badge">8.1.0</span> + <span class="vox-bg-blue-100 vox-color-blue-900 vox-p-x-sm vox-radius-full vox-ts-xs vox-text-semibold" style="display: inline-block; margin-bottom: 0.35rem;">Database</span> Installs PostgreSQL and PuppetDB, sets up the connection to Puppet master. - 313,347,865 downloads - - - 12.0.1 - Web Server + <span slot="footer">313,347,865 downloads</span> + </vox-card> + <vox-card heading="apache"> + <span slot="badge">12.0.1</span> + <span class="vox-bg-gold-100 vox-color-gold-900 vox-p-x-sm vox-radius-full vox-ts-xs vox-text-semibold" style="display: inline-block; margin-bottom: 0.35rem;">Web Server</span> Installs and configures Apache HTTP Server, including virtual hosts and modules. - 204,981,112 downloads - - - 6.0.1 - Networking + <span slot="footer">204,981,112 downloads</span> + </vox-card> + <vox-card heading="nginx"> + <span slot="badge">6.0.1</span> + <span class="vox-bg-purple-100 vox-color-purple-900 vox-p-x-sm vox-radius-full vox-ts-xs vox-text-semibold" style="display: inline-block; margin-bottom: 0.35rem;">Networking</span> Installs and configures nginx, including sites, upstreams, and SSL/TLS. - 128,442,003 downloads - - -
    - - -``` + <span slot="footer">128,442,003 downloads</span> + </vox-card> + </vox-grid> + </div> +</body> +</html> +
    diff --git a/docs/layouts/docs-sidenav/index.md b/docs/layouts/docs-sidenav/index.md index 4fa11d1..52c8793 100644 --- a/docs/layouts/docs-sidenav/index.md +++ b/docs/layouts/docs-sidenav/index.md @@ -63,74 +63,74 @@ A documentation page: fixed sidebar section nav, breadcrumbs, prose content, and ## Full HTML -```html - - - - - - Installing on Debian / Ubuntu — OpenVox Docs - - - - - - Server - Agent - Modules - + +<!doctype html> +<html lang="en"> +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Installing on Debian / Ubuntu — OpenVox Docs</title> + <link rel="stylesheet" href="https://unpkg.com/@openvoxproject/voxblocks/dist/cdn/voxblocks.css" /> + <script type="module" src="https://unpkg.com/@openvoxproject/voxblocks/dist/cdn/voxblocks.js"></script> +</head> +<body> + <vox-header site-title="OpenVox Docs" href="/"> + <a href="/server" aria-current="page">Server</a> + <a href="/agent">Agent</a> + <a href="/modules">Modules</a> + </vox-header> -
    -
    - - Overview - - Debian / Ubuntu - RHEL / Fedora - - - Server settings - Agent settings - - -
    + <div class="vox-display-flex vox-items-start vox-gap-xl vox-p-all-lg"> + <div style="flex: 0 0 14rem;"> + <vox-sidenav label="Server docs"> + <vox-sidenav-item href="/server">Overview</vox-sidenav-item> + <vox-sidenav-group heading="Installation" open> + <vox-sidenav-item href="/server/deb" current>Debian / Ubuntu</vox-sidenav-item> + <vox-sidenav-item href="/server/rhel">RHEL / Fedora</vox-sidenav-item> + </vox-sidenav-group> + <vox-sidenav-group heading="Configuration"> + <vox-sidenav-item href="/server/config">Server settings</vox-sidenav-item> + <vox-sidenav-item href="/agent/config">Agent settings</vox-sidenav-item> + </vox-sidenav-group> + </vox-sidenav> + </div> -
    - - Docs - Server - Debian / Ubuntu - + <div style="flex: 1 1 auto; min-width: 0;"> + <vox-breadcrumbs> + <a href="/docs">Docs</a> + <a href="/server">Server</a> + <span aria-current="page">Debian / Ubuntu</span> + </vox-breadcrumbs> -
    -

    Installing on Debian / Ubuntu

    -

    The server package is published to the OpenVox apt repository and supports Debian 12 and Ubuntu 22.04+.

    + <div class="vox-prose vox-m-top-lg"> + <h1>Installing on Debian / Ubuntu</h1> + <p>The server package is published to the OpenVox apt repository and supports Debian 12 and Ubuntu 22.04+.</p> - -

    Server and agent major versions must match. Mixed-version fleets are only supported during a rolling upgrade.

    -
    + <vox-callout variant="warning"> + <p>Server and agent major versions must match. Mixed-version fleets are only supported during a rolling upgrade.</p> + </vox-callout> -

    Add the repository

    -

    Import the signing key and add the apt source before installing the openvox-server package.

    -
    + <h2>Add the repository</h2> + <p>Import the signing key and add the apt source before installing the <code>openvox-server</code> package.</p> + </div> - -
    -
    + ></vox-series-nav> + </div> + </div> - - - Server - Agent - - © OpenVox. Apache-2.0 licensed. - - - -``` + <vox-footer> + <vox-footer-column heading="Docs"> + <a href="/server">Server</a> + <a href="/agent">Agent</a> + </vox-footer-column> + <span slot="bottom">© OpenVox. Apache-2.0 licensed.</span> + </vox-footer> +</body> +</html> +
    diff --git a/docs/layouts/marketing/index.md b/docs/layouts/marketing/index.md index 3380631..e80e5a7 100644 --- a/docs/layouts/marketing/index.md +++ b/docs/layouts/marketing/index.md @@ -75,81 +75,81 @@ A landing page for a project home page or a campaign: hero, proof points, featur ## Full HTML -```html - - - - - - OpenVox — Infrastructure automation for the community - - - - - - Docs - Modules - Community - GitHub - - - + +<!doctype html> +<html lang="en"> +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>OpenVox — Infrastructure automation for the community</title> + <link rel="stylesheet" href="https://unpkg.com/@openvoxproject/voxblocks/dist/cdn/voxblocks.css" /> + <script type="module" src="https://unpkg.com/@openvoxproject/voxblocks/dist/cdn/voxblocks.js"></script> +</head> +<body> + <vox-header site-title="OpenVox" href="/"> + <a href="/docs">Docs</a> + <a href="/modules">Modules</a> + <a href="/community">Community</a> + <vox-button slot="actions" size="sm" variant="alt" href="https://github.com/OpenVoxProject" target="_blank">GitHub</vox-button> + </vox-header> + + <vox-hero eyebrow="Open source, community-run" heading="Infrastructure automation that answers to its community"> OpenVox is a soft fork of Puppet — functionally compatible, openly governed, and built by the people who run it in production. - Get started - Read the docs - - -
    - - Community-maintained - Active this year - Since the OpenVox fork - -
    - -
    -

    Why teams switch

    - - - + <vox-button slot="actions" href="/docs/getting-started">Get started</vox-button> + <vox-button slot="actions" variant="alt" href="/docs">Read the docs</vox-button> + </vox-hero> + + <div class="vox-p-x-lg vox-p-bottom-xxl"> + <vox-grid min="140px" gap="lg"> + <vox-stat value="450+" label="Modules">Community-maintained</vox-stat> + <vox-stat value="140" label="Contributors">Active this year</vox-stat> + <vox-stat value="2" label="Years">Since the OpenVox fork</vox-stat> + </vox-grid> + </div> + + <div class="vox-p-x-lg vox-p-bottom-xxl"> + <h2 class="vox-text-center vox-m-bottom-lg">Why teams switch</h2> + <vox-grid min="220px" gap="md"> + <vox-card heading="Drop-in compatible"> + <span slot="icon"><vox-icon name="plug" size="lg"></vox-icon></span> Your existing Puppet manifests and modules work unchanged. - - - + </vox-card> + <vox-card heading="Openly governed"> + <span slot="icon"><vox-icon name="ballot" size="lg"></vox-icon></span> Roadmap and releases are decided in public, by maintainers. - - - + </vox-card> + <vox-card heading="No license surprises"> + <span slot="icon"><vox-icon name="verified" size="lg"></vox-icon></span> Apache-2.0, forever — no relicensing risk down the line. - - -
    + </vox-card> + </vox-grid> + </div> -
    - + <div class="vox-p-x-lg vox-p-bottom-xxl vox-display-flex vox-justify-center"> + <vox-quote attribution="A happy operator" detail="Platform team, mid-size SaaS"> Migrating to OpenVox was the least dramatic infrastructure change we made all year. - -
    - - -

    Install OpenVox in minutes and bring your existing Puppet code with you.

    -
    - Installation guide - Join the community -
    -
    - - - - Modules - Documentation - - - GitHub - Slack - - © OpenVox. Apache-2.0 licensed. - - - -``` + </vox-quote> + </div> + + <vox-cta-band heading="Your infrastructure. Your community. Your platform."> + <p>Install OpenVox in minutes and bring your existing Puppet code with you.</p> + <div slot="actions"> + <vox-button href="/docs/getting-started" variant="brand">Installation guide</vox-button> + <vox-button href="/community" variant="alt">Join the community</vox-button> + </div> + </vox-cta-band> + + <vox-footer> + <vox-footer-column heading="Project"> + <a href="/modules">Modules</a> + <a href="/docs">Documentation</a> + </vox-footer-column> + <vox-footer-column heading="Community"> + <a href="https://github.com/OpenVoxProject">GitHub</a> + <a href="#">Slack</a> + </vox-footer-column> + <span slot="bottom">© OpenVox. Apache-2.0 licensed.</span> + </vox-footer> +</body> +</html> + diff --git a/docs/src/site.css b/docs/src/site.css index e6f5f15..aa1d840 100644 --- a/docs/src/site.css +++ b/docs/src/site.css @@ -203,57 +203,3 @@ vox-footer { .vox-doc-content .vox-prose td { color: var(--vox-color-text-2); } - -/* Rouge syntax highlighting ---------------------------------------------- */ - -.highlight { - color: var(--vox-color-text-1); -} - -.highlight .c, -.highlight .c1, -.highlight .cm { - color: var(--vox-color-text-3); - font-style: italic; -} - -.highlight .k, -.highlight .kd, -.highlight .kn, -.highlight .kt { - color: var(--vox-color-brand-1); -} - -.highlight .s, -.highlight .s1, -.highlight .s2, -.highlight .sb { - color: #0e7267; -} - -.highlight .n, -.highlight .nx { - color: var(--vox-color-text-1); -} - -.highlight .na, -.highlight .nt { - color: var(--vox-color-brand-1); -} - -.highlight .nb, -.highlight .nc, -.highlight .nf { - color: #7c5cbf; -} - -.highlight .m, -.highlight .mi, -.highlight .mf { - color: #b45309; -} - -.highlight .o, -.highlight .p { - color: var(--vox-color-text-2); -} diff --git a/src/components/code-block/languages.ts b/src/components/code-block/languages.ts new file mode 100644 index 0000000..edb01a0 --- /dev/null +++ b/src/components/code-block/languages.ts @@ -0,0 +1,259 @@ +/** + * A small, dependency-free syntax tokenizer. It trades completeness for + * being self-contained (no Prism/highlight.js/Shiki) and covers the + * languages that actually show up in OpenVox docs: shell, Puppet + * manifests, Hiera/YAML data, JSON, Ruby, plus the web basics. + * + * Each language is an ordered list of rules; the first rule whose regex + * matches at the current position wins, so more specific rules (e.g. "a + * quoted key before a colon") must be listed before the general-purpose + * ones they'd otherwise be shadowed by. + */ + +export type TokenType = + | 'plain' + | 'comment' + | 'keyword' + | 'string' + | 'number' + | 'function' + | 'property' + | 'tag'; + +export interface Token { + type: TokenType; + text: string; +} + +type TypeResolver = (matchText: string, code: string, matchEnd: number) => TokenType; + +interface Rule { + re: RegExp; + type: TokenType | TypeResolver; +} + +function skipInlineSpace(code: string, from: number): number { + let j = from; + while (j < code.length && (code[j] === ' ' || code[j] === '\t')) j++; + return j; +} + +/** Matches an identifier; resolves to `keyword` if it's in `keywords`, `function` if followed by `(`, else `plain`. */ +function identifierRule(re: RegExp, keywords: Set): Rule { + return { + re, + type: (text, code, end) => { + if (keywords.has(text)) return 'keyword'; + return code[skipInlineSpace(code, end)] === '(' ? 'function' : 'plain'; + }, + }; +} + +/** Matches a quoted string; resolves to `property` if it's a key (followed by `:`), else `string`. */ +function quotedPropertyRule(re: RegExp): Rule { + return { + re, + type: (_text, code, end) => (code[skipInlineSpace(code, end)] === ':' ? 'property' : 'string'), + }; +} + +const NUMBER = /-?\b\d+\.?\d*(?:[eE][+-]?\d+)?\b/y; +const DQ_STRING = /"(?:\\.|[^"\\])*"/y; +const SQ_STRING = /'(?:\\.|[^'\\])*'/y; +const HASH_COMMENT = /#.*/y; +const SLASH_COMMENT = /\/\/.*/y; +const BLOCK_COMMENT = /\/\*[\s\S]*?\*\//y; + +function pushToken(tokens: Token[], type: TokenType, text: string): void { + const last = tokens[tokens.length - 1]; + if (last && last.type === type) { + last.text += text; + } else { + tokens.push({ type, text }); + } +} + +/** Runs `rules` over `code`, falling back to a single plain-text token for anything unmatched. */ +export function tokenize(code: string, rules: Rule[]): Token[] { + const tokens: Token[] = []; + let i = 0; + while (i < code.length) { + let matched = false; + for (const rule of rules) { + rule.re.lastIndex = i; + const m = rule.re.exec(code); + if (m && m.index === i && m[0].length > 0) { + const text = m[0]; + const type = typeof rule.type === 'function' ? rule.type(text, code, i + text.length) : rule.type; + pushToken(tokens, type, text); + i += text.length; + matched = true; + break; + } + } + if (!matched) { + pushToken(tokens, 'plain', code[i]); + i++; + } + } + return tokens; +} + +const BASH_KEYWORDS = new Set([ + 'if', 'then', 'elif', 'else', 'fi', 'for', 'while', 'until', 'do', 'done', + 'case', 'esac', 'in', 'function', 'select', 'time', 'return', 'exit', + 'break', 'continue', 'local', 'export', 'readonly', 'declare', 'unset', + 'shift', 'eval', 'exec', 'trap', 'set', 'source', 'alias', 'unalias', 'true', 'false', +]); + +const bash: Rule[] = [ + { re: HASH_COMMENT, type: 'comment' }, + { re: /\$\{[^}]*\}|\$[A-Za-z_]\w*|\$[0-9@#?$!*_-]/y, type: 'function' }, + { re: DQ_STRING, type: 'string' }, + { re: SQ_STRING, type: 'string' }, + { re: NUMBER, type: 'number' }, + identifierRule(/[A-Za-z_][\w-]*/y, BASH_KEYWORDS), +]; + +const YAML_KEYWORDS = new Set([ + 'true', 'false', 'yes', 'no', 'null', 'on', 'off', + 'True', 'False', 'Yes', 'No', 'Null', 'On', 'Off', + 'TRUE', 'FALSE', 'YES', 'NO', 'NULL', 'ON', 'OFF', +]); + +const yaml: Rule[] = [ + { re: HASH_COMMENT, type: 'comment' }, + quotedPropertyRule(DQ_STRING), + quotedPropertyRule(SQ_STRING), + { re: /[A-Za-z_][\w .-]*?(?=:(\s|$))/y, type: 'property' }, + { re: /[&*!][A-Za-z_][\w:.]*/y, type: 'function' }, + { re: NUMBER, type: 'number' }, + identifierRule(/[A-Za-z_][\w-]*/y, YAML_KEYWORDS), +]; + +const json: Rule[] = [ + quotedPropertyRule(DQ_STRING), + { re: NUMBER, type: 'number' }, + identifierRule(/[A-Za-z_]\w*/y, new Set(['true', 'false', 'null'])), +]; + +const JS_KEYWORDS = new Set([ + 'const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', + 'do', 'switch', 'case', 'default', 'break', 'continue', 'class', 'extends', + 'super', 'new', 'this', 'import', 'export', 'from', 'as', 'async', 'await', + 'try', 'catch', 'finally', 'throw', 'typeof', 'instanceof', 'in', 'of', + 'yield', 'static', 'get', 'set', 'void', 'delete', 'null', 'undefined', + 'true', 'false', 'public', 'private', 'protected', 'readonly', 'interface', + 'type', 'enum', 'implements', 'namespace', 'declare', 'abstract', 'keyof', + 'satisfies', +]); + +const javascript: Rule[] = [ + { re: SLASH_COMMENT, type: 'comment' }, + { re: BLOCK_COMMENT, type: 'comment' }, + { re: DQ_STRING, type: 'string' }, + { re: SQ_STRING, type: 'string' }, + { re: /`(?:\\.|[^`\\])*`/y, type: 'string' }, + { re: /-?\b0[xXbBoO][0-9a-fA-F]+\b|-?\b\d+\.?\d*(?:[eE][+-]?\d+)?\b/y, type: 'number' }, + identifierRule(/[A-Za-z_$][\w$]*/y, JS_KEYWORDS), +]; + +const css: Rule[] = [ + { re: BLOCK_COMMENT, type: 'comment' }, + { re: DQ_STRING, type: 'string' }, + { re: SQ_STRING, type: 'string' }, + { re: /@[\w-]+/y, type: 'keyword' }, + { re: /!important/y, type: 'keyword' }, + { re: /#[0-9a-fA-F]{3,8}\b/y, type: 'number' }, + { re: /-?\b\d+\.?\d*[a-zA-Z%]*\b/y, type: 'number' }, + { re: /[a-zA-Z-]+(?=\s*:)/y, type: 'property' }, + identifierRule(/[a-zA-Z-]+/y, new Set()), +]; + +const htmlLang: Rule[] = [ + { re: //y, type: 'comment' }, + { re: /<\/?[a-zA-Z][\w:-]*/y, type: 'tag' }, + { re: /[a-zA-Z-][\w-]*(?=\s*=)/y, type: 'property' }, + { re: DQ_STRING, type: 'string' }, + { re: SQ_STRING, type: 'string' }, + { re: /&[\w#]+;/y, type: 'keyword' }, +]; + +const RUBY_KEYWORDS = new Set([ + 'def', 'end', 'if', 'elsif', 'else', 'unless', 'while', 'until', 'for', + 'in', 'do', 'class', 'module', 'begin', 'rescue', 'ensure', 'raise', + 'return', 'yield', 'break', 'next', 'redo', 'retry', 'case', 'when', + 'then', 'and', 'or', 'not', 'nil', 'true', 'false', 'self', 'super', + 'require', 'require_relative', 'include', 'extend', 'attr_accessor', + 'attr_reader', 'attr_writer', 'private', 'protected', 'public', 'lambda', + 'proc', 'new', +]); + +const ruby: Rule[] = [ + { re: HASH_COMMENT, type: 'comment' }, + { re: /:[A-Za-z_]\w*[?!]?/y, type: 'string' }, + { re: /@{1,2}[A-Za-z_]\w*|\$[A-Za-z_]\w*/y, type: 'function' }, + { re: DQ_STRING, type: 'string' }, + { re: SQ_STRING, type: 'string' }, + { re: NUMBER, type: 'number' }, + { re: /[A-Z]\w*/y, type: 'property' }, + identifierRule(/[a-z_]\w*[?!]?/y, RUBY_KEYWORDS), +]; + +const PUPPET_KEYWORDS = new Set([ + 'class', 'define', 'node', 'inherits', 'if', 'elsif', 'else', 'unless', + 'case', 'and', 'or', 'in', 'undef', 'true', 'false', 'default', 'import', + 'include', 'require', 'contain', 'function', 'type', 'application', + 'produces', 'consumes', 'private', 'return', 'break', 'next', 'each', + 'map', 'filter', 'reduce', 'with', + 'String', 'Integer', 'Boolean', 'Array', 'Hash', 'Optional', 'Enum', + 'Variant', 'Numeric', 'Float', 'Undef', 'Any', 'Pattern', 'Regexp', + 'Sensitive', 'Struct', 'Tuple', 'Type', 'Callable', 'Data', 'Scalar', +]); + +const puppet: Rule[] = [ + { re: HASH_COMMENT, type: 'comment' }, + { re: DQ_STRING, type: 'string' }, + { re: SQ_STRING, type: 'string' }, + { re: /\$[\w:]+/y, type: 'function' }, + { re: /[a-zA-Z_][\w:]*(?=\s*\{)/y, type: 'tag' }, + { re: /[a-z_]\w*(?=\s*=>)/y, type: 'property' }, + { re: NUMBER, type: 'number' }, + identifierRule(/[A-Za-z_][\w:]*/y, PUPPET_KEYWORDS), +]; + +const GRAMMARS: Record = { + bash, sh: bash, shell: bash, zsh: bash, + yaml, yml: yaml, + json, + javascript, js: javascript, + typescript: javascript, ts: javascript, + css, + html: htmlLang, xml: htmlLang, + ruby, rb: ruby, + puppet, pp: puppet, +}; + +export const LANGUAGE_LABELS: Record = { + bash: 'Bash', sh: 'Shell', shell: 'Shell', zsh: 'Zsh', + yaml: 'YAML', yml: 'YAML', + json: 'JSON', + javascript: 'JavaScript', js: 'JavaScript', + typescript: 'TypeScript', ts: 'TypeScript', + css: 'CSS', + html: 'HTML', xml: 'XML', + ruby: 'Ruby', rb: 'Ruby', + puppet: 'Puppet', pp: 'Puppet', + plaintext: 'Plain Text', text: 'Plain Text', +}; + +export type CodeBlockLanguage = + | 'plaintext' | 'bash' | 'sh' | 'shell' | 'zsh' | 'yaml' | 'yml' | 'json' + | 'javascript' | 'js' | 'typescript' | 'ts' | 'css' | 'html' | 'xml' + | 'ruby' | 'rb' | 'puppet' | 'pp'; + +/** Tokenizes `code` for `language`; unrecognized languages render as plain, unhighlighted text. */ +export function highlight(code: string, language: string): Token[] { + const rules = GRAMMARS[language]; + return rules ? tokenize(code, rules) : [{ type: 'plain', text: code }]; +} diff --git a/src/components/code-block/vox-code-block.stories.ts b/src/components/code-block/vox-code-block.stories.ts new file mode 100644 index 0000000..240835b --- /dev/null +++ b/src/components/code-block/vox-code-block.stories.ts @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; +import './vox-code-block.js'; + +const meta: Meta = { + title: 'Components/Page Content/Code Block', + parameters: { + docs: { + description: { + component: '`` is a syntax-highlighted code block with a copy button. Highlighting is built in — no Prism/highlight.js/Shiki setup required.', + }, + }, + }, +}; +export default meta; + +type Story = StoryObj; + +export const Puppet: Story = { + render: () => html`
    + + class profile::base ( + String $version = '1.0', + ) { + package { 'openssl': + ensure => installed, + } + + file { '/etc/motd': + ensure => file, + content => "Managed by Puppet \${version}\\n", + } + } + +
    `, +}; + +export const Languages: Story = { + render: () => html`
    + + version: 5 + defaults: + datadir: data + data_hash: yaml_data + hierarchy: + - name: "Per-node data" + path: "nodes/%{trusted.certname}.yaml" + + + #!/usr/bin/env bash + set -euo pipefail + + # Install the OpenVox agent + curl -fsSL https://apt.voxpupuli.org/openvox-release.deb -o /tmp/release.deb + sudo dpkg -i /tmp/release.deb + sudo apt-get update && sudo apt-get install -y openvox-agent + + + { + "name": "openvoxproject-motd", + "version": "1.0.0", + "dependencies": [], + "operatingsystem_support": [ + { "operatingsystem": "RedHat", "operatingsystemrelease": ["8", "9"] } + ] + } + +
    `, +}; + +export const LineNumbers: Story = { + render: () => html`
    + + Facter.add(:os_release) do + setcode do + if File.exist?('/etc/os-release') + File.read('/etc/os-release') + end + end + end + +
    `, +}; + +export const PlainText: Story = { + render: () => html`
    + + No language set — renders as plain, unhighlighted text. + +
    `, +}; + +export const NoCopyButton: Story = { + render: () => html`
    + + echo "read-only example output" + +
    `, +}; diff --git a/src/components/code-block/vox-code-block.ts b/src/components/code-block/vox-code-block.ts new file mode 100644 index 0000000..7a6a6bb --- /dev/null +++ b/src/components/code-block/vox-code-block.ts @@ -0,0 +1,288 @@ +import { LitElement, html, css, nothing } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ICON_PATHS } from '../icon/icon-paths.js'; +import { highlight, LANGUAGE_LABELS, type Token } from './languages.js'; + +export type { CodeBlockLanguage } from './languages.js'; + +/** Strips a shared leading indent and surrounding blank lines, so authored (and thus HTML-indented) code renders flush left. */ +function dedent(text: string): string { + const lines = text.split('\n'); + while (lines.length && lines[0].trim() === '') lines.shift(); + while (lines.length && lines[lines.length - 1].trim() === '') lines.pop(); + const indents = lines.filter((l) => l.trim() !== '').map((l) => l.match(/^[ \t]*/)?.[0].length ?? 0); + const minIndent = indents.length ? Math.min(...indents) : 0; + return lines.map((l) => l.slice(minIndent)).join('\n'); +} + +/** Splits tokens on embedded newlines into one array of tokens per source line. */ +function toLines(tokens: Token[]): Token[][] { + const lines: Token[][] = [[]]; + for (const token of tokens) { + token.text.split('\n').forEach((part, i) => { + if (i > 0) lines.push([]); + if (part) lines[lines.length - 1].push({ type: token.type, text: part }); + }); + } + return lines; +} + +/** + * A syntax-highlighted code block with a copy button, no external + * highlighter required. Covers shell, Puppet, YAML, JSON, Ruby, JS/TS, + * CSS, and HTML; unrecognized languages render as plain text. + * + * @slot - The code, as plain text (escape `<` and `&` as you would in + * any HTML source, e.g. inside a `` child). + */ +@customElement('vox-code-block') +export class VoxCodeBlock extends LitElement { + @property() language = ''; + + /** Shown in the header instead of/alongside the language label. */ + @property() filename = ''; + + @property({ type: Boolean, attribute: 'line-numbers', reflect: true }) lineNumbers = false; + + @property({ type: Boolean, attribute: 'no-copy' }) noCopy = false; + + @state() private _code = ''; + @state() private _copied = false; + + private _copyResetTimer?: ReturnType; + + static styles = css` + :host { + display: block; + font-family: var(--vox-font-family-base); + } + + .block { + border: 1px solid var(--vox-color-border); + border-radius: var(--vox-radius-md); + background-color: var(--vox-color-bg-alt); + overflow: hidden; + } + + .header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--vox-space-3); + padding: var(--vox-space-2) var(--vox-space-2) var(--vox-space-2) var(--vox-space-4); + border-bottom: 1px solid var(--vox-color-divider); + font-size: 12px; + } + + .meta { + display: flex; + align-items: baseline; + gap: var(--vox-space-3); + min-width: 0; + overflow: hidden; + color: var(--vox-color-text-2); + } + + .filename { + color: var(--vox-color-text-1); + font-family: var(--vox-font-family-mono); + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .lang { + flex: none; + text-transform: uppercase; + letter-spacing: 0.04em; + } + + .copy { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + border: none; + border-radius: var(--vox-radius-sm); + background: none; + color: var(--vox-color-text-2); + cursor: pointer; + } + + .copy:hover { + background-color: var(--vox-color-brand-soft); + color: var(--vox-color-brand-1); + } + + .copy:focus-visible { + outline: 2px solid var(--vox-color-brand-1); + outline-offset: 2px; + } + + .copy svg { + width: 15px; + height: 15px; + } + + pre { + margin: 0; + padding: var(--vox-space-4); + overflow-x: auto; + white-space: pre; + tab-size: 2; + } + + pre:focus-visible { + outline: 2px solid var(--vox-color-brand-1); + outline-offset: -2px; + } + + code { + font-family: var(--vox-font-family-mono); + font-size: 13px; + line-height: 1.7; + color: var(--vox-color-text-1); + } + + .line { + display: block; + } + + :host([line-numbers]) code { + counter-reset: line; + } + + :host([line-numbers]) .line { + padding-left: 3.5ch; + position: relative; + } + + :host([line-numbers]) .line::before { + counter-increment: line; + content: counter(line); + position: absolute; + left: 0; + width: 2.5ch; + text-align: right; + color: var(--vox-color-text-3); + user-select: none; + } + + .tok-comment { + color: var(--vox-code-comment); + font-style: italic; + } + .tok-keyword { + color: var(--vox-code-keyword); + } + .tok-string { + color: var(--vox-code-string); + } + .tok-number { + color: var(--vox-code-number); + } + .tok-function { + color: var(--vox-code-function); + } + .tok-property { + color: var(--vox-code-property); + } + .tok-tag { + color: var(--vox-code-tag); + } + + slot { + display: none; + } + + .visually-hidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + } + `; + + disconnectedCallback() { + super.disconnectedCallback(); + clearTimeout(this._copyResetTimer); + } + + private _handleSlotChange(event: Event) { + const slot = event.target as HTMLSlotElement; + const text = slot + .assignedNodes({ flatten: true }) + .map((node) => node.textContent ?? '') + .join(''); + this._code = dedent(text); + } + + private async _copy() { + try { + await navigator.clipboard.writeText(this._code); + } catch { + const textarea = document.createElement('textarea'); + textarea.value = this._code; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + this.shadowRoot?.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + textarea.remove(); + } + this._copied = true; + clearTimeout(this._copyResetTimer); + this._copyResetTimer = setTimeout(() => { + this._copied = false; + }, 1500); + } + + render() { + const label = LANGUAGE_LABELS[this.language] ?? this.language; + const lines = toLines(highlight(this._code, this.language)); + + return html` +
    +
    + + ${this.filename ? html`${this.filename}` : nothing} + ${label ? html`${label}` : nothing} + + ${this.noCopy + ? nothing + : html` + + `} +
    +
    ${lines.map(
    +          (line) => html`${line.map((token) =>
    +            token.type === 'plain' ? token.text : html`${token.text}`,
    +          )}`,
    +        )}
    + ${this._copied ? 'Copied to clipboard' : ''} +
    + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'vox-code-block': VoxCodeBlock; + } +} diff --git a/src/index.ts b/src/index.ts index 935d994..57d9274 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,6 +46,7 @@ export { VoxBadge } from './components/badge/vox-badge.js'; export { VoxCalendarTile } from './components/calendar-tile/vox-calendar-tile.js'; export { VoxCallout } from './components/callout/vox-callout.js'; export { VoxCard } from './components/card/vox-card.js'; +export { VoxCodeBlock } from './components/code-block/vox-code-block.js'; export { VoxCtaBand } from './components/cta-band/vox-cta-band.js'; export { VoxEmptyState } from './components/empty-state/vox-empty-state.js'; export { VoxFooter, VoxFooterColumn } from './components/footer/vox-footer.js'; @@ -64,6 +65,7 @@ export { VoxTimeline, VoxTimelineItem } from './components/timeline/vox-timeline export type { IconName, IconSize } from './components/icon/vox-icon.js'; export type { ButtonVariant, ButtonSize } from './components/button/vox-button.js'; export type { CalloutVariant } from './components/callout/vox-callout.js'; +export type { CodeBlockLanguage } from './components/code-block/vox-code-block.js'; export type { BadgeVariant } from './components/badge/vox-badge.js'; export type { AlertVariant } from './components/alert/vox-alert.js'; export type { AvatarSize } from './components/avatar/vox-avatar.js'; diff --git a/src/tokens/tokens.css b/src/tokens/tokens.css index 17f26d1..e65b1f5 100644 --- a/src/tokens/tokens.css +++ b/src/tokens/tokens.css @@ -33,6 +33,15 @@ --vox-color-danger-3: #e0575b; --vox-color-danger-soft: rgba(244, 63, 94, 0.14); + /* Code syntax highlighting — one palette hue per token role */ + --vox-code-comment: var(--vox-palette-gray-700); + --vox-code-keyword: var(--vox-palette-purple-700); + --vox-code-string: var(--vox-palette-green-800); + --vox-code-number: var(--vox-palette-gold-800); + --vox-code-function: var(--vox-palette-blue-700); + --vox-code-property: var(--vox-palette-teal-800); + --vox-code-tag: var(--vox-palette-red-700); + /* Surfaces */ --vox-color-bg: #f5faf9; --vox-color-bg-alt: #e8f1ef; @@ -96,6 +105,14 @@ --vox-color-danger-3: #b62a3c; --vox-color-danger-soft: rgba(244, 63, 94, 0.16); + --vox-code-comment: var(--vox-palette-gray-400); + --vox-code-keyword: var(--vox-palette-purple-300); + --vox-code-string: var(--vox-palette-green-400); + --vox-code-number: var(--vox-palette-gold-400); + --vox-code-function: var(--vox-palette-blue-300); + --vox-code-property: var(--vox-palette-teal-400); + --vox-code-tag: var(--vox-palette-red-300); + --vox-color-bg: #0d1417; --vox-color-bg-alt: #131b1f; --vox-color-bg-soft: #131b1f; From bdd9a35063e867e4c9055177a6d5633e092fec9f Mon Sep 17 00:00:00 2001 From: Anthony Vitacco Date: Sat, 22 Aug 2026 14:47:42 -0400 Subject: [PATCH 2/2] Added some new variants for the code block element Added the ability to remove the header and the border for tight areas where you don't want a big element. Assisted-by: Claude Signed-off-by: Anthony Vitacco --- docs/components/code-block/index.md | 42 +++++++++++++++ package.json | 2 +- .../code-block/vox-code-block.stories.ts | 53 ++++++++++++++++++ src/components/code-block/vox-code-block.ts | 54 ++++++++++++------- 4 files changed, 130 insertions(+), 21 deletions(-) diff --git a/docs/components/code-block/index.md b/docs/components/code-block/index.md index ae504b7..4898fb0 100644 --- a/docs/components/code-block/index.md +++ b/docs/components/code-block/index.md @@ -77,6 +77,46 @@ Add `line-numbers` for a numbered gutter — useful when prose elsewhere on the </vox-code-block>
    +## Without a header or border + +Add `no-header` to drop the whole header bar — filename, language label, and copy button — and `no-border` to drop the outer edge too, for dense contexts like a table cell, where a full header and border per row would take up more space than the code itself. + +
    + + { + "version": "1.14.1" + } + +
    + + +<vox-code-block language="json" no-header no-border> + { "version": "1.14.1" } +</vox-code-block> + + +
    + + + + + + + + + + + + + + +
    FactValue
    clientversion8.28.1
    augeas + + { "version": "1.14.1" } + +
    +
    + ## API | Attribute | Type | Default | Description | @@ -85,5 +125,7 @@ Add `line-numbers` for a numbered gutter — useful when prose elsewhere on the | `filename` | `string` | `''` | Shown at the start of the header, e.g. `profile/base.pp` | | `line-numbers` | `boolean` | `false` | Adds a numbered gutter | | `no-copy` | `boolean` | `false` | Hides the copy-to-clipboard button | +| `no-header` | `boolean` | `false` | Hides the whole header bar, including the copy button | +| `no-border` | `boolean` | `false` | Drops the outer border (the background surface stays) | The copy button copies the raw, un-highlighted code via the Clipboard API (falling back to `execCommand` where unavailable) and announces success to screen readers through a live region. The block itself is keyboard-scrollable (`tabindex="0"`) when its content overflows horizontally. diff --git a/package.json b/package.json index 2146cf0..5bddc74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openvoxproject/voxblocks", - "version": "0.10.0", + "version": "0.12.0", "description": "Web components for OpenVox community web sites and apps", "type": "module", "license": "Apache-2.0", diff --git a/src/components/code-block/vox-code-block.stories.ts b/src/components/code-block/vox-code-block.stories.ts index 240835b..11ae8ee 100644 --- a/src/components/code-block/vox-code-block.stories.ts +++ b/src/components/code-block/vox-code-block.stories.ts @@ -97,3 +97,56 @@ export const NoCopyButton: Story = { `, }; + +export const NoHeader: Story = { + render: () => html`
    + + { + "version": "1.14.1" + } + +
    `, +}; + +export const NoBorder: Story = { + render: () => html`
    + + { "version": "1.14.1" } + +
    `, +}; + +export const InADenseTable: Story = { + render: () => html`
    + + + + + + + + + + + + + + + + + + +
    FactValue
    clientversion8.28.1
    augeas + + { "version": "1.14.1" } + +
    disks + + { + "sda": { "size": "0 bytes", "type": "ssd" }, + "nvme0n1": { "size": "465.76 GiB", "type": "ssd" } + } + +
    +
    `, +}; diff --git a/src/components/code-block/vox-code-block.ts b/src/components/code-block/vox-code-block.ts index 7a6a6bb..28bcd3e 100644 --- a/src/components/code-block/vox-code-block.ts +++ b/src/components/code-block/vox-code-block.ts @@ -46,6 +46,12 @@ export class VoxCodeBlock extends LitElement { @property({ type: Boolean, attribute: 'no-copy' }) noCopy = false; + /** Hides the whole header bar (filename, language label, and copy button) — for dense contexts like a table cell. */ + @property({ type: Boolean, attribute: 'no-header' }) noHeader = false; + + /** Drops the outer border — for dropping into a surface (e.g. a table cell) that already has its own edge. */ + @property({ type: Boolean, attribute: 'no-border', reflect: true }) noBorder = false; + @state() private _code = ''; @state() private _copied = false; @@ -64,6 +70,10 @@ export class VoxCodeBlock extends LitElement { overflow: hidden; } + :host([no-border]) .block { + border: none; + } + .header { display: flex; align-items: center; @@ -249,26 +259,30 @@ export class VoxCodeBlock extends LitElement { return html`
    -
    - - ${this.filename ? html`${this.filename}` : nothing} - ${label ? html`${label}` : nothing} - - ${this.noCopy - ? nothing - : html` - - `} -
    + ${this.noHeader + ? nothing + : html` +
    + + ${this.filename ? html`${this.filename}` : nothing} + ${label ? html`${label}` : nothing} + + ${this.noCopy + ? nothing + : html` + + `} +
    + `}
    ${lines.map(
               (line) => html`${line.map((token) =>
                 token.type === 'plain' ? token.text : html`${token.text}`,