@multiplatform.one/components 6.4.0 → 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/dist/cjs/icons/index.cjs +2 -1
- package/dist/cjs/icons/index.native.js +1 -0
- package/dist/cjs/icons/index.native.js.map +1 -1
- package/dist/esm/icons/index.mjs +1 -0
- package/dist/esm/icons/index.mjs.map +1 -1
- package/dist/esm/icons/index.native.js +1 -0
- package/dist/esm/icons/index.native.js.map +1 -1
- package/dist/jsx/icons/index.mjs +1 -0
- package/dist/jsx/icons/index.mjs.map +1 -1
- package/dist/jsx/icons/index.native.js +1 -0
- package/dist/jsx/icons/index.native.js.map +1 -1
- 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/icons/index.ts +4 -0
- package/src/layouts/ViewSwitcher.tsx +35 -2
- package/src/layouts/page.tsx +15 -10
- package/src/views/Calendar.tsx +6 -23
- package/src/views/Dashboard.tsx +29 -15
- package/src/views/ReportBuilder.tsx +24 -17
- package/src/views/TreeView.tsx +5 -3
- package/src/views/calendarMath.ts +9 -7
- package/src/views/ganttMath.ts +20 -12
- package/types/icons/index.d.ts +1 -0
- package/types/icons/index.d.ts.map +1 -1
package/dist/cjs/icons/index.cjs
CHANGED
|
@@ -19,4 +19,5 @@ var icons_exports = {};
|
|
|
19
19
|
module.exports = __toCommonJS(icons_exports);
|
|
20
20
|
__reExport(icons_exports, require("./CheckRegular.cjs"), module.exports);
|
|
21
21
|
__reExport(icons_exports, require("./MinusRegular.cjs"), module.exports);
|
|
22
|
-
__reExport(icons_exports, require("./lucide.cjs"), module.exports);
|
|
22
|
+
__reExport(icons_exports, require("./lucide.cjs"), module.exports);
|
|
23
|
+
__reExport(icons_exports, require("./themed.cjs"), module.exports);
|
|
@@ -22,4 +22,5 @@ module.exports = __toCommonJS(icons_exports);
|
|
|
22
22
|
__reExport(icons_exports, require("./CheckRegular.native.js"), module.exports);
|
|
23
23
|
__reExport(icons_exports, require("./MinusRegular.native.js"), module.exports);
|
|
24
24
|
__reExport(icons_exports, require("./lucide.native.js"), module.exports);
|
|
25
|
+
__reExport(icons_exports, require("./themed.native.js"), module.exports);
|
|
25
26
|
//# sourceMappingURL=index.native.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["__toCommonJS","mod","__copyProps","__defProp","value","icons_exports","module","exports","__reExport","require"],"sources":["../../../src/icons/index.ts"],"sourcesContent":[null],"mappings":";;;;;;;;;;;;;;;;AAAA,IAAAA,YAAA,GAAAC,GAAA,IAAAC,WAAA,CAAAC,SAAA;EAAAC,KAAA;AAAA,IAAAH,GAAA;AAAA,IAAAI,aAAA;AAAAC,MAAA,CAAAC,OAAA,GAAAP,YAAc,CAAAK,aAAA;AACdG,UAAA,CAAAH,aAAA,EAAcI,OAAA,8BADdH,MAAA,CAAAC,OAAA;AAEAC,UAAA,CAAAH,aAAA,EAAcI,OAAA,2BAFd,GAAAH,MAAA,CAAAC,OAAA","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["__toCommonJS","mod","__copyProps","__defProp","value","icons_exports","module","exports","__reExport","require"],"sources":["../../../src/icons/index.ts"],"sourcesContent":[null],"mappings":";;;;;;;;;;;;;;;;AAAA,IAAAA,YAAA,GAAAC,GAAA,IAAAC,WAAA,CAAAC,SAAA;EAAAC,KAAA;AAAA,IAAAH,GAAA;AAAA,IAAAI,aAAA;AAAAC,MAAA,CAAAC,OAAA,GAAAP,YAAc,CAAAK,aAAA;AACdG,UAAA,CAAAH,aAAA,EAAcI,OAAA,8BADdH,MAAA,CAAAC,OAAA;AAEAC,UAAA,CAAAH,aAAA,EAAcI,OAAA,2BAFd,GAAAH,MAAA,CAAAC,OAAA;AAMAC,UAAA,CAAAH,aAAA,EAAcI,OAAA,wBANdH,MAAA,CAAAC,OAAA","ignoreList":[]}
|
package/dist/esm/icons/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sources":["../../../src/icons/index.ts"],"sourcesContent":[null],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":[],"sources":["../../../src/icons/index.ts"],"sourcesContent":[null],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AAId,cAAc","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sources":["../../../src/icons/index.ts"],"sourcesContent":[null],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":[],"sources":["../../../src/icons/index.ts"],"sourcesContent":[null],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AAId,cAAc","ignoreList":[]}
|
package/dist/jsx/icons/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sources":["../../../src/icons/index.ts"],"sourcesContent":[null],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":[],"sources":["../../../src/icons/index.ts"],"sourcesContent":[null],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AAId,cAAc","ignoreList":[]}
|
|
@@ -22,4 +22,5 @@ module.exports = __toCommonJS(icons_exports);
|
|
|
22
22
|
__reExport(icons_exports, require("./CheckRegular.native.js"), module.exports);
|
|
23
23
|
__reExport(icons_exports, require("./MinusRegular.native.js"), module.exports);
|
|
24
24
|
__reExport(icons_exports, require("./lucide.native.js"), module.exports);
|
|
25
|
+
__reExport(icons_exports, require("./themed.native.js"), module.exports);
|
|
25
26
|
//# sourceMappingURL=index.native.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["__defProp","Object","defineProperty","__getOwnPropDesc","getOwnPropertyDescriptor"],"sources":["../../../src/icons/index.ts"],"sourcesContent":[null],"mappings":"AAAA;;AACA,IAAAA,SAAA,GAAcC,MAAA,CAAAC,cAAA;AACd,IAAAC,gBAAc,GAAAF,MAAA,CAAAG,wBAAA","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["__defProp","Object","defineProperty","__getOwnPropDesc","getOwnPropertyDescriptor","__getOwnPropNames","getOwnPropertyNames"],"sources":["../../../src/icons/index.ts"],"sourcesContent":[null],"mappings":"AAAA;;AACA,IAAAA,SAAA,GAAcC,MAAA,CAAAC,cAAA;AACd,IAAAC,gBAAc,GAAAF,MAAA,CAAAG,wBAAA;AAId,IAAAC,iBAAc,GAAAJ,MAAA,CAAAK,mBAAA","ignoreList":[]}
|
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/router": "6.4.
|
|
80
|
-
"@multiplatform.one/theme": "6.4.
|
|
81
|
-
"@multiplatform.one/platform": "6.4.
|
|
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/config": "6.4.
|
|
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
|
|
package/src/icons/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export * from "./CheckRegular";
|
|
2
2
|
export * from "./MinusRegular";
|
|
3
3
|
export * from "./lucide";
|
|
4
|
+
// Token-resolving icon wrapper (LC-53 GLYPH-SLOT): icon-only controls slot
|
|
5
|
+
// `themed(Icon)` elements and color them with bare theme keys ("red9"),
|
|
6
|
+
// instead of baseline-shifting Text wrappers around raw icons.
|
|
7
|
+
export * from "./themed";
|
|
@@ -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/Calendar.tsx
CHANGED
|
@@ -18,6 +18,10 @@ import {
|
|
|
18
18
|
type CalendarRange,
|
|
19
19
|
} from "./calendarMath";
|
|
20
20
|
import {
|
|
21
|
+
formatDayLong,
|
|
22
|
+
formatTimeOfDay,
|
|
23
|
+
getMonthNames,
|
|
24
|
+
getWeekdayNames,
|
|
21
25
|
hairline,
|
|
22
26
|
hairlineWidth,
|
|
23
27
|
pressTargetHitSlop,
|
|
@@ -389,34 +393,13 @@ const DAY_DISC_PX = 22;
|
|
|
389
393
|
/** Month-view chip visual floor (hit slop fills the 44px target, DG-LAY-04). */
|
|
390
394
|
const MONTH_CHIP_MIN_PX = 24;
|
|
391
395
|
|
|
392
|
-
function getWeekdayNames(): string[] {
|
|
393
|
-
const formatter = new Intl.DateTimeFormat(undefined, { weekday: "short" });
|
|
394
|
-
return Array.from({ length: 7 }, (_, i) => formatter.format(new Date(2004, 0, 4 + i)));
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function getMonthNames(): string[] {
|
|
398
|
-
const formatter = new Intl.DateTimeFormat(undefined, { month: "long" });
|
|
399
|
-
return Array.from({ length: 12 }, (_, i) => formatter.format(new Date(2000, i, 1)));
|
|
400
|
-
}
|
|
401
|
-
|
|
402
396
|
function getHours(): number[] {
|
|
403
397
|
return Array.from({ length: 24 }, (_, i) => i);
|
|
404
398
|
}
|
|
405
399
|
|
|
400
|
+
/** Hour ticks speak the house time-of-day register without minutes ("2 PM"). */
|
|
406
401
|
function formatHour(hour: number): string {
|
|
407
|
-
|
|
408
|
-
return new Intl.DateTimeFormat(undefined, {
|
|
409
|
-
hour: "numeric",
|
|
410
|
-
hour12: true,
|
|
411
|
-
}).format(date);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function formatDayLong(date: Date): string {
|
|
415
|
-
return new Intl.DateTimeFormat(undefined, {
|
|
416
|
-
weekday: "long",
|
|
417
|
-
month: "long",
|
|
418
|
-
day: "numeric",
|
|
419
|
-
}).format(date);
|
|
402
|
+
return formatTimeOfDay(new Date(2000, 0, 1, hour, 0, 0), { minutes: false });
|
|
420
403
|
}
|
|
421
404
|
|
|
422
405
|
const weekdayNames = getWeekdayNames();
|
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"
|
|
@@ -10,8 +10,14 @@
|
|
|
10
10
|
* values — the component layer tweens between them with `knobProps.transition`
|
|
11
11
|
* (LC-25 CONTROL-SMOOTH-JUMP) while raw pointer tracking stays 1:1
|
|
12
12
|
* (A-GESTURE).
|
|
13
|
+
*
|
|
14
|
+
* Every visible/spoken date string here routes through the canonical
|
|
15
|
+
* registers in `@multiplatform.one/theme` (LC-12 render channel) — the
|
|
16
|
+
* calendar speaks the same date voice as tables, feeds and pickers.
|
|
13
17
|
*/
|
|
14
18
|
|
|
19
|
+
import { formatDayLong, formatTimeOfDay } from "@multiplatform.one/theme";
|
|
20
|
+
|
|
15
21
|
export const MINUTES_PER_DAY = 1440;
|
|
16
22
|
|
|
17
23
|
/** Wall-clock minutes since midnight for a date. */
|
|
@@ -201,9 +207,9 @@ export function moveFocusSlot(
|
|
|
201
207
|
|
|
202
208
|
// ── Formatting / accessible names ─────────────────────────────
|
|
203
209
|
|
|
204
|
-
/** "2:00 PM"
|
|
210
|
+
/** "2:00 PM" — the house time-of-day register. */
|
|
205
211
|
export function formatTimeShort(date: Date): string {
|
|
206
|
-
return
|
|
212
|
+
return formatTimeOfDay(date);
|
|
207
213
|
}
|
|
208
214
|
|
|
209
215
|
/** "2:00 PM – 3:30 PM" visible label used by resize / create sketches. */
|
|
@@ -217,11 +223,7 @@ export function formatTimeRangeLabel(start: Date, end: Date): string {
|
|
|
217
223
|
* Untimed events fall back to the day name alone.
|
|
218
224
|
*/
|
|
219
225
|
export function eventAccessibleLabel(event: { title: string; start: Date; end?: Date }): string {
|
|
220
|
-
const dayLabel =
|
|
221
|
-
weekday: "long",
|
|
222
|
-
month: "long",
|
|
223
|
-
day: "numeric",
|
|
224
|
-
}).format(event.start);
|
|
226
|
+
const dayLabel = formatDayLong(event.start);
|
|
225
227
|
if (!event.end || event.end.getTime() === event.start.getTime()) {
|
|
226
228
|
return `${event.title}, ${dayLabel}`;
|
|
227
229
|
}
|
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;
|
|
@@ -241,9 +246,12 @@ export interface GanttTick {
|
|
|
241
246
|
label?: string;
|
|
242
247
|
}
|
|
243
248
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
249
|
+
// Timeline labels ride the canonical date registers (LC-12 render channel).
|
|
250
|
+
// Ticks take `year: "never"` — the header states the year, and a widened
|
|
251
|
+
// "Mar 10, 2025" tick would collide with its neighbour.
|
|
252
|
+
const dayLabel = (date: Date) => formatDayOfMonth(date);
|
|
253
|
+
const monthDayLabel = (date: Date) => formatAbsoluteDate(date, { year: "never" });
|
|
254
|
+
const monthYearLabel = (date: Date) => formatMonthYear(date);
|
|
247
255
|
|
|
248
256
|
/**
|
|
249
257
|
* Axis ticks for a range at a zoom level.
|
|
@@ -271,7 +279,7 @@ export function generateTicks(
|
|
|
271
279
|
x,
|
|
272
280
|
date,
|
|
273
281
|
major: true,
|
|
274
|
-
label: monthEdge ?
|
|
282
|
+
label: monthEdge ? monthDayLabel(date) : dayLabel(date),
|
|
275
283
|
});
|
|
276
284
|
continue;
|
|
277
285
|
}
|
|
@@ -281,7 +289,7 @@ export function generateTicks(
|
|
|
281
289
|
x,
|
|
282
290
|
date,
|
|
283
291
|
major: weekStart,
|
|
284
|
-
label: weekStart ?
|
|
292
|
+
label: weekStart ? monthDayLabel(date) : undefined,
|
|
285
293
|
});
|
|
286
294
|
continue;
|
|
287
295
|
}
|
|
@@ -292,7 +300,7 @@ export function generateTicks(
|
|
|
292
300
|
x,
|
|
293
301
|
date,
|
|
294
302
|
major: monthStart,
|
|
295
|
-
label: monthStart ?
|
|
303
|
+
label: monthStart ? monthYearLabel(date) : undefined,
|
|
296
304
|
});
|
|
297
305
|
}
|
|
298
306
|
return ticks;
|
|
@@ -300,8 +308,8 @@ export function generateTicks(
|
|
|
300
308
|
|
|
301
309
|
/** Visible header label for the chart range: "Mar 2026 – Jun 2026". */
|
|
302
310
|
export function formatRangeLabel(range: GanttRange): string {
|
|
303
|
-
const start =
|
|
304
|
-
const end =
|
|
311
|
+
const start = monthYearLabel(range.start);
|
|
312
|
+
const end = monthYearLabel(addDays(range.end, -1));
|
|
305
313
|
return start === end ? start : `${start} – ${end}`;
|
|
306
314
|
}
|
|
307
315
|
|
|
@@ -312,8 +320,8 @@ export function formatRangeLabel(range: GanttRange): string {
|
|
|
312
320
|
export function ganttTaskAccessibleLabel(task: { title: string; start: Date; end: Date }): string {
|
|
313
321
|
const { startEdge, endEdge } = normalizeSpanEdges(task);
|
|
314
322
|
const lastDay = addDays(endEdge, -1);
|
|
315
|
-
const startLabel =
|
|
316
|
-
const endLabel =
|
|
323
|
+
const startLabel = monthDayLabel(startEdge);
|
|
324
|
+
const endLabel = monthDayLabel(
|
|
317
325
|
lastDay.getTime() >= startEdge.getTime() ? lastDay : startEdge,
|
|
318
326
|
);
|
|
319
327
|
return startLabel === endLabel
|
|
@@ -325,8 +333,8 @@ export function ganttTaskAccessibleLabel(task: { title: string; start: Date; end
|
|
|
325
333
|
export function formatSpanLabel(span: GanttSpan): string {
|
|
326
334
|
const { startEdge, endEdge } = normalizeSpanEdges(span);
|
|
327
335
|
const lastDay = addDays(endEdge, -1);
|
|
328
|
-
const startLabel =
|
|
329
|
-
const endLabel =
|
|
336
|
+
const startLabel = monthDayLabel(startEdge);
|
|
337
|
+
const endLabel = monthDayLabel(
|
|
330
338
|
lastDay.getTime() >= startEdge.getTime() ? lastDay : startEdge,
|
|
331
339
|
);
|
|
332
340
|
return startLabel === endLabel ? startLabel : `${startLabel} – ${endLabel}`;
|
package/types/icons/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/icons/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/icons/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AAIzB,cAAc,UAAU,CAAC"}
|