@multiplatform.one/components 6.4.1 → 6.4.2
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 +9 -9
- package/src/Carousel/index.tsx +1 -1
- package/src/DotIndicator/index.tsx +3 -1
- package/src/Pagination.tsx +5 -1
- package/src/ProgressSteps/index.tsx +6 -3
- package/src/Skeleton/Skeleton.stories.tsx +7 -1
- package/src/Skeleton/SkeletonCircle.stories.tsx +5 -1
- package/src/Skeleton/SkeletonText.stories.tsx +4 -1
- package/src/Wheel/index.tsx +1 -1
- package/src/charts/Axis.tsx +4 -2
- package/src/charts/BarChart.tsx +8 -2
- package/src/charts/LineChart.tsx +8 -2
- package/src/charts/PieChart.tsx +2 -1
- package/src/charts/accessibility.ts +3 -2
- package/src/charts/index.ts +1 -0
- package/src/charts/math.ts +21 -2
- package/src/componentColors.ts +11 -3
- package/src/layouts/ViewSwitcher.tsx +35 -2
- package/src/layouts/page.tsx +15 -10
- package/src/views/Dashboard.tsx +29 -15
- package/src/views/ReportBuilder.tsx +24 -17
- package/src/views/TreeView.tsx +5 -3
- package/src/views/ganttMath.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/components",
|
|
3
|
-
"version": "6.4.
|
|
3
|
+
"version": "6.4.2",
|
|
4
4
|
"description": "multiplatform.one ui components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"components",
|
|
@@ -74,11 +74,11 @@
|
|
|
74
74
|
"link": "^2.1.2",
|
|
75
75
|
"shiki": "^4.0.2",
|
|
76
76
|
"tamagui": "2.0.0-rc.41",
|
|
77
|
-
"@multiplatform.one/forms": "6.4.
|
|
78
|
-
"@multiplatform.one/i18n": "6.4.
|
|
79
|
-
"@multiplatform.one/
|
|
80
|
-
"@multiplatform.one/
|
|
81
|
-
"@multiplatform.one/
|
|
77
|
+
"@multiplatform.one/forms": "6.4.2",
|
|
78
|
+
"@multiplatform.one/i18n": "6.4.2",
|
|
79
|
+
"@multiplatform.one/router": "6.4.2",
|
|
80
|
+
"@multiplatform.one/theme": "6.4.2",
|
|
81
|
+
"@multiplatform.one/platform": "6.4.2"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
84
|
"@tamagui/build": "2.0.0-rc.41",
|
|
@@ -96,9 +96,9 @@
|
|
|
96
96
|
"react-native-svg": "15.15.4",
|
|
97
97
|
"react-native-web": "^0.21.2",
|
|
98
98
|
"vitest": "^4.1.5",
|
|
99
|
-
"@multiplatform.one/
|
|
100
|
-
"@multiplatform.one/
|
|
101
|
-
"@multiplatform.one/
|
|
99
|
+
"@multiplatform.one/test-utils": "6.4.2",
|
|
100
|
+
"@multiplatform.one/config": "6.4.2",
|
|
101
|
+
"@multiplatform.one/storybook": "6.4.2"
|
|
102
102
|
},
|
|
103
103
|
"peerDependencies": {
|
|
104
104
|
"@mdx-js/mdx": "^3.1.1",
|
package/src/Carousel/index.tsx
CHANGED
|
@@ -81,7 +81,9 @@ const Dot = styled(View, {
|
|
|
81
81
|
variants: {
|
|
82
82
|
active: {
|
|
83
83
|
true: {
|
|
84
|
-
|
|
84
|
+
// LC-05: the active dot is the selected mark, so it carries the accent
|
|
85
|
+
// (it read $color9 neutral — indistinguishable from idle chrome).
|
|
86
|
+
backgroundColor: componentColors.indicator.selected,
|
|
85
87
|
},
|
|
86
88
|
},
|
|
87
89
|
variant: {
|
package/src/Pagination.tsx
CHANGED
|
@@ -334,11 +334,15 @@ export function Pagination({
|
|
|
334
334
|
width: isActive ? activeDotSize : dotSize,
|
|
335
335
|
height: dotSize,
|
|
336
336
|
borderRadius: "$10",
|
|
337
|
+
// LC-05 / D-01 rider (a): the current dot speaks the SAME selected
|
|
338
|
+
// language as this component's numbered body (accent) — it used to
|
|
339
|
+
// paint ink while the numbered page button painted accent, so one
|
|
340
|
+
// component taught two meanings for "current".
|
|
337
341
|
backgroundColor:
|
|
338
342
|
isCompleted && !isActive
|
|
339
343
|
? componentColors.indicator.completed
|
|
340
344
|
: isActive
|
|
341
|
-
? componentColors.indicator.
|
|
345
|
+
? componentColors.indicator.selected
|
|
342
346
|
: componentColors.indicator.track,
|
|
343
347
|
transition: motionTransition,
|
|
344
348
|
cursor: canJump ? "pointer" : undefined,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CheckIcon } from "@phosphor-icons/react";
|
|
2
|
-
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
2
|
+
import { useReadableTextOn, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
3
3
|
import { useTranslation } from "react-i18next";
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
5
|
import type { GetProps } from "tamagui";
|
|
@@ -100,6 +100,9 @@ export function ProgressSteps({
|
|
|
100
100
|
}: ProgressStepsProps) {
|
|
101
101
|
const { t } = useTranslation();
|
|
102
102
|
const { knobProps } = useResolvedKnobs({ component: "ProgressSteps", compact });
|
|
103
|
+
// LC-05: the current marker rides the accent fill, so its step number takes
|
|
104
|
+
// the luminance-computed paper/ink anchor rather than an assumed `$color1`.
|
|
105
|
+
const onSelected = useReadableTextOn(componentColors.indicator.selected);
|
|
103
106
|
const markerSize = markerSizeMap[knobProps.sizeToken] ?? 28;
|
|
104
107
|
const markerIconSize = markerIconSizeMap[knobProps.sizeToken] ?? 14;
|
|
105
108
|
const markerTextSize = markerTextSizeMap[knobProps.sizeToken] ?? "$2";
|
|
@@ -125,7 +128,7 @@ export function ProgressSteps({
|
|
|
125
128
|
state === "completed"
|
|
126
129
|
? componentColors.indicator.completed
|
|
127
130
|
: state === "current"
|
|
128
|
-
? componentColors.indicator.
|
|
131
|
+
? componentColors.indicator.selected
|
|
129
132
|
: componentColors.interactive.background;
|
|
130
133
|
const markerBorder = state === "upcoming" ? componentColors.indicator.track : markerBg;
|
|
131
134
|
const markerFg = state === "upcoming" ? componentColors.text.muted : "$color1";
|
|
@@ -146,7 +149,7 @@ export function ProgressSteps({
|
|
|
146
149
|
<SizableText
|
|
147
150
|
fontSize={markerTextSize as any}
|
|
148
151
|
fontWeight="600"
|
|
149
|
-
color={state === "current" ? "$color1" : markerFg}
|
|
152
|
+
color={state === "current" ? (onSelected ?? "$color1") : markerFg}
|
|
150
153
|
>
|
|
151
154
|
{index + 1}
|
|
152
155
|
</SizableText>
|
|
@@ -7,10 +7,16 @@ const meta: Meta<typeof Skeleton> = {
|
|
|
7
7
|
component: Skeleton,
|
|
8
8
|
parameters: {
|
|
9
9
|
status: { type: "stable" },
|
|
10
|
+
// D-10 SKELETONS-RESOLVE carve-out: this is the component's own anatomy
|
|
11
|
+
// catalog, so its skeletons are the subject rather than a promise about
|
|
12
|
+
// data that never arrives. Explicitly labeled so the skeleton-at-rest
|
|
13
|
+
// probe exempts it; every non-specimen surface must show zero skeletons
|
|
14
|
+
// after settle.
|
|
15
|
+
skeletonSpecimen: true,
|
|
10
16
|
docs: {
|
|
11
17
|
description: {
|
|
12
18
|
component:
|
|
13
|
-
"A loading placeholder
|
|
19
|
+
"SKELETON SPECIMEN (D-10). A loading placeholder that pulses to indicate content is being loaded. Supports multiple variants for different content types. The skeletons on this page persist by design because the skeleton itself is the subject — in product surfaces a skeleton may only persist while a real pending source exists.",
|
|
14
20
|
},
|
|
15
21
|
},
|
|
16
22
|
},
|
|
@@ -7,9 +7,13 @@ const meta: Meta<typeof SkeletonCircle> = {
|
|
|
7
7
|
component: SkeletonCircle,
|
|
8
8
|
parameters: {
|
|
9
9
|
status: { type: "beta" },
|
|
10
|
+
// D-10 SKELETONS-RESOLVE: explicitly-labeled skeleton specimen (the
|
|
11
|
+
// skeleton is the subject here, not a promise about pending data).
|
|
12
|
+
skeletonSpecimen: true,
|
|
10
13
|
docs: {
|
|
11
14
|
description: {
|
|
12
|
-
component:
|
|
15
|
+
component:
|
|
16
|
+
"SKELETON SPECIMEN (D-10). Circular loading placeholder (avatar-shaped). Default diameter 40px. Skeletons persist on this page by design; product surfaces must show zero skeletons once their pending source settles.",
|
|
13
17
|
},
|
|
14
18
|
},
|
|
15
19
|
},
|
|
@@ -7,10 +7,13 @@ const meta: Meta<typeof SkeletonText> = {
|
|
|
7
7
|
component: SkeletonText,
|
|
8
8
|
parameters: {
|
|
9
9
|
status: { type: "beta" },
|
|
10
|
+
// D-10 SKELETONS-RESOLVE: explicitly-labeled skeleton specimen (the
|
|
11
|
+
// skeleton is the subject here, not a promise about pending data).
|
|
12
|
+
skeletonSpecimen: true,
|
|
10
13
|
docs: {
|
|
11
14
|
description: {
|
|
12
15
|
component:
|
|
13
|
-
"Multi-line text loading placeholder. Last line renders at lastLineWidth (default 70%).",
|
|
16
|
+
"SKELETON SPECIMEN (D-10). Multi-line text loading placeholder. Last line renders at lastLineWidth (default 70%). Skeletons persist on this page by design; product surfaces must show zero skeletons once their pending source settles.",
|
|
14
17
|
},
|
|
15
18
|
},
|
|
16
19
|
},
|
package/src/Wheel/index.tsx
CHANGED
|
@@ -666,7 +666,7 @@ export function Wheel<T = string | number>({
|
|
|
666
666
|
return (
|
|
667
667
|
<WheelContainer
|
|
668
668
|
height={containerHeight}
|
|
669
|
-
{...knobProps.
|
|
669
|
+
{...knobProps.containerRadius}
|
|
670
670
|
data-testid="wheel"
|
|
671
671
|
data-disabled={disabled ? true : undefined}
|
|
672
672
|
aria-disabled={disabled || undefined}
|
package/src/charts/Axis.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ScaleBand, ScaleLinear, ScalePoint } from "d3-scale";
|
|
2
|
-
import { getLinearTicks, truncateLabel } from "./math";
|
|
2
|
+
import { formatAxisTick, getLinearTicks, truncateLabel } from "./math";
|
|
3
3
|
import { G, Line, SvgText } from "./svg";
|
|
4
4
|
import type { PlotArea } from "./types";
|
|
5
5
|
import { useChartTheme } from "./useChartTheme";
|
|
@@ -122,7 +122,9 @@ export function YAxis({
|
|
|
122
122
|
}: YAxisProps) {
|
|
123
123
|
const { axisColor, gridColor, labelColor, labelFontFamily, labelFontSize } = useChartTheme();
|
|
124
124
|
const tickNumbers = getLinearTicks(scale, ticks);
|
|
125
|
-
|
|
125
|
+
// Axis ticks ride the charts package's own density register — the declared
|
|
126
|
+
// exemption to the house number channel (see `formatAxisTick`).
|
|
127
|
+
const format = tickFormat ?? ((value: number) => formatAxisTick(value));
|
|
126
128
|
|
|
127
129
|
return (
|
|
128
130
|
<G>
|
package/src/charts/BarChart.tsx
CHANGED
|
@@ -4,7 +4,13 @@ import { XAxis, YAxis } from "./Axis";
|
|
|
4
4
|
import { ChartSurface } from "./ChartSurface";
|
|
5
5
|
import { describeChart, describeDatum } from "./accessibility";
|
|
6
6
|
import { computeCartesianPlot, resolveSeriesFills, thinCategories } from "./composeChart";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
createBandScale,
|
|
9
|
+
createLinearScale,
|
|
10
|
+
formatChartValue,
|
|
11
|
+
getLinearTicks,
|
|
12
|
+
valueDomain,
|
|
13
|
+
} from "./math";
|
|
8
14
|
import { BarSeries } from "./series";
|
|
9
15
|
import type { ChartDatum, ChartDatumEvents } from "./types";
|
|
10
16
|
import { useChartTheme } from "./useChartTheme";
|
|
@@ -64,7 +70,7 @@ export function BarChart({
|
|
|
64
70
|
const { t } = useTranslation();
|
|
65
71
|
const { palette, labelFontSize } = useChartTheme();
|
|
66
72
|
const fills = resolveSeriesFills(data, palette, { color, colors, categorical });
|
|
67
|
-
const format = valueFormatter ??
|
|
73
|
+
const format = valueFormatter ?? formatChartValue;
|
|
68
74
|
const ariaLabel =
|
|
69
75
|
label ?? describeChart({ typeLabel: t("Bar chart"), title, data, formatValue: valueFormatter });
|
|
70
76
|
const interactive = Boolean(onDatumPress || onDatumHoverIn || onDatumHoverOut);
|
package/src/charts/LineChart.tsx
CHANGED
|
@@ -4,7 +4,13 @@ import { XAxis, YAxis } from "./Axis";
|
|
|
4
4
|
import { ChartSurface } from "./ChartSurface";
|
|
5
5
|
import { describeChart, describeDatum } from "./accessibility";
|
|
6
6
|
import { computeCartesianPlot, thinCategories } from "./composeChart";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
createLinearScale,
|
|
9
|
+
createPointScale,
|
|
10
|
+
formatChartValue,
|
|
11
|
+
getLinearTicks,
|
|
12
|
+
valueDomain,
|
|
13
|
+
} from "./math";
|
|
8
14
|
import { AreaSeries, LineSeries } from "./series";
|
|
9
15
|
import type { ChartCurve, ChartDatum, ChartDatumEvents } from "./types";
|
|
10
16
|
import { useChartTheme } from "./useChartTheme";
|
|
@@ -71,7 +77,7 @@ export function LineChart({
|
|
|
71
77
|
// One line = one series: explicit `color` is a data value and passes
|
|
72
78
|
// through; the default is the theme identity (LC-27).
|
|
73
79
|
const stroke = color ?? palette.single;
|
|
74
|
-
const format = valueFormatter ??
|
|
80
|
+
const format = valueFormatter ?? formatChartValue;
|
|
75
81
|
const ariaLabel =
|
|
76
82
|
label ??
|
|
77
83
|
describeChart({
|
package/src/charts/PieChart.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { componentColors } from "../componentColors";
|
|
|
5
5
|
import { ChartSurface } from "./ChartSurface";
|
|
6
6
|
import { describeChart, describeDatum } from "./accessibility";
|
|
7
7
|
import { resolveSeriesFills } from "./composeChart";
|
|
8
|
+
import { formatChartValue } from "./math";
|
|
8
9
|
import { PieSeries } from "./series";
|
|
9
10
|
import type { ChartDatum, ChartDatumEvents } from "./types";
|
|
10
11
|
import { useChartTheme } from "./useChartTheme";
|
|
@@ -65,7 +66,7 @@ export function PieChart({
|
|
|
65
66
|
const { knobProps } = useResolvedKnobs();
|
|
66
67
|
const { palette } = useChartTheme();
|
|
67
68
|
const fills = resolveSeriesFills(data, palette, { colors, categorical: true });
|
|
68
|
-
const format = valueFormatter ??
|
|
69
|
+
const format = valueFormatter ?? formatChartValue;
|
|
69
70
|
const total = data.reduce((sum, datum) => sum + datum.value, 0);
|
|
70
71
|
const ariaLabel =
|
|
71
72
|
label ??
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Every chart surface exposes `role="img"` with a generated text summary so
|
|
5
5
|
* screen-reader users get the shape of the data, not silence. The functions
|
|
6
6
|
* are pure; components pass already-translated type/title labels in, and the
|
|
7
|
-
* numbers format through
|
|
7
|
+
* numbers format through the house number register (`formatChartValue`) so a
|
|
8
|
+
* summary speaks the same value a sighted reader sees.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { formatChartValue } from "./math";
|
|
@@ -16,7 +17,7 @@ export interface DescribeChartInput {
|
|
|
16
17
|
/** Optional already-translated chart title. */
|
|
17
18
|
title?: string;
|
|
18
19
|
data: ChartDatum[];
|
|
19
|
-
/** Value formatter (defaults to
|
|
20
|
+
/** Value formatter (defaults to the house number register). */
|
|
20
21
|
formatValue?: (value: number) => string;
|
|
21
22
|
/**
|
|
22
23
|
* Include each datum's share of the total (pie/donut summaries).
|
package/src/charts/index.ts
CHANGED
package/src/charts/math.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* tests exercise them directly.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { formatNumber } from "@multiplatform.one/theme";
|
|
10
11
|
import { scaleBand, scaleLinear, scalePoint, scaleTime } from "d3-scale";
|
|
11
12
|
import type { ScaleBand, ScaleLinear, ScalePoint, ScaleTime } from "d3-scale";
|
|
12
13
|
import {
|
|
@@ -132,11 +133,29 @@ export function getTimeTicks(scale: ScaleTime<number, number>, count = 4): Date[
|
|
|
132
133
|
return scale.ticks(count);
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
/**
|
|
136
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Chart AXIS tick register — the one DECLARED exemption to the house number
|
|
138
|
+
* channel (LC-12 render channel / D-04). Tick labels answer to axis density,
|
|
139
|
+
* not to the human-register rules the catalog applies elsewhere: this
|
|
140
|
+
* package owns the right to thin, shorten or re-scale them (k/M suffixes,
|
|
141
|
+
* dropped fraction digits) as ticks crowd, without dragging every number in
|
|
142
|
+
* the catalog along. Keeping the exemption in one named function is what
|
|
143
|
+
* makes it a decision instead of an accident — anything outside this
|
|
144
|
+
* function belongs on the shared channel.
|
|
145
|
+
*/
|
|
146
|
+
export function formatAxisTick(value: number): string {
|
|
137
147
|
return value.toLocaleString();
|
|
138
148
|
}
|
|
139
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Chart data VALUE labels — NOT exempt. A value printed next to a mark is
|
|
152
|
+
* the same number class as the same figure in a table cell or a KPI tile,
|
|
153
|
+
* so it rides the shared register (`formatNumber`).
|
|
154
|
+
*/
|
|
155
|
+
export function formatChartValue(value: number): string {
|
|
156
|
+
return formatNumber(value);
|
|
157
|
+
}
|
|
158
|
+
|
|
140
159
|
// ── Shape generators ──────────────────────────────────────────
|
|
141
160
|
|
|
142
161
|
const curveFactories: Record<ChartCurve, CurveFactory> = {
|
package/src/componentColors.ts
CHANGED
|
@@ -40,13 +40,21 @@ export const componentColors = {
|
|
|
40
40
|
highlightLine: "$yellow3",
|
|
41
41
|
},
|
|
42
42
|
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Dot/step indicators.
|
|
45
|
+
*
|
|
46
|
+
* LC-05 ONE-EMPHASIS: the selected/current mark IS the active accent, so
|
|
47
|
+
* every strip in the catalog (DotIndicator, Pagination dots, ProgressSteps)
|
|
48
|
+
* reads one selected-state language. This is deliberately ONE key — the
|
|
49
|
+
* former `active` ($color9) / `current` ($color12) pair was two dialects for
|
|
50
|
+
* one concept and let the catalog drift back into ink (LC-16). Completion
|
|
51
|
+
* keeps its own semantic ramp; idle/track/hover stay neutral chrome.
|
|
52
|
+
*/
|
|
44
53
|
indicator: {
|
|
45
|
-
|
|
54
|
+
selected: "$accentBackground",
|
|
46
55
|
inactive: "$color5",
|
|
47
56
|
hover: "$color6",
|
|
48
57
|
completed: "$green9",
|
|
49
|
-
current: "$color12",
|
|
50
58
|
track: "$color8",
|
|
51
59
|
},
|
|
52
60
|
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
ensureKeyboardModalityTracking,
|
|
5
5
|
keyboardFocusRingProps,
|
|
6
6
|
pressTargetHitSlop,
|
|
7
|
+
useAccentTintedSurface,
|
|
8
|
+
useReadableTextOn,
|
|
7
9
|
useResolvedKnobs,
|
|
8
10
|
wasKeyboardFocus,
|
|
9
11
|
} from "@multiplatform.one/theme";
|
|
@@ -46,6 +48,17 @@ export function ViewSwitcher({ views, currentView, onViewChange }: ViewSwitcherP
|
|
|
46
48
|
// segment); when focus leaves, the anchor returns to the selection.
|
|
47
49
|
const [focusValue, setFocusValue] = useState<string | null>(null);
|
|
48
50
|
if (isWeb) ensureKeyboardModalityTracking();
|
|
51
|
+
// LC-05 / D-01 rider (b): the selected segment carried a neutral `$color3`
|
|
52
|
+
// -tier fill, so a segmented control marked its choice in the same grey it
|
|
53
|
+
// marks hover with. Selection is the accent language; segmented-control
|
|
54
|
+
// STRENGTH is pinned at tinted (not the solid an accent CTA takes), so the
|
|
55
|
+
// chip resolves the accent-ramp analogue of that neutral surface tier and
|
|
56
|
+
// its label/icon take the luminance-computed anchor for the actual tint.
|
|
57
|
+
const selectedTint = useAccentTintedSurface();
|
|
58
|
+
const onSelectedTint = useReadableTextOn(selectedTint);
|
|
59
|
+
const selectedChip = selectedTint
|
|
60
|
+
? { backgroundColor: selectedTint, ...(onSelectedTint ? { color: onSelectedTint } : undefined) }
|
|
61
|
+
: undefined;
|
|
49
62
|
const sz = knobProps.sizeToken;
|
|
50
63
|
// Segment inset ~$3 at medium; size knob steps it (small→$2, large→$4).
|
|
51
64
|
// Vertical pad is floored so the segment hits DG-LAY-04 (≥44) even at small.
|
|
@@ -103,6 +116,7 @@ export function ViewSwitcher({ views, currentView, onViewChange }: ViewSwitcherP
|
|
|
103
116
|
return activeIdx >= 0 ? activeIdx : 0;
|
|
104
117
|
})();
|
|
105
118
|
const isTabStop = viewIndex === anchorIndex;
|
|
119
|
+
const isSelected = view.type === currentView;
|
|
106
120
|
return (
|
|
107
121
|
// no size prop: ToggleGroup.Item's size makes square icon-buttons
|
|
108
122
|
// (fixed width) that clip text labels -- let content + pad size the item.
|
|
@@ -119,6 +133,12 @@ export function ViewSwitcher({ views, currentView, onViewChange }: ViewSwitcherP
|
|
|
119
133
|
flexGrow={0}
|
|
120
134
|
alignItems="center"
|
|
121
135
|
justifyContent="center"
|
|
136
|
+
// The selected fill is re-applied through hover/press so the
|
|
137
|
+
// neutral state layer never paints over the accent chip (the
|
|
138
|
+
// LC-05 rule the checked Switch track already follows).
|
|
139
|
+
{...(isSelected && selectedChip
|
|
140
|
+
? { ...selectedChip, hoverStyle: selectedChip, pressStyle: selectedChip }
|
|
141
|
+
: undefined)}
|
|
122
142
|
{...(isWeb
|
|
123
143
|
? {
|
|
124
144
|
onFocus: () => {
|
|
@@ -133,9 +153,22 @@ export function ViewSwitcher({ views, currentView, onViewChange }: ViewSwitcherP
|
|
|
133
153
|
: undefined)}
|
|
134
154
|
{...(kbFocusValue === view.type ? keyboardFocusRingProps : undefined)}
|
|
135
155
|
>
|
|
136
|
-
<XStack
|
|
156
|
+
<XStack
|
|
157
|
+
alignItems="center"
|
|
158
|
+
gap={rowGap}
|
|
159
|
+
flexShrink={0}
|
|
160
|
+
// Icons paint `currentColor`, so the row carries the resolved
|
|
161
|
+
// on-tint anchor for the glyph and the label takes it explicitly
|
|
162
|
+
// (tamagui Text resolves `$color` from the theme, not by CSS
|
|
163
|
+
// inheritance).
|
|
164
|
+
{...(isSelected && onSelectedTint ? { color: onSelectedTint } : undefined)}
|
|
165
|
+
>
|
|
137
166
|
{Icon && <Icon size={iconPx} />}
|
|
138
|
-
<Text
|
|
167
|
+
<Text
|
|
168
|
+
fontSize={labelSize}
|
|
169
|
+
numberOfLines={1}
|
|
170
|
+
{...(isSelected && onSelectedTint ? { color: onSelectedTint } : undefined)}
|
|
171
|
+
>
|
|
139
172
|
{view.label}
|
|
140
173
|
</Text>
|
|
141
174
|
</XStack>
|
package/src/layouts/page.tsx
CHANGED
|
@@ -19,7 +19,9 @@ import {
|
|
|
19
19
|
hairline,
|
|
20
20
|
layoutBreakpoints,
|
|
21
21
|
readingWidthStyle,
|
|
22
|
+
resolvePageTitleScale,
|
|
22
23
|
useResolvedKnobs,
|
|
24
|
+
type PageTitleScale,
|
|
23
25
|
} from "@multiplatform.one/theme";
|
|
24
26
|
import type { ReactNode } from "react";
|
|
25
27
|
import type { ScrollViewProps as TamaguiScrollViewProps, XStackProps, YStackProps } from "tamagui";
|
|
@@ -156,13 +158,14 @@ export interface PageHeaderProps extends YStackProps {
|
|
|
156
158
|
*/
|
|
157
159
|
headingLevel?: PageHeadingLevel;
|
|
158
160
|
/**
|
|
159
|
-
*
|
|
160
|
-
* the
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
161
|
+
* Per-instance eject for the page-title step (D-11 hero-H1 dial).
|
|
162
|
+
* Omitted — the default — the title follows the `pageTitleScale` knob, which
|
|
163
|
+
* ships `moderate` ($8 = 32px): a page title is wayfinding, not content.
|
|
164
|
+
* Pass `"display"` ($10 = 64px) for a single marketing hero, or reach for
|
|
165
|
+
* the `hero` preset / a `pageTitleScale` override to re-scale a whole
|
|
166
|
+
* surface. Semantics (headingLevel) are unchanged either way.
|
|
164
167
|
*/
|
|
165
|
-
titleScale?:
|
|
168
|
+
titleScale?: PageTitleScale;
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
/**
|
|
@@ -176,7 +179,7 @@ export function PageHeader({
|
|
|
176
179
|
actions,
|
|
177
180
|
divider = true,
|
|
178
181
|
headingLevel = 1,
|
|
179
|
-
titleScale
|
|
182
|
+
titleScale,
|
|
180
183
|
children,
|
|
181
184
|
...rest
|
|
182
185
|
}: PageHeaderProps) {
|
|
@@ -196,9 +199,11 @@ export function PageHeader({
|
|
|
196
199
|
// heading scale).
|
|
197
200
|
const headingFamily = isWeb ? null : { fontFamily: knobProps.heading.fontFamily };
|
|
198
201
|
const TitleTag = PAGE_HEADING_TAGS[headingLevel];
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
+
// D-11: the page-title step comes from the knob (`moderate` by default), so
|
|
203
|
+
// one dial re-scales every composed screen; `titleScale` is the explicit
|
|
204
|
+
// per-instance eject and resolves through the same table. The semantic tag
|
|
205
|
+
// still follows headingLevel — the step is emphasis, not structure.
|
|
206
|
+
const titleSize = titleScale ? resolvePageTitleScale(titleScale) : knobProps.pageTitle;
|
|
202
207
|
const titleNode =
|
|
203
208
|
heading ??
|
|
204
209
|
(title ? (
|
package/src/views/Dashboard.tsx
CHANGED
|
@@ -3,8 +3,13 @@ import { LineChart } from "../charts/LineChart";
|
|
|
3
3
|
import { PieChart } from "../charts/PieChart";
|
|
4
4
|
import { componentColors, sectionHeading } from "../componentColors";
|
|
5
5
|
import { AsyncBoundary } from "../layouts/AsyncBoundary";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
formatCurrency,
|
|
8
|
+
formatNumber,
|
|
9
|
+
formatPercent,
|
|
10
|
+
useResolvedKnobs,
|
|
11
|
+
} from "@multiplatform.one/theme";
|
|
12
|
+
import React, { useCallback, useMemo, type ReactNode } from "react";
|
|
8
13
|
import { useTranslation } from "react-i18next";
|
|
9
14
|
import {
|
|
10
15
|
Card,
|
|
@@ -168,18 +173,27 @@ export function resolveTrendColor(
|
|
|
168
173
|
*/
|
|
169
174
|
export function KPICard({ config, ...stackProps }: KPICardProps) {
|
|
170
175
|
const { knobProps } = useResolvedKnobs();
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
176
|
+
// One register for the metric, wherever the card shows it (LC-12 render
|
|
177
|
+
// channel): the headline value and the "vs previous" comparison are the
|
|
178
|
+
// same data class and may not speak in two voices. Summary tiles take the
|
|
179
|
+
// money register's `auto` variant — cents only when the value has them.
|
|
180
|
+
const formatMetric = useCallback(
|
|
181
|
+
(num: number) => {
|
|
182
|
+
if (config.format === "currency") {
|
|
183
|
+
return formatCurrency(num, {
|
|
184
|
+
symbol: config.currencySymbol || "$",
|
|
185
|
+
fractionDigits: "auto",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (config.format === "percent") return formatPercent(num);
|
|
189
|
+
return formatNumber(num);
|
|
190
|
+
},
|
|
191
|
+
[config.format, config.currencySymbol],
|
|
192
|
+
);
|
|
193
|
+
const formattedValue = useMemo(
|
|
194
|
+
() => (typeof config.value === "string" ? config.value : formatMetric(config.value)),
|
|
195
|
+
[config.value, formatMetric],
|
|
196
|
+
);
|
|
183
197
|
|
|
184
198
|
const trendColor = useMemo(
|
|
185
199
|
() => resolveTrendColor(config.trend, config.goodDirection),
|
|
@@ -240,7 +254,7 @@ export function KPICard({ config, ...stackProps }: KPICardProps) {
|
|
|
240
254
|
)}
|
|
241
255
|
{config.previousValue !== undefined && (
|
|
242
256
|
<Text fontSize="$2" color={componentColors.text.muted}>
|
|
243
|
-
vs {config.previousValue
|
|
257
|
+
vs {formatMetric(config.previousValue)}
|
|
244
258
|
</Text>
|
|
245
259
|
)}
|
|
246
260
|
</XStack>
|
|
@@ -2,7 +2,12 @@ import { BarChart } from "../charts/BarChart";
|
|
|
2
2
|
import { componentColors } from "../componentColors";
|
|
3
3
|
import { AsyncBoundary, AsyncSkeleton, resolveAsyncStatus } from "../layouts/AsyncBoundary";
|
|
4
4
|
import { EmptyState } from "../layouts/page";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
formatCurrency,
|
|
7
|
+
formatNumber,
|
|
8
|
+
formatPercent,
|
|
9
|
+
useResolvedKnobs,
|
|
10
|
+
} from "@multiplatform.one/theme";
|
|
6
11
|
// House Button (not raw tamagui): carries the DG-LAY-04 / D-02 ≥44px press
|
|
7
12
|
// floor — the raw tamagui Button rendered these toolbar actions at 28px.
|
|
8
13
|
import { Button } from "@multiplatform.one/forms";
|
|
@@ -114,21 +119,22 @@ function formatCellValue(value: unknown, fieldtype: string): string {
|
|
|
114
119
|
switch (fieldtype) {
|
|
115
120
|
case "Check":
|
|
116
121
|
return value ? "Yes" : "No";
|
|
122
|
+
// Report cells take the fixed-precision variants of the house number
|
|
123
|
+
// registers (LC-12 render channel): money keeps its two digits so a
|
|
124
|
+
// column stays aligned, floats keep report precision, ints stay whole.
|
|
117
125
|
case "Currency":
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
// No symbol travels with a report column, so the money register
|
|
127
|
+
// renders the amount alone — still the money register, not a
|
|
128
|
+
// hand-rolled one.
|
|
129
|
+
return formatCurrency(value as number);
|
|
122
130
|
case "Float":
|
|
123
|
-
return
|
|
124
|
-
maximumFractionDigits: 4,
|
|
125
|
-
});
|
|
131
|
+
return formatNumber(value as number, { maximumFractionDigits: 4 });
|
|
126
132
|
case "Int":
|
|
127
|
-
return
|
|
133
|
+
return formatNumber(value as number, { maximumFractionDigits: 0 });
|
|
128
134
|
case "Percent":
|
|
129
|
-
return
|
|
135
|
+
return formatPercent(value as number, { precision: 1 });
|
|
130
136
|
case "Rating":
|
|
131
|
-
return `${(Number(value) * 5
|
|
137
|
+
return `${formatNumber(Number(value) * 5, { precision: 1 })} / 5`;
|
|
132
138
|
default:
|
|
133
139
|
return String(value);
|
|
134
140
|
}
|
|
@@ -409,14 +415,15 @@ export function ReportBuilder({
|
|
|
409
415
|
|
|
410
416
|
return Object.entries(groupedData).map(([groupKey, group]) => {
|
|
411
417
|
const isCollapsed = collapsedGroups.has(groupKey);
|
|
418
|
+
// Group aggregates are the same number class as the cells below them,
|
|
419
|
+
// so they take the same register (LC-12 render channel) instead of
|
|
420
|
+
// dropping into raw digits.
|
|
412
421
|
const aggDisplay =
|
|
413
422
|
groupBy.aggregation === "count"
|
|
414
|
-
? `${aggLabel}: ${group.aggregationValue}`
|
|
415
|
-
: `${aggLabel}(${aggFieldCol?.label ?? groupBy.aggregationField ?? ""}): ${
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
: group.aggregationValue.toFixed(2)
|
|
419
|
-
}`;
|
|
423
|
+
? `${aggLabel}: ${formatNumber(group.aggregationValue, { maximumFractionDigits: 0 })}`
|
|
424
|
+
: `${aggLabel}(${aggFieldCol?.label ?? groupBy.aggregationField ?? ""}): ${formatNumber(
|
|
425
|
+
group.aggregationValue,
|
|
426
|
+
)}`;
|
|
420
427
|
|
|
421
428
|
return (
|
|
422
429
|
<YStack key={groupKey}>
|
package/src/views/TreeView.tsx
CHANGED
|
@@ -1038,10 +1038,12 @@ export function TreeView<T = unknown>({
|
|
|
1038
1038
|
const handleOutset = Math.max(0, (MIN_PRESS_TARGET - estimateSize) / 2);
|
|
1039
1039
|
|
|
1040
1040
|
// Rows span edge-to-edge inside the frame (SP-EDGE / LC-01): the container
|
|
1041
|
-
// owns only
|
|
1042
|
-
// hit targets reach both edges.
|
|
1041
|
+
// owns only the corner clip; rows own their inset, so hover/selection fills
|
|
1042
|
+
// and hit targets reach both edges. The tree is SF-TRANSPARENT, so it takes
|
|
1043
|
+
// the radius-only fragment — a border-bearing one would be unlicensed chrome
|
|
1044
|
+
// on a chromeless part (D-03).
|
|
1043
1045
|
return (
|
|
1044
|
-
<YStack {...knobProps.
|
|
1046
|
+
<YStack {...knobProps.containerRadius} overflow="hidden" {...stackProps}>
|
|
1045
1047
|
<View
|
|
1046
1048
|
ref={containerRef as any}
|
|
1047
1049
|
role="tree"
|
package/src/views/ganttMath.ts
CHANGED
|
@@ -9,8 +9,13 @@
|
|
|
9
9
|
* All day math is LOCAL wall-clock (date-component construction, deltas
|
|
10
10
|
* rounded) so ranges survive DST boundaries — the Calendar view's
|
|
11
11
|
* `computeEventMoveRange` convention.
|
|
12
|
+
*
|
|
13
|
+
* Visible and spoken labels route through the canonical date registers in
|
|
14
|
+
* `@multiplatform.one/theme` (LC-12 render channel).
|
|
12
15
|
*/
|
|
13
16
|
|
|
17
|
+
import { formatAbsoluteDate, formatDayOfMonth, formatMonthYear } from "@multiplatform.one/theme";
|
|
18
|
+
|
|
14
19
|
export type GanttZoom = "day" | "week" | "month";
|
|
15
20
|
|
|
16
21
|
export const MS_PER_DAY = 86_400_000;
|