@asnewyla/image 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 XD
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @asnewyla/image
2
+
3
+ Image display with org styling — aspect ratio, object-fit, token-based
4
+ rounding, and a load-error fallback.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install @asnewyla/image
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Image } from '@asnewyla/image';
16
+
17
+ <Image src="/hero.jpg" alt="Team at the launch event" />
18
+
19
+ <Image
20
+ src="/avatar.png"
21
+ alt="Jordan's profile picture"
22
+ aspectRatio="1 / 1"
23
+ radius="full"
24
+ fallback="/default-avatar.png"
25
+ />
26
+
27
+ // Decorative image — alt is still required, pass an empty string explicitly
28
+ <Image src="/divider.svg" alt="" />
29
+ ```
30
+
31
+ ### Props
32
+
33
+ | Prop | Type | Default |
34
+ |---|---|---|
35
+ | `src` | `string` | — |
36
+ | `alt` | `string` (**required**) | — |
37
+ | `aspectRatio` | `string` (any CSS `aspect-ratio` value, e.g. `"16 / 9"`) | — |
38
+ | `fit` | `'cover' \| 'contain' \| 'fill' \| 'none' \| 'scale-down'` | `'cover'` |
39
+ | `radius` | `'sm' \| 'md' \| 'lg' \| 'full'` | none (square corners) |
40
+ | `fallback` | `string` (src to swap to if `src` fails to load) | — |
41
+
42
+ `alt` has no default and isn't optional — TypeScript forces you to decide.
43
+ For a genuinely decorative image, pass `alt=""` explicitly rather than
44
+ omitting it.
45
+
46
+ If `fallback` also fails to load, `Image` stops swapping — it doesn't loop.
47
+ If `src` changes to a new value later, the fallback state resets and the new
48
+ `src` gets its own attempt.
49
+
50
+ ### Theming
51
+
52
+ `radius` reads from `@asnewyla/tokens` the same way `@asnewyla/button` does —
53
+ optional import, works standalone with built-in fallbacks:
54
+
55
+ ```ts
56
+ import '@asnewyla/tokens/tokens.css';
57
+ ```
58
+
59
+ ## License
60
+
61
+ MIT
package/dist/index.css ADDED
@@ -0,0 +1,33 @@
1
+ /* src/components/Image.css */
2
+ .xd-image {
3
+ display: block;
4
+ max-width: 100%;
5
+ }
6
+ .xd-image[data-fit=cover] {
7
+ object-fit: cover;
8
+ }
9
+ .xd-image[data-fit=contain] {
10
+ object-fit: contain;
11
+ }
12
+ .xd-image[data-fit=fill] {
13
+ object-fit: fill;
14
+ }
15
+ .xd-image[data-fit=none] {
16
+ object-fit: none;
17
+ }
18
+ .xd-image[data-fit=scale-down] {
19
+ object-fit: scale-down;
20
+ }
21
+ .xd-image[data-radius=sm] {
22
+ border-radius: var(--xd-radius-sm, 0.25rem);
23
+ }
24
+ .xd-image[data-radius=md] {
25
+ border-radius: var(--xd-radius-md, 0.375rem);
26
+ }
27
+ .xd-image[data-radius=lg] {
28
+ border-radius: var(--xd-radius-lg, 0.625rem);
29
+ }
30
+ .xd-image[data-radius=full] {
31
+ border-radius: var(--xd-radius-full, 9999px);
32
+ }
33
+ /*# sourceMappingURL=index.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/Image.css"],"sourcesContent":[".xd-image {\n display: block;\n max-width: 100%;\n}\n\n/* --- Fit --- */\n\n.xd-image[data-fit=\"cover\"] {\n object-fit: cover;\n}\n\n.xd-image[data-fit=\"contain\"] {\n object-fit: contain;\n}\n\n.xd-image[data-fit=\"fill\"] {\n object-fit: fill;\n}\n\n.xd-image[data-fit=\"none\"] {\n object-fit: none;\n}\n\n.xd-image[data-fit=\"scale-down\"] {\n object-fit: scale-down;\n}\n\n/* --- Radius ---\n Fallback values kept in sync with @asnewyla/tokens (soft dependency —\n see PROJECT.md, Tokens dependency model). No data-radius attribute means\n no rounding, matching the native <img> default. */\n\n.xd-image[data-radius=\"sm\"] {\n border-radius: var(--xd-radius-sm, 0.25rem);\n}\n\n.xd-image[data-radius=\"md\"] {\n border-radius: var(--xd-radius-md, 0.375rem);\n}\n\n.xd-image[data-radius=\"lg\"] {\n border-radius: var(--xd-radius-lg, 0.625rem);\n}\n\n.xd-image[data-radius=\"full\"] {\n border-radius: var(--xd-radius-full, 9999px);\n}\n"],"mappings":";AAAA,CAAC;AACC,WAAS;AACT,aAAW;AACb;AAIA,CAPC,QAOQ,CAAC;AACR,cAAY;AACd;AAEA,CAXC,QAWQ,CAAC;AACR,cAAY;AACd;AAEA,CAfC,QAeQ,CAAC;AACR,cAAY;AACd;AAEA,CAnBC,QAmBQ,CAAC;AACR,cAAY;AACd;AAEA,CAvBC,QAuBQ,CAAC;AACR,cAAY;AACd;AAOA,CAhCC,QAgCQ,CAAC;AACR,iBAAe,IAAI,cAAc,EAAE;AACrC;AAEA,CApCC,QAoCQ,CAAC;AACR,iBAAe,IAAI,cAAc,EAAE;AACrC;AAEA,CAxCC,QAwCQ,CAAC;AACR,iBAAe,IAAI,cAAc,EAAE;AACrC;AAEA,CA5CC,QA4CQ,CAAC;AACR,iBAAe,IAAI,gBAAgB,EAAE;AACvC;","names":[]}
@@ -0,0 +1,21 @@
1
+ import * as react from 'react';
2
+ import { ImgHTMLAttributes } from 'react';
3
+
4
+ type ImageFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
5
+ type ImageRadius = 'sm' | 'md' | 'lg' | 'full';
6
+ type ImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, 'alt'> & {
7
+ alt: string;
8
+ aspectRatio?: string;
9
+ fit?: ImageFit;
10
+ radius?: ImageRadius;
11
+ fallback?: string;
12
+ };
13
+ declare const Image: react.ForwardRefExoticComponent<Omit<ImgHTMLAttributes<HTMLImageElement>, "alt"> & {
14
+ alt: string;
15
+ aspectRatio?: string;
16
+ fit?: ImageFit;
17
+ radius?: ImageRadius;
18
+ fallback?: string;
19
+ } & react.RefAttributes<HTMLImageElement>>;
20
+
21
+ export { Image, type ImageFit, type ImageProps, type ImageRadius };
@@ -0,0 +1,21 @@
1
+ import * as react from 'react';
2
+ import { ImgHTMLAttributes } from 'react';
3
+
4
+ type ImageFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
5
+ type ImageRadius = 'sm' | 'md' | 'lg' | 'full';
6
+ type ImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, 'alt'> & {
7
+ alt: string;
8
+ aspectRatio?: string;
9
+ fit?: ImageFit;
10
+ radius?: ImageRadius;
11
+ fallback?: string;
12
+ };
13
+ declare const Image: react.ForwardRefExoticComponent<Omit<ImgHTMLAttributes<HTMLImageElement>, "alt"> & {
14
+ alt: string;
15
+ aspectRatio?: string;
16
+ fit?: ImageFit;
17
+ radius?: ImageRadius;
18
+ fallback?: string;
19
+ } & react.RefAttributes<HTMLImageElement>>;
20
+
21
+ export { Image, type ImageFit, type ImageProps, type ImageRadius };
package/dist/index.js ADDED
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/components/Image.tsx
7
+ function ImageInner({
8
+ alt,
9
+ aspectRatio,
10
+ fit = "cover",
11
+ radius,
12
+ fallback,
13
+ src,
14
+ className,
15
+ style,
16
+ onError,
17
+ ...restProps
18
+ }, ref) {
19
+ const [hasErrored, setHasErrored] = react.useState(false);
20
+ react.useEffect(() => {
21
+ setHasErrored(false);
22
+ }, [src]);
23
+ const resolvedSrc = hasErrored && fallback ? fallback : src;
24
+ const handleError = (e) => {
25
+ setHasErrored(true);
26
+ onError?.(e);
27
+ };
28
+ return /* @__PURE__ */ jsxRuntime.jsx(
29
+ "img",
30
+ {
31
+ ref,
32
+ src: resolvedSrc,
33
+ alt,
34
+ "data-fit": fit,
35
+ "data-radius": radius || void 0,
36
+ className: ["xd-image", className].filter(Boolean).join(" "),
37
+ style: aspectRatio !== void 0 ? { aspectRatio, ...style } : style,
38
+ onError: handleError,
39
+ ...restProps
40
+ }
41
+ );
42
+ }
43
+ var Image = react.forwardRef(ImageInner);
44
+
45
+ exports.Image = Image;
46
+ //# sourceMappingURL=index.js.map
47
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/Image.tsx"],"names":["useState","useEffect","jsx","forwardRef"],"mappings":";;;;;;AAoBA,SAAS,UAAA,CACP;AAAA,EACE,GAAA;AAAA,EACA,WAAA;AAAA,EACA,GAAA,GAAM,OAAA;AAAA,EACN,MAAA;AAAA,EACA,QAAA;AAAA,EACA,GAAA;AAAA,EACA,SAAA;AAAA,EACA,KAAA;AAAA,EACA,OAAA;AAAA,EACA,GAAG;AACL,CAAA,EACA,GAAA,EACA;AACA,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAIA,eAAS,KAAK,CAAA;AAGlD,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,aAAA,CAAc,KAAK,CAAA;AAAA,EACrB,CAAA,EAAG,CAAC,GAAG,CAAC,CAAA;AAER,EAAA,MAAM,WAAA,GAAc,UAAA,IAAc,QAAA,GAAW,QAAA,GAAW,GAAA;AAExD,EAAA,MAAM,WAAA,GAAyD,CAAC,CAAA,KAAM;AACpE,IAAA,aAAA,CAAc,IAAI,CAAA;AAClB,IAAA,OAAA,GAAU,CAAC,CAAA;AAAA,EACb,CAAA;AAEA,EAAA,uBACEC,cAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,GAAA;AAAA,MACA,GAAA,EAAK,WAAA;AAAA,MACL,GAAA;AAAA,MACA,UAAA,EAAU,GAAA;AAAA,MACV,eAAa,MAAA,IAAU,MAAA;AAAA,MACvB,SAAA,EAAW,CAAC,UAAA,EAAY,SAAS,EAAE,MAAA,CAAO,OAAO,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAAA,MAC3D,OAAO,WAAA,KAAgB,MAAA,GAAY,EAAE,WAAA,EAAa,GAAG,OAAM,GAAI,KAAA;AAAA,MAC/D,OAAA,EAAS,WAAA;AAAA,MACR,GAAG;AAAA;AAAA,GACN;AAEJ;AAEO,IAAM,KAAA,GAAQC,iBAAW,UAAU","file":"index.js","sourcesContent":["import {\n type ForwardedRef,\n type ImgHTMLAttributes,\n forwardRef,\n useEffect,\n useState,\n} from 'react';\nimport './Image.css';\n\nexport type ImageFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';\nexport type ImageRadius = 'sm' | 'md' | 'lg' | 'full';\n\nexport type ImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, 'alt'> & {\n alt: string;\n aspectRatio?: string;\n fit?: ImageFit;\n radius?: ImageRadius;\n fallback?: string;\n};\n\nfunction ImageInner(\n {\n alt,\n aspectRatio,\n fit = 'cover',\n radius,\n fallback,\n src,\n className,\n style,\n onError,\n ...restProps\n }: ImageProps,\n ref: ForwardedRef<HTMLImageElement>,\n) {\n const [hasErrored, setHasErrored] = useState(false);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: src triggers the reset intentionally, it isn't read in the body\n useEffect(() => {\n setHasErrored(false);\n }, [src]);\n\n const resolvedSrc = hasErrored && fallback ? fallback : src;\n\n const handleError: React.ReactEventHandler<HTMLImageElement> = (e) => {\n setHasErrored(true);\n onError?.(e);\n };\n\n return (\n <img\n ref={ref}\n src={resolvedSrc}\n alt={alt}\n data-fit={fit}\n data-radius={radius || undefined}\n className={['xd-image', className].filter(Boolean).join(' ')}\n style={aspectRatio !== undefined ? { aspectRatio, ...style } : style}\n onError={handleError}\n {...restProps}\n />\n );\n}\n\nexport const Image = forwardRef(ImageInner);\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,45 @@
1
+ import { forwardRef, useState, useEffect } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ // src/components/Image.tsx
5
+ function ImageInner({
6
+ alt,
7
+ aspectRatio,
8
+ fit = "cover",
9
+ radius,
10
+ fallback,
11
+ src,
12
+ className,
13
+ style,
14
+ onError,
15
+ ...restProps
16
+ }, ref) {
17
+ const [hasErrored, setHasErrored] = useState(false);
18
+ useEffect(() => {
19
+ setHasErrored(false);
20
+ }, [src]);
21
+ const resolvedSrc = hasErrored && fallback ? fallback : src;
22
+ const handleError = (e) => {
23
+ setHasErrored(true);
24
+ onError?.(e);
25
+ };
26
+ return /* @__PURE__ */ jsx(
27
+ "img",
28
+ {
29
+ ref,
30
+ src: resolvedSrc,
31
+ alt,
32
+ "data-fit": fit,
33
+ "data-radius": radius || void 0,
34
+ className: ["xd-image", className].filter(Boolean).join(" "),
35
+ style: aspectRatio !== void 0 ? { aspectRatio, ...style } : style,
36
+ onError: handleError,
37
+ ...restProps
38
+ }
39
+ );
40
+ }
41
+ var Image = forwardRef(ImageInner);
42
+
43
+ export { Image };
44
+ //# sourceMappingURL=index.mjs.map
45
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/Image.tsx"],"names":[],"mappings":";;;;AAoBA,SAAS,UAAA,CACP;AAAA,EACE,GAAA;AAAA,EACA,WAAA;AAAA,EACA,GAAA,GAAM,OAAA;AAAA,EACN,MAAA;AAAA,EACA,QAAA;AAAA,EACA,GAAA;AAAA,EACA,SAAA;AAAA,EACA,KAAA;AAAA,EACA,OAAA;AAAA,EACA,GAAG;AACL,CAAA,EACA,GAAA,EACA;AACA,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAI,SAAS,KAAK,CAAA;AAGlD,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,aAAA,CAAc,KAAK,CAAA;AAAA,EACrB,CAAA,EAAG,CAAC,GAAG,CAAC,CAAA;AAER,EAAA,MAAM,WAAA,GAAc,UAAA,IAAc,QAAA,GAAW,QAAA,GAAW,GAAA;AAExD,EAAA,MAAM,WAAA,GAAyD,CAAC,CAAA,KAAM;AACpE,IAAA,aAAA,CAAc,IAAI,CAAA;AAClB,IAAA,OAAA,GAAU,CAAC,CAAA;AAAA,EACb,CAAA;AAEA,EAAA,uBACE,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,GAAA;AAAA,MACA,GAAA,EAAK,WAAA;AAAA,MACL,GAAA;AAAA,MACA,UAAA,EAAU,GAAA;AAAA,MACV,eAAa,MAAA,IAAU,MAAA;AAAA,MACvB,SAAA,EAAW,CAAC,UAAA,EAAY,SAAS,EAAE,MAAA,CAAO,OAAO,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAAA,MAC3D,OAAO,WAAA,KAAgB,MAAA,GAAY,EAAE,WAAA,EAAa,GAAG,OAAM,GAAI,KAAA;AAAA,MAC/D,OAAA,EAAS,WAAA;AAAA,MACR,GAAG;AAAA;AAAA,GACN;AAEJ;AAEO,IAAM,KAAA,GAAQ,WAAW,UAAU","file":"index.mjs","sourcesContent":["import {\n type ForwardedRef,\n type ImgHTMLAttributes,\n forwardRef,\n useEffect,\n useState,\n} from 'react';\nimport './Image.css';\n\nexport type ImageFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';\nexport type ImageRadius = 'sm' | 'md' | 'lg' | 'full';\n\nexport type ImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, 'alt'> & {\n alt: string;\n aspectRatio?: string;\n fit?: ImageFit;\n radius?: ImageRadius;\n fallback?: string;\n};\n\nfunction ImageInner(\n {\n alt,\n aspectRatio,\n fit = 'cover',\n radius,\n fallback,\n src,\n className,\n style,\n onError,\n ...restProps\n }: ImageProps,\n ref: ForwardedRef<HTMLImageElement>,\n) {\n const [hasErrored, setHasErrored] = useState(false);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: src triggers the reset intentionally, it isn't read in the body\n useEffect(() => {\n setHasErrored(false);\n }, [src]);\n\n const resolvedSrc = hasErrored && fallback ? fallback : src;\n\n const handleError: React.ReactEventHandler<HTMLImageElement> = (e) => {\n setHasErrored(true);\n onError?.(e);\n };\n\n return (\n <img\n ref={ref}\n src={resolvedSrc}\n alt={alt}\n data-fit={fit}\n data-radius={radius || undefined}\n className={['xd-image', className].filter(Boolean).join(' ')}\n style={aspectRatio !== undefined ? { aspectRatio, ...style } : style}\n onError={handleError}\n {...restProps}\n />\n );\n}\n\nexport const Image = forwardRef(ImageInner);\n"]}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@asnewyla/image",
3
+ "version": "0.2.0",
4
+ "description": "Image display with org styling, aspect ratio, and load-error fallback",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/xavierDelaFuente/xd-components.git",
9
+ "directory": "packages/image"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "main": "dist/index.js",
15
+ "module": "dist/index.mjs",
16
+ "types": "dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.mjs",
20
+ "require": "./dist/index.js",
21
+ "types": "./dist/index.d.ts"
22
+ },
23
+ "./styles.css": "./dist/index.css"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "sideEffects": [
29
+ "*.css"
30
+ ],
31
+ "keywords": [
32
+ "react",
33
+ "image",
34
+ "accessible"
35
+ ],
36
+ "peerDependencies": {
37
+ "react": "^18.0.0 || ^19.0.0",
38
+ "react-dom": "^18.0.0 || ^19.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@testing-library/jest-dom": "^6.4.0",
42
+ "@testing-library/react": "^16.0.0",
43
+ "@testing-library/user-event": "^14.5.0",
44
+ "@vitest/coverage-v8": "^2.0.0",
45
+ "jsdom": "^24.0.0",
46
+ "react": "^18.3.0",
47
+ "react-dom": "^18.3.0",
48
+ "tsup": "^8.1.0",
49
+ "typescript": "^5.5.0",
50
+ "vitest": "^2.0.0"
51
+ },
52
+ "scripts": {
53
+ "build": "tsup",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "test:coverage": "vitest run --coverage",
57
+ "type-check": "tsc --noEmit"
58
+ }
59
+ }