@globalbrain/sefirot 4.12.0 → 4.14.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.
- package/config/vite.js +3 -1
- package/lib/components/SChartBar.vue +384 -0
- package/lib/components/SChartPie.vue +379 -0
- package/lib/components/STooltip.vue +1 -1
- package/lib/composables/Error.ts +12 -12
- package/lib/composables/Lang.ts +3 -3
- package/lib/http/Http.ts +12 -12
- package/lib/styles/base.css +8 -0
- package/lib/styles/utilities.css +9 -0
- package/lib/support/Chart.ts +92 -0
- package/lib/support/Utils.ts +1 -1
- package/package.json +27 -23
package/config/vite.js
CHANGED
|
@@ -57,6 +57,7 @@ export const baseConfig = {
|
|
|
57
57
|
'@vueuse/core',
|
|
58
58
|
'body-scroll-lock',
|
|
59
59
|
'dayjs',
|
|
60
|
+
'd3',
|
|
60
61
|
'file-saver',
|
|
61
62
|
'fuse.js',
|
|
62
63
|
'lodash-es',
|
|
@@ -80,7 +81,8 @@ export const baseConfig = {
|
|
|
80
81
|
'dayjs/plugin/timezone',
|
|
81
82
|
'dayjs/plugin/utc',
|
|
82
83
|
'markdown-it > argparse',
|
|
83
|
-
'markdown-it > entities'
|
|
84
|
+
'markdown-it > entities',
|
|
85
|
+
'file-saver'
|
|
84
86
|
],
|
|
85
87
|
exclude: [
|
|
86
88
|
'markdown-it'
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useElementSize } from '@vueuse/core'
|
|
3
|
+
import * as d3 from 'd3'
|
|
4
|
+
import { useTemplateRef, watch } from 'vue'
|
|
5
|
+
import { type ChartColor, type KV, type Margins, c, scheme } from '../support/Chart'
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(defineProps<{
|
|
8
|
+
// State
|
|
9
|
+
data: KV[]
|
|
10
|
+
|
|
11
|
+
// Chart appearance
|
|
12
|
+
type?: 'horizontal' | 'vertical'
|
|
13
|
+
colors?: ChartColor[]
|
|
14
|
+
margins?: Margins
|
|
15
|
+
maxBandwidth?: number
|
|
16
|
+
|
|
17
|
+
// Axis & labels
|
|
18
|
+
xLabel?: string
|
|
19
|
+
yLabel?: string
|
|
20
|
+
xLabelOffset?: number
|
|
21
|
+
yLabelOffset?: number
|
|
22
|
+
xLabelFontSize?: string
|
|
23
|
+
yLabelFontSize?: string
|
|
24
|
+
ticks?: number
|
|
25
|
+
tickFontSize?: string
|
|
26
|
+
|
|
27
|
+
// Tooltip & interactivity
|
|
28
|
+
tooltip?: boolean
|
|
29
|
+
tooltipFormat?: (d: KV, color: string) => string
|
|
30
|
+
|
|
31
|
+
// Animation & debug
|
|
32
|
+
animate?: boolean
|
|
33
|
+
debug?: boolean
|
|
34
|
+
}>(), {
|
|
35
|
+
type: 'vertical',
|
|
36
|
+
colors: () => ['blue'],
|
|
37
|
+
maxBandwidth: 100,
|
|
38
|
+
|
|
39
|
+
xLabelFontSize: '14px',
|
|
40
|
+
yLabelFontSize: '14px',
|
|
41
|
+
ticks: 5,
|
|
42
|
+
tickFontSize: '14px',
|
|
43
|
+
|
|
44
|
+
tooltip: true,
|
|
45
|
+
tooltipFormat: (d: KV) => `${d.key} – ${d.value}`,
|
|
46
|
+
|
|
47
|
+
animate: true,
|
|
48
|
+
debug: false
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const chartRef = useTemplateRef('chart')
|
|
52
|
+
const { width, height } = useElementSize(chartRef)
|
|
53
|
+
|
|
54
|
+
let tooltipTimeout = 0
|
|
55
|
+
|
|
56
|
+
// Function to render the chart
|
|
57
|
+
function renderChart({
|
|
58
|
+
clientWidth,
|
|
59
|
+
clientHeight,
|
|
60
|
+
animate
|
|
61
|
+
}: {
|
|
62
|
+
clientWidth: number
|
|
63
|
+
clientHeight: number
|
|
64
|
+
animate: boolean
|
|
65
|
+
}) {
|
|
66
|
+
if (!chartRef.value) { return }
|
|
67
|
+
|
|
68
|
+
// Create color scale
|
|
69
|
+
const color = scheme(props.data, props.colors)
|
|
70
|
+
|
|
71
|
+
// Clear any existing SVG
|
|
72
|
+
d3
|
|
73
|
+
.select(chartRef.value)
|
|
74
|
+
.selectAll('*')
|
|
75
|
+
.remove()
|
|
76
|
+
|
|
77
|
+
// Set dimensions and margins
|
|
78
|
+
const vertical = props.type === 'vertical'
|
|
79
|
+
|
|
80
|
+
const margin = {
|
|
81
|
+
top: props.margins?.top ?? 30,
|
|
82
|
+
right: props.margins?.right ?? 40,
|
|
83
|
+
bottom: props.margins?.bottom ?? (props.xLabel ? 80 : 60),
|
|
84
|
+
left: props.margins?.left ?? (props.yLabel ? (vertical ? 80 : 100) : (vertical ? 60 : 80))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const width = clientWidth - margin.left - margin.right
|
|
88
|
+
const height = clientHeight - margin.top - margin.bottom
|
|
89
|
+
|
|
90
|
+
const xLabelOffset = props.xLabelOffset ?? 46
|
|
91
|
+
const yLabelOffset = props.yLabelOffset ?? (vertical ? 40 : 56)
|
|
92
|
+
|
|
93
|
+
// Create SVG
|
|
94
|
+
const svg = d3
|
|
95
|
+
.select(chartRef.value)
|
|
96
|
+
.append('svg')
|
|
97
|
+
.attr('width', '100%')
|
|
98
|
+
.attr('height', height + margin.top + margin.bottom)
|
|
99
|
+
.append('g')
|
|
100
|
+
.attr('transform', `translate(${margin.left},${margin.top})`)
|
|
101
|
+
|
|
102
|
+
// Create a padded scale for the bars (with padding for visual spacing)
|
|
103
|
+
const paddedScale = d3
|
|
104
|
+
.scaleBand<string>()
|
|
105
|
+
.domain(props.data.map((d) => d.key))
|
|
106
|
+
.range(vertical ? [0, width] : [0, height])
|
|
107
|
+
.padding(0.4)
|
|
108
|
+
|
|
109
|
+
// Y scale for bar values
|
|
110
|
+
const y = d3
|
|
111
|
+
.scaleLinear()
|
|
112
|
+
.domain([0, d3.max(props.data, (d) => d.value)!])
|
|
113
|
+
.nice()
|
|
114
|
+
.range(vertical ? [height, 0] : [0, width])
|
|
115
|
+
|
|
116
|
+
// Compute a constant offset to center the colored bar inside its full band.
|
|
117
|
+
const groupOffset = (paddedScale.step() - paddedScale.bandwidth()) / 2
|
|
118
|
+
const heightPadding = 24
|
|
119
|
+
const bandwidth = Math.min(paddedScale.bandwidth(), props.maxBandwidth)
|
|
120
|
+
const innerOffset = (paddedScale.bandwidth() - bandwidth) / 2
|
|
121
|
+
|
|
122
|
+
// For the axes, use the paddedScale so ticks remain centered on the bars.
|
|
123
|
+
svg
|
|
124
|
+
.append('g')
|
|
125
|
+
.attr('transform', `translate(0,${height})`)
|
|
126
|
+
.call(vertical ? d3.axisBottom(paddedScale) : d3.axisBottom(y).ticks(props.ticks))
|
|
127
|
+
.selectAll('text')
|
|
128
|
+
.attr('fill', c.text2)
|
|
129
|
+
.style('font-size', props.tickFontSize)
|
|
130
|
+
.style('text-anchor', 'middle')
|
|
131
|
+
|
|
132
|
+
// Remove X axis line
|
|
133
|
+
svg
|
|
134
|
+
.select('.domain')
|
|
135
|
+
.remove()
|
|
136
|
+
|
|
137
|
+
// Add Y axis
|
|
138
|
+
svg
|
|
139
|
+
.append('g')
|
|
140
|
+
.call(vertical ? d3.axisLeft(y).ticks(props.ticks) : d3.axisLeft(paddedScale))
|
|
141
|
+
.selectAll('text')
|
|
142
|
+
.attr('fill', c.text2)
|
|
143
|
+
.style('font-size', props.tickFontSize)
|
|
144
|
+
|
|
145
|
+
// Remove Y axis line
|
|
146
|
+
svg
|
|
147
|
+
.select('.domain')
|
|
148
|
+
.remove()
|
|
149
|
+
|
|
150
|
+
// Add horizontal grid lines
|
|
151
|
+
const gridLines = svg
|
|
152
|
+
.selectAll()
|
|
153
|
+
.data(y.ticks(props.ticks))
|
|
154
|
+
.enter()
|
|
155
|
+
.append('line')
|
|
156
|
+
.attr('stroke', c.divider)
|
|
157
|
+
.attr('stroke-dasharray', '2,2')
|
|
158
|
+
|
|
159
|
+
if (vertical) {
|
|
160
|
+
gridLines
|
|
161
|
+
.attr('x1', 0)
|
|
162
|
+
.attr('x2', width)
|
|
163
|
+
.attr('y1', (d) => y(d))
|
|
164
|
+
.attr('y2', (d) => y(d))
|
|
165
|
+
} else {
|
|
166
|
+
gridLines
|
|
167
|
+
.attr('x1', (d) => y(d))
|
|
168
|
+
.attr('x2', (d) => y(d))
|
|
169
|
+
.attr('y1', 0)
|
|
170
|
+
.attr('y2', height)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Add axis labels
|
|
174
|
+
if (props.xLabel) {
|
|
175
|
+
svg
|
|
176
|
+
.append('text')
|
|
177
|
+
.attr('x', width / 2)
|
|
178
|
+
.attr('y', height + xLabelOffset)
|
|
179
|
+
.attr('fill', c.text2)
|
|
180
|
+
.style('font-size', props.xLabelFontSize)
|
|
181
|
+
.style('text-anchor', 'middle')
|
|
182
|
+
.html(props.xLabel)
|
|
183
|
+
}
|
|
184
|
+
if (props.yLabel) {
|
|
185
|
+
svg
|
|
186
|
+
.append('text')
|
|
187
|
+
.attr('x', -height / 2)
|
|
188
|
+
.attr('y', -yLabelOffset)
|
|
189
|
+
.attr('transform', 'rotate(-90)')
|
|
190
|
+
.attr('fill', c.text2)
|
|
191
|
+
.style('font-size', props.yLabelFontSize)
|
|
192
|
+
.style('text-anchor', 'middle')
|
|
193
|
+
.html(props.yLabel)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Add bars
|
|
197
|
+
const barGroups = svg
|
|
198
|
+
.selectAll()
|
|
199
|
+
.data(props.data)
|
|
200
|
+
.enter()
|
|
201
|
+
.append('g')
|
|
202
|
+
.attr('transform', (d) =>
|
|
203
|
+
vertical
|
|
204
|
+
? `translate(${(paddedScale(d.key)! - groupOffset)},0)`
|
|
205
|
+
: `translate(0,${(paddedScale(d.key)! - groupOffset)})`
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
// Each group gets a transparent rect covering the full band (using paddedScale.step())
|
|
209
|
+
const outerBars = barGroups
|
|
210
|
+
.append('rect')
|
|
211
|
+
.attr('fill', 'transparent')
|
|
212
|
+
.attr('pointer-events', 'all')
|
|
213
|
+
|
|
214
|
+
// Append the colored bar rect inside each group.
|
|
215
|
+
// We now offset it by groupOffset so its left edge is at paddedScale(d.key)
|
|
216
|
+
const bars = barGroups
|
|
217
|
+
.append('rect')
|
|
218
|
+
.attr('fill', (d) => color(d))
|
|
219
|
+
.attr('rx', 2)
|
|
220
|
+
.attr('ry', 2)
|
|
221
|
+
|
|
222
|
+
if (!animate) {
|
|
223
|
+
if (vertical) {
|
|
224
|
+
outerBars
|
|
225
|
+
.attr('x', 0)
|
|
226
|
+
.attr('y', (d) => Math.max(0, y(d.value) - heightPadding))
|
|
227
|
+
.attr('width', paddedScale.step())
|
|
228
|
+
.attr('height', (d) => height - Math.max(0, y(d.value) - heightPadding))
|
|
229
|
+
bars
|
|
230
|
+
.attr('x', groupOffset + innerOffset)
|
|
231
|
+
.attr('y', (d) => y(d.value))
|
|
232
|
+
.attr('width', bandwidth)
|
|
233
|
+
.attr('height', (d) => height - y(d.value))
|
|
234
|
+
} else {
|
|
235
|
+
outerBars
|
|
236
|
+
.attr('x', 0)
|
|
237
|
+
.attr('y', 0)
|
|
238
|
+
.attr('width', (d) => Math.min(width, y(d.value) + heightPadding))
|
|
239
|
+
.attr('height', paddedScale.step())
|
|
240
|
+
bars
|
|
241
|
+
.attr('x', 0)
|
|
242
|
+
.attr('y', groupOffset + innerOffset)
|
|
243
|
+
.attr('width', (d) => y(d.value))
|
|
244
|
+
.attr('height', bandwidth)
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
// Animate the bars
|
|
248
|
+
if (vertical) {
|
|
249
|
+
outerBars
|
|
250
|
+
.attr('x', 0)
|
|
251
|
+
.attr('y', height)
|
|
252
|
+
.attr('width', paddedScale.step())
|
|
253
|
+
.attr('height', 0)
|
|
254
|
+
.transition()
|
|
255
|
+
.duration(800)
|
|
256
|
+
.delay((_, i) => i * 100)
|
|
257
|
+
.attr('y', (d) => Math.max(0, y(d.value) - heightPadding))
|
|
258
|
+
.attr('height', (d) => height - Math.max(0, y(d.value) - heightPadding))
|
|
259
|
+
bars
|
|
260
|
+
.attr('x', groupOffset + innerOffset)
|
|
261
|
+
.attr('y', height)
|
|
262
|
+
.attr('width', bandwidth)
|
|
263
|
+
.attr('height', 0)
|
|
264
|
+
.transition()
|
|
265
|
+
.duration(800)
|
|
266
|
+
.delay((_, i) => i * 100)
|
|
267
|
+
.attr('y', (d) => y(d.value))
|
|
268
|
+
.attr('height', (d) => height - y(d.value))
|
|
269
|
+
} else {
|
|
270
|
+
outerBars
|
|
271
|
+
.attr('x', 0)
|
|
272
|
+
.attr('y', 0)
|
|
273
|
+
.attr('width', 0)
|
|
274
|
+
.attr('height', paddedScale.step())
|
|
275
|
+
.transition()
|
|
276
|
+
.duration(800)
|
|
277
|
+
.delay((_, i) => i * 100)
|
|
278
|
+
.attr('width', (d) => Math.min(width, y(d.value) + heightPadding))
|
|
279
|
+
bars
|
|
280
|
+
.attr('x', 0)
|
|
281
|
+
.attr('y', groupOffset + innerOffset)
|
|
282
|
+
.attr('width', 0)
|
|
283
|
+
.attr('height', bandwidth)
|
|
284
|
+
.transition()
|
|
285
|
+
.duration(800)
|
|
286
|
+
.delay((_, i) => i * 100)
|
|
287
|
+
.attr('width', (d) => y(d.value))
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (props.tooltip) {
|
|
292
|
+
const Tooltip = d3
|
|
293
|
+
.select(chartRef.value)
|
|
294
|
+
.append('div')
|
|
295
|
+
.attr('class', 'tooltip')
|
|
296
|
+
|
|
297
|
+
function updatePos(event: PointerEvent) {
|
|
298
|
+
const [x, y] = d3.pointer(event, chartRef.value)
|
|
299
|
+
Tooltip
|
|
300
|
+
.style('left', `${x + 14}px`)
|
|
301
|
+
.style('top', `${y + 14}px`)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
barGroups
|
|
305
|
+
.on('pointerenter', (event: PointerEvent, d) => {
|
|
306
|
+
window.clearTimeout(tooltipTimeout)
|
|
307
|
+
Tooltip
|
|
308
|
+
.html(props.tooltipFormat(d, color(d)))
|
|
309
|
+
.style('opacity', '1')
|
|
310
|
+
updatePos(event)
|
|
311
|
+
})
|
|
312
|
+
.on('pointermove', updatePos)
|
|
313
|
+
.on('pointerleave', () => {
|
|
314
|
+
window.clearTimeout(tooltipTimeout)
|
|
315
|
+
tooltipTimeout = window.setTimeout(() => {
|
|
316
|
+
Tooltip
|
|
317
|
+
.style('opacity', '0')
|
|
318
|
+
}, 400)
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Render outline for debugging
|
|
323
|
+
if (props.debug) {
|
|
324
|
+
d3
|
|
325
|
+
.select(chartRef.value)
|
|
326
|
+
.append('div')
|
|
327
|
+
.style('position', 'absolute')
|
|
328
|
+
.style('top', `${margin.top}px`)
|
|
329
|
+
.style('left', `${margin.left}px`)
|
|
330
|
+
.style('width', `${width}px`)
|
|
331
|
+
.style('height', `${height}px`)
|
|
332
|
+
.style('outline', '1px solid blue')
|
|
333
|
+
.style('pointer-events', 'none')
|
|
334
|
+
outerBars
|
|
335
|
+
.style('outline', '1px solid green')
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
watch(
|
|
340
|
+
[width, height, () => props],
|
|
341
|
+
([clientWidth, clientHeight], [oldWidth, oldHeight]) => {
|
|
342
|
+
if (!clientWidth || !clientHeight) { return }
|
|
343
|
+
renderChart({
|
|
344
|
+
clientWidth,
|
|
345
|
+
clientHeight,
|
|
346
|
+
animate:
|
|
347
|
+
props.animate
|
|
348
|
+
&& ((oldWidth === 0 && oldHeight === 0)
|
|
349
|
+
|| (clientHeight === oldHeight && clientWidth === oldWidth))
|
|
350
|
+
})
|
|
351
|
+
},
|
|
352
|
+
{ immediate: true, deep: true }
|
|
353
|
+
)
|
|
354
|
+
</script>
|
|
355
|
+
|
|
356
|
+
<template>
|
|
357
|
+
<div ref="chart" class="SChartBar" />
|
|
358
|
+
</template>
|
|
359
|
+
|
|
360
|
+
<style scoped lang="postcss">
|
|
361
|
+
.SChartBar {
|
|
362
|
+
position: relative;
|
|
363
|
+
width: 100%;
|
|
364
|
+
height: 100%;
|
|
365
|
+
font-feature-settings: 'tnum' 1;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
:deep(.tooltip) {
|
|
369
|
+
opacity: 0;
|
|
370
|
+
pointer-events: none;
|
|
371
|
+
position: absolute;
|
|
372
|
+
top: 0;
|
|
373
|
+
left: 0;
|
|
374
|
+
padding: 2px 8px;
|
|
375
|
+
background-color: var(--c-bg-elv-2);
|
|
376
|
+
border: 1px solid var(--c-divider);
|
|
377
|
+
border-radius: 6px;
|
|
378
|
+
font-size: 12px;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
:deep(.tick line) {
|
|
382
|
+
display: none;
|
|
383
|
+
}
|
|
384
|
+
</style>
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useElementSize } from '@vueuse/core'
|
|
3
|
+
import * as d3 from 'd3'
|
|
4
|
+
import { useTemplateRef, watch } from 'vue'
|
|
5
|
+
import { type ChartColor, type KV, type Margins, c, scheme } from '../support/Chart'
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(defineProps<{
|
|
8
|
+
// State
|
|
9
|
+
data: KV[]
|
|
10
|
+
activeKey?: string
|
|
11
|
+
|
|
12
|
+
// Chart appearance
|
|
13
|
+
type?: 'pie' | 'donut'
|
|
14
|
+
half?: boolean
|
|
15
|
+
colors?: ChartColor[]
|
|
16
|
+
margins?: Margins
|
|
17
|
+
innerRadius?: (outerRadius: number) => number
|
|
18
|
+
|
|
19
|
+
// Labels
|
|
20
|
+
labels?: boolean
|
|
21
|
+
labelFormat?: (d: KV) => string
|
|
22
|
+
labelFontSize?: string
|
|
23
|
+
|
|
24
|
+
// Legend
|
|
25
|
+
legend?: boolean
|
|
26
|
+
legendFormatKey?: (d: KV) => string
|
|
27
|
+
legendFormatValue?: (d: KV) => string
|
|
28
|
+
legendPadding?: number
|
|
29
|
+
legendFontSize?: string
|
|
30
|
+
|
|
31
|
+
// Tooltip & interactivity
|
|
32
|
+
tooltip?: boolean
|
|
33
|
+
tooltipFormat?: (d: KV, color: string) => string
|
|
34
|
+
|
|
35
|
+
// Animation & debug
|
|
36
|
+
animate?: boolean
|
|
37
|
+
debug?: boolean
|
|
38
|
+
}>(), {
|
|
39
|
+
type: 'donut',
|
|
40
|
+
half: false,
|
|
41
|
+
|
|
42
|
+
labels: false,
|
|
43
|
+
labelFormat: (d: KV) => `${d.key} – ${d.value}`,
|
|
44
|
+
labelFontSize: '14px',
|
|
45
|
+
|
|
46
|
+
legend: true,
|
|
47
|
+
legendFormatKey: (d: KV) => d.key,
|
|
48
|
+
legendFormatValue: (d: KV) => d.value.toString(),
|
|
49
|
+
legendPadding: 70,
|
|
50
|
+
legendFontSize: '14px',
|
|
51
|
+
|
|
52
|
+
tooltip: true,
|
|
53
|
+
tooltipFormat: (d: KV) => `${d.key} – ${d.value}`,
|
|
54
|
+
|
|
55
|
+
animate: true,
|
|
56
|
+
debug: false
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const chartRef = useTemplateRef('chart')
|
|
60
|
+
const { width, height } = useElementSize(chartRef)
|
|
61
|
+
|
|
62
|
+
let tooltipTimeout = 0
|
|
63
|
+
|
|
64
|
+
// Function to render the chart
|
|
65
|
+
function renderChart({
|
|
66
|
+
clientWidth,
|
|
67
|
+
clientHeight,
|
|
68
|
+
animate
|
|
69
|
+
}: {
|
|
70
|
+
clientWidth: number
|
|
71
|
+
clientHeight: number
|
|
72
|
+
animate: boolean
|
|
73
|
+
}) {
|
|
74
|
+
if (!chartRef.value) { return }
|
|
75
|
+
|
|
76
|
+
// Create color scale
|
|
77
|
+
const color = scheme(props.data, props.colors)
|
|
78
|
+
|
|
79
|
+
// Clear any existing SVG
|
|
80
|
+
d3
|
|
81
|
+
.select(chartRef.value)
|
|
82
|
+
.selectAll('*')
|
|
83
|
+
.remove()
|
|
84
|
+
|
|
85
|
+
// Set dimensions and margins
|
|
86
|
+
const margin = {
|
|
87
|
+
top: props.margins?.top ?? 30,
|
|
88
|
+
right: props.margins?.right ?? 30,
|
|
89
|
+
bottom: props.margins?.bottom ?? 30,
|
|
90
|
+
left: props.margins?.left ?? 30
|
|
91
|
+
}
|
|
92
|
+
const width = clientWidth - margin.left - margin.right
|
|
93
|
+
const height = clientHeight - margin.top - margin.bottom
|
|
94
|
+
|
|
95
|
+
const width_2 = width / 2
|
|
96
|
+
const height_2 = props.half ? height : height / 2
|
|
97
|
+
|
|
98
|
+
// Create SVG
|
|
99
|
+
const svg = d3
|
|
100
|
+
.select(chartRef.value)
|
|
101
|
+
.append('svg')
|
|
102
|
+
.attr('width', '100%')
|
|
103
|
+
.attr('height', clientHeight)
|
|
104
|
+
.append('g')
|
|
105
|
+
|
|
106
|
+
// Prepare data
|
|
107
|
+
const pie = d3
|
|
108
|
+
.pie<KV>()
|
|
109
|
+
.value((d) => d.value)
|
|
110
|
+
.sort(null)
|
|
111
|
+
.padAngle(0.02)
|
|
112
|
+
|
|
113
|
+
if (props.half) {
|
|
114
|
+
pie.startAngle(-Math.PI / 2)
|
|
115
|
+
pie.endAngle(Math.PI / 2)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const dataReady = pie(props.data)
|
|
119
|
+
|
|
120
|
+
// Render legend
|
|
121
|
+
let legendGroup
|
|
122
|
+
let legendHeight = 0
|
|
123
|
+
let legendWidth = 0
|
|
124
|
+
if (props.legend) {
|
|
125
|
+
// Create a group for the legend
|
|
126
|
+
legendGroup = svg
|
|
127
|
+
.append('g')
|
|
128
|
+
|
|
129
|
+
// Add legend
|
|
130
|
+
const legend = legendGroup
|
|
131
|
+
.selectAll()
|
|
132
|
+
.data(dataReady)
|
|
133
|
+
.enter()
|
|
134
|
+
.append('g')
|
|
135
|
+
.attr('transform', (d, i) => `translate(0,${i * 24})`)
|
|
136
|
+
.style('font-size', props.legendFontSize)
|
|
137
|
+
|
|
138
|
+
// Add colored rectangles to the legend
|
|
139
|
+
legend
|
|
140
|
+
.append('rect')
|
|
141
|
+
.attr('x', 0)
|
|
142
|
+
.attr('rx', 2)
|
|
143
|
+
.attr('ry', 2)
|
|
144
|
+
.attr('width', 14)
|
|
145
|
+
.attr('height', 14)
|
|
146
|
+
.attr('fill', (d) => color(d.data))
|
|
147
|
+
|
|
148
|
+
// Add legend text
|
|
149
|
+
legend
|
|
150
|
+
.append('text')
|
|
151
|
+
.attr('x', 30)
|
|
152
|
+
.attr('y', 14 / 2)
|
|
153
|
+
.attr('dy', '0.35em')
|
|
154
|
+
.attr('fill', c.text2)
|
|
155
|
+
.html((d) => props.legendFormatKey(d.data))
|
|
156
|
+
|
|
157
|
+
// Show value next to the legend
|
|
158
|
+
legend
|
|
159
|
+
.append('text')
|
|
160
|
+
.attr('x', 30 + props.legendPadding)
|
|
161
|
+
.attr('y', 14 / 2)
|
|
162
|
+
.attr('dy', '0.35em')
|
|
163
|
+
.attr('fill', c.text1)
|
|
164
|
+
.attr('text-anchor', 'end')
|
|
165
|
+
.html((d) => props.legendFormatValue(d.data))
|
|
166
|
+
|
|
167
|
+
;({ width: legendWidth = 0, height: legendHeight = 0 } = legendGroup?.node()?.getBBox() ?? {})
|
|
168
|
+
|
|
169
|
+
// Animate the legends
|
|
170
|
+
if (animate) {
|
|
171
|
+
legend
|
|
172
|
+
.attr('opacity', 0)
|
|
173
|
+
.transition()
|
|
174
|
+
.delay((d, i) => i * 100)
|
|
175
|
+
.duration(800)
|
|
176
|
+
.attr('opacity', 1)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Calculate radius and center the chart
|
|
181
|
+
const r_k = props.half ? 0.25 : 0.5
|
|
182
|
+
const radius = Math.min(height_2, (width - legendWidth) / (2 + (props.legend ? r_k : 0)))
|
|
183
|
+
const innerRadius = props.innerRadius?.(radius) ?? (props.type === 'pie' ? 6 : Math.max(radius / 1.5, radius - 50))
|
|
184
|
+
|
|
185
|
+
legendGroup
|
|
186
|
+
?.attr('transform', `translate(${radius * (1 + r_k)},${-(props.half ? height_2 : 0) / 2 - legendHeight / 2})`)
|
|
187
|
+
svg
|
|
188
|
+
.attr('transform', `translate(${margin.left + width_2 - (props.legend ? radius * r_k + legendWidth : 0) / 2},${margin.top + height_2})`)
|
|
189
|
+
|
|
190
|
+
// Create arc generator
|
|
191
|
+
const arc = d3
|
|
192
|
+
.arc<d3.PieArcDatum<KV>>()
|
|
193
|
+
.innerRadius(innerRadius)
|
|
194
|
+
.outerRadius(radius)
|
|
195
|
+
.cornerRadius(2)
|
|
196
|
+
|
|
197
|
+
// Add the arcs
|
|
198
|
+
const transform = `translate(0,${-(height_2 - radius) / 2})`
|
|
199
|
+
const activeTransform = `${transform} scale(1.05)`
|
|
200
|
+
|
|
201
|
+
const arcs = svg
|
|
202
|
+
.selectAll()
|
|
203
|
+
.data(dataReady)
|
|
204
|
+
.join('path')
|
|
205
|
+
.attr('fill', (d) => color(d.data))
|
|
206
|
+
.attr('transform', transform)
|
|
207
|
+
|
|
208
|
+
const activeArcs = arcs
|
|
209
|
+
.filter((d) => d.data.key === props.activeKey)
|
|
210
|
+
|
|
211
|
+
if (!animate) {
|
|
212
|
+
arcs
|
|
213
|
+
.attr('d', arc)
|
|
214
|
+
|
|
215
|
+
activeArcs
|
|
216
|
+
.attr('transform', activeTransform)
|
|
217
|
+
} else {
|
|
218
|
+
// Animate the arcs
|
|
219
|
+
arcs
|
|
220
|
+
.attr('d', arc({ startAngle: 0, endAngle: 0, data: {} } as any))
|
|
221
|
+
.transition()
|
|
222
|
+
.duration(800 + props.data.length * 100)
|
|
223
|
+
.attrTween('d', (d) => {
|
|
224
|
+
const startAngle = props.half ? -Math.PI / 2 : 0
|
|
225
|
+
const i = d3.interpolate({ startAngle, endAngle: startAngle }, d)
|
|
226
|
+
return (t) => arc(i(t))!
|
|
227
|
+
})
|
|
228
|
+
.end()
|
|
229
|
+
.then(() => {
|
|
230
|
+
activeArcs
|
|
231
|
+
.transition()
|
|
232
|
+
.duration(200)
|
|
233
|
+
.attr('transform', activeTransform)
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (props.labels) {
|
|
238
|
+
const labelArc = d3.arc<d3.PieArcDatum<KV>>()
|
|
239
|
+
.innerRadius(radius)
|
|
240
|
+
.outerRadius(radius)
|
|
241
|
+
|
|
242
|
+
const labels = svg
|
|
243
|
+
.selectAll()
|
|
244
|
+
.data(dataReady)
|
|
245
|
+
.enter()
|
|
246
|
+
.append('g')
|
|
247
|
+
.attr('transform', transform)
|
|
248
|
+
|
|
249
|
+
const leftOrRight = (d: d3.PieArcDatum<KV>) => {
|
|
250
|
+
const midAngle = (d.startAngle + d.endAngle) / 2
|
|
251
|
+
if (props.half) {
|
|
252
|
+
return midAngle < 0 ? -1 : 1
|
|
253
|
+
}
|
|
254
|
+
return midAngle < Math.PI ? 1 : -1
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
labels
|
|
258
|
+
.append('polyline')
|
|
259
|
+
.attr('stroke', c.divider)
|
|
260
|
+
.attr('fill', 'none')
|
|
261
|
+
.attr('points', (d) => {
|
|
262
|
+
const posA = arc.centroid(d)
|
|
263
|
+
const posB = labelArc.centroid(d)
|
|
264
|
+
const posC = labelArc.centroid(d)
|
|
265
|
+
posC[0] = radius * 1.05 * leftOrRight(d)
|
|
266
|
+
return [posA, posB, posC].map((p) => p.join(','))
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
labels
|
|
270
|
+
.append('text')
|
|
271
|
+
.attr('transform', (d) => {
|
|
272
|
+
const pos = labelArc.centroid(d)
|
|
273
|
+
pos[0] = radius * 1.1 * leftOrRight(d)
|
|
274
|
+
return `translate(${pos})`
|
|
275
|
+
})
|
|
276
|
+
.attr('dy', '0.35em')
|
|
277
|
+
.attr('fill', c.text2)
|
|
278
|
+
.attr('text-anchor', (d) => leftOrRight(d) === 1 ? 'start' : 'end')
|
|
279
|
+
.style('font-size', props.labelFontSize)
|
|
280
|
+
.html((d) => props.labelFormat(d.data))
|
|
281
|
+
|
|
282
|
+
if (animate) {
|
|
283
|
+
labels
|
|
284
|
+
.attr('opacity', 0)
|
|
285
|
+
.transition()
|
|
286
|
+
.delay((_, i) => 800 + i * 100)
|
|
287
|
+
.duration(500)
|
|
288
|
+
.attr('opacity', 1)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (props.tooltip) {
|
|
293
|
+
const Tooltip = d3
|
|
294
|
+
.select(chartRef.value)
|
|
295
|
+
.append('div')
|
|
296
|
+
.attr('class', 'tooltip')
|
|
297
|
+
|
|
298
|
+
function updatePos(event: PointerEvent) {
|
|
299
|
+
const [x, y] = d3.pointer(event, chartRef.value)
|
|
300
|
+
Tooltip
|
|
301
|
+
.style('left', `${x + 14}px`)
|
|
302
|
+
.style('top', `${y + 14}px`)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
arcs
|
|
306
|
+
.on('pointerenter', (event: PointerEvent, { data: d }) => {
|
|
307
|
+
window.clearTimeout(tooltipTimeout)
|
|
308
|
+
Tooltip
|
|
309
|
+
.html(props.tooltipFormat(d, color(d)))
|
|
310
|
+
.style('opacity', '1')
|
|
311
|
+
updatePos(event)
|
|
312
|
+
})
|
|
313
|
+
.on('pointermove', updatePos)
|
|
314
|
+
.on('pointerleave', () => {
|
|
315
|
+
window.clearTimeout(tooltipTimeout)
|
|
316
|
+
tooltipTimeout = window.setTimeout(() => {
|
|
317
|
+
Tooltip
|
|
318
|
+
.style('opacity', '0')
|
|
319
|
+
}, 400)
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Render outline for debugging
|
|
324
|
+
if (props.debug) {
|
|
325
|
+
d3
|
|
326
|
+
.select(chartRef.value)
|
|
327
|
+
.append('div')
|
|
328
|
+
.style('position', 'absolute')
|
|
329
|
+
.style('top', `${margin.top}px`)
|
|
330
|
+
.style('left', `${margin.left}px`)
|
|
331
|
+
.style('width', `${width}px`)
|
|
332
|
+
.style('height', `${height}px`)
|
|
333
|
+
.style('outline', '1px solid blue')
|
|
334
|
+
.style('pointer-events', 'none')
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
watch(
|
|
339
|
+
[width, height, () => props],
|
|
340
|
+
([clientWidth, clientHeight], [oldWidth, oldHeight]) => {
|
|
341
|
+
if (!clientWidth || !clientHeight) { return }
|
|
342
|
+
renderChart({
|
|
343
|
+
clientWidth,
|
|
344
|
+
clientHeight,
|
|
345
|
+
animate:
|
|
346
|
+
props.animate
|
|
347
|
+
&& ((oldWidth === 0 && oldHeight === 0)
|
|
348
|
+
|| (clientHeight === oldHeight && clientWidth === oldWidth))
|
|
349
|
+
})
|
|
350
|
+
},
|
|
351
|
+
{ immediate: true, deep: true }
|
|
352
|
+
)
|
|
353
|
+
</script>
|
|
354
|
+
|
|
355
|
+
<template>
|
|
356
|
+
<div ref="chart" class="SChartPie" />
|
|
357
|
+
</template>
|
|
358
|
+
|
|
359
|
+
<style scoped lang="postcss">
|
|
360
|
+
.SChartPie {
|
|
361
|
+
position: relative;
|
|
362
|
+
width: 100%;
|
|
363
|
+
height: 100%;
|
|
364
|
+
font-feature-settings: 'tnum' 1;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
:deep(.tooltip) {
|
|
368
|
+
opacity: 0;
|
|
369
|
+
pointer-events: none;
|
|
370
|
+
position: absolute;
|
|
371
|
+
top: 0;
|
|
372
|
+
left: 0;
|
|
373
|
+
padding: 2px 8px;
|
|
374
|
+
background-color: var(--c-bg-elv-2);
|
|
375
|
+
border: 1px solid var(--c-divider);
|
|
376
|
+
border-radius: 6px;
|
|
377
|
+
font-size: 12px;
|
|
378
|
+
}
|
|
379
|
+
</style>
|
|
@@ -87,7 +87,7 @@ const cleanups = [
|
|
|
87
87
|
setTimeout(() => { ignore.value = false })
|
|
88
88
|
}
|
|
89
89
|
}),
|
|
90
|
-
onClickOutside(root, hide, { ignore: [content] }),
|
|
90
|
+
onClickOutside(root, hide, { ignore: [content], controls: false }),
|
|
91
91
|
() => timeoutId.value != null && window.clearTimeout(timeoutId.value)
|
|
92
92
|
]
|
|
93
93
|
|
package/lib/composables/Error.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Adapted from
|
|
3
|
-
* @see https://github.com/vuejs/core/blob/
|
|
4
|
-
* @see https://github.com/vuejs/core/blob/
|
|
5
|
-
* @see https://github.com/getsentry/sentry-javascript/blob/
|
|
6
|
-
* @see https://github.com/vercel/ai/blob/d544886d4f61440bacd6e44c86144bfac7c98282/packages/provider-utils/src/get-error-message.ts
|
|
3
|
+
* @see https://github.com/vuejs/core/blob/1755ac0a108ba3486bd8397e56d3bdcd69196594/packages/runtime-core/src/component.ts
|
|
4
|
+
* @see https://github.com/vuejs/core/blob/ac9e7e8bfa55432a73a10864805fdf48bda2ff73/packages/runtime-core/src/warning.ts
|
|
5
|
+
* @see https://github.com/getsentry/sentry-javascript/blob/04711c20246f7cdaac2305286fec783ab1859a18/packages/vue/src/errorhandler.ts
|
|
6
|
+
* @see https://github.com/vercel/ai/blob/d544886d4f61440bacd6e44c86144bfac7c98282/packages/provider-utils/src/get-error-message.ts
|
|
7
7
|
*
|
|
8
8
|
* Original licenses:
|
|
9
9
|
*
|
|
10
10
|
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
|
11
11
|
* @license MIT
|
|
12
12
|
*
|
|
13
|
-
* (c)
|
|
13
|
+
* (c) 2012-2024 Functional Software, Inc. dba Sentry
|
|
14
14
|
* @license MIT
|
|
15
15
|
*
|
|
16
16
|
* (c) 2023 Vercel, Inc.
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import * as Sentry from '@sentry/browser'
|
|
21
|
+
import { createSentryPiniaPlugin } from '@sentry/vue'
|
|
21
22
|
import { pauseTracking, resetTracking } from '@vue/reactivity'
|
|
23
|
+
import { getActivePinia } from 'pinia'
|
|
22
24
|
import {
|
|
23
25
|
type ComponentInternalInstance,
|
|
24
26
|
type ComponentOptions,
|
|
@@ -115,11 +117,8 @@ function formatTrace(instance: ComponentInternalInstance | null): string {
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
function formatTraceEntry({ vnode, recurseCount }: TraceEntry): string {
|
|
118
|
-
return `at <${formatComponentName(vnode.component)}${
|
|
119
|
-
|
|
120
|
-
}>${
|
|
121
|
-
recurseCount > 0 ? ` ... (${recurseCount} recursive call${recurseCount > 1 ? 's' : ''})` : ''
|
|
122
|
-
}`
|
|
120
|
+
return `at <${formatComponentName(vnode.component)}${vnode.props ? ` ${formatProps(vnode.props)}` : ''}>\
|
|
121
|
+
${recurseCount > 0 ? ` ... (${recurseCount} recursive call${recurseCount > 1 ? 's' : ''})` : ''}`
|
|
123
122
|
}
|
|
124
123
|
|
|
125
124
|
function formatProps(props: Record<string, unknown>): string {
|
|
@@ -183,6 +182,7 @@ export function useErrorHandler({
|
|
|
183
182
|
|
|
184
183
|
if (enabled) {
|
|
185
184
|
Sentry.init({ dsn, environment, ignoreErrors })
|
|
185
|
+
getActivePinia()?.use(createSentryPiniaPlugin())
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
return function errorHandler(
|
|
@@ -190,8 +190,6 @@ export function useErrorHandler({
|
|
|
190
190
|
instance: ComponentPublicInstance | null = null,
|
|
191
191
|
info: string = ''
|
|
192
192
|
) {
|
|
193
|
-
set(e)
|
|
194
|
-
|
|
195
193
|
if (enabled) {
|
|
196
194
|
pauseTracking()
|
|
197
195
|
|
|
@@ -222,6 +220,8 @@ export function useErrorHandler({
|
|
|
222
220
|
} else {
|
|
223
221
|
console.error(e)
|
|
224
222
|
}
|
|
223
|
+
|
|
224
|
+
set(e)
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
|
package/lib/composables/Lang.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type InjectionKey, getCurrentInstance, inject } from 'vue'
|
|
2
2
|
|
|
3
3
|
export type Lang = 'en' | 'ja'
|
|
4
4
|
|
|
@@ -15,7 +15,7 @@ export interface HasLang {
|
|
|
15
15
|
lang: Lang
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export const SefirotLangKey = 'sefirot-lang-key'
|
|
18
|
+
export const SefirotLangKey: InjectionKey<Lang> = Symbol.for('sefirot-lang-key')
|
|
19
19
|
|
|
20
20
|
export function useSetupLang(): (user?: HasLang | null) => void {
|
|
21
21
|
const browserLang = useBrowserLang()
|
|
@@ -26,7 +26,7 @@ export function useSetupLang(): (user?: HasLang | null) => void {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export function provideLang(lang: Lang) {
|
|
29
|
-
provide(SefirotLangKey, lang)
|
|
29
|
+
getCurrentInstance()?.appContext.app.provide(SefirotLangKey, lang)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function useLang(): Lang {
|
package/lib/http/Http.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { parse as parseContentDisposition } from '@tinyhttp/content-disposition'
|
|
|
2
2
|
import { parse as parseCookie } from '@tinyhttp/cookie'
|
|
3
3
|
import FileSaver from 'file-saver'
|
|
4
4
|
import { FetchError, type FetchOptions, type FetchRequest, type FetchResponse, ofetch } from 'ofetch'
|
|
5
|
-
import { stringify } from 'qs'
|
|
5
|
+
import { type IStringifyOptions, stringify } from 'qs'
|
|
6
6
|
import { type Lang } from '../composables/Lang'
|
|
7
7
|
import { isBlob, isError, isFormData, isRequest, isResponse, isString } from '../support/Utils'
|
|
8
8
|
|
|
@@ -20,6 +20,7 @@ export interface HttpOptions {
|
|
|
20
20
|
lang?: Lang
|
|
21
21
|
payloadKey?: string
|
|
22
22
|
headers?: () => Awaitable<Record<string, string>>
|
|
23
|
+
stringifyOptions?: IStringifyOptions
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export class Http {
|
|
@@ -29,6 +30,7 @@ export class Http {
|
|
|
29
30
|
private static lang: Lang | undefined = undefined
|
|
30
31
|
private static payloadKey = '__payload__'
|
|
31
32
|
private static headers: () => Awaitable<Record<string, string>> = async () => ({})
|
|
33
|
+
private static stringifyOptions: IStringifyOptions = {}
|
|
32
34
|
|
|
33
35
|
static config(options: HttpOptions): void {
|
|
34
36
|
if (options.baseUrl) {
|
|
@@ -49,6 +51,9 @@ export class Http {
|
|
|
49
51
|
if (options.headers) {
|
|
50
52
|
Http.headers = options.headers
|
|
51
53
|
}
|
|
54
|
+
if (options.stringifyOptions) {
|
|
55
|
+
Http.stringifyOptions = options.stringifyOptions
|
|
56
|
+
}
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
private async ensureXsrfToken(): Promise<string | undefined> {
|
|
@@ -69,7 +74,8 @@ export class Http {
|
|
|
69
74
|
private async buildRequest(url: string, _options: FetchOptions = {}): Promise<[string, FetchOptions]> {
|
|
70
75
|
const { method, params, query, ...options } = _options
|
|
71
76
|
const xsrfToken = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method || '') && (await this.ensureXsrfToken())
|
|
72
|
-
|
|
77
|
+
|
|
78
|
+
const queryString = stringify({ ...params, ...query }, { encodeValuesOnly: true, ...Http.stringifyOptions })
|
|
73
79
|
|
|
74
80
|
return [
|
|
75
81
|
`${url}${queryString ? `?${queryString}` : ''}`,
|
|
@@ -195,16 +201,10 @@ export class Http {
|
|
|
195
201
|
export function isFetchError(e: unknown): e is FetchError {
|
|
196
202
|
return (
|
|
197
203
|
e instanceof FetchError
|
|
198
|
-
|| (isError(e)
|
|
199
|
-
&& (
|
|
200
|
-
&& ((e
|
|
201
|
-
|
|
202
|
-
`[${
|
|
203
|
-
((e as FetchError).request as Request | undefined)?.method || (e as FetchError).options?.method || 'GET'
|
|
204
|
-
}] ${JSON.stringify(
|
|
205
|
-
((e as FetchError).request as Request | undefined)?.url || String((e as FetchError).request) || '/'
|
|
206
|
-
)}: `
|
|
207
|
-
))
|
|
204
|
+
|| (isError<FetchError>(e)
|
|
205
|
+
&& (e.response === undefined || isResponse(e.response))
|
|
206
|
+
&& ((isString(e.request) && e.message.startsWith(`[${e.options?.method || 'GET'}] ${JSON.stringify(e.request || '/')}: `))
|
|
207
|
+
|| (isRequest(e.request) && e.message.startsWith(`[${e.request.method}] ${JSON.stringify(e.request.url)}: `))))
|
|
208
208
|
)
|
|
209
209
|
}
|
|
210
210
|
|
package/lib/styles/base.css
CHANGED
package/lib/styles/utilities.css
CHANGED
|
@@ -155,6 +155,15 @@
|
|
|
155
155
|
.s-max-w-lg { max-width: 960px; }
|
|
156
156
|
.s-max-w-xl { max-width: 1216px; }
|
|
157
157
|
|
|
158
|
+
.s-h-256 { height: 256px; }
|
|
159
|
+
.s-h-320 { height: 320px; }
|
|
160
|
+
.s-h-512 { height: 512px; }
|
|
161
|
+
.s-h-full { height: 100%; }
|
|
162
|
+
|
|
163
|
+
.s-aspect-16-9 { aspect-ratio: 16 / 9; }
|
|
164
|
+
.s-aspect-4-3 { aspect-ratio: 4 / 3; }
|
|
165
|
+
.s-aspect-1-1 { aspect-ratio: 1 / 1; }
|
|
166
|
+
|
|
158
167
|
/**
|
|
159
168
|
* Typography
|
|
160
169
|
* -------------------------------------------------------------------------- */
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credits:
|
|
3
|
+
*
|
|
4
|
+
* - @radix-ui/colors - MIT License
|
|
5
|
+
* Copyright (c) 2021 Radix
|
|
6
|
+
* https://github.com/radix-ui/colors/blob/main/LICENSE
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import FileSaver from 'file-saver'
|
|
10
|
+
import html2canvas from 'html2canvas'
|
|
11
|
+
|
|
12
|
+
export const c = {
|
|
13
|
+
text1: 'light-dark(#1c2024, #edeef0)',
|
|
14
|
+
text2: 'light-dark(#5d616b, #abafb7)',
|
|
15
|
+
divider: 'light-dark(#e0e0e1, #2e3035)'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// radixLight.9, radixDark.10
|
|
19
|
+
export const chartColors = {
|
|
20
|
+
// orange: 'light-dark(#f76b15, #ff801f)',
|
|
21
|
+
tomato: 'light-dark(#e54d2e, #ec6142)',
|
|
22
|
+
// red: 'light-dark(#e5484d, #ec5d5e)',
|
|
23
|
+
ruby: 'light-dark(#e54666, #ec5a72)',
|
|
24
|
+
// crimson: 'light-dark(#e93d82, #ee518a)',
|
|
25
|
+
pink: 'light-dark(#d6409f, #de51a8)',
|
|
26
|
+
// plum: 'light-dark(#ab4aba, #b658c4)',
|
|
27
|
+
purple: 'light-dark(#8e4ec6, #9a5cd0)',
|
|
28
|
+
// violet: 'light-dark(#6e56cf, #7d66d9)',
|
|
29
|
+
iris: 'light-dark(#5b5bd6, #6e6ade)',
|
|
30
|
+
// indigo: 'light-dark(#3e63dd, #5472e4)',
|
|
31
|
+
blue: 'light-dark(#0090ff, #3b9eff)',
|
|
32
|
+
// cyan: 'light-dark(#00a2c7, #23afd0)',
|
|
33
|
+
teal: 'light-dark(#12a594, #0eb39e)',
|
|
34
|
+
// jade: 'light-dark(#29a383, #27b08b)',
|
|
35
|
+
green: 'light-dark(#30a46c, #33b074)',
|
|
36
|
+
// grass: 'light-dark(#46a758, #53b365)',
|
|
37
|
+
gray: 'light-dark(#8d8d8d, #7b7b7b)'
|
|
38
|
+
} as const
|
|
39
|
+
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
41
|
+
export type ChartColor = keyof typeof chartColors | (string & {})
|
|
42
|
+
export type KV = { key: string; value: number; color?: ChartColor }
|
|
43
|
+
export type Margins = Partial<{ top: number; right: number; bottom: number; left: number }>
|
|
44
|
+
|
|
45
|
+
export function getColor(...colors: (ChartColor | string | null | undefined)[]): string {
|
|
46
|
+
const color = colors.find((color) => color != null)
|
|
47
|
+
return chartColors[color as keyof typeof chartColors] || color || chartColors.blue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { gray: _gray, ...rest } = chartColors
|
|
51
|
+
|
|
52
|
+
export function scheme<T extends { key: string; color?: ChartColor }>(
|
|
53
|
+
data: T[],
|
|
54
|
+
colors: ChartColor[] = Object.keys(rest),
|
|
55
|
+
unknown: ChartColor = 'gray'
|
|
56
|
+
): (d: T) => string {
|
|
57
|
+
const map = new Map<string, string>(
|
|
58
|
+
data.map((d, i) => [d.key, getColor(d.color, colors?.[i % (colors?.length ?? 1)])])
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return (d: T): string => map.get(d.key) ?? unknown
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function exportAsPng(_el: any, fileName = 'chart.png', delay = 0): Promise<void> {
|
|
65
|
+
if (!_el) { return }
|
|
66
|
+
|
|
67
|
+
// TODO: automatically wait for chart to be ready
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
69
|
+
|
|
70
|
+
const el = '$el' in _el ? _el.$el : _el
|
|
71
|
+
if (!(el instanceof HTMLElement)) { return }
|
|
72
|
+
|
|
73
|
+
const SCard = el.closest('.SCard')
|
|
74
|
+
if (!(SCard instanceof HTMLElement)) { return }
|
|
75
|
+
|
|
76
|
+
const canvas = await html2canvas(SCard, {
|
|
77
|
+
scale: 2,
|
|
78
|
+
logging: false,
|
|
79
|
+
ignoreElements: (el) => el.classList.contains('SControlActionBar'),
|
|
80
|
+
onclone(document, element) {
|
|
81
|
+
document.documentElement.classList.remove('dark')
|
|
82
|
+
element.querySelectorAll<HTMLElement>('*').forEach((el) => {
|
|
83
|
+
el.style.backgroundColor = 'transparent'
|
|
84
|
+
el.style.fill = el.getAttribute('fill') ?? el.style.fill
|
|
85
|
+
el.style.stroke = el.getAttribute('stroke') ?? el.style.stroke
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const dataUrl = canvas.toDataURL('image/png')
|
|
91
|
+
FileSaver.saveAs(dataUrl, fileName)
|
|
92
|
+
}
|
package/lib/support/Utils.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function isDate(value: unknown): value is Date {
|
|
|
16
16
|
return _isDate(value)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function isError(value: unknown): value is
|
|
19
|
+
export function isError<T extends Error = Error>(value: unknown): value is T {
|
|
20
20
|
return _isError(value)
|
|
21
21
|
}
|
|
22
22
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@globalbrain/sefirot",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
5
|
-
"packageManager": "pnpm@9.15.
|
|
4
|
+
"version": "4.14.0",
|
|
5
|
+
"packageManager": "pnpm@9.15.4",
|
|
6
6
|
"description": "Vue Components for Global Brain Design System.",
|
|
7
7
|
"author": "Kia Ishii <ka.ishii@globalbrains.com>",
|
|
8
8
|
"license": "MIT",
|
|
@@ -51,32 +51,36 @@
|
|
|
51
51
|
"@vue/reactivity": "^3.5.13",
|
|
52
52
|
"@vuelidate/core": "^2.0.3",
|
|
53
53
|
"@vuelidate/validators": "^2.0.4",
|
|
54
|
-
"@vueuse/core": "^12
|
|
54
|
+
"@vueuse/core": "^12 || ^13",
|
|
55
55
|
"body-scroll-lock": "4.0.0-beta.0",
|
|
56
56
|
"dayjs": "^1.11.13",
|
|
57
|
-
"fuse.js": "^7.
|
|
57
|
+
"fuse.js": "^7.1.0",
|
|
58
58
|
"lodash-es": "^4.17.21",
|
|
59
59
|
"markdown-it": "^14.1.0",
|
|
60
60
|
"normalize.css": "^8.0.1",
|
|
61
|
-
"pinia": "^
|
|
62
|
-
"postcss": "^8.
|
|
61
|
+
"pinia": "^3.0.1",
|
|
62
|
+
"postcss": "^8.5.3",
|
|
63
63
|
"postcss-nested": "^7.0.2",
|
|
64
64
|
"v-calendar": "3.0.1",
|
|
65
65
|
"vue": "^3.5.13",
|
|
66
66
|
"vue-router": "^4.5.0"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@sentry/browser": "^
|
|
69
|
+
"@sentry/browser": "^9.5.0",
|
|
70
|
+
"@sentry/vue": "^9.5.0",
|
|
70
71
|
"@tanstack/vue-virtual": "3.0.0-beta.62",
|
|
71
72
|
"@tinyhttp/content-disposition": "^2.2.2",
|
|
72
73
|
"@tinyhttp/cookie": "^2.1.1",
|
|
74
|
+
"@types/d3": "^7.4.3",
|
|
73
75
|
"@types/file-saver": "^2.0.7",
|
|
74
|
-
"@types/qs": "^6.9.
|
|
76
|
+
"@types/qs": "^6.9.18",
|
|
77
|
+
"d3": "^7.9.0",
|
|
75
78
|
"file-saver": "^2.0.5",
|
|
79
|
+
"html2canvas": "^1.4.1",
|
|
76
80
|
"magic-string": "^0.30.17",
|
|
77
81
|
"ofetch": "^1.4.1",
|
|
78
|
-
"qs": "^6.
|
|
79
|
-
"unplugin-icons": "^22.
|
|
82
|
+
"qs": "^6.14.0",
|
|
83
|
+
"unplugin-icons": "^22.1.0"
|
|
80
84
|
},
|
|
81
85
|
"devDependencies": {
|
|
82
86
|
"@globalbrain/eslint-config": "^1.7.1",
|
|
@@ -87,35 +91,35 @@
|
|
|
87
91
|
"@types/body-scroll-lock": "^3.1.2",
|
|
88
92
|
"@types/lodash-es": "^4.17.12",
|
|
89
93
|
"@types/markdown-it": "^14.1.2",
|
|
90
|
-
"@types/node": "^22.10
|
|
94
|
+
"@types/node": "^22.13.10",
|
|
91
95
|
"@vitejs/plugin-vue": "^5.2.1",
|
|
92
|
-
"@vitest/coverage-v8": "
|
|
96
|
+
"@vitest/coverage-v8": "^3.0.8",
|
|
93
97
|
"@vue/reactivity": "^3.5.13",
|
|
94
98
|
"@vue/test-utils": "^2.4.6",
|
|
95
99
|
"@vuelidate/core": "^2.0.3",
|
|
96
100
|
"@vuelidate/validators": "^2.0.4",
|
|
97
|
-
"@vueuse/core": "^
|
|
101
|
+
"@vueuse/core": "^13.0.0",
|
|
98
102
|
"body-scroll-lock": "4.0.0-beta.0",
|
|
99
103
|
"dayjs": "^1.11.13",
|
|
100
104
|
"eslint": "8.57.0",
|
|
101
|
-
"fuse.js": "^7.
|
|
102
|
-
"happy-dom": "^
|
|
105
|
+
"fuse.js": "^7.1.0",
|
|
106
|
+
"happy-dom": "^17.4.4",
|
|
103
107
|
"histoire": "0.16.5",
|
|
104
108
|
"lodash-es": "^4.17.21",
|
|
105
109
|
"markdown-it": "^14.1.0",
|
|
106
110
|
"normalize.css": "^8.0.1",
|
|
107
|
-
"pinia": "^
|
|
108
|
-
"postcss": "^8.
|
|
111
|
+
"pinia": "^3.0.1",
|
|
112
|
+
"postcss": "^8.5.3",
|
|
109
113
|
"postcss-nested": "^7.0.2",
|
|
110
114
|
"punycode": "^2.3.1",
|
|
111
|
-
"release-it": "^18.1.
|
|
112
|
-
"typescript": "~5.
|
|
115
|
+
"release-it": "^18.1.2",
|
|
116
|
+
"typescript": "~5.8.2",
|
|
113
117
|
"v-calendar": "3.0.1",
|
|
114
|
-
"vite": "^6.
|
|
115
|
-
"vitepress": "
|
|
116
|
-
"vitest": "
|
|
118
|
+
"vite": "^6.2.1",
|
|
119
|
+
"vitepress": ">=2.0.0-alpha.3",
|
|
120
|
+
"vitest": "^3.0.8",
|
|
117
121
|
"vue": "^3.5.13",
|
|
118
122
|
"vue-router": "^4.5.0",
|
|
119
|
-
"vue-tsc": "^2.2.
|
|
123
|
+
"vue-tsc": "^2.2.8"
|
|
120
124
|
}
|
|
121
125
|
}
|