react-justified-layout-ts 2.0.9 → 2.1.4

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/README.md CHANGED
@@ -4,7 +4,8 @@ A TypeScript component similar to Flickr's justified layout.
4
4
 
5
5
 
6
6
  Add the `TSJustifiedLayout` component to your code with the following props:
7
- - `layoutItems: number[];` - An array of aspect ratios for the images you are adding to the grid
7
+
8
+ - `aspectRatioList: number[];` - An array of aspect ratios for the images you are adding to the grid
8
9
  - `itemSpacing?: number;` - The amount of spacing between each image, in pixels. (Default: 10)
9
10
  - `rowSpacing?: number;` - The amount of spacing between each row, in pixels. (Default: 10)
10
11
  - `targetRowHeight?: number;` - The target row height for a row, in pixels. (Default: 320)
@@ -18,7 +19,7 @@ Add the `TSJustifiedLayout` component to your code with the following props:
18
19
 
19
20
  ```typescript jsx
20
21
  <TSJustifiedLayout width={width}
21
- layoutItems={imagesOnPage.map(value => ({
22
+ aspectRatioList={imagesOnPage.map(value => ({
22
23
  dimensions: value.aspectRatio || 1
23
24
  }))}>
24
25
  {imagesOnPage.map(value => <img
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import './layout.css';
3
+ export interface JustifiedGridProps {
4
+ aspectRatioList: number[];
5
+ itemSpacing?: number;
6
+ rowSpacing?: number;
7
+ targetRowHeight?: number;
8
+ targetRowHeightTolerance?: number;
9
+ width: number;
10
+ children: React.JSX.Element[];
11
+ containerStyle?: React.CSSProperties;
12
+ }
13
+ export declare function JustifiedGrid(props: Readonly<JustifiedGridProps>): React.JSX.Element;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.JustifiedGrid = JustifiedGrid;
7
+ const react_1 = __importDefault(require("react"));
8
+ const lodash_1 = __importDefault(require("lodash"));
9
+ require("./layout.css");
10
+ function getJustifiedGridRowConfiguration(width, targetRowHeightTolerance, aspectRatioList, targetRowHeight, itemSpacing) {
11
+ let buffer = [];
12
+ const rows = [];
13
+ // We assume that the row will have the specified height, which allows us to convert aspect ratios into widths
14
+ const minRowWidth = width * (1 / (1 + targetRowHeightTolerance));
15
+ const maxRowWidth = width * (1 / (1 - targetRowHeightTolerance));
16
+ aspectRatioList.forEach((aspectRatio, index) => {
17
+ let currentRowWidth = (lodash_1.default.sum(buffer) * targetRowHeight + aspectRatio * targetRowHeight + itemSpacing * (buffer.length));
18
+ // If the new item's width would cause it to fall between the tolerances for max widths, finish the row
19
+ // If the new item's width would cause it to exceed the tolerances for the row, add it to a new row
20
+ if (buffer.length === 0 || currentRowWidth < maxRowWidth) {
21
+ buffer.push(aspectRatio);
22
+ if (currentRowWidth > minRowWidth) {
23
+ rows.push(buffer);
24
+ buffer = [];
25
+ }
26
+ // If we're handling the last item, and it doesn't exceed the minimum tolerance for the row width, we add a "dummy" item to make the rest of the row now take up the whole width
27
+ else if (index === aspectRatioList.length - 1) {
28
+ buffer.push((width - lodash_1.default.sum(buffer) * targetRowHeight - itemSpacing * (buffer.length - 1)) / targetRowHeight);
29
+ }
30
+ }
31
+ else {
32
+ rows.push(buffer);
33
+ buffer = [aspectRatio];
34
+ }
35
+ });
36
+ if (buffer.length !== 0) {
37
+ rows.push(buffer);
38
+ }
39
+ return rows;
40
+ }
41
+ function JustifiedGrid(props) {
42
+ const { targetRowHeightTolerance = .25, width, targetRowHeight = 320, itemSpacing = 8, rowSpacing = 8, children, containerStyle, aspectRatioList } = props;
43
+ const rows = getJustifiedGridRowConfiguration(width, targetRowHeightTolerance, aspectRatioList, targetRowHeight, itemSpacing);
44
+ let childNodeCounter = -1;
45
+ return (react_1.default.createElement("div", { style: {
46
+ display: 'flex',
47
+ flexDirection: 'column',
48
+ gap: rowSpacing
49
+ } }, rows.map((value) => {
50
+ return react_1.default.createElement("div", { className: 'justified-row', style: {
51
+ display: "flex",
52
+ flexDirection: "row",
53
+ gap: itemSpacing,
54
+ } }, value.map((aspectRatio) => {
55
+ childNodeCounter++;
56
+ return react_1.default.createElement("div", { style: Object.assign({ flex: value.length === 1 ? 1 : aspectRatio }, containerStyle) }, children[childNodeCounter]);
57
+ }));
58
+ })));
59
+ }
@@ -1,8 +1,7 @@
1
1
  import React from "react";
2
2
  import './layout.css';
3
- type ElementDimensions = number;
4
3
  export interface TSJustifiedLayoutProps {
5
- layoutItems: ElementDimensions[];
4
+ aspectRatioList: number[];
6
5
  itemSpacing?: number;
7
6
  rowSpacing?: number;
8
7
  targetRowHeight?: number;
@@ -12,5 +11,8 @@ export interface TSJustifiedLayoutProps {
12
11
  showWidows?: boolean;
13
12
  containerStyle?: React.CSSProperties;
14
13
  }
15
- declare function TSJustifiedLayout({ children, layoutItems, itemSpacing, rowSpacing, showWidows, targetRowHeight, targetRowHeightTolerance, width, containerStyle }: TSJustifiedLayoutProps): React.JSX.Element;
16
- export { TSJustifiedLayout };
14
+ /**
15
+ * @deprecated Use the new {@link JustifiedGrid} component instead as it has better handling for heights of widow rows and future work will be done there
16
+ */
17
+ declare function TSJustifiedLayout({ children, aspectRatioList, itemSpacing, rowSpacing, showWidows, targetRowHeight, targetRowHeightTolerance, width, containerStyle }: Readonly<TSJustifiedLayoutProps>): React.JSX.Element;
18
+ export { TSJustifiedLayout, TSJustifiedLayout as JustifiedGrid };
@@ -3,10 +3,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.TSJustifiedLayout = void 0;
6
+ exports.TSJustifiedLayout = TSJustifiedLayout;
7
+ exports.JustifiedGrid = TSJustifiedLayout;
7
8
  const react_1 = __importDefault(require("react"));
8
9
  require("./layout.css");
9
- function TSJustifiedLayout({ children, layoutItems, itemSpacing = 10, rowSpacing = 10, showWidows = true, targetRowHeight = 320, targetRowHeightTolerance = .25, width, containerStyle }) {
10
+ /**
11
+ * @deprecated Use the new {@link JustifiedGrid} component instead as it has better handling for heights of widow rows and future work will be done there
12
+ */
13
+ function TSJustifiedLayout({ children, aspectRatioList, itemSpacing = 10, rowSpacing = 10, showWidows = true, targetRowHeight = 320, targetRowHeightTolerance = .25, width, containerStyle }) {
10
14
  const minAspectRatio = width / targetRowHeight * (1 - targetRowHeightTolerance);
11
15
  const maxAspectRatio = width / targetRowHeight * (1 + targetRowHeightTolerance);
12
16
  const rows = [];
@@ -58,7 +62,7 @@ function TSJustifiedLayout({ children, layoutItems, itemSpacing = 10, rowSpacing
58
62
  rowBuffer = [];
59
63
  }
60
64
  }
61
- layoutItems.forEach((value) => {
65
+ aspectRatioList.forEach((value) => {
62
66
  addItem(value);
63
67
  });
64
68
  // Handle leftover content
@@ -69,9 +73,11 @@ function TSJustifiedLayout({ children, layoutItems, itemSpacing = 10, rowSpacing
69
73
  /**
70
74
  * Clone the children element, and inject the height of the element as a prop
71
75
  */
72
- function renderRowItem(aspectRatio, isSoloRow) {
76
+ function renderRowItem(aspectRatio, isSoloRow, lastRowWithinTolerance, fakeElementAspectRatio) {
73
77
  childNodeCounter++;
74
- return react_1.default.createElement("div", { style: Object.assign({ aspectRatio: aspectRatio, flex: isSoloRow ? 1 : aspectRatio }, containerStyle) }, children[childNodeCounter]);
78
+ return react_1.default.createElement("div", { style: Object.assign({ flex: isSoloRow ? 1 : aspectRatio }, containerStyle) },
79
+ children[childNodeCounter],
80
+ lastRowWithinTolerance && react_1.default.createElement("div", { style: { flex: fakeElementAspectRatio } }));
75
81
  }
76
82
  // TODO Figure out how to eliminate the tiny gap between div and actual image height
77
83
  // TODO Make bottom row respect length restrictions
@@ -81,16 +87,13 @@ function TSJustifiedLayout({ children, layoutItems, itemSpacing = 10, rowSpacing
81
87
  gap: rowSpacing
82
88
  } }, rows.map((value, index, array) => {
83
89
  let isLastRow = index === array.length - 1 && showWidows;
84
- let rowTotalAspectRatio = value.items.reduce((previousValue, currentValue) => previousValue + currentValue, 0);
85
- const isLastRowWithinTolerance = isLastRow && rowTotalAspectRatio * value.height + (value.items.length - 1) * itemSpacing < minAspectRatio * value.height;
86
- const fakeElementAspectRatio = (width - rowTotalAspectRatio * value.height - (value.items.length - 1) * itemSpacing) / value.height;
90
+ let rowAspectRatioSum = value.items.reduce((previousValue, currentValue) => previousValue + currentValue, 0);
91
+ const isLastRowWithinTolerance = isLastRow && rowAspectRatioSum * value.height + (value.items.length - 1) * itemSpacing < minAspectRatio * value.height;
92
+ const fakeElementAspectRatio = (width - rowAspectRatioSum * value.height - (value.items.length - 1) * itemSpacing) / value.height;
87
93
  return react_1.default.createElement("div", { className: 'justified-row', style: {
88
94
  display: "flex",
89
95
  flexDirection: "row",
90
96
  gap: itemSpacing,
91
- } },
92
- value.items.map((aspectRatio) => renderRowItem(aspectRatio, value.items.length === 1)),
93
- isLastRowWithinTolerance && react_1.default.createElement("div", { style: { flex: fakeElementAspectRatio } }));
97
+ } }, value.items.map((aspectRatio) => renderRowItem(aspectRatio, value.items.length === 1, isLastRowWithinTolerance, fakeElementAspectRatio)));
94
98
  })));
95
99
  }
96
- exports.TSJustifiedLayout = TSJustifiedLayout;
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { TSJustifiedLayout } from "./components/TSJustifiedLayout";
2
+ export { JustifiedGridProps, JustifiedGrid } from "./components/JustifiedGrid";
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TSJustifiedLayout = void 0;
3
+ exports.JustifiedGrid = exports.TSJustifiedLayout = void 0;
4
4
  var TSJustifiedLayout_1 = require("./components/TSJustifiedLayout");
5
5
  Object.defineProperty(exports, "TSJustifiedLayout", { enumerable: true, get: function () { return TSJustifiedLayout_1.TSJustifiedLayout; } });
6
+ var JustifiedGrid_1 = require("./components/JustifiedGrid");
7
+ Object.defineProperty(exports, "JustifiedGrid", { enumerable: true, get: function () { return JustifiedGrid_1.JustifiedGrid; } });
@@ -1,3 +1,3 @@
1
1
  import { TSJustifiedLayoutProps } from "../components/TSJustifiedLayout";
2
2
  import React from "react";
3
- export declare const ConfiguredJustifiedLayout: ({ layoutItems, rowSpacing, width, itemSpacing, targetRowHeight, targetRowHeightTolerance, showWidows, children, ...props }: TSJustifiedLayoutProps) => React.JSX.Element;
3
+ export declare const ConfiguredJustifiedLayout: ({ aspectRatioList, rowSpacing, width, itemSpacing, targetRowHeight, targetRowHeightTolerance, children }: TSJustifiedLayoutProps) => React.JSX.Element;
@@ -1,24 +1,12 @@
1
1
  "use strict";
2
- var __rest = (this && this.__rest) || function (s, e) {
3
- var t = {};
4
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
- t[p] = s[p];
6
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
- t[p[i]] = s[p[i]];
10
- }
11
- return t;
12
- };
13
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
14
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
15
4
  };
16
5
  Object.defineProperty(exports, "__esModule", { value: true });
17
6
  exports.ConfiguredJustifiedLayout = void 0;
18
- const TSJustifiedLayout_1 = require("../components/TSJustifiedLayout");
19
7
  const react_1 = __importDefault(require("react"));
20
- const ConfiguredJustifiedLayout = (_a) => {
21
- var { layoutItems, rowSpacing = 8, width = 1000, itemSpacing = 8, targetRowHeight = 320, targetRowHeightTolerance = 0.10, showWidows = true, children } = _a, props = __rest(_a, ["layoutItems", "rowSpacing", "width", "itemSpacing", "targetRowHeight", "targetRowHeightTolerance", "showWidows", "children"]);
22
- return react_1.default.createElement(TSJustifiedLayout_1.TSJustifiedLayout, Object.assign({ layoutItems: layoutItems, width: width, itemSpacing: itemSpacing, targetRowHeight: targetRowHeight, targetRowHeightTolerance: targetRowHeightTolerance, rowSpacing: rowSpacing, showWidows: showWidows }, props), children.map((child) => child));
8
+ const JustifiedGrid_1 = require("../components/JustifiedGrid");
9
+ const ConfiguredJustifiedLayout = ({ aspectRatioList, rowSpacing = 8, width = 1000, itemSpacing = 8, targetRowHeight = 320, targetRowHeightTolerance = 0.10, children }) => {
10
+ return react_1.default.createElement(JustifiedGrid_1.JustifiedGrid, { aspectRatioList: aspectRatioList, width: width, itemSpacing: itemSpacing, targetRowHeight: targetRowHeight, targetRowHeightTolerance: targetRowHeightTolerance, rowSpacing: rowSpacing }, children.map((child) => child));
23
11
  };
24
12
  exports.ConfiguredJustifiedLayout = ConfiguredJustifiedLayout;
@@ -1,12 +1,14 @@
1
1
  import type { StoryObj } from "@storybook/react";
2
2
  import React from "react";
3
+ import 'react-loading-skeleton/dist/skeleton.css';
3
4
  declare const meta: {
4
5
  title: string;
5
- component: ({ layoutItems, rowSpacing, width, itemSpacing, targetRowHeight, targetRowHeightTolerance, showWidows, children, ...props }: import("../components/TSJustifiedLayout").TSJustifiedLayoutProps) => React.JSX.Element;
6
+ component: ({ aspectRatioList, rowSpacing, width, itemSpacing, targetRowHeight, targetRowHeightTolerance, children }: import("../components/TSJustifiedLayout").TSJustifiedLayoutProps) => React.JSX.Element;
6
7
  };
7
8
  export default meta;
8
9
  type Story = StoryObj<typeof meta>;
9
10
  export declare const Primary: Story;
10
11
  export declare const Secondary: Story;
12
+ export declare const Divs: Story;
11
13
  export declare const Single: Story;
12
14
  export declare const SingleWithDiv: Story;
@@ -3,9 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SingleWithDiv = exports.Single = exports.Secondary = exports.Primary = void 0;
6
+ exports.SingleWithDiv = exports.Single = exports.Divs = exports.Secondary = exports.Primary = void 0;
7
7
  const ConfiguredJustifiedLayout_1 = require("./ConfiguredJustifiedLayout");
8
8
  const react_1 = __importDefault(require("react"));
9
+ const react_loading_skeleton_1 = __importDefault(require("react-loading-skeleton"));
10
+ require("react-loading-skeleton/dist/skeleton.css");
9
11
  const meta = {
10
12
  title: 'JustifiedLayout/Basic',
11
13
  component: ConfiguredJustifiedLayout_1.ConfiguredJustifiedLayout,
@@ -44,70 +46,11 @@ const displayedImages = [
44
46
  ],
45
47
  "href": "https://x.com/RSN_07/status/1775664237119217719?s=20",
46
48
  "published": "2024-04-03",
47
- "aspectRatio": 0.6328963051251489,
49
+ "aspectRatio": 0.6328125,
48
50
  "thumbnailUrl": "https://alcorsiteartbucket.s3.amazonaws.com/600h/quick_draw_stance.webp",
49
51
  "webp": "https://alcorsiteartbucket.s3.amazonaws.com/webp/quick_draw_stance.webp",
50
52
  "src": "https://alcorsiteartbucket.s3.amazonaws.com/quick_draw_stance.PNG"
51
53
  },
52
- {
53
- "title": "Glimmer on the Shore",
54
- "artist": "@SiN_remyheart",
55
- "tags": [
56
- "Jupiter Form",
57
- "Featured"
58
- ],
59
- "href": "https://x.com/SiN_remyheart/status/1750043825161244678?s=20",
60
- "published": "2024-01-24",
61
- "aspectRatio": 1.15625,
62
- "thumbnailUrl": "https://alcorsiteartbucket.s3.amazonaws.com/600h/glimmer_on_the_shore.webp",
63
- "webp": "https://alcorsiteartbucket.s3.amazonaws.com/webp/glimmer_on_the_shore.png",
64
- "src": "https://alcorsiteartbucket.s3.amazonaws.com/glimmer_on_the_shore.png"
65
- },
66
- {
67
- "title": "Castor Evolved",
68
- "artist": "@TOOMIRO",
69
- "tags": [
70
- "Castor",
71
- "Featured"
72
- ],
73
- "href": "https://x.com/FaintAlcor/status/1749579348045111636?s=20",
74
- "published": "2024-01-22",
75
- "aspectRatio": 1.48,
76
- "thumbnailUrl": "https://alcorsiteartbucket.s3.amazonaws.com/600h/castor_evolved.webp",
77
- "webp": "https://alcorsiteartbucket.s3.amazonaws.com/webp/castor_evolved.png",
78
- "src": "https://alcorsiteartbucket.s3.amazonaws.com/castor_evolved.png"
79
- },
80
- {
81
- "title": "Happy 24th Birthday!",
82
- "artist": "@DrawingDDoom",
83
- "tags": [
84
- "Aldhibah Form",
85
- "Techwear",
86
- "Castor",
87
- "Featured"
88
- ],
89
- "href": "https://x.com/DrawingDDoom/status/1715601315492245545?s=20",
90
- "published": "2023-10-21",
91
- "thumbnailUrl": "https://alcorsiteartbucket.s3.amazonaws.com/600h/happy_24_th_birthday.webp",
92
- "src": "https://alcorsiteartbucket.s3.amazonaws.com/happy_24_th_birthday.png",
93
- "aspectRatio": 0.6097560975609756,
94
- "webp": "https://alcorsiteartbucket.s3.amazonaws.com/webp/happy_24_th_birthday.webp"
95
- },
96
- {
97
- "src": "https://alcorsiteartbucket.s3.amazonaws.com/combat_ready_duo.png",
98
- "title": "Combat Ready Duo",
99
- "artist": "@illusummer",
100
- "tags": [
101
- "Rastaban Form",
102
- "Techwear",
103
- "Featured"
104
- ],
105
- "href": "https://skeb.jp/@arcielsummer/works/33",
106
- "published": "2023-05-05",
107
- "thumbnailUrl": "https://alcorsiteartbucket.s3.amazonaws.com/600h/combat_ready_duo.webp",
108
- "aspectRatio": 1.7777777777777777,
109
- "webp": "https://alcorsiteartbucket.s3.amazonaws.com/webp/combat_ready_duo.webp"
110
- },
111
54
  {
112
55
  "title": "Evening Train",
113
56
  "artist": "@greyy_arts",
@@ -168,20 +111,6 @@ const displayedImages = [
168
111
  "published": "2022-05-04",
169
112
  "aspectRatio": 0.85107421875
170
113
  },
171
- {
172
- "title": "The Beginning",
173
- "href": "https://twitter.com/Mike202112/status/1406146656660197379?s=20",
174
- "src": "https://pbs.twimg.com/media/E4Oi2GKVcAIQKzo?format=png&name=900x900",
175
- "artist": "@Mike202112",
176
- "tags": [
177
- "Rastaban Form",
178
- "Knives",
179
- "Standard Outfit",
180
- "Featured"
181
- ],
182
- "published": "2021-06-19",
183
- "aspectRatio": 0.7084745762711865
184
- },
185
114
  {
186
115
  "src": "https://pbs.twimg.com/media/FnHp4nWaUAA-lgD?format=jpg&name=large",
187
116
  "title": "Special Archery Training!",
@@ -208,11 +137,14 @@ exports.Primary = {
208
137
  targetRowHeight: undefined,
209
138
  rowSpacing: undefined,
210
139
  itemSpacing: undefined,
211
- layoutItems: displayedImages.map(value => value.aspectRatio),
140
+ aspectRatioList: displayedImages.map(value => value.aspectRatio),
212
141
  targetRowHeightTolerance: undefined,
213
- children: displayedImages.map(value => react_1.default.createElement(react_1.default.Fragment, null,
214
- react_1.default.createElement("div", { style: { top: 16, left: 16, position: "absolute" } }, "Testing"),
215
- react_1.default.createElement("img", { src: value.webp || value.src }))),
142
+ children: displayedImages.map(value => {
143
+ var _a;
144
+ return react_1.default.createElement(react_1.default.Fragment, null,
145
+ react_1.default.createElement("div", { style: { top: 16, left: 16, position: "absolute" } }, "Testing"),
146
+ react_1.default.createElement("img", { src: (_a = value.webp) !== null && _a !== void 0 ? _a : value.src }));
147
+ }),
216
148
  containerStyle: { position: 'relative' }
217
149
  },
218
150
  };
@@ -224,9 +156,22 @@ exports.Secondary = {
224
156
  targetRowHeight: undefined,
225
157
  rowSpacing: undefined,
226
158
  itemSpacing: undefined,
227
- layoutItems: displayedImages.slice(1, 5).map(value => value.aspectRatio),
159
+ aspectRatioList: displayedImages.slice(1, 5).map(value => value.aspectRatio),
160
+ targetRowHeightTolerance: undefined,
161
+ children: displayedImages.slice(1, 5).map(value => { var _a; return react_1.default.createElement("img", { src: (_a = value.webp) !== null && _a !== void 0 ? _a : value.src }); })
162
+ },
163
+ };
164
+ exports.Divs = {
165
+ name: "Divs Only",
166
+ args: {
167
+ width: 847,
168
+ showWidows: true,
169
+ targetRowHeight: undefined,
170
+ rowSpacing: undefined,
171
+ itemSpacing: undefined,
172
+ aspectRatioList: displayedImages.slice(1, 5).map(value => value.aspectRatio),
228
173
  targetRowHeightTolerance: undefined,
229
- children: displayedImages.slice(1, 5).map(value => react_1.default.createElement("img", { src: value.webp }))
174
+ children: displayedImages.slice(1, 5).map(value => react_1.default.createElement(react_loading_skeleton_1.default, { style: { aspectRatio: value.aspectRatio } }))
230
175
  },
231
176
  };
232
177
  exports.Single = {
@@ -239,7 +184,7 @@ exports.Single = {
239
184
  rowSpacing: undefined,
240
185
  itemSpacing: undefined,
241
186
  targetRowHeightTolerance: undefined,
242
- layoutItems: [1],
187
+ aspectRatioList: [1],
243
188
  }
244
189
  };
245
190
  exports.SingleWithDiv = {
@@ -253,6 +198,6 @@ exports.SingleWithDiv = {
253
198
  rowSpacing: undefined,
254
199
  itemSpacing: undefined,
255
200
  targetRowHeightTolerance: undefined,
256
- layoutItems: [1],
201
+ aspectRatioList: [1],
257
202
  }
258
203
  };
package/package.json CHANGED
@@ -1,46 +1,49 @@
1
- {
2
- "name": "react-justified-layout-ts",
3
- "version": "2.0.9",
4
- "description": "A component based off Flickr's justified layout that is compatible with Typescript",
5
- "main": "./dist/index.js",
6
- "types": "./dist/index.d.ts",
7
- "scripts": {
8
- "test": "echo \"Error: no test specified\" && exit 1",
9
- "typescript": "tsc && copyfiles -u 1 src/**/*.css dist/",
10
- "storybook": "storybook dev -p 6006",
11
- "build-storybook": "storybook build"
12
- },
13
- "keywords": [],
14
- "author": "Alan19",
15
- "license": "MIT",
16
- "dependencies": {
17
- "@types/node": "^20.10.7",
18
- "react-use": "^17.5.0",
19
- "react-use-measure": "^2.1.1"
20
- },
21
- "devDependencies": {
22
- "@chromatic-com/storybook": "^1.5.0",
23
- "@storybook/addon-essentials": "^8.1.5",
24
- "@storybook/addon-interactions": "^8.1.5",
25
- "@storybook/addon-links": "^8.1.5",
26
- "@storybook/addon-onboarding": "^8.1.5",
27
- "@storybook/addon-webpack5-compiler-swc": "^1.0.3",
28
- "@storybook/blocks": "^8.1.5",
29
- "@storybook/react": "^8.1.5",
30
- "@storybook/react-webpack5": "^8.1.5",
31
- "@storybook/test": "^8.1.5",
32
- "@types/react": "^18.2.47",
33
- "copyfiles": "^2.4.1",
34
- "react": "^18.2.0",
35
- "rimraf": "^5.0.7",
36
- "storybook": "^8.1.5",
37
- "typescript": "^5.3.3"
38
- },
39
- "peerDependencies": {
40
- "react": "^18.2.0"
41
- },
42
- "repository": {
43
- "type": "git",
44
- "url": "git+https://github.com/Alan19/react-justified-layout-ts.git"
45
- }
46
- }
1
+ {
2
+ "name": "react-justified-layout-ts",
3
+ "version": "2.1.4",
4
+ "description": "A component based off Flickr's justified layout that is compatible with Typescript",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1",
9
+ "typescript": "tsc && copyfiles -u 1 src/**/*.css dist/",
10
+ "storybook": "storybook dev -p 6006",
11
+ "build-storybook": "storybook build"
12
+ },
13
+ "keywords": [],
14
+ "author": "Alan19",
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@types/node": "^24.0.3",
18
+ "lodash": "^4.17.21",
19
+ "react-use": "^17.6.0",
20
+ "react-use-measure": "^2.1.7"
21
+ },
22
+ "devDependencies": {
23
+ "@chromatic-com/storybook": "^3.2.7",
24
+ "@storybook/addon-essentials": "^8.6.14",
25
+ "@storybook/addon-interactions": "^8.6.14",
26
+ "@storybook/addon-links": "^8.6.14",
27
+ "@storybook/addon-onboarding": "^8.6.14",
28
+ "@storybook/addon-webpack5-compiler-swc": "^3.0.0",
29
+ "@storybook/blocks": "^8.6.14",
30
+ "@storybook/react": "^8.6.14",
31
+ "@storybook/react-webpack5": "^8.6.14",
32
+ "@storybook/test": "^8.6.14",
33
+ "@types/lodash": "^4.17.18",
34
+ "@types/react": "^19.1.8",
35
+ "copyfiles": "^2.4.1",
36
+ "react": "^19.1.0",
37
+ "react-loading-skeleton": "^3.5.0",
38
+ "rimraf": "^6.0.1",
39
+ "storybook": "^8.6.14",
40
+ "typescript": "^5.8.3"
41
+ },
42
+ "peerDependencies": {
43
+ "react": "^18.2.0"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/Alan19/react-justified-layout-ts.git"
48
+ }
49
+ }
@@ -0,0 +1,80 @@
1
+ import React from "react";
2
+ import _ from "lodash";
3
+ import './layout.css'
4
+
5
+ export interface JustifiedGridProps {
6
+ aspectRatioList: number[];
7
+ itemSpacing?: number;
8
+ rowSpacing?: number;
9
+ targetRowHeight?: number;
10
+ targetRowHeightTolerance?: number;
11
+ width: number;
12
+ children: React.JSX.Element[];
13
+ containerStyle?: React.CSSProperties;
14
+ }
15
+
16
+ function getJustifiedGridRowConfiguration(width: number, targetRowHeightTolerance: number, aspectRatioList: number[], targetRowHeight: number, itemSpacing: number): number[][] {
17
+ let buffer: number[] = []
18
+ const rows: number[][] = []
19
+
20
+ // We assume that the row will have the specified height, which allows us to convert aspect ratios into widths
21
+ const minRowWidth = width * (1 / (1 + targetRowHeightTolerance));
22
+ const maxRowWidth = width * (1 / (1 - targetRowHeightTolerance));
23
+ aspectRatioList.forEach((aspectRatio, index) => {
24
+ let currentRowWidth = (_.sum(buffer) * targetRowHeight + aspectRatio * targetRowHeight + itemSpacing * (buffer.length));
25
+ // If the new item's width would cause it to fall between the tolerances for max widths, finish the row
26
+ // If the new item's width would cause it to exceed the tolerances for the row, add it to a new row
27
+ if (buffer.length === 0 || currentRowWidth < maxRowWidth) {
28
+ buffer.push(aspectRatio);
29
+ if (currentRowWidth > minRowWidth) {
30
+ rows.push(buffer)
31
+ buffer = []
32
+ }
33
+ // If we're handling the last item, and it doesn't exceed the minimum tolerance for the row width, we add a "dummy" item to make the rest of the row now take up the whole width
34
+ else if (index === aspectRatioList.length - 1) {
35
+ buffer.push((width - _.sum(buffer) * targetRowHeight - itemSpacing * (buffer.length - 1)) / targetRowHeight);
36
+ }
37
+ }
38
+ else {
39
+ rows.push(buffer)
40
+ buffer = [aspectRatio];
41
+ }
42
+ })
43
+
44
+ if (buffer.length !== 0) {
45
+ rows.push(buffer)
46
+ }
47
+ return rows;
48
+ }
49
+
50
+ export function JustifiedGrid(props: Readonly<JustifiedGridProps>) {
51
+ const {targetRowHeightTolerance = .25, width, targetRowHeight = 320, itemSpacing = 8, rowSpacing = 8, children, containerStyle, aspectRatioList} = props;
52
+ const rows = getJustifiedGridRowConfiguration(width, targetRowHeightTolerance, aspectRatioList, targetRowHeight, itemSpacing);
53
+
54
+ let childNodeCounter = -1;
55
+ return (
56
+ <div style={{
57
+ display: 'flex',
58
+ flexDirection: 'column',
59
+ gap: rowSpacing
60
+ }}>
61
+ {rows.map((value) => {
62
+ return <div className={'justified-row'} style={{
63
+ display: "flex",
64
+ flexDirection: "row",
65
+ gap: itemSpacing,
66
+ }}>
67
+ {value.map((aspectRatio) => {
68
+ childNodeCounter++;
69
+ return <div style={{
70
+ flex: value.length === 1 ? 1 : aspectRatio,
71
+ ...containerStyle
72
+ }}>
73
+ {children[childNodeCounter]}
74
+ </div>;
75
+ })}
76
+ </div>
77
+ })}
78
+ </div>
79
+ );
80
+ }
@@ -4,7 +4,7 @@ import './layout.css'
4
4
  type ElementDimensions = number;
5
5
 
6
6
  export interface TSJustifiedLayoutProps {
7
- layoutItems: ElementDimensions[];
7
+ aspectRatioList: number[];
8
8
  itemSpacing?: number;
9
9
  rowSpacing?: number;
10
10
  targetRowHeight?: number;
@@ -19,9 +19,12 @@ export interface TSJustifiedLayoutProps {
19
19
  // widowLayoutStyle: "left" | "justify" | "center"
20
20
  }
21
21
 
22
+ /**
23
+ * @deprecated Use the new {@link JustifiedGrid} component instead as it has better handling for heights of widow rows and future work will be done there
24
+ */
22
25
  function TSJustifiedLayout({
23
26
  children,
24
- layoutItems,
27
+ aspectRatioList,
25
28
  itemSpacing = 10,
26
29
  rowSpacing = 10,
27
30
  showWidows = true,
@@ -29,7 +32,7 @@ function TSJustifiedLayout({
29
32
  targetRowHeightTolerance = .25,
30
33
  width,
31
34
  containerStyle
32
- }: TSJustifiedLayoutProps) {
35
+ }: Readonly<TSJustifiedLayoutProps>) {
33
36
  const minAspectRatio = width / targetRowHeight * (1 - targetRowHeightTolerance);
34
37
  const maxAspectRatio = width / targetRowHeight * (1 + targetRowHeightTolerance);
35
38
 
@@ -83,7 +86,7 @@ function TSJustifiedLayout({
83
86
  }
84
87
 
85
88
 
86
- layoutItems.forEach((value) => {
89
+ aspectRatioList.forEach((value) => {
87
90
  addItem(value);
88
91
  })
89
92
 
@@ -97,14 +100,15 @@ function TSJustifiedLayout({
97
100
  /**
98
101
  * Clone the children element, and inject the height of the element as a prop
99
102
  */
100
- function renderRowItem(aspectRatio: ElementDimensions, isSoloRow: boolean) {
103
+ function renderRowItem(aspectRatio: ElementDimensions, isSoloRow: boolean, lastRowWithinTolerance: undefined | boolean, fakeElementAspectRatio: number) {
101
104
  childNodeCounter++;
102
105
  return <div style={{
103
- aspectRatio: aspectRatio,
104
106
  flex: isSoloRow ? 1 : aspectRatio,
105
107
  ...containerStyle
106
108
  }}>
107
109
  {children[childNodeCounter]}
110
+ {/*Extra element for widowed rows to stop them from getting scaled up*/}
111
+ {lastRowWithinTolerance && <div style={{flex: fakeElementAspectRatio}}/>}
108
112
  </div>
109
113
  }
110
114
 
@@ -118,21 +122,19 @@ function TSJustifiedLayout({
118
122
  }}>
119
123
  {rows.map((value, index, array) => {
120
124
  let isLastRow = index === array.length - 1 && showWidows;
121
- let rowTotalAspectRatio = value.items.reduce((previousValue, currentValue) => previousValue + currentValue, 0);
122
- const isLastRowWithinTolerance = isLastRow && rowTotalAspectRatio * value.height + (value.items.length - 1) * itemSpacing < minAspectRatio * value.height;
123
- const fakeElementAspectRatio = (width - rowTotalAspectRatio * value.height - (value.items.length - 1) * itemSpacing) / value.height
125
+ let rowAspectRatioSum = value.items.reduce((previousValue, currentValue) => previousValue + currentValue, 0);
126
+ const isLastRowWithinTolerance = isLastRow && rowAspectRatioSum * value.height + (value.items.length - 1) * itemSpacing < minAspectRatio * value.height;
127
+ const fakeElementAspectRatio = (width - rowAspectRatioSum * value.height - (value.items.length - 1) * itemSpacing) / value.height;
124
128
  return <div className={'justified-row'} style={{
125
129
  display: "flex",
126
130
  flexDirection: "row",
127
131
  gap: itemSpacing,
128
- }
129
- }>
130
- {value.items.map((aspectRatio) => renderRowItem(aspectRatio, value.items.length === 1))}
131
- {isLastRowWithinTolerance && <div style={{flex: fakeElementAspectRatio}}></div>}
132
+ }}>
133
+ {value.items.map((aspectRatio) => renderRowItem(aspectRatio, value.items.length === 1, isLastRowWithinTolerance, fakeElementAspectRatio))}
132
134
  </div>
133
135
  })}
134
136
  </div>
135
137
  );
136
138
  }
137
139
 
138
- export {TSJustifiedLayout}
140
+ export {TSJustifiedLayout, TSJustifiedLayout as JustifiedGrid}
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
- export {TSJustifiedLayout} from "./components/TSJustifiedLayout"
1
+ export {TSJustifiedLayout} from "./components/TSJustifiedLayout"
2
+ export {JustifiedGridProps, JustifiedGrid} from "./components/JustifiedGrid"
@@ -1,25 +1,23 @@
1
- import {TSJustifiedLayout, TSJustifiedLayoutProps} from "../components/TSJustifiedLayout";
1
+ import {TSJustifiedLayoutProps} from "../components/TSJustifiedLayout";
2
2
  import React from "react";
3
+ import {JustifiedGrid} from "../components/JustifiedGrid";
3
4
 
4
5
 
5
6
  export const ConfiguredJustifiedLayout = ({
6
- layoutItems,
7
+ aspectRatioList,
7
8
  rowSpacing = 8,
8
9
  width = 1000,
9
10
  itemSpacing = 8,
10
11
  targetRowHeight = 320,
11
12
  targetRowHeightTolerance = 0.10,
12
- showWidows = true,
13
- children,
14
- ...props
13
+ children
15
14
  }: TSJustifiedLayoutProps) => {
16
- return <TSJustifiedLayout layoutItems={layoutItems}
17
- width={width}
18
- itemSpacing={itemSpacing}
19
- targetRowHeight={targetRowHeight}
20
- targetRowHeightTolerance={targetRowHeightTolerance}
21
- rowSpacing={rowSpacing}
22
- showWidows={showWidows} {...props}>
15
+ return <JustifiedGrid aspectRatioList={aspectRatioList}
16
+ width={width}
17
+ itemSpacing={itemSpacing}
18
+ targetRowHeight={targetRowHeight}
19
+ targetRowHeightTolerance={targetRowHeightTolerance}
20
+ rowSpacing={rowSpacing}>
23
21
  {children.map((child) => child)}
24
- </TSJustifiedLayout>;
22
+ </JustifiedGrid>;
25
23
  }
@@ -1,6 +1,8 @@
1
1
  import type {Meta, StoryObj} from "@storybook/react";
2
2
  import {ConfiguredJustifiedLayout} from "./ConfiguredJustifiedLayout";
3
3
  import React from "react";
4
+ import Skeleton from "react-loading-skeleton";
5
+ import 'react-loading-skeleton/dist/skeleton.css'
4
6
 
5
7
  const meta = {
6
8
  title: 'JustifiedLayout/Basic',
@@ -43,70 +45,11 @@ const displayedImages = [
43
45
  ],
44
46
  "href": "https://x.com/RSN_07/status/1775664237119217719?s=20",
45
47
  "published": "2024-04-03",
46
- "aspectRatio": 0.6328963051251489,
48
+ "aspectRatio": 0.6328125,
47
49
  "thumbnailUrl": "https://alcorsiteartbucket.s3.amazonaws.com/600h/quick_draw_stance.webp",
48
50
  "webp": "https://alcorsiteartbucket.s3.amazonaws.com/webp/quick_draw_stance.webp",
49
51
  "src": "https://alcorsiteartbucket.s3.amazonaws.com/quick_draw_stance.PNG"
50
52
  },
51
- {
52
- "title": "Glimmer on the Shore",
53
- "artist": "@SiN_remyheart",
54
- "tags": [
55
- "Jupiter Form",
56
- "Featured"
57
- ],
58
- "href": "https://x.com/SiN_remyheart/status/1750043825161244678?s=20",
59
- "published": "2024-01-24",
60
- "aspectRatio": 1.15625,
61
- "thumbnailUrl": "https://alcorsiteartbucket.s3.amazonaws.com/600h/glimmer_on_the_shore.webp",
62
- "webp": "https://alcorsiteartbucket.s3.amazonaws.com/webp/glimmer_on_the_shore.png",
63
- "src": "https://alcorsiteartbucket.s3.amazonaws.com/glimmer_on_the_shore.png"
64
- },
65
- {
66
- "title": "Castor Evolved",
67
- "artist": "@TOOMIRO",
68
- "tags": [
69
- "Castor",
70
- "Featured"
71
- ],
72
- "href": "https://x.com/FaintAlcor/status/1749579348045111636?s=20",
73
- "published": "2024-01-22",
74
- "aspectRatio": 1.48,
75
- "thumbnailUrl": "https://alcorsiteartbucket.s3.amazonaws.com/600h/castor_evolved.webp",
76
- "webp": "https://alcorsiteartbucket.s3.amazonaws.com/webp/castor_evolved.png",
77
- "src": "https://alcorsiteartbucket.s3.amazonaws.com/castor_evolved.png"
78
- },
79
- {
80
- "title": "Happy 24th Birthday!",
81
- "artist": "@DrawingDDoom",
82
- "tags": [
83
- "Aldhibah Form",
84
- "Techwear",
85
- "Castor",
86
- "Featured"
87
- ],
88
- "href": "https://x.com/DrawingDDoom/status/1715601315492245545?s=20",
89
- "published": "2023-10-21",
90
- "thumbnailUrl": "https://alcorsiteartbucket.s3.amazonaws.com/600h/happy_24_th_birthday.webp",
91
- "src": "https://alcorsiteartbucket.s3.amazonaws.com/happy_24_th_birthday.png",
92
- "aspectRatio": 0.6097560975609756,
93
- "webp": "https://alcorsiteartbucket.s3.amazonaws.com/webp/happy_24_th_birthday.webp"
94
- },
95
- {
96
- "src": "https://alcorsiteartbucket.s3.amazonaws.com/combat_ready_duo.png",
97
- "title": "Combat Ready Duo",
98
- "artist": "@illusummer",
99
- "tags": [
100
- "Rastaban Form",
101
- "Techwear",
102
- "Featured"
103
- ],
104
- "href": "https://skeb.jp/@arcielsummer/works/33",
105
- "published": "2023-05-05",
106
- "thumbnailUrl": "https://alcorsiteartbucket.s3.amazonaws.com/600h/combat_ready_duo.webp",
107
- "aspectRatio": 1.7777777777777777,
108
- "webp": "https://alcorsiteartbucket.s3.amazonaws.com/webp/combat_ready_duo.webp"
109
- },
110
53
  {
111
54
  "title": "Evening Train",
112
55
  "artist": "@greyy_arts",
@@ -167,20 +110,6 @@ const displayedImages = [
167
110
  "published": "2022-05-04",
168
111
  "aspectRatio": 0.85107421875
169
112
  },
170
- {
171
- "title": "The Beginning",
172
- "href": "https://twitter.com/Mike202112/status/1406146656660197379?s=20",
173
- "src": "https://pbs.twimg.com/media/E4Oi2GKVcAIQKzo?format=png&name=900x900",
174
- "artist": "@Mike202112",
175
- "tags": [
176
- "Rastaban Form",
177
- "Knives",
178
- "Standard Outfit",
179
- "Featured"
180
- ],
181
- "published": "2021-06-19",
182
- "aspectRatio": 0.7084745762711865
183
- },
184
113
  {
185
114
  "src": "https://pbs.twimg.com/media/FnHp4nWaUAA-lgD?format=jpg&name=large",
186
115
  "title": "Special Archery Training!",
@@ -208,17 +137,18 @@ export const Primary: Story = {
208
137
  targetRowHeight: undefined,
209
138
  rowSpacing: undefined,
210
139
  itemSpacing: undefined,
211
- layoutItems: displayedImages.map(value => value.aspectRatio),
140
+ aspectRatioList: displayedImages.map(value => value.aspectRatio),
212
141
  targetRowHeightTolerance: undefined,
213
142
  children: displayedImages.map(value => <>
214
143
  <div style={{top: 16, left: 16, position: "absolute"}}>Testing</div>
215
- <img src={value.webp || value.src}/>
144
+ <img src={value.webp ?? value.src}/>
216
145
  </>
217
146
  ),
218
147
  containerStyle: {position: 'relative'}
219
148
  },
220
149
  };
221
150
 
151
+
222
152
  export const Secondary: Story = {
223
153
  name: "Image Tag Elements",
224
154
  args: {
@@ -227,9 +157,23 @@ export const Secondary: Story = {
227
157
  targetRowHeight: undefined,
228
158
  rowSpacing: undefined,
229
159
  itemSpacing: undefined,
230
- layoutItems: displayedImages.slice(1, 5).map(value => value.aspectRatio),
160
+ aspectRatioList: displayedImages.slice(1, 5).map(value => value.aspectRatio),
161
+ targetRowHeightTolerance: undefined,
162
+ children: displayedImages.slice(1, 5).map(value => <img src={value.webp ?? value.src}/>)
163
+ },
164
+ }
165
+
166
+ export const Divs: Story = {
167
+ name: "Divs Only",
168
+ args: {
169
+ width: 847,
170
+ showWidows: true,
171
+ targetRowHeight: undefined,
172
+ rowSpacing: undefined,
173
+ itemSpacing: undefined,
174
+ aspectRatioList: displayedImages.slice(1, 5).map(value => value.aspectRatio),
231
175
  targetRowHeightTolerance: undefined,
232
- children: displayedImages.slice(1, 5).map(value => <img src={value.webp}/>)
176
+ children: displayedImages.slice(1, 5).map(value => <Skeleton style={{aspectRatio: value.aspectRatio}}/>)
233
177
  },
234
178
  }
235
179
 
@@ -243,7 +187,7 @@ export const Single: Story = {
243
187
  rowSpacing: undefined,
244
188
  itemSpacing: undefined,
245
189
  targetRowHeightTolerance: undefined,
246
- layoutItems: [1],
190
+ aspectRatioList: [1],
247
191
  }
248
192
  }
249
193
 
@@ -257,6 +201,6 @@ export const SingleWithDiv: Story = {
257
201
  rowSpacing: undefined,
258
202
  itemSpacing: undefined,
259
203
  targetRowHeightTolerance: undefined,
260
- layoutItems: [1],
204
+ aspectRatioList: [1],
261
205
  }
262
206
  }
package/.idea/discord.xml DELETED
@@ -1,7 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="DiscordProjectSettings">
4
- <option name="show" value="PROJECT_FILES" />
5
- <option name="description" value="" />
6
- </component>
7
- </project>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="JavaScriptLibraryMappings">
4
- <includedPredefinedLibrary name="Node.js Core" />
5
- </component>
6
- </project>
package/.idea/modules.xml DELETED
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/.idea/react-justified-layout-ts.iml" filepath="$PROJECT_DIR$/.idea/react-justified-layout-ts.iml" />
6
- </modules>
7
- </component>
8
- </project>
@@ -1,13 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <module type="WEB_MODULE" version="4">
3
- <component name="NewModuleRootManager">
4
- <content url="file://$MODULE_DIR$">
5
- <excludeFolder url="file://$MODULE_DIR$/.tmp" />
6
- <excludeFolder url="file://$MODULE_DIR$/temp" />
7
- <excludeFolder url="file://$MODULE_DIR$/tmp" />
8
- <excludeFolder url="file://$MODULE_DIR$/dist" />
9
- </content>
10
- <orderEntry type="inheritedJdk" />
11
- <orderEntry type="sourceFolder" forTests="false" />
12
- </component>
13
- </module>
package/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
- </component>
6
- </project>