nuxt-ui-basekit 0.1.1 → 0.2.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.
- package/README.md +93 -79
- package/app/assets/css/basekit.css +23 -24
- package/app/components/BaseKitBackLink.vue +9 -9
- package/app/components/BaseKitChoiceCard.vue +19 -19
- package/app/components/BaseKitConfirmModal.vue +7 -7
- package/app/components/BaseKitDataTable.vue +30 -30
- package/app/components/BaseKitEmptyState.vue +7 -7
- package/app/components/BaseKitFileUpload.vue +5 -5
- package/app/components/BaseKitIconPicker.vue +8 -8
- package/app/components/BaseKitMarkdownEditor.vue +11 -9
- package/app/components/BaseKitPending.vue +15 -16
- package/app/components/BaseKitRecordPicker.vue +14 -14
- package/app/components/BaseKitSettingRow.vue +15 -14
- package/app/components/BaseKitStatTile.vue +8 -8
- package/app/components/BaseKitTabs.vue +33 -34
- package/app/components/BaseKitViewLink.vue +14 -14
- package/app/components/charts/BaseKitChartBars.vue +8 -9
- package/app/components/charts/BaseKitChartColumns.vue +30 -31
- package/app/components/charts/BaseKitChartDonut.vue +18 -18
- package/app/components/charts/BaseKitChartFigure.vue +10 -10
- package/app/components/charts/BaseKitChartMeter.vue +4 -4
- package/app/composables/useBaseKit.ts +68 -68
- package/app/composables/useChartPalette.ts +14 -14
- package/app/composables/useChartWidth.ts +10 -10
- package/app/composables/useConfirm.ts +3 -3
- package/app/stores/confirm.ts +16 -16
- package/app/utils/html-to-markdown.ts +14 -16
- package/app/utils/markdown.ts +8 -7
- package/package.json +1 -1
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Stacked columns over a time axis — what accrued per day.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Stacked rather than several lines because the question is share of the
|
|
6
|
+
* whole, not the course of individual series. Columns rather than an area
|
|
7
|
+
* because daily values are counted events, not a continuous quantity: an area
|
|
8
|
+
* would interpolate between two days and claim something half-happened at
|
|
9
|
+
* midnight.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Segments are separated by a 2px gap in the card colour rather than a border.
|
|
12
|
+
* A border is ink that carries no data, and on thin segments it covers the
|
|
13
|
+
* value.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* Five series at most — see `useChartPalette`.
|
|
16
16
|
*/
|
|
17
17
|
import { computed, ref } from 'vue'
|
|
18
18
|
import { baseKitChartColor, useChartFormat } from '../../composables/useChartPalette'
|
|
@@ -25,13 +25,13 @@ interface ColumnSeries {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const props = withDefaults(defineProps<{
|
|
28
|
-
/**
|
|
28
|
+
/** Label per column, ISO dates for instance. */
|
|
29
29
|
categories: string[]
|
|
30
30
|
series: ColumnSeries[]
|
|
31
31
|
height?: number
|
|
32
|
-
/**
|
|
32
|
+
/** Short axis label derived from a category. */
|
|
33
33
|
formatCategory?: (value: string) => string
|
|
34
|
-
/**
|
|
34
|
+
/** The long form, for the tooltip. */
|
|
35
35
|
formatCategoryLong?: (value: string) => string
|
|
36
36
|
ariaLabel?: string
|
|
37
37
|
}>(), {
|
|
@@ -64,7 +64,7 @@ const plot = computed(() => {
|
|
|
64
64
|
}
|
|
65
65
|
})
|
|
66
66
|
|
|
67
|
-
/**
|
|
67
|
+
/** Daily totals — they set the scale and appear in the tooltip. */
|
|
68
68
|
const totals = computed(() =>
|
|
69
69
|
props.categories.map((_, index) =>
|
|
70
70
|
props.series.reduce((sum, s) => sum + (s.points[index] ?? 0), 0),
|
|
@@ -72,9 +72,8 @@ const totals = computed(() =>
|
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* ablesbare Auskunft mehr.
|
|
75
|
+
* Upper axis bound, rounded up to something even. Without it the labels sit on
|
|
76
|
+
* awkward values like 37 and the grid lines stop telling anyone anything.
|
|
78
77
|
*/
|
|
79
78
|
const scaleMax = computed(() => {
|
|
80
79
|
const peak = Math.max(0, ...totals.value)
|
|
@@ -100,7 +99,7 @@ function columnX(index: number): number {
|
|
|
100
99
|
return PADDING.left + plot.value.band * (index + 0.5) - plot.value.columnWidth / 2
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
/**
|
|
102
|
+
/** Rounded cap on top only — the column sits flat on the baseline. */
|
|
104
103
|
function cappedPath(x: number, y: number, w: number, h: number): string {
|
|
105
104
|
const r = Math.min(4, w / 2, h)
|
|
106
105
|
if (h <= 0) return ''
|
|
@@ -129,7 +128,7 @@ const columns = computed(() => {
|
|
|
129
128
|
const segments: Segment[] = []
|
|
130
129
|
let stacked = 0
|
|
131
130
|
|
|
132
|
-
//
|
|
131
|
+
// Stack bottom to top; the topmost series gets the cap.
|
|
133
132
|
const present = props.series
|
|
134
133
|
.map((s, order) => ({ s, order, value: s.points[index] ?? 0 }))
|
|
135
134
|
.filter(entry => entry.value > 0)
|
|
@@ -140,8 +139,8 @@ const columns = computed(() => {
|
|
|
140
139
|
const top = baseline - (stacked / max) * innerHeight
|
|
141
140
|
|
|
142
141
|
const isTop = position === present.length - 1
|
|
143
|
-
//
|
|
144
|
-
//
|
|
142
|
+
// A gap above, except on the topmost segment — there it would sit
|
|
143
|
+
// between the column and thin air and merely shorten it.
|
|
145
144
|
const height = Math.max(0, bottom - top - (isTop ? 0 : GAP))
|
|
146
145
|
if (height <= 0) return
|
|
147
146
|
|
|
@@ -159,9 +158,9 @@ const columns = computed(() => {
|
|
|
159
158
|
})
|
|
160
159
|
|
|
161
160
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
161
|
+
* Thin the labels out until they stop colliding — by leaving them out, not by
|
|
162
|
+
* hiding them: hidden text stays in the tree and screen readers read it
|
|
163
|
+
* anyway.
|
|
165
164
|
*/
|
|
166
165
|
const axisLabels = computed(() => {
|
|
167
166
|
const perLabel = 56
|
|
@@ -194,7 +193,7 @@ const tooltip = computed(() => {
|
|
|
194
193
|
value: s.points[index] ?? 0,
|
|
195
194
|
}))
|
|
196
195
|
.filter(row => row.value > 0),
|
|
197
|
-
//
|
|
196
|
+
// Near the edge the box flips inward instead of running off the card.
|
|
198
197
|
anchor: PADDING.left + plot.value.band * (index + 0.5),
|
|
199
198
|
}
|
|
200
199
|
})
|
|
@@ -218,7 +217,7 @@ function readout(index: number): string {
|
|
|
218
217
|
:aria-label="props.ariaLabel"
|
|
219
218
|
class="block max-w-full overflow-visible"
|
|
220
219
|
>
|
|
221
|
-
<!--
|
|
220
|
+
<!-- Grid: solid hairlines, one step away from the surface. -->
|
|
222
221
|
<g>
|
|
223
222
|
<line
|
|
224
223
|
v-for="tick in ticks"
|
|
@@ -242,7 +241,7 @@ function readout(index: number): string {
|
|
|
242
241
|
>{{ tick.label }}</text>
|
|
243
242
|
</g>
|
|
244
243
|
|
|
245
|
-
<!--
|
|
244
|
+
<!-- Data marks -->
|
|
246
245
|
<g
|
|
247
246
|
v-for="column in columns"
|
|
248
247
|
:key="column.category"
|
|
@@ -257,8 +256,8 @@ function readout(index: number): string {
|
|
|
257
256
|
/>
|
|
258
257
|
</g>
|
|
259
258
|
|
|
260
|
-
<!--
|
|
261
|
-
|
|
259
|
+
<!-- Hit areas: full height and the whole band, so nobody has to hit a
|
|
260
|
+
14px column. -->
|
|
262
261
|
<g>
|
|
263
262
|
<rect
|
|
264
263
|
v-for="column in columns"
|
|
@@ -279,7 +278,7 @@ function readout(index: number): string {
|
|
|
279
278
|
/>
|
|
280
279
|
</g>
|
|
281
280
|
|
|
282
|
-
<!--
|
|
281
|
+
<!-- Axis labels -->
|
|
283
282
|
<text
|
|
284
283
|
v-for="label in axisLabels"
|
|
285
284
|
:key="`label-${label.category}`"
|
|
@@ -291,7 +290,7 @@ function readout(index: number): string {
|
|
|
291
290
|
>{{ label.text }}</text>
|
|
292
291
|
</svg>
|
|
293
292
|
|
|
294
|
-
<!-- Tooltip:
|
|
293
|
+
<!-- Tooltip: the value leads, the series name follows. -->
|
|
295
294
|
<div
|
|
296
295
|
v-if="tooltip"
|
|
297
296
|
class="pointer-events-none absolute top-0 z-10 min-w-40 -translate-x-1/2 rounded-md border border-default bg-default p-2 shadow-lg"
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* A ring for part-of-whole across a few classes.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Deliberately narrow in scope: a ring answers "roughly how does this split",
|
|
6
|
+
* not "which of these two is bigger". As soon as two segments sit close
|
|
7
|
+
* together, a bar is the more honest shape. Four segments at most, otherwise
|
|
8
|
+
* the small ones lose their labels.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The total goes in the middle — it is the number people look for first, and
|
|
11
|
+
* it fills the space a ring leaves empty anyway.
|
|
12
12
|
*/
|
|
13
13
|
import { computed, ref } from 'vue'
|
|
14
14
|
import { baseKitChartColor, useChartFormat } from '../../composables/useChartPalette'
|
|
@@ -21,7 +21,7 @@ interface DonutSlice {
|
|
|
21
21
|
|
|
22
22
|
const props = withDefaults(defineProps<{
|
|
23
23
|
slices: DonutSlice[]
|
|
24
|
-
/**
|
|
24
|
+
/** Label below the total in the centre. */
|
|
25
25
|
centerLabel?: string
|
|
26
26
|
size?: number
|
|
27
27
|
ariaLabel?: string
|
|
@@ -34,7 +34,7 @@ const props = withDefaults(defineProps<{
|
|
|
34
34
|
const { number, percent } = useChartFormat()
|
|
35
35
|
|
|
36
36
|
const THICKNESS = 18
|
|
37
|
-
/**
|
|
37
|
+
/** Gap between segments, in degrees — the counterpart to the 2px gap. */
|
|
38
38
|
const GAP_DEGREES = 2
|
|
39
39
|
|
|
40
40
|
const active = ref<string | null>(null)
|
|
@@ -61,8 +61,8 @@ const arcs = computed(() => {
|
|
|
61
61
|
const start = cursor
|
|
62
62
|
cursor += sweep
|
|
63
63
|
|
|
64
|
-
//
|
|
65
|
-
//
|
|
64
|
+
// A segment filling the whole circle would have identical start and end
|
|
65
|
+
// points and the arc would vanish. Draw the full ring instead.
|
|
66
66
|
const full = sweep >= 359.9
|
|
67
67
|
const gap = full ? 0 : Math.min(GAP_DEGREES, sweep / 3)
|
|
68
68
|
const from = start + gap / 2
|
|
@@ -95,10 +95,10 @@ const legend = computed(() =>
|
|
|
95
95
|
</script>
|
|
96
96
|
|
|
97
97
|
<template>
|
|
98
|
-
<!--
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
<!-- Legend below the ring, not beside it: the card often sits in a narrow
|
|
99
|
+
column, and side by side so little was left for the labels that a word
|
|
100
|
+
like "Discontinued" shrank to a "D". Stacked, every row has the full
|
|
101
|
+
width. -->
|
|
102
102
|
<div class="flex flex-col items-center gap-5">
|
|
103
103
|
<svg
|
|
104
104
|
:width="props.size"
|
|
@@ -108,8 +108,8 @@ const legend = computed(() =>
|
|
|
108
108
|
:aria-label="props.ariaLabel"
|
|
109
109
|
class="shrink-0"
|
|
110
110
|
>
|
|
111
|
-
<!--
|
|
112
|
-
|
|
111
|
+
<!-- An empty ring when there is nothing: the shape stays recognisable
|
|
112
|
+
instead of the card collapsing into a blank area. -->
|
|
113
113
|
<circle
|
|
114
114
|
v-if="!arcs.length"
|
|
115
115
|
:cx="center"
|
|
@@ -156,7 +156,7 @@ const legend = computed(() =>
|
|
|
156
156
|
>{{ props.centerLabel }}</text>
|
|
157
157
|
</svg>
|
|
158
158
|
|
|
159
|
-
<!--
|
|
159
|
+
<!-- The legend carries the identity, not the colour alone. -->
|
|
160
160
|
<ul class="w-full space-y-1.5">
|
|
161
161
|
<li
|
|
162
162
|
v-for="entry in legend"
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* The frame around a chart: card, title, legend, and the switch to the table
|
|
4
|
+
* view.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* That switch is not a nicety. A chart encodes values through colour and
|
|
7
|
+
* length; anyone who gets nothing out of that — a colour vision deficiency, a
|
|
8
|
+
* screen reader, a printout — needs the same numbers in readable form. Which
|
|
9
|
+
* is why every card here brings its table along, and why the switch is visible
|
|
10
|
+
* rather than buried in a menu.
|
|
11
11
|
*/
|
|
12
12
|
import { computed, ref } from 'vue'
|
|
13
13
|
import { useBaseKitLabels } from '../../composables/useBaseKit'
|
|
@@ -15,11 +15,11 @@ import { useBaseKitLabels } from '../../composables/useBaseKit'
|
|
|
15
15
|
const props = withDefaults(defineProps<{
|
|
16
16
|
title: string
|
|
17
17
|
subtitle?: string
|
|
18
|
-
/**
|
|
18
|
+
/** Where the card leads. Without a target the title stays a title. */
|
|
19
19
|
href?: string
|
|
20
|
-
/**
|
|
20
|
+
/** Grey the chart out while something reloads in the background. */
|
|
21
21
|
stale?: boolean
|
|
22
|
-
/**
|
|
22
|
+
/** Without a table slot the switch disappears. */
|
|
23
23
|
tableAvailable?: boolean
|
|
24
24
|
}>(), {
|
|
25
25
|
subtitle: undefined,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* A ratio against a known limit — this many of that many.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* A meter presupposes that a whole exists. Where none does — bytes without a
|
|
6
|
+
* quota, a count without a ceiling — the number belongs in a tile and not
|
|
7
|
+
* here; a full bar would otherwise claim a limit had been reached.
|
|
8
8
|
*/
|
|
9
9
|
import { computed } from 'vue'
|
|
10
10
|
import { baseKitChartColor, useChartFormat } from '../../composables/useChartPalette'
|
|
@@ -1,55 +1,54 @@
|
|
|
1
1
|
import { computed, inject, type ComputedRef, type InjectionKey } from 'vue'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* die sie einsetzt.
|
|
4
|
+
* The only wire between the BaseKit components and the application using them.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* The components are meant to run in any Nuxt project. That rules out calling
|
|
7
|
+
* `vue-i18n` and it rules out knowing translation keys: `t('common.search')`
|
|
8
|
+
* exists only in the one application that carries that key, and a package
|
|
9
|
+
* which presupposes such keys is not a package.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Instead the application hands language and labels in once, from a plugin
|
|
12
|
+
* that pulls the values out of its own source:
|
|
14
13
|
*
|
|
15
14
|
* // plugins/basekit.ts
|
|
16
15
|
* export default defineNuxtPlugin((nuxtApp) => {
|
|
17
16
|
* const config = computed(() => ({
|
|
18
17
|
* locale: 'de-DE',
|
|
19
|
-
* labels: { ...BASEKIT_DEFAULTS.labels, search: 'Suchen' },
|
|
18
|
+
* labels: { ...BASEKIT_DEFAULTS.labels, search: 'Suchen', cancel: 'Abbrechen' },
|
|
20
19
|
* }))
|
|
21
20
|
* nuxtApp.vueApp.provide(baseKitKey, config)
|
|
22
21
|
* })
|
|
23
22
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* Provide nothing and the defaults below apply. One thing worth knowing when
|
|
24
|
+
* the values come from an i18n library: `useI18n()` requires a component setup
|
|
25
|
+
* context and throws inside a plugin. Take the instance from `nuxtApp.$i18n`,
|
|
26
|
+
* and only when the `computed` is read.
|
|
28
27
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* Over `provide`/`inject` rather than `useNuxtApp()`: that way the components
|
|
29
|
+
* also run in a Vitest mount without a Nuxt context, and two requests share
|
|
30
|
+
* nothing under SSR.
|
|
32
31
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* Individual labels stay overridable as props — what follows only decides what
|
|
33
|
+
* comes out when nothing is passed.
|
|
35
34
|
*/
|
|
36
35
|
export interface BaseKitLabels {
|
|
37
|
-
/**
|
|
36
|
+
/** Search field above lists and pickers. */
|
|
38
37
|
search: string
|
|
39
|
-
/** Filter
|
|
38
|
+
/** Filter entry for "no restriction". */
|
|
40
39
|
all: string
|
|
41
40
|
select: string
|
|
42
41
|
change: string
|
|
43
42
|
edit: string
|
|
44
43
|
cancel: string
|
|
45
44
|
confirm: string
|
|
46
|
-
/**
|
|
45
|
+
/** Heading of the confirmation when the caller passes none. */
|
|
47
46
|
confirmTitle: string
|
|
48
|
-
/**
|
|
47
|
+
/** Body of the confirmation when the caller passes none. */
|
|
49
48
|
confirmBody: string
|
|
50
|
-
/**
|
|
49
|
+
/** Empty list — nothing exists yet. */
|
|
51
50
|
empty: string
|
|
52
|
-
/**
|
|
51
|
+
/** Empty list — the search ran and found nothing. */
|
|
53
52
|
noResults: string
|
|
54
53
|
noResultsHint: string
|
|
55
54
|
back: string
|
|
@@ -58,15 +57,15 @@ export interface BaseKitLabels {
|
|
|
58
57
|
perPage: string
|
|
59
58
|
iconChoose: string
|
|
60
59
|
iconEmpty: string
|
|
61
|
-
/**
|
|
60
|
+
/** Clearing the choice — the empty option in the icon picker. */
|
|
62
61
|
iconClear: string
|
|
63
62
|
chartAsTable: string
|
|
64
63
|
chartAsChart: string
|
|
65
|
-
/**
|
|
64
|
+
/** "1–25 of 300" — a function, because the numbers sit inside the sentence. */
|
|
66
65
|
paginationRange: (range: { from: number, to: number, total: number }) => string
|
|
67
66
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
67
|
+
* Toolbar of the Markdown editor. Its own group, because the terms occur
|
|
68
|
+
* nowhere else and the top level would otherwise be half made of them.
|
|
70
69
|
*/
|
|
71
70
|
markdown: {
|
|
72
71
|
bold: string
|
|
@@ -78,60 +77,61 @@ export interface BaseKitLabels {
|
|
|
78
77
|
quote: string
|
|
79
78
|
code: string
|
|
80
79
|
link: string
|
|
81
|
-
/**
|
|
80
|
+
/** Prompt shown when setting a link. */
|
|
82
81
|
linkPrompt: string
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
export interface BaseKitConfig {
|
|
87
86
|
/**
|
|
88
|
-
* BCP-47
|
|
89
|
-
*
|
|
87
|
+
* BCP-47 tag for `Intl` — drives thousands separators, percentages and date
|
|
88
|
+
* formats in the charts.
|
|
90
89
|
*/
|
|
91
90
|
locale: string
|
|
92
91
|
labels: BaseKitLabels
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
95
|
+
* Defaults: English. The package cannot know what language the application
|
|
96
|
+
* runs in, and English is the one that shuts out the fewest readers. Anything
|
|
97
|
+
* else is handed in through the plugin above.
|
|
98
98
|
*/
|
|
99
99
|
export const BASEKIT_DEFAULTS: BaseKitConfig = {
|
|
100
|
-
locale: '
|
|
100
|
+
locale: 'en-US',
|
|
101
101
|
labels: {
|
|
102
|
-
search: '
|
|
103
|
-
all: '
|
|
104
|
-
select: '
|
|
105
|
-
change: '
|
|
106
|
-
edit: '
|
|
107
|
-
cancel: '
|
|
108
|
-
confirm: '
|
|
109
|
-
confirmTitle: '
|
|
110
|
-
confirmBody: '
|
|
111
|
-
empty: '
|
|
112
|
-
noResults: '
|
|
113
|
-
noResultsHint: '
|
|
114
|
-
back: '
|
|
115
|
-
view: '
|
|
116
|
-
upload: '
|
|
117
|
-
perPage: '
|
|
118
|
-
iconChoose: '
|
|
119
|
-
iconEmpty: '
|
|
120
|
-
iconClear: '
|
|
121
|
-
chartAsTable: '
|
|
122
|
-
chartAsChart: '
|
|
123
|
-
paginationRange: ({ from, to, total }) => `${from}–${to}
|
|
102
|
+
search: 'Search',
|
|
103
|
+
all: 'All',
|
|
104
|
+
select: 'Select',
|
|
105
|
+
change: 'Change',
|
|
106
|
+
edit: 'Edit',
|
|
107
|
+
cancel: 'Cancel',
|
|
108
|
+
confirm: 'Confirm',
|
|
109
|
+
confirmTitle: 'Are you sure?',
|
|
110
|
+
confirmBody: 'This action cannot be undone.',
|
|
111
|
+
empty: 'Nothing here yet',
|
|
112
|
+
noResults: 'No matches',
|
|
113
|
+
noResultsHint: 'Try a different spelling or fewer filters.',
|
|
114
|
+
back: 'Back to overview',
|
|
115
|
+
view: 'View',
|
|
116
|
+
upload: 'Choose file',
|
|
117
|
+
perPage: 'per page',
|
|
118
|
+
iconChoose: 'Choose icon',
|
|
119
|
+
iconEmpty: 'No icon found',
|
|
120
|
+
iconClear: 'No icon',
|
|
121
|
+
chartAsTable: 'As table',
|
|
122
|
+
chartAsChart: 'As chart',
|
|
123
|
+
paginationRange: ({ from, to, total }) => `${from}–${to} of ${total}`,
|
|
124
124
|
markdown: {
|
|
125
|
-
bold: '
|
|
126
|
-
italic: '
|
|
127
|
-
h2: '
|
|
128
|
-
h3: '
|
|
129
|
-
bullet: '
|
|
130
|
-
ordered: '
|
|
131
|
-
quote: '
|
|
125
|
+
bold: 'Bold',
|
|
126
|
+
italic: 'Italic',
|
|
127
|
+
h2: 'Heading 2',
|
|
128
|
+
h3: 'Heading 3',
|
|
129
|
+
bullet: 'Bullet list',
|
|
130
|
+
ordered: 'Numbered list',
|
|
131
|
+
quote: 'Quote',
|
|
132
132
|
code: 'Code',
|
|
133
133
|
link: 'Link',
|
|
134
|
-
linkPrompt: 'Link
|
|
134
|
+
linkPrompt: 'Link URL',
|
|
135
135
|
},
|
|
136
136
|
},
|
|
137
137
|
}
|
|
@@ -139,16 +139,16 @@ export const BASEKIT_DEFAULTS: BaseKitConfig = {
|
|
|
139
139
|
export const baseKitKey: InjectionKey<ComputedRef<BaseKitConfig>> = Symbol('basekit')
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
142
|
+
* Configuration for a BaseKit component. With nothing provided the defaults
|
|
143
|
+
* apply — the component then renders in English instead of showing empty
|
|
144
|
+
* labels.
|
|
145
145
|
*/
|
|
146
146
|
export function useBaseKit(): ComputedRef<BaseKitConfig> {
|
|
147
147
|
const provided = inject(baseKitKey, null)
|
|
148
148
|
return provided ?? computed(() => BASEKIT_DEFAULTS)
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
/**
|
|
151
|
+
/** Short form for the common case of only needing the labels. */
|
|
152
152
|
export function useBaseKitLabels(): ComputedRef<BaseKitLabels> {
|
|
153
153
|
const config = useBaseKit()
|
|
154
154
|
return computed(() => config.value.labels)
|
|
@@ -2,17 +2,17 @@ import { computed } from 'vue'
|
|
|
2
2
|
import { useBaseKit } from './useBaseKit'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Series colours and number formats for the `BaseKitChart*` components.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* The colours live as CSS variables in `app/assets/css/basekit.css` and are
|
|
8
|
+
* validated as a **set**, order included. That is why there is lookup by index
|
|
9
|
+
* here and no generation: a sixth colour would be indistinguishable from one
|
|
10
|
+
* of the five under a colour vision deficiency. More series than that means
|
|
11
|
+
* grouping them.
|
|
12
12
|
*/
|
|
13
13
|
export const BASEKIT_CHART_SLOTS = 5
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/** Colour for series `index`, zero-based. From slot 6 on, the muted grey. */
|
|
16
16
|
export function baseKitChartColor(index: number): string {
|
|
17
17
|
return index < BASEKIT_CHART_SLOTS
|
|
18
18
|
? `var(--basekit-chart-${index + 1})`
|
|
@@ -33,7 +33,7 @@ export function useChartFormat(): {
|
|
|
33
33
|
return new Intl.NumberFormat(locale.value).format(value)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/** For values on ticks and in tiles: 12,400 becomes 12.4K. */
|
|
37
37
|
function compact(value: number): string {
|
|
38
38
|
return value < 10000
|
|
39
39
|
? number(value)
|
|
@@ -41,9 +41,9 @@ export function useChartFormat(): {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* A share in percent, rounded down as long as the whole has not been
|
|
45
|
+
* reached — otherwise 9,999 out of 10,000 would pass as "100%" and the one
|
|
46
|
+
* missing item would be rounded out of sight.
|
|
47
47
|
*/
|
|
48
48
|
function percent(value: number, total: number): string {
|
|
49
49
|
if (total <= 0) return '—'
|
|
@@ -61,8 +61,8 @@ export function useChartFormat(): {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
64
|
+
* Decimal prefixes (kB, MB, GB), not KiB/MiB. Storage is billed that way,
|
|
65
|
+
* and the number here should match the one on the invoice.
|
|
66
66
|
*/
|
|
67
67
|
function bytes(value: number): string {
|
|
68
68
|
const units = ['B', 'kB', 'MB', 'GB', 'TB']
|
|
@@ -76,7 +76,7 @@ export function useChartFormat(): {
|
|
|
76
76
|
return `${new Intl.NumberFormat(locale.value, { maximumFractionDigits: digits }).format(size)} ${units[unit]}`
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/**
|
|
79
|
+
/** Runtime as "14 h 20 min"; below an hour, minutes only. */
|
|
80
80
|
function duration(seconds: number): string {
|
|
81
81
|
const total = Math.max(0, Math.round(seconds / 60))
|
|
82
82
|
const hours = Math.floor(total / 60)
|