Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ The component catalog, by category:
| Forms | `vox-checkbox`, `vox-file-input`, `vox-input`, `vox-input-group`, `vox-radio-group`/`vox-radio`, `vox-select`, `vox-switch`, `vox-textarea` |
| 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). `<vox-alert>`/`<vox-callout>` show one automatically; `<vox-sidenav-item>`/`<vox-card>`/`<vox-empty-state>`/`<vox-link-hub-item>` 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` |
| Overlays | `vox-dialog`, `vox-disclosure`, `vox-dropdown`, `vox-menu` |
| Page content | `vox-accordion`, `vox-alert`, `vox-badge`, `vox-calendar-tile`, `vox-callout`, `vox-card`, `vox-cta-band`, `vox-datum`, `vox-empty-state`, `vox-footer`, `vox-grid`, `vox-hero`, `vox-link-hub`, `vox-loader`, `vox-quote`, `vox-record-list`, `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 `<form>` like native inputs.
Expand Down
3 changes: 3 additions & 0 deletions docs/_data/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ components:
- { text: Dialog, link: /components/dialog/ }
- { text: Disclosure, link: /components/disclosure/ }
- { text: Dropdown, link: /components/dropdown/ }
- { text: Menu, link: /components/menu/ }
- text: Page Content
items:
- { text: Accordion, link: /components/accordion/ }
Expand All @@ -68,6 +69,7 @@ components:
- { text: Callout, link: /components/callout/ }
- { text: Card, link: /components/card/ }
- { text: CTA Band, link: /components/cta-band/ }
- { text: Datum, link: /components/datum/ }
- { text: Empty State, link: /components/empty-state/ }
- { text: Footer, link: /components/footer/ }
- { text: Grid, link: /components/grid/ }
Expand All @@ -77,6 +79,7 @@ components:
- { text: Loading Indicator, link: /components/loader/ }
- { text: Prose, link: /components/prose/ }
- { text: Quote, link: /components/quote/ }
- { text: Record List, link: /components/record-list/ }
- { text: Sponsor, link: /components/sponsor/ }
- { text: Stat, link: /components/stat/ }
- { text: Step Indicator, link: /components/step-indicator/ }
Expand Down
40 changes: 40 additions & 0 deletions docs/components/datum/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: "Datum"
---

# Datum

`<vox-datum>` is a small labeled value with an optional icon — an owner, a date, a target host, anything that's "icon + value" — for use inside record rows, cards, or wherever a compact data point is needed.

`name` isn't shown visually (the icon, if any, carries that meaning for sighted users); it's exposed to assistive tech as a spoken label, e.g. "User: J. Smith".

<div style="display: flex; gap: 1rem; margin: 1rem 0;">
<vox-datum name="User">
<vox-icon slot="icon" name="person" size="sm"></vox-icon>
J. Smith
</vox-datum>
<vox-datum name="Updated">
<vox-icon slot="icon" name="calendar" size="sm"></vox-icon>
April 1, 2021
</vox-datum>
<vox-datum name="Target">web01.example.com</vox-datum>
</div>

```html
<vox-datum name="User">
<vox-icon slot="icon" name="person" size="sm"></vox-icon>
J. Smith
</vox-datum>
<vox-datum name="Target">web01.example.com</vox-datum>
```

## API

| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | `''` | Accessible label, spoken before the value; not shown visually. |

| Slot | Description |
| --- | --- |
| *(default)* | The value |
| `icon` | Optional icon before the value, e.g. `<vox-icon size="sm">` |
41 changes: 41 additions & 0 deletions docs/components/menu/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: "Menu"
---

# Menu

`<vox-menu>` is an overlay menu anchored to an arbitrary trigger — an avatar, an icon button, anything you slot in. Unlike `<vox-dropdown>`, which owns a fixed label+chevron trigger, `<vox-menu>` lets you supply the trigger content yourself. Entries are plain `<a>` or `<button>` children; `<hr>` draws a separator.

<div style="display: flex; justify-content: flex-end; margin: 1rem 0; min-height: 220px;">
<vox-menu label="Account menu">
<vox-avatar slot="trigger" size="sm" initials="UN" alt="Username"></vox-avatar>
<a href="#">Profile</a>
<a href="#">Settings</a>
<hr />
<button type="button">Log out</button>
</vox-menu>
</div>

```html
<vox-menu label="Account menu">
<vox-avatar slot="trigger" size="sm" initials="UN" alt="Username"></vox-avatar>
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
<hr />
<button type="button">Log out</button>
</vox-menu>
```

Arrow keys move through entries; Escape and outside clicks close the menu.

## API

| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | — | Accessible name for the trigger button. Set this when the `trigger` slot has no visible text of its own (e.g. an avatar-only trigger); leave unset when it does. |
| `placement` | `'bottom-start' \| 'bottom-end'` | `bottom-end` | Which side of the trigger the menu aligns to. |
| `open` | `boolean` | `false` | Menu visibility. |

`trigger` slot holds the trigger content; default slot holds the menu entries.

Fires `vox-close` when the menu closes (Escape, outside click, or an entry click).
101 changes: 101 additions & 0 deletions docs/components/record-list/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
title: "Record List"
---

# Record List

`<vox-record-list>` is a list of linked records — search results, an admin index, anything that's a stack of "go to this thing" rows — built from `<vox-record-list-item>` rows. Pass metadata as [`<vox-datum>`](/components/datum/) elements into the default slot.

<div style="margin: 1rem 0;">
<vox-record-list>
<vox-record-list-item heading="Update scheduling software" href="#">
<vox-datum name="Owner"><vox-icon slot="icon" name="person" size="sm"></vox-icon>Mafalda_Lesch47</vox-datum>
<vox-datum name="Updated"><vox-icon slot="icon" name="calendar" size="sm"></vox-icon>April 1, 2021</vox-datum>
</vox-record-list-item>
<vox-record-list-item heading="Integrate command line tools" href="#">
<vox-datum name="Owner"><vox-icon slot="icon" name="person" size="sm"></vox-icon>Janessa79</vox-datum>
<vox-datum name="Updated"><vox-icon slot="icon" name="calendar" size="sm"></vox-icon>April 1, 2021</vox-datum>
</vox-record-list-item>
<vox-record-list-item heading="Synchronize email workflows" href="#">
<vox-datum name="Owner"><vox-icon slot="icon" name="person" size="sm"></vox-icon>Laura_Schamberger43</vox-datum>
<vox-datum name="Updated"><vox-icon slot="icon" name="calendar" size="sm"></vox-icon>April 1, 2021</vox-datum>
</vox-record-list-item>
</vox-record-list>
</div>

```html
<vox-record-list>
<vox-record-list-item heading="Update scheduling software" href="/records/1">
<vox-datum name="Owner"><vox-icon slot="icon" name="person" size="sm"></vox-icon>Mafalda_Lesch47</vox-datum>
<vox-datum name="Updated"><vox-icon slot="icon" name="calendar" size="sm"></vox-icon>April 1, 2021</vox-datum>
</vox-record-list-item>
</vox-record-list>
```

## Small size

`size="sm"` turns heading/meta/`end` into real table columns — built with CSS
`subgrid`, so each aligns against the same column in every other `size="sm"`
row in the same `<vox-record-list>`, sized to that column's widest cell like
a native `<table>`. The arrow drops out; `heading` and a populated meta
column each link to `href` individually, since a grid row can't itself be
one native link the way a block row can. Suited to a dense activity or run
log rather than an indexed record. Use the `end` slot for trailing content
like a status badge.

<div style="margin: 1rem 0;">
<vox-record-list>
<vox-record-list-item size="sm" heading="8:37 AM" href="#">
<vox-datum name="Target">web01.example.com</vox-datum>
<span slot="end"><vox-badge variant="warning">running</vox-badge> (#641)</span>
</vox-record-list-item>
<vox-record-list-item size="sm" heading="8:37 AM" href="#">
<vox-datum name="Target">web01.example.com</vox-datum>
<span slot="end"><vox-badge variant="warning">running</vox-badge> (#640)</span>
</vox-record-list-item>
<vox-record-list-item size="sm" heading="8:37 AM" href="#">
<vox-datum name="Target">no targets</vox-datum>
<span slot="end"><vox-badge variant="warning">running</vox-badge> (#638)</span>
</vox-record-list-item>
<vox-record-list-item size="sm" heading="8:37 AM" href="#">
<vox-datum name="Target">a.example.com, b.example.com, c.example.com, +1 more</vox-datum>
<span slot="end"><vox-badge variant="warning">running</vox-badge> (#637)</span>
</vox-record-list-item>
</vox-record-list>
</div>

```html
<vox-record-list>
<vox-record-list-item size="sm" heading="8:37 AM" href="/runs/641">
<vox-datum name="Target">web01.example.com</vox-datum>
<span slot="end"><vox-badge variant="warning">running</vox-badge> (#641)</span>
</vox-record-list-item>
</vox-record-list>
```

A `size="sm"` row only lays out correctly as a direct child of
`<vox-record-list>` — its columns are defined there and shared via
`subgrid`, so an item placed elsewhere renders as a plain block instead.
Multiple `<vox-datum>` elements in one `sm` row share a single meta column
(gap-separated, not independently aligned).

## API

`<vox-record-list>` has no attributes of its own — it exists to define the shared grid columns.

| Slot | Description |
| --- | --- |
| *(default)* | `<vox-record-list-item>` elements |

`<vox-record-list-item>`:

| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `heading` | `string` | `''` | Row title (`md`), or the first column (`sm`). |
| `href` | `string` | — | Link target. At `md`, the heading and the trailing arrow both link here. At `sm`, `heading` and a populated meta column each link here individually. Unset renders a static row with no link. |
| `size` | `'md' \| 'sm'` | `md` | Row density. `sm` is a table-aligned line for logs/activity; `md` is the icon+arrow layout for an indexed record. |

| Slot | Description |
| --- | --- |
| *(default)* | Metadata, e.g. [`<vox-datum>`](/components/datum/) elements |
| `end` | Trailing content after the meta, e.g. a `<vox-badge>` status |
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openvoxproject/voxblocks",
"version": "0.10.0",
"version": "0.14.0",
"description": "Web components for OpenVox community web sites and apps",
"type": "module",
"license": "Apache-2.0",
Expand Down
32 changes: 32 additions & 0 deletions src/components/datum/vox-datum.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from '@storybook/web-components-vite';
import { html } from 'lit';
import './vox-datum.js';
import '../icon/vox-icon.js';

const meta: Meta = {
title: 'Components/Page Content/Datum',
parameters: {
docs: {
description: {
component: '`<vox-datum>` is a small labeled value with an optional icon — an owner, a date, a target host — for use inside record rows, cards, or wherever a compact data point is needed.',
},
},
},
};
export default meta;

type Story = StoryObj;

export const Default: Story = {
render: () => html`<div style="display: flex; gap: 1rem; margin: 1rem 0;">
<vox-datum name="User">
<vox-icon slot="icon" name="person" size="sm"></vox-icon>
J. Smith
</vox-datum>
<vox-datum name="Updated">
<vox-icon slot="icon" name="calendar" size="sm"></vox-icon>
April 1, 2021
</vox-datum>
<vox-datum name="Target">web01.example.com</vox-datum>
</div>`,
};
76 changes: 76 additions & 0 deletions src/components/datum/vox-datum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { LitElement, html, css, nothing } from 'lit';
import { customElement, property } from 'lit/decorators.js';

/**
* A small labeled value with an optional icon — an owner, a date, a
* target host, anything that's "icon + value" — for use inside record
* rows, cards, or wherever a compact data point is needed.
*
* `name` isn't shown visually (the icon, if any, carries that meaning for
* sighted users); it's exposed to assistive tech as a spoken label, e.g.
* "User: J. Smith".
*
* @slot icon - Optional icon before the value, e.g. `<vox-icon size="sm">`.
* @slot - The value.
*/
@customElement('vox-datum')
export class VoxDatum extends LitElement {
@property() name = '';

static styles = css`
:host {
display: inline-flex;
align-items: center;
gap: var(--vox-space-1);
font-family: var(--vox-font-family-base);
font-size: 13px;
color: var(--vox-color-text-2);
}

.icon {
display: flex;
flex: none;
color: var(--vox-color-text-3);
}

.icon:not(.has-icon) {
display: none;
}

.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
`;

private hasIcon = false;

private handleIconSlotChange(event: Event) {
const slot = event.target as HTMLSlotElement;
this.hasIcon = slot.assignedNodes({ flatten: true }).length > 0;
this.requestUpdate();
}

render() {
return html`
<span class="icon ${this.hasIcon ? 'has-icon' : ''}">
<slot name="icon" @slotchange=${this.handleIconSlotChange}></slot>
</span>
${this.name ? html`<span class="sr-only">${this.name}: </span>` : nothing}
<slot></slot>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
'vox-datum': VoxDatum;
}
}
6 changes: 6 additions & 0 deletions src/components/dropdown/vox-dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export class VoxDropdown extends LitElement {
:host {
position: relative;
display: inline-block;
/* Without this, a flex/grid container's default stretch alignment
grows the host to fill the cross axis while the trigger button
inside stays content-sized — and since the menu's "top: 100%" is
measured against the host's own box, it then opens far below the
trigger instead of right under it. */
align-self: flex-start;
font-family: var(--vox-font-family-base);
}

Expand Down
30 changes: 30 additions & 0 deletions src/components/menu/vox-menu.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Meta, StoryObj } from '@storybook/web-components-vite';
import { html } from 'lit';
import './vox-menu.js';
import '../avatar/vox-avatar.js';

const meta: Meta = {
title: 'Components/Overlays/Menu',
parameters: {
docs: {
description: {
component: '`<vox-menu>` is an overlay menu anchored to an arbitrary trigger, e.g. an avatar. Entries are plain `<a>` or `<button>` children; `<hr>` draws a separator.',
},
},
},
};
export default meta;

type Story = StoryObj;

export const Default: Story = {
render: () => html`<div style="display: flex; justify-content: flex-end; margin: 1rem 0; min-height: 220px;">
<vox-menu label="Account menu">
<vox-avatar slot="trigger" size="sm" initials="UN" alt="Username"></vox-avatar>
<a href="#">Profile</a>
<a href="#">Settings</a>
<hr />
<button type="button">Log out</button>
</vox-menu>
</div>`,
};
Loading