Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 6 additions & 2 deletions docs/_utilities/code-previews.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,22 @@ module.exports = function (doc, options)
});

// Wrap code preview scripts in anonymous helpers so they don't run in the global scope
const jsTypes = new Set(['', 'text/javascript', 'application/javascript', 'application/ecmascript', 'text/ecmascript']);
doc.querySelectorAll('.code-preview__preview script').forEach(script =>
{
if(script.type === 'module')
const type = (script.getAttribute('type') || '').toLowerCase();
if(type === 'module')
{
// Modules are already scoped
script.textContent = script.innerHTML;
}
else
else if(jsTypes.has(type))
{
// Wrap non-modules in an anonymous function so they don't run in the global scope
script.textContent = `(() => { ${script.innerHTML} })();`;
}
// Otherwise leave alone: data blocks (e.g. type="application/json", type="zn-templates")
// are inert by spec and components read their original textContent directly.
});

return doc;
Expand Down
229 changes: 229 additions & 0 deletions docs/pages/components/data-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,235 @@ Available cell properties:
- `sortValue`: Value to use for sorting (overrides text)
- `gaid`: Google Analytics ID for tracking

### Column-level cell defaults

Two header-level shortcuts customise the built-in cell renderer without requiring a render function:

- **`cellTemplate`** — a partial `Cell` whose properties are applied as defaults to every row's cell in that column. The row's own cell properties win on conflict, so this is the right place for column-wide defaults (a baseline `chipColor`, an `iconSrc`, a `style`, etc.) that individual rows can still override.
- **`ifEmpty`** — a partial `Cell` used as a fallback when the row's cell has neither `text` nor `iconSrc`. Useful for showing a placeholder (em-dash, "—", a muted "n/a", etc.) so empty rows don't render as blank cells.

Both apply only to the built-in renderer. If the column has a `render` or `renderTemplate` set (see [Custom cell rendering](#custom-cell-rendering) below), the render function wins and `cellTemplate` / `ifEmpty` are skipped — your function receives the row's raw cell data unchanged.

```html:preview
<zn-data-table
data-uri="/data/data-table.json"
method="GET"
headers='[
{"key":"name","label":"Name"},
{"key":"status","label":"Status","cellTemplate":{"chipColor":"info"}},
{"key":"phone","label":"Phone","ifEmpty":{"text":"—","style":"italic"}}
]'>
</zn-data-table>
```

In this example the status column renders every value as an `info`-coloured chip by default; individual rows can still override `chipColor` in their own cell data. Phone cells with no text and no icon would fall back to an italic em-dash — the syntax holds regardless of whether the preview dataset happens to exercise that fallback.

### Custom cell rendering

By default each cell is built from its `Cell` properties — `text`, `style`, `chipColor`, `iconSrc`, `uri`, `copyable`, and so on (see [Cell Styling](#cell-styling) above). When those properties aren't enough — you need a conditional, a computed value, or markup the built-in vocabulary doesn't cover — register a **render function** on the column and take full control of its output.

A render function fully replaces the default rendering for that column. The built-in chip, icon, hover, copy, and uri decorations are not applied on top — your function owns the cell.

#### Function signature

```ts
type DisplayTemplate = (cell: Cell, row: Row, header: HeaderConfig) =>
TemplateResult | string;
```

**Parameters**

| Name | Type | Description |
|------|------|-------------|
| `cell` | `Cell` | The cell object for this column. Contains `text` plus any cell-level properties set on it (`chipColor`, `iconSrc`, `uri`, `sortValue`, etc.). |
| `row` | `Row` | The full row the cell belongs to — including `id`, `uri`, `actions`, and all of its other cells. Use this when a column's presentation depends on values in other columns. |
| `header` | `HeaderConfig` | The header configuration for this column (`key`, `label`, and any other column-level settings). |

**Return value**

Either a Lit `TemplateResult` (returned from a `` html`...` `` tagged template) or a string of HTML. Strings are inserted via the `unsafeHTML` directive, so any markup is rendered as-is — sanitize values that come from untrusted sources yourself.

#### Where the function can live

There are three places a render function can be registered. They are resolved in this order, highest precedence first:

1. **`header.render`** — an inline function placed directly on a header. Only reachable when `headers` is assigned as a JS property, because functions cannot appear in a JSON attribute.
2. **`displayTemplates[name]`** — an instance property of the table, mapping template names to functions. Assigned from a regular `<script>`. The header references it by name via `renderTemplate`.
3. **`<script type="zn-templates">`** — a script block placed inside the table that returns a name-to-function map. Compiled once when the table connects. The header references each template by name via `renderTemplate`. The component identifies these scripts by their `type` attribute — no `slot` attribute is required or used.

The instance `displayTemplates` map overrides scripts-compiled templates per name; an inline `header.render` overrides both.

#### Defining templates in HTML

Place a `<script type="zn-templates">` block inside the table. The body must `return` an object mapping names to render functions. Reference each template from its header with `renderTemplate`.

```html:preview
<zn-data-table
data-uri="/data/data-table.json"
method="GET"
headers='[
{"key":"name","label":"Name"},
{"key":"email","label":"Email","renderTemplate":"emailLink"},
{"key":"date","label":"Joined","renderTemplate":"shortDate"}
]'>

<script type="zn-templates">
return {
emailLink: (cell) => `<a href="mailto:${cell.text}"><code>${cell.text}</code></a>`,
shortDate: (cell) => new Date(cell.text).toLocaleDateString(),
};
</script>
</zn-data-table>
```

:::warning
The script body is compiled with `new Function()`, so this form requires `'unsafe-eval'` in your `script-src` Content-Security-Policy when one is set. Compilation runs once when the table connects.
:::

#### `displayTemplates` property

Assign the same name-to-function map from a normal `<script type="module">`. This path is CSP-safe — no `unsafe-eval` required — and useful when the templates already live in your application's JavaScript.

```html:preview
<zn-data-table
id="dt-display-templates"
data-uri="/data/data-table.json"
method="GET"
headers='[
{"key":"name","label":"Name"},
{"key":"email","label":"Email","renderTemplate":"emailLink"},
{"key":"date","label":"Joined","renderTemplate":"shortDate"}
]'>
</zn-data-table>

<script type="module">
document.querySelector('#dt-display-templates').displayTemplates = {
emailLink: (cell) => `<a href="mailto:${cell.text}"><code>${cell.text}</code></a>`,
shortDate: (cell) => new Date(cell.text).toLocaleDateString(),
};
</script>
```

#### Inline `header.render`

Skip the named-template indirection and place the function directly on a header. Useful for one-off renderers that don't need to be reused. Only available when `headers` is assigned as a JS property — JSON attributes cannot carry functions.

```html:preview
<zn-data-table id="dt-inline-render" data-uri="/data/data-table.json" method="GET"></zn-data-table>

<script type="module">
document.querySelector('#dt-inline-render').headers = [
{ key: 'name', label: 'Name' },
{
key: 'email',
label: 'Email',
render: (cell) => `<a href="mailto:${cell.text}"><code>${cell.text}</code></a>`,
},
{
key: 'date',
label: 'Joined',
render: (cell) => new Date(cell.text).toLocaleDateString(),
},
];
</script>
```

#### Using `row` and `header`

A render function can read values from sibling cells via `row.cells` — useful when a column's presentation depends on another column's data.

```html:preview
<zn-data-table
id="dt-row-aware"
data-uri="/data/data-table.json"
method="GET"
headers='[
{"key":"name","label":"Name","renderTemplate":"nameWithStatus"},
{"key":"email","label":"Email"}
]'>

<script type="zn-templates">
return {
nameWithStatus: (cell, row) => {
const status = row.cells.find(c => c.column === 'status')?.text ?? '';
const dim = status === 'Inactive' ? 'opacity:.5;text-decoration:line-through;' : '';
return `<span style="${dim}">${cell.text}</span>`;
},
};
</script>
</zn-data-table>
```

#### Showcase


```html:preview
<zn-data-table
data-uri="/data/data-table.json"
method="GET"
headers='[
{"key":"name", "label":"Customer", "renderTemplate":"customer"},
{"key":"status", "label":"Status", "renderTemplate":"pulse"},
{"key":"address", "label":"Address", "renderTemplate":"marquee"},
{"key":"date", "label":"Joined", "renderTemplate":"yearsAgo"}
]'>

<script type="zn-templates">
const initials = (s) => s.split(' ').map(p => p[0]).join('').slice(0, 2).toUpperCase();
const yearsAgo = (d) => {
const years = Math.floor((Date.now() - new Date(d)) / (365.25 * 24 * 3600 * 1000));
return years <= 0 ? 'this year' : `${years} year${years > 1 ? 's' : ''} ago`;
};

return {
customer: (cell, row) => {
const description = row.cells.find(c => c.column === 'description')?.text ?? '';
return `
<div style="display:block;max-width: 280px;overflow: clip;">
<span style="display:inline-flex;align-items:center;gap:8px;">
<span style="display:inline-flex;align-items:center;justify-content:center;
width:28px;height:28px;border-radius:50%;
background:linear-gradient(135deg,#6366f1,#ec4899);
color:white;font-weight:700;font-size:11px;">
${initials(cell.text)}
</span>
<strong>${cell.text}</strong>
</span>
<div><small>${description}</small></div>
</div>
`
},

pulse: (cell) => {
const color = cell.text === 'Active' ? '#22c55e' : '#f59e0b';
return `
<span style="display:inline-flex;align-items:center;gap:6px;">
<span style="width:8px;height:8px;border-radius:50%;background:${color};
animation:zn-pulse 1.6s infinite;"></span>
${cell.text}
</span>
<style>
@keyframes zn-pulse {
0% { box-shadow: 0 0 0 0 ${color}80; }
70% { box-shadow: 0 0 0 6px ${color}00; }
100% { box-shadow: 0 0 0 0 ${color}00; }
}
</style>`;
},

marquee: (cell) => `
<marquee scrollamount="3"
style="max-width:200px;font-family:monospace;
background:#fff7ed;padding:2px 6px;border-radius:4px;">
🏠 ${cell.text}
</marquee>`,

yearsAgo: (cell) => `<em style="opacity:.75">${yearsAgo(cell.text)}</em>`,
};
</script>
</zn-data-table>
```

### Row Actions

Rows can have contextual actions that appear in a dropdown menu.
Expand Down
11 changes: 10 additions & 1 deletion docs/pages/components/datepicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@ Combine `min-date` and `max-date` to constrain the selectable date range.
help-text="Select dates within the conference period (March 1-15, 2026)">
</zn-datepicker>
```

### Clearable

Use the `clearable` attribute to add a "Clear" button to the calendar popup, allowing users to easily remove a selected date.
Expand Down Expand Up @@ -417,4 +416,14 @@ Use [CSS parts](#css-parts) to customize the way form controls are drawn. This e
</style>
```

### Date Time

Displays time.

```html:preview
<zn-datepicker
time-picker
label="Conference dates"
help-text="Select date and time">
</zn-datepicker>
```
46 changes: 46 additions & 0 deletions docs/pages/components/style.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,49 @@ layout: component
<div class="style-abg"><zn-style border a-margin=a>ALL Margin</zn-style></div>
```

### Border

Accepts any combination of `t`, `b`, `l`, `r` to render specific sides. Use `a` (or the equivalent `tblr`) as a shortcut for all four sides. The bare `border` attribute (no value) is equivalent to `border="a"` and is kept for backward compatibility.

```html:preview
<zn-style border="t" pad=a>Top only</zn-style>
<zn-style border="b" pad=a>Bottom only</zn-style>
<zn-style border="l" pad=a>Left only</zn-style>
<zn-style border="r" pad=a>Right only</zn-style>
<zn-style border="tb" pad=a>Top + Bottom</zn-style>
<zn-style border="lr" pad=a>Left + Right</zn-style>
<zn-style border="tl" pad=a>Top + Left</zn-style>
<zn-style border="a" pad=a>All sides (a)</zn-style>
<zn-style border pad=a>All sides (bare attribute)</zn-style>
```

### Size

Adjusts font size in five steps mapped to the zinc font-size tokens. `m` is the default and applies no class.

| value | token |
| ----- | --------------------------- |
| `xs` | `--zn-font-size-x-small` |
| `s` | `--zn-font-size-small` |
| `m` | inherits (no class applied) |
| `l` | `--zn-font-size-large` |
| `xl` | `--zn-font-size-x-large` |

```html:preview
<zn-style size="xs">Extra small</zn-style>
<zn-style size="s">Small</zn-style>
<zn-style size="m">Medium (default)</zn-style>
<zn-style size="l">Large</zn-style>
<zn-style size="xl">Extra large</zn-style>
```

### Muted

Dims the content with reduced opacity. Useful for placeholder-like values, em-dashes for empty cells, or any text you want visually de-emphasized without changing its colour.

```html:preview
<zn-style>Regular text</zn-style>
<zn-style muted>Muted text</zn-style>
<zn-style muted>&mdash;</zn-style>
```

20 changes: 20 additions & 0 deletions scss/_root.scss
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,26 @@
font-family: var(--zn-font-family-mono, "monospace");
}

.zn-muted {
opacity: 0.5;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
opacity: 0.5;
color: var(--zn-color-muted-text);

}

.zn-size-xs {
font-size: var(--zn-font-size-x-small);
}

.zn-size-s {
font-size: var(--zn-font-size-small);
}

.zn-size-l {
font-size: var(--zn-font-size-large);
}

.zn-size-xl {
font-size: var(--zn-font-size-x-large);
}

:root, [t="light"], .theme-light, [t="dark"], .theme-dark {
--zn-body: var(--zn-color-body);

Expand Down
Loading