New grid style and rudimentary page support added to report

This commit is contained in:
PMKuipers 2023-07-27 15:07:14 +02:00
parent 1c63ad83c2
commit 5d5ac2ce1c
4 changed files with 264 additions and 199 deletions

View File

@ -1,5 +1,7 @@
/*eslint no-var: "error"*/ /*eslint no-var: "error"*/
/*eslint no-console: "off"*/ /*eslint no-console: "off"*/
/*eslint no-unused-vars: warn */
/*eslint max-len: ["error", { "code": 160 }] */
/*eslint-disable no-trailing-spaces */ /*eslint-disable no-trailing-spaces */
/*eslint-env es6*/ /*eslint-env es6*/
// Put this file in path/to/plugin/amd/src // Put this file in path/to/plugin/amd/src
@ -94,6 +96,9 @@ export default {
return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
} }
// Create new eventbus for interaction between item components
const ItemEventBus = new Vue();
Vue.component('r-progress-circle',{ Vue.component('r-progress-circle',{
props: { props: {
value: { value: {
@ -238,108 +243,192 @@ export default {
return { return {
}; };
}, },
updated(){
this.$root.$emit('redrawLines');
},
mounted(){
this.$root.$emit('redrawLines');
},
computed: { computed: {
columns() {
return 1+ (this.page.periods * 2);
},
columns_stylerule() {
// Uses css variables, so width for slots and filters can be configured in css
let s = "grid-template-columns: var(--studyplan-filter-width)"; // use css variable here
for(let i=0; i<this.page.periods;i++){
s+= " var(--studyplan-course-width) var(--studyplan-filter-width)";
}
return s+";";
},
page() {
//FIXME: Replace this when actual page management is implemented
return this.value.pages[0];
},
}, },
methods: { methods: {
countLineLayers(line){
let maxLayer = -1;
for(let i = 0; i <= this.value.slots; i++){
const slot = line.slots[i];
// Determine the amount of used layers in a studyline slit
for(const ix in line.slots[i].competencies){
const item = line.slots[i].competencies[ix];
if(item.layer > maxLayer){
maxLayer = item.layer;
}
}
for(const ix in line.slots[i].filters){
const item = line.slots[i].filters[ix];
if(item.layer > maxLayer){
maxLayer = item.layer;
}
}
}
return maxLayer+1;
},
}, },
template: ` template: `
<div class='r-studyplan-content'> <div class='r-studyplan-content'>
<template v-if="value && value.studylines"> <!-- First paint the headings-->
<r-studyline v-for="(item,lineindex) in value.studylines" <div class='r-studyplan-headings'
:key="item.id" ><r-studyline-heading v-for="(line,lineindex) in page.studylines"
:color='item.color' :key="line.id"
:name='item.name' v-model="page.studylines[lineindex]"
:code='item.shortname' :layers='countLineLayers(line)+1'
:sequence='lineindex' :class=" 't-studyline' + ((lineindex%2==0)?' odd ' :' even ' )
:numlines='value.studylines.length' + ((lineindex==0)?' first ':' ')
:guestmode='guestmode' + ((lineindex==page.studylines.length-1)?' last ':' ')"
:teachermode='teachermode' ></r-studyline-heading
> ></div>
<template v-for="(n,index) in (value.slots+1)"> <!-- Next, paint all the cells in the scrollable -->
<r-studyline-slot <div class="r-studyplan-scrollable" >
<div class="r-studyplan-timeline" :style="columns_stylerule">
<!-- Line by line add the items -->
<!-- The grid layout handles putting it in rows and columns -->
<template v-for="(line,lineindex) in page.studylines"
><template v-for="layeridx in countLineLayers(line)+1"
><template v-for="(n,index) in (page.periods+1)"
><r-studyline-slot
v-if="index > 0" v-if="index > 0"
type='competency' type='gradable'
v-model="item.slots[index].competencies" v-model="line.slots[index].competencies"
:key="'c-'+index" :key="'c-'+lineindex+'-'+index+'-'+layeridx"
:slotindex="index" :slotindex="index"
:lineid="item.id" :line="line"
:plan="value" :plan="value"
:page="page"
:guestmode='guestmode' :guestmode='guestmode'
:teachermode='teachermode'>
</r-studyline-slot>
<r-studyline-slot
type='filter'
v-model="item.slots[index].filters"
:key="'f-'+index"
:slotindex="index"
:lineid="item.id"
:plan="value"
:teachermode='teachermode' :teachermode='teachermode'
:layer="layeridx-1"
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
+ ((lineindex==0 && layeridx==1)?' first ':' ')
+ ((lineindex==page.studylines.length-1)?' last ':' ')"
></r-studyline-slot
><r-studyline-slot
type='filter'
v-model="line.slots[index].filters"
:teachermode='teachermode'
:key="'f-'+lineindex+'-'+index+'-'+layeridx"
:slotindex="index"
:line="line"
:plan="value"
:page="page"
:layer="layeridx-1"
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
+ ((lineindex==0 && layeridx==1)?' first ':'')
+ ((lineindex==page.studylines.length-1)?' last ':' ')
+ ((index==page.periods)?' rightmost':'')"
> >
</r-studyline-slot> </r-studyline-slot
</template> ></template
</r-studyline> ></template
</template> ></template
</div> ></div
></div
></div>
`, `,
}); });
/* /*
* R-STUDYLINE * R-STUDYLINE-HEADER
*/ */
Vue.component('r-studyline', { Vue.component('r-studyline-heading', {
props: ['color','name','code', 'slots','sequence','numlines','guestmode','teachermode'], props: {
value : {
type: Object, // Studyline
default: function(){ return {};},
},
layers: {
type: Number,
default: 1,
},
},
data() { data() {
return { return {
layerHeights: {}
}; };
}, },
created() {
// Listener for the signal that a new connection was made and needs to be drawn
// Sent by the incoming item - By convention, outgoing items are responsible for drawing the lines
ItemEventBus.$on('lineHeightChange', this.onLineHeightChange);
},
computed: { computed: {
}, },
methods: { methods: {
onLineHeightChange(lineid,layerid,newheight){
// All layers for this line have the first slot send an update message on layer height change.
// When one of those updates is received, record the height and recalculate the total height of the
// header
if(this.$refs.mainEl && lineid == this.value.id){
const items = document.querySelectorAll(
`.r-studyline-slot-0[data-studyline='${this.value.id}']`);
// determine the height of all the lines and add them up.
let heightSum = 0;
items.forEach((el) => {
// getBoundingClientRect() Gets the actual fractional height instead of rounded to integer pixels
const r = el.getBoundingClientRect();
const height = r.height;
heightSum += height;
});
const heightStyle=`${heightSum}px`;
this.$refs.mainEl.style.height = heightStyle;
}
}
}, },
template: ` template: `
<div :class="'r-studyline ' + ((sequence%2)?'odd':'even') + <div class="r-studyline r-studyline-heading "
(sequence==0?' first':'') + (sequence==numlines-1?' last':'')"> :data-studyline="value.id" ref="mainEl"
<div class="r-studyline-handle" :style="'background-color: ' + color"></div> ><div class="r-studyline-handle" :style="'background-color: ' + value.color"></div>
<div class="r-studyline-title"> <div class="r-studyline-title">
<abbr v-b-popover.hover.top :title="name">{{ code }}</abbr> <abbr v-b-tooltip.hover :title="value.name">{{ value.shortname }}</abbr>
</div> </div>
<slot></slot>
</div> </div>
`, `,
}); });
Vue.component('r-studyline-slot', { Vue.component('r-studyline-slot', {
props: { props: {
value: {
type: Array, // item to display
default(){ return [];},
},
type : { type : {
type: String, type: String,
default: 'competency', default: 'gradable',
}, },
slotindex : { slotindex : {
type: Number, type: Number,
default: 0, default: 0,
}, },
lineid : { line : {
type: Number, type: Object,
default: 0, default(){ return null;},
}, },
value: { layer : {
type: Array, type: Number,
default(){ return [];},
}, },
plan: { plan: {
type: Object, type: Object,
default(){ return null;} default(){ return null;},
}, },
guestmode: { guestmode: {
type: Boolean, type: Boolean,
@ -350,14 +439,27 @@ export default {
default: false, default: false,
} }
}, },
computed: { mounted() {
sorted(){ const self=this;
let copy = [...this.value]; if(self.type == "gradable" && self.slotindex == 1){
copy.sort(function(a,b){ self.resizeListener = new ResizeObserver(() => {
return a.layer - b.layer; if(self.$refs.sizeElement){
}); const height = self.$refs.sizeElement.getBoundingClientRect().height;
return copy; ItemEventBus.$emit('lineHeightChange', self.line.id, self.layer, height);
} }
}).observe(self.$refs.sizeElement);
}
},
computed: {
item(){
for(const ix in this.value){
const itm = this.value[ix];
if(itm.layer == this.layer){
return itm;
}
}
return null;
},
}, },
data() { data() {
return { return {
@ -367,13 +469,19 @@ export default {
}, },
template: ` template: `
<div :class="'r-studyline-slot '+type + ' r-studyline-slot-'+slotindex"> <div :class=" 'r-studyline-slot ' + type + ' '
<r-item v-for="(item, index) in sorted" + 'r-studyline-slot-' + slotindex + ' '
:key="item.id" v-model="sorted[index]" + ((slotindex==0)?'r-studyline-firstcolumn ':' ')"
:data-studyline="line.id" ref="sizeElement"
><div class="t-slot-item" v-if="item"
><r-item
v-model="item"
:plan="plan" :plan="plan"
:guestmode='guestmode' :guestmode='guestmode'
:teachermode='teachermode'></r-item></drag> :teachermode='teachermode'></r-item
</div> ></div
></r-item
></div>
`, `,
}); });
@ -405,22 +513,22 @@ export default {
methods: { methods: {
lineColor(){ lineColor(){
if(this.teachermode){ if(this.teachermode){
return "#aaa"; return "var(--gray)";
} }
else{ else{
switch(this.value.completion){ switch(this.value.completion){
default: // "incomplete" default: // "incomplete"
return "#777"; return "var(--gray)";
case "failed": case "failed":
return "#933"; return "var(--danger)";
case "progress": case "progress":
return "#da3"; return "var(--warning)";
case "completed": case "completed":
return "#383"; return "var(--success)";
case "good": case "good":
return "#398"; return "var(--info)";
case "excellent": case "excellent":
return "#36f"; return "var(--blue)";
} }
} }
}, },
@ -458,11 +566,6 @@ export default {
elmWrapper.appendChild(elmLine); elmWrapper.appendChild(elmLine);
lineinfo.lineElm = elmLine; // store line element so it can more easily be removed from the dom lineinfo.lineElm = elmLine; // store line element so it can more easily be removed from the dom
} }
setTimeout(function(){
if(lineinfo.line){
lineinfo.line.position();
}
},1);
} }
}, },
redrawLines(){ redrawLines(){
@ -499,7 +602,6 @@ export default {
// Add resize event listener // Add resize event listener
window.addEventListener('resize',this.onWindowResize); window.addEventListener('resize',this.onWindowResize);
}, },
beforeDestroy(){ beforeDestroy(){
for(let i in this.value.connections.out){ for(let i in this.value.connections.out){
@ -519,7 +621,6 @@ export default {
} }
// Remove resize event listener // Remove resize event listener
window.removeEventListener('resize',this.onWindowResize); window.removeEventListener('resize',this.onWindowResize);
}, },
beforeUpdate(){ beforeUpdate(){
}, },

View File

@ -1524,7 +1524,7 @@ export default {
// All layers for this line have the first slot send an update message on layer height change. // All layers for this line have the first slot send an update message on layer height change.
// When one of those updates is received, record the height and recalculate the total height of the // When one of those updates is received, record the height and recalculate the total height of the
// header // header
if(lineid == this.value.id){ if(this.$refs.mainEl && lineid == this.value.id){
const items = document.querySelectorAll( const items = document.querySelectorAll(
`.t-studyline-slot-0[data-studyline='${this.value.id}']`); `.t-studyline-slot-0[data-studyline='${this.value.id}']`);
@ -1672,7 +1672,8 @@ export default {
}, },
computed: { computed: {
item(){ item(){
for(const itm of this.value){ for(const ix in this.value){
const itm = this.value[ix];
if(itm.layer == this.layer){ if(itm.layer == this.layer){
return itm; return itm;
} }

View File

@ -95,7 +95,7 @@ class studyplanpage {
public static function editor_structure($value=VALUE_REQUIRED){ public static function editor_structure($value=VALUE_REQUIRED){
return new \external_single_structure([ return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of studyplan'), "id" => new \external_value(PARAM_INT, 'id of studyplan'),
"name" => new \external_value(PARAM_TEXT, 'name of studyplan page'), "fullname" => new \external_value(PARAM_TEXT, 'name of studyplan page'),
"shortname"=> new \external_value(PARAM_TEXT, 'shortname of studyplan page'), "shortname"=> new \external_value(PARAM_TEXT, 'shortname of studyplan page'),
"description"=> new \external_value(PARAM_TEXT, 'description of studyplan page'), "description"=> new \external_value(PARAM_TEXT, 'description of studyplan page'),
"periods" => new \external_value(PARAM_INT, 'number of periods in studyplan page'), "periods" => new \external_value(PARAM_INT, 'number of periods in studyplan page'),
@ -110,7 +110,7 @@ class studyplanpage {
$model = [ $model = [
'id' => $this->r->id, 'id' => $this->r->id,
'name' => $this->r->name, 'fullname' => $this->r->fullname,
'shortname' => $this->r->shortname, 'shortname' => $this->r->shortname,
'description' => $this->r->description, 'description' => $this->r->description,
'periods' => $this->r->periods, 'periods' => $this->r->periods,
@ -198,7 +198,7 @@ class studyplanpage {
$model = [ $model = [
'id' => $this->r->id, 'id' => $this->r->id,
'fullname' => $this->r->name, 'fullname' => $this->r->fullname,
'shortname' => $this->r->shortname, 'shortname' => $this->r->shortname,
'description' => $this->r->description, 'description' => $this->r->description,
'periods' => $this->r->periods, 'periods' => $this->r->periods,

View File

@ -10,18 +10,23 @@
} }
.t-studyplan-content { .t-studyplan-content,
.r-studyplan-content {
display: flex; display: flex;
} }
.t-studyplan-headings {
.t-studyplan-headings,
.r-studyplan-headings {
display: block; display: block;
} }
.t-studyplan-wrapper { .t-studyplan-wrapper,
.r-studyplan-wrapper {
display: block; display: block;
} }
.t-studyplan-timeline { .t-studyplan-timeline,
.r-studyplan-timeline {
display: grid; display: grid;
position: relative; /* make sure this grid is the offset for all arrows that are drawn by SimpleLine */ position: relative; /* make sure this grid is the offset for all arrows that are drawn by SimpleLine */
/* grid-template-columns will be set in the style attribute */ /* grid-template-columns will be set in the style attribute */
@ -30,27 +35,32 @@
--studyplan-course-width: auto; /* better leave this at auto for now*/ --studyplan-course-width: auto; /* better leave this at auto for now*/
} }
.t-studyplan-scrollable { .t-studyplan-scrollable,
.r-studyplan-scrollable {
overflow-x: scroll; overflow-x: scroll;
scrollbar-color: var(--primary) color-mix(in srgb, var(--primary) 20%, white); scrollbar-color: var(--primary) color-mix(in srgb, var(--primary) 20%, white);
scrollbar-width: thin; scrollbar-width: thin;
} }
.t-studyplan-scrollable::-webkit-scrollbar { .t-studyplan-scrollable::-webkit-scrollbar,
.r-studyplan-scrollable::-webkit-scrollbar {
width: 8px; width: 8px;
} }
/* Track */ /* Track */
.t-studyplan-scrollable::-webkit-scrollbar-track { .t-studyplan-scrollable::-webkit-scrollbar-track,
.r-studyplan-scrollable::-webkit-scrollbar-track {
background: color-mix(in srgb, var(--primary) 20%, white); background: color-mix(in srgb, var(--primary) 20%, white);
} }
/* Handle */ /* Handle */
.t-studyplan-scrollable::-webkit-scrollbar-thumb { .t-studyplan-scrollable::-webkit-scrollbar-thumb,
.r-studyplan-scrollable::-webkit-scrollbar-thumb {
background:var(--primary); background:var(--primary);
} }
.t-studyplan-column-heading { .t-studyplan-column-heading,
.r-studyplan-column-heading {
color: inherit; /* placeholder */ color: inherit; /* placeholder */
} }
@ -58,7 +68,8 @@ ul.dropdown-menu.show {
background-color: white; background-color: white;
} }
.t-studyline { .t-studyline,
.r-studyline {
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: column;
/*border-bottom-style: solid;*/ /*border-bottom-style: solid;*/
@ -70,12 +81,13 @@ ul.dropdown-menu.show {
justify-content: start; justify-content: start;
} }
.t-studyline.t-studyline-heading,
.t-studyline.t-studyline-heading { .r-studyline.r-studyline-heading {
border-right-style: none; border-right-style: none;
} }
.t-studyline.end { .t-studyline.end,
.r-studyline.end {
border-right-style: solid; border-right-style: solid;
} }
@ -124,7 +136,8 @@ ul.dropdown-menu.show {
margin-bottom: 1em; margin-bottom: 1em;
} }
.t-studyline-title { .t-studyline-title,
.r-studyline-title {
padding-top: 5px; padding-top: 5px;
padding-left: 10px; padding-left: 10px;
width: 150px; width: 150px;
@ -138,7 +151,8 @@ ul.dropdown-menu.show {
} }
.t-studyline-title abbr { .t-studyline-title abbr,
.r-studyline-title abbr {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
font-weight: bold; font-weight: bold;
@ -229,19 +243,26 @@ ul.t-competency-list li {
} }
.t-studyline-slot { .t-studyline-slot,
.r-studyline-slot{
width: 130px; width: 130px;
} }
.t-studyline-slot.t-studyline-slot-0 { .r-studyline-slot {
min-height: 32px;
}
.t-studyline-slot.t-studyline-slot-0,
.r-studyline-slot.r-studyline-slot-0 {
width: 75px; width: 75px;
} }
.t-studyline-slot.t-studyline-slot-0 .t-slot-drop.filter .t-slot-item { .t-studyline-slot.t-studyline-slot-0 .t-slot-drop.filter .t-slot-item,
.r-studyline-slot.r-studyline-slot-0 .r-item-base {
margin-left: 10px; margin-left: 10px;
} }
.t-slot-drop { .t-slot-drop {
min-height: 32px; min-height: 32px;
height: 100%; height: 100%;
@ -318,7 +339,8 @@ ul.t-competency-list li {
max-width: 300px; max-width: 300px;
} }
.gradable .t-slot-item { .gradable .t-slot-item,
.gradable .r-slit-item {
width: 100%; width: 100%;
} }
@ -422,29 +444,29 @@ ul.t-toolbox li {
} }
.t-item-junction i { .t-item-junction i {
color: #eebb00; color: var(--warning);
} }
.t-item-finish i { .t-item-finish i {
color: #009900; color: var(--success);
} }
.t-item-start i { .t-item-start i {
color: #009900; color: var(--success);
} }
.t-item-badge svg { .t-item-badge svg {
color: #ddaa00; color: var(--warning);
} }
.t-slot-drop.type-allowed { .t-slot-drop.type-allowed {
border-color: green; border-color: var(--success);
border-style: dashed; border-style: dashed;
border-width: 1px; border-width: 1px;
} }
.t-slot-drop.type-allowed.drop-forbidden { .t-slot-drop.type-allowed.drop-forbidden {
border-color: red; border-color: var(--danger);
} }
.t-slot-drop.filter .t-item-base { .t-slot-drop.filter .t-item-base {
@ -528,11 +550,7 @@ a.t-item-course-config {
width: inherit; width: inherit;
} }
.r-studyplan-content {
overflow-y: visible;
width: min-content;
position: relative;
}
.r-studyplan-tab, .r-studyplan-tab,
.t-studyplan-tab { .t-studyplan-tab {
@ -541,19 +559,11 @@ a.t-item-course-config {
} }
.r-studyline {
width: min-content;
display: grid;
grid-auto-flow: column;
/*border-bottom-style: solid;*/
border-color: #cccccc;
border-width: 1px;
}
.t-studyline-drag:nth-child(odd) .t-studyline div, .t-studyline-drag:nth-child(odd) .t-studyline div,
.t-studyline-heading.odd, .t-studyline-heading.odd,
.r-studyline-heading.odd, .r-studyline-heading.odd,
.t-studyline-slot.odd{ .t-studyline-slot.odd,
.r-studyline-slot.odd {
background-color: #f0f0f0; background-color: #f0f0f0;
} }
@ -561,7 +571,7 @@ a.t-item-course-config {
.t-studyline-heading.first, .t-studyline-heading.first,
.t-studyline-slot.first, .t-studyline-slot.first,
.r-studyline-heading.first, .r-studyline-heading.first,
.r-studyline.first { .r-studyline-slot.first {
border-top-style: solid; border-top-style: solid;
} }
@ -569,12 +579,13 @@ a.t-item-course-config {
.t-studyline-heading.last, .t-studyline-heading.last,
.t-studyline-slot.last, .t-studyline-slot.last,
.r-studyline-heading.last, .r-studyline-heading.last,
.r-studyline.last { .r-studyline-slot.last {
border-bottom-style: solid; border-bottom-style: solid;
} }
.t-studyline-slot.rightmost { .t-studyline-slot.rightmost,
.r-studyline-slot.rightmost {
border-right-style: solid; border-right-style: solid;
} }
@ -588,55 +599,7 @@ a.t-item-course-config {
border-color: rgba(0, 0, 0, 0.125); border-color: rgba(0, 0, 0, 0.125);
} }
.r-studyline-title {
padding-top: 5px;
padding-left: 10px;
width: 130px;
flex-shrink: 0;
white-space: nowrap;
border-color: rgba(0, 0, 0, 0.125);
border-width: 1px;
border-left-style: solid;
border-right-style: solid;
display: flex;
flex-direction: column;
justify-content: center;
}
.r-studyline-title abbr {
display: inline-block;
vertical-align: middle;
font-weight: bold;
font-style: italic;
}
.r-studyline-slot {
width: 130px;
min-height: 32px;
min-width: 50px;
display: flex;
flex-shrink: 0;
flex-direction: column;
align-content: center;
justify-content: center;
}
.r-studyline-slot.r-studyline-slot-0 {
width: 75px;
}
.r-studyline-slot.r-studyline-slot-0 .r-item-base {
margin-left: 10px;
}
.r-studyline-slot.competency {
min-width: 100px;
}
.r-studyline-slot.filter {
min-width: 50px;
}
.r-item-base { .r-item-base {
margin-top: 5px; margin-top: 5px;
@ -646,7 +609,7 @@ a.t-item-course-config {
position: relative; position: relative;
} }
.competency .r-item-base { .gradable .r-item-base {
width: 100%; width: 100%;
} }
@ -674,11 +637,11 @@ a.t-item-course-config {
} }
.r-item-start i { .r-item-start i {
color: #009900; color: var(--success);
} }
.r-item-badge i { .r-item-badge i {
color: #ddaa00; color: var(--warning);
} }
.r-badges li { .r-badges li {
@ -789,28 +752,28 @@ tr.r-completion-category-header {
.r-item-finish.completion-incomplete, .r-item-finish.completion-incomplete,
.r-item-junction.completion-incomplete { .r-item-junction.completion-incomplete {
color: rgb(127, 127, 127); color: var(--gray);
} }
.r-item-finish.completion-progress, .r-item-finish.completion-progress,
.r-item-junction.completion-progress { .r-item-junction.completion-progress {
color: rgb(139, 107, 0); color: var(--warning);
} }
.r-item-finish.completion-completed, .r-item-finish.completion-completed,
.r-item-junction.completion-completed { .r-item-junction.completion-completed {
color: rgb(0, 126, 0); color: var(--success);
} }
.r-item-finish.completion-good, .r-item-finish.completion-good,
.r-item-junction.completion-good { .r-item-junction.completion-good {
color: #398; color: var(--info);
} }
.r-item-finish.completion-excellent, .r-item-finish.completion-excellent,
.r-item-junction.completion-excellent { .r-item-junction.completion-excellent {
color: rgb(0, 103, 255); color: var(--blue);
} }
.r-item-finish.completion-failed, .r-item-finish.completion-failed,
.r-item-junction.completion-failed { .r-item-junction.completion-failed {
color: #933; color: var(--danger);
} }
.r-activity-icon { .r-activity-icon {