@purestack/ts-components 1.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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -0
  3. package/dist/ts-components.d.mts +1662 -0
  4. package/dist/ts-components.d.mts.map +1 -0
  5. package/dist/ts-components.mjs +9200 -0
  6. package/dist/ts-components.mjs.map +1 -0
  7. package/package.json +130 -0
  8. package/src/defineComponents.ts +87 -0
  9. package/src/index.ts +270 -0
  10. package/src/registerStyles.ts +63 -0
  11. package/src/standard/alertBox/alertBox.ts +64 -0
  12. package/src/standard/autoId.ts +25 -0
  13. package/src/standard/badge/badge.ts +41 -0
  14. package/src/standard/badge/badgeStyle.ts +21 -0
  15. package/src/standard/barChart/barChart.ts +338 -0
  16. package/src/standard/barChart/barChartStyle.ts +81 -0
  17. package/src/standard/btn/btn.ts +191 -0
  18. package/src/standard/btn/btnStyle.ts +59 -0
  19. package/src/standard/btnGroup/btnGroup.ts +208 -0
  20. package/src/standard/btnGroup/btnGroupStyle.ts +85 -0
  21. package/src/standard/chart/cartesianChart.ts +99 -0
  22. package/src/standard/chart/chartDefaults.ts +25 -0
  23. package/src/standard/chart/chartUtils.ts +84 -0
  24. package/src/standard/classicLogo/classicLogo.ts +429 -0
  25. package/src/standard/classicLogo/classicLogoStyle.ts +292 -0
  26. package/src/standard/componentVariant.ts +153 -0
  27. package/src/standard/composer/composer.ts +1035 -0
  28. package/src/standard/composer/composerCanvasInitialStyle.ts +225 -0
  29. package/src/standard/composer/composerCanvasRevertStyle.ts +14 -0
  30. package/src/standard/composer/composerCanvasSharedStyle.ts +60 -0
  31. package/src/standard/composer/composerCanvasStyleTypes.ts +8 -0
  32. package/src/standard/composer/composerCanvasThemeStyle.ts +53 -0
  33. package/src/standard/composer/composerStyle.ts +156 -0
  34. package/src/standard/composer/emailHtmlPolicy.ts +474 -0
  35. package/src/standard/consent/consent.ts +173 -0
  36. package/src/standard/consent/consentStyle.ts +44 -0
  37. package/src/standard/contactForm/contactForm.ts +135 -0
  38. package/src/standard/contactForm/contactFormStyle.ts +116 -0
  39. package/src/standard/doughnutChart/doughnutChart.ts +527 -0
  40. package/src/standard/doughnutChart/doughnutChartStyle.ts +64 -0
  41. package/src/standard/dropFiles/dropFiles.ts +480 -0
  42. package/src/standard/dropFiles/dropFilesStyle.ts +171 -0
  43. package/src/standard/expandablePanel/expandablePanel.ts +106 -0
  44. package/src/standard/expandablePanel/expandablePanelStyle.ts +56 -0
  45. package/src/standard/flex/flex.ts +269 -0
  46. package/src/standard/flex/flexStyle.ts +556 -0
  47. package/src/standard/footer/footer.ts +93 -0
  48. package/src/standard/footer/footerStyle.ts +39 -0
  49. package/src/standard/form/autoCompleteInput.ts +567 -0
  50. package/src/standard/form/form.ts +205 -0
  51. package/src/standard/form/formCheck.ts +96 -0
  52. package/src/standard/form/formInputField.ts +184 -0
  53. package/src/standard/form/formSelectField.ts +152 -0
  54. package/src/standard/form/formStyle.ts +585 -0
  55. package/src/standard/form/multiAutoCompleteInput.ts +880 -0
  56. package/src/standard/grid/grid.ts +177 -0
  57. package/src/standard/grid/gridStyle.ts +56 -0
  58. package/src/standard/icon/icon.ts +96 -0
  59. package/src/standard/icon/iconStyle.ts +50 -0
  60. package/src/standard/landing/codeShowcase.ts +54 -0
  61. package/src/standard/landing/comparisonColumn.ts +30 -0
  62. package/src/standard/landing/comparisonFeature.ts +22 -0
  63. package/src/standard/landing/comparisonTable.ts +38 -0
  64. package/src/standard/landing/ctaSection.ts +59 -0
  65. package/src/standard/landing/featureCard.ts +41 -0
  66. package/src/standard/landing/landing.ts +52 -0
  67. package/src/standard/landing/landingBand.ts +217 -0
  68. package/src/standard/landing/landingSection.ts +43 -0
  69. package/src/standard/landing/landingTypes.ts +141 -0
  70. package/src/standard/landing/metricItem.ts +25 -0
  71. package/src/standard/landing/metricStrip.ts +20 -0
  72. package/src/standard/lineChart/lineChart.ts +462 -0
  73. package/src/standard/lineChart/lineChartStyle.ts +91 -0
  74. package/src/standard/logo/logo.ts +192 -0
  75. package/src/standard/logo/logoStyle.ts +164 -0
  76. package/src/standard/modal/modal.ts +171 -0
  77. package/src/standard/modal/modalStore.ts +154 -0
  78. package/src/standard/modal/modalStyle.ts +190 -0
  79. package/src/standard/navMenu/navMenu.ts +278 -0
  80. package/src/standard/navMenu/navMenuStyle.ts +323 -0
  81. package/src/standard/pageLinks/pageLinks.ts +90 -0
  82. package/src/standard/pageScript/pageScript.ts +155 -0
  83. package/src/standard/pageToc/pageToc.ts +154 -0
  84. package/src/standard/pageToc/pageTocStyle.ts +372 -0
  85. package/src/standard/panel/panel.ts +55 -0
  86. package/src/standard/panel/panelStyle.ts +15 -0
  87. package/src/standard/pricing/pricing.ts +146 -0
  88. package/src/standard/searchBox/searchBox.ts +54 -0
  89. package/src/standard/searchBox/searchBoxStyle.ts +180 -0
  90. package/src/standard/sectionHeader/sectionHeader.ts +99 -0
  91. package/src/standard/signIn/signIn.ts +167 -0
  92. package/src/standard/signIn/signInStyle.ts +70 -0
  93. package/src/standard/tabs/tabs.ts +193 -0
  94. package/src/standard/tabs/tabsStyle.ts +231 -0
  95. package/src/standard/themeToggle/themeToggle.ts +33 -0
  96. package/src/standard/themeToggle/themeToggleStyle.ts +88 -0
  97. package/src/standard/toast/toastHost.ts +42 -0
  98. package/src/standard/toast/toastStore.ts +50 -0
  99. package/src/standard/topBar/topBar.ts +101 -0
  100. package/src/standard/topBar/topBarStyle.ts +169 -0
  101. package/src/standard/virtualList/variableVirtualTable.ts +371 -0
  102. package/src/standard/virtualList/virtualList.ts +423 -0
  103. package/src/standard/virtualList/virtualListStyle.ts +34 -0
  104. package/src/standard/virtualList/virtualTable.ts +179 -0
  105. package/src/standard/virtualList/virtualTableStyle.ts +24 -0
  106. package/tsconfig.json +23 -0
@@ -0,0 +1,527 @@
1
+ import type { SemanticTone } from '@purestack/ts-style'
2
+ import {
3
+ type ComputedRef,
4
+ computed,
5
+ defineComponent,
6
+ type RefOrValue,
7
+ svg,
8
+ unref,
9
+ } from 'regor'
10
+ import { createAutoId } from '../autoId'
11
+ import {
12
+ DEFAULT_CHART_COLORS,
13
+ DEFAULT_CHART_EMPTY_LABEL,
14
+ DEFAULT_CHART_SIZE,
15
+ } from '../chart/chartDefaults'
16
+ import {
17
+ clampChartNumber,
18
+ formatChartNumber,
19
+ formatChartPoint,
20
+ formatChartValue,
21
+ readChartNumber,
22
+ resolveChartCssSize,
23
+ resolveChartSize,
24
+ resolveChartText,
25
+ } from '../chart/chartUtils'
26
+ import type {
27
+ ComponentVariant,
28
+ ComponentVariantMode,
29
+ } from '../componentVariant'
30
+ import { resolveComponentClasses } from '../componentVariant'
31
+
32
+ export interface DoughnutChartSegment {
33
+ label?: RefOrValue<string>
34
+ value?: RefOrValue<number | string>
35
+ color?: RefOrValue<string>
36
+ }
37
+
38
+ export interface ResolvedDoughnutChartSegment {
39
+ label: string
40
+ value: number
41
+ percent: number
42
+ formattedValue: string
43
+ path: string
44
+ color: string
45
+ }
46
+
47
+ export interface ResolvedDoughnutChartSeparator {
48
+ path: string
49
+ }
50
+
51
+ export interface DoughnutChart {
52
+ segments?: RefOrValue<Array<RefOrValue<DoughnutChartSegment>>>
53
+ title?: RefOrValue<string>
54
+ description?: RefOrValue<string>
55
+ ariaLabel?: RefOrValue<string>
56
+ centerLabel?: RefOrValue<string>
57
+ centerValue?: RefOrValue<string>
58
+ centerLabelSize?: RefOrValue<number | string>
59
+ centerValueSize?: RefOrValue<number | string>
60
+ emptyLabel?: RefOrValue<string>
61
+ valueSuffix?: RefOrValue<string>
62
+ size?: RefOrValue<number | string>
63
+ thickness?: RefOrValue<number | string>
64
+ gap?: RefOrValue<number | string>
65
+ startAngle?: RefOrValue<number | string>
66
+ animated?: RefOrValue<boolean | string>
67
+ tone?: RefOrValue<SemanticTone>
68
+ variant?: RefOrValue<ComponentVariant>
69
+ variantMode?: RefOrValue<ComponentVariantMode>
70
+ classes?: ComputedRef<string>
71
+ resolvedSize?: ComputedRef<string | number>
72
+ resolvedAriaLabel?: ComputedRef<string>
73
+ resolvedEmptyLabel?: ComputedRef<string>
74
+ chartSegments?: ComputedRef<ResolvedDoughnutChartSegment[]>
75
+ separators?: ComputedRef<ResolvedDoughnutChartSeparator[]>
76
+ hasSeparators?: ComputedRef<boolean>
77
+ separatorMaskId?: string
78
+ separatorMaskUrl?: ComputedRef<string | undefined>
79
+ hasSegments?: ComputedRef<boolean>
80
+ total?: ComputedRef<number>
81
+ centerValueText?: ComputedRef<string>
82
+ centerLabelStyle?: ComputedRef<Record<string, string> | undefined>
83
+ centerValueStyle?: ComputedRef<Record<string, string> | undefined>
84
+ trackPath?: ComputedRef<string>
85
+ }
86
+
87
+ const CHART_CENTER = 50
88
+ const CHART_OUTER_RADIUS = 46
89
+ const DEFAULT_CHART_THICKNESS = 14
90
+ const DEFAULT_CHART_GAP = 2
91
+ const DEFAULT_CHART_START_ANGLE = -90
92
+ const FULL_CIRCLE = 360
93
+ const MAX_SEPARATOR_SEGMENT_SHARE = 0.15
94
+ const MIN_SEPARATOR_WIDTH = 0.25
95
+ const SEPARATOR_MASK_BLEED = 0.35
96
+ const resolveDoughnutChartMaskId = createAutoId('doughnut-chart-mask')
97
+
98
+ const doughnutChartTemplate = svg`<svg
99
+ class="doughnut-chart"
100
+ :class="classes"
101
+ viewBox="0 0 100 100"
102
+ :width="resolvedSize"
103
+ :height="resolvedSize"
104
+ role="img"
105
+ :aria-label="resolvedAriaLabel"
106
+ >
107
+ <title r-if="title">{{ title }}</title>
108
+ <desc r-if="description">{{ description }}</desc>
109
+ <defs r-if="hasSeparators">
110
+ <mask
111
+ class="doughnut-chart__separator-mask"
112
+ :id="separatorMaskId"
113
+ maskUnits="userSpaceOnUse"
114
+ x="0"
115
+ y="0"
116
+ width="100"
117
+ height="100"
118
+ >
119
+ <rect x="0" y="0" width="100" height="100" fill="white"/>
120
+ <path
121
+ r-for="separator in separators"
122
+ class="doughnut-chart__separator"
123
+ :d="separator.path"
124
+ fill="black"/>
125
+ </mask>
126
+ </defs>
127
+ <g class="doughnut-chart__plot" :mask="separatorMaskUrl">
128
+ <animate
129
+ r-if="animated"
130
+ attributeName="opacity"
131
+ from="0"
132
+ to="1"
133
+ dur="560ms"
134
+ fill="freeze"/>
135
+ <animateTransform
136
+ r-if="animated"
137
+ attributeName="transform"
138
+ type="rotate"
139
+ from="-90 50 50"
140
+ to="0 50 50"
141
+ dur="1760ms"
142
+ calcMode="spline"
143
+ keyTimes="0;1"
144
+ keySplines="0.16 1 0.3 1"
145
+ fill="freeze"/>
146
+ <path class="doughnut-chart__track" :d="trackPath" fill-rule="evenodd"/>
147
+ <g class="doughnut-chart__segments" r-if="hasSegments">
148
+ <path
149
+ r-for="segment in chartSegments"
150
+ class="doughnut-chart__segment"
151
+ :d="segment.path"
152
+ :fill="segment.color"
153
+ :aria-label="segment.label + ': ' + segment.formattedValue"
154
+ fill-rule="evenodd"
155
+ >
156
+ <title>{{ segment.label + ': ' + segment.formattedValue }}</title>
157
+ </path>
158
+ </g>
159
+ </g>
160
+ <g class="doughnut-chart__center" r-if="centerValueText || centerLabel">
161
+ <animate
162
+ r-if="animated"
163
+ attributeName="opacity"
164
+ from="0"
165
+ to="1"
166
+ dur="360ms"
167
+ fill="freeze"/>
168
+ <text
169
+ class="doughnut-chart__center-value"
170
+ x="50"
171
+ :y="centerLabel ? 48 : 52"
172
+ text-anchor="middle"
173
+ :style="centerValueStyle"
174
+ >
175
+ {{ centerValueText }}
176
+ </text>
177
+ <text
178
+ r-if="centerLabel"
179
+ class="doughnut-chart__center-label"
180
+ x="50"
181
+ y="61"
182
+ text-anchor="middle"
183
+ :style="centerLabelStyle"
184
+ >
185
+ {{ centerLabel }}
186
+ </text>
187
+ </g>
188
+ <text
189
+ r-if="!hasSegments && !centerValueText && !centerLabel"
190
+ class="doughnut-chart__empty"
191
+ x="50"
192
+ y="52"
193
+ text-anchor="middle"
194
+ >
195
+ {{ resolvedEmptyLabel }}
196
+ </text>
197
+ </svg>`
198
+
199
+ function defineDoughnutChartComponent() {
200
+ return defineComponent<DoughnutChart>(doughnutChartTemplate, {
201
+ props: [
202
+ 'segments',
203
+ 'title',
204
+ 'description',
205
+ 'ariaLabel',
206
+ 'centerLabel',
207
+ 'centerValue',
208
+ 'centerLabelSize',
209
+ 'centerValueSize',
210
+ 'emptyLabel',
211
+ 'valueSuffix',
212
+ 'size',
213
+ 'thickness',
214
+ 'gap',
215
+ 'startAngle',
216
+ 'animated',
217
+ 'tone',
218
+ 'variant',
219
+ 'variantMode',
220
+ ],
221
+ context: (head) => resolveDoughnutChart(head.props),
222
+ })
223
+ }
224
+
225
+ export function defineDoughnutChartComponents() {
226
+ return {
227
+ doughnutChart: defineDoughnutChartComponent(),
228
+ }
229
+ }
230
+
231
+ function resolveDoughnutChart(props: DoughnutChart): DoughnutChart {
232
+ const separatorMaskId = resolveDoughnutChartMaskId()
233
+ const thickness = computed(() =>
234
+ clampChartNumber(props.thickness, DEFAULT_CHART_THICKNESS, 2, 40),
235
+ )
236
+ const innerRadius = computed(() => CHART_OUTER_RADIUS - thickness())
237
+ const total = computed(() => resolveTotal(props.segments))
238
+ const chartSegments = computed(() =>
239
+ resolveChartSegments(props, total(), innerRadius()),
240
+ )
241
+ const separators = computed(() =>
242
+ resolveSeparators(props, chartSegments(), innerRadius()),
243
+ )
244
+
245
+ return {
246
+ animated: true,
247
+ ...props,
248
+ classes: computed(() =>
249
+ resolveComponentClasses(props, {
250
+ defaultVariant: 'none',
251
+ defaultVariantMode: 'stateless',
252
+ }),
253
+ ),
254
+ resolvedSize: computed(() =>
255
+ resolveChartSize(props.size, DEFAULT_CHART_SIZE),
256
+ ),
257
+ resolvedAriaLabel: computed(() => resolveAriaLabel(props)),
258
+ resolvedEmptyLabel: computed(
259
+ () => resolveChartText(props.emptyLabel) || DEFAULT_CHART_EMPTY_LABEL,
260
+ ),
261
+ total,
262
+ chartSegments,
263
+ separators,
264
+ hasSeparators: computed(() => separators().length > 0),
265
+ separatorMaskId,
266
+ separatorMaskUrl: computed(() =>
267
+ separators().length > 0 ? `url(#${separatorMaskId})` : undefined,
268
+ ),
269
+ hasSegments: computed(() => chartSegments().length > 0),
270
+ centerValueText: computed(() => resolveCenterValue(props, total())),
271
+ centerLabelStyle: computed<Record<string, string> | undefined>(() =>
272
+ resolveFontSizeStyle(props.centerLabelSize),
273
+ ),
274
+ centerValueStyle: computed<Record<string, string> | undefined>(() =>
275
+ resolveFontSizeStyle(props.centerValueSize),
276
+ ),
277
+ trackPath: computed(() => fullRingPath(CHART_OUTER_RADIUS, innerRadius())),
278
+ }
279
+ }
280
+
281
+ function resolveChartSegments(
282
+ props: DoughnutChart,
283
+ total: number,
284
+ innerRadius: number,
285
+ ): ResolvedDoughnutChartSegment[] {
286
+ if (total <= 0) return []
287
+
288
+ const startAngle = readChartNumber(
289
+ props.startAngle,
290
+ DEFAULT_CHART_START_ANGLE,
291
+ )
292
+ const valueSuffix = resolveChartText(props.valueSuffix)
293
+ let cursor = startAngle
294
+
295
+ return resolvePositiveSegments(props.segments).map((segment, index) => {
296
+ const span = (segment.value / total) * FULL_CIRCLE
297
+ const segmentStart = cursor
298
+ const segmentEnd = cursor + span
299
+ cursor += span
300
+
301
+ return {
302
+ ...segment,
303
+ percent: segment.value / total,
304
+ formattedValue: formatSegmentValue(segment.value, valueSuffix),
305
+ color:
306
+ segment.color ||
307
+ DEFAULT_CHART_COLORS[index % DEFAULT_CHART_COLORS.length],
308
+ path:
309
+ segmentEnd - segmentStart >= FULL_CIRCLE - 0.001
310
+ ? fullRingPath(CHART_OUTER_RADIUS, innerRadius)
311
+ : ringSegmentPath(
312
+ CHART_OUTER_RADIUS,
313
+ innerRadius,
314
+ segmentStart,
315
+ segmentEnd,
316
+ ),
317
+ }
318
+ })
319
+ }
320
+
321
+ function resolveSeparators(
322
+ props: DoughnutChart,
323
+ segments: ResolvedDoughnutChartSegment[],
324
+ innerRadius: number,
325
+ ): ResolvedDoughnutChartSeparator[] {
326
+ if (segments.length < 2) return []
327
+
328
+ const gap = clampChartNumber(props.gap, DEFAULT_CHART_GAP, 0, 24)
329
+ if (gap <= 0) return []
330
+
331
+ let cursor = readChartNumber(props.startAngle, DEFAULT_CHART_START_ANGLE)
332
+ return segments.flatMap((segment, index) => {
333
+ const previousSegment = segments.at(index - 1) || segment
334
+ const width = resolveAdaptiveSeparatorWidth(
335
+ gap,
336
+ innerRadius,
337
+ CHART_OUTER_RADIUS,
338
+ previousSegment,
339
+ segment,
340
+ )
341
+ if (width < MIN_SEPARATOR_WIDTH) {
342
+ cursor += segment.percent * FULL_CIRCLE
343
+ return []
344
+ }
345
+
346
+ const path = separatorPath(
347
+ cursor,
348
+ Math.max(0, innerRadius - SEPARATOR_MASK_BLEED),
349
+ CHART_OUTER_RADIUS + SEPARATOR_MASK_BLEED,
350
+ width,
351
+ )
352
+ cursor += segment.percent * FULL_CIRCLE
353
+ return [{ path }]
354
+ })
355
+ }
356
+
357
+ function resolveAdaptiveSeparatorWidth(
358
+ maxWidth: number,
359
+ innerRadius: number,
360
+ outerRadius: number,
361
+ previousSegment: ResolvedDoughnutChartSegment,
362
+ nextSegment: ResolvedDoughnutChartSegment,
363
+ ) {
364
+ const smallerSegmentSpan =
365
+ Math.min(previousSegment.percent, nextSegment.percent) * FULL_CIRCLE
366
+ const maxSeparatorAngle = smallerSegmentSpan * MAX_SEPARATOR_SEGMENT_SHARE
367
+ const midRadius = (innerRadius + outerRadius) / 2
368
+ const adaptiveWidth = chordWidth(midRadius, maxSeparatorAngle)
369
+ return Math.min(maxWidth, adaptiveWidth)
370
+ }
371
+
372
+ function chordWidth(radius: number, angle: number) {
373
+ if (radius <= 0 || angle <= 0) return 0
374
+ const radians = (angle * Math.PI) / 180
375
+ return 2 * radius * Math.sin(radians / 2)
376
+ }
377
+
378
+ function separatorPath(
379
+ angle: number,
380
+ innerRadius: number,
381
+ outerRadius: number,
382
+ width: number,
383
+ ) {
384
+ const halfWidth = Math.min(
385
+ width / 2,
386
+ innerRadius - 0.001,
387
+ outerRadius - 0.001,
388
+ )
389
+ if (halfWidth <= 0) return ''
390
+
391
+ const radians = (angle * Math.PI) / 180
392
+ const ux = Math.cos(radians)
393
+ const uy = Math.sin(radians)
394
+ const px = -uy
395
+ const py = ux
396
+ const outerDistance = Math.sqrt(outerRadius ** 2 - halfWidth ** 2)
397
+ const innerDistance = Math.sqrt(innerRadius ** 2 - halfWidth ** 2)
398
+ const outerStart = offsetPoint(ux, uy, px, py, outerDistance, halfWidth)
399
+ const outerEnd = offsetPoint(ux, uy, px, py, outerDistance, -halfWidth)
400
+ const innerEnd = offsetPoint(ux, uy, px, py, innerDistance, -halfWidth)
401
+ const innerStart = offsetPoint(ux, uy, px, py, innerDistance, halfWidth)
402
+
403
+ return [
404
+ `M ${formatPoint(outerStart)}`,
405
+ `A ${fmt(outerRadius)} ${fmt(outerRadius)} 0 0 0 ${formatPoint(outerEnd)}`,
406
+ `L ${formatPoint(innerEnd)}`,
407
+ `A ${fmt(innerRadius)} ${fmt(innerRadius)} 0 0 1 ${formatPoint(innerStart)}`,
408
+ 'Z',
409
+ ].join(' ')
410
+ }
411
+
412
+ function offsetPoint(
413
+ ux: number,
414
+ uy: number,
415
+ px: number,
416
+ py: number,
417
+ radialDistance: number,
418
+ offset: number,
419
+ ) {
420
+ return {
421
+ x: CHART_CENTER + ux * radialDistance + px * offset,
422
+ y: CHART_CENTER + uy * radialDistance + py * offset,
423
+ }
424
+ }
425
+
426
+ function resolvePositiveSegments(
427
+ segments: DoughnutChart['segments'],
428
+ ): Array<
429
+ Omit<ResolvedDoughnutChartSegment, 'percent' | 'formattedValue' | 'path'>
430
+ > {
431
+ return (unref(segments) || [])
432
+ .map((segmentInput, index) => {
433
+ const segment = unref(segmentInput)
434
+ const value = readChartNumber(segment?.value, 0)
435
+ return {
436
+ label: resolveChartText(segment?.label) || `Segment ${index + 1}`,
437
+ value: value > 0 ? value : 0,
438
+ color: resolveChartText(segment?.color),
439
+ }
440
+ })
441
+ .filter((segment) => segment.value > 0)
442
+ }
443
+
444
+ function resolveTotal(segments: DoughnutChart['segments']) {
445
+ return resolvePositiveSegments(segments).reduce(
446
+ (total, segment) => total + segment.value,
447
+ 0,
448
+ )
449
+ }
450
+
451
+ function resolveCenterValue(props: DoughnutChart, total: number) {
452
+ const explicit = resolveChartText(props.centerValue)
453
+ if (explicit) return explicit
454
+ if (total <= 0) return ''
455
+ return formatSegmentValue(total, resolveChartText(props.valueSuffix))
456
+ }
457
+
458
+ function resolveAriaLabel(props: DoughnutChart) {
459
+ return (
460
+ resolveChartText(props.ariaLabel) ||
461
+ resolveChartText(props.title) ||
462
+ 'Doughnut chart'
463
+ )
464
+ }
465
+
466
+ function resolveFontSizeStyle(size?: RefOrValue<number | string>) {
467
+ const fontSize = resolveChartCssSize(size)
468
+ return fontSize ? { fontSize } : undefined
469
+ }
470
+
471
+ function fullRingPath(outerRadius: number, innerRadius: number) {
472
+ const topOuter = pointAtAngle(outerRadius, -90)
473
+ const bottomOuter = pointAtAngle(outerRadius, 90)
474
+ const topInner = pointAtAngle(innerRadius, -90)
475
+ const bottomInner = pointAtAngle(innerRadius, 90)
476
+
477
+ return [
478
+ `M ${formatPoint(topOuter)}`,
479
+ `A ${fmt(outerRadius)} ${fmt(outerRadius)} 0 1 1 ${formatPoint(bottomOuter)}`,
480
+ `A ${fmt(outerRadius)} ${fmt(outerRadius)} 0 1 1 ${formatPoint(topOuter)}`,
481
+ `M ${formatPoint(topInner)}`,
482
+ `A ${fmt(innerRadius)} ${fmt(innerRadius)} 0 1 0 ${formatPoint(bottomInner)}`,
483
+ `A ${fmt(innerRadius)} ${fmt(innerRadius)} 0 1 0 ${formatPoint(topInner)}`,
484
+ 'Z',
485
+ ].join(' ')
486
+ }
487
+
488
+ function ringSegmentPath(
489
+ outerRadius: number,
490
+ innerRadius: number,
491
+ startAngle: number,
492
+ endAngle: number,
493
+ ) {
494
+ const outerStart = pointAtAngle(outerRadius, startAngle)
495
+ const outerEnd = pointAtAngle(outerRadius, endAngle)
496
+ const innerEnd = pointAtAngle(innerRadius, endAngle)
497
+ const innerStart = pointAtAngle(innerRadius, startAngle)
498
+ const largeArc = endAngle - startAngle > 180 ? 1 : 0
499
+
500
+ return [
501
+ `M ${formatPoint(outerStart)}`,
502
+ `A ${fmt(outerRadius)} ${fmt(outerRadius)} 0 ${largeArc} 1 ${formatPoint(outerEnd)}`,
503
+ `L ${formatPoint(innerEnd)}`,
504
+ `A ${fmt(innerRadius)} ${fmt(innerRadius)} 0 ${largeArc} 0 ${formatPoint(innerStart)}`,
505
+ 'Z',
506
+ ].join(' ')
507
+ }
508
+
509
+ function pointAtAngle(radius: number, angle: number) {
510
+ const radians = (angle * Math.PI) / 180
511
+ return {
512
+ x: CHART_CENTER + radius * Math.cos(radians),
513
+ y: CHART_CENTER + radius * Math.sin(radians),
514
+ }
515
+ }
516
+
517
+ function formatPoint(point: { x: number; y: number }) {
518
+ return formatChartPoint(point, { precision: 3 })
519
+ }
520
+
521
+ function formatSegmentValue(value: number, suffix: string) {
522
+ return formatChartValue(value, suffix, { precision: 3 })
523
+ }
524
+
525
+ function fmt(value: number) {
526
+ return formatChartNumber(value, { precision: 3 })
527
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ styleBuilder,
3
+ type ThemeMode,
4
+ type ThemePalette,
5
+ themes,
6
+ } from '@purestack/ts-style'
7
+
8
+ export function registerDoughnutChartStyles() {
9
+ themes.forEach((theme, palette) => {
10
+ registerDoughnutChartBaseStyles(theme)
11
+ registerDoughnutChartPaintStyles(theme, palette)
12
+ })
13
+ }
14
+
15
+ function registerDoughnutChartBaseStyles(theme: ThemeMode) {
16
+ styleBuilder
17
+ .select('.doughnut-chart__segment', theme)
18
+ .transition('opacity 180ms ease, filter 180ms ease, transform 180ms ease')
19
+ .transformOrigin('50px 50px')
20
+
21
+ styleBuilder
22
+ .select('.doughnut-chart__segments:hover .doughnut-chart__segment', theme)
23
+ .opacity('0.72')
24
+
25
+ styleBuilder
26
+ .select(
27
+ '.doughnut-chart__segments:hover .doughnut-chart__segment:hover',
28
+ theme,
29
+ )
30
+ .opacity('1')
31
+ .filter('brightness(1.08)')
32
+
33
+ styleBuilder
34
+ .select('.doughnut-chart__center, .doughnut-chart__empty', theme)
35
+ .pointerEvents('none')
36
+ }
37
+
38
+ function registerDoughnutChartPaintStyles(
39
+ theme: ThemeMode,
40
+ palette: ThemePalette,
41
+ ) {
42
+ styleBuilder
43
+ .select('.doughnut-chart__track', theme)
44
+ .fill(palette.current.surfaceAlt.rest.bgcolor)
45
+
46
+ styleBuilder
47
+ .select('.doughnut-chart__center-value', theme)
48
+ .fill(palette.current.text.default)
49
+ .apply(palette.applyFont(palette.font.size.body, palette.font.weight.w700))
50
+ .set('dominant-baseline', 'middle')
51
+
52
+ styleBuilder
53
+ .select('.doughnut-chart__center-label', theme)
54
+ .fill(palette.current.text.subtle)
55
+ .apply(palette.applyFont(palette.font.size.xxxs, palette.font.weight.w700))
56
+ .textTransform('uppercase')
57
+ .set('dominant-baseline', 'middle')
58
+
59
+ styleBuilder
60
+ .select('.doughnut-chart__empty', theme)
61
+ .fill(palette.current.text.subtle)
62
+ .apply(palette.applyFont(palette.font.size.xxs, palette.font.weight.w700))
63
+ .set('dominant-baseline', 'middle')
64
+ }