@kubed.io/cdn
v0.0.8
Published
A theme layer and web components for Grafana panels, loaded by URL from jsDelivr.
Readme
📦 cdn
A theme layer and web components for Grafana panels. Shared CSS and ES modules that dashboards load by URL instead of carrying copies: the npm package @kubed.io/cdn, served by jsDelivr. 🎨
🗂️ What is here
| Entry | What it brings |
|---|---|
| dist/kd.js | 🎨 the core: the theme layer, the Grafana page helpers, and every generic element |
| dist/kd.css | text, links, tables, badges, code, cards, and column/tile/part layouts for plain .kd markup |
| dist/openapi.js | 📜 <kd-schema>: any OpenAPI v3 or JSON Schema, kubectl-explain style, rendered lazily |
| dist/k8s.js | ☸️ Kubernetes objects mapped onto the core elements: <kd-k8s-object>, <kd-k8s-ref>, <kd-k8s-events> |
| dist/n8n.js | 💬 mount(config): the n8n agent chat tile for app dashboards |
| dist/github.js | 🐙 GitHub glue: a git tree for <kd-files>, blob/raw/README URLs, code-server activity and Claude edits per file |
The entries share chunks, so a page that loads several pays for Lit and the core once. The look lives in the core; a domain entry adds behaviour, never colours.
Elements (all kd-*, real custom elements with shadow DOM, themed from --kd-*):
| | |
|---|---|
| kd-pill kd-tile kd-link kd-mask kd-reveal | small things: a toned pill, a header tile, a dashboard link that keeps the time range and a back chain, a click-to-reveal value, one toggle for every masked value around it |
| kd-bar kd-sheet kd-table | a title bar with chips and a breadcrumb, a property sheet (or a grid of label-over-value tiles), a table with optional row headers; cells can be text, pills, links, code or data |
| kd-status kd-card | the health of anything as a headline and toned check badges; a bordered block with a tone accent, a header, a state line and a body |
| kd-groups kd-data | labels as grouped pills, annotations as a tree; any JSON value as folded YAML |
| kd-tabs kd-accordion kd-split | tabs that remember their choice, collapsible sections on one page, a list beside the selected item |
| kd-steps kd-meter | a stepper, a value against request/limit marks |
| kd-markdown kd-code | GitHub-flavoured markdown, sanitised; syntax highlighting with line numbers, in 190+ languages loaded one at a time |
| kd-files kd-file | a file explorer over any provider (a static list today, WebDAV later) that can drive a dashboard variable; one file shown with the right renderer |
The source is TypeScript under src/, one folder per entry. dist/ is built by CI and exists only in the published package.
🔗 The URL
https://cdn.jsdelivr.net/npm/@kubed.io/[email protected]/dist/<file>An exact version is cached for a year and can never change underneath a dashboard. Never a range like @0 in a dashboard: it is stale for up to a week and moves without notice.
Each dashboard holds the base URL once, in a hidden constant variable named assets:
- kind: ConstantVariable
spec:
name: assets
query: https://cdn.jsdelivr.net/npm/@kubed.io/[email protected]/dist
hide: hideVariableA release is then a one-line change per dashboard. Move every dashboard together: panels share one page, and the first version of a custom element loaded owns its tag until a full reload.
🎨 The theme
applyTheme sets these on the panel root, plus data-theme="dark" or "light". The names are the public API and follow semver.
| Property | From GrafanaTheme2 |
|---|---|
| --kd-bg, --kd-bg-2, --kd-canvas | colors.background.primary, .secondary, .canvas |
| --kd-text, --kd-text-2, --kd-text-dim | colors.text.primary, .secondary, .disabled |
| --kd-link | colors.text.link |
| --kd-border, --kd-border-strong | colors.border.weak, .medium |
| --kd-primary, --kd-success, --kd-warning, --kd-error, --kd-info | colors.<name>.main |
| --kd-font, --kd-font-mono | typography.fontFamily, .fontFamilyMonospace |
| --kd-radius | shape.radius.default |
| --kd-space | spacing(1) |
Everything is scoped under .kd. Business Text loads stylesheets into the whole page, so no rule here touches an element, * or :root outside a .kd root.
🧩 In a Business Text panel
externalStyles: [{ id: "kd", url: "${assets}/kd.css" }] # objects: a bare string never loads
content: <div class="kd"> ... </div>
afterRender: import(context.grafana.replaceVariables("${assets}") + "/kd.js")
.then(m => m.applyTheme(context.element, context.grafana.theme))afterRender runs on every re-render and applyTheme is idempotent, so switching Grafana's theme restyles the panel. Code with no context.grafana.theme calls followTheme(element) instead: it reads the live theme from Grafana's runtime and follows a theme switch.
Every panel imports what it needs itself. The browser loads a URL once per page however many panels import it, and an element defined by one panel works in all of them.
Feeding an element. Elements draw from their data and nothing else, because Business Text recreates them whenever the panel's HTML changes. Push data in from afterRender on every render:
content: <div class="kd"><kd-table></kd-table></div>
afterRender: import(base + "/kd.js").then(m => {
m.applyTheme(context.element, context.grafana.theme)
context.element.querySelector("kd-table").data = { rows: context.data }
})Markup from a query can carry the data as a JSON attribute instead, escaped: data="{{kdjson rows}}" with the kdjson helper (registerHelpers(context.handlebars)), or @json | @html in Infinity jq. Never {{{ }}} JSON into an attribute.
Or let feed do it - it themes the panel and hands each element its data in one call, and elements write
dashboard variables themselves:
content: <kd-files variable="file"></kd-files>
afterRender: import(base + "/kd.js").then(m => m.feed(context, { "kd-files": { entries: context.data } }))Ready-made panels are in examples/.
🛠️ Develop
npm ci
npm run typecheck
npm test
npm run buildUnreleased code is tried in Grafana by publishing a pre-release (a pre* bump in the 🧬 Publish Version workflow), which goes to npm under the next tag.
