@aceshooting/lyra-ui 2.0.0 → 2.1.0

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 (224) hide show
  1. package/README.md +87 -14
  2. package/custom-elements.json +11555 -5539
  3. package/dist/components/app-rail/app-rail-item.d.ts +30 -0
  4. package/dist/components/app-rail/app-rail-item.js +68 -0
  5. package/dist/components/app-rail/app-rail-item.styles.d.ts +1 -0
  6. package/dist/components/app-rail/app-rail-item.styles.js +65 -0
  7. package/dist/components/app-rail/app-rail.d.ts +21 -24
  8. package/dist/components/app-rail/app-rail.js +68 -132
  9. package/dist/components/app-rail/app-rail.styles.js +5 -2
  10. package/dist/components/attachment-chip/attachment-chip.d.ts +38 -11
  11. package/dist/components/attachment-chip/attachment-chip.js +73 -29
  12. package/dist/components/attachment-trigger/attachment-trigger.d.ts +13 -0
  13. package/dist/components/attachment-trigger/attachment-trigger.js +13 -1
  14. package/dist/components/attachment-trigger/attachment-trigger.styles.js +28 -6
  15. package/dist/components/chart/bar-chart.js +2 -18
  16. package/dist/components/chart/box-plot.d.ts +25 -1
  17. package/dist/components/chart/box-plot.js +136 -6
  18. package/dist/components/chart/box-plot.styles.js +13 -0
  19. package/dist/components/chart/bubble-chart.js +2 -18
  20. package/dist/components/chart/chart.d.ts +54 -10
  21. package/dist/components/chart/chart.js +182 -39
  22. package/dist/components/chart/doughnut-chart.js +2 -18
  23. package/dist/components/chart/histogram-bin.d.ts +5 -2
  24. package/dist/components/chart/histogram-bin.js +12 -4
  25. package/dist/components/chart/histogram.js +8 -16
  26. package/dist/components/chart/line-chart.js +2 -18
  27. package/dist/components/chart/lite-chart.d.ts +80 -3
  28. package/dist/components/chart/lite-chart.js +250 -53
  29. package/dist/components/chart/pie-chart.js +2 -18
  30. package/dist/components/chart/polar-area-chart.js +2 -18
  31. package/dist/components/chart/radar-chart.js +2 -18
  32. package/dist/components/chart/scatter-chart.js +2 -18
  33. package/dist/components/chat-composer/chat-composer.d.ts +42 -1
  34. package/dist/components/chat-composer/chat-composer.js +84 -3
  35. package/dist/components/chat-composer/chat-composer.styles.js +11 -1
  36. package/dist/components/chat-message/chat-message.d.ts +32 -4
  37. package/dist/components/chat-message/chat-message.js +57 -17
  38. package/dist/components/checkbox/checkbox.d.ts +66 -8
  39. package/dist/components/checkbox/checkbox.js +173 -50
  40. package/dist/components/checkbox/checkbox.styles.js +9 -2
  41. package/dist/components/chip/chip-group.js +8 -2
  42. package/dist/components/chip/chip.js +9 -2
  43. package/dist/components/citation-badge/citation-badge.js +21 -3
  44. package/dist/components/code-block/code-block.d.ts +38 -0
  45. package/dist/components/code-block/code-block.js +72 -7
  46. package/dist/components/code-block/code-loader.d.ts +44 -1
  47. package/dist/components/code-block/code-loader.js +63 -0
  48. package/dist/components/combobox/combobox.d.ts +46 -5
  49. package/dist/components/combobox/combobox.js +175 -35
  50. package/dist/components/combobox/combobox.styles.js +3 -3
  51. package/dist/components/context-meter/context-meter.js +17 -8
  52. package/dist/components/context-meter/context-meter.styles.js +0 -6
  53. package/dist/components/conversation-item/conversation-item.d.ts +13 -16
  54. package/dist/components/conversation-item/conversation-item.js +29 -20
  55. package/dist/components/date-picker/calendar-core.d.ts +24 -0
  56. package/dist/components/date-picker/calendar-core.js +51 -4
  57. package/dist/components/date-picker/date-input.d.ts +77 -10
  58. package/dist/components/date-picker/date-input.js +278 -73
  59. package/dist/components/date-picker/date-picker.d.ts +28 -4
  60. package/dist/components/date-picker/date-picker.js +95 -37
  61. package/dist/components/dialog/confirm.js +3 -6
  62. package/dist/components/dialog/dialog.d.ts +75 -31
  63. package/dist/components/dialog/dialog.js +147 -131
  64. package/dist/components/dialog/dialog.styles.js +46 -2
  65. package/dist/components/dock-panel/dock-panel.d.ts +8 -0
  66. package/dist/components/dock-panel/dock-panel.js +15 -1
  67. package/dist/components/dock-panel/dock-panel.styles.js +15 -1
  68. package/dist/components/document-preview/document-preview.d.ts +15 -7
  69. package/dist/components/document-preview/document-preview.js +29 -13
  70. package/dist/components/empty/empty.d.ts +1 -2
  71. package/dist/components/empty/empty.js +18 -12
  72. package/dist/components/empty/empty.styles.js +0 -3
  73. package/dist/components/export-button/export-button.d.ts +12 -0
  74. package/dist/components/export-button/export-button.js +33 -5
  75. package/dist/components/export-button/export-button.styles.js +5 -1
  76. package/dist/components/file-input/accept.js +2 -0
  77. package/dist/components/file-input/file-input.d.ts +10 -1
  78. package/dist/components/file-input/file-input.js +36 -5
  79. package/dist/components/flag/flag.d.ts +1 -1
  80. package/dist/components/flag/flag.js +15 -1
  81. package/dist/components/flag/flag.styles.js +1 -1
  82. package/dist/components/flag/language-map.d.ts +4 -1
  83. package/dist/components/flag/language-map.js +6 -3
  84. package/dist/components/gauge/gauge.d.ts +10 -1
  85. package/dist/components/gauge/gauge.js +28 -9
  86. package/dist/components/generation-status/generation-status.d.ts +13 -5
  87. package/dist/components/generation-status/generation-status.js +59 -5
  88. package/dist/components/graph/graph.d.ts +37 -3
  89. package/dist/components/graph/graph.js +132 -20
  90. package/dist/components/heatmap/calendar-grid.d.ts +15 -6
  91. package/dist/components/heatmap/calendar-grid.js +17 -6
  92. package/dist/components/heatmap/heatmap-scale.d.ts +5 -1
  93. package/dist/components/heatmap/heatmap-scale.js +8 -2
  94. package/dist/components/heatmap/heatmap.d.ts +166 -25
  95. package/dist/components/heatmap/heatmap.js +372 -89
  96. package/dist/components/json-viewer/json-viewer.d.ts +22 -5
  97. package/dist/components/json-viewer/json-viewer.js +115 -37
  98. package/dist/components/json-viewer/json-viewer.styles.js +2 -1
  99. package/dist/components/kbd/kbd.js +24 -7
  100. package/dist/components/live-region/live-region.d.ts +10 -1
  101. package/dist/components/live-region/live-region.js +42 -6
  102. package/dist/components/map/map.d.ts +24 -1
  103. package/dist/components/map/map.js +89 -24
  104. package/dist/components/markdown/markdown-loader.d.ts +24 -0
  105. package/dist/components/markdown/markdown-loader.js +35 -1
  106. package/dist/components/markdown/markdown.d.ts +46 -8
  107. package/dist/components/markdown/markdown.js +136 -31
  108. package/dist/components/markdown/markdown.styles.js +7 -1
  109. package/dist/components/media-card/media-card.d.ts +25 -17
  110. package/dist/components/media-card/media-card.js +43 -65
  111. package/dist/components/mention-popover/mention-popover.d.ts +4 -1
  112. package/dist/components/mention-popover/mention-popover.js +23 -5
  113. package/dist/components/menu/menu-item.d.ts +32 -3
  114. package/dist/components/menu/menu-item.js +94 -7
  115. package/dist/components/menu/menu-item.styles.js +8 -0
  116. package/dist/components/menu/menu.d.ts +25 -1
  117. package/dist/components/menu/menu.js +99 -3
  118. package/dist/components/menu/menu.styles.js +3 -2
  119. package/dist/components/model-select/model-select.d.ts +48 -3
  120. package/dist/components/model-select/model-select.js +120 -22
  121. package/dist/components/model-select/model-select.styles.js +17 -3
  122. package/dist/components/model-settings-panel/model-settings-panel.d.ts +25 -1
  123. package/dist/components/model-settings-panel/model-settings-panel.js +65 -1
  124. package/dist/components/playback/playback.d.ts +6 -2
  125. package/dist/components/playback/playback.js +72 -29
  126. package/dist/components/playback/playback.styles.js +6 -1
  127. package/dist/components/responsive-panel/responsive-panel.d.ts +29 -16
  128. package/dist/components/responsive-panel/responsive-panel.js +130 -132
  129. package/dist/components/responsive-panel/responsive-panel.styles.js +1 -1
  130. package/dist/components/result-card/result-card.d.ts +8 -2
  131. package/dist/components/result-card/result-card.js +9 -3
  132. package/dist/components/result-card/result-card.styles.js +0 -1
  133. package/dist/components/select/select.d.ts +59 -20
  134. package/dist/components/select/select.js +125 -43
  135. package/dist/components/skeleton/skeleton.d.ts +3 -0
  136. package/dist/components/skeleton/skeleton.js +7 -1
  137. package/dist/components/skeleton/skeleton.styles.js +5 -4
  138. package/dist/components/slider/slider.d.ts +57 -5
  139. package/dist/components/slider/slider.js +195 -80
  140. package/dist/components/source-card/source-card.d.ts +17 -5
  141. package/dist/components/source-card/source-card.js +55 -8
  142. package/dist/components/source-card/source-card.styles.js +1 -1
  143. package/dist/components/source-list/source-list.js +9 -1
  144. package/dist/components/sparkline/sparkline.js +34 -10
  145. package/dist/components/split/split.d.ts +141 -14
  146. package/dist/components/split/split.js +357 -52
  147. package/dist/components/split/split.styles.js +18 -1
  148. package/dist/components/stat/stat.d.ts +10 -4
  149. package/dist/components/stat/stat.js +46 -11
  150. package/dist/components/stream-status/stream-status.d.ts +27 -11
  151. package/dist/components/stream-status/stream-status.js +95 -13
  152. package/dist/components/streaming-text/streaming-text.d.ts +9 -3
  153. package/dist/components/streaming-text/streaming-text.js +25 -10
  154. package/dist/components/streaming-text/streaming-text.styles.js +3 -2
  155. package/dist/components/switch/switch.d.ts +52 -7
  156. package/dist/components/switch/switch.js +111 -40
  157. package/dist/components/switch/switch.styles.js +1 -1
  158. package/dist/components/table/table.d.ts +18 -3
  159. package/dist/components/table/table.js +88 -20
  160. package/dist/components/tabs/tabs.d.ts +23 -0
  161. package/dist/components/tabs/tabs.js +76 -12
  162. package/dist/components/tabs/tabs.styles.js +11 -0
  163. package/dist/components/thinking-panel/thinking-panel.d.ts +3 -1
  164. package/dist/components/thinking-panel/thinking-panel.js +34 -9
  165. package/dist/components/thinking-panel/thinking-panel.styles.js +6 -0
  166. package/dist/components/time-range/time-range.d.ts +59 -2
  167. package/dist/components/time-range/time-range.js +163 -67
  168. package/dist/components/toast/toast-item.d.ts +12 -3
  169. package/dist/components/toast/toast-item.js +90 -53
  170. package/dist/components/toast/toast-item.styles.js +59 -28
  171. package/dist/components/toast/toast.js +17 -5
  172. package/dist/components/toast/toaster.js +10 -1
  173. package/dist/components/tool-approval-dialog/tool-approval-dialog.d.ts +11 -10
  174. package/dist/components/tool-approval-dialog/tool-approval-dialog.js +50 -124
  175. package/dist/components/tool-approval-dialog/tool-approval-dialog.styles.js +1 -1
  176. package/dist/components/tool-call-chip/tool-call-chip.d.ts +18 -2
  177. package/dist/components/tool-call-chip/tool-call-chip.js +86 -25
  178. package/dist/components/tool-param-form/tool-param-form.d.ts +89 -28
  179. package/dist/components/tool-param-form/tool-param-form.js +350 -106
  180. package/dist/components/tool-result-dialog/tool-result-dialog.d.ts +27 -7
  181. package/dist/components/tool-result-dialog/tool-result-dialog.js +80 -115
  182. package/dist/components/tool-result-dialog/tool-result-dialog.styles.js +1 -1
  183. package/dist/components/tool-result-view/registry.d.ts +0 -8
  184. package/dist/components/tool-result-view/registry.js +7 -2
  185. package/dist/components/tool-result-view/tool-result-view.d.ts +6 -3
  186. package/dist/components/tool-result-view/tool-result-view.js +18 -4
  187. package/dist/components/tool-select-dialog/tool-select-dialog.d.ts +11 -11
  188. package/dist/components/tool-select-dialog/tool-select-dialog.js +44 -133
  189. package/dist/components/tool-select-dialog/tool-select-dialog.styles.js +13 -4
  190. package/dist/components/tree/tree-node.d.ts +4 -1
  191. package/dist/components/tree/tree-node.js +4 -1
  192. package/dist/components/tree/tree.d.ts +17 -8
  193. package/dist/components/tree/tree.js +21 -20
  194. package/dist/components/typing-indicator/typing-indicator.d.ts +12 -2
  195. package/dist/components/typing-indicator/typing-indicator.js +16 -4
  196. package/dist/components/typing-indicator/typing-indicator.styles.js +2 -2
  197. package/dist/components/virtual-list/virtual-list.d.ts +56 -19
  198. package/dist/components/virtual-list/virtual-list.js +184 -38
  199. package/dist/components/virtual-list/virtual-list.styles.js +12 -0
  200. package/dist/components/widget/widget.d.ts +5 -4
  201. package/dist/components/widget/widget.js +54 -120
  202. package/dist/components/widget/widget.styles.js +8 -2
  203. package/dist/components/word-cloud/word-cloud-layout.d.ts +7 -0
  204. package/dist/components/word-cloud/word-cloud-layout.js +20 -6
  205. package/dist/components/word-cloud/word-cloud.d.ts +22 -2
  206. package/dist/components/word-cloud/word-cloud.js +45 -6
  207. package/dist/components/word-cloud/word-cloud.styles.js +13 -1
  208. package/dist/internal/anchored-validity.d.ts +1 -0
  209. package/dist/internal/anchored-validity.js +86 -0
  210. package/dist/internal/form-associated.d.ts +7 -0
  211. package/dist/internal/form-associated.js +43 -4
  212. package/dist/internal/numbers.d.ts +12 -0
  213. package/dist/internal/numbers.js +26 -0
  214. package/dist/internal/overlay-manager.d.ts +51 -0
  215. package/dist/internal/overlay-manager.js +523 -0
  216. package/dist/internal/positioner.js +27 -3
  217. package/dist/internal/safe-url.d.ts +12 -0
  218. package/dist/internal/safe-url.js +41 -0
  219. package/dist/internal/tokens.styles.js +17 -11
  220. package/dist/lyra.d.ts +5 -2
  221. package/dist/lyra.js +3 -1
  222. package/llms-full.txt +970 -426
  223. package/llms.txt +5 -4
  224. package/package.json +19 -2
@@ -71,10 +71,11 @@ export declare class LyraGenerationStatus extends LyraElement {
71
71
  /** Whether generation is currently in progress. The elapsed-time ticker
72
72
  * runs only while this is `true` (see the class doc). */
73
73
  active: boolean;
74
- /** Epoch-ms timestamp of when generation began. Optional -- when unset
75
- * while `active` is `true`, this component captures the current time
76
- * itself the moment `active` becomes `true` and counts from there instead
77
- * (see the class doc). */
74
+ /** Epoch-ms timestamp of when generation began. Optional -- when unset (or
75
+ * when set to a value that fails to parse as a finite number, e.g. an
76
+ * ISO-8601 date string) while `active` is `true`, this component captures
77
+ * the current time itself the moment `active` becomes `true` and counts
78
+ * from there instead (see the class doc). */
78
79
  startedAt?: number;
79
80
  /** Running token count so far. Omitted from the readout entirely (no
80
81
  * `tokens` segment) while unset. */
@@ -84,16 +85,23 @@ export declare class LyraGenerationStatus extends LyraElement {
84
85
  * itself once one is available -- see the class doc for the exact rule
85
86
  * and why. */
86
87
  tokensPerSecond?: number;
87
- /** Whether the built-in Stop button renders at all. */
88
+ /** Whether the built-in Stop button renders at all. Defaults to `true`.
89
+ * Uses {@link showStopConverter} rather than Lit's default presence-based
90
+ * `type: Boolean` handling, so a plain-HTML consumer with no way to write
91
+ * a `.showStop` property binding can still turn this off with
92
+ * `show-stop="false"`; a Lit template can do the same with either that
93
+ * attribute string or a `.showStop=${false}` property binding. */
88
94
  showStop: boolean;
89
95
  private elapsedMs;
90
96
  private tickTimer?;
91
97
  private fallbackStartMs?;
98
+ connectedCallback(): void;
92
99
  disconnectedCallback(): void;
93
100
  protected willUpdate(changed: PropertyValues): void;
94
101
  protected updated(changed: PropertyValues): void;
95
102
  private startTicker;
96
103
  private stopTicker;
104
+ private get validStartedAt();
97
105
  private computeElapsedMs;
98
106
  /** `tokens-per-second` when set; otherwise a derived figure once enough
99
107
  * elapsed time has accumulated for it to be meaningful; otherwise
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { html, nothing, svg } from 'lit';
7
+ import { html, nothing, svg, } from 'lit';
8
8
  import { property, state } from 'lit/decorators.js';
9
9
  import { LyraElement } from '../../internal/lyra-element.js';
10
10
  import { defineElement } from '../../internal/prefix.js';
@@ -71,6 +71,29 @@ function formatThroughput(value) {
71
71
  const rounded = clamped < 10 ? Math.round(clamped * 10) / 10 : Math.round(clamped);
72
72
  return `${rounded} tok/s`;
73
73
  }
74
+ /**
75
+ * String-aware boolean attribute converter for `show-stop`. Lit's built-in
76
+ * `type: Boolean` converter is presence-based -- the attribute's mere
77
+ * presence (regardless of its string value) maps to `true`, so a plain-
78
+ * markup consumer writing the literal `show-stop="false"` would actually get
79
+ * the button *shown*, the opposite of what that string reads as (the same
80
+ * bug class `<lyra-streaming-text>`'s `optionalBooleanConverter` and
81
+ * `<lyra-line-chart>`'s `WithoutBeginAtZero` story both document). Unlike
82
+ * `<lyra-streaming-text>`'s tri-state converter, this property's default is
83
+ * `true`, not "unset" -- so this one only needs two states: attribute absent
84
+ * (or removed) -> `true` (the default); `show-stop="false"` -> `false`;
85
+ * anything else present (no value, `="true"`, ...) -> `true`.
86
+ */
87
+ const showStopConverter = {
88
+ fromAttribute(value) {
89
+ return value !== 'false';
90
+ },
91
+ toAttribute(value) {
92
+ // `true` is this property's default, so there's nothing worth reflecting
93
+ // for it; only the non-default `false` needs an attribute at all.
94
+ return value ? null : 'false';
95
+ },
96
+ };
74
97
  /**
75
98
  * `<lyra-generation-status>` — a compact, ticking status readout shown
76
99
  * alongside an in-progress AI response: elapsed time, token count, and
@@ -143,7 +166,12 @@ export class LyraGenerationStatus extends LyraElement {
143
166
  /** Whether generation is currently in progress. The elapsed-time ticker
144
167
  * runs only while this is `true` (see the class doc). */
145
168
  this.active = false;
146
- /** Whether the built-in Stop button renders at all. */
169
+ /** Whether the built-in Stop button renders at all. Defaults to `true`.
170
+ * Uses {@link showStopConverter} rather than Lit's default presence-based
171
+ * `type: Boolean` handling, so a plain-HTML consumer with no way to write
172
+ * a `.showStop` property binding can still turn this off with
173
+ * `show-stop="false"`; a Lit template can do the same with either that
174
+ * attribute string or a `.showStop=${false}` property binding. */
147
175
  this.showStop = true;
148
176
  // Recomputed on activation and on every ~1s tick; frozen (not reset) once
149
177
  // `active` goes false -- see the class doc's "ticker" paragraph.
@@ -153,6 +181,21 @@ export class LyraGenerationStatus extends LyraElement {
153
181
  };
154
182
  }
155
183
  static { this.styles = [LyraElement.styles, styles]; }
184
+ // A re-parenting host (e.g. a virtualized/reordering list) disconnects and
185
+ // reconnects this element without ever toggling `active` -- `willUpdate`/
186
+ // `updated` only react to *changes* of `active`, so with no override here
187
+ // a still-active element would come back with `disconnectedCallback`'s
188
+ // cleared ticker never restarted, freezing the readout even though
189
+ // `active` (still `true`) says generation is ongoing. Restarting from
190
+ // `computeElapsedMs()` (rather than resuming a stale in-flight interval)
191
+ // matches how `startTicker()` already re-baselines on every fresh start.
192
+ connectedCallback() {
193
+ super.connectedCallback();
194
+ if (this.active) {
195
+ this.elapsedMs = this.computeElapsedMs();
196
+ this.startTicker();
197
+ }
198
+ }
156
199
  disconnectedCallback() {
157
200
  super.disconnectedCallback();
158
201
  this.stopTicker();
@@ -175,7 +218,7 @@ export class LyraGenerationStatus extends LyraElement {
175
218
  // first-update special case needed.
176
219
  willUpdate(changed) {
177
220
  if (changed.has('active') && this.active) {
178
- if (this.startedAt == null)
221
+ if (this.validStartedAt == null)
179
222
  this.fallbackStartMs = Date.now();
180
223
  this.elapsedMs = this.computeElapsedMs();
181
224
  }
@@ -214,8 +257,19 @@ export class LyraGenerationStatus extends LyraElement {
214
257
  this.tickTimer = undefined;
215
258
  }
216
259
  }
260
+ // `startedAt` is host-supplied and, unlike `tokenCount`/`tokensPerSecond`
261
+ // (both already guarded at their read sites), had no `Number.isFinite`
262
+ // check of its own -- a non-numeric `started-at` attribute (e.g. an
263
+ // ISO-8601 date string that failed the `type: Number` conversion, landing
264
+ // as `NaN`) would flow straight into `Date.now() - start`, permanently
265
+ // rendering the literal text "NaNm NaNs" with no fallback or recovery.
266
+ // Treating an invalid value the same as "unset" here restores this
267
+ // property's own documented fallback-clock behavior instead.
268
+ get validStartedAt() {
269
+ return this.startedAt != null && Number.isFinite(this.startedAt) ? this.startedAt : undefined;
270
+ }
217
271
  computeElapsedMs() {
218
- const start = this.startedAt ?? this.fallbackStartMs;
272
+ const start = this.validStartedAt ?? this.fallbackStartMs;
219
273
  return start == null ? 0 : Math.max(0, Date.now() - start);
220
274
  }
221
275
  /** `tokens-per-second` when set; otherwise a derived figure once enough
@@ -265,7 +319,7 @@ __decorate([
265
319
  property({ type: Number, attribute: 'tokens-per-second' })
266
320
  ], LyraGenerationStatus.prototype, "tokensPerSecond", void 0);
267
321
  __decorate([
268
- property({ type: Boolean, attribute: 'show-stop' })
322
+ property({ attribute: 'show-stop', converter: showStopConverter })
269
323
  ], LyraGenerationStatus.prototype, "showStop", void 0);
270
324
  __decorate([
271
325
  state()
@@ -4,6 +4,7 @@ import '../skeleton/skeleton.js';
4
4
  export interface GraphNode {
5
5
  id: string;
6
6
  label?: string;
7
+ /** Clamped to [6, 24] (a non-finite/missing value uses the midpoint, 15) — never rendered smaller/larger. */
7
8
  radius?: number;
8
9
  color?: string;
9
10
  }
@@ -19,12 +20,24 @@ export interface GraphLink {
19
20
  *
20
21
  * Set `seed` for a deterministic layout: node initial positions become
21
22
  * reproducible (keyed by node id) and the settle happens synchronously
22
- * instead of animating, like `prefers-reduced-motion`.
23
+ * instead of animating, like `prefers-reduced-motion`. `seed` only takes
24
+ * effect on the update that first populates `nodes`/`links` (or a later
25
+ * update that adds genuinely new node ids) — willUpdate() only reads it from
26
+ * inside rebuildSimulation(), which itself only ever assigns x/y to nodes
27
+ * that don't already have a settled position, so changing `seed` on an
28
+ * already-rendered graph is a no-op; nothing re-derives already-positioned
29
+ * nodes' x/y from the new value.
23
30
  *
24
31
  * @customElement lyra-graph
25
32
  * @event lyra-node-click - `detail: { id }`.
26
33
  * @event lyra-link-click - `detail: { source, target }`.
27
- * @csspart base, svg, node, link, label
34
+ * @csspart base - The graph wrapper.
35
+ * @csspart svg - The graph SVG.
36
+ * @csspart node - A graph node.
37
+ * @csspart link - A graph link.
38
+ * @csspart label - A node label.
39
+ * @csspart live-region - The current graph item announcement.
40
+ * @csspart data-list - A visually hidden list alternative for graph data.
28
41
  */
29
42
  export declare class LyraGraph extends LyraElement {
30
43
  static styles: import("lit").CSSResultGroup[];
@@ -38,12 +51,20 @@ export declare class LyraGraph extends LyraElement {
38
51
  maxZoom: number;
39
52
  /** When set, seeds each node's initial x/y deterministically (keyed by
40
53
  * node id, not array index) instead of forceSimulation()'s own random
41
- * start, and settles the simulation synchronously — see rebuildSimulation(). */
54
+ * start, and settles the simulation synchronously — see rebuildSimulation().
55
+ * Only takes effect on the update that first assigns a given node id an
56
+ * x/y (i.e. supplied at/before `nodes`/`links` first populate, or when a
57
+ * later update introduces new node ids) — changing `seed` afterwards does
58
+ * not retroactively reposition already-settled nodes; there is currently
59
+ * no way to make an already-rendered graph reproducible after the fact. */
42
60
  seed?: number;
43
61
  /** True until the lazy-loaded d3 peer dependencies have settled (success or failure). */
44
62
  private loading;
45
63
  private simNodes;
46
64
  private simLinks;
65
+ /** One roving tab stop across all nodes and links; nodes are the initial entry order. */
66
+ private activeGraphItem;
67
+ private graphLiveText;
47
68
  private simulation?;
48
69
  /** The live charge/link force objects, kept so chargeStrength/linkDistance
49
70
  * changes can retune them in place (see updated()) instead of requiring a
@@ -73,6 +94,13 @@ export declare class LyraGraph extends LyraElement {
73
94
  private linkEls;
74
95
  connectedCallback(): void;
75
96
  disconnectedCallback(): void;
97
+ /**
98
+ * A caller-supplied `radius` is clamped to [MIN_RADIUS, MAX_RADIUS] (and a
99
+ * non-finite/NaN value falls back to the same default average as an unset
100
+ * one) — an unclamped 0/negative radius would render an invisibly small
101
+ * `<circle>` that's still `role="button" tabindex="0"`, an invisible,
102
+ * focusable/clickable control with no visible focus indicator.
103
+ */
76
104
  private nodeRadius;
77
105
  protected willUpdate(changed: PropertyValues): void;
78
106
  protected updated(changed: PropertyValues): void;
@@ -114,6 +142,12 @@ export declare class LyraGraph extends LyraElement {
114
142
  private rebuildSimulation;
115
143
  private onNodeClick;
116
144
  private onLinkClick;
145
+ private graphItemCount;
146
+ private normalizedGraphItem;
147
+ private graphItemText;
148
+ private onGraphItemFocus;
149
+ private focusGraphItem;
150
+ private onGraphKeyDown;
117
151
  render(): TemplateResult;
118
152
  }
119
153
  declare global {
@@ -9,6 +9,7 @@ import { property, state } from 'lit/decorators.js';
9
9
  import { styleMap } from 'lit/directives/style-map.js';
10
10
  import { LyraElement } from '../../internal/lyra-element.js';
11
11
  import { defineElement } from '../../internal/prefix.js';
12
+ import { srOnly } from '../../internal/a11y.js';
12
13
  import { styles } from './graph.styles.js';
13
14
  import { loadD3 } from './graph-loader.js';
14
15
  import '../skeleton/skeleton.js';
@@ -67,12 +68,24 @@ function sanitizeNodeColor(color) {
67
68
  *
68
69
  * Set `seed` for a deterministic layout: node initial positions become
69
70
  * reproducible (keyed by node id) and the settle happens synchronously
70
- * instead of animating, like `prefers-reduced-motion`.
71
+ * instead of animating, like `prefers-reduced-motion`. `seed` only takes
72
+ * effect on the update that first populates `nodes`/`links` (or a later
73
+ * update that adds genuinely new node ids) — willUpdate() only reads it from
74
+ * inside rebuildSimulation(), which itself only ever assigns x/y to nodes
75
+ * that don't already have a settled position, so changing `seed` on an
76
+ * already-rendered graph is a no-op; nothing re-derives already-positioned
77
+ * nodes' x/y from the new value.
71
78
  *
72
79
  * @customElement lyra-graph
73
80
  * @event lyra-node-click - `detail: { id }`.
74
81
  * @event lyra-link-click - `detail: { source, target }`.
75
- * @csspart base, svg, node, link, label
82
+ * @csspart base - The graph wrapper.
83
+ * @csspart svg - The graph SVG.
84
+ * @csspart node - A graph node.
85
+ * @csspart link - A graph link.
86
+ * @csspart label - A node label.
87
+ * @csspart live-region - The current graph item announcement.
88
+ * @csspart data-list - A visually hidden list alternative for graph data.
76
89
  */
77
90
  export class LyraGraph extends LyraElement {
78
91
  constructor() {
@@ -89,6 +102,9 @@ export class LyraGraph extends LyraElement {
89
102
  this.loading = true;
90
103
  this.simNodes = [];
91
104
  this.simLinks = [];
105
+ /** One roving tab stop across all nodes and links; nodes are the initial entry order. */
106
+ this.activeGraphItem = 0;
107
+ this.graphLiveText = '';
92
108
  /** Node `<circle>`s already wired up with d3-drag; cleared on every simulation rebuild
93
109
  * so DOM elements Lit reuses across a rebuild get rebound to their fresh datum. */
94
110
  this.boundNodeEls = new WeakSet();
@@ -100,9 +116,22 @@ export class LyraGraph extends LyraElement {
100
116
  this.nodeLabelEls = [];
101
117
  this.linkEls = [];
102
118
  }
103
- static { this.styles = [LyraElement.styles, styles]; }
119
+ static { this.styles = [LyraElement.styles, styles, srOnly]; }
104
120
  connectedCallback() {
105
121
  super.connectedCallback();
122
+ // A reconnect (e.g. a drag-and-drop reparent that keeps this same
123
+ // element instance) fires disconnectedCallback then connectedCallback
124
+ // synchronously with no update in between — this.d3 is already set from
125
+ // the initial mount, and the live simulation/DOM are still intact
126
+ // (disconnectedCallback only stops the simulation's timer, it doesn't
127
+ // tear anything down). Redoing the lazy-load + rebuildSimulation() here
128
+ // would discard every already-settled node's position and restart the
129
+ // whole ~300-tick alpha=1 settle animation for no reason — just resume
130
+ // the existing simulation in place instead.
131
+ if (this.d3) {
132
+ this.simulation?.restart();
133
+ return;
134
+ }
106
135
  void loadD3().then((mods) => {
107
136
  this.loading = false;
108
137
  // The element may have been removed from the DOM while the dynamic
@@ -118,8 +147,16 @@ export class LyraGraph extends LyraElement {
118
147
  super.disconnectedCallback();
119
148
  this.simulation?.stop();
120
149
  }
150
+ /**
151
+ * A caller-supplied `radius` is clamped to [MIN_RADIUS, MAX_RADIUS] (and a
152
+ * non-finite/NaN value falls back to the same default average as an unset
153
+ * one) — an unclamped 0/negative radius would render an invisibly small
154
+ * `<circle>` that's still `role="button" tabindex="0"`, an invisible,
155
+ * focusable/clickable control with no visible focus indicator.
156
+ */
121
157
  nodeRadius(n) {
122
- return n.radius ?? (MIN_RADIUS + MAX_RADIUS) / 2;
158
+ const r = n.radius ?? (MIN_RADIUS + MAX_RADIUS) / 2;
159
+ return Number.isFinite(r) ? Math.min(MAX_RADIUS, Math.max(MIN_RADIUS, r)) : (MIN_RADIUS + MAX_RADIUS) / 2;
123
160
  }
124
161
  willUpdate(changed) {
125
162
  // rebuildSimulation() (re)assigns the simNodes/simLinks reactive
@@ -355,6 +392,69 @@ export class LyraGraph extends LyraElement {
355
392
  const target = typeof link.target === 'object' ? link.target.id : String(link.target);
356
393
  this.emit('lyra-link-click', { source, target });
357
394
  }
395
+ graphItemCount() {
396
+ return this.simNodes.length + this.simLinks.length;
397
+ }
398
+ normalizedGraphItem(index = this.activeGraphItem) {
399
+ const count = this.graphItemCount();
400
+ return count ? Math.min(Math.max(index, 0), count - 1) : -1;
401
+ }
402
+ graphItemText(index) {
403
+ if (index < this.simNodes.length) {
404
+ const node = this.simNodes[index];
405
+ return node ? `Node ${node.label ?? node.id}` : '';
406
+ }
407
+ const link = this.simLinks[index - this.simNodes.length];
408
+ if (!link)
409
+ return '';
410
+ const source = typeof link.source === 'object' ? link.source.label ?? link.source.id : String(link.source);
411
+ const target = typeof link.target === 'object' ? link.target.label ?? link.target.id : String(link.target);
412
+ return `Link from ${source} to ${target}`;
413
+ }
414
+ onGraphItemFocus(index) {
415
+ if (this.normalizedGraphItem(index) < 0)
416
+ return;
417
+ this.activeGraphItem = index;
418
+ this.graphLiveText = `${this.graphItemText(index)} (${index + 1} of ${this.graphItemCount()})`;
419
+ }
420
+ focusGraphItem(index) {
421
+ const normalized = this.normalizedGraphItem(index);
422
+ if (normalized < 0)
423
+ return;
424
+ this.activeGraphItem = normalized;
425
+ this.graphLiveText = `${this.graphItemText(normalized)} (${normalized + 1} of ${this.graphItemCount()})`;
426
+ void this.updateComplete.then(() => {
427
+ const items = [
428
+ ...Array.from(this.renderRoot.querySelectorAll('[part="node"]')),
429
+ ...Array.from(this.renderRoot.querySelectorAll('[part="link"]')),
430
+ ];
431
+ items[normalized]?.focus();
432
+ });
433
+ }
434
+ onGraphKeyDown(e, index, activate) {
435
+ if (e.key === 'Enter' || e.key === ' ') {
436
+ e.preventDefault();
437
+ this.onGraphItemFocus(index);
438
+ activate();
439
+ return;
440
+ }
441
+ const count = this.graphItemCount();
442
+ if (!count)
443
+ return;
444
+ let next = index;
445
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown')
446
+ next = Math.min(count - 1, index + 1);
447
+ else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp')
448
+ next = Math.max(0, index - 1);
449
+ else if (e.key === 'Home')
450
+ next = 0;
451
+ else if (e.key === 'End')
452
+ next = count - 1;
453
+ else
454
+ return;
455
+ e.preventDefault();
456
+ this.focusGraphItem(next);
457
+ }
358
458
  render() {
359
459
  if (this.loading) {
360
460
  return html `
@@ -373,15 +473,17 @@ export class LyraGraph extends LyraElement {
373
473
  role="group"
374
474
  aria-label="Node-link diagram with ${this.simNodes.length} nodes and ${this.simLinks.length} links"
375
475
  viewBox="0 0 ${this.width} ${this.height}"
476
+ tabindex=${this.graphItemCount() ? '-1' : '0'}
376
477
  >
377
478
  <g transform="">
378
- ${this.simLinks.map((l) => {
479
+ ${this.simLinks.map((l, linkIndex) => {
379
480
  const source = l.source;
380
481
  const target = l.target;
482
+ const itemIndex = this.simNodes.length + linkIndex;
381
483
  return svg `<line
382
484
  part="link"
383
485
  role="button"
384
- tabindex="0"
486
+ tabindex=${this.normalizedGraphItem() === itemIndex ? '0' : '-1'}
385
487
  aria-label="Link from ${source.label ?? source.id} to ${target.label ?? target.id}"
386
488
  stroke-width=${l.width ?? 1.5}
387
489
  x1=${source.x ?? 0}
@@ -389,33 +491,26 @@ export class LyraGraph extends LyraElement {
389
491
  x2=${target.x ?? 0}
390
492
  y2=${target.y ?? 0}
391
493
  @click=${() => this.onLinkClick(l)}
392
- @keydown=${(e) => {
393
- if (e.key === 'Enter' || e.key === ' ') {
394
- e.preventDefault();
395
- this.onLinkClick(l);
396
- }
397
- }}
494
+ @focus=${() => this.onGraphItemFocus(itemIndex)}
495
+ @keydown=${(e) => this.onGraphKeyDown(e, itemIndex, () => this.onLinkClick(l))}
398
496
  ></line>`;
399
497
  })}
400
- ${this.simNodes.map((n) => {
498
+ ${this.simNodes.map((n, nodeIndex) => {
401
499
  const fill = sanitizeNodeColor(n.color);
500
+ const itemIndex = nodeIndex;
402
501
  return svg `<g>
403
502
  <circle
404
503
  part="node"
405
504
  role="button"
406
- tabindex="0"
505
+ tabindex=${this.normalizedGraphItem() === itemIndex ? '0' : '-1'}
407
506
  aria-label=${n.label ?? n.id}
408
507
  r=${this.nodeRadius(n)}
409
508
  cx=${n.x ?? 0}
410
509
  cy=${n.y ?? 0}
411
510
  style=${styleMap(fill ? { '--lyra-node-fill': fill } : {})}
412
511
  @click=${() => this.onNodeClick(n)}
413
- @keydown=${(e) => {
414
- if (e.key === 'Enter' || e.key === ' ') {
415
- e.preventDefault();
416
- this.onNodeClick(n);
417
- }
418
- }}
512
+ @focus=${() => this.onGraphItemFocus(itemIndex)}
513
+ @keydown=${(e) => this.onGraphKeyDown(e, itemIndex, () => this.onNodeClick(n))}
419
514
  ></circle>
420
515
  ${n.label
421
516
  ? svg `<text part="label" aria-hidden="true" x=${(n.x ?? 0) + this.nodeRadius(n) + 2} y=${n.y ?? 0}>${n.label}</text>`
@@ -424,6 +519,17 @@ export class LyraGraph extends LyraElement {
424
519
  })}
425
520
  </g>
426
521
  </svg>
522
+ <div part="live-region" class="sr-only" role="status" aria-live="polite" aria-atomic="true">
523
+ ${this.graphLiveText || (this.normalizedGraphItem() >= 0 ? `${this.graphItemText(this.normalizedGraphItem())} (1 of ${this.graphItemCount()})` : '')}
524
+ </div>
525
+ <ul part="data-list" class="sr-only" aria-label="Graph data">
526
+ ${this.simNodes.map((node) => html `<li>Node ${node.label ?? node.id}</li>`)}
527
+ ${this.simLinks.map((link) => {
528
+ const source = link.source;
529
+ const target = link.target;
530
+ return html `<li>Link from ${source.label ?? source.id} to ${target.label ?? target.id}</li>`;
531
+ })}
532
+ </ul>
427
533
  </div>
428
534
  `;
429
535
  }
@@ -464,4 +570,10 @@ __decorate([
464
570
  __decorate([
465
571
  state()
466
572
  ], LyraGraph.prototype, "simLinks", void 0);
573
+ __decorate([
574
+ state()
575
+ ], LyraGraph.prototype, "activeGraphItem", void 0);
576
+ __decorate([
577
+ state()
578
+ ], LyraGraph.prototype, "graphLiveText", void 0);
467
579
  defineElement('graph', LyraGraph);
@@ -6,17 +6,26 @@ export interface CalendarDay {
6
6
  export interface CalendarCell {
7
7
  date: string;
8
8
  value: number;
9
- /** 0-based week column, counting from the grid's first (earliest-containing) Sunday. */
9
+ /** 0-based week column, counting from the grid's first (earliest-containing) anchor week — see `buildCalendarGrid()`'s `firstDayOfWeek` parameter. */
10
10
  week: number;
11
- /** 0 (Sunday) .. 6 (Saturday). */
11
+ /**
12
+ * Day-of-week position within the row, relative to `firstDayOfWeek`: 0 for
13
+ * the anchor weekday itself, up to 6 for the day immediately before it.
14
+ * With the default `firstDayOfWeek` of 0, this is the familiar 0 (Sunday)
15
+ * .. 6 (Saturday) numbering, unchanged from before `firstDayOfWeek` existed.
16
+ */
12
17
  weekday: number;
13
18
  }
14
19
  /** Parses a `yyyy-mm-dd` string as UTC midnight, avoiding local-timezone day-boundary drift. */
15
20
  export declare function parseIsoDate(iso: string): Date;
16
21
  /**
17
- * Lays `days` out onto a GitHub-style Sunday-Saturday x week grid. `days`
18
- * need not be sorted or contiguous; the grid spans from the earliest to the
19
- * latest date present, anchored at the Sunday on/before the earliest date.
22
+ * Lays `days` out onto a GitHub-style week grid, 7 rows deep. `days` need not
23
+ * be sorted or contiguous; the grid spans from the earliest to the latest
24
+ * date present, anchored at the `firstDayOfWeek`-weekday on/before the
25
+ * earliest date (`firstDayOfWeek` is 0–6, Sunday-first by default — same
26
+ * numbering as `CalendarCell.weekday`). With the default `firstDayOfWeek` of
27
+ * 0, this is the Sunday on/before the earliest date, exactly as before this
28
+ * parameter existed.
20
29
  *
21
30
  * An entry whose `date` doesn't parse to a valid calendar date (e.g. `''` or
22
31
  * `'2026-03'`) is dropped rather than included: `parseIsoDate` on a malformed
@@ -28,7 +37,7 @@ export declare function parseIsoDate(iso: string): Date;
28
37
  * over into March) is dropped rather than silently renormalized — see
29
38
  * `isCalendarValid()`.
30
39
  */
31
- export declare function buildCalendarGrid(days: CalendarDay[]): {
40
+ export declare function buildCalendarGrid(days: CalendarDay[], firstDayOfWeek?: number): {
32
41
  cells: CalendarCell[];
33
42
  weekCount: number;
34
43
  firstWeekStart: Date;
@@ -12,9 +12,13 @@ function isCalendarValid(iso, parsed) {
12
12
  return parsed.getUTCFullYear() === y && parsed.getUTCMonth() === m - 1 && parsed.getUTCDate() === d;
13
13
  }
14
14
  /**
15
- * Lays `days` out onto a GitHub-style Sunday-Saturday x week grid. `days`
16
- * need not be sorted or contiguous; the grid spans from the earliest to the
17
- * latest date present, anchored at the Sunday on/before the earliest date.
15
+ * Lays `days` out onto a GitHub-style week grid, 7 rows deep. `days` need not
16
+ * be sorted or contiguous; the grid spans from the earliest to the latest
17
+ * date present, anchored at the `firstDayOfWeek`-weekday on/before the
18
+ * earliest date (`firstDayOfWeek` is 0–6, Sunday-first by default — same
19
+ * numbering as `CalendarCell.weekday`). With the default `firstDayOfWeek` of
20
+ * 0, this is the Sunday on/before the earliest date, exactly as before this
21
+ * parameter existed.
18
22
  *
19
23
  * An entry whose `date` doesn't parse to a valid calendar date (e.g. `''` or
20
24
  * `'2026-03'`) is dropped rather than included: `parseIsoDate` on a malformed
@@ -26,7 +30,7 @@ function isCalendarValid(iso, parsed) {
26
30
  * over into March) is dropped rather than silently renormalized — see
27
31
  * `isCalendarValid()`.
28
32
  */
29
- export function buildCalendarGrid(days) {
33
+ export function buildCalendarGrid(days, firstDayOfWeek = 0) {
30
34
  const parsed = days
31
35
  .map((d) => ({ ...d, dateObj: parseIsoDate(d.date) }))
32
36
  .filter((d) => !Number.isNaN(d.dateObj.getTime()) && isCalendarValid(d.date, d.dateObj));
@@ -35,9 +39,16 @@ export function buildCalendarGrid(days) {
35
39
  }
36
40
  const timeBounds = minMax(parsed.map((d) => d.dateObj.getTime()));
37
41
  const min = new Date(timeBounds[0]);
38
- const firstWeekStart = new Date(Date.UTC(min.getUTCFullYear(), min.getUTCMonth(), min.getUTCDate() - min.getUTCDay()));
42
+ // Days back from `min` to the nearest `firstDayOfWeek`-weekday on/before
43
+ // it — with the default firstDayOfWeek of 0, this reduces to
44
+ // `min.getUTCDay()` exactly, i.e. today's original Sunday anchor.
45
+ const daysBackToAnchor = (min.getUTCDay() - firstDayOfWeek + 7) % 7;
46
+ const firstWeekStart = new Date(Date.UTC(min.getUTCFullYear(), min.getUTCMonth(), min.getUTCDate() - daysBackToAnchor));
39
47
  const cells = parsed.map((d) => {
40
- const weekday = d.dateObj.getUTCDay();
48
+ // Relative to the anchor weekday (see `CalendarCell.weekday`'s doc
49
+ // comment) — with the default firstDayOfWeek of 0 this is just
50
+ // `d.dateObj.getUTCDay()`, unchanged from before.
51
+ const weekday = (d.dateObj.getUTCDay() - firstDayOfWeek + 7) % 7;
41
52
  const daysSinceStart = Math.round((d.dateObj.getTime() - firstWeekStart.getTime()) / MS_PER_DAY);
42
53
  const week = Math.floor(daysSinceStart / 7);
43
54
  return { date: d.date, value: d.value, week, weekday };
@@ -1,4 +1,4 @@
1
- /** Maps `value` in `[lo, hi]` to a 0.11.0 alpha, matching the cd-heatmap ramp. */
1
+ /** Maps `value` in `[lo, hi]` to a 0.1-1.0 alpha so the lowest real value still reads as faintly present rather than invisible. */
2
2
  export declare function linearAlpha(value: number, lo: number, hi: number): number;
3
3
  /**
4
4
  * Computes `[min, max]` of `values` via a linear scan, or `null` for an
@@ -18,5 +18,9 @@ export declare function minMax(values: number[]): [number, number] | null;
18
18
  * (`drawMatrix()`) before `sqrtStep` is ever invoked, so a real `count === 0`
19
19
  * (e.g. "zero events that day") reaches this function and must bucket to the
20
20
  * lowest ramp step like any other legitimate value, not render as no-data.
21
+ * Likewise, `max <= 0` means every real value in the dataset is zero (a
22
+ * legitimate "zero events everywhere" dataset, not an absence of data) — the
23
+ * whole range collapses to a single point, so `count` (itself necessarily 0)
24
+ * buckets to the lowest step rather than being misread as no-data.
21
25
  */
22
26
  export declare function sqrtStep(count: number, max: number, steps: number): number;
@@ -1,4 +1,4 @@
1
- /** Maps `value` in `[lo, hi]` to a 0.11.0 alpha, matching the cd-heatmap ramp. */
1
+ /** Maps `value` in `[lo, hi]` to a 0.1-1.0 alpha so the lowest real value still reads as faintly present rather than invisible. */
2
2
  export function linearAlpha(value, lo, hi) {
3
3
  const span = hi - lo || 1;
4
4
  const t = (value - lo) / span;
@@ -35,10 +35,16 @@ export function minMax(values) {
35
35
  * (`drawMatrix()`) before `sqrtStep` is ever invoked, so a real `count === 0`
36
36
  * (e.g. "zero events that day") reaches this function and must bucket to the
37
37
  * lowest ramp step like any other legitimate value, not render as no-data.
38
+ * Likewise, `max <= 0` means every real value in the dataset is zero (a
39
+ * legitimate "zero events everywhere" dataset, not an absence of data) — the
40
+ * whole range collapses to a single point, so `count` (itself necessarily 0)
41
+ * buckets to the lowest step rather than being misread as no-data.
38
42
  */
39
43
  export function sqrtStep(count, max, steps) {
40
- if (count < 0 || max <= 0)
44
+ if (count < 0)
41
45
  return -1;
46
+ if (max <= 0)
47
+ return 0;
42
48
  const ratio = Math.sqrt(count) / Math.sqrt(max);
43
49
  return Math.min(steps - 1, Math.floor(ratio * steps));
44
50
  }