@agregio-solutions/design-system 1.98.0 → 1.99.0

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.
@@ -1,4 +1,4 @@
1
- import { Line } from './utils/Timeline.types';
1
+ import { ExpandedGroups, Line } from './utils/Timeline.types';
2
2
  import { CSSProperties, ReactNode } from 'react';
3
3
  export type TimelineProps = {
4
4
  /**
@@ -17,6 +17,18 @@ export type TimelineProps = {
17
17
  * The configuration of the timeline to display in this line
18
18
  */
19
19
  emptyPlaceholder?: ReactNode;
20
+ /**
21
+ * Expanded state of the group lines, keyed by group name (controlled mode).
22
+ * When provided, every group line is controlled and a name missing from the
23
+ * map reads as collapsed. Leave it out to let each group keep its own
24
+ * internal state. Group names must be unique for this to be unambiguous.
25
+ */
26
+ expandedGroups?: ExpandedGroups;
27
+ /**
28
+ * Called with the complete next map when a group line is expanded or
29
+ * collapsed, so it can be passed straight to a state setter.
30
+ */
31
+ onExpandedGroupsChange?: (expandedGroups: ExpandedGroups) => void;
20
32
  /**
21
33
  * The class name of the timeline
22
34
  */
@@ -26,4 +38,4 @@ export type TimelineProps = {
26
38
  */
27
39
  style?: CSSProperties;
28
40
  };
29
- export default function Timeline({ emptyPlaceholder, lines, startDate, endDate, ...props }: TimelineProps): import("react").JSX.Element;
41
+ export default function Timeline({ emptyPlaceholder, lines, startDate, endDate, expandedGroups, onExpandedGroupsChange, ...props }: TimelineProps): import("react").JSX.Element;
@@ -10,6 +10,14 @@ export declare const GroupContainer: import('@emotion/styled').StyledComponent<{
10
10
  theme?: import('@emotion/react').Theme;
11
11
  as?: React.ElementType;
12
12
  }, import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
13
+ /**
14
+ * One visual row of an overlap group. Its blocks are absolutely positioned
15
+ * inside it, so blocks that don't overlap can share a row.
16
+ */
17
+ export declare const LaneContainer: import('@emotion/styled').StyledComponent<{
18
+ theme?: import('@emotion/react').Theme;
19
+ as?: React.ElementType;
20
+ }, import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
13
21
  export declare const Overlay: import('@emotion/styled').StyledComponent<{
14
22
  theme?: import('@emotion/react').Theme;
15
23
  as?: React.ElementType;
@@ -0,0 +1,18 @@
1
+ import { Block } from '../../../../utils/Timeline.types';
2
+ type Lane = Array<Block>;
3
+ /**
4
+ * Packs the blocks of one overlap group into lanes (the visual rows of a line).
5
+ *
6
+ * Blocks sharing a color share a lane as long as they don't overlap each other.
7
+ * The color carries the entity identity in a Timeline, so an entity reads as a
8
+ * single row instead of being spread over several rows by an unrelated block
9
+ * that happens to bridge two of its periods. A color that overlaps itself
10
+ * consumes as many lanes as needed, so a block is never hidden behind another.
11
+ *
12
+ * Lane order is derived from the data alone — earliest start of the color, then
13
+ * the color string — never from the input order: consumers refetch on an
14
+ * interval, and an insertion-ordered layout would make the rows jump on every
15
+ * refresh.
16
+ */
17
+ export declare function assignBlocksToLanes(blocks: Array<Block>): Array<Lane>;
18
+ export {};
@@ -1,7 +1,7 @@
1
1
  type GetLineHeightParams = {
2
- nbOfBlocks: number;
2
+ nbOfLanes: number;
3
3
  isCollapsed: boolean;
4
4
  indentLevel: number;
5
5
  };
6
- export default function getLineHeight({ nbOfBlocks, isCollapsed, indentLevel, }: GetLineHeightParams): number;
6
+ export default function getLineHeight({ nbOfLanes, isCollapsed, indentLevel, }: GetLineHeightParams): number;
7
7
  export {};
@@ -5,6 +5,15 @@ interface Props {
5
5
  lineStartDate: Block["startDate"];
6
6
  lineEndDate: Block["endDate"];
7
7
  groupHoverText?: GroupHoverTextFormatter;
8
+ /**
9
+ * Whether the group is expanded (controlled). Leave it undefined to let the
10
+ * group keep its own internal state.
11
+ */
12
+ isExpanded?: boolean;
13
+ /**
14
+ * Called with the next expanded state whenever the group name is clicked.
15
+ */
16
+ onExpandedChange?: (isExpanded: boolean) => void;
8
17
  }
9
- export default function GroupLineContainer({ name, subLines, lineStartDate, lineEndDate, groupHoverText, }: Props): import("react").JSX.Element;
18
+ export default function GroupLineContainer({ name, subLines, lineStartDate, lineEndDate, groupHoverText, isExpanded: controlledIsExpanded, onExpandedChange, }: Props): import("react").JSX.Element;
10
19
  export {};
@@ -15,7 +15,8 @@ Base stories:
15
15
  ```tsx
16
16
  import { Meta, StoryObj } from "@storybook/react-vite";
17
17
  import Timeline from "./Timeline";
18
- import { within, userEvent, fn } from "storybook/test";
18
+ import { Block } from "./utils/Timeline.types";
19
+ import { within, userEvent, expect, fn } from "storybook/test";
19
20
  import MockDate from "mockdate";
20
21
  import {
21
22
  expectNotPresent,
@@ -505,6 +506,11 @@ export const GroupSimple: Story = {
505
506
  await canvas.findByText("Installation 1 PRM_1");
506
507
  await canvas.findByTestId("Installation 1 PRM_1 - Indisponibilité 1");
507
508
  await canvas.findByTestId("Installation 1 PRM_1 - Activation 1");
509
+
510
+ // Clicking again collapses it back: the group holds its own state when the
511
+ // consumer doesn't control it.
512
+ await user.click(canvas.getByText("Installation 1"));
513
+ await expectNotPresent(() => canvas.queryByText("Installation 1 PRM_1"));
508
514
  },
509
515
  };
510
516
 
@@ -970,6 +976,449 @@ export const GroupFullOverlap: Story = {
970
976
  await user.click(canvas.getByText("Maison Dieu - PRM[50014414454079]"));
971
977
  },
972
978
  };
979
+
980
+ const DAY_START = new Date("2024-01-01T00:00:00.000Z");
981
+ const DAY_END = new Date("2024-01-02T00:00:00.000Z");
982
+
983
+ /**
984
+ * Names the merged blocks of a collapsed group row after the block they were
985
+ * merged from, so the stories can target them by `data-testid`. Using the id
986
+ * rather than a formatted date keeps the testids timezone-independent.
987
+ */
988
+ const groupHoverTextFromFirstBlockId = ({ blocks }: { blocks: Array<Block> }) =>
989
+ blocks[0].id;
990
+
991
+ export const GroupCollapsedSameAggregateTwice: Story = {
992
+ args: {
993
+ startDate: DAY_START,
994
+ endDate: DAY_END,
995
+ lines: [
996
+ {
997
+ type: "group",
998
+ name: "PSN",
999
+ groupHoverText: groupHoverTextFromFirstBlockId,
1000
+ subLines: [
1001
+ {
1002
+ // The reported bug: one aggregate, two solicitations separated by a
1003
+ // 1h30 gap. They must read as a single row.
1004
+ name: "Agrégat A",
1005
+ blocks: [
1006
+ {
1007
+ id: "agregat-a-matin",
1008
+ startDate: new Date("2024-01-01T11:20:00.000Z"),
1009
+ endDate: new Date("2024-01-01T13:00:00.000Z"),
1010
+ color: "var(--color-content-dataviz-purple-3)",
1011
+ hoverText: "Agrégat A - matin",
1012
+ },
1013
+ {
1014
+ id: "agregat-a-apres-midi",
1015
+ startDate: new Date("2024-01-01T14:30:00.000Z"),
1016
+ endDate: new Date("2024-01-01T16:25:00.000Z"),
1017
+ color: "var(--color-content-dataviz-purple-3)",
1018
+ hoverText: "Agrégat A - après-midi",
1019
+ },
1020
+ ],
1021
+ },
1022
+ {
1023
+ // Long blocks that bridge the two purple ones into a single overlap
1024
+ // group — the actual cause of the bug.
1025
+ name: "Agrégat B",
1026
+ blocks: [
1027
+ {
1028
+ id: "agregat-b",
1029
+ startDate: new Date("2024-01-01T10:45:00.000Z"),
1030
+ endDate: new Date("2024-01-01T16:20:00.000Z"),
1031
+ color: "var(--color-content-dataviz-azure-3)",
1032
+ hoverText: "Agrégat B",
1033
+ },
1034
+ ],
1035
+ },
1036
+ {
1037
+ name: "Agrégat C",
1038
+ blocks: [
1039
+ {
1040
+ id: "agregat-c",
1041
+ startDate: new Date("2024-01-01T11:00:00.000Z"),
1042
+ endDate: new Date("2024-01-01T16:20:00.000Z"),
1043
+ color: "var(--color-content-dataviz-pink-3)",
1044
+ hoverText: "Agrégat C",
1045
+ },
1046
+ ],
1047
+ },
1048
+ {
1049
+ name: "Agrégat D",
1050
+ blocks: [
1051
+ {
1052
+ id: "agregat-d",
1053
+ startDate: new Date("2024-01-01T10:45:00.000Z"),
1054
+ endDate: new Date("2024-01-01T16:35:00.000Z"),
1055
+ color: "var(--color-content-dataviz-ocean-3)",
1056
+ hoverText: "Agrégat D",
1057
+ },
1058
+ ],
1059
+ },
1060
+ ],
1061
+ },
1062
+ ],
1063
+ },
1064
+ play: async ({ canvasElement, mount }) => {
1065
+ MockDate.set("2024-01-01T12:00:00.000Z");
1066
+ await mount();
1067
+ const canvas = within(canvasElement);
1068
+
1069
+ const morning = canvas.getByTestId("agregat-a-matin");
1070
+ const afternoon = canvas.getByTestId("agregat-a-apres-midi");
1071
+
1072
+ // Both belong to the same overlap group, bridged by the long blocks…
1073
+ await expect(morning).toHaveAttribute("data-group-index", "0");
1074
+ await expect(afternoon).toHaveAttribute("data-group-index", "0");
1075
+
1076
+ // …and they are two distinct blocks, not a single merged one: the 1h30 gap
1077
+ // between 13:00 and 14:30 stays visible.
1078
+ await expect(morning.getAttribute("data-start")).not.toBe(
1079
+ afternoon.getAttribute("data-start"),
1080
+ );
1081
+
1082
+ // The fix: one lane for the aggregate, instead of one row per solicitation.
1083
+ await expect(morning).toHaveAttribute("data-lane-index", "3");
1084
+ await expect(afternoon).toHaveAttribute("data-lane-index", "3");
1085
+
1086
+ // Lanes are ordered by the earliest start of their color, then by the color
1087
+ // string: azure and ocean both start at 10:45, azure wins the tie.
1088
+ await expect(canvas.getByTestId("agregat-b")).toHaveAttribute(
1089
+ "data-lane-index",
1090
+ "0",
1091
+ );
1092
+ await expect(canvas.getByTestId("agregat-d")).toHaveAttribute(
1093
+ "data-lane-index",
1094
+ "1",
1095
+ );
1096
+ await expect(canvas.getByTestId("agregat-c")).toHaveAttribute(
1097
+ "data-lane-index",
1098
+ "2",
1099
+ );
1100
+ },
1101
+ };
1102
+
1103
+ export const GroupExpandedLanePacking: Story = {
1104
+ args: {
1105
+ startDate: DAY_START,
1106
+ endDate: DAY_END,
1107
+ lines: [
1108
+ {
1109
+ type: "group",
1110
+ name: "PSN",
1111
+ groupHoverText: groupHoverTextFromFirstBlockId,
1112
+ subLines: [
1113
+ {
1114
+ name: "Agrégat A",
1115
+ blocks: [
1116
+ // Same color, disjoint: they share a lane even on a sub-line.
1117
+ {
1118
+ id: "purple-matin",
1119
+ startDate: new Date("2024-01-01T10:00:00.000Z"),
1120
+ endDate: new Date("2024-01-01T12:00:00.000Z"),
1121
+ color: "var(--color-content-dataviz-purple-3)",
1122
+ hoverText: "Sub-line - purple matin",
1123
+ },
1124
+ {
1125
+ id: "purple-apres-midi",
1126
+ startDate: new Date("2024-01-01T12:30:00.000Z"),
1127
+ endDate: new Date("2024-01-01T14:00:00.000Z"),
1128
+ color: "var(--color-content-dataviz-purple-3)",
1129
+ hoverText: "Sub-line - purple après-midi",
1130
+ },
1131
+ // Three more colors, so the sub-line needs four lanes and crosses
1132
+ // its own threshold of three.
1133
+ {
1134
+ id: "azure",
1135
+ startDate: new Date("2024-01-01T10:00:00.000Z"),
1136
+ endDate: new Date("2024-01-01T14:00:00.000Z"),
1137
+ color: "var(--color-content-dataviz-azure-3)",
1138
+ hoverText: "Sub-line - azure",
1139
+ },
1140
+ {
1141
+ id: "ocean",
1142
+ startDate: new Date("2024-01-01T10:00:00.000Z"),
1143
+ endDate: new Date("2024-01-01T14:00:00.000Z"),
1144
+ color: "var(--color-content-dataviz-ocean-3)",
1145
+ hoverText: "Sub-line - ocean",
1146
+ },
1147
+ {
1148
+ id: "pink",
1149
+ startDate: new Date("2024-01-01T10:00:00.000Z"),
1150
+ endDate: new Date("2024-01-01T14:00:00.000Z"),
1151
+ color: "var(--color-content-dataviz-pink-3)",
1152
+ hoverText: "Sub-line - pink",
1153
+ },
1154
+ ],
1155
+ },
1156
+ {
1157
+ name: "Agrégat B",
1158
+ blocks: [
1159
+ {
1160
+ id: "brown",
1161
+ startDate: new Date("2024-01-01T11:00:00.000Z"),
1162
+ endDate: new Date("2024-01-01T13:00:00.000Z"),
1163
+ color: "var(--color-content-dataviz-brown-3)",
1164
+ hoverText: "Sub-line - brown",
1165
+ },
1166
+ ],
1167
+ },
1168
+ ],
1169
+ },
1170
+ ],
1171
+ },
1172
+ play: async ({ canvasElement, mount }) => {
1173
+ MockDate.set("2024-01-01T12:00:00.000Z");
1174
+ await mount();
1175
+ const canvas = within(canvasElement);
1176
+ const user = userEvent.setup();
1177
+
1178
+ await user.click(canvas.getByText("PSN"));
1179
+ await canvas.findByText("Agrégat A");
1180
+
1181
+ // A sub-line collapses at three lanes, not five. Purple sorts last, so it
1182
+ // is the lane that gets hidden.
1183
+ await expect(canvas.getByTestId("Sub-line - azure")).toHaveAttribute(
1184
+ "data-lane-index",
1185
+ "0",
1186
+ );
1187
+ await expect(canvas.getByTestId("Sub-line - ocean")).toHaveAttribute(
1188
+ "data-lane-index",
1189
+ "1",
1190
+ );
1191
+ await expect(canvas.getByTestId("Sub-line - pink")).toHaveAttribute(
1192
+ "data-lane-index",
1193
+ "2",
1194
+ );
1195
+ await expectNotPresent(() =>
1196
+ canvas.queryByTestId("Sub-line - purple matin"),
1197
+ );
1198
+
1199
+ await user.click(canvas.getByRole("button", { name: "Afficher plus" }));
1200
+
1201
+ // Once expanded, the two purple blocks share the fourth lane.
1202
+ await expect(canvas.getByTestId("Sub-line - purple matin")).toHaveAttribute(
1203
+ "data-lane-index",
1204
+ "3",
1205
+ );
1206
+ await expect(
1207
+ canvas.getByTestId("Sub-line - purple après-midi"),
1208
+ ).toHaveAttribute("data-lane-index", "3");
1209
+ },
1210
+ };
1211
+
1212
+ const TWO_PERIODS = [
1213
+ {
1214
+ startDate: new Date("2024-01-01T10:00:00.000Z"),
1215
+ endDate: new Date("2024-01-01T14:00:00.000Z"),
1216
+ },
1217
+ {
1218
+ startDate: new Date("2024-01-01T15:00:00.000Z"),
1219
+ endDate: new Date("2024-01-01T19:00:00.000Z"),
1220
+ },
1221
+ ];
1222
+
1223
+ export const GroupCollapsedExpandButtonCountsLanes: Story = {
1224
+ args: {
1225
+ startDate: DAY_START,
1226
+ endDate: DAY_END,
1227
+ lines: [
1228
+ {
1229
+ type: "group",
1230
+ name: "PSN",
1231
+ groupHoverText: groupHoverTextFromFirstBlockId,
1232
+ subLines: [
1233
+ // Seven blocks, but only five lanes: two aggregates are active over
1234
+ // two disjoint periods each. The collapse threshold is five at this
1235
+ // indent level, so counting blocks would truncate and show the expand
1236
+ // button, while counting lanes shows everything.
1237
+ {
1238
+ name: "Agrégat azure",
1239
+ blocks: [
1240
+ {
1241
+ id: "azure",
1242
+ startDate: new Date("2024-01-01T10:00:00.000Z"),
1243
+ endDate: new Date("2024-01-01T20:00:00.000Z"),
1244
+ color: "var(--color-content-dataviz-azure-3)",
1245
+ hoverText: "Agrégat azure",
1246
+ },
1247
+ ],
1248
+ },
1249
+ {
1250
+ name: "Agrégat brown",
1251
+ blocks: TWO_PERIODS.map((period, index) => ({
1252
+ ...period,
1253
+ id: `brown-${index + 1}`,
1254
+ color: "var(--color-content-dataviz-brown-3)",
1255
+ hoverText: `Agrégat brown ${index + 1}`,
1256
+ })),
1257
+ },
1258
+ {
1259
+ name: "Agrégat negative",
1260
+ blocks: TWO_PERIODS.map((period, index) => ({
1261
+ ...period,
1262
+ id: `negative-${index + 1}`,
1263
+ color: "var(--color-content-dataviz-negative-3)",
1264
+ hoverText: `Agrégat negative ${index + 1}`,
1265
+ })),
1266
+ },
1267
+ {
1268
+ name: "Agrégat neutral",
1269
+ blocks: [
1270
+ {
1271
+ id: "neutral",
1272
+ startDate: new Date("2024-01-01T10:00:00.000Z"),
1273
+ endDate: new Date("2024-01-01T20:00:00.000Z"),
1274
+ color: "var(--color-content-dataviz-neutral-3)",
1275
+ hoverText: "Agrégat neutral",
1276
+ },
1277
+ ],
1278
+ },
1279
+ {
1280
+ name: "Agrégat ocean",
1281
+ blocks: [
1282
+ {
1283
+ id: "ocean",
1284
+ startDate: new Date("2024-01-01T10:00:00.000Z"),
1285
+ endDate: new Date("2024-01-01T20:00:00.000Z"),
1286
+ color: "var(--color-content-dataviz-ocean-3)",
1287
+ hoverText: "Agrégat ocean",
1288
+ },
1289
+ ],
1290
+ },
1291
+ ],
1292
+ },
1293
+ ],
1294
+ },
1295
+ play: async ({ canvasElement, mount }) => {
1296
+ MockDate.set("2024-01-01T12:00:00.000Z");
1297
+ await mount();
1298
+ const canvas = within(canvasElement);
1299
+
1300
+ // Five lanes for seven blocks: nothing is truncated, so no expand button.
1301
+ await expectNotPresent(() =>
1302
+ canvas.queryByRole("button", { name: "Afficher plus" }),
1303
+ );
1304
+
1305
+ // Every block stays visible, including the second period of each of the two
1306
+ // aggregates that have one.
1307
+ for (const id of [
1308
+ "azure",
1309
+ "brown-1",
1310
+ "brown-2",
1311
+ "negative-1",
1312
+ "negative-2",
1313
+ "neutral",
1314
+ "ocean",
1315
+ ]) {
1316
+ await canvas.findByTestId(id);
1317
+ }
1318
+
1319
+ // All five colors start at 10:00, so the color string orders the lanes, and
1320
+ // each aggregate's two periods share its lane.
1321
+ await expect(canvas.getByTestId("azure")).toHaveAttribute(
1322
+ "data-lane-index",
1323
+ "0",
1324
+ );
1325
+ await expect(canvas.getByTestId("brown-1")).toHaveAttribute(
1326
+ "data-lane-index",
1327
+ "1",
1328
+ );
1329
+ await expect(canvas.getByTestId("brown-2")).toHaveAttribute(
1330
+ "data-lane-index",
1331
+ "1",
1332
+ );
1333
+ await expect(canvas.getByTestId("negative-1")).toHaveAttribute(
1334
+ "data-lane-index",
1335
+ "2",
1336
+ );
1337
+ await expect(canvas.getByTestId("negative-2")).toHaveAttribute(
1338
+ "data-lane-index",
1339
+ "2",
1340
+ );
1341
+ await expect(canvas.getByTestId("neutral")).toHaveAttribute(
1342
+ "data-lane-index",
1343
+ "3",
1344
+ );
1345
+ await expect(canvas.getByTestId("ocean")).toHaveAttribute(
1346
+ "data-lane-index",
1347
+ "4",
1348
+ );
1349
+ },
1350
+ };
1351
+
1352
+ export const GroupControlledExpansion: Story = {
1353
+ args: {
1354
+ startDate: new Date("2024-01-01T00:00:00.000Z"),
1355
+ endDate: new Date("2024-01-08T00:00:00.000Z"),
1356
+ // MA is expanded and PSN collapsed from the outside: the consumer owns the state
1357
+ // (e.g. to keep it across data changes that would otherwise remount the group).
1358
+ expandedGroups: { MA: true, PSN: false },
1359
+ onExpandedGroupsChange: fn(),
1360
+ lines: [
1361
+ {
1362
+ type: "group",
1363
+ name: "MA",
1364
+ subLines: [
1365
+ {
1366
+ name: "Agrégat MA 1",
1367
+ blocks: [
1368
+ {
1369
+ id: "ma-1",
1370
+ startDate: new Date("2024-01-02T00:00:00.000Z"),
1371
+ endDate: new Date("2024-01-04T00:00:00.000Z"),
1372
+ color: "var(--color-content-dataviz-azure-3)",
1373
+ hoverText: "Agrégat MA 1 - Sollicitation 1",
1374
+ },
1375
+ ],
1376
+ },
1377
+ ],
1378
+ },
1379
+ {
1380
+ type: "group",
1381
+ name: "PSN",
1382
+ subLines: [
1383
+ {
1384
+ name: "Agrégat PSN 1",
1385
+ blocks: [
1386
+ {
1387
+ id: "psn-1",
1388
+ startDate: new Date("2024-01-05T00:00:00.000Z"),
1389
+ endDate: new Date("2024-01-07T00:00:00.000Z"),
1390
+ color: "var(--color-content-dataviz-purple-3)",
1391
+ hoverText: "Agrégat PSN 1 - Sollicitation 1",
1392
+ },
1393
+ ],
1394
+ },
1395
+ ],
1396
+ },
1397
+ ],
1398
+ },
1399
+ play: async ({ args, canvasElement, mount }) => {
1400
+ MockDate.set("2024-01-05T12:00:00.000Z");
1401
+ await mount();
1402
+ const canvas = within(canvasElement);
1403
+ const user = userEvent.setup();
1404
+
1405
+ // Expanded state comes from the prop only: no click needed for MA, and PSN stays
1406
+ // collapsed even though both groups render the exact same way.
1407
+ await canvas.findByText("Agrégat MA 1");
1408
+ await expectNotPresent(() => canvas.queryByText("Agrégat PSN 1"));
1409
+
1410
+ await user.click(canvas.getByText("MA"));
1411
+
1412
+ // The callback hands back the whole next map, ready for a state setter.
1413
+ expect(args.onExpandedGroupsChange).toHaveBeenCalledWith({
1414
+ MA: false,
1415
+ PSN: false,
1416
+ });
1417
+ // The story's args are static, so the row must stay expanded: the prop is the
1418
+ // single source of truth, the internal state never shadows it.
1419
+ expect(canvas.getByText("Agrégat MA 1")).toBeInTheDocument();
1420
+ },
1421
+ };
973
1422
  ```
974
1423
 
975
1424
  ## Developer notes
@@ -1084,6 +1533,24 @@ Give `groupHoverText` a stable identity (`useCallback`, or defined outside the c
1084
1533
 
1085
1534
  Clicking the group name **expands** it, revealing each sub-line on its own indented row. Clicking again collapses it back.
1086
1535
 
1536
+ By default each group owns that state internally. It is therefore lost whenever the group unmounts — which happens more often than it looks, since removing a line shifts its siblings' React keys. Pass `expandedGroups` (a `Record<groupName, boolean>`) and `onExpandedGroupsChange` to hold it yourself instead:
1537
+
1538
+ ```tsx
1539
+ const [expandedGroups, setExpandedGroups] = useState({ MA: true });
1540
+
1541
+ <Timeline
1542
+ lines={lines}
1543
+ startDate={startDate}
1544
+ endDate={endDate}
1545
+ expandedGroups={expandedGroups}
1546
+ onExpandedGroupsChange={setExpandedGroups}
1547
+ />;
1548
+ ```
1549
+
1550
+ `onExpandedGroupsChange` receives the complete next map, so it can be handed straight to a state setter. A group name missing from the map reads as collapsed, and the two props go together: providing `expandedGroups` without `onExpandedGroupsChange` freezes every group. Since the map is keyed by name, **group names must be unique**.
1551
+
1552
+ <Canvas of={Timeline.GroupControlledExpansion} />
1553
+
1087
1554
  ### Simple group (one sub-line)
1088
1555
 
1089
1556
  <Canvas of={Timeline.GroupSimple} />
@@ -100,8 +100,12 @@ export type GroupLine = {
100
100
  groupHoverText?: GroupHoverTextFormatter;
101
101
  };
102
102
  export type Line = SimpleLine | GroupLine;
103
+ /**
104
+ * Expanded state of the group lines, keyed by group line name.
105
+ */
106
+ export type ExpandedGroups = Record<string, boolean>;
103
107
  export declare function isGroupLine(line: Line): line is GroupLine;
104
108
  export declare const NAMES_SECTION_WIDTH = 188;
105
109
  export declare const lineHeightForIndentLevel: (indentLevel: number) => 40 | 58;
106
- export declare const maxBlocksToDisplayBeforeCollapsingForIndentLevel: (indentLevel: number) => 3 | 5;
110
+ export declare const maxLanesToDisplayBeforeCollapsingForIndentLevel: (indentLevel: number) => 3 | 5;
107
111
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agregio-solutions/design-system",
3
- "version": "1.98.0",
3
+ "version": "1.99.0",
4
4
  "description": "React Component library and Storybook that is part of the Design System for Agregio Solutions",
5
5
  "type": "module",
6
6
  "module": "dist/design-system.js",