@prestack/thread-ui 0.1.10 → 0.1.11
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/CHANGELOG.md +187 -0
- package/dist/ui/attach.js +6 -7
- package/dist/ui/compositions.d.ts +0 -8
- package/dist/ui/compositions.js +3 -166
- package/dist/ui/generators/routes.js +1 -0
- package/package.json +2 -2
- package/dist/ui/animations/anm_appear.ts +0 -13
- package/dist/ui/animations/anm_blur_in.ts +0 -19
- package/dist/ui/animations/anm_blur_out.ts +0 -18
- package/dist/ui/animations/anm_dismiss.ts +0 -18
- package/dist/ui/animations/anm_drop.ts +0 -19
- package/dist/ui/animations/anm_fall.ts +0 -18
- package/dist/ui/animations/anm_flash.ts +0 -18
- package/dist/ui/animations/anm_glow.ts +0 -16
- package/dist/ui/animations/anm_lift.ts +0 -16
- package/dist/ui/animations/anm_nudge.ts +0 -16
- package/dist/ui/animations/anm_pop_in.ts +0 -23
- package/dist/ui/animations/anm_pop_out.ts +0 -22
- package/dist/ui/animations/anm_press.ts +0 -12
- package/dist/ui/animations/anm_pulse.ts +0 -13
- package/dist/ui/animations/anm_rise.ts +0 -19
- package/dist/ui/animations/anm_shake.ts +0 -13
- package/dist/ui/animations/anm_slide_in_left.ts +0 -19
- package/dist/ui/animations/anm_slide_in_right.ts +0 -19
- package/dist/ui/animations/anm_slide_out_left.ts +0 -18
- package/dist/ui/animations/anm_slide_out_right.ts +0 -18
- package/dist/ui/animations/anm_spin.ts +0 -12
- package/dist/ui/animations/anm_swell.ts +0 -16
- package/dist/ui/animations/anm_wobble.ts +0 -24
- package/dist/ui/animations/anm_zoom_in.ts +0 -19
- package/dist/ui/animations/anm_zoom_out.ts +0 -18
- package/dist/ui/animations.ts +0 -308
- package/dist/ui/attach.ts +0 -553
- package/dist/ui/charts.tsx +0 -69
- package/dist/ui/checked.ts +0 -32
- package/dist/ui/collector.ts +0 -140
- package/dist/ui/compositions.ts +0 -613
- package/dist/ui/consent.ts +0 -55
- package/dist/ui/content.tsx +0 -27
- package/dist/ui/contrast.ts +0 -27
- package/dist/ui/controls.tsx +0 -25
- package/dist/ui/disclosure.tsx +0 -20
- package/dist/ui/dismiss-layer.ts +0 -87
- package/dist/ui/door.ts +0 -140
- package/dist/ui/drag.ts +0 -316
- package/dist/ui/elements/accordion.tsx +0 -47
- package/dist/ui/elements/accordion.works.ts +0 -66
- package/dist/ui/elements/aspect-ratio.tsx +0 -31
- package/dist/ui/elements/aspect-ratio.works.ts +0 -5
- package/dist/ui/elements/avatar.tsx +0 -34
- package/dist/ui/elements/avatar.works.ts +0 -16
- package/dist/ui/elements/badge.tsx +0 -33
- package/dist/ui/elements/badge.works.ts +0 -9
- package/dist/ui/elements/bars.tsx +0 -50
- package/dist/ui/elements/bars.works.ts +0 -29
- package/dist/ui/elements/box.tsx +0 -36
- package/dist/ui/elements/breadcrumb.tsx +0 -38
- package/dist/ui/elements/breadcrumb.works.ts +0 -24
- package/dist/ui/elements/button.tsx +0 -46
- package/dist/ui/elements/button.works.tsx +0 -52
- package/dist/ui/elements/column.tsx +0 -29
- package/dist/ui/elements/consent.tsx +0 -71
- package/dist/ui/elements/consent.works.tsx +0 -132
- package/dist/ui/elements/donut.tsx +0 -45
- package/dist/ui/elements/donut.works.ts +0 -42
- package/dist/ui/elements/each.tsx +0 -34
- package/dist/ui/elements/each.works.tsx +0 -82
- package/dist/ui/elements/empty.tsx +0 -33
- package/dist/ui/elements/field.tsx +0 -45
- package/dist/ui/elements/field.works.ts +0 -65
- package/dist/ui/elements/funnel.tsx +0 -50
- package/dist/ui/elements/funnel.works.ts +0 -37
- package/dist/ui/elements/gauge.tsx +0 -40
- package/dist/ui/elements/gauge.works.ts +0 -42
- package/dist/ui/elements/glyph.tsx +0 -30
- package/dist/ui/elements/glyph.works.tsx +0 -42
- package/dist/ui/elements/grid.tsx +0 -29
- package/dist/ui/elements/heatmap.tsx +0 -48
- package/dist/ui/elements/heatmap.works.ts +0 -24
- package/dist/ui/elements/hit-box.tsx +0 -47
- package/dist/ui/elements/hit-box.works.tsx +0 -576
- package/dist/ui/elements/image.tsx +0 -29
- package/dist/ui/elements/line-chart.tsx +0 -45
- package/dist/ui/elements/line-chart.works.ts +0 -41
- package/dist/ui/elements/link.tsx +0 -38
- package/dist/ui/elements/link.works.ts +0 -25
- package/dist/ui/elements/media.tsx +0 -57
- package/dist/ui/elements/menu.tsx +0 -51
- package/dist/ui/elements/menu.works.tsx +0 -154
- package/dist/ui/elements/money.tsx +0 -36
- package/dist/ui/elements/money.works.ts +0 -9
- package/dist/ui/elements/pagination.tsx +0 -39
- package/dist/ui/elements/pagination.works.ts +0 -20
- package/dist/ui/elements/progress.tsx +0 -34
- package/dist/ui/elements/progress.works.ts +0 -8
- package/dist/ui/elements/radio-group.tsx +0 -50
- package/dist/ui/elements/radio-group.works.ts +0 -33
- package/dist/ui/elements/row.tsx +0 -29
- package/dist/ui/elements/scatter.tsx +0 -42
- package/dist/ui/elements/scatter.works.ts +0 -38
- package/dist/ui/elements/select.tsx +0 -43
- package/dist/ui/elements/select.works.ts +0 -5
- package/dist/ui/elements/separator.tsx +0 -31
- package/dist/ui/elements/skeleton.tsx +0 -31
- package/dist/ui/elements/slider.tsx +0 -34
- package/dist/ui/elements/slider.works.ts +0 -5
- package/dist/ui/elements/sparkline.tsx +0 -37
- package/dist/ui/elements/sparkline.works.ts +0 -32
- package/dist/ui/elements/spinner.tsx +0 -33
- package/dist/ui/elements/stack.tsx +0 -29
- package/dist/ui/elements/stat.tsx +0 -36
- package/dist/ui/elements/stat.works.ts +0 -14
- package/dist/ui/elements/surface.tsx +0 -36
- package/dist/ui/elements/surface.works.ts +0 -9
- package/dist/ui/elements/table.tsx +0 -62
- package/dist/ui/elements/table.works.ts +0 -30
- package/dist/ui/elements/tabs.tsx +0 -47
- package/dist/ui/elements/tabs.works.ts +0 -84
- package/dist/ui/elements/text.tsx +0 -33
- package/dist/ui/elements/textarea.tsx +0 -38
- package/dist/ui/elements/textarea.works.ts +0 -5
- package/dist/ui/elements/time.tsx +0 -40
- package/dist/ui/elements/time.works.ts +0 -29
- package/dist/ui/elements/toggle.tsx +0 -34
- package/dist/ui/elements/toggle.works.ts +0 -5
- package/dist/ui/elements/tooltip.tsx +0 -40
- package/dist/ui/elements/tooltip.works.ts +0 -77
- package/dist/ui/elements/value.tsx +0 -38
- package/dist/ui/elements/value.works.ts +0 -9
- package/dist/ui/elements.ts +0 -127
- package/dist/ui/focus-scope.ts +0 -121
- package/dist/ui/fonts.ts +0 -355
- package/dist/ui/generators/dev.d.ts +0 -1
- package/dist/ui/generators/dev.js +0 -62
- package/dist/ui/generators/dev.ts +0 -75
- package/dist/ui/generators/elements.ts +0 -796
- package/dist/ui/generators/i18n.ts +0 -283
- package/dist/ui/generators/icons.ts +0 -207
- package/dist/ui/generators/react.ts +0 -267
- package/dist/ui/generators/routes.ts +0 -847
- package/dist/ui/generators/show.ts +0 -261
- package/dist/ui/generators/styles.ts +0 -677
- package/dist/ui/generators/variants.ts +0 -196
- package/dist/ui/i18n/catalogs/en.ts +0 -191
- package/dist/ui/i18n/format.ts +0 -221
- package/dist/ui/i18n/locales.ts +0 -33
- package/dist/ui/i18n/parse.ts +0 -315
- package/dist/ui/i18n/translator.ts +0 -31
- package/dist/ui/i18n/types.ts +0 -96
- package/dist/ui/icons.ts +0 -331
- package/dist/ui/islands/map.ts +0 -58
- package/dist/ui/localizations.ts +0 -75
- package/dist/ui/material.ts +0 -236
- package/dist/ui/materials/mat_bordered.ts +0 -13
- package/dist/ui/materials/mat_ceramic.ts +0 -13
- package/dist/ui/materials/mat_clay.ts +0 -13
- package/dist/ui/materials/mat_elevated.ts +0 -24
- package/dist/ui/materials/mat_flat.ts +0 -34
- package/dist/ui/materials/mat_glass.ts +0 -20
- package/dist/ui/materials/mat_lit.ts +0 -13
- package/dist/ui/materials/mat_moulded.ts +0 -11
- package/dist/ui/materials.ts +0 -197
- package/dist/ui/modes.ts +0 -44
- package/dist/ui/navigation.tsx +0 -29
- package/dist/ui/positioning.ts +0 -90
- package/dist/ui/presented.tsx +0 -119
- package/dist/ui/provider.tsx +0 -184
- package/dist/ui/render-loop.ts +0 -133
- package/dist/ui/roving-focus.ts +0 -161
- package/dist/ui/screens.ts +0 -141
- package/dist/ui/scroll-lock.ts +0 -28
- package/dist/ui/series.ts +0 -97
- package/dist/ui/signals.ts +0 -386
- package/dist/ui/styleguide.ts +0 -231
- package/dist/ui/styleguides.ts +0 -374
- package/dist/ui/themes.ts +0 -22
- package/dist/ui/token.ts +0 -78
- package/dist/ui/tokens/shadow.ts +0 -25
- package/dist/ui/tokens.ts +0 -234
- package/dist/ui/validators.ts +0 -232
- package/dist/ui/variant.ts +0 -349
- package/dist/ui/variants/var_avatar-round.ts +0 -14
- package/dist/ui/variants/var_bubble-me.ts +0 -14
- package/dist/ui/variants/var_bubble-them.ts +0 -13
- package/dist/ui/variants/var_button-border.ts +0 -76
- package/dist/ui/variants/var_button-cta.ts +0 -76
- package/dist/ui/variants/var_button-destructive-ghost.ts +0 -72
- package/dist/ui/variants/var_button-destructive.ts +0 -76
- package/dist/ui/variants/var_button-fill.ts +0 -76
- package/dist/ui/variants/var_button-ghost.ts +0 -72
- package/dist/ui/variants/var_button-glass.ts +0 -55
- package/dist/ui/variants/var_button-normal.ts +0 -76
- package/dist/ui/variants/var_button-quiet.ts +0 -74
- package/dist/ui/variants/var_card.ts +0 -17
- package/dist/ui/variants/var_chip.ts +0 -13
- package/dist/ui/variants/var_choice.ts +0 -15
- package/dist/ui/variants/var_framed.ts +0 -10
- package/dist/ui/variants/var_glyph-plain.ts +0 -11
- package/dist/ui/variants/var_input.ts +0 -43
- package/dist/ui/variants/var_link-inline.ts +0 -29
- package/dist/ui/variants/var_list-row.ts +0 -34
- package/dist/ui/variants/var_rule.ts +0 -10
- package/dist/ui/variants/var_skeleton-block.ts +0 -12
- package/dist/ui/variants/var_spinner-turning.ts +0 -12
- package/dist/ui/variants/var_stat-tile.ts +0 -13
- package/dist/ui/variants/var_tab-bar.ts +0 -15
- package/dist/ui/variants/var_tap-target.ts +0 -25
- package/dist/ui/variants.ts +0 -64
- package/dist/ui/wrappers.tsx +0 -429
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
|
|
5
|
-
import { consentCategories, type ConsentCategory, type ConsentDecision } from '../consent.ts'
|
|
6
|
-
import type { Row } from '../charts.tsx'
|
|
7
|
-
import type { ButtonProps } from './button.tsx'
|
|
8
|
-
import type { ConsentProps } from './consent.tsx'
|
|
9
|
-
import type { TextProps } from './text.tsx'
|
|
10
|
-
import type { ToggleProps } from './toggle.tsx'
|
|
11
|
-
|
|
12
|
-
type Kept = {
|
|
13
|
-
readonly categories: readonly ConsentCategory[]
|
|
14
|
-
readonly selected: readonly string[]
|
|
15
|
-
readonly accepted: boolean
|
|
16
|
-
readonly offered: readonly string[]
|
|
17
|
-
readonly toggle: (id: string, on: boolean) => void
|
|
18
|
-
readonly decide: (decision: ConsentDecision['decision'], accepted: readonly string[]) => void
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function firstSelection(
|
|
22
|
-
categories: readonly ConsentCategory[],
|
|
23
|
-
decision: ConsentDecision | undefined,
|
|
24
|
-
preselected: readonly string[],
|
|
25
|
-
): readonly string[] {
|
|
26
|
-
const answered = decision !== undefined
|
|
27
|
-
return categories
|
|
28
|
-
.filter(
|
|
29
|
-
(one) =>
|
|
30
|
-
one.required ||
|
|
31
|
-
(answered
|
|
32
|
-
? decision.decision === 'accepted' && decision.accepted.includes(one.id)
|
|
33
|
-
: preselected.includes(one.id)),
|
|
34
|
-
)
|
|
35
|
-
.map((one) => one.id)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function uses(props: ConsentProps): {
|
|
39
|
-
readonly holds?: readonly string[]
|
|
40
|
-
readonly animates?: boolean
|
|
41
|
-
readonly on?: Readonly<Record<string, unknown>>
|
|
42
|
-
readonly categories: readonly ConsentCategory[]
|
|
43
|
-
readonly selected: readonly string[]
|
|
44
|
-
readonly accepted: boolean
|
|
45
|
-
readonly offered: readonly string[]
|
|
46
|
-
readonly toggle: (id: string, on: boolean) => void
|
|
47
|
-
readonly decide: (decision: ConsentDecision['decision'], accepted: readonly string[]) => void
|
|
48
|
-
} {
|
|
49
|
-
const categories = consentCategories()
|
|
50
|
-
const [selected, setSelected] = useState<readonly string[]>(() =>
|
|
51
|
-
firstSelection(categories, props.decision, props.preselected ?? []),
|
|
52
|
-
)
|
|
53
|
-
const offered = categories.map((one) => one.id)
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
categories,
|
|
57
|
-
selected,
|
|
58
|
-
offered,
|
|
59
|
-
accepted: props.decision?.decision === 'accepted',
|
|
60
|
-
toggle: (id: string, on: boolean): void => {
|
|
61
|
-
setSelected((was) => (on ? [...was, id] : was.filter((one) => one !== id)))
|
|
62
|
-
},
|
|
63
|
-
decide: (decision: ConsentDecision['decision'], accepted: readonly string[]): void => {
|
|
64
|
-
props.onDecide({ decision, accepted: [...accepted], offered })
|
|
65
|
-
},
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function categories(_props: ConsentProps, kept: Kept): readonly Row[] {
|
|
70
|
-
return kept.categories as unknown as readonly Row[]
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function hasPolicy(props: ConsentProps, _kept: Kept): boolean {
|
|
74
|
-
return props.policyKey !== undefined
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function withdrawable(_props: ConsentProps, kept: Kept): boolean {
|
|
78
|
-
return kept.accepted
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function optional(_props: ConsentProps, _kept: Kept, row: Row): boolean {
|
|
82
|
-
return row['required'] !== true
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function required(_props: ConsentProps, _kept: Kept, row: Row): boolean {
|
|
86
|
-
return row['required'] === true
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function title(_props: ConsentProps, _kept: Kept, _at?: number): TextProps {
|
|
90
|
-
return { k: 'consent.title', is: 'heading' }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function policy(props: ConsentProps, _kept: Kept, _at?: number): TextProps {
|
|
94
|
-
return { k: props.policyKey ?? 'consent.title', is: 'body' }
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function label(_props: ConsentProps, _kept: Kept, row: Row, _at: number): TextProps {
|
|
98
|
-
return { k: String(row['labelKey']), is: 'label' }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function describe(_props: ConsentProps, _kept: Kept, row: Row, _at: number): TextProps {
|
|
102
|
-
return { k: String(row['describeKey']), is: 'body' }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function fixed(_props: ConsentProps, _kept: Kept, _row: Row, _at: number): TextProps {
|
|
106
|
-
return { k: 'consent.required', is: 'label' }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function chooser(_props: ConsentProps, kept: Kept, row: Row, _at?: number): ToggleProps {
|
|
110
|
-
const id = String(row['id'])
|
|
111
|
-
return {
|
|
112
|
-
labelKey: String(row['labelKey']),
|
|
113
|
-
on: kept.selected.includes(id),
|
|
114
|
-
onToggle: (on: boolean): void => kept.toggle(id, on),
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function withdraw(_props: ConsentProps, kept: Kept): ButtonProps {
|
|
119
|
-
return { label: 'consent.withdraw', onPress: (): void => kept.decide('withdrawn', []) }
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function decline(_props: ConsentProps, kept: Kept): ButtonProps {
|
|
123
|
-
return { label: 'consent.decline', onPress: (): void => kept.decide('declined', []) }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function accept(_props: ConsentProps, kept: Kept): ButtonProps {
|
|
127
|
-
return { label: 'consent.accept', onPress: (): void => kept.decide('accepted', kept.selected) }
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function acceptAll(_props: ConsentProps, kept: Kept): ButtonProps {
|
|
131
|
-
return { label: 'consent.acceptAll', onPress: (): void => kept.decide('accepted', kept.offered) }
|
|
132
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type { MessageKey } from '../content.tsx'
|
|
4
|
-
import type { Row } from '../charts.tsx'
|
|
5
|
-
import { roleClass } from '../fonts.ts'
|
|
6
|
-
|
|
7
|
-
import type { Dispatching } from '../checked.ts'
|
|
8
|
-
import { type Element, element, useSurface } from '../wrappers.tsx'
|
|
9
|
-
import { Each } from '../elements/each.tsx'
|
|
10
|
-
import { slice, uses } from './donut.works.ts'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export interface DonutProps {
|
|
14
|
-
readonly rows: readonly Row[]
|
|
15
|
-
readonly by: string
|
|
16
|
-
readonly value: string
|
|
17
|
-
readonly titleKey?: MessageKey
|
|
18
|
-
readonly dispatch?: keyof Dispatching
|
|
19
|
-
readonly input?: Readonly<Record<string, unknown>>
|
|
20
|
-
readonly invokeOn?: 'press' | 'shown'
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const Donut: Element<'Donut', DonutProps> = element(
|
|
24
|
-
'Donut',
|
|
25
|
-
(props: DonutProps, className) => {
|
|
26
|
-
const kept = uses(props)
|
|
27
|
-
const surface = useSurface({ className: roleClass(undefined, className),
|
|
28
|
-
invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) },
|
|
29
|
-
holds: [
|
|
30
|
-
...(kept.holds ?? []),
|
|
31
|
-
],
|
|
32
|
-
...(kept.animates === undefined ? {} : { animates: kept.animates }), })
|
|
33
|
-
return (
|
|
34
|
-
<svg className={surface.className} {...surface.marks} {...surface.invokes} {...surface.a11y} {...(kept.on ?? {})} role="img" {...(props.titleKey === undefined ? {} : { 'aria-label': surface.text(props.titleKey) })}>
|
|
35
|
-
<Each rows={props.rows} by={props.by}>
|
|
36
|
-
{(each0, at) => (
|
|
37
|
-
<circle {...slice(props, kept, each0, at)}></circle>
|
|
38
|
-
)}
|
|
39
|
-
</Each>
|
|
40
|
-
</svg>
|
|
41
|
-
)
|
|
42
|
-
},
|
|
43
|
-
'e-donut',
|
|
44
|
-
{"card":"thread-card"},
|
|
45
|
-
)
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { seriesColour } from '../series.ts'
|
|
2
|
-
import { num, useRamp, type Row } from '../charts.tsx'
|
|
3
|
-
import type { DonutProps } from './donut.tsx'
|
|
4
|
-
|
|
5
|
-
const RADIUS = 40
|
|
6
|
-
const ROUND = 2 * Math.PI * RADIUS
|
|
7
|
-
|
|
8
|
-
export function uses(props: DonutProps): {
|
|
9
|
-
readonly holds?: readonly string[]
|
|
10
|
-
readonly animates?: boolean
|
|
11
|
-
readonly on?: Readonly<Record<string, unknown>>
|
|
12
|
-
readonly ramp: readonly string[]
|
|
13
|
-
} {
|
|
14
|
-
const ramp = useRamp(props.rows.length)
|
|
15
|
-
return {
|
|
16
|
-
...(props.rows.length === 0 ? { holds: ['empty'] as const } : {}),
|
|
17
|
-
on: { viewBox: '0 0 100 100' },
|
|
18
|
-
ramp,
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function slice(
|
|
23
|
-
props: DonutProps,
|
|
24
|
-
kept: { readonly ramp: readonly string[] },
|
|
25
|
-
row: Row,
|
|
26
|
-
at: number,
|
|
27
|
-
): Readonly<Record<string, string | number>> {
|
|
28
|
-
const whole = props.rows.reduce((sum: number, one: Row) => sum + num(one, props.value), 0) || 1
|
|
29
|
-
const share = num(row, props.value) / whole
|
|
30
|
-
const before = props.rows.slice(0, at).reduce((sum: number, one: Row) => sum + num(one, props.value), 0) / whole
|
|
31
|
-
return {
|
|
32
|
-
cx: 50,
|
|
33
|
-
cy: 50,
|
|
34
|
-
r: RADIUS,
|
|
35
|
-
fill: 'none',
|
|
36
|
-
stroke: seriesColour(kept.ramp, at),
|
|
37
|
-
strokeWidth: 16,
|
|
38
|
-
strokeDasharray: `${(share * ROUND).toFixed(2)} ${ROUND.toFixed(2)}`,
|
|
39
|
-
strokeDashoffset: (-before * ROUND).toFixed(2),
|
|
40
|
-
transform: 'rotate(-90 50 50)',
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type { ReactNode } from 'react'
|
|
4
|
-
import type { Row } from '../charts.tsx'
|
|
5
|
-
import { roleClass } from '../fonts.ts'
|
|
6
|
-
|
|
7
|
-
import type { Dispatching } from '../checked.ts'
|
|
8
|
-
import { type Element, element, useSurface } from '../wrappers.tsx'
|
|
9
|
-
import { shows } from './each.works.tsx'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export interface EachProps {
|
|
13
|
-
readonly rows: readonly object[]
|
|
14
|
-
readonly by: string
|
|
15
|
-
readonly children: (row: Row, at: number) => ReactNode
|
|
16
|
-
readonly settles?: string
|
|
17
|
-
readonly paced?: string
|
|
18
|
-
readonly dispatch?: keyof Dispatching
|
|
19
|
-
readonly input?: Readonly<Record<string, unknown>>
|
|
20
|
-
readonly invokeOn?: 'press' | 'shown'
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const Each: Element<'Each', EachProps> = element(
|
|
24
|
-
'Each',
|
|
25
|
-
(props: EachProps, className) => {
|
|
26
|
-
const surface = useSurface({ className: roleClass(undefined, className),
|
|
27
|
-
invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) }, })
|
|
28
|
-
return (
|
|
29
|
-
shows(props, surface)
|
|
30
|
-
)
|
|
31
|
-
},
|
|
32
|
-
'e-each',
|
|
33
|
-
{},
|
|
34
|
-
)
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { Fragment, useLayoutEffect, useRef, type ReactNode } from 'react'
|
|
2
|
-
|
|
3
|
-
import type { Row } from '../charts.tsx'
|
|
4
|
-
import { nameOf } from '../token.ts'
|
|
5
|
-
import type { EachProps } from './each.tsx'
|
|
6
|
-
|
|
7
|
-
function keyOf(row: object, by: string, at: number): string {
|
|
8
|
-
const value = (row as Record<string, unknown>)[by]
|
|
9
|
-
if (value === undefined || value === null) {
|
|
10
|
-
throw new Error(
|
|
11
|
-
`Each: row ${String(at)} has no value for "${by}", so it has no key. React reuses DOM by key, and a ` +
|
|
12
|
-
"row without one is matched by position — which silently keeps the previous row's state when " +
|
|
13
|
-
'the list reorders. Name the field that identifies a row in this view.',
|
|
14
|
-
)
|
|
15
|
-
}
|
|
16
|
-
return String(value)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function following(marker: HTMLElement): readonly HTMLElement[] {
|
|
20
|
-
const found: HTMLElement[] = []
|
|
21
|
-
let node = marker.nextElementSibling
|
|
22
|
-
while (node !== null) {
|
|
23
|
-
found.push(node as HTMLElement)
|
|
24
|
-
node = node.nextElementSibling
|
|
25
|
-
}
|
|
26
|
-
return found
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function boxes(marker: HTMLElement, ids: readonly string[]): Map<string, DOMRect> {
|
|
30
|
-
const found = new Map<string, DOMRect>()
|
|
31
|
-
following(marker).forEach((node, at) => {
|
|
32
|
-
const id = ids[at]
|
|
33
|
-
if (id !== undefined) found.set(id, node.getBoundingClientRect())
|
|
34
|
-
})
|
|
35
|
-
return found
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function settle(marker: HTMLElement, ids: readonly string[], was: Map<string, DOMRect>, over: string, paced: string): void {
|
|
39
|
-
following(marker).forEach((node, at) => {
|
|
40
|
-
const id = ids[at]
|
|
41
|
-
if (id === undefined) return
|
|
42
|
-
const before = was.get(id)
|
|
43
|
-
if (before === undefined) return
|
|
44
|
-
const after = node.getBoundingClientRect()
|
|
45
|
-
const dx = before.left - after.left
|
|
46
|
-
const dy = before.top - after.top
|
|
47
|
-
if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) return
|
|
48
|
-
|
|
49
|
-
node.style.transition = `transform var(${nameOf('motion-instant')})`
|
|
50
|
-
node.style.transform = `translate(${String(dx)}px, ${String(dy)}px)`
|
|
51
|
-
void node.offsetWidth
|
|
52
|
-
node.style.transition = `transform var(--${over}) var(--${paced})`
|
|
53
|
-
node.style.transform = ''
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function shows(props: EachProps, _surface: unknown): ReactNode {
|
|
58
|
-
const within = useRef<HTMLElement | null>(null)
|
|
59
|
-
const was = useRef<Map<string, DOMRect>>(new Map())
|
|
60
|
-
const drawn = useRef('')
|
|
61
|
-
|
|
62
|
-
const order = props.rows.map((row, at) => keyOf(row, props.by, at)).join('|')
|
|
63
|
-
|
|
64
|
-
useLayoutEffect(() => {
|
|
65
|
-
const marker = within.current
|
|
66
|
-
if (marker === null || props.settles === undefined || props.paced === undefined) return
|
|
67
|
-
const ids = order === '' ? [] : order.split('|')
|
|
68
|
-
const moved = drawn.current !== '' && drawn.current !== order
|
|
69
|
-
if (moved) settle(marker, ids, was.current, props.settles, props.paced)
|
|
70
|
-
drawn.current = order
|
|
71
|
-
was.current = boxes(marker, ids)
|
|
72
|
-
}, [order, props.settles, props.paced])
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
<>
|
|
76
|
-
<span ref={within} hidden />
|
|
77
|
-
{props.rows.map((row, at) => (
|
|
78
|
-
<Fragment key={keyOf(row, props.by, at)}>{props.children(row as Row, at)}</Fragment>
|
|
79
|
-
))}
|
|
80
|
-
</>
|
|
81
|
-
)
|
|
82
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type { MessageKey } from '../content.tsx'
|
|
4
|
-
import { roleClass } from '../fonts.ts'
|
|
5
|
-
|
|
6
|
-
import type { Dispatching } from '../checked.ts'
|
|
7
|
-
import { type Element, element, useSurface } from '../wrappers.tsx'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export interface EmptyProps {
|
|
11
|
-
readonly labelKey: MessageKey
|
|
12
|
-
readonly dispatch?: keyof Dispatching
|
|
13
|
-
readonly input?: Readonly<Record<string, unknown>>
|
|
14
|
-
readonly invokeOn?: 'press' | 'shown'
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const Empty: Element<'Empty', EmptyProps> = element(
|
|
18
|
-
'Empty',
|
|
19
|
-
(props: EmptyProps, className) => {
|
|
20
|
-
const surface = useSurface({ className: roleClass(undefined, className),
|
|
21
|
-
invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) },
|
|
22
|
-
holds: [
|
|
23
|
-
'empty' as const,
|
|
24
|
-
], })
|
|
25
|
-
return (
|
|
26
|
-
<div className={surface.className} {...surface.marks} {...surface.invokes} {...surface.a11y}>
|
|
27
|
-
{surface.text(props.labelKey)}
|
|
28
|
-
</div>
|
|
29
|
-
)
|
|
30
|
-
},
|
|
31
|
-
'e-empty',
|
|
32
|
-
{"card":"thread-card"},
|
|
33
|
-
)
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type { MessageKey } from '../content.tsx'
|
|
4
|
-
import { roleClass } from '../fonts.ts'
|
|
5
|
-
|
|
6
|
-
import type { Dispatching } from '../checked.ts'
|
|
7
|
-
import { type Element, element, useSurface } from '../wrappers.tsx'
|
|
8
|
-
import { shown, entry, uses } from './field.works.ts'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export interface FieldProps {
|
|
12
|
-
readonly name: string
|
|
13
|
-
readonly validator: string
|
|
14
|
-
readonly labelKey: MessageKey
|
|
15
|
-
readonly value?: string
|
|
16
|
-
readonly onValue?: (said: string) => void
|
|
17
|
-
readonly context?: Readonly<Record<string, unknown>>
|
|
18
|
-
readonly autocomplete?: string
|
|
19
|
-
readonly disabled?: boolean
|
|
20
|
-
readonly dispatch?: keyof Dispatching
|
|
21
|
-
readonly input?: Readonly<Record<string, unknown>>
|
|
22
|
-
readonly invokeOn?: 'press' | 'shown'
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const Field: Element<'Field', FieldProps> = element(
|
|
26
|
-
'Field',
|
|
27
|
-
(props: FieldProps, className) => {
|
|
28
|
-
const kept = uses(props)
|
|
29
|
-
const surface = useSurface({ className: roleClass(undefined, className),
|
|
30
|
-
invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) },
|
|
31
|
-
holds: [
|
|
32
|
-
...(kept.holds ?? []),
|
|
33
|
-
],
|
|
34
|
-
...(kept.animates === undefined ? {} : { animates: kept.animates }), })
|
|
35
|
-
return (
|
|
36
|
-
<label className={surface.className} {...surface.marks} {...surface.invokes} {...surface.a11y} {...(kept.on ?? {})}>
|
|
37
|
-
<span>{surface.text(props.labelKey)}</span>
|
|
38
|
-
<input {...entry(props, kept)} />
|
|
39
|
-
<span>{shown(props, surface, kept).fault}</span>
|
|
40
|
-
</label>
|
|
41
|
-
)
|
|
42
|
-
},
|
|
43
|
-
'e-field',
|
|
44
|
-
{"input":"thread-input"},
|
|
45
|
-
)
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { useState, type ReactNode } from 'react'
|
|
2
|
-
|
|
3
|
-
import { acceptsKeystroke, applyMask, entryConstraints, evaluateMoney, runValidator } from '../validators.ts'
|
|
4
|
-
import type { Surface } from '../door.ts'
|
|
5
|
-
import type { FieldProps } from './field.tsx'
|
|
6
|
-
|
|
7
|
-
export function uses(props: FieldProps): {
|
|
8
|
-
readonly holds?: readonly string[]
|
|
9
|
-
readonly animates?: boolean
|
|
10
|
-
readonly on?: Readonly<Record<string, unknown>>
|
|
11
|
-
readonly wrong: string | null
|
|
12
|
-
readonly said: (to: string | null) => void
|
|
13
|
-
} {
|
|
14
|
-
const [wrong, said] = useState<string | null>(null)
|
|
15
|
-
return { ...(props.disabled === true ? { holds: ['disabled'] as const } : {}), wrong, said }
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function entry(
|
|
19
|
-
props: FieldProps,
|
|
20
|
-
kept: { readonly wrong: string | null; readonly said: (to: string | null) => void },
|
|
21
|
-
): Readonly<Record<string, unknown>> {
|
|
22
|
-
const rules = entryConstraints(props.validator, props.context ?? {})
|
|
23
|
-
return {
|
|
24
|
-
name: props.name,
|
|
25
|
-
...(rules.secret === true ? { type: 'password' } : {}),
|
|
26
|
-
...(props.autocomplete ?? rules.autocomplete === undefined
|
|
27
|
-
? {}
|
|
28
|
-
: { autoComplete: props.autocomplete ?? rules.autocomplete }),
|
|
29
|
-
...(rules.keyboard === 'text' ? {} : { inputMode: rules.keyboard }),
|
|
30
|
-
...(rules.maxChars === undefined ? {} : { maxLength: rules.maxChars }),
|
|
31
|
-
value: props.value ?? '',
|
|
32
|
-
...(kept.wrong === null ? {} : { 'aria-invalid': true }),
|
|
33
|
-
|
|
34
|
-
onChange: (event: { readonly target: { readonly value: string } }): void => {
|
|
35
|
-
let kept_ = ''
|
|
36
|
-
for (const ch of event.target.value) {
|
|
37
|
-
if (acceptsKeystroke(props.validator, kept_, ch)) kept_ += ch
|
|
38
|
-
}
|
|
39
|
-
props.onValue?.(applyMask(props.validator, kept_))
|
|
40
|
-
},
|
|
41
|
-
|
|
42
|
-
onBlur: (): void => {
|
|
43
|
-
const text = props.value ?? ''
|
|
44
|
-
let judged: unknown = text
|
|
45
|
-
if (rules.entry === 'calculator') {
|
|
46
|
-
const currency = props.context?.['currency']
|
|
47
|
-
const resolved = typeof currency === 'string' ? evaluateMoney(text, currency) : undefined
|
|
48
|
-
if (resolved !== undefined) {
|
|
49
|
-
judged = { amount: resolved.amount, currency }
|
|
50
|
-
props.onValue?.(String(resolved.amount))
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
const result = runValidator(props.validator, judged, props.context ?? {})
|
|
54
|
-
kept.said(result.ok ? null : result.errorKey)
|
|
55
|
-
},
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function shown(
|
|
60
|
-
_props: FieldProps,
|
|
61
|
-
surface: Surface,
|
|
62
|
-
kept: { readonly wrong: string | null },
|
|
63
|
-
): Readonly<Record<string, ReactNode>> {
|
|
64
|
-
return { fault: kept.wrong === null ? null : surface.text(kept.wrong) }
|
|
65
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type { MessageKey } from '../content.tsx'
|
|
4
|
-
import type { Row } from '../charts.tsx'
|
|
5
|
-
import { roleClass } from '../fonts.ts'
|
|
6
|
-
import { measured as marking } from '../charts.tsx'
|
|
7
|
-
|
|
8
|
-
import type { Dispatching } from '../checked.ts'
|
|
9
|
-
import { type Element, element, useSurface } from '../wrappers.tsx'
|
|
10
|
-
import { Each } from '../elements/each.tsx'
|
|
11
|
-
import { shown, sized, uses } from './funnel.works.ts'
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export interface FunnelProps {
|
|
15
|
-
readonly rows: readonly Row[]
|
|
16
|
-
readonly by: string
|
|
17
|
-
readonly value: string
|
|
18
|
-
readonly format?: string
|
|
19
|
-
readonly titleKey?: MessageKey
|
|
20
|
-
readonly dispatch?: keyof Dispatching
|
|
21
|
-
readonly input?: Readonly<Record<string, unknown>>
|
|
22
|
-
readonly invokeOn?: 'press' | 'shown'
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const Funnel: Element<'Funnel', FunnelProps> = element(
|
|
26
|
-
'Funnel',
|
|
27
|
-
(props: FunnelProps, className) => {
|
|
28
|
-
const kept = uses(props)
|
|
29
|
-
const surface = useSurface({ className: roleClass(undefined, className),
|
|
30
|
-
invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) },
|
|
31
|
-
holds: [
|
|
32
|
-
...(kept.holds ?? []),
|
|
33
|
-
],
|
|
34
|
-
...(kept.animates === undefined ? {} : { animates: kept.animates }), })
|
|
35
|
-
return (
|
|
36
|
-
<div className={surface.className} {...surface.marks} {...surface.invokes} {...surface.a11y} {...(kept.on ?? {})} role="img" {...(props.titleKey === undefined ? {} : { 'aria-label': surface.text(props.titleKey) })}>
|
|
37
|
-
<Each rows={props.rows} by={props.by}>
|
|
38
|
-
{(each0, at) => (
|
|
39
|
-
<span style={marking(sized(props, kept, each0, at))}>
|
|
40
|
-
<span>{shown(props, each0, surface, kept).label}</span>
|
|
41
|
-
<span>{shown(props, each0, surface, kept).amount}</span>
|
|
42
|
-
</span>
|
|
43
|
-
)}
|
|
44
|
-
</Each>
|
|
45
|
-
</div>
|
|
46
|
-
)
|
|
47
|
-
},
|
|
48
|
-
'e-funnel',
|
|
49
|
-
{"card":"thread-card"},
|
|
50
|
-
)
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { formatValue } from '../validators.ts'
|
|
2
|
-
import { seriesColour } from '../series.ts'
|
|
3
|
-
import { num, str, useRamp, type Row } from '../charts.tsx'
|
|
4
|
-
import type { FunnelProps } from './funnel.tsx'
|
|
5
|
-
|
|
6
|
-
export function uses(props: FunnelProps): {
|
|
7
|
-
readonly holds?: readonly string[]
|
|
8
|
-
readonly animates?: boolean
|
|
9
|
-
readonly on?: Readonly<Record<string, unknown>>
|
|
10
|
-
readonly ramp: readonly string[]
|
|
11
|
-
} {
|
|
12
|
-
const ramp = useRamp(props.rows.length)
|
|
13
|
-
return { ...(props.rows.length === 0 ? { holds: ['empty'] as const } : {}), ramp }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function sized(
|
|
17
|
-
props: FunnelProps,
|
|
18
|
-
kept: { readonly ramp: readonly string[] },
|
|
19
|
-
row: Row,
|
|
20
|
-
at: number,
|
|
21
|
-
): Readonly<Record<string, string | number>> {
|
|
22
|
-
const top = num(props.rows[0] as Row, props.value) || 1
|
|
23
|
-
const before = at === 0 ? null : num(props.rows[at - 1] as Row, props.value)
|
|
24
|
-
const drop = before === null || before === 0 ? null : 1 - num(row, props.value) / before
|
|
25
|
-
return {
|
|
26
|
-
series: seriesColour(kept.ramp, at),
|
|
27
|
-
fraction: `${((num(row, props.value) / top) * 100).toFixed(1)}%`,
|
|
28
|
-
...(drop === null ? {} : { drop: `${(drop * 100).toFixed(1)}%` }),
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function shown(props: FunnelProps, row: Row, _surface: unknown, _kept?: unknown): Readonly<Record<string, string>> {
|
|
33
|
-
return {
|
|
34
|
-
label: str(row, props.by),
|
|
35
|
-
amount: String(formatValue(props.format ?? 'number', num(row, props.value))),
|
|
36
|
-
}
|
|
37
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type { MessageKey } from '../content.tsx'
|
|
4
|
-
import { roleClass } from '../fonts.ts'
|
|
5
|
-
|
|
6
|
-
import type { Dispatching } from '../checked.ts'
|
|
7
|
-
import { type Element, element, useSurface } from '../wrappers.tsx'
|
|
8
|
-
import { track, needle, uses } from './gauge.works.ts'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export interface GaugeProps {
|
|
12
|
-
readonly value?: number
|
|
13
|
-
readonly min?: number
|
|
14
|
-
readonly max?: number
|
|
15
|
-
readonly labelKey: MessageKey
|
|
16
|
-
readonly dispatch?: keyof Dispatching
|
|
17
|
-
readonly input?: Readonly<Record<string, unknown>>
|
|
18
|
-
readonly invokeOn?: 'press' | 'shown'
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const Gauge: Element<'Gauge', GaugeProps> = element(
|
|
22
|
-
'Gauge',
|
|
23
|
-
(props: GaugeProps, className) => {
|
|
24
|
-
const kept = uses(props)
|
|
25
|
-
const surface = useSurface({ className: roleClass(undefined, className),
|
|
26
|
-
invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) },
|
|
27
|
-
holds: [
|
|
28
|
-
...(kept.holds ?? []),
|
|
29
|
-
],
|
|
30
|
-
...(kept.animates === undefined ? {} : { animates: kept.animates }), })
|
|
31
|
-
return (
|
|
32
|
-
<svg className={surface.className} {...surface.marks} {...surface.invokes} {...surface.a11y} {...(kept.on ?? {})} role="img" aria-label={surface.text(props.labelKey)}>
|
|
33
|
-
<path {...track(props, kept)}></path>
|
|
34
|
-
<path {...needle(props, kept)}></path>
|
|
35
|
-
</svg>
|
|
36
|
-
)
|
|
37
|
-
},
|
|
38
|
-
'e-gauge',
|
|
39
|
-
{"statTile":"thread-stat-tile"},
|
|
40
|
-
)
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { useRamp } from '../charts.tsx'
|
|
2
|
-
import type { GaugeProps } from './gauge.tsx'
|
|
3
|
-
|
|
4
|
-
const LEFT = 10
|
|
5
|
-
const RIGHT = 90
|
|
6
|
-
const MIDDLE = 50
|
|
7
|
-
const RADIUS = 40
|
|
8
|
-
|
|
9
|
-
export function uses(_props: GaugeProps): {
|
|
10
|
-
readonly holds?: readonly string[]
|
|
11
|
-
readonly animates?: boolean
|
|
12
|
-
readonly on?: Readonly<Record<string, unknown>>
|
|
13
|
-
readonly ramp: readonly string[]
|
|
14
|
-
} {
|
|
15
|
-
return { on: { viewBox: `0 0 100 56` }, ramp: useRamp(1) }
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function track(_props: GaugeProps, _kept: unknown): Readonly<Record<string, string | number>> {
|
|
19
|
-
return {
|
|
20
|
-
d: `M ${String(LEFT)} ${String(MIDDLE)} A ${String(RADIUS)} ${String(RADIUS)} 0 0 1 ${String(RIGHT)} ${String(MIDDLE)}`,
|
|
21
|
-
fill: 'none',
|
|
22
|
-
stroke: 'currentColor',
|
|
23
|
-
strokeOpacity: 0.15,
|
|
24
|
-
strokeWidth: 8,
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function needle(props: GaugeProps, kept: { readonly ramp: readonly string[] }): Readonly<Record<string, string | number>> {
|
|
29
|
-
const least = props.min ?? 0
|
|
30
|
-
const most = props.max ?? 0
|
|
31
|
-
const span = most - least
|
|
32
|
-
const part = span <= 0 ? 0 : Math.min(1, Math.max(0, ((props.value ?? 0) - least) / span))
|
|
33
|
-
const angle = Math.PI * (1 - part)
|
|
34
|
-
return {
|
|
35
|
-
d:
|
|
36
|
-
`M ${String(LEFT)} ${String(MIDDLE)} A ${String(RADIUS)} ${String(RADIUS)} 0 0 1 ` +
|
|
37
|
-
`${(MIDDLE + RADIUS * Math.cos(angle)).toFixed(2)} ${(MIDDLE - RADIUS * Math.sin(angle)).toFixed(2)}`,
|
|
38
|
-
fill: 'none',
|
|
39
|
-
stroke: kept.ramp[0] ?? 'currentColor',
|
|
40
|
-
strokeWidth: 8,
|
|
41
|
-
}
|
|
42
|
-
}
|