11<script setup>
2- import { computed , onMounted , ref } from ' vue' ;
2+ import { computed , onBeforeUnmount , onMounted , ref } from ' vue' ;
33import featuresData from ' ../../../roadmap/features.json' ;
44import { getModuleCoverage } from ' ../utils' ;
55
@@ -10,21 +10,20 @@ const statusFilters = [
1010 { key: ' all' , label: ' All' },
1111 { key: ' available' , label: ' Available' },
1212 { key: ' partially-implemented' , label: ' Partial' },
13- { key: ' not-serialized' , label: ' Not serialized' },
14- { key: ' na' , label: ' NA' },
13+ { key: ' na' , label: ' Unavailable' },
1514 { key: ' other' , label: ' Other' },
1615];
1716
18- const serializedModuleRows = computed (() => {
19- const entries = Object .entries (featuresData .moduleSerialization ? . serializedModules || {})
17+ const availableModules = computed (() => {
18+ const entries = Object .entries (featuresData .moduleStatus ? . available || {})
2019 return entries
2120 .map (([module , item ]) => ({ module , ... item }))
2221 .sort ((a , b ) => a .module .localeCompare (b .module ))
2322});
2423
2524function getRowStatusKey (row ) {
26- if (row? .status === ' not-serialized ' ) {
27- return ' not-serialized ' ;
25+ if (row? .status === ' na ' ) {
26+ return ' na ' ;
2827 }
2928 if (row? .status === ' available' ) {
3029 return ' available' ;
@@ -51,14 +50,13 @@ const statusCounts = computed(() => {
5150});
5251
5352const allModuleRows = computed (() => {
54- const serializedRows = serializedModuleRows .value ;
55- const notSerialized = (featuresData .moduleSerialization ? .notSerializedModules || []).map ((module ) => ({
53+ const unavailableRows = (featuresData .moduleStatus ? .unavailable || []).map ((module ) => ({
5654 module ,
57- status: ' not-serialized ' ,
55+ status: ' na ' ,
5856 version: ' —' ,
5957 }));
6058
61- return [... serializedRows , ... notSerialized ].sort ((a , b ) => a .module .localeCompare (b .module ));
59+ return [... availableModules . value , ... unavailableRows ].sort ((a , b ) => a .module .localeCompare (b .module ));
6260});
6361
6462const filteredModuleRows = computed (() => {
@@ -82,6 +80,7 @@ function getModuleGroupName(moduleName) {
8280 if (! moduleId) {
8381 return ' Other' ;
8482 }
83+ // These two have an uppercase character in the group name, so we need to check them first
8584 if (moduleId .startsWith (' GeoVis' )) {
8685 return ' GeoVis' ;
8786 }
@@ -110,8 +109,49 @@ const moduleGroups = computed(() => {
110109});
111110
112111const moduleCoverage = ref (new Map ());
112+ const openMissingPopoverFor = ref (null );
113+
114+ function getMissingClasses (moduleName ) {
115+ const moduleStats = moduleCoverage .value ? .get (moduleName);
116+ return Array .isArray (moduleStats? .missingClasses ) ? moduleStats .missingClasses : [];
117+ }
118+
119+ function getMissingCount (moduleName ) {
120+ return getMissingClasses (moduleName).length ;
121+ }
122+
123+ function getCoveragePercent (moduleName ) {
124+ const rawCoverage = moduleCoverage .value ? .get (moduleName)? .coverage ;
125+ if (typeof rawCoverage !== ' number' || Number .isNaN (rawCoverage)) {
126+ return 0 ;
127+ }
128+ return Math .min (100 , Math .max (0 , rawCoverage));
129+ }
130+
131+ function getMissingPopoverId (moduleName ) {
132+ return ` missing-classes-${ String (moduleName || ' ' ).replace (/ [^ a-zA-Z0-9 _-] / g , ' -' )} ` ;
133+ }
134+
135+ function toggleMissingPopover (moduleName ) {
136+ openMissingPopoverFor .value = openMissingPopoverFor .value === moduleName ? null : moduleName;
137+ }
138+
139+ function isMissingPopoverOpen (moduleName ) {
140+ return openMissingPopoverFor .value === moduleName;
141+ }
142+
143+ function closeMissingPopover () {
144+ openMissingPopoverFor .value = null ;
145+ }
146+
147+ function onDocumentClick (event ) {
148+ if (! event .target .closest (' .missing-badge-container' )) {
149+ closeMissingPopover ();
150+ }
151+ }
113152
114153onMounted (async () => {
154+ document .addEventListener (' click' , onDocumentClick);
115155 try {
116156 moduleCoverage .value = await getModuleCoverage ();
117157 } catch (error) {
@@ -120,6 +160,10 @@ onMounted(async () => {
120160 }
121161});
122162
163+ onBeforeUnmount (() => {
164+ document .removeEventListener (' click' , onDocumentClick);
165+ });
166+
123167function formatCoverage (value ) {
124168 if (value === undefined || value === null ) {
125169 return ' —' ;
@@ -217,14 +261,43 @@ function getHighlightParts(text) {
217261 < / template>
218262 < / td>
219263 < td>
220- < div>
221- < template v- if = " row.status === 'not-serialized '" > Not serialized < / template>
264+ < div class = " status-content " >
265+ < template v- if = " row.status === 'na '" > Unavailable < / template>
222266 < template v- else - if = " row.status === 'available'" >
223267 < div> ✓< / div>
224268 < small> {{ row .version || ' —' }}< / small>
225269 < / template>
226270 < template v- else - if = " isNotApplicable(row.version) || row.status === 'na'" > ⊘< / template>
227- < template v- else - if = " row.status === 'partially-implemented'" > {{ formatCoverage (moduleCoverage? .get (row .module )? .coverage ) }}< div> completed< / div>< / template>
271+ < template v- else - if = " row.status === 'partially-implemented'" >
272+ < div v- if = " getMissingCount(row.module) > 0" class = " missing-badge-container" >
273+ < button
274+ type= " button"
275+ class = " missing-badge"
276+ : aria- expanded= " isMissingPopoverOpen(row.module)"
277+ : aria- controls= " getMissingPopoverId(row.module)"
278+ : style= " { '--missing-progress': `${getCoveragePercent(row.module)}%` }"
279+ @click .stop = " toggleMissingPopover(row.module)"
280+ >
281+ < span class = " missing-badge-fill" aria- hidden= " true" / >
282+ < span class = " missing-badge-label" > {{ getMissingCount (row .module ) }} missing · {{ formatCoverage (getCoveragePercent (row .module )) }}< / span>
283+ < / button>
284+ < div
285+ v- if = " isMissingPopoverOpen(row.module)"
286+ : id= " getMissingPopoverId(row.module)"
287+ class = " missing-popover"
288+ role= " dialog"
289+ aria- label= " Missing class names"
290+ @click .stop
291+ >
292+ < div class = " missing-popover-title" > Missing classes< / div>
293+ < ul>
294+ < li v- for = " className in getMissingClasses(row.module)" : key= " `${row.module}-${className}`" >
295+ < a : href= " `https://vtk.org/doc/nightly/html/class${className}.html`" target= " _blank" rel= " noopener noreferrer" > {{ className }}< / a>
296+ < / li>
297+ < / ul>
298+ < / div>
299+ < / div>
300+ < / template>
228301 < template v- else > —< / template>
229302 < / div>
230303 < / td>
@@ -265,8 +338,8 @@ function getHighlightParts(text) {
265338}
266339
267340.status - chip .active {
268- border- color: var (-- vp- c- brand- 1 );
269- color: var (-- vp- c- brand- 1 );
341+ border- color: var (-- vp- c- brand- 2 );
342+ color: var (-- vp- c- brand- 2 );
270343 font- weight: 600 ;
271344}
272345
@@ -293,4 +366,81 @@ function getHighlightParts(text) {
293366.module - table th {
294367 text- align: center;
295368}
369+
370+ .status - content {
371+ position: relative;
372+ display: inline- flex;
373+ flex- direction: column;
374+ align- items: center;
375+ gap: 0 .1rem ;
376+ }
377+
378+ .missing - badge- container {
379+ position: relative;
380+ margin- top: 0 .1rem ;
381+ }
382+
383+ .missing - badge {
384+ border: 2px solid var (-- vp- c- divider);
385+ background: var (-- vp- c- bg- soft);
386+ color: var (-- vp- c- text- 1 );
387+ border- radius: 999px ;
388+ padding: 0 .1rem 0 .5rem ;
389+ font- size: 0 .72rem ;
390+ cursor: pointer;
391+ position: relative;
392+ overflow: hidden;
393+ }
394+
395+ .missing - badge: hover {
396+ border- color: var (-- vp- c- brand- 1 );
397+ color: var (-- vp- c- brand- 1 );
398+ }
399+
400+ .missing - badge- fill {
401+ position: absolute;
402+ inset: 0 ;
403+ width: var (-- missing- progress, 0 % );
404+ background: var (-- vp- c- brand- 2 );
405+ pointer- events: none;
406+ }
407+
408+ .missing - badge- label {
409+ position: relative;
410+ z- index: 1 ;
411+ }
412+
413+ .missing - popover {
414+ position: absolute;
415+ top: calc (100 % + 0 .25rem );
416+ left: 50 % ;
417+ transform: translateX (- 50 % );
418+ z- index: 20 ;
419+ min- width: 16rem ;
420+ max- width: 24rem ;
421+ max- height: 14rem ;
422+ overflow: auto;
423+ padding: 0 .45rem 0 .6rem ;
424+ border: 1px solid var (-- vp- c- divider);
425+ border- radius: 0 .4rem ;
426+ background: var (-- vp- c- bg);
427+ text- align: left;
428+ box- shadow: var (-- vp- shadow- 2 );
429+ }
430+
431+ .missing - popover- title {
432+ font- size: 0 .78rem ;
433+ font- weight: 600 ;
434+ margin- bottom: 0 .2rem ;
435+ }
436+
437+ .missing - popover ul {
438+ margin: 0 ;
439+ padding- left: 1rem ;
440+ }
441+
442+ .missing - popover li {
443+ font- size: 0 .75rem ;
444+ line- height: 1.35 ;
445+ }
296446< / style>
0 commit comments