Skip to content

Commit 77b2fb7

Browse files
committed
feat: Add data-table shortcode
Render JSON, CSV, or YAML data files as static HTML tables at build time with optional client-side column sorting via progressive enhancement. Support column selection, header label overrides, and wide/narrow layout variants. Interactive mode adds vanilla JS sorting with Lucide sort icons. No-JS fallback is a full static table.
1 parent 4c61a34 commit 77b2fb7

File tree

8 files changed

+733
-2
lines changed

8 files changed

+733
-2
lines changed

assets/css/v2/style.css

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,14 @@ textarea:not([rows]) {
134134
/* More info: https://docs.coveo.com/en/atomic/latest/usage/themes-and-visual-customization/ */
135135

136136
/* biome-ignore lint: Coveo override */
137-
--atomic-primary: oklch(var(--color-brand)) !important; /* Adjust the primary color */
137+
--atomic-primary: oklch(
138+
var(--color-brand)
139+
) !important; /* Adjust the primary color */
138140
/* biome-ignore lint: necessary override */
139-
--atomic-ring-primary: oklch(var(--color-brand) / 0.4) !important; /* Adjust the focus color */
141+
--atomic-ring-primary: oklch(
142+
var(--color-brand) /
143+
0.4
144+
) !important; /* Adjust the focus color */
140145
/* biome-ignore lint: necessary override */
141146
--atomic-primary-light: oklch(var(--color-brand)) !important;
142147
--atomic-background: oklch(0.9911 0 0) !important;
@@ -2003,6 +2008,77 @@ table hr {
20032008
width: auto;
20042009
}
20052010

2011+
/* MARK: Data Tables
2012+
*/
2013+
2014+
.data-table-wrapper {
2015+
--dt-header-bg: oklch(var(--color-foreground) / 5%);
2016+
--dt-focus-ring: oklch(var(--color-foreground) / 40%);
2017+
2018+
margin-block: var(--margin-table);
2019+
}
2020+
2021+
/* Sortable column headers */
2022+
.data-table-wrapper th[data-column] {
2023+
cursor: pointer;
2024+
user-select: none;
2025+
white-space: nowrap;
2026+
2027+
&:hover {
2028+
background: var(--dt-header-bg);
2029+
}
2030+
2031+
&:focus-visible {
2032+
outline: 2px solid var(--dt-focus-ring);
2033+
outline-offset: -2px;
2034+
}
2035+
}
2036+
2037+
.data-table-th-content {
2038+
display: inline-flex;
2039+
align-items: center;
2040+
gap: 0.25rem;
2041+
}
2042+
2043+
/* Sort icons: three Lucide SVGs per header, toggled by aria-sort */
2044+
.data-table-sort-icon {
2045+
display: none;
2046+
line-height: 0;
2047+
2048+
& .lucide {
2049+
width: 0.85em;
2050+
height: 0.85em;
2051+
vertical-align: middle;
2052+
}
2053+
}
2054+
2055+
.data-table-sort-icon--none {
2056+
display: inline-flex;
2057+
opacity: 0.35;
2058+
}
2059+
2060+
th[aria-sort="ascending"] .data-table-sort-icon--none,
2061+
th[aria-sort="descending"] .data-table-sort-icon--none {
2062+
display: none;
2063+
}
2064+
2065+
th[aria-sort="ascending"] .data-table-sort-icon--asc {
2066+
display: inline-flex;
2067+
opacity: 1;
2068+
}
2069+
2070+
th[aria-sort="descending"] .data-table-sort-icon--desc {
2071+
display: inline-flex;
2072+
opacity: 1;
2073+
}
2074+
2075+
.data-table-noscript {
2076+
font-size: var(--step--1);
2077+
font-style: italic;
2078+
color: oklch(var(--color-foreground) / 60%);
2079+
margin-block-start: 0.5rem;
2080+
}
2081+
20062082
/* MARK: Callouts
20072083
*/
20082084
blockquote {

assets/js/data-table.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* data-table.js
3+
* Progressive enhancement for data-table shortcode.
4+
* Adds column sorting to server-rendered HTML tables.
5+
*
6+
* No-JS fallback: tables remain fully readable without this script.
7+
*/
8+
9+
(() => {
10+
const SORT_ASC = 'ascending';
11+
const SORT_DESC = 'descending';
12+
13+
/**
14+
* Initialize all data-table instances on the page.
15+
*/
16+
function initAll() {
17+
const wrappers = document.querySelectorAll('.data-table-wrapper');
18+
for (const wrapper of wrappers) {
19+
initTable(wrapper);
20+
}
21+
}
22+
23+
/**
24+
* Initialize a single data-table instance.
25+
* @param {HTMLElement} wrapper - The .data-table-wrapper element
26+
*/
27+
function initTable(wrapper) {
28+
const table = wrapper.querySelector('table');
29+
if (!table) return;
30+
31+
const thead = table.querySelector('thead');
32+
const tbody = table.querySelector('tbody');
33+
if (!thead || !tbody) return;
34+
35+
const headers = Array.from(thead.querySelectorAll('th[data-column]'));
36+
37+
// State
38+
const state = {
39+
sortCol: wrapper.dataset.defaultSort || null,
40+
sortDir: wrapper.dataset.defaultSortDir || 'asc',
41+
};
42+
43+
// Cache original rows
44+
const allRows = Array.from(tbody.querySelectorAll('tr'));
45+
46+
// Build column index map
47+
const colIndex = {};
48+
for (let i = 0; i < headers.length; i++) {
49+
colIndex[headers[i].dataset.column] = i;
50+
}
51+
52+
// --- Sort controls ---
53+
for (const th of headers) {
54+
th.setAttribute('tabindex', '0');
55+
th.setAttribute('role', 'columnheader');
56+
57+
const handleSort = () => {
58+
const col = th.dataset.column;
59+
if (state.sortCol === col) {
60+
state.sortDir = state.sortDir === 'asc' ? 'desc' : 'asc';
61+
} else {
62+
state.sortCol = col;
63+
state.sortDir = 'asc';
64+
}
65+
updateSortIndicators(headers, state);
66+
render(tbody, allRows, colIndex, state);
67+
};
68+
69+
th.addEventListener('click', handleSort);
70+
th.addEventListener('keydown', (e) => {
71+
if (e.key === 'Enter' || e.key === ' ') {
72+
e.preventDefault();
73+
handleSort();
74+
}
75+
});
76+
}
77+
78+
// Set initial sort indicators
79+
updateSortIndicators(headers, state);
80+
}
81+
82+
/**
83+
* Update sort direction indicators on column headers.
84+
* Icons are rendered by Hugo (Lucide SVGs) and toggled via CSS
85+
* based on the aria-sort attribute. JS only manages aria-sort.
86+
*/
87+
function updateSortIndicators(headers, state) {
88+
for (const th of headers) {
89+
const col = th.dataset.column;
90+
if (col === state.sortCol) {
91+
th.setAttribute(
92+
'aria-sort',
93+
state.sortDir === 'desc' ? SORT_DESC : SORT_ASC
94+
);
95+
} else {
96+
th.removeAttribute('aria-sort');
97+
}
98+
}
99+
}
100+
101+
/**
102+
* Sort rows and update the DOM.
103+
*/
104+
function render(tbody, allRows, colIndex, state) {
105+
let rows = allRows;
106+
107+
if (state.sortCol && colIndex[state.sortCol] !== undefined) {
108+
const idx = colIndex[state.sortCol];
109+
const dir = state.sortDir === 'desc' ? -1 : 1;
110+
111+
rows = [...rows].sort((a, b) => {
112+
const aCell = a.querySelectorAll('td')[idx];
113+
const bCell = b.querySelectorAll('td')[idx];
114+
const aText = aCell?.textContent?.trim() || '';
115+
const bText = bCell?.textContent?.trim() || '';
116+
117+
// Try numeric comparison first
118+
const aNum = Number.parseFloat(aText);
119+
const bNum = Number.parseFloat(bText);
120+
if (!Number.isNaN(aNum) && !Number.isNaN(bNum)) {
121+
return (aNum - bNum) * dir;
122+
}
123+
124+
// Fall back to locale-aware string comparison
125+
return (
126+
aText.localeCompare(bText, undefined, { sensitivity: 'base' }) * dir
127+
);
128+
});
129+
}
130+
131+
// Update DOM -- clear tbody and re-append in order
132+
while (tbody.firstChild) {
133+
tbody.removeChild(tbody.firstChild);
134+
}
135+
136+
for (const row of rows) {
137+
tbody.appendChild(row);
138+
}
139+
}
140+
141+
// Initialize when DOM is ready
142+
if (document.readyState === 'loading') {
143+
document.addEventListener('DOMContentLoaded', initAll);
144+
} else {
145+
initAll();
146+
}
147+
})();
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
[
2+
{
3+
"endpoint": "/api/v1/users",
4+
"method": "GET",
5+
"description": "List all users with optional pagination",
6+
"auth": "Bearer token",
7+
"rate_limit": "100/min",
8+
"status": "stable"
9+
},
10+
{
11+
"endpoint": "/api/v1/users/{id}",
12+
"method": "GET",
13+
"description": "Get a single user by ID",
14+
"auth": "Bearer token",
15+
"rate_limit": "200/min",
16+
"status": "stable"
17+
},
18+
{
19+
"endpoint": "/api/v1/users",
20+
"method": "POST",
21+
"description": "Create a new user",
22+
"auth": "Bearer token",
23+
"rate_limit": "50/min",
24+
"status": "stable"
25+
},
26+
{
27+
"endpoint": "/api/v1/users/{id}",
28+
"method": "PUT",
29+
"description": "Update an existing user",
30+
"auth": "Bearer token",
31+
"rate_limit": "50/min",
32+
"status": "stable"
33+
},
34+
{
35+
"endpoint": "/api/v1/users/{id}",
36+
"method": "DELETE",
37+
"description": "Delete a user",
38+
"auth": "Admin token",
39+
"rate_limit": "20/min",
40+
"status": "stable"
41+
},
42+
{
43+
"endpoint": "/api/v1/groups",
44+
"method": "GET",
45+
"description": "List all groups",
46+
"auth": "Bearer token",
47+
"rate_limit": "100/min",
48+
"status": "stable"
49+
},
50+
{
51+
"endpoint": "/api/v1/groups/{id}/members",
52+
"method": "GET",
53+
"description": "List members of a group",
54+
"auth": "Bearer token",
55+
"rate_limit": "100/min",
56+
"status": "stable"
57+
},
58+
{
59+
"endpoint": "/api/v2/analytics",
60+
"method": "GET",
61+
"description": "Retrieve analytics data with date range filtering",
62+
"auth": "Bearer token",
63+
"rate_limit": "30/min",
64+
"status": "beta"
65+
},
66+
{
67+
"endpoint": "/api/v2/analytics/export",
68+
"method": "POST",
69+
"description": "Export analytics data to CSV or PDF",
70+
"auth": "Admin token",
71+
"rate_limit": "5/min",
72+
"status": "beta"
73+
},
74+
{
75+
"endpoint": "/api/v1/health",
76+
"method": "GET",
77+
"description": "Health check endpoint",
78+
"auth": "None",
79+
"rate_limit": "unlimited",
80+
"status": "stable"
81+
},
82+
{
83+
"endpoint": "/api/v1/config",
84+
"method": "GET",
85+
"description": "Get current server configuration",
86+
"auth": "Admin token",
87+
"rate_limit": "10/min",
88+
"status": "stable"
89+
},
90+
{
91+
"endpoint": "/api/v1/config",
92+
"method": "PATCH",
93+
"description": "Update server configuration",
94+
"auth": "Admin token",
95+
"rate_limit": "5/min",
96+
"status": "stable"
97+
},
98+
{
99+
"endpoint": "/api/v2/certificates",
100+
"method": "GET",
101+
"description": "List SSL/TLS certificates",
102+
"auth": "Bearer token",
103+
"rate_limit": "50/min",
104+
"status": "beta"
105+
},
106+
{
107+
"endpoint": "/api/v2/certificates",
108+
"method": "POST",
109+
"description": "Upload a new SSL/TLS certificate",
110+
"auth": "Admin token",
111+
"rate_limit": "10/min",
112+
"status": "beta"
113+
},
114+
{
115+
"endpoint": "/api/v1/upstreams",
116+
"method": "GET",
117+
"description": "List all upstream server groups",
118+
"auth": "Bearer token",
119+
"rate_limit": "100/min",
120+
"status": "stable"
121+
}
122+
]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Parameter,Required,Default,Description
2+
path,Yes,,Path to the data file relative to assets/ (e.g. /data/endpoints.json)
3+
interactive,No,false,Set to true to enable client-side column sorting
4+
sort,No,,Default sort column. Append :asc or :desc for direction (e.g. name:desc)
5+
columns,No,all,Comma-separated list of columns to display in the given order
6+
hide,No,,Comma-separated list of columns to exclude
7+
labels,No,,Column header overrides in key:Label format comma-separated (e.g. rate_limit:Throttle)
8+
variant,No,wide,Layout width: wide (full content area) or narrow (two-thirds)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Version,Release Date,Status,NGINX Plus,OS Support,EOL Date
2+
3.2.0,2025-09-15,current,R33,Ubuntu 24.04 / RHEL 9 / Debian 12,2026-09-15
3+
3.1.2,2025-06-10,supported,R32,Ubuntu 22.04 / RHEL 9 / Debian 12,2026-06-10
4+
3.1.1,2025-04-22,supported,R32,Ubuntu 22.04 / RHEL 9 / Debian 12,2026-04-22
5+
3.1.0,2025-02-01,supported,R31,Ubuntu 22.04 / RHEL 9 / Debian 11,2026-02-01
6+
3.0.3,2024-11-18,supported,R31,Ubuntu 22.04 / RHEL 8 / Debian 11,2025-11-18
7+
3.0.2,2024-09-05,eol,R30,Ubuntu 22.04 / RHEL 8 / Debian 11,2025-09-05
8+
3.0.1,2024-07-12,eol,R30,Ubuntu 22.04 / RHEL 8 / Debian 11,2025-07-12
9+
3.0.0,2024-05-01,eol,R30,Ubuntu 22.04 / RHEL 8 / Debian 11,2025-05-01
10+
2.9.1,2024-02-15,eol,R29,Ubuntu 20.04 / RHEL 8 / Debian 11,2025-02-15
11+
2.9.0,2023-12-01,eol,R29,Ubuntu 20.04 / RHEL 8 / Debian 10,2024-12-01

0 commit comments

Comments
 (0)