@delacour/react-native-ui 0.1.0-alpha.20260925055209 → 0.1.0-alpha.20260925055848
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/package.json +4 -2
- package/src/components/meter/AGENTS.md +135 -0
- package/src/components/meter/index.ts +43 -0
- package/src/components/meter/meter-fill.tsx +19 -0
- package/src/components/meter/meter-header.tsx +17 -0
- package/src/components/meter/meter-label.tsx +18 -0
- package/src/components/meter/meter-output.tsx +42 -0
- package/src/components/meter/meter-segments.tsx +83 -0
- package/src/components/meter/meter-track.tsx +16 -0
- package/src/components/meter/meter.context.tsx +62 -0
- package/src/components/meter/meter.tsx +286 -0
- package/src/components/meter/meter.types.ts +30 -0
- package/src/components/meter/meter.variants.test.ts +430 -0
- package/src/components/meter/meter.variants.ts +347 -0
- package/src/components/progress/AGENTS.md +120 -0
- package/src/components/progress/index.ts +40 -0
- package/src/components/progress/progress-fill.tsx +152 -0
- package/src/components/progress/progress-header.tsx +19 -0
- package/src/components/progress/progress-label.tsx +27 -0
- package/src/components/progress/progress-output.tsx +47 -0
- package/src/components/progress/progress-track.tsx +38 -0
- package/src/components/progress/progress.context.tsx +79 -0
- package/src/components/progress/progress.tsx +171 -0
- package/src/components/progress/progress.types.ts +23 -0
- package/src/components/progress/progress.variants.test.ts +335 -0
- package/src/components/progress/progress.variants.ts +284 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delacour/react-native-ui",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.20260925055848",
|
|
4
4
|
"description": "React Native UI components — Uniwind, Reanimated, Gesture Handler",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,7 +43,9 @@
|
|
|
43
43
|
"./icon": "./src/components/icon/index.ts",
|
|
44
44
|
"./input": "./src/components/input/index.ts",
|
|
45
45
|
"./list-group": "./src/components/list-group/index.ts",
|
|
46
|
+
"./meter": "./src/components/meter/index.ts",
|
|
46
47
|
"./pressable": "./src/components/pressable/index.ts",
|
|
48
|
+
"./progress": "./src/components/progress/index.ts",
|
|
47
49
|
"./provider": "./src/components/provider/index.ts",
|
|
48
50
|
"./radio": "./src/components/radio/index.ts",
|
|
49
51
|
"./screen": "./src/components/screen/index.ts",
|
|
@@ -82,7 +84,7 @@
|
|
|
82
84
|
},
|
|
83
85
|
"peerDependencies": {
|
|
84
86
|
"@central-icons-react-native/round-outlined-radius-1-stroke-1.5": "^1.1",
|
|
85
|
-
"@delacour/react-native-charts": "0.1.0-alpha.
|
|
87
|
+
"@delacour/react-native-charts": "0.1.0-alpha.20260925055848",
|
|
86
88
|
"@gorhom/bottom-sheet": "^5.2.8",
|
|
87
89
|
"@legendapp/list": ">=3.3",
|
|
88
90
|
"expo-linear-gradient": ">=15",
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Meter
|
|
2
|
+
|
|
3
|
+
A measurement on a fixed scale, coloured by where it falls — storage used, a
|
|
4
|
+
battery's charge, a password's strength. Built on `Progress`: the same track, fill,
|
|
5
|
+
header and label, with a root that clamps the reading, judges it, and paints the
|
|
6
|
+
fill that judgement's colour. Adds `Meter.Segments` for a scale drawn as whole
|
|
7
|
+
blocks.
|
|
8
|
+
|
|
9
|
+
`import { Meter } from "@delacour/react-native-ui/meter";`
|
|
10
|
+
|
|
11
|
+
## Files
|
|
12
|
+
|
|
13
|
+
| File | What it holds |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `index.ts` | → `@delacour/react-native-ui/meter` |
|
|
16
|
+
| `meter.tsx` | Root + the `Object.assign` compound surface; publishes both contexts |
|
|
17
|
+
| `meter-header.tsx` | `Meter.Header` — `Progress.Header`, named for the meter |
|
|
18
|
+
| `meter-label.tsx` | `Meter.Label` — `Progress.Label`, named for the meter |
|
|
19
|
+
| `meter-output.tsx` | `Meter.Output`, the readout, whose function child is handed the region |
|
|
20
|
+
| `meter-track.tsx` | `Meter.Track` — `Progress.Track`, named for the meter |
|
|
21
|
+
| `meter-fill.tsx` | `Meter.Fill` — `Progress.Fill`, named for the meter |
|
|
22
|
+
| `meter-segments.tsx` | `Meter.Segments`, the blocks, and the internal `Meter.Segments.Segment` |
|
|
23
|
+
| `meter.context.tsx` | `MeterContext`, `useMeter()`, `useMeterContext()`, `useMeterPart()` |
|
|
24
|
+
| `meter.types.ts` | `MeterRenderProps`, shared by the context and the readout |
|
|
25
|
+
| `meter.variants.ts` | The blocks' `tv()` slots + the scale, region, threshold and segment resolvers |
|
|
26
|
+
| `meter.variants.test.ts` | |
|
|
27
|
+
|
|
28
|
+
## Built on Progress
|
|
29
|
+
|
|
30
|
+
- **The root publishes `ProgressContextValue` as well as its own context.**
|
|
31
|
+
`Progress.Track` and `Progress.Fill` read only `ratio`, `isIndeterminate`,
|
|
32
|
+
`color`, `size` and `trackSize` — the progress bar was written to be reused
|
|
33
|
+
this way — so `Meter.Track` and `Meter.Fill` *are* those parts, and the fill's
|
|
34
|
+
UI-thread transform, its seeded first frame and its hide-until-measured come
|
|
35
|
+
along without a line restated. `isIndeterminate` is always `false`.
|
|
36
|
+
- **Each reused part is a thin wrapper, not the progress part assigned
|
|
37
|
+
directly.** The wrapper calls `useMeterPart` first, so a `Meter.Fill` outside
|
|
38
|
+
a meter throws naming `Meter.Fill` rather than `Progress.Fill`, and DevTools
|
|
39
|
+
shows `DelacourUI.Meter.Fill` (rule 12).
|
|
40
|
+
- **The parts come through `../progress`, the index; the resolvers through
|
|
41
|
+
`../progress/progress.variants`, the leaf** (rule 3). Progress imports nothing
|
|
42
|
+
from here, so there is no cycle, and the registry records `progress` as a
|
|
43
|
+
dependency of `meter`.
|
|
44
|
+
- **Colours and sizes are the progress bar's tuples, re-exported, not
|
|
45
|
+
restated.** A test pins them equal, and another pins a block's height and
|
|
46
|
+
paint to the progress track's and fill's, so a segmented meter and a
|
|
47
|
+
continuous one in one card read as one instrument.
|
|
48
|
+
|
|
49
|
+
## Judging the reading
|
|
50
|
+
|
|
51
|
+
- **Two ways, never both, and the prop type says so.** `MeterProps` is a union:
|
|
52
|
+
`low` / `high` / `optimum` *or* `thresholds`. Passing both is a type error
|
|
53
|
+
rather than a precedence rule. `resolveMeterScale` turns the props into a
|
|
54
|
+
`MeterScale` — `plain`, `regions` or `thresholds` — and `resolveMeterColor`
|
|
55
|
+
switches on its `kind`.
|
|
56
|
+
- **Regions follow a gauge's three-part model.** `low` and `high` default to the
|
|
57
|
+
scale's ends and `optimum` to its middle; each is clamped into the scale and
|
|
58
|
+
`high` is never below `low`, so a boundary written the wrong way round is
|
|
59
|
+
pulled into order rather than inverting every region. `optimum` below `low`
|
|
60
|
+
makes the bottom good (a disk), above `high` the top (a battery), between them
|
|
61
|
+
the band (a temperature). The regions paint `success`, `warning`,
|
|
62
|
+
`destructive` — `METER_REGION_COLORS`, fixed, so a region reads the same in
|
|
63
|
+
every meter in an app — and `color` is ignored.
|
|
64
|
+
- **Thresholds: the highest `from` reached wins, in any listed order.** `from`
|
|
65
|
+
is in the reading's units, not a percentage — a threshold on a 0–256 GB meter
|
|
66
|
+
is written in gigabytes. A non-finite `from` is ignored. Below every threshold
|
|
67
|
+
`color` applies.
|
|
68
|
+
- **The clamped reading is what is judged.** A disk over its capacity is
|
|
69
|
+
`critical`, not off the scale.
|
|
70
|
+
- **Colour is never the only signal, and `valueLabel` takes a function for it.**
|
|
71
|
+
Nothing speaks the region on its own — the words are domain-specific ("Almost
|
|
72
|
+
full", "Weak") — so `valueLabel` may be a function handed the judged
|
|
73
|
+
`MeterReading`, and `resolveMeterValueLabel` calls it. A `Meter.Output`
|
|
74
|
+
function reading `region` is **not** enough: the output is hidden from
|
|
75
|
+
assistive technology, so its words are seen and never heard. The simulator
|
|
76
|
+
pass caught exactly that — the regions demo drew "Filling up · 82%" while
|
|
77
|
+
VoiceOver's value was "82%".
|
|
78
|
+
|
|
79
|
+
## The scale
|
|
80
|
+
|
|
81
|
+
- **`meterRange` makes the scale finite and ordered.** A non-finite bound takes
|
|
82
|
+
0 or 100; an inverted or empty scale collapses at its floor and reads empty.
|
|
83
|
+
`progressRatio` would already return 0 for it, but the accessibility value
|
|
84
|
+
needs `min ≤ now ≤ max` too.
|
|
85
|
+
- **`meterValue` clamps to the ends; `NaN` reads at the floor and an infinity at
|
|
86
|
+
the end it points to.** `progressRatio` alone returns 0 for `+Infinity`,
|
|
87
|
+
which would draw a full disk as empty.
|
|
88
|
+
- **The formatted readout uses the clamped reading**, so a non-percent format
|
|
89
|
+
never prints `300 GB` on a 256 GB scale.
|
|
90
|
+
|
|
91
|
+
## Segments
|
|
92
|
+
|
|
93
|
+
- **Whole blocks only.** `litSegments` floors, so four blocks say "three out of
|
|
94
|
+
four" where a bar says "about seventy percent" — a password is not seventy
|
|
95
|
+
percent strong. A small epsilon absorbs floating-point error, so `0.7 × 10`
|
|
96
|
+
lights seven.
|
|
97
|
+
- **Any reading above the floor lights at least one block.** Rounding down would
|
|
98
|
+
leave the first quarter of a four-block meter dark, and "a little" looking like
|
|
99
|
+
"none" is the reading a meter can least afford to get wrong.
|
|
100
|
+
- **`meterSegmentCount` rounds a fractional count down, clamps above
|
|
101
|
+
`METER_MAX_SEGMENTS` (100), and falls back to the continuous bar** for a count
|
|
102
|
+
under one or not finite, rather than drawing nothing.
|
|
103
|
+
- **A block's lit colour is a layer that fades its opacity**, not the block's own
|
|
104
|
+
background: an opacity animates on the UI thread, where a background would need
|
|
105
|
+
two token values interpolated in JavaScript. Each layer is seeded at its
|
|
106
|
+
current state, so a list of meters does not replay the lighting as rows mount,
|
|
107
|
+
and the timing keeps Reanimated's default reduce-motion policy — with the system
|
|
108
|
+
setting on, a block lights without a fade. Unlike the progress bar's loop,
|
|
109
|
+
nothing here has to keep moving to be read.
|
|
110
|
+
- **`segments` lives on the root, not on `Meter.Segments`.** The lit count is
|
|
111
|
+
part of the render props a readout reads, so the root has to know it.
|
|
112
|
+
`Meter.Segments` without a count renders nothing; `Meter.Track` with one still
|
|
113
|
+
draws the continuous bar.
|
|
114
|
+
|
|
115
|
+
## What it refuses
|
|
116
|
+
|
|
117
|
+
- **No `isDisabled`, `isInvalid`, `Field` or indeterminate state.** A meter
|
|
118
|
+
reports a reading and accepts no input; `resolveMeterAxes` takes one argument
|
|
119
|
+
and a test pins that. A reading that is not known yet is `Progress`'s
|
|
120
|
+
indeterminate loop, not a meter.
|
|
121
|
+
- **No `label` / `showValueLabel` props.** The anatomy is written out, as
|
|
122
|
+
`Progress`'s is — `Meter.Header` with a `Meter.Label` and a `Meter.Output` — and
|
|
123
|
+
a bare `<Meter value={68} />` still draws the scale, because a compound root
|
|
124
|
+
with no default children renders an empty `View` with no error.
|
|
125
|
+
|
|
126
|
+
## Accessibility
|
|
127
|
+
|
|
128
|
+
- **The root is the one accessible element**, `accessibilityRole="progressbar"`
|
|
129
|
+
— React Native has no meter role, and this is the one that publishes a range
|
|
130
|
+
and a value on both platforms. `resolveProgressAccessibility` decides the
|
|
131
|
+
spoken text: the percentage over 0–100, `168 of 256` otherwise, or
|
|
132
|
+
`valueLabel`. Thresholds, regions and segments never change it.
|
|
133
|
+
- **`Meter.Label` is the name, `Meter.Output` is hidden** — the root already
|
|
134
|
+
speaks the value, so a visible readout would be read twice. A meter with no
|
|
135
|
+
label needs an `accessibilityLabel`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export { Meter, type MeterProps } from "./meter";
|
|
2
|
+
export { type MeterContextValue, MeterProvider, useMeter, useMeterContext } from "./meter.context";
|
|
3
|
+
export type { MeterRenderChildren, MeterRenderProps } from "./meter.types";
|
|
4
|
+
export {
|
|
5
|
+
litSegments,
|
|
6
|
+
METER_COLORS,
|
|
7
|
+
METER_DEFAULT_COLOR,
|
|
8
|
+
METER_DEFAULT_SIZE,
|
|
9
|
+
METER_MAX_SEGMENTS,
|
|
10
|
+
METER_MAX_VALUE,
|
|
11
|
+
METER_MIN_VALUE,
|
|
12
|
+
METER_REGION_COLORS,
|
|
13
|
+
METER_REGIONS,
|
|
14
|
+
METER_SEGMENT_FADE_MS,
|
|
15
|
+
METER_SIZES,
|
|
16
|
+
type MeterAxes,
|
|
17
|
+
type MeterColor,
|
|
18
|
+
type MeterOwnAxes,
|
|
19
|
+
type MeterReading,
|
|
20
|
+
type MeterRegion,
|
|
21
|
+
type MeterScale,
|
|
22
|
+
type MeterScaleInput,
|
|
23
|
+
type MeterSize,
|
|
24
|
+
type MeterThreshold,
|
|
25
|
+
type MeterValueLabel,
|
|
26
|
+
type MeterVariantProps,
|
|
27
|
+
meterRange,
|
|
28
|
+
meterSegmentCount,
|
|
29
|
+
meterValue,
|
|
30
|
+
meterVariants,
|
|
31
|
+
resolveMeterAxes,
|
|
32
|
+
resolveMeterColor,
|
|
33
|
+
resolveMeterRegion,
|
|
34
|
+
resolveMeterScale,
|
|
35
|
+
resolveMeterValueLabel,
|
|
36
|
+
thresholdColor,
|
|
37
|
+
} from "./meter.variants";
|
|
38
|
+
export type { MeterFillProps } from "./meter-fill";
|
|
39
|
+
export type { MeterHeaderProps } from "./meter-header";
|
|
40
|
+
export type { MeterLabelProps } from "./meter-label";
|
|
41
|
+
export type { MeterOutputProps } from "./meter-output";
|
|
42
|
+
export type { MeterSegmentsProps } from "./meter-segments";
|
|
43
|
+
export type { MeterTrackProps } from "./meter-track";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Progress, type ProgressFillProps } from "../progress";
|
|
3
|
+
import { useMeterPart } from "./meter.context";
|
|
4
|
+
|
|
5
|
+
export type MeterFillProps = ProgressFillProps;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The painted bar, in the colour the reading was judged to be.
|
|
9
|
+
*
|
|
10
|
+
* The progress bar's own fill: a transform on the UI thread, one timing per new
|
|
11
|
+
* reading, landing on the value without travel under the system's reduce-motion
|
|
12
|
+
* setting. A change of colour is a class, so it lands on the render that judged
|
|
13
|
+
* it.
|
|
14
|
+
*/
|
|
15
|
+
export function MeterFill(props: MeterFillProps): ReactElement {
|
|
16
|
+
useMeterPart("Meter.Fill");
|
|
17
|
+
return <Progress.Fill {...props} />;
|
|
18
|
+
}
|
|
19
|
+
MeterFill.displayName = "DelacourUI.Meter.Fill";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Progress, type ProgressHeaderProps } from "../progress";
|
|
3
|
+
import { useMeterPart } from "./meter.context";
|
|
4
|
+
|
|
5
|
+
export type MeterHeaderProps = ProgressHeaderProps;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The row above the track: a label at the start, a readout at the end.
|
|
9
|
+
*
|
|
10
|
+
* The progress bar's own header — the meter publishes that bar's context, so the
|
|
11
|
+
* row, its gap and its type step are shared rather than restated.
|
|
12
|
+
*/
|
|
13
|
+
export function MeterHeader(props: MeterHeaderProps): ReactElement {
|
|
14
|
+
useMeterPart("Meter.Header");
|
|
15
|
+
return <Progress.Header {...props} />;
|
|
16
|
+
}
|
|
17
|
+
MeterHeader.displayName = "DelacourUI.Meter.Header";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Progress, type ProgressLabelProps } from "../progress";
|
|
3
|
+
import { useMeterPart } from "./meter.context";
|
|
4
|
+
|
|
5
|
+
export type MeterLabelProps = ProgressLabelProps;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What is being measured — "Storage", "Battery".
|
|
9
|
+
*
|
|
10
|
+
* Left visible to assistive technology: the root is one accessible element, and
|
|
11
|
+
* a label inside it is what VoiceOver and TalkBack read as that element's name.
|
|
12
|
+
* A meter without one announces a number with nothing attached to it.
|
|
13
|
+
*/
|
|
14
|
+
export function MeterLabel(props: MeterLabelProps): ReactElement {
|
|
15
|
+
useMeterPart("Meter.Label");
|
|
16
|
+
return <Progress.Label {...props} />;
|
|
17
|
+
}
|
|
18
|
+
MeterLabel.displayName = "DelacourUI.Meter.Label";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { PROGRESS_OUTPUT_TEXT_SIZE, progressVariants } from "../progress/progress.variants";
|
|
3
|
+
import { Text, type TextPresetProps } from "../text";
|
|
4
|
+
import { useMeterPart } from "./meter.context";
|
|
5
|
+
import type { MeterRenderChildren } from "./meter.types";
|
|
6
|
+
|
|
7
|
+
export type MeterOutputProps = Omit<TextPresetProps, "children"> & {
|
|
8
|
+
/** Custom content, or a function called with the meter's settled state. */
|
|
9
|
+
children?: MeterRenderChildren;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The reading, formatted — `68%` by default, or `valueLabel` when one is given.
|
|
14
|
+
*
|
|
15
|
+
* A function child is handed the meter's state, including the `region` it was
|
|
16
|
+
* judged to be in. Words it draws are seen but not heard — see below — so a
|
|
17
|
+
* judgement a screen reader must hear belongs in a `valueLabel` function.
|
|
18
|
+
*
|
|
19
|
+
* **Hidden from assistive technology on purpose.** The root already publishes
|
|
20
|
+
* the reading as `accessibilityValue`, so a readout left visible inside it would
|
|
21
|
+
* be read twice.
|
|
22
|
+
*/
|
|
23
|
+
export function MeterOutput({ children, className, size, ...props }: MeterOutputProps): ReactElement | null {
|
|
24
|
+
const { size: meterSize, renderProps } = useMeterPart("Meter.Output");
|
|
25
|
+
|
|
26
|
+
const content = typeof children === "function" ? children(renderProps) : (children ?? renderProps.formatted);
|
|
27
|
+
|
|
28
|
+
if (content === null || content === undefined) return null;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Text.Label
|
|
32
|
+
accessibilityElementsHidden
|
|
33
|
+
className={progressVariants({ size: meterSize }).output({ className })}
|
|
34
|
+
importantForAccessibility="no-hide-descendants"
|
|
35
|
+
size={size ?? PROGRESS_OUTPUT_TEXT_SIZE[meterSize]}
|
|
36
|
+
{...props}
|
|
37
|
+
>
|
|
38
|
+
{content}
|
|
39
|
+
</Text.Label>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
MeterOutput.displayName = "DelacourUI.Meter.Output";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type ReactElement, useEffect } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
|
|
4
|
+
import { useMeterPart } from "./meter.context";
|
|
5
|
+
import { METER_SEGMENT_FADE_MS, type MeterColor, type MeterSize, meterVariants } from "./meter.variants";
|
|
6
|
+
|
|
7
|
+
export type MeterSegmentsProps = Omit<ViewProps, "children"> & {
|
|
8
|
+
/** Extra classes for every block. */
|
|
9
|
+
segmentClassName?: string;
|
|
10
|
+
/** Extra classes for a lit block's painted layer. */
|
|
11
|
+
fillClassName?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type MeterSegmentProps = {
|
|
15
|
+
isLit: boolean;
|
|
16
|
+
color: MeterColor;
|
|
17
|
+
size: MeterSize;
|
|
18
|
+
className?: string;
|
|
19
|
+
fillClassName?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One block: an unlit groove with its lit layer fading over it.
|
|
24
|
+
*
|
|
25
|
+
* The layer is seeded at its current state, so a meter mounting at three of
|
|
26
|
+
* four does not replay the lighting on every row that scrolls in. The timing
|
|
27
|
+
* takes Reanimated's default reduce-motion policy, so with the system setting on
|
|
28
|
+
* a block lights without a fade — nothing here needs to stay moving to be read.
|
|
29
|
+
*/
|
|
30
|
+
function MeterSegment({ isLit, color, size, className, fillClassName }: MeterSegmentProps): ReactElement {
|
|
31
|
+
const opacity = useSharedValue(isLit ? 1 : 0);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
opacity.value = withTiming(isLit ? 1 : 0, { duration: METER_SEGMENT_FADE_MS });
|
|
35
|
+
}, [isLit, opacity]);
|
|
36
|
+
|
|
37
|
+
const fillStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
|
|
38
|
+
const slots = meterVariants({ color, size });
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View className={slots.segment({ className })}>
|
|
42
|
+
<Animated.View className={slots.segmentFill({ className: fillClassName })} style={fillStyle} />
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
MeterSegment.displayName = "DelacourUI.Meter.Segments.Segment";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The scale drawn as discrete blocks — the segmented meter's track and fill in one.
|
|
50
|
+
*
|
|
51
|
+
* Draws the count the root was given as `segments`, lighting whole blocks only:
|
|
52
|
+
* four blocks say "three out of four" where a bar says "about seventy percent".
|
|
53
|
+
* With no count on the root it draws nothing, because there is no scale to cut.
|
|
54
|
+
*/
|
|
55
|
+
export function MeterSegments({
|
|
56
|
+
className,
|
|
57
|
+
segmentClassName,
|
|
58
|
+
fillClassName,
|
|
59
|
+
...props
|
|
60
|
+
}: MeterSegmentsProps): ReactElement | null {
|
|
61
|
+
const { size, renderProps } = useMeterPart("Meter.Segments");
|
|
62
|
+
const { segments, litSegments, color } = renderProps;
|
|
63
|
+
|
|
64
|
+
if (segments === null) return null;
|
|
65
|
+
|
|
66
|
+
const blocks = Array.from({ length: segments }, (_, index) => index);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<View className={meterVariants({ color, size }).segments({ className })} {...props}>
|
|
70
|
+
{blocks.map((index) => (
|
|
71
|
+
<MeterSegment
|
|
72
|
+
className={segmentClassName}
|
|
73
|
+
color={color}
|
|
74
|
+
fillClassName={fillClassName}
|
|
75
|
+
isLit={index < litSegments}
|
|
76
|
+
key={index}
|
|
77
|
+
size={size}
|
|
78
|
+
/>
|
|
79
|
+
))}
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
MeterSegments.displayName = "DelacourUI.Meter.Segments";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Progress, type ProgressTrackProps } from "../progress";
|
|
3
|
+
import { useMeterPart } from "./meter.context";
|
|
4
|
+
|
|
5
|
+
export type MeterTrackProps = ProgressTrackProps;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The continuous groove the fill slides along — the progress bar's own track.
|
|
9
|
+
*
|
|
10
|
+
* For a segmented meter write `Meter.Segments` instead.
|
|
11
|
+
*/
|
|
12
|
+
export function MeterTrack(props: MeterTrackProps): ReactElement {
|
|
13
|
+
useMeterPart("Meter.Track");
|
|
14
|
+
return <Progress.Track {...props} />;
|
|
15
|
+
}
|
|
16
|
+
MeterTrack.displayName = "DelacourUI.Meter.Track";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createContext, type ReactElement, type ReactNode, use } from "react";
|
|
2
|
+
import type { MeterRenderProps } from "./meter.types";
|
|
3
|
+
import type { MeterSize } from "./meter.variants";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a meter's own parts read.
|
|
7
|
+
*
|
|
8
|
+
* The continuous track and fill do not read this — the root also publishes the
|
|
9
|
+
* progress bar's context, and `Meter.Track` and `Meter.Fill` are the progress
|
|
10
|
+
* bar's parts reading that. This holds only what a meter adds: the judged colour,
|
|
11
|
+
* the region and the blocks.
|
|
12
|
+
*/
|
|
13
|
+
export type MeterContextValue = {
|
|
14
|
+
size: MeterSize;
|
|
15
|
+
/** The settled state `Meter.Output` formats and a render function is handed. */
|
|
16
|
+
renderProps: MeterRenderProps;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const MeterContext = createContext<MeterContextValue | null>(null);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Supplies one meter's state to its own parts.
|
|
23
|
+
*
|
|
24
|
+
* Its own module, importing nothing but React and types, so a part can read it
|
|
25
|
+
* without importing `./meter` and closing a cycle.
|
|
26
|
+
*/
|
|
27
|
+
export function MeterProvider({ value, children }: { value: MeterContextValue; children: ReactNode }): ReactElement {
|
|
28
|
+
return <MeterContext value={value}>{children}</MeterContext>;
|
|
29
|
+
}
|
|
30
|
+
MeterProvider.displayName = "DelacourUI.Meter.Provider";
|
|
31
|
+
|
|
32
|
+
/** The enclosing meter's state, or null outside a `<Meter>`. */
|
|
33
|
+
export function useMeterContext(): MeterContextValue | null {
|
|
34
|
+
return use(MeterContext);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reads the enclosing meter's state.
|
|
39
|
+
*
|
|
40
|
+
* For a custom readout or legend that has to match the meter it sits in. Throws
|
|
41
|
+
* outside one — use {@link useMeterContext} where the meter is optional.
|
|
42
|
+
*/
|
|
43
|
+
export function useMeter(): MeterContextValue {
|
|
44
|
+
const context = useMeterContext();
|
|
45
|
+
if (!context) {
|
|
46
|
+
throw new Error("useMeter must be called inside a <Meter>.");
|
|
47
|
+
}
|
|
48
|
+
return context;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The enclosing meter's state, for a compound part that cannot work without one.
|
|
53
|
+
*
|
|
54
|
+
* Internal: deliberately not re-exported from `index.ts`.
|
|
55
|
+
*/
|
|
56
|
+
export function useMeterPart(component: string): MeterContextValue {
|
|
57
|
+
const context = useMeterContext();
|
|
58
|
+
if (!context) {
|
|
59
|
+
throw new Error(`${component} must be rendered inside a <Meter>.`);
|
|
60
|
+
}
|
|
61
|
+
return context;
|
|
62
|
+
}
|