Useful CSS Stuff

No, I try to keep most things consistent like you do, seeing your shared iterations. This was more geared towards widgets in the Dashboard component. If they have it take up half of the screen, there is a lot of space available to make the text bigger.

Alternative Alarm Status Table Styling - Use Pills for Priority and State columns

Disclaimer: I haven’t tested any of this all that thoroughly yet!

I had a request from a customer to change the alarm status table styles. This was a bit of fun (and headache).

This requires CSS and a bit of JS injection (required for the splitting of the State values).

It has a number of parts:

  1. Alarm Status Table component which contains specific rowStyles

    Alarm Status Table - Copy JSON and Paste into a Flex Container
    [
      {
        "type": "ia.display.alarmstatustable",
        "version": 0,
        "props": {
          "refreshRate": 2000,
          "toolbar": {
            "enableShelvedTab": false,
            "enableFilter": false,
            "toggleableFilter": false,
            "enablePreFilters": false,
            "enableConfiguration": false
          },
          "filters": {
            "active": {
              "priorities": {
                "diagnostic": true
              }
            }
          },
          "rowStyles": {
            "activeUnacked": {
              "base": {
                "backgroundColor": "white",
                "color": "hsl(0 0% 5%)",
                "maxHeight": "3rem",
                "minHeight": "3rem"
              },
              "priorities": {
                "low": {
                  "backgroundColor": "white"
                },
                "medium": {
                  "backgroundColor": "white"
                },
                "high": {
                  "backgroundColor": "white"
                },
                "critical": {
                  "backgroundColor": "white"
                }
              }
            },
            "activeAcked": {
              "base": {
                "backgroundColor": "white",
                "color": "hsl(0 0% 5%)",
                "fontWeight": "normal",
                "maxHeight": "3rem",
                "minHeight": "3rem"
              },
              "priorities": {
                "low": {
                  "backgroundColor": "white"
                },
                "medium": {
                  "backgroundColor": "white"
                },
                "high": {
                  "backgroundColor": "white"
                },
                "critical": {
                  "backgroundColor": "white"
                }
              }
            },
            "clearUnacked": {
              "base": {
                "backgroundColor": "white",
                "fontWeight": "lighter",
                "color": "hsl(0 0% 70%)",
                "maxHeight": "3rem",
                "minHeight": "3rem"
              },
              "priorities": {
                "diagnostic": {
                  "backgroundColor": "white",
                  "color": "hsl(0 0% 70%)"
                },
                "low": {
                  "backgroundColor": "white"
                },
                "medium": {
                  "backgroundColor": "white"
                },
                "high": {
                  "backgroundColor": "white"
                },
                "critical": {
                  "backgroundColor": "white"
                }
              }
            },
            "clearAcked": {
              "base": {
                "backgroundColor": "white",
                "color": "hsl(0 0% 70%)",
                "fontWeight": "lighter",
                "maxHeight": "3rem",
                "minHeight": "3rem"
              },
              "priorities": {
                "diagnostic": {
                  "color": "hsl(0 0% 70%)"
                },
                "low": {
                  "backgroundColor": "white"
                },
                "medium": {
                  "backgroundColor": "white"
                },
                "high": {
                  "backgroundColor": "white"
                },
                "critical": {
                  "backgroundColor": "white"
                }
              }
            }
          },
          "dateFormat": "DD MMM yyyy HH:mm z",
          "activeSortOrder": [
            "activeTime"
          ],
          "columns": {
            "active": {
              "activeTime": {
                "sort": "descending",
                "width": 230,
                "strictWidth": true,
                "order": 3
              },
              "displayPath": {
                "width": 350,
                "strictWidth": true
              },
              "priority": {
                "sort": "none",
                "width": 160,
                "strictWidth": true,
                "order": 0
              },
              "state": {
                "sort": "none",
                "width": 320,
                "strictWidth": true,
                "order": 4
              },
              "source": {
                "enabled": false,
                "width": 240,
                "order": 3
              },
              "name": {
                "enabled": false,
                "order": 2
              }
            }
          },
          "columnsAssociated": {
            "active": [
              {
                "field": "Description",
                "enabled": true,
                "width": "",
                "strictWidth": false,
                "sort": "none",
                "order": 2
              }
            ]
          }
        },
        "meta": {
          "name": "AlarmStatusTable"
        },
        "position": {
          "grow": 1,
          "basis": "400px"
        },
        "custom": {}
      }
    ]
    
  2. Markdown Javascript injector component which converts the State column text into separate components and adds them into a flex

    Markdown JS Injector (State-only mods) - Copy JSON and Paste into the View containing the AST
    [
      {
        "type": "ia.display.markdown",
        "version": 0,
        "props": {
          "style": {
            "flex": "--neutral-40"
          },
          "markdown": {
            "escapeHtml": false
          }
        },
        "meta": {
          "name": "JSInject_ConvertStateColToPillBoxes"
        },
        "position": {
          "shrink": 0,
          "basis": 0
        },
        "custom": {
          "inlineJavascript": "(() => {\n    const styleAlarmStates = () => {\n        document.querySelectorAll(\".ia_table__cell[data-column-id=state]:not(.ia_table__head__header__cell) .content > div\").forEach(el => {\n            if (el.querySelector(\".custom_alarmStatusTable__stateCol_pill\"))\n                return;\n            const text = el.textContent.trim();\n            if (!text)\n                return;\n            const parts = text.split(\",\").map(s => s.trim());\n            const classMap = {\n                \"Active\": \"custom_alarmStatusTable__stateCol_active\",\n                \"Cleared\": \"custom_alarmStatusTable__stateCol_cleared\",\n                \"Acknowledged\": \"custom_alarmStatusTable__stateCol_ack\",\n                \"Unacknowledged\": \"custom_alarmStatusTable__stateCol_unack\"\n            };\n            const pills = parts.map(part => {\n                const stateClass = classMap[part] || \"\";\n                return \"<span class=\\\"custom_alarmStatusTable__stateCol_pill \" + stateClass + \"\\\">\" + part + \"</span>\";\n            }).join(\"\");\n            el.innerHTML = \"<div class=\\\"custom_alarmStatusTable__stateCol_container\\\">\" + pills + \"</div>\";\n        });\n    };\n    const setupObserver = () => {\n        let rafId;\n        const observer = new MutationObserver(() => {\n            if (rafId)\n                return;\n            rafId = requestAnimationFrame(() => {\n                styleAlarmStates();\n                rafId = null;\n            });\n        });\n        const tableContainer = document.querySelector(\".ReactVirtualized__Grid__innerScrollContainer\");\n        if (tableContainer) {\n            styleAlarmStates();\n            observer.observe(tableContainer, {\n                childList: true,\n                attributes: true,\n                subtree: true,\n                attributeFilter: [\"title\", \"style\"]\n            });\n        }\n    };\n    setupObserver();\n    if (!document.querySelector(\".ReactVirtualized__Grid__innerScrollContainer\")) {\n        const waitObserver = new MutationObserver(() => {\n            if (document.querySelector(\".ReactVirtualized__Grid__innerScrollContainer\")) {\n                waitObserver.disconnect();\n                setupObserver();\n            }\n        });\n        waitObserver.observe(document.body, {\n            childList: true,\n            subtree: true\n        });\n    }\n})();\n"
        },
        "propConfig": {
          "props.source": {
            "binding": {
              "config": {
                "struct": {
                  "script": "{this.custom.inlineJavascript}"
                },
                "waitOnAll": true
              },
              "transforms": [
                {
                  "code": "\tcode =  \"<img style='display:none' src='/favicon.ico' onload=\\\"\" + value.script.replace('\"', '&quot;') + '\\\"></img>'\n\t\n\tcode = code.replace(\"\\n\", \"\").replace(\"\\t\", \"\").replace(\"\\n\", \"\").replace(\"\\r\", \"\").replace(\"\\r\", \"\").replace(\"  \", \" \").replace(\"  \", \" \").replace(\"  \", \" \").replace(\"  \", \" \")\n\t\n\treturn code",
                  "type": "script"
                }
              ],
              "type": "expr-struct"
            }
          }
        }
      }
    ]
    
  3. CSS to add to the Advanced Stylesheet

    Stylesheet CSS to define the: alarm priority colours and the state pill styles
    /* === Setup the standard colour variables ================================================================================= */
    
    :root {
    	/** Format: --c-<area>-<prop><-modifier>, where 'c' means 'colour' **/
    	
    	/* Alarm Priority Colours */
    	--c-alarm-critical:   hsl(5 95% 65%); /* red */
    	--c-alarm-critical-text:   white;
    	
    	--c-alarm-high:       hsl(30, 98%, 55%); /* orange */
    	--c-alarm-high-text:       white;
    	
    	--c-alarm-medium:     hsl(50, 98%, 71%); /* yellow */
    	--c-alarm-medium-text:     hsl(0, 0%, 5%);
    	
    	--c-alarm-low:        hsl(200, 88%, 51%); /* blue */
    	--c-alarm-low-text:        white;
    	
    	--c-alarm-diagnostic: hsl(0, 0%, 75%); /* grey */
    	--c-alarm-diagnostic-text: white;
    }
    
    /* === Alarm Status Table Styles =================================================================== */
    
    /* Allow cell text to word wrap */
    .alarmStatusTable .ia_table__cell .content div {
    	white-space: pre-wrap;
    }
    
    /* Make the Priority column show a pill-box with the priority colour in the background */
    .alarmStatusTable .ia_table__cell[data-column-id="priority"] .content:has([title]){
    	width: 7rem;
    	border-radius: 50vh;
    	height: 1.7rem;
    	padding: 0.2rem 0.5rem;
    	text-align: center;
    	font-weight: normal;
    	text-transform: uppercase;
    	justify-content: center !important;
    	align-items: center;
    }
    .alarmStatusTable .ia_table__cell[data-column-id="priority"] .content:has([title="Critical"]) {
    	background-color: var(--c-alarm-critical);
    	color: var(--c-alarm-critical-text);
    }
    .alarmStatusTable .ia_table__cell[data-column-id="priority"] .content:has([title="High"]) {
    	background-color: var(--c-alarm-high);
    	color: var(--c-alarm-high-text);
    }
    .alarmStatusTable .ia_table__cell[data-column-id="priority"] .content:has([title="Medium"]) {
    	background-color: var(--c-alarm-medium);
    	color: var(--c-alarm-medium-text);
    }
    .alarmStatusTable .ia_table__cell[data-column-id="priority"] .content:has([title="Low"]) {
    	background-color: var(--c-alarm-low);
    	color: var(--c-alarm-low-text);
    }
    .alarmStatusTable .ia_table__cell[data-column-id="priority"] .content:has([title="Diagnostic"]) {
    	background-color: var(--c-alarm-diagnostic);
    	color: var(--c-alarm-diagnostic-text);
    }
    
    
    /* defines the flex container housing the two state pills */
    .custom_alarmStatusTable__stateCol_container {
      display: flex;
      gap: 0.3rem;
      align-items: center;;
    }
    /* styles for the pill containers housing the active/cleared and ack/unack states */
    .custom_alarmStatusTable__stateCol_pill {
      border-top-left-radius: 50vh;
      border-top-right-radius: 50vh;
      border-bottom-left-radius: 50vh;
      border-bottom-right-radius: 50vh;
      margin: ;height: 150%;
      padding: 0.2rem 0.5rem;
      text-transform: uppercase;
    }
    /* styles for the ack state */
    .custom_alarmStatusTable__stateCol_ack {
      background-color: #C0C1BE;
      color: hsl(0 0% 5%);
      font-weight: normal;
    }
    /* styles for the unack state */
    .custom_alarmStatusTable__stateCol_unack {
      background-color: #FFC700;
      color: hsl(0 0% 5%);
      font-weight: normal;
    }
    /* styles for the active state */
    .custom_alarmStatusTable__stateCol_active {
      background-color: #98E737;
      color: hsl(0 0% 5%);
      font-weight: normal;
    }
    /* styles for the cleared state */
    .custom_alarmStatusTable__stateCol_cleared {
      background-color: #C0C1BE;
      color: hsl(0 0% 5%);
      font-weight: normal;
    }
    

If you just want to import a single View, here’s the project export in 8.3.2. You’ll still need to copy in the styling into the adv. stylesheet though.

AlarmStatusTable-AlternateStyling.zip (44.2 KB)

They also wanted the datetime formatted differently as well, in case you want this too. If so, replace the Markdown above with this version:

Markdown JS Injector (State & Datetime mods) - Copy JSON and Paste into the View containing the AST
[
  {
    "type": "ia.display.markdown",
    "version": 0,
    "props": {
      "style": {
        "position": "absolute"
      },
      "markdown": {
        "escapeHtml": false
      }
    },
    "meta": {
      "name": "JSInject_ConvertStateColToPillBoxes_0"
    },
    "position": {
      "shrink": 0,
      "basis": 0
    },
    "custom": {
      "inlineJavascript": "(() => {\n    const styleAlarmStates = () => {\n        document.querySelectorAll(\".ia_table__cell[data-column-id=state]:not(.ia_table__head__header__cell) .content > div\").forEach(el => {\n            if (el.querySelector(\".custom_alarmStatusTable__stateCol_pill\"))\n                return;\n            const text = el.textContent.trim();\n            if (!text)\n                return;\n            const parts = text.split(\",\").map(s => s.trim());\n            const classMap = {\n                \"Active\": \"custom_alarmStatusTable__stateCol_active\",\n                \"Cleared\": \"custom_alarmStatusTable__stateCol_cleared\",\n                \"Acknowledged\": \"custom_alarmStatusTable__stateCol_ack\",\n                \"Unacknowledged\": \"custom_alarmStatusTable__stateCol_unack\"\n            };\n            const pills = parts.map(part => {\n                const stateClass = classMap[part] || \"\";\n                return \"<span class=\\\"custom_alarmStatusTable__stateCol_pill \" + stateClass + \"\\\">\" + part + \"</span>\";\n            }).join(\"\");\n            el.innerHTML = \"<div class=\\\"custom_alarmStatusTable__stateCol_container\\\">\" + pills + \"</div>\";\n        });\n    };\n\n    const formatDateTimeCells = () => {\n        document.querySelectorAll('.alarmStatusTable .ia_table__cell:not(.ia_table__head__header__cell) .content > div').forEach(div => {\n            const title = div.getAttribute('title');\n            const datetimeRegex = /^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{4}$/;\n            \n            if (title && datetimeRegex.test(title) && !div.querySelector('div[style*=\"flex\"]')) {\n                const text = div.textContent.trim();\n                const parts = text.split(' ');\n                \n                if (parts.length >= 5) {\n                    const date = parts.slice(0, 3).join(' ').trim();\n                    const time = parts.slice(3).join(' ').trim();\n                    \n                    div.innerHTML = `\n                        <div style=\"display: flex; flex-direction: column; gap: 2px; align-items: flex-end;\">\n                            <div style=\"font-weight: bold;\">${date}</div>\n                            <div style=\"font-size: 0.9em; color: #666;\">${time}</div>\n                        </div>\n                    `;\n                }\n            }\n        });\n    };\n\n    const setupObserver = () => {\n        let rafId;\n        const observer = new MutationObserver(() => {\n            if (rafId)\n                return;\n            rafId = requestAnimationFrame(() => {\n                styleAlarmStates();\n                formatDateTimeCells();\n                rafId = null;\n            });\n        });\n        const tableContainer = document.querySelector(\".ReactVirtualized__Grid__innerScrollContainer\");\n        if (tableContainer) {\n            styleAlarmStates();\n            formatDateTimeCells();\n            observer.observe(tableContainer, {\n                childList: true,\n                attributes: true,\n                subtree: true,\n                attributeFilter: [\"title\", \"style\"]\n            });\n        }\n    };\n\n    const visibilityObserver = new MutationObserver((mutations) => {\n        for (const mutation of mutations) {\n            if (mutation.type === \"attributes\" &&\n                mutation.attributeName === \"style\" &&\n                mutation.target.getAttribute(\"data-component\") === \"ia.display.alarmstatustable\" &&\n                mutation.target.style.display !== \"none\") {\n                console.log(\"alarmStatusTable visibility observer fired\");\n                if (document.querySelector(\".ReactVirtualized__Grid__innerScrollContainer\")) {\n                    setupObserver();\n                } else {\n                    const revealWaitObserver = new MutationObserver(() => {\n                        if (document.querySelector(\".ReactVirtualized__Grid__innerScrollContainer\")) {\n                            revealWaitObserver.disconnect();\n                            setupObserver();\n                        }\n                    });\n                    revealWaitObserver.observe(document.body, {\n                        childList: true,\n                        subtree: true\n                    });\n                }\n            }\n        }\n    });\n\n    visibilityObserver.observe(document.body, {\n        subtree: true,\n        attributes: true,\n        attributeFilter: [\"style\"]\n    });\n\n    setupObserver();\n    if (!document.querySelector(\".ReactVirtualized__Grid__innerScrollContainer\")) {\n        const waitObserver = new MutationObserver(() => {\n            if (document.querySelector(\".ReactVirtualized__Grid__innerScrollContainer\")) {\n                waitObserver.disconnect();\n                setupObserver();\n            }\n        });\n        waitObserver.observe(document.body, {\n            childList: true,\n            subtree: true\n        });\n    }\n})();"
    },
    "propConfig": {
      "props.source": {
        "binding": {
          "config": {
            "struct": {
              "script": "{this.custom.inlineJavascript}"
            },
            "waitOnAll": true
          },
          "transforms": [
            {
              "code": "#\tcode =  \"<img style='display:none' src='/favicon.ico' onload=\\\"\" + value.script.replace('\"', '&quot;') + '\\\"></img>'\n#\t\n#\tcode = code.replace(\"\\n\", \"\").replace(\"\\t\", \"\").replace(\"\\n\", \"\").replace(\"\\r\", \"\").replace(\"\\r\", \"\").replace(\"  \", \" \").replace(\"  \", \" \").replace(\"  \", \" \").replace(\"  \", \" \")\n#\t\n#\treturn code\n\tscript = value.script\n\tscript = script.replace('\"', '&quot;')\n\tscript = script.replace('\\n', '').replace('\\t', '').replace('\\r', '')\n\t\n\t# Collapse multiple spaces - repeat until stable\n\twhile '  ' in script:\n\t    script = script.replace('  ', ' ')\n\t\n\tcode = '<img style=\"display:none\" src=\"/favicon.ico\" onload=\"{}\"></img>'.format(script)\n\treturn code",
              "type": "script"
            }
          ],
          "type": "expr-struct"
        }
      }
    }
  }
]

Edit: Updated the Markdown JSON (State & Datetime mods version only) with a fix to address the AST not re-formatting when it’s hidden and shown again by toggling the display prop

17 Likes

HSL (ā€œhuman readableā€) Colour Cheat Sheet

I've added this to a few posts now before realising it really belongs here…

I made an HSL cheat sheet in Perspective to help remember the Hue values in the human-readable HSL colour format (as opposed to the relatively human-unintelligable RGB).

And if you need help convincing why you should abandon RGB for HSL:

View Exports:

HSL Cheat Sheet.zip (27.8 KB)

I won’t* even mention the OKLCH colour model… (similar to HSL except lightness is perceptually uniform across all hues for the same LC values, but the C values are kind of weird..). The only minor downside of HSL is that between L=30% and 70%, different hues will look to have different lightness, which OKLCH solves.

HSL OKLCH Colour Cheat Sheet.zip (30.0 KB)

*too late

2 Likes

Imagine a scenario where you have a rotation animation in a css style. If you try to apply that to a sub-element of an SVG it will rotate out of frame because SVGs anchor at 0,0 instead of center.

Apply this style along with your spinner animation to rotate a sub-element of an SVG around its center

.psc-center-origin{
transform-origin: center;
transform-box: fill-box;
}

1 Like

I looked into this again briefly.

A comment, you need to set a parent ā€œcontainerā€ to be a container for this to work. If you don’t, the page container is used and the units don’t behave any different to vh and vw.

E.g.

However, as I suspected, using these units and always setting the container to be the embedded view or the parent container (flex/coord/etc) will result in a vast array of differing font sizes, since it’s based on the size of the container (embedded view / flex container / etc) and these will more than likely be different for different elements. See the example above, where the top and bottom embedded views are both instances of the same view, but one is made wider resulting in the font size being larger.

I thought I had font sizes mostly sorted with rem units and media queries to set the root font-size based on screen size, however a colleague came to me yesterday with an issue where he’s embedded main overview views onto a wider overview page where the embedded main overviews are made smaller to fit on the 1 page, and obviously the font size is now still relative to screen size rather than the size of the embedded view container, and hence the fonts are too big to fit… Using container query units would fix this, however at the expense of standardised font sizes which makes it look unprofessional… the font size saga continues.

Waffle…

Edit: The above is true for cqw and cqh, but cqmax and cqmin (and by extension the i and b units) don’t seem to have this behaviour… :distorted_face: i.e. below blue bordered containers are the same embedded views at different sizes. the cqmax labels, despite being contained within different max-size containers, are both the same size. Whereas the cqw labels are different sizes. I need to read up more on how the cqi and cqb units work..

Edit2:

OK, so container-type: inline-size only looks at the width, for a horizontal language like English, and cqmax looks at both width (i - inline) and height (b - block), and because inline-size only provides the width, this container is ignored and it searches further up the tree until it finds a container-type: size which provides both width and height, or the viewport itself. So if I set the embedded views instead to container-type: size, they become effectively the same size as cqw in this instance..

4 Likes

I’m glad I’m not alone… :face_with_bags_under_eyes:

I’m not worried, I haven’t been using Perspective for very long, only like 7 years. Font sizing is a pretty advanced topic :slightly_smiling_face:

2 Likes

I did notice that the state pill boxes are inconsistent when using this as a docked view. Sometimes they transform and sometimes they dont. That’s the only bug that I have observed.

1 Like

I updated the Markdown JSON for the state and datetime version to address something similar. Not sure if it also addresses your issue.