@sproutsocial/seeds-react-image 1.0.28 → 1.0.29
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/.turbo/turbo-build.log +67 -11
- package/CHANGELOG.md +10 -0
- package/dist/esm/index.js +89 -23
- package/dist/esm/index.js.map +1 -1
- package/dist/image.css +18 -0
- package/dist/index.d.mts +10 -20
- package/dist/index.d.ts +10 -20
- package/dist/index.js +90 -24
- package/dist/index.js.map +1 -1
- package/package.json +11 -3
- package/postcss.config.js +6 -0
- package/src/Image.stories.tsx +15 -0
- package/src/Image.tsx +19 -87
- package/src/ImageHybrid.tsx +126 -0
- package/src/ImageTailwind.tsx +53 -0
- package/src/__tests__/features.test.tsx +33 -0
- package/src/css.d.ts +1 -0
- package/src/image.css +18 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
yarn run v1.22.22
|
|
2
|
-
$ tsup --dts
|
|
2
|
+
$ tsup --dts && cp src/image.css dist/image.css
|
|
3
|
+
▲ [WARNING] The condition "types" here will never be used as it comes after both "import" and "require" [package.json]
|
|
4
|
+
|
|
5
|
+
package.json:14:6:
|
|
6
|
+
14 │ "types": "./dist/index.d.ts"
|
|
7
|
+
╵ ~~~~~~~
|
|
8
|
+
|
|
9
|
+
The "import" condition comes earlier and will be used for all "import" statements:
|
|
10
|
+
|
|
11
|
+
package.json:12:6:
|
|
12
|
+
12 │ "import": "./dist/esm/index.js",
|
|
13
|
+
╵ ~~~~~~~~
|
|
14
|
+
|
|
15
|
+
The "require" condition comes earlier and will be used for all "require" calls:
|
|
16
|
+
|
|
17
|
+
package.json:13:6:
|
|
18
|
+
13 │ "require": "./dist/index.js",
|
|
19
|
+
╵ ~~~~~~~~~
|
|
20
|
+
|
|
3
21
|
[34mCLI[39m Building entry: src/index.ts
|
|
4
22
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
5
23
|
[34mCLI[39m tsup v8.5.0
|
|
@@ -8,14 +26,52 @@ $ tsup --dts
|
|
|
8
26
|
[34mCLI[39m Cleaning output folder
|
|
9
27
|
[34mCJS[39m Build start
|
|
10
28
|
[34mESM[39m Build start
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
[
|
|
15
|
-
[
|
|
16
|
-
|
|
29
|
+
[warn] [33m▲ [43;33m[[43;30mWARNING[43;33m][0m [1mThe condition "types" here will never be used as it comes after both "import" and "require"[0m [package.json]
|
|
30
|
+
|
|
31
|
+
package.json:14:6:
|
|
32
|
+
[37m 14 │ [32m"types"[37m: "./dist/index.d.ts"
|
|
33
|
+
╵ [32m~~~~~~~[0m
|
|
34
|
+
|
|
35
|
+
The "import" condition comes earlier and will be used for all "import" statements:
|
|
36
|
+
|
|
37
|
+
package.json:12:6:
|
|
38
|
+
[37m 12 │ [32m"import"[37m: "./dist/esm/index.js",
|
|
39
|
+
╵ [32m~~~~~~~~[0m
|
|
40
|
+
|
|
41
|
+
The "require" condition comes earlier and will be used for all "require" calls:
|
|
42
|
+
|
|
43
|
+
package.json:13:6:
|
|
44
|
+
[37m 13 │ [32m"require"[37m: "./dist/index.js",
|
|
45
|
+
╵ [32m~~~~~~~~~[0m
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
[warn] [33m▲ [43;33m[[43;30mWARNING[43;33m][0m [1mThe condition "types" here will never be used as it comes after both "import" and "require"[0m [package.json]
|
|
49
|
+
|
|
50
|
+
package.json:14:6:
|
|
51
|
+
[37m 14 │ [32m"types"[37m: "./dist/index.d.ts"
|
|
52
|
+
╵ [32m~~~~~~~[0m
|
|
53
|
+
|
|
54
|
+
The "import" condition comes earlier and will be used for all "import" statements:
|
|
55
|
+
|
|
56
|
+
package.json:12:6:
|
|
57
|
+
[37m 12 │ [32m"import"[37m: "./dist/esm/index.js",
|
|
58
|
+
╵ [32m~~~~~~~~[0m
|
|
59
|
+
|
|
60
|
+
The "require" condition comes earlier and will be used for all "require" calls:
|
|
61
|
+
|
|
62
|
+
package.json:13:6:
|
|
63
|
+
[37m 13 │ [32m"require"[37m: "./dist/index.js",
|
|
64
|
+
╵ [32m~~~~~~~~~[0m
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
[32mCJS[39m [1mdist/index.js [22m[32m5.74 KB[39m
|
|
68
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m9.86 KB[39m
|
|
69
|
+
[32mCJS[39m ⚡️ Build success in 90ms
|
|
70
|
+
[32mESM[39m [1mdist/esm/index.js [22m[32m3.77 KB[39m
|
|
71
|
+
[32mESM[39m [1mdist/esm/index.js.map [22m[32m9.73 KB[39m
|
|
72
|
+
[32mESM[39m ⚡️ Build success in 91ms
|
|
17
73
|
[34mDTS[39m Build start
|
|
18
|
-
[32mDTS[39m ⚡️ Build success in
|
|
19
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m1.
|
|
20
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[32m1.
|
|
21
|
-
Done in
|
|
74
|
+
[32mDTS[39m ⚡️ Build success in 3441ms
|
|
75
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m1.31 KB[39m
|
|
76
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m1.31 KB[39m
|
|
77
|
+
Done in 4.82s.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @sproutsocial/seeds-react-image
|
|
2
2
|
|
|
3
|
+
## 1.0.29
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c36361c: Migrate seeds-react-image to the hybrid Tailwind pattern. Adds ImageTailwind
|
|
8
|
+
(pure CSS-class img leaf) and ImageHybrid (routes to the styled-components path
|
|
9
|
+
when styled-system props are present, Tailwind otherwise); Image now re-exports
|
|
10
|
+
the hybrid. Component CSS ships via the racine components.css aggregate. No
|
|
11
|
+
public API change — fully backward compatible.
|
|
12
|
+
|
|
3
13
|
## 1.0.28
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/dist/esm/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// src/Image.tsx
|
|
2
|
-
import
|
|
2
|
+
import "react";
|
|
3
|
+
|
|
4
|
+
// src/ImageHybrid.tsx
|
|
5
|
+
import * as React2 from "react";
|
|
3
6
|
|
|
4
7
|
// src/styles.ts
|
|
5
8
|
import styled, { css } from "styled-components";
|
|
@@ -18,11 +21,46 @@ var ImageContainer = styled.img.attrs({
|
|
|
18
21
|
`;
|
|
19
22
|
var styles_default = ImageContainer;
|
|
20
23
|
|
|
21
|
-
// src/
|
|
24
|
+
// src/ImageTailwind.tsx
|
|
25
|
+
import React from "react";
|
|
22
26
|
import { jsx } from "react/jsx-runtime";
|
|
27
|
+
function cn(...inputs) {
|
|
28
|
+
const classes = [];
|
|
29
|
+
for (const input of inputs) {
|
|
30
|
+
if (!input) continue;
|
|
31
|
+
if (typeof input === "string") {
|
|
32
|
+
classes.push(input);
|
|
33
|
+
} else if (typeof input === "object") {
|
|
34
|
+
for (const [key, value] of Object.entries(input)) {
|
|
35
|
+
if (value) {
|
|
36
|
+
classes.push(key);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return classes.join(" ");
|
|
42
|
+
}
|
|
43
|
+
var ImageTailwind = React.forwardRef(({ isLoading, className, ...rest }, ref) => /* @__PURE__ */ jsx(
|
|
44
|
+
"img",
|
|
45
|
+
{
|
|
46
|
+
ref,
|
|
47
|
+
className: cn(
|
|
48
|
+
"Image",
|
|
49
|
+
"seeds-image",
|
|
50
|
+
{ "seeds-image-loading": !!isLoading },
|
|
51
|
+
className
|
|
52
|
+
),
|
|
53
|
+
...rest
|
|
54
|
+
}
|
|
55
|
+
));
|
|
56
|
+
ImageTailwind.displayName = "ImageTailwind";
|
|
57
|
+
|
|
58
|
+
// src/ImageHybrid.tsx
|
|
59
|
+
import { hasStyledProps } from "@sproutsocial/seeds-react-system-props";
|
|
60
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
23
61
|
var noop = () => {
|
|
24
62
|
};
|
|
25
|
-
var
|
|
63
|
+
var ImageHybrid = class extends React2.Component {
|
|
26
64
|
static defaultProps = {
|
|
27
65
|
title: "",
|
|
28
66
|
onError: noop,
|
|
@@ -70,34 +108,62 @@ var Image = class extends React.Component {
|
|
|
70
108
|
this.props.onClick();
|
|
71
109
|
};
|
|
72
110
|
render() {
|
|
73
|
-
const {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
const {
|
|
112
|
+
alt,
|
|
113
|
+
title,
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
115
|
+
onClick,
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
117
|
+
onError,
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
119
|
+
onLoad,
|
|
120
|
+
src,
|
|
121
|
+
qa,
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
123
|
+
defaultImage,
|
|
124
|
+
...rest
|
|
125
|
+
} = this.props;
|
|
126
|
+
const isLoading = !this.state.didLoad;
|
|
127
|
+
const refCb = (img) => this.imageRef = img;
|
|
128
|
+
const shared = {
|
|
129
|
+
onClick: this.onClick,
|
|
130
|
+
src: this.state.imageUrl,
|
|
131
|
+
onError: (e) => this.onError(e),
|
|
132
|
+
onLoad: this.onLoad,
|
|
133
|
+
alt,
|
|
134
|
+
title,
|
|
135
|
+
"data-qa-image": title ? title : alt ? alt : "",
|
|
136
|
+
"data-qa-image-src": src || "",
|
|
137
|
+
...qa,
|
|
138
|
+
...rest
|
|
139
|
+
};
|
|
140
|
+
if (hasStyledProps(rest)) {
|
|
141
|
+
return /* @__PURE__ */ jsx2(styles_default, { ref: refCb, isLoading, ...shared });
|
|
142
|
+
}
|
|
143
|
+
return /* @__PURE__ */ jsx2(ImageTailwind, { ref: refCb, isLoading, ...shared });
|
|
91
144
|
}
|
|
92
145
|
};
|
|
93
146
|
|
|
147
|
+
// src/Image.tsx
|
|
148
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
149
|
+
var noop2 = () => {
|
|
150
|
+
};
|
|
151
|
+
var Image = (props) => /* @__PURE__ */ jsx3(ImageHybrid, { ...props });
|
|
152
|
+
Image.defaultProps = {
|
|
153
|
+
title: "",
|
|
154
|
+
onError: noop2,
|
|
155
|
+
onClick: noop2,
|
|
156
|
+
onLoad: noop2
|
|
157
|
+
};
|
|
158
|
+
var Image_default = Image;
|
|
159
|
+
|
|
94
160
|
// src/ImageTypes.ts
|
|
95
161
|
import "react";
|
|
96
162
|
|
|
97
163
|
// src/index.ts
|
|
98
|
-
var index_default =
|
|
164
|
+
var index_default = Image_default;
|
|
99
165
|
export {
|
|
100
|
-
Image,
|
|
166
|
+
Image_default as Image,
|
|
101
167
|
index_default as default
|
|
102
168
|
};
|
|
103
169
|
//# sourceMappingURL=index.js.map
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/Image.tsx","../../src/styles.ts","../../src/ImageTypes.ts","../../src/index.ts"],"sourcesContent":["import * as React from \"react\";\nimport ImageContainer from \"./styles\";\nimport type { TypeImageProps } from \"./ImageTypes\";\n\ntype TypeState = {\n didError: boolean;\n didLoad: boolean;\n imageUrl: string;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst noop = () => {};\n\nexport default class
|
|
1
|
+
{"version":3,"sources":["../../src/Image.tsx","../../src/ImageHybrid.tsx","../../src/styles.ts","../../src/ImageTailwind.tsx","../../src/ImageTypes.ts","../../src/index.ts"],"sourcesContent":["import * as React from \"react\";\nimport ImageHybrid from \"./ImageHybrid\";\nimport type { TypeImageProps } from \"./ImageTypes\";\n\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst noop = () => {};\n\n/**\n * Image component. Automatically routes between the Tailwind implementation\n * (preferred) and the styled-components implementation (for consumers using\n * system props or a styled() extension). The load/error state lives in\n * ImageHybrid so behavior is identical on both paths.\n */\nconst Image = (props: TypeImageProps) => <ImageHybrid {...props} />;\n\n// Preserve the original public type surface: defaultProps keep title/onError/\n// onClick/onLoad optional at the JSX call site (these are required in\n// TypeImageProps). The defaults are applied before reaching ImageHybrid.\nImage.defaultProps = {\n title: \"\",\n onError: noop,\n onClick: noop,\n onLoad: noop,\n};\n\nexport default Image;\n","/**\n * Hybrid Image Component\n * Owns the load/error state + lifecycle in ONE place and switches only the\n * rendered leaf: the styled-components element when styled-system props are\n * present, otherwise the lighter Tailwind <img>.\n */\n\nimport * as React from \"react\";\nimport ImageContainer from \"./styles\"; // Styled-components version\nimport { ImageTailwind } from \"./ImageTailwind\"; // Tailwind version\nimport { hasStyledProps } from \"@sproutsocial/seeds-react-system-props\";\nimport type { TypeImageProps } from \"./ImageTypes\";\n\ntype TypeState = {\n didError: boolean;\n didLoad: boolean;\n imageUrl: string;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst noop = () => {};\n\nexport default class ImageHybrid extends React.Component<\n TypeImageProps,\n TypeState\n> {\n static defaultProps = {\n title: \"\",\n onError: noop,\n onClick: noop,\n onLoad: noop,\n };\n\n override state = {\n didError: false,\n didLoad: false,\n imageUrl: this.props.src,\n };\n\n imageRef?: HTMLImageElement | null;\n\n override componentDidMount() {\n // eslint-disable-next-line react/no-did-mount-set-state\n if (this.imageRef) {\n this.setState({\n didLoad: this.imageRef.complete,\n });\n }\n }\n\n override componentDidUpdate(prevProps: TypeImageProps) {\n if (this.props.src !== prevProps.src) {\n this.setState({\n didError: false,\n didLoad: false,\n imageUrl: this.props.src,\n });\n }\n }\n\n onError = (e: React.SyntheticEvent<HTMLImageElement, Event>) => {\n if (!this.state.didError && this.props.defaultImage) {\n this.setState({\n didError: true,\n imageUrl: this.props.defaultImage,\n });\n }\n\n this.props.onError(e);\n };\n\n onLoad = () => {\n this.setState({\n didLoad: true,\n });\n this.props.onLoad();\n };\n\n onClick = () => {\n this.props.onClick();\n };\n\n override render() {\n // defaultImage is consumed by onError (this.props.defaultImage) and must be\n // pulled out of rest so it never reaches the DOM <img> on the Tailwind path\n // (the styled-components leaf filtered it implicitly; a plain <img> does not).\n const {\n alt,\n title,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onClick,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onError,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onLoad,\n src,\n qa,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n defaultImage,\n ...rest\n } = this.props;\n const isLoading = !this.state.didLoad;\n const refCb = (img: HTMLImageElement | null) => (this.imageRef = img);\n const shared = {\n onClick: this.onClick,\n src: this.state.imageUrl,\n onError: (e: React.SyntheticEvent<HTMLImageElement, Event>) =>\n this.onError(e),\n onLoad: this.onLoad,\n alt,\n title,\n \"data-qa-image\": title ? title : alt ? alt : \"\",\n \"data-qa-image-src\": src || \"\",\n ...(qa as any),\n ...rest,\n };\n\n if (hasStyledProps(rest)) {\n // Legacy styled-components leaf — styles.ts unchanged.\n return <ImageContainer ref={refCb} isLoading={isLoading} {...shared} />;\n }\n\n // Tailwind leaf (preferred - lighter, no styled-components runtime).\n return <ImageTailwind ref={refCb} isLoading={isLoading} {...shared} />;\n }\n}\n","import styled, { css } from \"styled-components\";\nimport { COMMON, LAYOUT } from \"@sproutsocial/seeds-react-system-props\";\n\ninterface ImageContainerProps {\n isLoading?: boolean;\n}\nconst ImageContainer = styled.img.attrs({\n className: \"Image\",\n})<ImageContainerProps>`\n display: block;\n\n ${(props) =>\n props.isLoading &&\n css`\n opacity: 0;\n `}\n\n ${COMMON}\n ${LAYOUT}\n`;\n\nexport default ImageContainer;\n","/**\n * Tailwind CSS implementation of Image component\n * Uses Seeds image CSS classes from image.css\n */\n\nimport React from \"react\";\n\n// Utility for merging class names properly\nfunction cn(\n ...inputs: (string | undefined | null | false | Record<string, boolean>)[]\n): string {\n const classes: string[] = [];\n\n for (const input of inputs) {\n if (!input) continue;\n\n if (typeof input === \"string\") {\n classes.push(input);\n } else if (typeof input === \"object\") {\n for (const [key, value] of Object.entries(input)) {\n if (value) {\n classes.push(key);\n }\n }\n }\n }\n\n return classes.join(\" \");\n}\n\ntype TypeImageTailwindProps = React.ComponentPropsWithoutRef<\"img\"> & {\n isLoading?: boolean;\n};\n\nexport const ImageTailwind = React.forwardRef<\n HTMLImageElement,\n TypeImageTailwindProps\n>(({ isLoading, className, ...rest }, ref) => (\n <img\n ref={ref}\n // Preserve the legacy \"Image\" marker class (set by styles.ts's .attrs)\n // for any consumer selectors targeting it.\n className={cn(\n \"Image\",\n \"seeds-image\",\n { \"seeds-image-loading\": !!isLoading },\n className\n )}\n {...rest}\n />\n));\n\nImageTailwind.displayName = \"ImageTailwind\";\n","import * as React from \"react\";\nimport type {\n TypeSystemCommonProps,\n TypeSystemLayoutProps,\n TypeStyledComponentsCommonProps,\n} from \"@sproutsocial/seeds-react-system-props\";\n\nexport interface TypeImageProps\n extends TypeStyledComponentsCommonProps,\n TypeSystemCommonProps,\n TypeSystemLayoutProps,\n Omit<React.ComponentPropsWithoutRef<\"img\">, \"color\" | \"height\" | \"width\"> {\n src: string;\n\n /** Alt text is required for non-decorative images */\n alt: string;\n title: string;\n onError: (e: React.SyntheticEvent<HTMLImageElement, Event>) => void;\n onClick: () => void;\n\n /** A URL for a default image to load if the source image is not available */\n defaultImage?: string;\n onLoad: () => void;\n qa?: object;\n}\n","import Image from \"./Image\";\n\nexport default Image;\nexport { Image };\nexport * from \"./ImageTypes\";\n"],"mappings":"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","names":["React","jsx","jsx","noop"]}
|
package/dist/image.css
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds Image component classes
|
|
3
|
+
* Use these instead of writing out individual Tailwind utility classes.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <img class="seeds-image" />
|
|
7
|
+
* <img class="seeds-image seeds-image-loading" />
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
@layer components {
|
|
11
|
+
.seeds-image {
|
|
12
|
+
display: block;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.seeds-image-loading {
|
|
16
|
+
opacity: 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/dist/index.d.mts
CHANGED
|
@@ -15,30 +15,20 @@ interface TypeImageProps extends TypeStyledComponentsCommonProps, TypeSystemComm
|
|
|
15
15
|
qa?: object;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Image component. Automatically routes between the Tailwind implementation
|
|
20
|
+
* (preferred) and the styled-components implementation (for consumers using
|
|
21
|
+
* system props or a styled() extension). The load/error state lives in
|
|
22
|
+
* ImageHybrid so behavior is identical on both paths.
|
|
23
|
+
*/
|
|
24
|
+
declare const Image: {
|
|
25
|
+
(props: TypeImageProps): react_jsx_runtime.JSX.Element;
|
|
26
|
+
defaultProps: {
|
|
25
27
|
title: string;
|
|
26
28
|
onError: () => void;
|
|
27
29
|
onClick: () => void;
|
|
28
30
|
onLoad: () => void;
|
|
29
31
|
};
|
|
30
|
-
|
|
31
|
-
didError: boolean;
|
|
32
|
-
didLoad: boolean;
|
|
33
|
-
imageUrl: string;
|
|
34
|
-
};
|
|
35
|
-
imageRef?: HTMLImageElement | null;
|
|
36
|
-
componentDidMount(): void;
|
|
37
|
-
componentDidUpdate(prevProps: TypeImageProps): void;
|
|
38
|
-
onError: (e: React.SyntheticEvent<HTMLImageElement, Event>) => void;
|
|
39
|
-
onLoad: () => void;
|
|
40
|
-
onClick: () => void;
|
|
41
|
-
render(): react_jsx_runtime.JSX.Element;
|
|
42
|
-
}
|
|
32
|
+
};
|
|
43
33
|
|
|
44
34
|
export { Image, type TypeImageProps, Image as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -15,30 +15,20 @@ interface TypeImageProps extends TypeStyledComponentsCommonProps, TypeSystemComm
|
|
|
15
15
|
qa?: object;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Image component. Automatically routes between the Tailwind implementation
|
|
20
|
+
* (preferred) and the styled-components implementation (for consumers using
|
|
21
|
+
* system props or a styled() extension). The load/error state lives in
|
|
22
|
+
* ImageHybrid so behavior is identical on both paths.
|
|
23
|
+
*/
|
|
24
|
+
declare const Image: {
|
|
25
|
+
(props: TypeImageProps): react_jsx_runtime.JSX.Element;
|
|
26
|
+
defaultProps: {
|
|
25
27
|
title: string;
|
|
26
28
|
onError: () => void;
|
|
27
29
|
onClick: () => void;
|
|
28
30
|
onLoad: () => void;
|
|
29
31
|
};
|
|
30
|
-
|
|
31
|
-
didError: boolean;
|
|
32
|
-
didLoad: boolean;
|
|
33
|
-
imageUrl: string;
|
|
34
|
-
};
|
|
35
|
-
imageRef?: HTMLImageElement | null;
|
|
36
|
-
componentDidMount(): void;
|
|
37
|
-
componentDidUpdate(prevProps: TypeImageProps): void;
|
|
38
|
-
onError: (e: React.SyntheticEvent<HTMLImageElement, Event>) => void;
|
|
39
|
-
onLoad: () => void;
|
|
40
|
-
onClick: () => void;
|
|
41
|
-
render(): react_jsx_runtime.JSX.Element;
|
|
42
|
-
}
|
|
32
|
+
};
|
|
43
33
|
|
|
44
34
|
export { Image, type TypeImageProps, Image as default };
|
package/dist/index.js
CHANGED
|
@@ -30,13 +30,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
Image: () =>
|
|
33
|
+
Image: () => Image_default,
|
|
34
34
|
default: () => index_default
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
37
|
|
|
38
38
|
// src/Image.tsx
|
|
39
|
-
var
|
|
39
|
+
var React3 = require("react");
|
|
40
|
+
|
|
41
|
+
// src/ImageHybrid.tsx
|
|
42
|
+
var React2 = __toESM(require("react"));
|
|
40
43
|
|
|
41
44
|
// src/styles.ts
|
|
42
45
|
var import_styled_components = __toESM(require("styled-components"));
|
|
@@ -55,11 +58,46 @@ var ImageContainer = import_styled_components.default.img.attrs({
|
|
|
55
58
|
`;
|
|
56
59
|
var styles_default = ImageContainer;
|
|
57
60
|
|
|
58
|
-
// src/
|
|
61
|
+
// src/ImageTailwind.tsx
|
|
62
|
+
var import_react = __toESM(require("react"));
|
|
59
63
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
64
|
+
function cn(...inputs) {
|
|
65
|
+
const classes = [];
|
|
66
|
+
for (const input of inputs) {
|
|
67
|
+
if (!input) continue;
|
|
68
|
+
if (typeof input === "string") {
|
|
69
|
+
classes.push(input);
|
|
70
|
+
} else if (typeof input === "object") {
|
|
71
|
+
for (const [key, value] of Object.entries(input)) {
|
|
72
|
+
if (value) {
|
|
73
|
+
classes.push(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return classes.join(" ");
|
|
79
|
+
}
|
|
80
|
+
var ImageTailwind = import_react.default.forwardRef(({ isLoading, className, ...rest }, ref) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
81
|
+
"img",
|
|
82
|
+
{
|
|
83
|
+
ref,
|
|
84
|
+
className: cn(
|
|
85
|
+
"Image",
|
|
86
|
+
"seeds-image",
|
|
87
|
+
{ "seeds-image-loading": !!isLoading },
|
|
88
|
+
className
|
|
89
|
+
),
|
|
90
|
+
...rest
|
|
91
|
+
}
|
|
92
|
+
));
|
|
93
|
+
ImageTailwind.displayName = "ImageTailwind";
|
|
94
|
+
|
|
95
|
+
// src/ImageHybrid.tsx
|
|
96
|
+
var import_seeds_react_system_props2 = require("@sproutsocial/seeds-react-system-props");
|
|
97
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
60
98
|
var noop = () => {
|
|
61
99
|
};
|
|
62
|
-
var
|
|
100
|
+
var ImageHybrid = class extends React2.Component {
|
|
63
101
|
static defaultProps = {
|
|
64
102
|
title: "",
|
|
65
103
|
onError: noop,
|
|
@@ -107,32 +145,60 @@ var Image = class extends React.Component {
|
|
|
107
145
|
this.props.onClick();
|
|
108
146
|
};
|
|
109
147
|
render() {
|
|
110
|
-
const {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
148
|
+
const {
|
|
149
|
+
alt,
|
|
150
|
+
title,
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
152
|
+
onClick,
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
154
|
+
onError,
|
|
155
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
156
|
+
onLoad,
|
|
157
|
+
src,
|
|
158
|
+
qa,
|
|
159
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
160
|
+
defaultImage,
|
|
161
|
+
...rest
|
|
162
|
+
} = this.props;
|
|
163
|
+
const isLoading = !this.state.didLoad;
|
|
164
|
+
const refCb = (img) => this.imageRef = img;
|
|
165
|
+
const shared = {
|
|
166
|
+
onClick: this.onClick,
|
|
167
|
+
src: this.state.imageUrl,
|
|
168
|
+
onError: (e) => this.onError(e),
|
|
169
|
+
onLoad: this.onLoad,
|
|
170
|
+
alt,
|
|
171
|
+
title,
|
|
172
|
+
"data-qa-image": title ? title : alt ? alt : "",
|
|
173
|
+
"data-qa-image-src": src || "",
|
|
174
|
+
...qa,
|
|
175
|
+
...rest
|
|
176
|
+
};
|
|
177
|
+
if ((0, import_seeds_react_system_props2.hasStyledProps)(rest)) {
|
|
178
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(styles_default, { ref: refCb, isLoading, ...shared });
|
|
179
|
+
}
|
|
180
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ImageTailwind, { ref: refCb, isLoading, ...shared });
|
|
128
181
|
}
|
|
129
182
|
};
|
|
130
183
|
|
|
184
|
+
// src/Image.tsx
|
|
185
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
186
|
+
var noop2 = () => {
|
|
187
|
+
};
|
|
188
|
+
var Image = (props) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ImageHybrid, { ...props });
|
|
189
|
+
Image.defaultProps = {
|
|
190
|
+
title: "",
|
|
191
|
+
onError: noop2,
|
|
192
|
+
onClick: noop2,
|
|
193
|
+
onLoad: noop2
|
|
194
|
+
};
|
|
195
|
+
var Image_default = Image;
|
|
196
|
+
|
|
131
197
|
// src/ImageTypes.ts
|
|
132
|
-
var
|
|
198
|
+
var React4 = require("react");
|
|
133
199
|
|
|
134
200
|
// src/index.ts
|
|
135
|
-
var index_default =
|
|
201
|
+
var index_default = Image_default;
|
|
136
202
|
// Annotate the CommonJS export names for ESM import in node:
|
|
137
203
|
0 && (module.exports = {
|
|
138
204
|
Image
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/Image.tsx","../src/styles.ts","../src/ImageTypes.ts"],"sourcesContent":["import Image from \"./Image\";\n\nexport default Image;\nexport { Image };\nexport * from \"./ImageTypes\";\n","import * as React from \"react\";\nimport ImageContainer from \"./styles\";\nimport type { TypeImageProps } from \"./ImageTypes\";\n\ntype TypeState = {\n didError: boolean;\n didLoad: boolean;\n imageUrl: string;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst noop = () => {};\n\nexport default class Image extends React.Component<TypeImageProps, TypeState> {\n static defaultProps = {\n title: \"\",\n onError: noop,\n onClick: noop,\n onLoad: noop,\n };\n\n override state = {\n didError: false,\n didLoad: false,\n imageUrl: this.props.src,\n };\n\n imageRef?: HTMLImageElement | null;\n\n override componentDidMount() {\n // eslint-disable-next-line react/no-did-mount-set-state\n if (this.imageRef) {\n this.setState({\n didLoad: this.imageRef.complete,\n });\n }\n }\n\n override componentDidUpdate(prevProps: TypeImageProps) {\n if (this.props.src !== prevProps.src) {\n this.setState({\n didError: false,\n didLoad: false,\n imageUrl: this.props.src,\n });\n }\n }\n\n onError = (e: React.SyntheticEvent<HTMLImageElement, Event>) => {\n if (!this.state.didError && this.props.defaultImage) {\n this.setState({\n didError: true,\n imageUrl: this.props.defaultImage,\n });\n }\n\n this.props.onError(e);\n };\n\n onLoad = () => {\n this.setState({\n didLoad: true,\n });\n this.props.onLoad();\n };\n\n onClick = () => {\n this.props.onClick();\n };\n\n override render() {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { alt, title, onClick, onError, onLoad, src, qa, ...rest } =\n this.props;\n return (\n <ImageContainer\n isLoading={!this.state.didLoad}\n ref={(img) => (this.imageRef = img)}\n onClick={this.onClick}\n src={this.state.imageUrl}\n onError={(e: React.SyntheticEvent<HTMLImageElement, Event>) =>\n this.onError(e)\n }\n onLoad={this.onLoad}\n alt={alt}\n title={title}\n data-qa-image={title ? title : alt ? alt : \"\"}\n data-qa-image-src={src || \"\"}\n {...(qa as any)}\n {...rest}\n />\n );\n }\n}\n","import styled, { css } from \"styled-components\";\nimport { COMMON, LAYOUT } from \"@sproutsocial/seeds-react-system-props\";\n\ninterface ImageContainerProps {\n isLoading?: boolean;\n}\nconst ImageContainer = styled.img.attrs({\n className: \"Image\",\n})<ImageContainerProps>`\n display: block;\n\n ${(props) =>\n props.isLoading &&\n css`\n opacity: 0;\n `}\n\n ${COMMON}\n ${LAYOUT}\n`;\n\nexport default ImageContainer;\n","import * as React from \"react\";\nimport type {\n TypeSystemCommonProps,\n TypeSystemLayoutProps,\n TypeStyledComponentsCommonProps,\n} from \"@sproutsocial/seeds-react-system-props\";\n\nexport interface TypeImageProps\n extends TypeStyledComponentsCommonProps,\n TypeSystemCommonProps,\n TypeSystemLayoutProps,\n Omit<React.ComponentPropsWithoutRef<\"img\">, \"color\" | \"height\" | \"width\"> {\n src: string;\n\n /** Alt text is required for non-decorative images */\n alt: string;\n title: string;\n onError: (e: React.SyntheticEvent<HTMLImageElement, Event>) => void;\n onClick: () => void;\n\n /** A URL for a default image to load if the source image is not available */\n defaultImage?: string;\n onLoad: () => void;\n qa?: object;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,YAAuB;;;ACAvB,+BAA4B;AAC5B,sCAA+B;AAK/B,IAAM,iBAAiB,yBAAAA,QAAO,IAAI,MAAM;AAAA,EACtC,WAAW;AACb,CAAC;AAAA;AAAA;AAAA,IAGG,CAAC,UACD,MAAM,aACN;AAAA;AAAA,KAEC;AAAA;AAAA,IAED,sCAAM;AAAA,IACN,sCAAM;AAAA;AAGV,IAAO,iBAAQ;;;ADsDT;AAhEN,IAAM,OAAO,MAAM;AAAC;AAEpB,IAAqB,QAArB,cAAyC,gBAAqC;AAAA,EAC5E,OAAO,eAAe;AAAA,IACpB,OAAO;AAAA,IACP,SAAS;AAAA,IACT,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AAAA,EAES,QAAQ;AAAA,IACf,UAAU;AAAA,IACV,SAAS;AAAA,IACT,UAAU,KAAK,MAAM;AAAA,EACvB;AAAA,EAEA;AAAA,EAES,oBAAoB;AAE3B,QAAI,KAAK,UAAU;AACjB,WAAK,SAAS;AAAA,QACZ,SAAS,KAAK,SAAS;AAAA,MACzB,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAES,mBAAmB,WAA2B;AACrD,QAAI,KAAK,MAAM,QAAQ,UAAU,KAAK;AACpC,WAAK,SAAS;AAAA,QACZ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,UAAU,KAAK,MAAM;AAAA,MACvB,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,UAAU,CAAC,MAAqD;AAC9D,QAAI,CAAC,KAAK,MAAM,YAAY,KAAK,MAAM,cAAc;AACnD,WAAK,SAAS;AAAA,QACZ,UAAU;AAAA,QACV,UAAU,KAAK,MAAM;AAAA,MACvB,CAAC;AAAA,IACH;AAEA,SAAK,MAAM,QAAQ,CAAC;AAAA,EACtB;AAAA,EAEA,SAAS,MAAM;AACb,SAAK,SAAS;AAAA,MACZ,SAAS;AAAA,IACX,CAAC;AACD,SAAK,MAAM,OAAO;AAAA,EACpB;AAAA,EAEA,UAAU,MAAM;AACd,SAAK,MAAM,QAAQ;AAAA,EACrB;AAAA,EAES,SAAS;AAEhB,UAAM,EAAE,KAAK,OAAO,SAAS,SAAS,QAAQ,KAAK,IAAI,GAAG,KAAK,IAC7D,KAAK;AACP,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,CAAC,KAAK,MAAM;AAAA,QACvB,KAAK,CAAC,QAAS,KAAK,WAAW;AAAA,QAC/B,SAAS,KAAK;AAAA,QACd,KAAK,KAAK,MAAM;AAAA,QAChB,SAAS,CAAC,MACR,KAAK,QAAQ,CAAC;AAAA,QAEhB,QAAQ,KAAK;AAAA,QACb;AAAA,QACA;AAAA,QACA,iBAAe,QAAQ,QAAQ,MAAM,MAAM;AAAA,QAC3C,qBAAmB,OAAO;AAAA,QACzB,GAAI;AAAA,QACJ,GAAG;AAAA;AAAA,IACN;AAAA,EAEJ;AACF;;;AE7FA,IAAAC,SAAuB;;;AHEvB,IAAO,gBAAQ;","names":["styled","React"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/Image.tsx","../src/ImageHybrid.tsx","../src/styles.ts","../src/ImageTailwind.tsx","../src/ImageTypes.ts"],"sourcesContent":["import Image from \"./Image\";\n\nexport default Image;\nexport { Image };\nexport * from \"./ImageTypes\";\n","import * as React from \"react\";\nimport ImageHybrid from \"./ImageHybrid\";\nimport type { TypeImageProps } from \"./ImageTypes\";\n\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst noop = () => {};\n\n/**\n * Image component. Automatically routes between the Tailwind implementation\n * (preferred) and the styled-components implementation (for consumers using\n * system props or a styled() extension). The load/error state lives in\n * ImageHybrid so behavior is identical on both paths.\n */\nconst Image = (props: TypeImageProps) => <ImageHybrid {...props} />;\n\n// Preserve the original public type surface: defaultProps keep title/onError/\n// onClick/onLoad optional at the JSX call site (these are required in\n// TypeImageProps). The defaults are applied before reaching ImageHybrid.\nImage.defaultProps = {\n title: \"\",\n onError: noop,\n onClick: noop,\n onLoad: noop,\n};\n\nexport default Image;\n","/**\n * Hybrid Image Component\n * Owns the load/error state + lifecycle in ONE place and switches only the\n * rendered leaf: the styled-components element when styled-system props are\n * present, otherwise the lighter Tailwind <img>.\n */\n\nimport * as React from \"react\";\nimport ImageContainer from \"./styles\"; // Styled-components version\nimport { ImageTailwind } from \"./ImageTailwind\"; // Tailwind version\nimport { hasStyledProps } from \"@sproutsocial/seeds-react-system-props\";\nimport type { TypeImageProps } from \"./ImageTypes\";\n\ntype TypeState = {\n didError: boolean;\n didLoad: boolean;\n imageUrl: string;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst noop = () => {};\n\nexport default class ImageHybrid extends React.Component<\n TypeImageProps,\n TypeState\n> {\n static defaultProps = {\n title: \"\",\n onError: noop,\n onClick: noop,\n onLoad: noop,\n };\n\n override state = {\n didError: false,\n didLoad: false,\n imageUrl: this.props.src,\n };\n\n imageRef?: HTMLImageElement | null;\n\n override componentDidMount() {\n // eslint-disable-next-line react/no-did-mount-set-state\n if (this.imageRef) {\n this.setState({\n didLoad: this.imageRef.complete,\n });\n }\n }\n\n override componentDidUpdate(prevProps: TypeImageProps) {\n if (this.props.src !== prevProps.src) {\n this.setState({\n didError: false,\n didLoad: false,\n imageUrl: this.props.src,\n });\n }\n }\n\n onError = (e: React.SyntheticEvent<HTMLImageElement, Event>) => {\n if (!this.state.didError && this.props.defaultImage) {\n this.setState({\n didError: true,\n imageUrl: this.props.defaultImage,\n });\n }\n\n this.props.onError(e);\n };\n\n onLoad = () => {\n this.setState({\n didLoad: true,\n });\n this.props.onLoad();\n };\n\n onClick = () => {\n this.props.onClick();\n };\n\n override render() {\n // defaultImage is consumed by onError (this.props.defaultImage) and must be\n // pulled out of rest so it never reaches the DOM <img> on the Tailwind path\n // (the styled-components leaf filtered it implicitly; a plain <img> does not).\n const {\n alt,\n title,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onClick,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onError,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onLoad,\n src,\n qa,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n defaultImage,\n ...rest\n } = this.props;\n const isLoading = !this.state.didLoad;\n const refCb = (img: HTMLImageElement | null) => (this.imageRef = img);\n const shared = {\n onClick: this.onClick,\n src: this.state.imageUrl,\n onError: (e: React.SyntheticEvent<HTMLImageElement, Event>) =>\n this.onError(e),\n onLoad: this.onLoad,\n alt,\n title,\n \"data-qa-image\": title ? title : alt ? alt : \"\",\n \"data-qa-image-src\": src || \"\",\n ...(qa as any),\n ...rest,\n };\n\n if (hasStyledProps(rest)) {\n // Legacy styled-components leaf — styles.ts unchanged.\n return <ImageContainer ref={refCb} isLoading={isLoading} {...shared} />;\n }\n\n // Tailwind leaf (preferred - lighter, no styled-components runtime).\n return <ImageTailwind ref={refCb} isLoading={isLoading} {...shared} />;\n }\n}\n","import styled, { css } from \"styled-components\";\nimport { COMMON, LAYOUT } from \"@sproutsocial/seeds-react-system-props\";\n\ninterface ImageContainerProps {\n isLoading?: boolean;\n}\nconst ImageContainer = styled.img.attrs({\n className: \"Image\",\n})<ImageContainerProps>`\n display: block;\n\n ${(props) =>\n props.isLoading &&\n css`\n opacity: 0;\n `}\n\n ${COMMON}\n ${LAYOUT}\n`;\n\nexport default ImageContainer;\n","/**\n * Tailwind CSS implementation of Image component\n * Uses Seeds image CSS classes from image.css\n */\n\nimport React from \"react\";\n\n// Utility for merging class names properly\nfunction cn(\n ...inputs: (string | undefined | null | false | Record<string, boolean>)[]\n): string {\n const classes: string[] = [];\n\n for (const input of inputs) {\n if (!input) continue;\n\n if (typeof input === \"string\") {\n classes.push(input);\n } else if (typeof input === \"object\") {\n for (const [key, value] of Object.entries(input)) {\n if (value) {\n classes.push(key);\n }\n }\n }\n }\n\n return classes.join(\" \");\n}\n\ntype TypeImageTailwindProps = React.ComponentPropsWithoutRef<\"img\"> & {\n isLoading?: boolean;\n};\n\nexport const ImageTailwind = React.forwardRef<\n HTMLImageElement,\n TypeImageTailwindProps\n>(({ isLoading, className, ...rest }, ref) => (\n <img\n ref={ref}\n // Preserve the legacy \"Image\" marker class (set by styles.ts's .attrs)\n // for any consumer selectors targeting it.\n className={cn(\n \"Image\",\n \"seeds-image\",\n { \"seeds-image-loading\": !!isLoading },\n className\n )}\n {...rest}\n />\n));\n\nImageTailwind.displayName = \"ImageTailwind\";\n","import * as React from \"react\";\nimport type {\n TypeSystemCommonProps,\n TypeSystemLayoutProps,\n TypeStyledComponentsCommonProps,\n} from \"@sproutsocial/seeds-react-system-props\";\n\nexport interface TypeImageProps\n extends TypeStyledComponentsCommonProps,\n TypeSystemCommonProps,\n TypeSystemLayoutProps,\n Omit<React.ComponentPropsWithoutRef<\"img\">, \"color\" | \"height\" | \"width\"> {\n src: string;\n\n /** Alt text is required for non-decorative images */\n alt: string;\n title: string;\n onError: (e: React.SyntheticEvent<HTMLImageElement, Event>) => void;\n onClick: () => void;\n\n /** A URL for a default image to load if the source image is not available */\n defaultImage?: string;\n onLoad: () => void;\n qa?: object;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAAA,SAAuB;;;ACOvB,IAAAC,SAAuB;;;ACPvB,+BAA4B;AAC5B,sCAA+B;AAK/B,IAAM,iBAAiB,yBAAAC,QAAO,IAAI,MAAM;AAAA,EACtC,WAAW;AACb,CAAC;AAAA;AAAA;AAAA,IAGG,CAAC,UACD,MAAM,aACN;AAAA;AAAA,KAEC;AAAA;AAAA,IAED,sCAAM;AAAA,IACN,sCAAM;AAAA;AAGV,IAAO,iBAAQ;;;AChBf,mBAAkB;AAiChB;AA9BF,SAAS,MACJ,QACK;AACR,QAAM,UAAoB,CAAC;AAE3B,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,MAAO;AAEZ,QAAI,OAAO,UAAU,UAAU;AAC7B,cAAQ,KAAK,KAAK;AAAA,IACpB,WAAW,OAAO,UAAU,UAAU;AACpC,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,YAAI,OAAO;AACT,kBAAQ,KAAK,GAAG;AAAA,QAClB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO,QAAQ,KAAK,GAAG;AACzB;AAMO,IAAM,gBAAgB,aAAAC,QAAM,WAGjC,CAAC,EAAE,WAAW,WAAW,GAAG,KAAK,GAAG,QACpC;AAAA,EAAC;AAAA;AAAA,IACC;AAAA,IAGA,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA,EAAE,uBAAuB,CAAC,CAAC,UAAU;AAAA,MACrC;AAAA,IACF;AAAA,IACC,GAAG;AAAA;AACN,CACD;AAED,cAAc,cAAc;;;AF1C5B,IAAAC,mCAA+B;AA6GlB,IAAAC,sBAAA;AAnGb,IAAM,OAAO,MAAM;AAAC;AAEpB,IAAqB,cAArB,cAA+C,iBAG7C;AAAA,EACA,OAAO,eAAe;AAAA,IACpB,OAAO;AAAA,IACP,SAAS;AAAA,IACT,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AAAA,EAES,QAAQ;AAAA,IACf,UAAU;AAAA,IACV,SAAS;AAAA,IACT,UAAU,KAAK,MAAM;AAAA,EACvB;AAAA,EAEA;AAAA,EAES,oBAAoB;AAE3B,QAAI,KAAK,UAAU;AACjB,WAAK,SAAS;AAAA,QACZ,SAAS,KAAK,SAAS;AAAA,MACzB,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAES,mBAAmB,WAA2B;AACrD,QAAI,KAAK,MAAM,QAAQ,UAAU,KAAK;AACpC,WAAK,SAAS;AAAA,QACZ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,UAAU,KAAK,MAAM;AAAA,MACvB,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,UAAU,CAAC,MAAqD;AAC9D,QAAI,CAAC,KAAK,MAAM,YAAY,KAAK,MAAM,cAAc;AACnD,WAAK,SAAS;AAAA,QACZ,UAAU;AAAA,QACV,UAAU,KAAK,MAAM;AAAA,MACvB,CAAC;AAAA,IACH;AAEA,SAAK,MAAM,QAAQ,CAAC;AAAA,EACtB;AAAA,EAEA,SAAS,MAAM;AACb,SAAK,SAAS;AAAA,MACZ,SAAS;AAAA,IACX,CAAC;AACD,SAAK,MAAM,OAAO;AAAA,EACpB;AAAA,EAEA,UAAU,MAAM;AACd,SAAK,MAAM,QAAQ;AAAA,EACrB;AAAA,EAES,SAAS;AAIhB,UAAM;AAAA,MACJ;AAAA,MACA;AAAA;AAAA,MAEA;AAAA;AAAA,MAEA;AAAA;AAAA,MAEA;AAAA,MACA;AAAA,MACA;AAAA;AAAA,MAEA;AAAA,MACA,GAAG;AAAA,IACL,IAAI,KAAK;AACT,UAAM,YAAY,CAAC,KAAK,MAAM;AAC9B,UAAM,QAAQ,CAAC,QAAkC,KAAK,WAAW;AACjE,UAAM,SAAS;AAAA,MACb,SAAS,KAAK;AAAA,MACd,KAAK,KAAK,MAAM;AAAA,MAChB,SAAS,CAAC,MACR,KAAK,QAAQ,CAAC;AAAA,MAChB,QAAQ,KAAK;AAAA,MACb;AAAA,MACA;AAAA,MACA,iBAAiB,QAAQ,QAAQ,MAAM,MAAM;AAAA,MAC7C,qBAAqB,OAAO;AAAA,MAC5B,GAAI;AAAA,MACJ,GAAG;AAAA,IACL;AAEA,YAAI,iDAAe,IAAI,GAAG;AAExB,aAAO,6CAAC,kBAAe,KAAK,OAAO,WAAuB,GAAG,QAAQ;AAAA,IACvE;AAGA,WAAO,6CAAC,iBAAc,KAAK,OAAO,WAAuB,GAAG,QAAQ;AAAA,EACtE;AACF;;;ADhHyC,IAAAC,sBAAA;AARzC,IAAMC,QAAO,MAAM;AAAC;AAQpB,IAAM,QAAQ,CAAC,UAA0B,6CAAC,eAAa,GAAG,OAAO;AAKjE,MAAM,eAAe;AAAA,EACnB,OAAO;AAAA,EACP,SAASA;AAAA,EACT,SAASA;AAAA,EACT,QAAQA;AACV;AAEA,IAAO,gBAAQ;;;AIzBf,IAAAC,SAAuB;;;ALEvB,IAAO,gBAAQ;","names":["React","React","styled","React","import_seeds_react_system_props","import_jsx_runtime","import_jsx_runtime","noop","React"]}
|
package/package.json
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutsocial/seeds-react-image",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.29",
|
|
4
4
|
"description": "Seeds React Image",
|
|
5
5
|
"author": "Sprout Social, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"module": "dist/esm/index.js",
|
|
9
9
|
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/esm/index.js",
|
|
13
|
+
"require": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./dist/image.css": "./dist/image.css"
|
|
17
|
+
},
|
|
10
18
|
"scripts": {
|
|
11
|
-
"build": "tsup --dts",
|
|
12
|
-
"build:debug": "tsup --dts --metafile",
|
|
19
|
+
"build": "tsup --dts && cp src/image.css dist/image.css",
|
|
20
|
+
"build:debug": "tsup --dts --metafile && cp src/image.css dist/image.css",
|
|
13
21
|
"dev": "tsup --watch --dts",
|
|
14
22
|
"clean": "rm -rf .turbo dist",
|
|
15
23
|
"clean:modules": "rm -rf node_modules",
|
package/src/Image.stories.tsx
CHANGED
|
@@ -28,3 +28,18 @@ export const ExampleWithFallback: Story = {
|
|
|
28
28
|
defaultImage: backupImgData,
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Passing Tailwind utility classes via `className` on a bare `<Image>` (no
|
|
34
|
+
* styled-system props) routes to the Tailwind leaf. Because `image.css` wraps
|
|
35
|
+
* its rules in `@layer components`, the unlayered utilities (`mx-200`, `p-300`,
|
|
36
|
+
* `inline-flex`) win the cascade and visibly override the component's own
|
|
37
|
+
* `.seeds-image` styles — proving the `@layer components` fix.
|
|
38
|
+
*/
|
|
39
|
+
export const TailwindOverrides: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
src: "http://static01.nyt.com/images/2015/10/24/arts/24drakeart2/24drakeart2-master675.jpg",
|
|
42
|
+
alt: "Tailwind className overrides applied via @layer components",
|
|
43
|
+
className: "mx-200 p-300 inline-flex",
|
|
44
|
+
},
|
|
45
|
+
};
|
package/src/Image.tsx
CHANGED
|
@@ -1,94 +1,26 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import
|
|
2
|
+
import ImageHybrid from "./ImageHybrid";
|
|
3
3
|
import type { TypeImageProps } from "./ImageTypes";
|
|
4
4
|
|
|
5
|
-
type TypeState = {
|
|
6
|
-
didError: boolean;
|
|
7
|
-
didLoad: boolean;
|
|
8
|
-
imageUrl: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
5
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
12
6
|
const noop = () => {};
|
|
13
7
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// eslint-disable-next-line react/no-did-mount-set-state
|
|
32
|
-
if (this.imageRef) {
|
|
33
|
-
this.setState({
|
|
34
|
-
didLoad: this.imageRef.complete,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
override componentDidUpdate(prevProps: TypeImageProps) {
|
|
40
|
-
if (this.props.src !== prevProps.src) {
|
|
41
|
-
this.setState({
|
|
42
|
-
didError: false,
|
|
43
|
-
didLoad: false,
|
|
44
|
-
imageUrl: this.props.src,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
onError = (e: React.SyntheticEvent<HTMLImageElement, Event>) => {
|
|
50
|
-
if (!this.state.didError && this.props.defaultImage) {
|
|
51
|
-
this.setState({
|
|
52
|
-
didError: true,
|
|
53
|
-
imageUrl: this.props.defaultImage,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
this.props.onError(e);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
onLoad = () => {
|
|
61
|
-
this.setState({
|
|
62
|
-
didLoad: true,
|
|
63
|
-
});
|
|
64
|
-
this.props.onLoad();
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
onClick = () => {
|
|
68
|
-
this.props.onClick();
|
|
69
|
-
};
|
|
8
|
+
/**
|
|
9
|
+
* Image component. Automatically routes between the Tailwind implementation
|
|
10
|
+
* (preferred) and the styled-components implementation (for consumers using
|
|
11
|
+
* system props or a styled() extension). The load/error state lives in
|
|
12
|
+
* ImageHybrid so behavior is identical on both paths.
|
|
13
|
+
*/
|
|
14
|
+
const Image = (props: TypeImageProps) => <ImageHybrid {...props} />;
|
|
15
|
+
|
|
16
|
+
// Preserve the original public type surface: defaultProps keep title/onError/
|
|
17
|
+
// onClick/onLoad optional at the JSX call site (these are required in
|
|
18
|
+
// TypeImageProps). The defaults are applied before reaching ImageHybrid.
|
|
19
|
+
Image.defaultProps = {
|
|
20
|
+
title: "",
|
|
21
|
+
onError: noop,
|
|
22
|
+
onClick: noop,
|
|
23
|
+
onLoad: noop,
|
|
24
|
+
};
|
|
70
25
|
|
|
71
|
-
|
|
72
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
73
|
-
const { alt, title, onClick, onError, onLoad, src, qa, ...rest } =
|
|
74
|
-
this.props;
|
|
75
|
-
return (
|
|
76
|
-
<ImageContainer
|
|
77
|
-
isLoading={!this.state.didLoad}
|
|
78
|
-
ref={(img) => (this.imageRef = img)}
|
|
79
|
-
onClick={this.onClick}
|
|
80
|
-
src={this.state.imageUrl}
|
|
81
|
-
onError={(e: React.SyntheticEvent<HTMLImageElement, Event>) =>
|
|
82
|
-
this.onError(e)
|
|
83
|
-
}
|
|
84
|
-
onLoad={this.onLoad}
|
|
85
|
-
alt={alt}
|
|
86
|
-
title={title}
|
|
87
|
-
data-qa-image={title ? title : alt ? alt : ""}
|
|
88
|
-
data-qa-image-src={src || ""}
|
|
89
|
-
{...(qa as any)}
|
|
90
|
-
{...rest}
|
|
91
|
-
/>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
26
|
+
export default Image;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid Image Component
|
|
3
|
+
* Owns the load/error state + lifecycle in ONE place and switches only the
|
|
4
|
+
* rendered leaf: the styled-components element when styled-system props are
|
|
5
|
+
* present, otherwise the lighter Tailwind <img>.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
import ImageContainer from "./styles"; // Styled-components version
|
|
10
|
+
import { ImageTailwind } from "./ImageTailwind"; // Tailwind version
|
|
11
|
+
import { hasStyledProps } from "@sproutsocial/seeds-react-system-props";
|
|
12
|
+
import type { TypeImageProps } from "./ImageTypes";
|
|
13
|
+
|
|
14
|
+
type TypeState = {
|
|
15
|
+
didError: boolean;
|
|
16
|
+
didLoad: boolean;
|
|
17
|
+
imageUrl: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
21
|
+
const noop = () => {};
|
|
22
|
+
|
|
23
|
+
export default class ImageHybrid extends React.Component<
|
|
24
|
+
TypeImageProps,
|
|
25
|
+
TypeState
|
|
26
|
+
> {
|
|
27
|
+
static defaultProps = {
|
|
28
|
+
title: "",
|
|
29
|
+
onError: noop,
|
|
30
|
+
onClick: noop,
|
|
31
|
+
onLoad: noop,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
override state = {
|
|
35
|
+
didError: false,
|
|
36
|
+
didLoad: false,
|
|
37
|
+
imageUrl: this.props.src,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
imageRef?: HTMLImageElement | null;
|
|
41
|
+
|
|
42
|
+
override componentDidMount() {
|
|
43
|
+
// eslint-disable-next-line react/no-did-mount-set-state
|
|
44
|
+
if (this.imageRef) {
|
|
45
|
+
this.setState({
|
|
46
|
+
didLoad: this.imageRef.complete,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override componentDidUpdate(prevProps: TypeImageProps) {
|
|
52
|
+
if (this.props.src !== prevProps.src) {
|
|
53
|
+
this.setState({
|
|
54
|
+
didError: false,
|
|
55
|
+
didLoad: false,
|
|
56
|
+
imageUrl: this.props.src,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onError = (e: React.SyntheticEvent<HTMLImageElement, Event>) => {
|
|
62
|
+
if (!this.state.didError && this.props.defaultImage) {
|
|
63
|
+
this.setState({
|
|
64
|
+
didError: true,
|
|
65
|
+
imageUrl: this.props.defaultImage,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.props.onError(e);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
onLoad = () => {
|
|
73
|
+
this.setState({
|
|
74
|
+
didLoad: true,
|
|
75
|
+
});
|
|
76
|
+
this.props.onLoad();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
onClick = () => {
|
|
80
|
+
this.props.onClick();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
override render() {
|
|
84
|
+
// defaultImage is consumed by onError (this.props.defaultImage) and must be
|
|
85
|
+
// pulled out of rest so it never reaches the DOM <img> on the Tailwind path
|
|
86
|
+
// (the styled-components leaf filtered it implicitly; a plain <img> does not).
|
|
87
|
+
const {
|
|
88
|
+
alt,
|
|
89
|
+
title,
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
91
|
+
onClick,
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
93
|
+
onError,
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
95
|
+
onLoad,
|
|
96
|
+
src,
|
|
97
|
+
qa,
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
99
|
+
defaultImage,
|
|
100
|
+
...rest
|
|
101
|
+
} = this.props;
|
|
102
|
+
const isLoading = !this.state.didLoad;
|
|
103
|
+
const refCb = (img: HTMLImageElement | null) => (this.imageRef = img);
|
|
104
|
+
const shared = {
|
|
105
|
+
onClick: this.onClick,
|
|
106
|
+
src: this.state.imageUrl,
|
|
107
|
+
onError: (e: React.SyntheticEvent<HTMLImageElement, Event>) =>
|
|
108
|
+
this.onError(e),
|
|
109
|
+
onLoad: this.onLoad,
|
|
110
|
+
alt,
|
|
111
|
+
title,
|
|
112
|
+
"data-qa-image": title ? title : alt ? alt : "",
|
|
113
|
+
"data-qa-image-src": src || "",
|
|
114
|
+
...(qa as any),
|
|
115
|
+
...rest,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (hasStyledProps(rest)) {
|
|
119
|
+
// Legacy styled-components leaf — styles.ts unchanged.
|
|
120
|
+
return <ImageContainer ref={refCb} isLoading={isLoading} {...shared} />;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Tailwind leaf (preferred - lighter, no styled-components runtime).
|
|
124
|
+
return <ImageTailwind ref={refCb} isLoading={isLoading} {...shared} />;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind CSS implementation of Image component
|
|
3
|
+
* Uses Seeds image CSS classes from image.css
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
|
|
8
|
+
// Utility for merging class names properly
|
|
9
|
+
function cn(
|
|
10
|
+
...inputs: (string | undefined | null | false | Record<string, boolean>)[]
|
|
11
|
+
): string {
|
|
12
|
+
const classes: string[] = [];
|
|
13
|
+
|
|
14
|
+
for (const input of inputs) {
|
|
15
|
+
if (!input) continue;
|
|
16
|
+
|
|
17
|
+
if (typeof input === "string") {
|
|
18
|
+
classes.push(input);
|
|
19
|
+
} else if (typeof input === "object") {
|
|
20
|
+
for (const [key, value] of Object.entries(input)) {
|
|
21
|
+
if (value) {
|
|
22
|
+
classes.push(key);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return classes.join(" ");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type TypeImageTailwindProps = React.ComponentPropsWithoutRef<"img"> & {
|
|
32
|
+
isLoading?: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const ImageTailwind = React.forwardRef<
|
|
36
|
+
HTMLImageElement,
|
|
37
|
+
TypeImageTailwindProps
|
|
38
|
+
>(({ isLoading, className, ...rest }, ref) => (
|
|
39
|
+
<img
|
|
40
|
+
ref={ref}
|
|
41
|
+
// Preserve the legacy "Image" marker class (set by styles.ts's .attrs)
|
|
42
|
+
// for any consumer selectors targeting it.
|
|
43
|
+
className={cn(
|
|
44
|
+
"Image",
|
|
45
|
+
"seeds-image",
|
|
46
|
+
{ "seeds-image-loading": !!isLoading },
|
|
47
|
+
className
|
|
48
|
+
)}
|
|
49
|
+
{...rest}
|
|
50
|
+
/>
|
|
51
|
+
));
|
|
52
|
+
|
|
53
|
+
ImageTailwind.displayName = "ImageTailwind";
|
|
@@ -60,6 +60,39 @@ describe("Image", () => {
|
|
|
60
60
|
);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it("should render a plain img with the seeds-image class when no styled props are passed (Tailwind path)", () => {
|
|
64
|
+
render(
|
|
65
|
+
<Image
|
|
66
|
+
alt="image"
|
|
67
|
+
src="http://media3.giphy.com/media/jbxQLpOKN2URa/giphy.gif"
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
const img = screen.getByDataQaLabel({
|
|
71
|
+
"image-src": "http://media3.giphy.com/media/jbxQLpOKN2URa/giphy.gif",
|
|
72
|
+
});
|
|
73
|
+
expect(img.tagName).toBe("IMG");
|
|
74
|
+
expect(img).toHaveClass("seeds-image");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should route to the styled-components leaf when a styled-system prop (width) is passed", () => {
|
|
78
|
+
render(
|
|
79
|
+
<Image
|
|
80
|
+
alt="image"
|
|
81
|
+
src="http://media3.giphy.com/media/jbxQLpOKN2URa/giphy.gif"
|
|
82
|
+
width={1 / 2}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
const img = screen.getByDataQaLabel({
|
|
86
|
+
"image-src": "http://media3.giphy.com/media/jbxQLpOKN2URa/giphy.gif",
|
|
87
|
+
});
|
|
88
|
+
// styled-components emits its generated class, never the static seeds-image class.
|
|
89
|
+
expect(img).not.toHaveClass("seeds-image");
|
|
90
|
+
expect(img).toHaveAttribute(
|
|
91
|
+
"src",
|
|
92
|
+
"http://media3.giphy.com/media/jbxQLpOKN2URa/giphy.gif"
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
63
96
|
it("should update when the src prop changes", () => {
|
|
64
97
|
const src1 = "http://media3.giphy.com/media/jbxQLpOKN2URa/giphy.gif";
|
|
65
98
|
const src2 = "https://media3.giphy.com/media/gYZ7qO81g4dt6/giphy.gif";
|
package/src/css.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*.css";
|
package/src/image.css
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds Image component classes
|
|
3
|
+
* Use these instead of writing out individual Tailwind utility classes.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <img class="seeds-image" />
|
|
7
|
+
* <img class="seeds-image seeds-image-loading" />
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
@layer components {
|
|
11
|
+
.seeds-image {
|
|
12
|
+
display: block;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.seeds-image-loading {
|
|
16
|
+
opacity: 0;
|
|
17
|
+
}
|
|
18
|
+
}
|