| 1 | // @effect-diagnostics globalDate:off globalTimers:off - Vendored @clo/lib has no Effect dependency. |
| 2 | import { UNWRAP } from "./assert.ts"; |
| 3 | import type { Events } from "./Events.ts"; |
| 4 | import type * as log from "./log.ts"; |
| 5 | import * as math from "./math.ts"; |
| 6 | import type { ReadOnlyNode, Root } from "./progress.ts"; |
| 7 | import { isVisible, maxLogLines, remainingSeconds } from "./progress-format.ts"; |
| 8 | import * as string from "./string.ts"; |
| 9 | import * as ansi from "./string/ansi.ts"; |
| 10 | import * as ts from "./ts.ts"; |
| 11 | |
| 12 | /** |
| 13 | * draw `root` into the shadow root of `host`, after the Windows 7 progress bar |
| 14 | * and busy cursor, on the animation frame after each change, until disposed. |
| 15 | * the shadow root keeps the page and the tree from styling each other, and the |
| 16 | * tree follows the page's `color-scheme`. `host` must accept a shadow root, |
| 17 | * such as a `div`; one rendered by {@linkcode formatHtml} is taken over. |
| 18 | * ```ts |
| 19 | * const root = new progress.Root(); |
| 20 | * const detach = progress.attachToDocument(root, document.getElementById("progress")!); |
| 21 | * await build(root); |
| 22 | * detach(); |
| 23 | * ``` |
| 24 | */ |
| 25 | export function attachToDocument<R, M extends Events.Map>( |
| 26 | root: Root<R, M>, |
| 27 | host: Element, |
| 28 | ): ts.Dispose { |
| 29 | const shadow = host.shadowRoot ?? host.attachShadow({ mode: "open" }); |
| 30 | shadow.innerHTML = formatShadow(""); |
| 31 | const tree = UNWRAP(shadow.lastElementChild); |
| 32 | const template = document.createElement("template"); |
| 33 | const elements = new WeakMap<ReadOnlyNode, Element>(); |
| 34 | const changedAt = new WeakMap<ReadOnlyNode, number>(); |
| 35 | const staleLogs = new Set<ReadOnlyNode>(); |
| 36 | let frame: number | null = null; |
| 37 | let tick: ts.Timer | undefined; |
| 38 | let tickIn = Infinity; |
| 39 | |
| 40 | const redraw = () => void (frame ??= requestAnimationFrame(draw)); |
| 41 | function draw() { |
| 42 | frame = null; |
| 43 | for (const node of staleLogs) { |
| 44 | const logs = elements.get(node)?.children[2]; |
| 45 | if (logs) logs.innerHTML = formatLogs(node.logs); |
| 46 | } |
| 47 | staleLogs.clear(); |
| 48 | clearTimeout(tick); |
| 49 | tickIn = Infinity; |
| 50 | sync(tree, root.active); |
| 51 | if (tickIn !== Infinity) tick = setTimeout(redraw, tickIn); |
| 52 | } |
| 53 | function sync(list: Element, nodes: readonly ReadOnlyNode[]) { |
| 54 | let next = list.firstElementChild; |
| 55 | for (const node of nodes) { |
| 56 | if (!isVisible(node)) continue; |
| 57 | // estimates show whole seconds, rounded, so each changes on a half second |
| 58 | const ms = (node.estimatedTime ?? 0) - Date.now(); |
| 59 | if (ms > 0) tickIn = Math.min(tickIn, ((ms + 500) % 1000) + 5); |
| 60 | let li = elements.get(node); |
| 61 | if (li) { |
| 62 | update(li, node, changedAt); |
| 63 | } else { |
| 64 | template.innerHTML = formatNode(node, ""); |
| 65 | elements.set(node, (li = UNWRAP(template.content.firstElementChild))); |
| 66 | changedAt.set(node, performance.now()); |
| 67 | } |
| 68 | if (li === next) next = li.nextElementSibling; |
| 69 | else list.insertBefore(li, next); |
| 70 | sync(UNWRAP(li.children[1]), node.children); |
| 71 | } |
| 72 | while (next) { |
| 73 | const stale = next; |
| 74 | next = next.nextElementSibling; |
| 75 | stale.remove(); |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | const listeners = [ |
| 80 | root.on("node-start", redraw), |
| 81 | root.on("node-change", (node, key) => { |
| 82 | if (key === "logs") staleLogs.add(node); |
| 83 | redraw(); |
| 84 | }), |
| 85 | root.on("node-end", redraw), |
| 86 | ]; |
| 87 | draw(); |
| 88 | return ts.defer(() => { |
| 89 | for (const off of listeners) off(); |
| 90 | if (frame !== null) cancelAnimationFrame(frame); |
| 91 | clearTimeout(tick); |
| 92 | shadow.replaceChildren(); |
| 93 | }); |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * the markup {@linkcode attachToDocument} draws for `list`, as a declarative |
| 98 | * shadow root to place inside the element it later takes over. browsers attach |
| 99 | * it only while parsing a document, such as a server rendered page. |
| 100 | */ |
| 101 | export function formatHtml(list: readonly ReadOnlyNode[]): string { |
| 102 | const format = (nodes: readonly ReadOnlyNode[]): string => |
| 103 | nodes |
| 104 | .filter(isVisible) |
| 105 | .map((node) => formatNode(node, format(node.children))) |
| 106 | .join(""); |
| 107 | return `<template shadowrootmode="open">${formatShadow(format(list))}</template>`; |
| 108 | } |
| 109 | |
| 110 | const formatShadow = (items: string) => |
| 111 | `<style>${css}</style><ol class=tree role=list>${items}</ol>`; |
| 112 | |
| 113 | /** the parts of a node's markup that change after it is drawn */ |
| 114 | function fields(node: ReadOnlyNode) { |
| 115 | const fraction = |
| 116 | node.showTotal && node.total > 0 ? math.clamp(node.value / node.total, 0, 1) : null; |
| 117 | const remaining = remainingSeconds(node); |
| 118 | return { |
| 119 | className: fraction === null ? "spin" : fraction === 1 ? "bar full" : "bar", |
| 120 | fraction, |
| 121 | count: formatCount(node), |
| 122 | eta: remaining |
| 123 | ? string.formatDurationLetters(remaining).replace(/(?<=\D)(?=\d)/g, " ") + " left" |
| 124 | : "", |
| 125 | }; |
| 126 | } |
| 127 | |
| 128 | /** `attachToDocument` relies on this layout: row, children, logs */ |
| 129 | function formatNode(node: ReadOnlyNode, children: string): string { |
| 130 | const { className, fraction, count, eta } = fields(node); |
| 131 | const id = `n${node.key}`; |
| 132 | const value = |
| 133 | fraction === null |
| 134 | ? "" |
| 135 | : ` aria-valuenow="${node.value}" aria-valuemax="${node.total}" style="--p:${fraction}"`; |
| 136 | return ( |
| 137 | `<li class="${className}"><div class=row>` + |
| 138 | `<div class=indicator role=progressbar aria-labelledby=${id}${value}><i></i></div>` + |
| 139 | `<span class=count><b>${esc(count[0])}</b><span>${esc(count[1])}</span></span>` + |
| 140 | `<span class=eta>${eta}</span>` + |
| 141 | `<span class=label id=${id}>${esc(node.text)}</span>` + |
| 142 | `</div><ol role=list>${children}</ol><div class=logs>${formatLogs(node.logs)}</div></li>` |
| 143 | ); |
| 144 | } |
| 145 | |
| 146 | /** |
| 147 | * bring `li` from {@linkcode formatNode} up to date, except its children and |
| 148 | * logs. `changedAt` holds when each bar last moved. |
| 149 | */ |
| 150 | function update(li: Element, node: ReadOnlyNode, changedAt: WeakMap<ReadOnlyNode, number>) { |
| 151 | const { className, fraction, count, eta } = fields(node); |
| 152 | const [indicator, counter, estimate, label] = UNWRAP(li.firstElementChild).children; |
| 153 | const [value, total] = UNWRAP(counter).children; |
| 154 | const progressbar = UNWRAP(indicator); |
| 155 | setAttribute(li, "class", className); |
| 156 | setText(UNWRAP(value), count[0]); |
| 157 | setText(UNWRAP(total), count[1]); |
| 158 | setText(UNWRAP(estimate), eta); |
| 159 | setText(UNWRAP(label), node.text); |
| 160 | if (fraction === null) { |
| 161 | for (const name of ["aria-valuenow", "aria-valuemax", "style"]) |
| 162 | setAttribute(progressbar, name, null); |
| 163 | return; |
| 164 | } |
| 165 | const shown = math.clamp( |
| 166 | Number(progressbar.getAttribute("aria-valuenow")) / |
| 167 | Number(progressbar.getAttribute("aria-valuemax")), |
| 168 | 0, |
| 169 | 1, |
| 170 | ); |
| 171 | setAttribute(progressbar, "aria-valuenow", String(node.value)); |
| 172 | setAttribute(progressbar, "aria-valuemax", String(node.total)); |
| 173 | if (fraction === shown) return; |
| 174 | // a forward glide lasts as long as the last update took to arrive, so steady |
| 175 | // progress moves steadily; going back snaps, as the original does |
| 176 | const now = performance.now(); |
| 177 | const glide = fraction > shown ? Math.min(now - (changedAt.get(node) ?? now), 1000) : 0; |
| 178 | changedAt.set(node, now); |
| 179 | setAttribute(progressbar, "style", `--p:${fraction};--glide:${Math.round(glide)}ms`); |
| 180 | } |
| 181 | |
| 182 | function setAttribute(element: Element, name: string, value: string | null) { |
| 183 | if (element.getAttribute(name) === value) return; |
| 184 | if (value === null) element.removeAttribute(name); |
| 185 | else element.setAttribute(name, value); |
| 186 | } |
| 187 | |
| 188 | function setText(element: Element, text: string) { |
| 189 | if (element.textContent !== text) element.textContent = text; |
| 190 | } |
| 191 | |
| 192 | /** `[value, total]`, split at the formatter's slash so the total can be styled apart */ |
| 193 | function formatCount({ value, total, showTotal, valueFormatter }: ReadOnlyNode): [string, string] { |
| 194 | const text = valueFormatter(value, total); |
| 195 | const slash = text.lastIndexOf("/"); |
| 196 | if (slash === -1) return [text, ""]; |
| 197 | return [text.slice(0, slash), showTotal ? text.slice(slash + 1) : ""]; |
| 198 | } |
| 199 | |
| 200 | /** the last lines of `logs`. partial writes join into lines of their own. */ |
| 201 | function formatLogs(logs: readonly log.Message[]): string { |
| 202 | let start = logs.length; |
| 203 | for (let breaks = 0; start > 0 && breaks <= maxLogLines;) { |
| 204 | const { text, newline } = UNWRAP(logs[--start]); |
| 205 | breaks += string.countNewlines(text) + (newline === false ? 0 : 1); |
| 206 | } |
| 207 | const lines: { kind: string; html: string }[] = []; |
| 208 | let partial: { kind: string; html: string } | null = null; |
| 209 | for (const { text, newline, level = "info", scope } of logs.slice(start)) { |
| 210 | ansi |
| 211 | .toHtml(text) |
| 212 | .split("\n") |
| 213 | .forEach((html, i) => { |
| 214 | if (newline !== false) { |
| 215 | // a message starts its own line, so a write's trailing newline leaves no blank one |
| 216 | if (i === 0 && partial?.html === "") lines.pop(); |
| 217 | const prefix = i === 0 && scope ? `<span class=scope>${esc(scope)}</span> ` : ""; |
| 218 | lines.push({ kind: i === 0 ? level : "more", html: prefix + html }); |
| 219 | } else if (i === 0 && partial) { |
| 220 | partial.html += html; |
| 221 | } else { |
| 222 | lines.push((partial = { kind: "raw", html })); |
| 223 | } |
| 224 | }); |
| 225 | if (newline !== false) partial = null; |
| 226 | } |
| 227 | while (lines.at(-1)?.html === "") lines.pop(); |
| 228 | return lines |
| 229 | .slice(-maxLogLines) |
| 230 | .map(({ kind, html }) => `<div class=${kind}>${html}</div>`) |
| 231 | .join(""); |
| 232 | } |
| 233 | |
| 234 | const esc = string.escapeHtmlContent; |
| 235 | |
| 236 | /** inline svg for a background image */ |
| 237 | const svg = (body: string) => |
| 238 | `url("data:image/svg+xml,${encodeURIComponent( |
| 239 | `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'>${body}</svg>`, |
| 240 | )}")`; |
| 241 | |
| 242 | // light-dark() resolves against `color-scheme`, which is inherited from the |
| 243 | // host; `all: initial` keeps every other inherited property out. |
| 244 | const css = /* @__PURE__ */ (() => ` |
| 245 | :host{display:block} |
| 246 | .tree{all:initial;display:block;color-scheme:inherit;container-type:inline-size; |
| 247 | font:12px/23px "Segoe UI",system-ui,sans-serif;color:var(--progress-foreground,light-dark(#1e1e1e,#e3e5e8)); |
| 248 | --line:var(--progress-border,light-dark(#a9b3bd,#5a626c));--muted:var(--progress-muted,light-dark(#56616c,#a4aeb9));${ansi.htmlColors}} |
| 249 | ol{margin:0;padding:0} |
| 250 | li{display:flow-root;position:relative} |
| 251 | .tree>li+li{margin-top:4px} |
| 252 | .row{display:flex;flex-wrap:wrap;align-items:center;column-gap:6px;min-height:23px;white-space:nowrap} |
| 253 | .label{overflow:hidden;text-overflow:ellipsis;min-width:0} |
| 254 | .tree>li>.row>.label{font-size:13px;color:var(--progress-accent,light-dark(#039,#8fb4ff))} |
| 255 | .count,.eta{flex:none;margin:4px 0;padding:0 6px;border-radius:8px;font-size:11px;line-height:15px; |
| 256 | font-variant-numeric:tabular-nums;color:var(--muted); |
| 257 | background:linear-gradient(light-dark(#fdfeff,#363b43),light-dark(#edf1f5,#2b3037)); |
| 258 | box-shadow:inset 0 0 0 1px light-dark(#d5dce4,#40474f)} |
| 259 | .count:has(>b:empty),.eta:empty{display:none} |
| 260 | .count>b{font-weight:600;color:var(--progress-foreground,light-dark(#2c3640,#d5dbe1))} |
| 261 | .count>span:not(:empty)::before{content:"/";margin:0 .2em;opacity:.55} |
| 262 | |
| 263 | /* bars lead the row, wider at the top level, as in the terminal. the track is |
| 264 | a 1px frame, 6px of gloss, a stretched body, and 5px of bottom light */ |
| 265 | .tree>li{--bar:176} |
| 266 | li li{--bar:96} |
| 267 | @container (width < 400px){.tree>li{--bar:112}li li{--bar:64}} |
| 268 | .bar>.row>.indicator{position:relative;flex:none;box-sizing:border-box;width:calc(var(--bar) * 1px); |
| 269 | height:15px;margin:4px 0;border:1px solid transparent;border-radius:2px; |
| 270 | background: |
| 271 | linear-gradient(light-dark(#f3f3f3,#34383e),light-dark(#fcfcfc,#3b3f45) 1.5px, |
| 272 | light-dark(#dbdbdb,#2f3237) 6px,light-dark(#cacaca,#26292d) 6px, |
| 273 | light-dark(#cacaca,#26292d) calc(100% - 5px),light-dark(#d5d5d5,#2b2e33) calc(100% - 1px), |
| 274 | light-dark(#eaeaea,#33373c)) padding-box, |
| 275 | linear-gradient(light-dark(#b2b2b2,#4b5058) 0 1px,light-dark(#bdbdbd,#464b53) 1px, |
| 276 | light-dark(#919191,#393d44) calc(100% - 1px),light-dark(#8c8c8c,#393d44) calc(100% - 1px)) |
| 277 | border-box} |
| 278 | .indicator>i{position:absolute;top:0;bottom:0;left:0;width:calc(var(--p) * 100%);overflow-x:clip; |
| 279 | --gloss:#d2f1fc 1.5px,#addcf3 3.5px,#9ad3ef;--body:var(--progress-accent,#0794d1);--rest:var(--progress-accent,#1ea0d8);--edge:var(--progress-accent,#035f8f); |
| 280 | --glow:160 225 255;--shade:color-mix(in srgb,var(--edge) 40%,#0000); |
| 281 | background: |
| 282 | linear-gradient(90deg,#0000 1px,var(--shade) 1px,var(--shade) 5px,#0000 27px), |
| 283 | linear-gradient(270deg,#0000 1px,var(--shade) 1px,#0000 14px), |
| 284 | linear-gradient(var(--gloss) 6px,var(--body) 6px,var(--body) calc(100% - 5px),var(--rest)); |
| 285 | transition:width var(--glide,0s) linear} |
| 286 | /* a glow sweeps the fill at one speed for every bar, over the frame too, while |
| 287 | the bottom light swells under it */ |
| 288 | .indicator>i::before{content:"";position:absolute;left:0;right:0;bottom:0;height:5px; |
| 289 | background:linear-gradient(#0000,rgb(var(--glow)/.6));animation:swell 2.4s ease-in-out infinite} |
| 290 | .indicator>i::after{content:"";position:absolute;top:-1px;bottom:-1px;left:0;width:64px; |
| 291 | background:linear-gradient(90deg,#0000,rgb(var(--glow)/.03) 8.3%,rgb(var(--glow)/.08) 16.7%, |
| 292 | rgb(var(--glow)/.2) 25%,rgb(var(--glow)/.36) 33.3%,rgb(var(--glow)/.53) 41.7%, |
| 293 | rgb(var(--glow)/.6),rgb(var(--glow)/.53) 58.3%,rgb(var(--glow)/.36) 66.7%, |
| 294 | rgb(var(--glow)/.2) 75%,rgb(var(--glow)/.08) 83.3%,rgb(var(--glow)/.03) 91.7%,#0000); |
| 295 | animation:glow 2.4s linear infinite} |
| 296 | @keyframes glow{from{translate:-100%}50%,to{translate:176px}} |
| 297 | @keyframes swell{from{opacity:.4}25%{opacity:1}65%,88%{opacity:0}to{opacity:.4}} |
| 298 | :is(.full,li:has(>.logs>:is(.warn,.error)))>.row>.indicator>i::before, |
| 299 | :is(.full,li:has(>.logs>:is(.warn,.error)))>.row>.indicator>i::after{content:none} |
| 300 | li:has(>.logs>.warn)>.row>.indicator>i{ |
| 301 | --gloss:#fffdcd 1.5px,#fffba1 3.5px,#fffa8b;--body:#d2ca00;--rest:#fef400;--edge:#8f8600} |
| 302 | li:has(>.logs>.error)>.row>.indicator>i{ |
| 303 | --gloss:#ffcdcd 1.5px,#ffa1a1 3.5px,#ff8b8b;--body:#d20000;--rest:#fe0000;--edge:#8f0000} |
| 304 | |
| 305 | /* the busy ring leads a top-level row, lit from above, with a highlight |
| 306 | running clockwise; below the top level the terminal shows no spinner */ |
| 307 | .spin>.row>.indicator{position:relative;flex:none;width:15px;height:15px;margin:4px 0;border-radius:50%; |
| 308 | background:conic-gradient(#9bc6da,#8dc2d0 30deg,#6daebd 60deg,#2b8982 90deg,#319f9d 120deg, |
| 309 | #4dc7bb 150deg,#5ecfc5 180deg,#4fc6bb 210deg,#2da09e 240deg,#238382 270deg,#6db2bb 300deg, |
| 310 | #91c5d1 330deg,#9bc6da); |
| 311 | mask:radial-gradient(closest-side,#0000 61%,#000 67%)} |
| 312 | li li.spin>.row>.indicator{display:none} |
| 313 | .spin>.row>.indicator::before,.spin>.row>.indicator::after{content:"";position:absolute;inset:0;border-radius:50%} |
| 314 | .spin>.row>.indicator::before{animation:spin .9s linear infinite; |
| 315 | background:conic-gradient(from -150deg,#0000,#2fd4dc99 40deg,#4fe6eae6 90deg,#8ff3f4 125deg, |
| 316 | #e8fcfd 150deg,#b4eef0b3 162deg,#0000 180deg)} |
| 317 | .spin>.row>.indicator::after{background:radial-gradient(closest-side,#00323c73 64%,#00323c00 76%, |
| 318 | #fff3 86%,#00323c00 92%,#00323c80)} |
| 319 | .spin>.row>.indicator>i{display:none} |
| 320 | @keyframes spin{to{rotate:1turn}} |
| 321 | |
| 322 | /* tree lines: a node's children hang from the start of its row, and each child |
| 323 | is drawn with the terminal's ├─, │ and └─ */ |
| 324 | li li{padding-left:22px} |
| 325 | li>ol{position:relative} |
| 326 | li>ol:not(:empty)::before{content:"";position:absolute;left:7px;top:-4px;height:5px; |
| 327 | border-left:1px solid var(--line)} |
| 328 | li li::before,li li::after{content:"";position:absolute;left:7px;border:0 solid var(--line)} |
| 329 | li li::before{top:0;bottom:-1px;border-left-width:1px} |
| 330 | li li::after{top:11px;width:11px;border-top-width:1px} |
| 331 | li li:last-child::before{bottom:auto;height:12px} |
| 332 | |
| 333 | .logs{margin:1px 0 4px;padding:3px 8px;border-radius:4px; |
| 334 | background:var(--progress-background,light-dark(#f3f5f8,#24282e));box-shadow:inset 0 0 0 1px light-dark(#e1e6ec,#31363d); |
| 335 | font:11px/16px ui-monospace,"Cascadia Mono",Consolas,"SF Mono",Menlo,monospace;color:var(--muted)} |
| 336 | .logs:empty{display:none} |
| 337 | .logs>div{overflow:hidden;text-overflow:ellipsis;white-space:pre} |
| 338 | .logs>.more{padding-left:16px} |
| 339 | .logs>.debug{opacity:.7} |
| 340 | .logs>:is(.info,.warn,.error)::before{content:"";display:inline-block;width:11px;height:11px; |
| 341 | margin-right:5px;vertical-align:-2px;background:center/contain no-repeat} |
| 342 | .logs>.info::before{background-image:${svg( |
| 343 | "<defs><radialGradient id='g' cx='.35' cy='.3' r='.8'><stop stop-color='#7cbaf6'/><stop offset='1' stop-color='#1557b8'/></radialGradient></defs><circle cx='6' cy='6' r='5.5' fill='url(#g)' stroke='#0c3f8c' stroke-width='.6'/><rect x='5.25' y='5' width='1.5' height='4.2' rx='.5' fill='#fff'/><circle cx='6' cy='3.3' r='.9' fill='#fff'/>", |
| 344 | )}} |
| 345 | .logs>.warn::before{background-image:${svg( |
| 346 | "<defs><linearGradient id='g' x2='0' y2='1'><stop stop-color='#ffe680'/><stop offset='1' stop-color='#f0b400'/></linearGradient></defs><path d='M6 .8l5.4 10H.6z' stroke-linejoin='round' fill='url(#g)' stroke='#b58300' stroke-width='.7'/><rect x='5.3' y='4' width='1.4' height='3.8' rx='.5' fill='#333'/><circle cx='6' cy='9.2' r='.8' fill='#333'/>", |
| 347 | )}} |
| 348 | .logs>.error::before{background-image:${svg( |
| 349 | "<defs><radialGradient id='g' cx='.35' cy='.3' r='.8'><stop stop-color='#ff8a80'/><stop offset='1' stop-color='#c41a1a'/></radialGradient></defs><circle cx='6' cy='6' r='5.5' fill='url(#g)' stroke='#8a0f0f' stroke-width='.6'/><path d='M4 4l4 4m0-4l-4 4' stroke='#fff' stroke-width='1.5' stroke-linecap='round'/>", |
| 350 | )}} |
| 351 | .scope{color:var(--progress-secondary,light-dark(#7a6a9e,#b5a6d8))} |
| 352 | |
| 353 | |
| 354 | @media (prefers-reduced-motion:reduce){ |
| 355 | .indicator>i{transition:none} |
| 356 | .indicator>i::before,.indicator>i::after{content:none} |
| 357 | .spin>.row>.indicator::before{animation:pulse 1.8s ease-in-out infinite alternate} |
| 358 | } |
| 359 | @keyframes pulse{from{opacity:.25}} |
| 360 | @media (forced-colors:active){ |
| 361 | .indicator{forced-color-adjust:none} |
| 362 | .bar>.row>.indicator{background:Canvas;border-color:CanvasText} |
| 363 | .indicator>i{background:Highlight} |
| 364 | .indicator>i::before,.indicator>i::after{content:none} |
| 365 | .spin>.row>.indicator{background:Highlight} |
| 366 | } |
| 367 | :host([data-progress-motion="static"]) .indicator, |
| 368 | :host([data-progress-motion="static"]) .indicator>i, |
| 369 | :host([data-progress-motion="static"]) .indicator::before, |
| 370 | :host([data-progress-motion="static"]) .indicator::after, |
| 371 | :host([data-progress-motion="static"]) .indicator>i::before, |
| 372 | :host([data-progress-motion="static"]) .indicator>i::after{animation:none;transition:none} |
| 373 | `)(); |