@sentropic/design-system-svelte 0.9.0 → 0.10.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 (68) hide show
  1. package/dist/AreaChart.svelte +6 -0
  2. package/dist/Button.svelte +59 -20
  3. package/dist/Card.svelte +13 -4
  4. package/dist/Checkbox.svelte +26 -3
  5. package/dist/Checkbox.svelte.d.ts.map +1 -1
  6. package/dist/Combobox.svelte +17 -6
  7. package/dist/DatePicker.svelte +17 -6
  8. package/dist/DonutChart.svelte +169 -0
  9. package/dist/DonutChart.svelte.d.ts +21 -0
  10. package/dist/DonutChart.svelte.d.ts.map +1 -0
  11. package/dist/Footer.svelte +124 -0
  12. package/dist/Footer.svelte.d.ts +21 -0
  13. package/dist/Footer.svelte.d.ts.map +1 -0
  14. package/dist/Highlight.svelte +63 -0
  15. package/dist/Highlight.svelte.d.ts +15 -0
  16. package/dist/Highlight.svelte.d.ts.map +1 -0
  17. package/dist/Input.svelte +50 -13
  18. package/dist/LanguageSelector.svelte +181 -0
  19. package/dist/LanguageSelector.svelte.d.ts +18 -0
  20. package/dist/LanguageSelector.svelte.d.ts.map +1 -0
  21. package/dist/Link.svelte +24 -10
  22. package/dist/MessageStatusBadge.svelte +11 -7
  23. package/dist/MessageStatusBadge.svelte.d.ts +2 -0
  24. package/dist/MessageStatusBadge.svelte.d.ts.map +1 -1
  25. package/dist/MultiSelect.svelte +17 -7
  26. package/dist/NumberInput.svelte +17 -6
  27. package/dist/OrderedList.svelte +103 -0
  28. package/dist/OrderedList.svelte.d.ts +17 -0
  29. package/dist/OrderedList.svelte.d.ts.map +1 -0
  30. package/dist/OverflowMenu.svelte +5 -2
  31. package/dist/PaginationNav.svelte +5 -2
  32. package/dist/PasswordInput.svelte +17 -6
  33. package/dist/Quote.svelte +72 -0
  34. package/dist/Quote.svelte.d.ts +16 -0
  35. package/dist/Quote.svelte.d.ts.map +1 -0
  36. package/dist/Radio.svelte +29 -1
  37. package/dist/ScatterPlot.svelte +179 -0
  38. package/dist/ScatterPlot.svelte.d.ts +21 -0
  39. package/dist/ScatterPlot.svelte.d.ts.map +1 -0
  40. package/dist/Search.svelte +17 -6
  41. package/dist/Select.svelte +57 -9
  42. package/dist/SkipLink.svelte +50 -0
  43. package/dist/SkipLink.svelte.d.ts +12 -0
  44. package/dist/SkipLink.svelte.d.ts.map +1 -0
  45. package/dist/StackedBarChart.svelte +190 -0
  46. package/dist/StackedBarChart.svelte.d.ts +22 -0
  47. package/dist/StackedBarChart.svelte.d.ts.map +1 -0
  48. package/dist/StreamingMessage.svelte +47 -0
  49. package/dist/StreamingMessage.svelte.d.ts +7 -0
  50. package/dist/StreamingMessage.svelte.d.ts.map +1 -1
  51. package/dist/Switch.svelte +5 -1
  52. package/dist/Tabs.svelte +37 -8
  53. package/dist/Textarea.svelte +47 -9
  54. package/dist/Tile.svelte +165 -0
  55. package/dist/Tile.svelte.d.ts +24 -0
  56. package/dist/Tile.svelte.d.ts.map +1 -0
  57. package/dist/Toggle.svelte +5 -1
  58. package/dist/Toggletip.svelte +5 -2
  59. package/dist/TreeView.svelte +213 -0
  60. package/dist/TreeView.svelte.d.ts +20 -0
  61. package/dist/TreeView.svelte.d.ts.map +1 -0
  62. package/dist/UnorderedList.svelte +4 -3
  63. package/dist/UnorderedList.svelte.d.ts +3 -2
  64. package/dist/UnorderedList.svelte.d.ts.map +1 -1
  65. package/dist/index.d.ts +19 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +11 -0
  68. package/package.json +4 -2
@@ -0,0 +1,103 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+
4
+ export type OrderedListInput = string | OrderedListItem;
5
+
6
+ export interface OrderedListItem {
7
+ content: string | Snippet;
8
+ /** Sous-items : chaînes ou objets (normalisés au rendu). */
9
+ children?: OrderedListInput[];
10
+ }
11
+ </script>
12
+
13
+ <script lang="ts">
14
+ import type { HTMLAttributes } from "svelte/elements";
15
+
16
+ type OrderedListProps = Omit<HTMLAttributes<HTMLOListElement>, "class"> & {
17
+ items: OrderedListInput[];
18
+ nested?: boolean;
19
+ class?: string;
20
+ };
21
+
22
+ let {
23
+ items,
24
+ nested = false,
25
+ class: className,
26
+ ...rest
27
+ }: OrderedListProps = $props();
28
+
29
+ const classes = () =>
30
+ ["st-orderedList", nested ? "st-orderedList--nested" : null, className]
31
+ .filter(Boolean)
32
+ .join(" ");
33
+
34
+ function normalize(item: OrderedListInput): OrderedListItem {
35
+ if (typeof item === "string") return { content: item };
36
+ return item;
37
+ }
38
+
39
+ function isSnippet(value: string | Snippet): value is Snippet {
40
+ return typeof value === "function";
41
+ }
42
+ </script>
43
+
44
+ <ol {...rest} class={classes()}>
45
+ {#each items as raw, index (index)}
46
+ {@const item = normalize(raw)}
47
+ <li class="st-orderedList__item">
48
+ {#if isSnippet(item.content)}
49
+ {@render item.content()}
50
+ {:else}
51
+ {item.content}
52
+ {/if}
53
+ {#if item.children && item.children.length > 0}
54
+ <ol class="st-orderedList st-orderedList--nested">
55
+ {#each item.children as childRaw, childIndex (childIndex)}
56
+ {@const child = normalize(childRaw)}
57
+ <li class="st-orderedList__item">
58
+ {#if isSnippet(child.content)}
59
+ {@render child.content()}
60
+ {:else}
61
+ {child.content}
62
+ {/if}
63
+ </li>
64
+ {/each}
65
+ </ol>
66
+ {/if}
67
+ </li>
68
+ {/each}
69
+ </ol>
70
+
71
+ <style>
72
+ .st-orderedList {
73
+ color: var(--st-semantic-text-primary);
74
+ counter-reset: st-ol;
75
+ display: grid;
76
+ gap: var(--st-spacing-1, 0.25rem);
77
+ list-style: none;
78
+ margin: 0;
79
+ padding: 0;
80
+ }
81
+
82
+ .st-orderedList--nested {
83
+ margin-block-start: var(--st-spacing-1, 0.25rem);
84
+ margin-inline-start: var(--st-spacing-4, 1rem);
85
+ }
86
+
87
+ .st-orderedList__item {
88
+ counter-increment: st-ol;
89
+ line-height: 1.5;
90
+ padding-inline-start: var(--st-spacing-8, 2rem);
91
+ position: relative;
92
+ }
93
+
94
+ .st-orderedList__item::before {
95
+ color: var(--st-semantic-text-secondary);
96
+ content: counter(st-ol) ".";
97
+ font-variant-numeric: tabular-nums;
98
+ inset-inline-start: 0;
99
+ position: absolute;
100
+ text-align: end;
101
+ width: var(--st-spacing-6, 1.5rem);
102
+ }
103
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from "svelte";
2
+ export type OrderedListInput = string | OrderedListItem;
3
+ export interface OrderedListItem {
4
+ content: string | Snippet;
5
+ /** Sous-items : chaînes ou objets (normalisés au rendu). */
6
+ children?: OrderedListInput[];
7
+ }
8
+ import type { HTMLAttributes } from "svelte/elements";
9
+ type OrderedListProps = Omit<HTMLAttributes<HTMLOListElement>, "class"> & {
10
+ items: OrderedListInput[];
11
+ nested?: boolean;
12
+ class?: string;
13
+ };
14
+ declare const OrderedList: import("svelte").Component<OrderedListProps, {}, "">;
15
+ type OrderedList = ReturnType<typeof OrderedList>;
16
+ export default OrderedList;
17
+ //# sourceMappingURL=OrderedList.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OrderedList.svelte.d.ts","sourceRoot":"","sources":["../src/lib/OrderedList.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,eAAe,CAAC;AAExD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC/B;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,gBAAgB,GAAG,IAAI,CAAC,cAAc,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC,GAAG;IACxE,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA4DJ,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -180,10 +180,13 @@
180
180
  );
181
181
  }
182
182
 
183
+ /* Focus = stratégie d'anatomie partagée (outline DSFR / inset Carbon / ring base). */
183
184
  .st-overflowMenu__trigger:focus-visible {
184
185
  border-color: var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
185
- box-shadow: 0 0 0 2px var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
186
- outline: none;
186
+ outline: var(--st-component-control-anatomy-focus-outline, none);
187
+ outline-offset: var(--st-component-control-anatomy-focus-offset, 0);
188
+ box-shadow: var(--st-component-control-anatomy-focus-boxShadow,
189
+ 0 0 0 2px var(--st-component-control-focusRing, var(--st-semantic-border-interactive)));
187
190
  }
188
191
 
189
192
  .st-overflowMenu__trigger[aria-expanded="true"] {
@@ -168,11 +168,14 @@
168
168
  );
169
169
  }
170
170
 
171
+ /* Focus = stratégie d'anatomie partagée (outline DSFR / inset Carbon / ring base). */
171
172
  .st-paginationNav__page:focus-visible,
172
173
  .st-paginationNav__nav:focus-visible {
173
174
  border-color: var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
174
- box-shadow: 0 0 0 2px var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
175
- outline: none;
175
+ outline: var(--st-component-control-anatomy-focus-outline, none);
176
+ outline-offset: var(--st-component-control-anatomy-focus-offset, 0);
177
+ box-shadow: var(--st-component-control-anatomy-focus-boxShadow,
178
+ 0 0 0 2px var(--st-component-control-focusRing, var(--st-semantic-border-interactive)));
176
179
  }
177
180
 
178
181
  .st-paginationNav__page--active {
@@ -89,8 +89,12 @@
89
89
  }
90
90
 
91
91
  .st-field__label {
92
- font-size: 0.875rem;
93
- font-weight: 600;
92
+ font-family: var(--st-component-field-labelTypography-family, inherit);
93
+ font-size: var(--st-component-field-labelTypography-size, 0.875rem);
94
+ font-weight: var(--st-component-field-labelTypography-weight, 600);
95
+ line-height: var(--st-component-field-labelTypography-lineHeight, 1.4);
96
+ letter-spacing: var(--st-component-field-labelTypography-letterSpacing, 0);
97
+ text-transform: var(--st-component-field-labelTypography-textTransform, none);
94
98
  }
95
99
 
96
100
  .st-field__help,
@@ -107,11 +111,15 @@
107
111
  color: var(--st-component-field-errorText, var(--st-semantic-feedback-error));
108
112
  }
109
113
 
114
+ /* Field box = resolved field anatomy (v1.2.0), same as Input. */
110
115
  .st-passwordInput {
111
116
  align-items: center;
112
- background: var(--st-component-control-background, var(--st-semantic-surface-default));
113
- border: 1px solid var(--st-component-control-border, var(--st-semantic-border-subtle));
114
- border-radius: var(--st-component-control-radius, 0.375rem);
117
+ background: var(--st-component-control-anatomy-field-fillBg, var(--st-component-control-background, var(--st-semantic-surface-default)));
118
+ border-top: var(--st-component-control-anatomy-field-borderTop, var(--st-component-control-anatomy-shape-borderWidth, 1px) var(--st-component-control-anatomy-shape-borderStyle, solid) var(--st-component-control-border, var(--st-semantic-border-subtle)));
119
+ border-right: var(--st-component-control-anatomy-field-borderRight, var(--st-component-control-anatomy-shape-borderWidth, 1px) var(--st-component-control-anatomy-shape-borderStyle, solid) var(--st-component-control-border, var(--st-semantic-border-subtle)));
120
+ border-bottom: var(--st-component-control-anatomy-field-borderBottom, var(--st-component-control-anatomy-shape-borderWidth, 1px) var(--st-component-control-anatomy-shape-borderStyle, solid) var(--st-component-control-border, var(--st-semantic-border-subtle)));
121
+ border-left: var(--st-component-control-anatomy-field-borderLeft, var(--st-component-control-anatomy-shape-borderWidth, 1px) var(--st-component-control-anatomy-shape-borderStyle, solid) var(--st-component-control-border, var(--st-semantic-border-subtle)));
122
+ border-radius: var(--st-component-control-anatomy-shape-radius, 0.375rem);
115
123
  color: var(--st-component-control-text, var(--st-semantic-text-primary));
116
124
  display: inline-flex;
117
125
  transition:
@@ -138,7 +146,10 @@
138
146
 
139
147
  .st-passwordInput:focus-within {
140
148
  border-color: var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
141
- box-shadow: 0 0 0 2px var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
149
+ outline: var(--st-component-control-anatomy-focus-outline, none);
150
+ outline-offset: var(--st-component-control-anatomy-focus-offset, 0);
151
+ box-shadow: var(--st-component-control-anatomy-focus-boxShadow,
152
+ 0 0 0 2px var(--st-component-control-focusRing, var(--st-semantic-border-interactive)));
142
153
  }
143
154
 
144
155
  .st-passwordInput:has([aria-invalid="true"]) {
@@ -0,0 +1,72 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ type QuoteProps = Omit<HTMLAttributes<HTMLElement>, "class" | "cite"> & {
6
+ /** Auteur de la citation. */
7
+ author?: string;
8
+ /** Source / référence (affichée après l'auteur). */
9
+ source?: string;
10
+ /** URL source (attribut cite du blockquote). */
11
+ cite?: string;
12
+ class?: string;
13
+ children?: Snippet;
14
+ };
15
+
16
+ let {
17
+ author,
18
+ source,
19
+ cite,
20
+ class: className,
21
+ children,
22
+ ...rest
23
+ }: QuoteProps = $props();
24
+
25
+ const classes = () => ["st-quote", className].filter(Boolean).join(" ");
26
+ const hasAttribution = () => Boolean(author || source);
27
+ </script>
28
+
29
+ <figure {...rest} class={classes()}>
30
+ <blockquote class="st-quote__text" {cite}>
31
+ {#if children}{@render children()}{/if}
32
+ </blockquote>
33
+ {#if hasAttribution()}
34
+ <figcaption class="st-quote__attribution">
35
+ {#if author}<span class="st-quote__author">{author}</span>{/if}
36
+ {#if source}<cite class="st-quote__source">{source}</cite>{/if}
37
+ </figcaption>
38
+ {/if}
39
+ </figure>
40
+
41
+ <style>
42
+ .st-quote {
43
+ border-left: 4px solid var(--st-semantic-action-primary);
44
+ margin: 0;
45
+ padding: var(--st-spacing-2, 0.5rem) 0 var(--st-spacing-2, 0.5rem) var(--st-spacing-4, 1rem);
46
+ }
47
+
48
+ .st-quote__text {
49
+ color: var(--st-semantic-text-primary);
50
+ font-size: 1.125rem;
51
+ line-height: 1.6;
52
+ margin: 0;
53
+ }
54
+
55
+ .st-quote__attribution {
56
+ color: var(--st-semantic-text-secondary);
57
+ font-size: 0.875rem;
58
+ margin-top: var(--st-spacing-2, 0.5rem);
59
+ }
60
+
61
+ .st-quote__author {
62
+ font-weight: 600;
63
+ }
64
+
65
+ .st-quote__author + .st-quote__source::before {
66
+ content: ", ";
67
+ }
68
+
69
+ .st-quote__source {
70
+ font-style: italic;
71
+ }
72
+ </style>
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ type QuoteProps = Omit<HTMLAttributes<HTMLElement>, "class" | "cite"> & {
4
+ /** Auteur de la citation. */
5
+ author?: string;
6
+ /** Source / référence (affichée après l'auteur). */
7
+ source?: string;
8
+ /** URL source (attribut cite du blockquote). */
9
+ cite?: string;
10
+ class?: string;
11
+ children?: Snippet;
12
+ };
13
+ declare const Quote: import("svelte").Component<QuoteProps, {}, "">;
14
+ type Quote = ReturnType<typeof Quote>;
15
+ export default Quote;
16
+ //# sourceMappingURL=Quote.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Quote.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Quote.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,UAAU,GAAG,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,GAAG;IACtE,6BAA6B;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAmCJ,QAAA,MAAM,KAAK,gDAAwC,CAAC;AACpD,KAAK,KAAK,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AACtC,eAAe,KAAK,CAAC"}
package/dist/Radio.svelte CHANGED
@@ -13,7 +13,7 @@
13
13
  </script>
14
14
 
15
15
  <label class={classes()}>
16
- <input {...rest} class="st-choice__input" type="radio" data-invalid={invalid ? "true" : undefined} />
16
+ <input {...rest} class="st-choice__input" type="radio" aria-invalid={invalid ? "true" : undefined} />
17
17
  <span class="st-choice__content">
18
18
  <span class="st-choice__label">{label}</span>
19
19
  {#if helperText}<span class="st-choice__help">{helperText}</span>{/if}
@@ -24,17 +24,45 @@
24
24
  .st-choice {
25
25
  align-items: start;
26
26
  color: var(--st-component-field-labelText, var(--st-semantic-text-primary));
27
+ cursor: var(--st-cursor-interactive, pointer);
27
28
  display: inline-grid;
28
29
  gap: 0.625rem;
29
30
  grid-template-columns: auto 1fr;
30
31
  }
31
32
 
32
33
  .st-choice__input {
34
+ /* Natif stylé : couleur de sélection thémée + taille + focus par stratégie
35
+ d'anatomie + états. Aucun widget custom, a11y native préservée. */
36
+ accent-color: var(--st-component-selection-checkedBackground, var(--st-semantic-action-primary));
37
+ cursor: inherit;
33
38
  height: 1rem;
34
39
  margin: 0.125rem 0 0;
35
40
  width: 1rem;
36
41
  }
37
42
 
43
+ /* Focus = stratégie d'anatomie partagée (outline DSFR / inset Carbon / ring base). */
44
+ .st-choice__input:focus-visible {
45
+ outline: var(--st-component-control-anatomy-focus-outline, none);
46
+ outline-offset: var(--st-component-control-anatomy-focus-offset, 0);
47
+ box-shadow: var(--st-component-control-anatomy-focus-boxShadow,
48
+ 0 0 0 2px var(--st-component-control-focusRing, var(--st-semantic-border-interactive)));
49
+ }
50
+
51
+ .st-choice__input[aria-invalid="true"] {
52
+ accent-color: var(--st-component-control-invalidBorder, var(--st-semantic-feedback-error));
53
+ outline-color: var(--st-component-control-invalidBorder, var(--st-semantic-feedback-error));
54
+ }
55
+
56
+ .st-choice__input:disabled {
57
+ cursor: var(--st-cursor-disabled, not-allowed);
58
+ opacity: 0.55;
59
+ }
60
+
61
+ .st-choice:has(.st-choice__input:disabled) {
62
+ color: var(--st-component-control-disabledText, var(--st-semantic-text-muted));
63
+ cursor: var(--st-cursor-disabled, not-allowed);
64
+ }
65
+
38
66
  .st-choice__content {
39
67
  display: grid;
40
68
  gap: 0.25rem;
@@ -0,0 +1,179 @@
1
+ <script lang="ts" module>
2
+ export type ScatterPlotTone =
3
+ | "category1" | "category2" | "category3" | "category4"
4
+ | "category5" | "category6" | "category7" | "category8";
5
+
6
+ export type ScatterPlotDatum = {
7
+ x: number;
8
+ y: number;
9
+ label?: string;
10
+ tone?: ScatterPlotTone;
11
+ };
12
+ </script>
13
+
14
+ <script lang="ts">
15
+ type ScatterPlotProps = {
16
+ data: ScatterPlotDatum[];
17
+ width?: number;
18
+ height?: number;
19
+ xLabel?: string;
20
+ yLabel?: string;
21
+ radius?: number;
22
+ label: string;
23
+ class?: string;
24
+ };
25
+
26
+ let {
27
+ data,
28
+ width = 480,
29
+ height = 280,
30
+ xLabel,
31
+ yLabel,
32
+ radius = 5,
33
+ label,
34
+ class: className
35
+ }: ScatterPlotProps = $props();
36
+
37
+ const MARGIN = { top: 14, right: 18, bottom: 36, left: 48 };
38
+
39
+ function niceTicks(min: number, max: number, target = 5): number[] {
40
+ if (!Number.isFinite(min) || !Number.isFinite(max) || min === max) {
41
+ return [Number.isFinite(max) ? max : 0];
42
+ }
43
+ const range = max - min;
44
+ const rough = range / Math.max(target - 1, 1);
45
+ const pow = Math.pow(10, Math.floor(Math.log10(rough)));
46
+ const norm = rough / pow;
47
+ let step: number;
48
+ if (norm < 1.5) step = pow;
49
+ else if (norm < 3) step = 2 * pow;
50
+ else if (norm < 7) step = 5 * pow;
51
+ else step = 10 * pow;
52
+ const start = Math.floor(min / step) * step;
53
+ const end = Math.ceil(max / step) * step;
54
+ const ticks: number[] = [];
55
+ for (let v = start; v <= end + step / 2; v += step) ticks.push(Number(v.toFixed(10)));
56
+ return ticks;
57
+ }
58
+
59
+ function scaleLinear(v: number, d0: number, d1: number, r0: number, r1: number) {
60
+ if (d1 === d0) return r0;
61
+ return r0 + ((v - d0) * (r1 - r0)) / (d1 - d0);
62
+ }
63
+
64
+ function fmt(v: number): string {
65
+ if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(v % 1000 === 0 ? 0 : 1)}k`;
66
+ return Number.isInteger(v) ? String(v) : v.toFixed(1);
67
+ }
68
+
69
+ let hoveredIndex: number | null = $state(null);
70
+
71
+ const TONES = ["category1","category2","category3","category4","category5","category6","category7","category8"] as const;
72
+
73
+ const scales = $derived.by(() => {
74
+ const xs = data.map((d) => d.x);
75
+ const ys = data.map((d) => d.y);
76
+ const xTicks = niceTicks(Math.min(...xs), Math.max(...xs));
77
+ const yTicks = niceTicks(Math.min(...ys), Math.max(...ys));
78
+ const plotW = Math.max(width - MARGIN.left - MARGIN.right, 1);
79
+ const plotH = Math.max(height - MARGIN.top - MARGIN.bottom, 1);
80
+ return {
81
+ xTicks, yTicks,
82
+ xMin: xTicks[0], xMax: xTicks[xTicks.length - 1],
83
+ yMin: yTicks[0], yMax: yTicks[yTicks.length - 1],
84
+ plotW, plotH
85
+ };
86
+ });
87
+
88
+ const points = $derived.by(() => {
89
+ const { xMin, xMax, yMin, yMax, plotW, plotH } = scales;
90
+ return data.map((d, i) => ({
91
+ cx: MARGIN.left + scaleLinear(d.x, xMin, xMax, 0, plotW),
92
+ cy: MARGIN.top + scaleLinear(d.y, yMin, yMax, plotH, 0),
93
+ datum: d,
94
+ tone: d.tone ?? TONES[i % TONES.length]
95
+ }));
96
+ });
97
+
98
+ const classes = () => ["st-scatterPlot", className].filter(Boolean).join(" ");
99
+ </script>
100
+
101
+ <div class={classes()} role="img" aria-label={label}>
102
+ <svg viewBox="0 0 {width} {height}" preserveAspectRatio="xMidYMid meet" width="100%" height="100%" focusable="false" aria-hidden="true">
103
+ <!-- gridlines + ticks Y -->
104
+ {#each scales.yTicks as t (t)}
105
+ {@const y = MARGIN.top + scaleLinear(t, scales.yMin, scales.yMax, scales.plotH, 0)}
106
+ <line class="st-scatterPlot__grid" x1={MARGIN.left} x2={width - MARGIN.right} y1={y} y2={y} />
107
+ <text class="st-scatterPlot__tick" x={MARGIN.left - 6} y={y} text-anchor="end" dominant-baseline="middle">{fmt(t)}</text>
108
+ {/each}
109
+ <!-- ticks X -->
110
+ {#each scales.xTicks as t (t)}
111
+ {@const x = MARGIN.left + scaleLinear(t, scales.xMin, scales.xMax, 0, scales.plotW)}
112
+ <text class="st-scatterPlot__tick" x={x} y={height - MARGIN.bottom + 16} text-anchor="middle">{fmt(t)}</text>
113
+ {/each}
114
+
115
+ <!-- axes -->
116
+ <line class="st-scatterPlot__axis" x1={MARGIN.left} x2={MARGIN.left} y1={MARGIN.top} y2={height - MARGIN.bottom} />
117
+ <line class="st-scatterPlot__axis" x1={MARGIN.left} x2={width - MARGIN.right} y1={height - MARGIN.bottom} y2={height - MARGIN.bottom} />
118
+
119
+ {#if xLabel}
120
+ <text class="st-scatterPlot__axisLabel" x={MARGIN.left + scales.plotW / 2} y={height - 4} text-anchor="middle">{xLabel}</text>
121
+ {/if}
122
+ {#if yLabel}
123
+ <text class="st-scatterPlot__axisLabel" x={12} y={MARGIN.top + scales.plotH / 2} text-anchor="middle" transform="rotate(-90 12 {MARGIN.top + scales.plotH / 2})">{yLabel}</text>
124
+ {/if}
125
+
126
+ <!-- points -->
127
+ {#each points as p, i (i)}
128
+ <circle
129
+ class="st-scatterPlot__point st-scatterPlot__point--{p.tone}"
130
+ cx={p.cx}
131
+ cy={p.cy}
132
+ r={radius}
133
+ tabindex="0"
134
+ role="img"
135
+ aria-label="{p.datum.label ? p.datum.label + ': ' : ''}x {p.datum.x}, y {p.datum.y}"
136
+ onmouseenter={() => (hoveredIndex = i)}
137
+ onmouseleave={() => (hoveredIndex = null)}
138
+ onfocus={() => (hoveredIndex = i)}
139
+ onblur={() => (hoveredIndex = null)}
140
+ />
141
+ {/each}
142
+ </svg>
143
+
144
+ {#if hoveredIndex !== null && points[hoveredIndex]}
145
+ {@const p = points[hoveredIndex]}
146
+ <div class="st-scatterPlot__tooltip" role="presentation" style="left: {(p.cx / width) * 100}%; top: {(p.cy / height) * 100}%">
147
+ {#if p.datum.label}<span class="st-scatterPlot__tooltipLabel">{p.datum.label}</span>{/if}
148
+ <span class="st-scatterPlot__tooltipValue">x {p.datum.x} · y {p.datum.y}</span>
149
+ </div>
150
+ {/if}
151
+ </div>
152
+
153
+ <style>
154
+ .st-scatterPlot { color: var(--st-semantic-text-secondary); display: block; font-family: inherit; position: relative; width: 100%; }
155
+ .st-scatterPlot svg { display: block; overflow: visible; }
156
+ .st-scatterPlot__grid { stroke: var(--st-semantic-border-subtle); stroke-dasharray: 2 3; stroke-width: 1; opacity: 0.7; }
157
+ .st-scatterPlot__axis { stroke: var(--st-semantic-border-subtle); stroke-width: 1; }
158
+ .st-scatterPlot__tick { fill: var(--st-semantic-text-secondary); font-size: 0.6875rem; }
159
+ .st-scatterPlot__axisLabel { fill: var(--st-semantic-text-secondary); font-size: 0.75rem; font-weight: 600; }
160
+ .st-scatterPlot__point { cursor: pointer; fill-opacity: 0.85; transition: fill-opacity 120ms ease, r 120ms ease; }
161
+ .st-scatterPlot__point:hover, .st-scatterPlot__point:focus-visible { fill-opacity: 1; }
162
+ .st-scatterPlot__point:focus-visible { outline: 2px solid var(--st-semantic-border-interactive); outline-offset: 1px; }
163
+ .st-scatterPlot__point--category1 { fill: var(--st-semantic-data-category1); }
164
+ .st-scatterPlot__point--category2 { fill: var(--st-semantic-data-category2); }
165
+ .st-scatterPlot__point--category3 { fill: var(--st-semantic-data-category3); }
166
+ .st-scatterPlot__point--category4 { fill: var(--st-semantic-data-category4); }
167
+ .st-scatterPlot__point--category5 { fill: var(--st-semantic-data-category5); }
168
+ .st-scatterPlot__point--category6 { fill: var(--st-semantic-data-category6); }
169
+ .st-scatterPlot__point--category7 { fill: var(--st-semantic-data-category7); }
170
+ .st-scatterPlot__point--category8 { fill: var(--st-semantic-data-category8); }
171
+ .st-scatterPlot__tooltip {
172
+ background: var(--st-semantic-surface-inverse); border-radius: var(--st-radius-sm, 0.25rem);
173
+ color: var(--st-semantic-text-inverse); display: inline-flex; flex-direction: column;
174
+ font-size: 0.75rem; gap: 0.125rem; line-height: 1.2; padding: 0.375rem 0.5rem;
175
+ pointer-events: none; position: absolute; transform: translate(-50%, calc(-100% - 8px)); white-space: nowrap; z-index: 1;
176
+ }
177
+ .st-scatterPlot__tooltipLabel { font-weight: 600; }
178
+ .st-scatterPlot__tooltipValue { opacity: 0.85; }
179
+ </style>
@@ -0,0 +1,21 @@
1
+ export type ScatterPlotTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
2
+ export type ScatterPlotDatum = {
3
+ x: number;
4
+ y: number;
5
+ label?: string;
6
+ tone?: ScatterPlotTone;
7
+ };
8
+ type ScatterPlotProps = {
9
+ data: ScatterPlotDatum[];
10
+ width?: number;
11
+ height?: number;
12
+ xLabel?: string;
13
+ yLabel?: string;
14
+ radius?: number;
15
+ label: string;
16
+ class?: string;
17
+ };
18
+ declare const ScatterPlot: import("svelte").Component<ScatterPlotProps, {}, "">;
19
+ type ScatterPlot = ReturnType<typeof ScatterPlot>;
20
+ export default ScatterPlot;
21
+ //# sourceMappingURL=ScatterPlot.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScatterPlot.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ScatterPlot.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,eAAe,GACvB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,eAAe,CAAC;CACxB,CAAC;AAEF,KAAK,gBAAgB,GAAG;IACtB,IAAI,EAAE,gBAAgB,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA6HJ,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -87,8 +87,12 @@
87
87
  }
88
88
 
89
89
  .st-field__label {
90
- font-size: 0.875rem;
91
- font-weight: 600;
90
+ font-family: var(--st-component-field-labelTypography-family, inherit);
91
+ font-size: var(--st-component-field-labelTypography-size, 0.875rem);
92
+ font-weight: var(--st-component-field-labelTypography-weight, 600);
93
+ line-height: var(--st-component-field-labelTypography-lineHeight, 1.4);
94
+ letter-spacing: var(--st-component-field-labelTypography-letterSpacing, 0);
95
+ text-transform: var(--st-component-field-labelTypography-textTransform, none);
92
96
  }
93
97
 
94
98
  .st-field__help,
@@ -105,11 +109,15 @@
105
109
  color: var(--st-component-field-errorText, var(--st-semantic-feedback-error));
106
110
  }
107
111
 
112
+ /* Field box = resolved field anatomy (v1.2.0), same as Input. */
108
113
  .st-search {
109
114
  align-items: center;
110
- background: var(--st-component-control-background, var(--st-semantic-surface-default));
111
- border: 1px solid var(--st-component-control-border, var(--st-semantic-border-subtle));
112
- border-radius: var(--st-component-control-radius, 0.375rem);
115
+ background: var(--st-component-control-anatomy-field-fillBg, var(--st-component-control-background, var(--st-semantic-surface-default)));
116
+ border-top: var(--st-component-control-anatomy-field-borderTop, var(--st-component-control-anatomy-shape-borderWidth, 1px) var(--st-component-control-anatomy-shape-borderStyle, solid) var(--st-component-control-border, var(--st-semantic-border-subtle)));
117
+ border-right: var(--st-component-control-anatomy-field-borderRight, var(--st-component-control-anatomy-shape-borderWidth, 1px) var(--st-component-control-anatomy-shape-borderStyle, solid) var(--st-component-control-border, var(--st-semantic-border-subtle)));
118
+ border-bottom: var(--st-component-control-anatomy-field-borderBottom, var(--st-component-control-anatomy-shape-borderWidth, 1px) var(--st-component-control-anatomy-shape-borderStyle, solid) var(--st-component-control-border, var(--st-semantic-border-subtle)));
119
+ border-left: var(--st-component-control-anatomy-field-borderLeft, var(--st-component-control-anatomy-shape-borderWidth, 1px) var(--st-component-control-anatomy-shape-borderStyle, solid) var(--st-component-control-border, var(--st-semantic-border-subtle)));
120
+ border-radius: var(--st-component-control-anatomy-shape-radius, 0.375rem);
113
121
  color: var(--st-component-control-text, var(--st-semantic-text-primary));
114
122
  display: inline-flex;
115
123
  transition:
@@ -136,7 +144,10 @@
136
144
 
137
145
  .st-search:focus-within {
138
146
  border-color: var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
139
- box-shadow: 0 0 0 2px var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
147
+ outline: var(--st-component-control-anatomy-focus-outline, none);
148
+ outline-offset: var(--st-component-control-anatomy-focus-offset, 0);
149
+ box-shadow: var(--st-component-control-anatomy-focus-boxShadow,
150
+ 0 0 0 2px var(--st-component-control-focusRing, var(--st-semantic-border-interactive)));
140
151
  }
141
152
 
142
153
  .st-search:has([aria-invalid="true"]) {