@internetstiftelsen/styleguide 5.1.14 β†’ 5.1.15-beta.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,313 @@
1
+ import * as agGrid from 'ag-grid-community';
2
+ import { themeQuartz, iconSetMaterial } from 'ag-grid-community';
3
+ import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community';
4
+
5
+ ModuleRegistry.registerModules([AllCommunityModule]);
6
+
7
+ // ---- THEME -----------------------------------------------------------
8
+ const iisTheme = themeQuartz
9
+ .withPart(iconSetMaterial)
10
+ .withParams({
11
+ accentColor: '#0477CE',
12
+ backgroundColor: '#FFFFFF',
13
+ borderRadius: 3,
14
+ browserColorScheme: 'light',
15
+ cellTextColor: '#1F2A36',
16
+ chromeBackgroundColor: { ref: 'foregroundColor', mix: 0.07, onto: 'backgroundColor' },
17
+ fontFamily: 'inherit',
18
+ fontSize: 16,
19
+ foregroundColor: '#1F2A36',
20
+ headerBackgroundColor: '#D8D8D8',
21
+ headerFontFamily: 'inherit',
22
+ headerFontSize: 16,
23
+ headerFontWeight: 400,
24
+ headerTextColor: '#1F2A36',
25
+ oddRowBackgroundColor: '#EDEDED',
26
+ wrapperBorderRadius: 3,
27
+ });
28
+
29
+ // ---- RENDERERS -------------------------------------------------------
30
+ const renderStatusIcon = (params) => {
31
+ const status = params.value;
32
+ if (!status) return '';
33
+ const map = {
34
+ passed: { icon: '#icon-security-variant', color: '#25c279', label: 'Passed' },
35
+ failed: { icon: '#icon-unsecure-variant', color: '#d9002f', label: 'Failed' },
36
+ warning: { icon: '#icon-warning-variant', color: '#f99963', label: 'Warning' },
37
+ error: { icon: '#icon-unsecure-variant', color: '#8E9297', label: 'Error' },
38
+ not_tested: { icon: '#icon-unsecure-variant', color: '#d8d8d8', label: 'Not tested' },
39
+ informational: { icon: '#icon-info-variant', color: '#50b2fc', label: 'Informational' },
40
+ };
41
+ const { icon, color, label } = map[status] || {};
42
+ return icon
43
+ ? `<span class="cell-center">
44
+ <svg class="status-icon" fill="${color}" width="20" height="20" aria-label="${label}">
45
+ <use xlink:href="${icon}"></use>
46
+ </svg>
47
+ <span class="status-text">${label}</span>
48
+ </span>`
49
+ : status;
50
+ };
51
+
52
+ // ---- HELPERS ---------------------------------------------------------
53
+ const titleCase = (s) => s.replace(/[_\-\.]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
54
+
55
+ function buildColumnDefsFromData(data) {
56
+ if (!Array.isArray(data) || data.length === 0) return [{ headerName: 'No Data', field: 'noData' }];
57
+ const sample = data[0];
58
+
59
+ // Internet.nl-like shape
60
+ if (sample && sample.results && typeof sample.results === 'object') {
61
+ const base = [
62
+ { headerName: 'Domain', field: 'domain', minWidth: 200 },
63
+ { headerName: 'Score (%)', field: 'score_percent', filter: 'agNumberColumnFilter', minWidth: 140 },
64
+ {
65
+ headerName: 'Report',
66
+ field: 'report_url',
67
+ cellRenderer: (p) => (p.value ? `<a href="${p.value}" target="_blank" rel="noopener">View</a>` : ''),
68
+ maxWidth: 220,
69
+ flex: 0,
70
+ },
71
+ ];
72
+
73
+ const resultKeys = Object.keys(sample.results);
74
+ const childColIds = [];
75
+ const children = resultKeys.map((k, idx) => {
76
+ const colId = `results.${k}.status`;
77
+ childColIds.push(colId);
78
+ return {
79
+ colId,
80
+ headerName: titleCase(k),
81
+ field: colId,
82
+ cellRenderer: renderStatusIcon,
83
+ tooltipValueGetter: (p) => {
84
+ const since = p?.data?.results?.[k]?.since;
85
+ return since ? `Since: ${since}` : '';
86
+ },
87
+ cellClass: (p) => `status-${p.value}`,
88
+ minWidth: 150,
89
+ sortable: true,
90
+ filter: true,
91
+ hide: idx > 0, // start collapsed (only first child visible)
92
+ };
93
+ });
94
+
95
+ base.push({
96
+ headerName: 'Results',
97
+ headerGroupComponent: 'ExpandHeader', // πŸ‘ˆ correct prop for groups
98
+ headerGroupComponentParams: {
99
+ childColIds, // toggle these children together
100
+ startExpanded: false, // set true to start expanded
101
+ },
102
+ children,
103
+ });
104
+
105
+ return base;
106
+ }
107
+
108
+ // Generic fallback
109
+ const cols = [];
110
+ for (const [key, val] of Object.entries(sample)) {
111
+ if (val && typeof val === 'object' && !Array.isArray(val)) {
112
+ for (const subKey of Object.keys(val)) {
113
+ if (val[subKey] !== null && typeof val[subKey] !== 'object') {
114
+ cols.push({
115
+ headerName: `${titleCase(key)} β€’ ${titleCase(subKey)}`,
116
+ field: `${key}.${subKey}`,
117
+ sortable: true,
118
+ filter: true,
119
+ minWidth: 140,
120
+ });
121
+ }
122
+ }
123
+ } else {
124
+ cols.push({
125
+ headerName: titleCase(key),
126
+ field: key,
127
+ sortable: true,
128
+ filter: true,
129
+ minWidth: 120,
130
+ });
131
+ }
132
+ }
133
+ return cols;
134
+ }
135
+
136
+
137
+
138
+ // Version-safe column state helper
139
+ function setColumnsState(params, { state = [], defaultState = { hide: false, pinned: null } }) {
140
+ const columnApi =
141
+ params.columnApi ||
142
+ (params.api && typeof params.api.getColumnApi === 'function' ? params.api.getColumnApi() : null);
143
+
144
+ if (columnApi && typeof columnApi.applyColumnState === 'function') {
145
+ columnApi.applyColumnState({ state, defaultState });
146
+ return;
147
+ }
148
+ if (params.api && typeof params.api.applyColumnState === 'function') {
149
+ params.api.applyColumnState({ state, defaultState });
150
+ return;
151
+ }
152
+ if (columnApi && typeof columnApi.setColumnState === 'function') {
153
+ if (typeof columnApi.resetColumnState === 'function') columnApi.resetColumnState();
154
+ columnApi.setColumnState(state);
155
+ return;
156
+ }
157
+ console.warn('No applicable column state API found in this AG Grid version.');
158
+ }
159
+
160
+ // Per-grid responsive visibility
161
+ function applyResponsiveVisibility(params, el) {
162
+ const w = el?.getBoundingClientRect().width || el?.clientWidth || 0;
163
+ if (w < 520) {
164
+ setColumnsState(params, {
165
+ state: [
166
+ { colId: 'domain', pinned: 'left' },
167
+ { colId: 'results.rpki.status', hide: true },
168
+ { colId: 'results.security_options.status', hide: true },
169
+ { colId: 'results.ipv6.status', hide: true },
170
+ ],
171
+ });
172
+ } else if (w < 760) {
173
+ setColumnsState(params, {
174
+ state: [
175
+ { colId: 'domain', pinned: null },
176
+ { colId: 'results.rpki.status', hide: true },
177
+ ],
178
+ });
179
+ } else {
180
+ setColumnsState(params, { state: [] });
181
+ }
182
+ }
183
+
184
+
185
+ // ---- EXPAND/COLLAPSE GROUP HEADER -----------------------------------
186
+ class ExpandHeader {
187
+ init(params) {
188
+ this.params = params;
189
+ this.childColIds = params.childColIds || [];
190
+ this.expanded = !!params.startExpanded;
191
+
192
+ // Debug: prove we mounted
193
+ console.log('[ExpandHeader] init for group:', params.displayName, 'children:', this.childColIds);
194
+
195
+ // Create a full-width, clickable label (no AG classes needed)
196
+ const e = document.createElement('div');
197
+ e.className = 'expand-header';
198
+ e.style.cssText = 'display:flex;align-items:center;gap:6px;cursor:pointer;width:100%;';
199
+ e.innerHTML = `
200
+ <span class="expander" style="display:inline-block;transition:transform .15s">β–Ά</span>
201
+ <span class="title">${params.displayName ?? 'Group'}</span>
202
+ `;
203
+
204
+ e.addEventListener('click', () => {
205
+ this.expanded = !this.expanded;
206
+ this.updateIcon();
207
+ toggleChildColumnsVisibility(params, this.childColIds, this.expanded);
208
+ });
209
+
210
+ this.eGui = e;
211
+ this.updateIcon();
212
+
213
+ // Apply initial expanded state
214
+ if (this.expanded) {
215
+ toggleChildColumnsVisibility(params, this.childColIds, true);
216
+ }
217
+ }
218
+ updateIcon() {
219
+ const icon = this.eGui.querySelector('.expander');
220
+ if (icon) icon.style.transform = this.expanded ? 'rotate(90deg)' : 'rotate(0deg)';
221
+ }
222
+ getGui() { return this.eGui; }
223
+ refresh() { return true; }
224
+ }
225
+
226
+ function toggleChildColumnsVisibility(params, colIds, makeVisible) {
227
+ const columnApi =
228
+ params.columnApi ||
229
+ (params.api && typeof params.api.getColumnApi === 'function'
230
+ ? params.api.getColumnApi()
231
+ : null);
232
+ if (!columnApi) return;
233
+ columnApi.setColumnsVisible(colIds, !!makeVisible);
234
+ params.api?.sizeColumnsToFit?.();
235
+ }
236
+
237
+ // Factory to create per-grid options with the correct closures
238
+ function makeGridOptionsFor(el) {
239
+ return {
240
+ theme: iisTheme,
241
+ components: { ExpandHeader },
242
+ defaultColDef: {
243
+ resizable: true,
244
+ sortable: true,
245
+ filter: true,
246
+ flex: 1,
247
+ minWidth: 150,
248
+ unSortIcon: true,
249
+ },
250
+ columnDefs: [],
251
+ rowData: [],
252
+ async onGridReady(params) {
253
+ try {
254
+ // dynamic JSON import based on *this* element's data attribute
255
+ const attr = el.dataset.json || './table.json';
256
+ const jsonUrl = new URL(attr, import.meta.url).toString();
257
+ const mod = await import(/* @vite-ignore */ jsonUrl);
258
+ const data = mod?.default ?? mod;
259
+
260
+ const cols = buildColumnDefsFromData(data);
261
+ params.api.setGridOption('columnDefs', cols);
262
+ params.api.setGridOption('rowData', data);
263
+
264
+ applyResponsiveVisibility(params, el);
265
+ params.api.sizeColumnsToFit();
266
+ } catch (e) {
267
+ console.error('Dynamic JSON import failed for grid:', el, e);
268
+ }
269
+ },
270
+ onGridSizeChanged(params) {
271
+ applyResponsiveVisibility(params, el);
272
+ params.api.sizeColumnsToFit();
273
+ },
274
+ animateRows: true,
275
+ };
276
+ }
277
+
278
+ // ---- INIT ALL GRIDS --------------------------------------------------
279
+ document.addEventListener('DOMContentLoaded', () => {
280
+ // Use a class selector so you can have multiple grids;
281
+ // make sure your HTML uses: <div class="ag-theme-quartz js-ag-grid" data-json="./table1.json"></div>
282
+ const containers = document.querySelectorAll('.js-ag-grid');
283
+
284
+ containers.forEach((el) => {
285
+ // Ensure theme class + measurable size BEFORE createGrid (each grid separately)
286
+ if (!el.classList.contains('ag-theme-quartz')) el.classList.add('ag-theme-quartz');
287
+ //if (!el.style.height) el.style.height = '600px';
288
+ if (!el.style.width) el.style.width = '100%';
289
+
290
+ const gridOptions = makeGridOptionsFor(el);
291
+ const api = agGrid.createGrid(el, gridOptions);
292
+
293
+ // Per-grid ResizeObserver (don’t reuse one global API)
294
+ const ro = new ResizeObserver(() => {
295
+ gridOptions.api?.onGridSizeChanged();
296
+ gridOptions.api?.sizeColumnsToFit();
297
+ });
298
+ ro.observe(el);
299
+ });
300
+ });
301
+
302
+ // ---- TOGGLE FULLSCREEN CLASS ON PARENT CONTAINER WHEN CLICKING FULLSCREEN BUTTON ----
303
+ document.querySelectorAll('[data-ag-grid-fullscreen]').forEach((btn) => {
304
+ btn.addEventListener('click', (e) => {
305
+
306
+ // Find the nearest parent container for this button
307
+ const gridEl = btn.closest('.js-ag-grid');
308
+ if (!gridEl) return;
309
+
310
+ // Toggle fullscreen class only for this specific element
311
+ gridEl.classList.toggle('has-fullscreen');
312
+ });
313
+ });
@@ -0,0 +1,72 @@
1
+ [
2
+ {
3
+ "Category": "Modern address (IPv6)",
4
+ "passed": "43.11%",
5
+ "info": "0%",
6
+ "warning": "0%",
7
+ "failed": "56.89%",
8
+ "not testable": "0%",
9
+ "not applicable": "0%",
10
+ "test error": "0%"
11
+ },
12
+ {
13
+ "Category": "Signed domain name (DNSSEC)",
14
+ "passed": "52.96%",
15
+ "info": "0%",
16
+ "warning": "0%",
17
+ "failed": "47.04%",
18
+ "not testable": "0%",
19
+ "not applicable": "0%",
20
+ "test error": "0%"
21
+ },
22
+ {
23
+ "Category": "Secure connection following NCSC requirements (HTTPS)",
24
+ "passed": "19.22%",
25
+ "info": "0%",
26
+ "warning": "0%",
27
+ "failed": "76.6%",
28
+ "not testable": "0%",
29
+ "not applicable": "0%",
30
+ "test error": "4.17%"
31
+ },
32
+ {
33
+ "Category": "Security options",
34
+ "passed": "1.09%",
35
+ "info": "0%",
36
+ "warning": "98.91%",
37
+ "failed": "0%",
38
+ "not testable": "0%",
39
+ "not applicable": "0%",
40
+ "test error": "0%"
41
+ },
42
+ {
43
+ "Category": "Route authorisation (RPKI)",
44
+ "passed": "92%",
45
+ "info": "0%",
46
+ "warning": "0%",
47
+ "failed": "8%",
48
+ "not testable": "0%",
49
+ "not applicable": "0%",
50
+ "test error": "0%"
51
+ },
52
+ {
53
+ "Category": "Extra Fields",
54
+ "passed": "2.43%",
55
+ "info": "5.61%",
56
+ "warning": "0.99%",
57
+ "failed": "90.81%",
58
+ "not testable": "0%",
59
+ "not applicable": "0%",
60
+ "test error": "0.16%"
61
+ },
62
+ {
63
+ "Category": "A",
64
+ "passed": "42.16%",
65
+ "info": "1.12%",
66
+ "warning": "19.98%",
67
+ "failed": "55.87%",
68
+ "not testable": "0%",
69
+ "not applicable": "0%",
70
+ "test error": "0.87%"
71
+ }
72
+ ]