oxedyne/daimond/www/css/graph.css
14.0 KiB, 1 run
created by r2519314175:1101, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | /* graph.css — the Graph pane. Rules for #panel-graph and its SVG picture only. |
| 2 | * |
| 3 | * Every colour comes from the palette variables, never from a literal, so the |
| 4 | * picture holds up in the dark, light and candy themes without a rule per |
| 5 | * theme. There are two exceptions and both are colours somebody chose rather |
| 6 | * than colours a theme owns: the hue of a relation chip, which is hashed from |
| 7 | * the relation's own name so one word is one colour everywhere, and a Diamond's |
| 8 | * own fill, handed in as `--node-bg` from the store the rest of the app keeps |
| 9 | * it in. Both arrive as custom properties with the theme's colour as the |
| 10 | * fallback, so a Diamond nobody has coloured is drawn exactly as before. |
| 11 | * |
| 12 | * A box is a flattened hexagon rather than a rectangle: horizontal top and |
| 13 | * bottom, and the two sides kinked out to a point at the middle. Those points |
| 14 | * are where the lines meet it, and graph.js computes them from the same two |
| 15 | * numbers this file rounds the joins with. |
| 16 | * |
| 17 | * The pane is now edited as well as read, so there are three more things to |
| 18 | * style than there were: the strip of controls above the picture, the menu the |
| 19 | * right button opens, and the small form a link is edited in. All three float |
| 20 | * INSIDE the panel card, which clips, so graph.js clamps them to it rather than |
| 21 | * letting one hang off an edge and disappear. |
| 22 | */ |
| 23 | |
| 24 | /* The panel is a stage card like the AI, the Web page and a message, but it is |
| 25 | not in app.css's list of them -- so it is given the same card here. */ |
| 26 | .graph { |
| 27 | min-width: 0; |
| 28 | background: var(--bg-secondary); |
| 29 | border: 1px solid var(--border); |
| 30 | border-radius: var(--radius-lg); |
| 31 | display: flex; |
| 32 | flex-direction: column; |
| 33 | overflow: hidden; |
| 34 | /* The menu and the link form are positioned against this card. */ |
| 35 | position: relative; |
| 36 | } |
| 37 | |
| 38 | /* The picture is panned by scrolling, and scaled only when asked: an instrument |
| 39 | that quietly rescaled itself would make two stores of different sizes look |
| 40 | alike, so "All" is a button and never a reflex. */ |
| 41 | .graph-body { |
| 42 | flex: 1 1 auto; |
| 43 | min-height: 0; |
| 44 | overflow: auto; |
| 45 | padding: 4px 4px 0; |
| 46 | } |
| 47 | |
| 48 | .graph-svg { |
| 49 | display: block; |
| 50 | /* SVG text inherits nothing from the page by default. */ |
| 51 | font-family: var(--font); |
| 52 | /* A press and a sweep is a drag here, not a text selection. The mousedown |
| 53 | handler refuses the selection as well; this is the belt, so a gesture that |
| 54 | starts somewhere the handler does not run still cannot leave the picture |
| 55 | striped in highlight. The stats line below is left selectable. */ |
| 56 | user-select: none; |
| 57 | -webkit-user-select: none; |
| 58 | } |
| 59 | |
| 60 | /* ── Nodes ──────────────────────────────────────────────────── */ |
| 61 | |
| 62 | .graph-node { cursor: pointer; } |
| 63 | |
| 64 | /* The box. `--node-bg` is the colour this Diamond was given elsewhere in the |
| 65 | app; every rule below keeps the theme's own colour as its fallback, so the |
| 66 | two kinds of Diamond differ in one property and not in a ruleset each. |
| 67 | `stroke-linejoin: round` is what keeps the six corners from drawing spikes |
| 68 | where the sides kink. */ |
| 69 | .graph-node-box { |
| 70 | fill: var(--node-bg, var(--bg-tertiary)); |
| 71 | stroke: var(--border-2); |
| 72 | stroke-width: 1; |
| 73 | stroke-linejoin: round; |
| 74 | } |
| 75 | /* Hover moves the outline rather than the fill when a Diamond has a colour of |
| 76 | its own -- a chosen colour that changed under the pointer would read as a |
| 77 | different Diamond. */ |
| 78 | .graph-node:hover .graph-node-box { |
| 79 | fill: var(--node-bg, var(--bg-hover)); |
| 80 | stroke: var(--accent); |
| 81 | } |
| 82 | |
| 83 | /* An unlinked Diamond is stated, not hidden: the same box, drawn quieter. */ |
| 84 | .graph-node.isolate .graph-node-box { |
| 85 | fill: var(--node-bg, var(--bg-primary)); |
| 86 | stroke: var(--border); |
| 87 | stroke-dasharray: 3 3; |
| 88 | } |
| 89 | |
| 90 | /* A Diamond on a cycle is worth noticing without being alarming, so the box is |
| 91 | outlined in the warning colour rather than filled with it. */ |
| 92 | .graph-node.cycled .graph-node-box { |
| 93 | stroke: var(--warn); |
| 94 | stroke-width: 1.5; |
| 95 | } |
| 96 | |
| 97 | /* Being dragged. No transition on the transform: the box has to be under the |
| 98 | pointer, and a box easing towards where the pointer was is a box that is not |
| 99 | where the user is holding it. */ |
| 100 | .graph-node.dragging { cursor: grabbing; } |
| 101 | .graph-node.dragging .graph-node-box { |
| 102 | stroke: var(--accent); |
| 103 | stroke-width: 1.5; |
| 104 | } |
| 105 | |
| 106 | /* The Diamond a link is being drawn FROM, while it is being drawn. */ |
| 107 | .graph-node.link-source .graph-node-box { |
| 108 | stroke: var(--accent); |
| 109 | stroke-width: 2; |
| 110 | } |
| 111 | |
| 112 | .graph-node-name { |
| 113 | fill: var(--node-fg, var(--text-primary)); |
| 114 | font-size: var(--fs-sm); |
| 115 | dominant-baseline: alphabetic; |
| 116 | } |
| 117 | .graph-node.isolate .graph-node-name { fill: var(--node-fg, var(--text-secondary)); } |
| 118 | |
| 119 | /* The artefact count: files, pages and chats this Diamond is linked to, which |
| 120 | are counted rather than drawn so they cannot crowd out the structure. */ |
| 121 | .graph-badge-text { |
| 122 | fill: var(--text-muted); |
| 123 | font-size: var(--fs-3xs); |
| 124 | text-anchor: end; |
| 125 | } |
| 126 | .graph-cycle-text { |
| 127 | fill: var(--warn); |
| 128 | font-size: var(--fs-2xs); |
| 129 | text-anchor: end; |
| 130 | } |
| 131 | |
| 132 | /* ── Edges ──────────────────────────────────────────────────── */ |
| 133 | |
| 134 | .graph-edge-line { |
| 135 | fill: none; |
| 136 | stroke: var(--text-muted); |
| 137 | stroke-width: 1.4; |
| 138 | } |
| 139 | .graph-edge:hover .graph-edge-line { stroke: var(--accent); } |
| 140 | |
| 141 | /* The wide invisible twin that takes the clicks. 1.4 pixels of stroke is not |
| 142 | something a pointer can be asked to hit, and widening the drawn line to make |
| 143 | it hittable would be letting the interaction decide how the picture looks. */ |
| 144 | .graph-edge-hit { |
| 145 | fill: none; |
| 146 | stroke: transparent; |
| 147 | stroke-width: 16; |
| 148 | cursor: pointer; |
| 149 | } |
| 150 | |
| 151 | /* A closing edge -- the one that makes a cycle a cycle. Dashed and in the |
| 152 | warning colour, because the instrument's job is to make it visible, not to |
| 153 | pretend the graph is a tree. */ |
| 154 | .graph-edge.back .graph-edge-line { |
| 155 | stroke: var(--warn); |
| 156 | stroke-dasharray: 6 4; |
| 157 | } |
| 158 | |
| 159 | .graph-arrow { fill: var(--text-muted); } |
| 160 | .graph-arrow.back { fill: var(--warn); } |
| 161 | .graph-arrow.live { fill: var(--accent); } |
| 162 | |
| 163 | /* ── The relation chips ─────────────────────────────────────── */ |
| 164 | /* A link's relations ride its own line as pills, one per relation, stacked when |
| 165 | there are several. They are the Diamonds' tag chips in another medium: the |
| 166 | same hue per word, the same shape. The halo the bare word used to be painted |
| 167 | with is gone with it -- a pill is opaque, so the line runs up to its edge and |
| 168 | stops there, which is the join the words never had. |
| 169 | |
| 170 | The type is pinned at 11px rather than taken from `--fs-2xs`. The pill's |
| 171 | width is arithmetic in graph.js, done without asking the page how wide the |
| 172 | word came out, and type that floated against that arithmetic would sit in a |
| 173 | pill of the wrong size. */ |
| 174 | .graph-chip-box { |
| 175 | fill: hsl(var(--tag-h, 0) 30% 20%); |
| 176 | stroke: hsl(var(--tag-h, 0) 28% 34%); |
| 177 | stroke-width: 1; |
| 178 | } |
| 179 | .graph-edge-label { |
| 180 | fill: hsl(var(--tag-h, 0) 55% 78%); |
| 181 | font-size: 11px; |
| 182 | text-anchor: middle; |
| 183 | dominant-baseline: central; |
| 184 | pointer-events: none; |
| 185 | } |
| 186 | /* The light themes take the reverse, exactly as `.tag-chip` does in app.css: |
| 187 | pale pill, dark lettering, and neither side has to know which relations |
| 188 | exist. */ |
| 189 | :root[data-ink="dark"] .graph-chip-box { |
| 190 | fill: hsl(var(--tag-h, 0) 70% 94%); |
| 191 | stroke: hsl(var(--tag-h, 0) 45% 80%); |
| 192 | } |
| 193 | :root[data-ink="dark"] .graph-edge-label { fill: hsl(var(--tag-h, 0) 55% 30%); } |
| 194 | |
| 195 | /* ── The live arrow ─────────────────────────────────────────── */ |
| 196 | /* The only moving thing in the pane. It follows the pointer while a link is |
| 197 | being drawn and is thrown away when the gesture ends; nothing about it is |
| 198 | ever stored, which is why the picture is still a function of the store. */ |
| 199 | .graph-live-line { |
| 200 | fill: none; |
| 201 | stroke: var(--accent); |
| 202 | stroke-width: 1.6; |
| 203 | stroke-dasharray: 5 4; |
| 204 | pointer-events: none; |
| 205 | } |
| 206 | .graph-live { pointer-events: none; } |
| 207 | |
| 208 | /* While a link is being aimed, the pointer says what a click will do. */ |
| 209 | .graph-body.linking { cursor: crosshair; } |
| 210 | .graph-body.panning { cursor: grabbing; } |
| 211 | |
| 212 | /* ── Furniture ──────────────────────────────────────────────── */ |
| 213 | |
| 214 | .graph-band { |
| 215 | fill: var(--text-muted); |
| 216 | font-size: var(--fs-2xs); |
| 217 | letter-spacing: 0.06em; |
| 218 | text-transform: uppercase; |
| 219 | } |
| 220 | |
| 221 | .graph-empty { |
| 222 | margin: 0; |
| 223 | padding: 14px 16px; |
| 224 | color: var(--text-secondary); |
| 225 | font-size: var(--fs-sm); |
| 226 | line-height: 1.5; |
| 227 | } |
| 228 | |
| 229 | .graph-stats { |
| 230 | flex: none; |
| 231 | padding: 6px 14px 8px; |
| 232 | border-top: 1px solid var(--border); |
| 233 | color: var(--text-muted); |
| 234 | font-size: var(--fs-2xs); |
| 235 | font-family: var(--font-mono); |
| 236 | } |
| 237 | /* The stats line is the panel's foot, not part of what scrolls. */ |
| 238 | .graph-body > .graph-stats { position: sticky; bottom: 0; background: var(--bg-secondary); } |
| 239 | |
| 240 | /* ── The control strip ──────────────────────────────────────── */ |
| 241 | /* Above the picture and outside the scroller, so it neither scrolls away nor |
| 242 | competes with the picture for the panel's own head. */ |
| 243 | .graph-bar { |
| 244 | flex: none; |
| 245 | display: flex; |
| 246 | align-items: center; |
| 247 | gap: 6px; |
| 248 | padding: 5px 10px 6px; |
| 249 | border-bottom: 1px solid var(--border); |
| 250 | min-width: 0; |
| 251 | } |
| 252 | |
| 253 | .graph-btn { |
| 254 | flex: none; |
| 255 | font: inherit; |
| 256 | font-size: var(--fs-2xs); |
| 257 | line-height: 1.4; |
| 258 | padding: 3px 10px; |
| 259 | border-radius: var(--radius-sm); |
| 260 | border: 1px solid var(--border-2); |
| 261 | background: var(--bg-tertiary); |
| 262 | color: var(--text-secondary); |
| 263 | cursor: pointer; |
| 264 | } |
| 265 | .graph-btn:hover { |
| 266 | background: var(--bg-hover); |
| 267 | color: var(--text-primary); |
| 268 | border-color: var(--border-strong); |
| 269 | } |
| 270 | /* Armed. The pressed state has to read as pressed without the label changing, |
| 271 | or the button says one thing and means another. */ |
| 272 | .graph-btn.on { |
| 273 | background: var(--accent-soft); |
| 274 | border-color: var(--accent); |
| 275 | color: var(--accent-text); |
| 276 | } |
| 277 | .graph-btn.primary { |
| 278 | background: var(--accent); |
| 279 | border-color: var(--accent); |
| 280 | color: var(--on-fill); |
| 281 | } |
| 282 | .graph-btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); } |
| 283 | .graph-btn.danger { color: var(--danger); } |
| 284 | .graph-btn.danger:hover { color: var(--danger); border-color: var(--danger); } |
| 285 | |
| 286 | /* What the mode is waiting for, said where the control that armed it is. */ |
| 287 | .graph-say { |
| 288 | min-width: 0; |
| 289 | overflow: hidden; |
| 290 | text-overflow: ellipsis; |
| 291 | white-space: nowrap; |
| 292 | font-size: var(--fs-2xs); |
| 293 | color: var(--text-muted); |
| 294 | } |
| 295 | .graph-say.warn { color: var(--warn); } |
| 296 | |
| 297 | /* ── The right-click menu ───────────────────────────────────── */ |
| 298 | |
| 299 | .graph-menu { |
| 300 | position: absolute; |
| 301 | z-index: 30; |
| 302 | min-width: 170px; |
| 303 | padding: 4px; |
| 304 | border: 1px solid var(--border-2); |
| 305 | border-radius: var(--radius-sm); |
| 306 | background: var(--bg-tertiary); |
| 307 | box-shadow: 0 6px 22px rgba(0, 0, 0, 0.28); |
| 308 | } |
| 309 | |
| 310 | .graph-menu-item { |
| 311 | display: block; |
| 312 | width: 100%; |
| 313 | text-align: left; |
| 314 | font: inherit; |
| 315 | font-size: var(--fs-xs); |
| 316 | padding: 5px 9px; |
| 317 | border: 0; |
| 318 | border-radius: var(--radius-sm); |
| 319 | background: transparent; |
| 320 | color: var(--text-primary); |
| 321 | cursor: pointer; |
| 322 | } |
| 323 | .graph-menu-item:hover { background: var(--bg-hover); } |
| 324 | .graph-menu-item.danger { color: var(--danger); } |
| 325 | .graph-menu-sep { |
| 326 | height: 1px; |
| 327 | margin: 4px 2px; |
| 328 | background: var(--border); |
| 329 | } |
| 330 | |
| 331 | /* ── The link form ──────────────────────────────────────────── */ |
| 332 | |
| 333 | .graph-edit { |
| 334 | position: absolute; |
| 335 | z-index: 31; |
| 336 | width: 260px; |
| 337 | padding: 10px 12px 11px; |
| 338 | border: 1px solid var(--border-2); |
| 339 | border-radius: var(--radius); |
| 340 | background: var(--bg-tertiary); |
| 341 | box-shadow: 0 8px 26px rgba(0, 0, 0, 0.32); |
| 342 | } |
| 343 | |
| 344 | .graph-edit-head { |
| 345 | font-size: var(--fs-xs); |
| 346 | color: var(--text-primary); |
| 347 | margin-bottom: 8px; |
| 348 | overflow: hidden; |
| 349 | text-overflow: ellipsis; |
| 350 | white-space: nowrap; |
| 351 | } |
| 352 | |
| 353 | .graph-edit-lab { |
| 354 | display: block; |
| 355 | font-size: var(--fs-3xs); |
| 356 | letter-spacing: 0.06em; |
| 357 | text-transform: uppercase; |
| 358 | color: var(--text-muted); |
| 359 | margin: 6px 0 3px; |
| 360 | } |
| 361 | |
| 362 | .graph-edit-input, |
| 363 | .graph-edit-note { |
| 364 | display: block; |
| 365 | width: 100%; |
| 366 | box-sizing: border-box; |
| 367 | font: inherit; |
| 368 | font-size: var(--fs-xs); |
| 369 | padding: 4px 7px; |
| 370 | border: 1px solid var(--border-2); |
| 371 | border-radius: var(--radius-sm); |
| 372 | background: var(--bg-primary); |
| 373 | color: var(--text-primary); |
| 374 | } |
| 375 | .graph-edit-note { resize: vertical; min-height: 40px; } |
| 376 | .graph-edit-input:focus, |
| 377 | .graph-edit-note:focus { outline: 2px solid var(--accent); outline-offset: 1px; } |
| 378 | |
| 379 | /* ── Relations, in the form ─────────────────────────────────── */ |
| 380 | /* The chips on the link, and under the box the ones the store already holds. |
| 381 | Both rows scroll rather than grow: the form floats inside the panel card, |
| 382 | which clips, and a link given six relations must not push its own buttons off |
| 383 | the bottom of it. */ |
| 384 | .graph-rel-row { |
| 385 | display: flex; |
| 386 | flex-wrap: wrap; |
| 387 | gap: 4px; |
| 388 | margin-top: 3px; |
| 389 | max-height: 62px; |
| 390 | overflow-y: auto; |
| 391 | } |
| 392 | .graph-rel-add { |
| 393 | display: flex; |
| 394 | align-items: center; |
| 395 | gap: 6px; |
| 396 | margin-top: 6px; |
| 397 | } |
| 398 | .graph-rel-add .graph-edit-input { flex: 1 1 auto; min-width: 0; } |
| 399 | .graph-rel-add .graph-btn { flex: none; padding: 3px 9px; } |
| 400 | |
| 401 | .graph-rel-none { |
| 402 | font-size: var(--fs-3xs); |
| 403 | color: var(--text-muted); |
| 404 | } |
| 405 | .graph-rel-pool-lab { |
| 406 | margin-top: 8px; |
| 407 | font-size: var(--fs-3xs); |
| 408 | letter-spacing: 0.06em; |
| 409 | text-transform: uppercase; |
| 410 | color: var(--text-muted); |
| 411 | } |
| 412 | |
| 413 | /* Said when a relation will not fit, beside the box it was refused from. In the |
| 414 | warning colour and not the muted one: it is the reason a save did not happen, |
| 415 | which is not a footnote. */ |
| 416 | .graph-rel-hint { |
| 417 | margin-top: 7px; |
| 418 | font-size: var(--fs-3xs); |
| 419 | line-height: 1.45; |
| 420 | color: var(--warn); |
| 421 | } |
| 422 | |
| 423 | .graph-edit-row { |
| 424 | display: flex; |
| 425 | gap: 6px; |
| 426 | margin-top: 10px; |
| 427 | } |
| 428 | |
| 429 | /* The link editor's title row: the pair being linked, and the way out. The head |
| 430 | ellipsises rather than pushing the cross off the end of a 260px box, which is |
| 431 | what `min-width: 0` on a flex child is for. The row carries the space the head |
| 432 | used to carry itself. */ |
| 433 | .graph-edit-top { margin-bottom: 8px; } |
| 434 | .graph-edit-top .graph-edit-head { margin-bottom: 0; min-width: 0; } |