@0001-labs/design-system-1 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/DS1/0-face/constraint-panel.ts +27 -6
  2. package/DS1/0-face/constraint-site.ts +3 -2
  3. package/DS1/0-face/constraints.ts +27 -0
  4. package/DS1/0-face/define.ts +1 -1
  5. package/DS1/0-face/fault-lines.ts +159 -0
  6. package/DS1/1-root/cjk.css +1 -1
  7. package/DS1/1-root/fonts.css +1 -1
  8. package/DS1/1-root/one.css +3 -2
  9. package/DS1/2-core/ds-avatar.ts +47 -8
  10. package/DS1/2-core/styles/ds-button-host.css +2 -1
  11. package/DS1/2-core/styles/ds-button.css +2 -1
  12. package/DS1/2-core/styles/ds-tag.css +2 -1
  13. package/DS1/3-unit/context-trigger.ts +29 -2
  14. package/DS1/3-unit/ds-form.ts +2 -1
  15. package/DS1/3-unit/ds-table.ts +2 -1
  16. package/DS1/4-page/ds-container.ts +2 -1
  17. package/dist/0-face/constraint-panel.d.ts.map +1 -1
  18. package/dist/0-face/constraint-panel.js +28 -6
  19. package/dist/0-face/constraint-site.d.ts +1 -0
  20. package/dist/0-face/constraint-site.d.ts.map +1 -1
  21. package/dist/0-face/constraint-site.js +3 -2
  22. package/dist/0-face/constraints.d.ts.map +1 -1
  23. package/dist/0-face/constraints.js +27 -0
  24. package/dist/0-face/define.js +1 -1
  25. package/dist/0-face/fault-lines.d.ts +20 -0
  26. package/dist/0-face/fault-lines.d.ts.map +1 -0
  27. package/dist/0-face/fault-lines.js +172 -0
  28. package/dist/2-core/ds-avatar.d.ts +5 -0
  29. package/dist/2-core/ds-avatar.d.ts.map +1 -1
  30. package/dist/2-core/ds-avatar.js +53 -9
  31. package/dist/3-unit/context-trigger.d.ts.map +1 -1
  32. package/dist/3-unit/context-trigger.js +32 -2
  33. package/dist/3-unit/ds-form.d.ts.map +1 -1
  34. package/dist/3-unit/ds-form.js +2 -1
  35. package/dist/3-unit/ds-table.d.ts.map +1 -1
  36. package/dist/3-unit/ds-table.js +2 -1
  37. package/dist/4-page/ds-container.d.ts.map +1 -1
  38. package/dist/4-page/ds-container.js +2 -1
  39. package/dist/design-system-1.bundle.js +372 -32
  40. package/dist/design-system-1.bundle.js.map +1 -1
  41. package/dist/design-system-1.bundle.min.js +3380 -3128
  42. package/dist/design-system-1.bundle.min.js.map +1 -1
  43. package/package.json +6 -4
@@ -1,5 +1,6 @@
1
+ import { paintFaultLines } from './fault-lines';
1
2
  import { measureConstraints } from './constraints';
2
- import { PHONE_WIDTHS, checkSite, findSitePages, inspectInFrame, ruleLine, summarizePages, summarizeRules } from './constraint-site';
3
+ import { VIEWPORT_WIDTHS, checkSite, findSitePages, inspectInFrame, ruleLine, summarizePages, summarizeRules } from './constraint-site';
3
4
  // appendChild, not append: a Worker project's types redefine append for its HTML rewriter.
4
5
  /** The presses of C, each within 800ms of the last, that toggle the panel. */
5
6
  export const CONSTRAINT_PRESSES = 5;
@@ -35,7 +36,8 @@ export function createConstraintPanel() {
35
36
  --danger: var(--ds-danger-color, light-dark(#de0331, #ff5f5f));
36
37
  }
37
38
  :host([hidden]) { display: none !important; }
38
- .rings { position: absolute; inset: 0; overflow: hidden; }
39
+ .fault-lines { z-index: 0 !important; }
40
+ .rings { z-index: 1; position: absolute; inset: 0; overflow: hidden; }
39
41
  .frames { position: absolute; width: 0; height: 0; overflow: hidden; }
40
42
  .ring {
41
43
  position: absolute;
@@ -57,6 +59,7 @@ export function createConstraintPanel() {
57
59
  text-align: center;
58
60
  }
59
61
  aside {
62
+ z-index: 2;
60
63
  position: absolute;
61
64
  inset-block: 0;
62
65
  inset-inline-end: 0;
@@ -103,14 +106,18 @@ export function createConstraintPanel() {
103
106
  .code b { color: var(--danger); font-weight: 400; }
104
107
  .rule { color: var(--muted); }
105
108
  p { margin: 0; }
109
+ :host([data-lines-only]) aside { inset-block: 0 auto; width: auto; padding: calc(var(--u) / 2); }
110
+ :host([data-lines-only]) aside > :not(header), :host([data-lines-only]) header > :not([data-lines], [data-close]) { display: none; }
111
+ :host([data-lines-only]) header { margin: 0; }
106
112
  @media print { :host { display: none !important; } }
107
- </style><div class="rings"></div><div class="frames"></div><aside aria-label="Constraints">
108
- <header><strong>Constraints</strong><button type="button" data-run>Page</button><button type="button" data-site>Site</button><button type="button" data-close aria-label="Close">Close</button></header>
109
- <p data-status aria-live="polite"></p>
113
+ </style><svg class="fault-lines" aria-hidden="true"></svg><div class="rings"></div><div class="frames"></div><aside aria-label="Constraints">
114
+ <header><strong>Constraints</strong><button type="button" data-run>Page</button><button type="button" data-site>Site</button><button type="button" data-lines>Lines</button><button type="button" data-close aria-label="Close">Close</button></header>
115
+ <p data-status aria-live="polite"></p><p class="rule">Fault lines: dashed reference · red drift</p>
110
116
  <ol data-findings></ol>
111
117
  <section data-rules hidden><h3>Rules</h3><ol></ol></section>
112
118
  <section data-pages hidden><h3>Pages</h3><ol></ol></section>
113
119
  </aside>`;
120
+ const faultLines = shadow.querySelector('.fault-lines');
114
121
  const rings = shadow.querySelector('.rings');
115
122
  const list = shadow.querySelector('[data-findings]');
116
123
  const rulesSection = shadow.querySelector('[data-rules]');
@@ -120,6 +127,9 @@ export function createConstraintPanel() {
120
127
  const status = shadow.querySelector('[data-status]');
121
128
  const title = shadow.querySelector('strong');
122
129
  const frames = shadow.querySelector('.frames');
130
+ const linesButton = shadow.querySelector('[data-lines]');
131
+ const setLinesOnly = (enabled) => { panel.toggleAttribute('data-lines-only', enabled); linesButton.textContent = enabled ? 'Results' : 'Lines'; };
132
+ linesButton.addEventListener('click', () => setLinesOnly(!panel.hasAttribute('data-lines-only')));
123
133
  const siteButton = shadow.querySelector('[data-site]');
124
134
  let site = null;
125
135
  let findings = [];
@@ -127,6 +137,7 @@ export function createConstraintPanel() {
127
137
  let frame = 0;
128
138
  const place = () => {
129
139
  frame = 0;
140
+ paintFaultLines(document, faultLines);
130
141
  rings.replaceChildren(...findings.map(({ element }, index) => {
131
142
  const rect = element.getBoundingClientRect();
132
143
  const ring = document.createElement('div');
@@ -141,6 +152,8 @@ export function createConstraintPanel() {
141
152
  };
142
153
  const schedule = () => { if (!frame)
143
154
  frame = requestAnimationFrame(place); };
155
+ const layoutChanges = new MutationObserver(schedule);
156
+ const sizeChanges = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
144
157
  const stopSite = () => {
145
158
  site?.abort();
146
159
  site = null;
@@ -274,7 +287,7 @@ export function createConstraintPanel() {
274
287
  title.textContent = 'Constraints';
275
288
  status.textContent = 'Finding the pages…';
276
289
  const pages = await findSitePages();
277
- const widths = `${PHONE_WIDTHS.slice(0, -1).join(', ')} and ${PHONE_WIDTHS[PHONE_WIDTHS.length - 1]}px`;
290
+ const widths = `${VIEWPORT_WIDTHS.slice(0, -1).join(', ')} and ${VIEWPORT_WIDTHS[VIEWPORT_WIDTHS.length - 1]}px`;
278
291
  let broken = 0;
279
292
  const results = await checkSite({
280
293
  pages,
@@ -302,16 +315,25 @@ export function createConstraintPanel() {
302
315
  if (!panel.isConnected)
303
316
  document.body.appendChild(panel);
304
317
  panel.hidden = false;
318
+ setLinesOnly(window.innerWidth < 640);
305
319
  if (mode === 'site')
306
320
  void runSite();
307
321
  else
308
322
  run();
323
+ layoutChanges.observe(document.body, { subtree: true, childList: true, attributes: true, attributeFilter: ['style', 'class', 'hidden', 'data-ds-fault-lines', 'data-ds-fault-axis'] });
324
+ sizeChanges?.observe(document.body);
309
325
  window.addEventListener('scroll', schedule, { passive: true, capture: true });
310
326
  window.addEventListener('resize', schedule);
311
327
  };
312
328
  const hide = () => {
313
329
  stopSite();
314
330
  panel.hidden = true;
331
+ if (frame)
332
+ cancelAnimationFrame(frame);
333
+ frame = 0;
334
+ faultLines.replaceChildren();
335
+ layoutChanges.disconnect();
336
+ sizeChanges?.disconnect();
315
337
  window.removeEventListener('scroll', schedule, { capture: true });
316
338
  window.removeEventListener('resize', schedule);
317
339
  };
@@ -1,6 +1,7 @@
1
1
  import { type Constraint } from './constraints';
2
2
  /** The narrowest phone still sold, the common ones and the widest. */
3
3
  export declare const PHONE_WIDTHS: number[];
4
+ export declare const VIEWPORT_WIDTHS: number[];
4
5
  /** Pages checked side by side, each in its own frame. */
5
6
  export declare const PAGES_AT_ONCE = 3;
6
7
  /** A rule broken on a page at a width, said in words, since the element stays in its frame. */
@@ -1 +1 @@
1
- {"version":3,"file":"constraint-site.d.ts","sourceRoot":"","sources":["../../DS1/0-face/constraint-site.ts"],"names":[],"mappings":"AAKA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAGjF,sEAAsE;AACtE,eAAO,MAAM,YAAY,UAAuB,CAAC;AACjD,yDAAyD;AACzD,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B,+FAA+F;AAC/F,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAC7G,0GAA0G;AAC1G,MAAM,MAAM,SAAS,GAAG;IAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC;AACtF,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpF,4FAA4F;AAC5F,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAIxD;AAED,+GAA+G;AAC/G,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAUlE;AAED,uGAAuG;AACvG,wBAAsB,aAAa,CAAC,IAAI,GAAE,QAAmB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAahF;AAID;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,EAAiB,EAAE,MAAM,SAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAmCjJ;AAED,0FAA0F;AAC1F,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,CAQrD;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,EAAE,KAAK,EAAE,MAAqB,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAsB,EAAE,EAAE;IACnH,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,yDAAyD;IACzD,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACjE,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAuBxB;AAmBD,+GAA+G;AAC/G,MAAM,MAAM,WAAW,GAAG;IAAE,UAAU,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtG,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,WAAW,EAAE,CAUlE;AAID,8JAA8J;AAC9J,wBAAgB,QAAQ,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMpG;AAED,sFAAsF;AACtF,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,EAAE,CAMpI"}
1
+ {"version":3,"file":"constraint-site.d.ts","sourceRoot":"","sources":["../../DS1/0-face/constraint-site.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,7 @@ import { constraints, measureConstraints } from './constraints';
7
7
  import { calculateScalingFactor, defaultScalingConfig } from './scaling';
8
8
  /** The narrowest phone still sold, the common ones and the widest. */
9
9
  export const PHONE_WIDTHS = [320, 375, 393, 430];
10
+ export const VIEWPORT_WIDTHS = [...PHONE_WIDTHS, 768, 1024, 1440];
10
11
  /** Pages checked side by side, each in its own frame. */
11
12
  export const PAGES_AT_ONCE = 3;
12
13
  /** An element as a person finds it: its tag, id and classes, and the start of its words. */
@@ -52,7 +53,7 @@ const wait = (ms) => new Promise(done => setTimeout(done, ms));
52
53
  * order of `widths`. The page reflows at each width without loading again. The address carries `ph=out`,
53
54
  * the analytics opt-out most trackers honour, so the check counts no visits.
54
55
  */
55
- export async function inspectInFrame(host, url, widths = PHONE_WIDTHS, height = 844) {
56
+ export async function inspectInFrame(host, url, widths = VIEWPORT_WIDTHS, height = 844) {
56
57
  const frame = document.createElement('iframe');
57
58
  frame.setAttribute('aria-hidden', 'true');
58
59
  frame.tabIndex = -1;
@@ -106,7 +107,7 @@ export function checkPage(page) {
106
107
  * each width as it comes. Stops starting pages when `signal` aborts. Whatever the pages store in this
107
108
  * browser while they are checked is put back afterwards.
108
109
  */
109
- export async function checkSite({ pages, widths = PHONE_WIDTHS, inspect, onResult, signal, atOnce = PAGES_AT_ONCE }) {
110
+ export async function checkSite({ pages, widths = VIEWPORT_WIDTHS, inspect, onResult, signal, atOnce = PAGES_AT_ONCE }) {
110
111
  const kept = storedNow();
111
112
  const results = [];
112
113
  const total = pages.length * widths.length;
@@ -1 +1 @@
1
- {"version":3,"file":"constraints.d.ts","sourceRoot":"","sources":["../../DS1/0-face/constraints.ts"],"names":[],"mappings":"AAeA;+GAC+G;AAC/G,MAAM,MAAM,OAAO,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE9E,MAAM,MAAM,UAAU,GAAG;IACvB,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,gGAAgG;IAChG,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,4EAA4E;IAC5E,OAAO,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,EAAE,CAAC;IACzC,uGAAuG;IACvG,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,KAAK,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,MAAM,CAAC;CACrC,CAAC;AAqEF,eAAO,MAAM,WAAW,EAAE,UAAU,EAwInC,CAAC;AAEF,qDAAqD;AACrD,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,UAAqB,GAAG,CAAC,OAAO,GAAG;IAAE,UAAU,EAAE,UAAU,CAAA;CAAE,CAAC,EAAE,CAExG;AAED,iHAAiH;AACjH,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,UAAqB,GAAG;IAAE,UAAU,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;CAAE,EAAE,CAEnI;AAED,qEAAqE;AACrE,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,WAGxC,CAAC;AAEF,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,IAAI,GAAE,UAAU,EAAgB,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,EAAE,CAOtF;AAED,eAAO,MAAM,UAAU,wGAAwG,CAAC;AAChI,eAAO,MAAM,QAAQ,6BAA6B,CAAC;AAEnD,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,GAAE,UAAU,EAAgB,GAAG,MAAM,CAQnE"}
1
+ {"version":3,"file":"constraints.d.ts","sourceRoot":"","sources":["../../DS1/0-face/constraints.ts"],"names":[],"mappings":"AAgBA;+GAC+G;AAC/G,MAAM,MAAM,OAAO,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE9E,MAAM,MAAM,UAAU,GAAG;IACvB,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,gGAAgG;IAChG,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,4EAA4E;IAC5E,OAAO,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,EAAE,CAAC;IACzC,uGAAuG;IACvG,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,KAAK,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,MAAM,CAAC;CACrC,CAAC;AA6EF,eAAO,MAAM,WAAW,EAAE,UAAU,EA0JnC,CAAC;AAEF,qDAAqD;AACrD,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,UAAqB,GAAG,CAAC,OAAO,GAAG;IAAE,UAAU,EAAE,UAAU,CAAA;CAAE,CAAC,EAAE,CAExG;AAED,iHAAiH;AACjH,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,UAAqB,GAAG;IAAE,UAAU,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;CAAE,EAAE,CAEnI;AAED,qEAAqE;AACrE,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,WAGxC,CAAC;AAEF,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,IAAI,GAAE,UAAU,EAAgB,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,EAAE,CAOtF;AAED,eAAO,MAAM,UAAU,wGAAwG,CAAC;AAChI,eAAO,MAAM,QAAQ,6BAA6B,CAAC;AAEnD,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,GAAE,UAAU,EAAgB,GAAG,MAAM,CAQnE"}
@@ -1,6 +1,15 @@
1
+ import { faultLineGrids, inspectFaultLines } from './fault-lines';
1
2
  import { labelledButtons, inspectButtonLabels } from './button-label';
2
3
  import { inspectTextContrast, measureTextContrast } from "./text-contrast";
3
4
  import { inspectWordWrap, wordHeaders } from './word-wrap';
5
+ /** The actual initial, including a component nested in another open shadow root. */
6
+ function isAvatarInitial(element) {
7
+ for (let parent = element; parent; parent = parent.assignedSlot ?? parent.parentElement ?? parent.getRootNode().host ?? null) {
8
+ if (parent.matches('ds-avatar, .ds-avatar'))
9
+ return true;
10
+ }
11
+ return false;
12
+ }
4
13
  /** A pixel either way is the same place. */
5
14
  const near = (a, b) => Math.abs(a - b) <= 1;
6
15
  const px = (n) => `${Math.round(n)}px`;
@@ -68,6 +77,14 @@ function rows(found) {
68
77
  return groups.filter(row => row.length > 1);
69
78
  }
70
79
  export const constraints = [
80
+ {
81
+ code: 'page.fault-lines',
82
+ rule: 'Fault lines keep related card and control grids aligned across the whole page, with matching track edges and gaps at every checked viewport width.',
83
+ why: 'Compare separate sections in the same alignment family; responsive layouts may change together, while unrelated groups and intentional exceptions remain independent.',
84
+ components: ['ds-page'],
85
+ check: { file: 'tests/fault-lines.test.ts', test: 'fault lines catch shifted tracks and unequal gaps across sections at each viewport' },
86
+ measures: ['alignment group', 'alignment groups'], count: root => faultLineGrids(root).length, inspect: inspectFaultLines,
87
+ },
71
88
  {
72
89
  code: 'select.hug',
73
90
  rule: 'Select and date-picker triggers use hug sizing, never fill sizing or growing to occupy the available row.',
@@ -111,6 +128,16 @@ export const constraints = [
111
128
  count: root => measureTextContrast(root).length,
112
129
  inspect: inspectTextContrast,
113
130
  },
131
+ {
132
+ code: 'avatar.initial-contrast',
133
+ rule: 'Avatar initials have at least 4.5:1 contrast against their rendered fill, including after a theme or identity colour changes.',
134
+ why: 'Check each initial inside the avatar’s shadow root and in overlapping groups; decorative initials still need to be readable. Explicit ink overrides must pass too.',
135
+ components: ['ds-avatar'],
136
+ check: { file: 'tests/avatar-contrast.test.ts', test: 'avatar initials stay readable after theme and CSS-variable changes, and the avatar constraint catches overrides' },
137
+ measures: ['avatar initial', 'avatar initials'],
138
+ count: root => measureTextContrast(root).filter(result => isAvatarInitial(result.element)).length,
139
+ inspect: root => inspectTextContrast(root).filter(finding => isAvatarInitial(finding.element)),
140
+ },
114
141
  {
115
142
  code: 'card.media-level',
116
143
  rule: "A card's picture stands on the card's foot, a square below the words at least, so the pictures of cards side by side stand on one level whatever the words above them.",
@@ -5,7 +5,7 @@ export function define(name, element) {
5
5
  const type = element;
6
6
  const prototype = element.prototype;
7
7
  const original = prototype.createRenderRoot;
8
- if (original) {
8
+ if (original && typeof document !== 'undefined') {
9
9
  const definingDocument = document;
10
10
  prototype.createRenderRoot = function () {
11
11
  var _a;
@@ -0,0 +1,20 @@
1
+ import type { Finding } from './constraints';
2
+ type Grid = {
3
+ element: Element;
4
+ family: string | null;
5
+ axis: 'x' | 'y';
6
+ lines: number[];
7
+ gaps: number[];
8
+ crossGap?: number;
9
+ internalDrift: boolean;
10
+ start: number;
11
+ end: number;
12
+ };
13
+ /** Related grids opt in by name; unlabelled card grids are candidates for human review. */
14
+ export declare function faultLineGrids(root: ParentNode): Grid[];
15
+ /** Fault lines compare rendered tracks across separate sections, in the current viewport. */
16
+ export declare function inspectFaultLines(root: ParentNode): Finding[];
17
+ /** The same rendered geometry as the checker, painted without affecting layout or hit testing. */
18
+ export declare function paintFaultLines(root: ParentNode, layer: SVGSVGElement): void;
19
+ export {};
20
+ //# sourceMappingURL=fault-lines.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fault-lines.d.ts","sourceRoot":"","sources":["../../DS1/0-face/fault-lines.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAe7C,KAAK,IAAI,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjL,2FAA2F;AAC3F,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,EAAE,CA2BvD;AAWD,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,EAAE,CAkB7D;AAGD,kGAAkG;AAClG,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,CA+E5E"}
@@ -0,0 +1,172 @@
1
+ const tolerance = 1;
2
+ const near = (a, b) => Math.abs(a - b) <= tolerance;
3
+ const style = (el) => el.ownerDocument.defaultView.getComputedStyle(el);
4
+ const visible = (el) => {
5
+ const box = el.getBoundingClientRect();
6
+ return box.width > 0 && box.height > 0 && style(el).visibility !== 'hidden' && style(el).display !== 'none';
7
+ };
8
+ /** Walk each open shadow tree once; slotted children already occur in the light tree. */
9
+ function elements(root) {
10
+ return Array.from(root.querySelectorAll('*')).flatMap(el => [el, ...(el.shadowRoot ? elements(el.shadowRoot) : [])]);
11
+ }
12
+ /** Related grids opt in by name; unlabelled card grids are candidates for human review. */
13
+ export function faultLineGrids(root) {
14
+ return elements(root).flatMap(element => {
15
+ const family = element.getAttribute('data-ds-fault-lines');
16
+ if (family === 'off' || !visible(element))
17
+ return [];
18
+ const children = Array.from(element.children).filter(visible);
19
+ if (!children.length)
20
+ return [];
21
+ if (family === null && (!/grid/.test(style(element).display) || children.length < 2 ||
22
+ !children.every(child => child.matches('ds-card, .ds-card, ds-button, button'))))
23
+ return [];
24
+ const axis = element.getAttribute('data-ds-fault-axis') === 'y' ? 'y' : 'x';
25
+ const boxes = children.map(child => child.getBoundingClientRect());
26
+ const crossStart = (box) => axis === 'x' ? box.top : box.left;
27
+ const crossEnd = (box) => axis === 'x' ? box.bottom : box.right;
28
+ const start = (box) => axis === 'x' ? box.left : box.top;
29
+ const end = (box) => axis === 'x' ? box.right : box.bottom;
30
+ const bands = [];
31
+ for (const box of boxes.sort((a, b) => crossStart(a) - crossStart(b))) {
32
+ const band = bands.find(row => near(crossStart(row[0]), crossStart(box)));
33
+ if (band)
34
+ band.push(box);
35
+ else
36
+ bands.push([box]);
37
+ }
38
+ // The fullest band defines the tracks; an incomplete final row is valid.
39
+ const band = [...bands].sort((a, b) => b.length - a.length)[0].sort((a, b) => start(a) - start(b));
40
+ const lines = band.flatMap(box => [start(box), end(box)]);
41
+ const gaps = band.slice(1).map((box, index) => start(box) - end(band[index]));
42
+ const crossGaps = bands.slice(1).map((row, index) => Math.min(...row.map(crossStart)) - Math.max(...bands[index].map(crossEnd)));
43
+ const internalDrift = bands.some(row => row.length === band.length && row.slice().sort((a, b) => start(a) - start(b)).some((box, index) => !near(start(box), lines[index * 2]) || !near(end(box), lines[index * 2 + 1]))) || crossGaps.some(gap => !near(gap, crossGaps[0]));
44
+ return [{ element, family, axis, lines, gaps, crossGap: crossGaps[0], internalDrift, start: lines[0], end: lines[lines.length - 1] }];
45
+ });
46
+ }
47
+ function referenceFor(grids, i) {
48
+ const current = grids[i];
49
+ return grids.slice(0, i).find(other => {
50
+ if (other.axis !== current.axis || other.lines.length !== current.lines.length)
51
+ return false;
52
+ if (current.family !== null || other.family !== null)
53
+ return current.family !== null && current.family === other.family;
54
+ return near(current.start, other.start) && near(current.end, other.end);
55
+ });
56
+ }
57
+ /** Fault lines compare rendered tracks across separate sections, in the current viewport. */
58
+ export function inspectFaultLines(root) {
59
+ const grids = faultLineGrids(root);
60
+ const findings = [];
61
+ for (let i = 0; i < grids.length; i++) {
62
+ const current = grids[i];
63
+ if (current.internalDrift)
64
+ findings.push({ element: current.element, ...(current.family === null ? { review: true } : {}), message: `Fault lines at ${current.element.ownerDocument.documentElement.clientWidth}px: tracks or repeated gaps drift inside ${current.family || 'this card grid'}. Align its complete rows and repeated spacing.` });
65
+ const reference = referenceFor(grids, i);
66
+ if (!reference)
67
+ continue;
68
+ const drift = current.lines.map((line, index) => Math.abs(line - reference.lines[index]));
69
+ const gapDrift = current.gaps.some((gap, index) => !near(gap, reference.gaps[index]));
70
+ const rowDrift = current.crossGap !== undefined && reference.crossGap !== undefined && !near(current.crossGap, reference.crossGap);
71
+ if (!drift.some(value => value > tolerance) && !gapDrift && !rowDrift)
72
+ continue;
73
+ const width = current.element.ownerDocument.documentElement.clientWidth;
74
+ const review = current.family === null;
75
+ findings.push({ element: current.element, ...(review ? { review: true } : {}), message: `Fault lines at ${width}px: ${current.family || 'these card grids'} ${review ? 'may be related but have' : 'have'} misaligned ${current.axis === 'x' ? 'column' : 'row'} edges or unequal gaps (edge drift ${Math.round(Math.max(...drift))}px; gaps ${current.gaps.map(Math.round).join('/')}px versus ${reference.gaps.map(Math.round).join('/')}px${rowDrift ? `; cross-axis gap ${Math.round(current.crossGap)}px versus ${Math.round(reference.crossGap)}px` : ''}). ${review ? 'Name the related groups with data-ds-fault-lines to enforce alignment, or mark an intentional exception off.' : 'Use the same tracks and gap for this family at this viewport.'}` });
76
+ }
77
+ return findings;
78
+ }
79
+ /** The same rendered geometry as the checker, painted without affecting layout or hit testing. */
80
+ export function paintFaultLines(root, layer) {
81
+ const doc = layer.ownerDocument;
82
+ const width = doc.documentElement.clientWidth;
83
+ const height = doc.defaultView.innerHeight;
84
+ layer.setAttribute('viewBox', `0 0 ${width} ${height}`);
85
+ layer.setAttribute('width', String(width));
86
+ layer.setAttribute('height', String(height));
87
+ layer.setAttribute('aria-hidden', 'true');
88
+ layer.style.cssText = 'position:fixed;inset:0;pointer-events:none;overflow:hidden;z-index:2147483644';
89
+ layer.replaceChildren();
90
+ const grids = faultLineGrids(root);
91
+ const failed = new Set(inspectFaultLines(root).map(finding => finding.element));
92
+ const drawn = new Set();
93
+ const referenceInk = '#087ba8';
94
+ const faultInk = '#de0331';
95
+ function mark(tag, attributes, title) {
96
+ const node = doc.createElementNS('http://www.w3.org/2000/svg', tag);
97
+ for (const [key, value] of Object.entries(attributes))
98
+ node.setAttribute(key, String(value));
99
+ if (title) {
100
+ const tip = doc.createElementNS(node.namespaceURI, 'title');
101
+ tip.textContent = title;
102
+ node.appendChild(tip);
103
+ }
104
+ layer.appendChild(node);
105
+ return node;
106
+ }
107
+ for (let index = 0; index < grids.length; index++) {
108
+ const grid = grids[index];
109
+ const reference = referenceFor(grids, index) ?? grid;
110
+ for (const edge of reference.lines) {
111
+ const key = `${grid.axis}:${edge}`;
112
+ if (drawn.has(key))
113
+ continue;
114
+ drawn.add(key);
115
+ mark('line', { 'data-fault-reference': '', x1: grid.axis === 'x' ? edge : 0, y1: grid.axis === 'y' ? edge : 0,
116
+ x2: grid.axis === 'x' ? edge : width, y2: grid.axis === 'y' ? edge : height,
117
+ stroke: referenceInk, 'stroke-width': 1, 'stroke-dasharray': '4 4' }, 'Reference edge');
118
+ }
119
+ const broken = failed.has(grid.element);
120
+ if (!broken)
121
+ continue;
122
+ // Draw each actual child so drift inside a later row is visible too.
123
+ for (const child of Array.from(grid.element.children).filter(visible)) {
124
+ const box = child.getBoundingClientRect();
125
+ const edges = grid.axis === 'x' ? [box.left, box.right] : [box.top, box.bottom];
126
+ for (const edge of edges) {
127
+ if (reference.lines.some(expected => near(edge, expected)))
128
+ continue;
129
+ mark('line', { 'data-fault-actual': '', x1: grid.axis === 'x' ? edge : box.left, y1: grid.axis === 'y' ? edge : box.top,
130
+ x2: grid.axis === 'x' ? edge : box.right, y2: grid.axis === 'y' ? edge : box.bottom,
131
+ stroke: faultInk, 'stroke-width': 2 }, 'Actual edge differs from the dashed reference');
132
+ }
133
+ }
134
+ // Row spacing can drift even when every column edge still aligns.
135
+ const rows = [];
136
+ for (const child of Array.from(grid.element.children).filter(visible)) {
137
+ const box = child.getBoundingClientRect();
138
+ const start = grid.axis === 'x' ? box.top : box.left;
139
+ const row = rows.find(row => near(grid.axis === 'x' ? row[0].top : row[0].left, start));
140
+ if (row)
141
+ row.push(box);
142
+ else
143
+ rows.push([box]);
144
+ }
145
+ rows.sort((a, b) => grid.axis === 'x' ? a[0].top - b[0].top : a[0].left - b[0].left);
146
+ const bounds = grid.element.getBoundingClientRect();
147
+ const crossGap = reference.crossGap ?? grid.crossGap;
148
+ if (crossGap !== undefined)
149
+ for (let row = 1; row < rows.length; row++) {
150
+ const previousEnd = Math.max(...rows[row - 1].map(box => grid.axis === 'x' ? box.bottom : box.right));
151
+ const actual = Math.min(...rows[row].map(box => grid.axis === 'x' ? box.top : box.left));
152
+ const expected = previousEnd + crossGap;
153
+ if (near(actual, expected))
154
+ continue;
155
+ for (const [edge, kind] of [[expected, 'reference'], [actual, 'actual']]) {
156
+ mark('line', { [`data-fault-${kind}`]: '', x1: grid.axis === 'x' ? bounds.left : edge, y1: grid.axis === 'x' ? edge : bounds.top,
157
+ x2: grid.axis === 'x' ? bounds.right : edge, y2: grid.axis === 'x' ? edge : bounds.bottom,
158
+ stroke: kind === 'reference' ? referenceInk : faultInk, 'stroke-width': kind === 'reference' ? 1 : 2,
159
+ 'stroke-dasharray': kind === 'reference' ? '4 4' : '' }, `Row gap ${Math.round(actual - previousEnd)}px; reference ${Math.round(crossGap)}px`);
160
+ }
161
+ }
162
+ const box = grid.element.getBoundingClientRect();
163
+ const label = mark('text', { 'data-fault-gap': '', x: Math.max(4, box.left), y: Math.max(16, Math.min(height - 4, box.top + 16)),
164
+ fill: faultInk, stroke: 'var(--ds-background-color, white)', 'stroke-width': 3, 'paint-order': 'stroke',
165
+ 'font-family': 'var(--ds-typeface-regular, sans-serif)', 'font-size': 12 });
166
+ const gaps = grid.gaps.map(gap => Math.round(gap)).join('/');
167
+ const expected = reference.gaps.map(gap => Math.round(gap)).join('/');
168
+ label.textContent = `Gap ${gaps || '—'}px · reference ${expected || '—'}px` +
169
+ (grid.crossGap !== undefined && reference.crossGap !== undefined && !near(grid.crossGap, reference.crossGap)
170
+ ? ` · row gap ${Math.round(grid.crossGap)}px / ${Math.round(reference.crossGap)}px` : '');
171
+ }
172
+ }
@@ -90,6 +90,11 @@ export declare class Avatar extends LitElement {
90
90
  private failed;
91
91
  static styles: import("lit").CSSResult;
92
92
  constructor();
93
+ private inkObserver?;
94
+ private inkFrame;
95
+ private readonly queueInk;
96
+ connectedCallback(): void;
97
+ disconnectedCallback(): void;
93
98
  protected willUpdate(changed: PropertyValues<this>): void;
94
99
  /** A fill named by a variable (`var(--brand-highlight)`) gets its ink once the browser has resolved it. */
95
100
  protected updated(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"ds-avatar.d.ts","sourceRoot":"","sources":["../../DS1/2-core/ds-avatar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA4B,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAmBhF,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK3D;AAED,6GAA6G;AAC7G,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAM9D;AAED;;;;;;;GAOG;AACH,qBAAa,MAAO,SAAQ,UAAU;IACpC,MAAM,CAAC,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAiBf;IACM,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,OAAO,CAAC;IACtB,kEAAkE;IAC1D,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC;IAClC,2EAA2E;IACnE,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;IAC/C,GAAG,EAAE,OAAO,GAAG,SAAS,CAAC;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACvB,QAAgB,MAAM,CAAU;IAChC,QAAgB,MAAM,CAAU;IAChC,MAAM,CAAC,MAAM,0BAAqB;;IAclC,SAAS,CAAC,UAAU,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC;IAOlD,2GAA2G;IAC3G,SAAS,CAAC,OAAO;IAIjB;oEACgE;IAChE,OAAO,CAAC,UAAU;IAUlB,MAAM;CAoDP;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,WAAW,EAAE,MAAM,CAAC;KACrB;CACF"}
1
+ {"version":3,"file":"ds-avatar.d.ts","sourceRoot":"","sources":["../../DS1/2-core/ds-avatar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA4B,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAsBhF,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK3D;AAED,6GAA6G;AAC7G,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAM9D;AAED;;;;;;;GAOG;AACH,qBAAa,MAAO,SAAQ,UAAU;IACpC,MAAM,CAAC,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAiBf;IACM,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,OAAO,CAAC;IACtB,kEAAkE;IAC1D,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC;IAClC,2EAA2E;IACnE,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;IAC/C,GAAG,EAAE,OAAO,GAAG,SAAS,CAAC;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACvB,QAAgB,MAAM,CAAU;IAChC,QAAgB,MAAM,CAAU;IAChC,MAAM,CAAC,MAAM,0BAAqB;;IAclC,OAAO,CAAC,WAAW,CAAC,CAAmB;IACvC,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAGvB;IACF,iBAAiB;IAUjB,oBAAoB;IAMpB,SAAS,CAAC,UAAU,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC;IAOlD,2GAA2G;IAC3G,SAAS,CAAC,OAAO;IAIjB;oEACgE;IAChE,OAAO,CAAC,UAAU;IAwBlB,MAAM;CAoDP;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,WAAW,EAAE,MAAM,CAAC;KACrB;CACF"}
@@ -1,6 +1,7 @@
1
1
  import { LitElement, html, nothing, unsafeCSS } from "lit";
2
2
  import { styleMap } from "lit/directives/style-map.js";
3
3
  import { keyed } from "lit/directives/keyed.js";
4
+ import { contrastRatio, colorReader, composite } from "../0-face/text-contrast";
4
5
  import { define } from "../0-face/define";
5
6
  import { provideIcons } from "./ds-icon";
6
7
  import { avatarIcon } from "./generated/icon-avatar";
@@ -10,9 +11,13 @@ import styles from "./styles/ds-avatar.css?inline";
10
11
  provideIcons({ avatar: avatarIcon });
11
12
  /** The ink that reads on a fill: black on a light one, white on a dark one. */
12
13
  const inkOn = (rgb) => {
13
- const linear = rgb.map((n) => (n <= 0.04045 ? n / 12.92 : ((n + 0.055) / 1.055) ** 2.4));
14
- const luminance = linear[0] * 0.2126 + linear[1] * 0.7152 + linear[2] * 0.0722;
15
- return luminance > 0.22 ? "var(--ds-black)" : "var(--ds-white)";
14
+ const fill = [...rgb, 1];
15
+ const white = [1, 1, 1, 1], black = [42 / 255, 42 / 255, 42 / 255, 1];
16
+ if (contrastRatio(black, fill) >= 4.5)
17
+ return "var(--ds-black)";
18
+ if (contrastRatio(white, fill) >= 4.5)
19
+ return "var(--ds-white)";
20
+ return "black";
16
21
  };
17
22
  /** Saved identity colours are hex; their ink must stay legible in either theme. */
18
23
  export function avatarInk(color) {
@@ -43,6 +48,12 @@ export function inkOnResolved(fill) {
43
48
  export class Avatar extends LitElement {
44
49
  constructor() {
45
50
  super();
51
+ this.inkFrame = 0;
52
+ this.queueInk = () => {
53
+ const view = this.ownerDocument.defaultView;
54
+ if (!this.inkFrame)
55
+ this.inkFrame = view.requestAnimationFrame(() => { this.inkFrame = 0; this.resolveInk(); });
56
+ };
46
57
  this.label = "";
47
58
  this.initial = "?";
48
59
  this.src = "";
@@ -54,6 +65,24 @@ export class Avatar extends LitElement {
54
65
  this.fit = "cover";
55
66
  this.logo = this.mask = this.outlined = this.pending = this.plain = this.loaded = this.failed = false;
56
67
  }
68
+ connectedCallback() {
69
+ super.connectedCallback();
70
+ const view = this.ownerDocument.defaultView;
71
+ this.inkObserver = new view.MutationObserver(this.queueInk);
72
+ // Observe only the painted ancestors, never our own shadow's ink writes.
73
+ for (let parent = this; parent; parent = parent.parentElement ?? parent.getRootNode().host ?? null) {
74
+ this.inkObserver.observe(parent, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });
75
+ }
76
+ view.addEventListener('load', this.queueInk, true);
77
+ }
78
+ disconnectedCallback() {
79
+ super.disconnectedCallback();
80
+ this.inkObserver?.disconnect();
81
+ const view = this.ownerDocument.defaultView;
82
+ view.cancelAnimationFrame(this.inkFrame);
83
+ this.inkFrame = 0;
84
+ view.removeEventListener('load', this.queueInk, true);
85
+ }
57
86
  willUpdate(changed) {
58
87
  if (changed.has("src")) {
59
88
  this.loaded = false;
@@ -63,7 +92,7 @@ export class Avatar extends LitElement {
63
92
  }
64
93
  /** A fill named by a variable (`var(--brand-highlight)`) gets its ink once the browser has resolved it. */
65
94
  updated() {
66
- if (this.ink || this.pending || this.plain || !this.color || avatarInk(this.color))
95
+ if (this.ink || this.pending || this.plain)
67
96
  return;
68
97
  this.resolveInk();
69
98
  }
@@ -73,13 +102,28 @@ export class Avatar extends LitElement {
73
102
  const frame = this.renderRoot.querySelector(".frame");
74
103
  if (!frame || !this.isConnected)
75
104
  return;
76
- const ink = inkOnResolved(getComputedStyle(frame).backgroundColor);
77
- if (ink) {
78
- frame.style.color = ink;
105
+ if (this.ink || this.pending || this.plain)
106
+ return;
107
+ const view = this.ownerDocument.defaultView;
108
+ const read = colorReader(this.ownerDocument);
109
+ let fill = read(view.getComputedStyle(frame).backgroundColor);
110
+ if (fill && fill[3] > 0) {
111
+ for (let parent = this; parent && fill[3] < 1; parent = parent.parentElement ?? parent.getRootNode().host ?? null) {
112
+ const background = read(view.getComputedStyle(parent).backgroundColor);
113
+ if (background)
114
+ fill = composite(fill, background);
115
+ }
116
+ if (fill[3] < 1)
117
+ return;
118
+ const tokens = view.getComputedStyle(this);
119
+ const black = read(tokens.getPropertyValue('--ds-black').trim()), white = read(tokens.getPropertyValue('--ds-white').trim());
120
+ const ink = black && contrastRatio(black, fill) >= 4.5 ? 'var(--ds-black)' : white && contrastRatio(white, fill) >= 4.5 ? 'var(--ds-white)' : contrastRatio([0, 0, 0, 1], fill) >= 4.5 ? 'black' : 'white';
121
+ if (frame.style.color !== ink)
122
+ frame.style.color = ink;
79
123
  return;
80
124
  }
81
125
  if (tries < 60)
82
- requestAnimationFrame(() => this.resolveInk(tries + 1));
126
+ this.ownerDocument.defaultView.requestAnimationFrame(() => this.resolveInk(tries + 1));
83
127
  if (tries === 0 && this.ownerDocument.readyState !== "complete") {
84
128
  this.ownerDocument.defaultView?.addEventListener("load", () => this.resolveInk(60), { once: true });
85
129
  }
@@ -96,7 +140,7 @@ export class Avatar extends LitElement {
96
140
  >`;
97
141
  return html `<span
98
142
  class="frame"
99
- style=${styleMap({ backgroundColor: !this.pending && !this.plain ? this.color || undefined : undefined, color: !this.pending ? this.ink || avatarInk(this.color) : undefined })}
143
+ style=${styleMap({ backgroundColor: !this.pending && !this.plain ? this.color || undefined : undefined, color: !this.pending ? this.ink || (!this.plain ? avatarInk(this.color) : undefined) : undefined })}
100
144
  role=${this.label ? "img" : nothing}
101
145
  aria-label=${this.label || nothing}
102
146
  aria-hidden=${this.label ? nothing : "true"}
@@ -1 +1 @@
1
- {"version":3,"file":"context-trigger.d.ts","sourceRoot":"","sources":["../../DS1/3-unit/context-trigger.ts"],"names":[],"mappings":"AAAA,OAAO,8BAA8B,CAAC;AAEtC,yGAAyG;AACzG,eAAO,MAAM,UAAU,MAAM,CAAC;AAE9B,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,YAAY,GAAG,aAAa,CAAC;AACvE,MAAM,MAAM,YAAY,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AACpD,KAAK,OAAO,GAAG;IACb,wEAAwE;IACxE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,WAAW,GAAG,IAAI,CAAC;IACxD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iFAAiF;IACjF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF;gFACgF;AAChF,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,WAAW,EACpB,IAAI,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,KAAK,EACxF,EAAE,OAAuB,EAAE,KAAY,EAAE,SAAiB,EAAE,IAAW,EAAE,GAAE,OAAY;;;EAqFxF"}
1
+ {"version":3,"file":"context-trigger.d.ts","sourceRoot":"","sources":["../../DS1/3-unit/context-trigger.ts"],"names":[],"mappings":"AAAA,OAAO,8BAA8B,CAAC;AAEtC,yGAAyG;AACzG,eAAO,MAAM,UAAU,MAAM,CAAC;AAE9B,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,YAAY,GAAG,aAAa,CAAC;AACvE,MAAM,MAAM,YAAY,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AACpD,KAAK,OAAO,GAAG;IACb,wEAAwE;IACxE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,WAAW,GAAG,IAAI,CAAC;IACxD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iFAAiF;IACjF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF;gFACgF;AAChF,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,WAAW,EACpB,IAAI,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,KAAK,EACxF,EAAE,OAAuB,EAAE,KAAY,EAAE,SAAiB,EAAE,IAAW,EAAE,GAAE,OAAY;;;EAgHxF"}