diff --git a/packages/chusho/src/components/CPicture/CPicture.spec.js b/packages/chusho/src/components/CPicture/CPicture.spec.js
new file mode 100644
index 00000000..115fe915
--- /dev/null
+++ b/packages/chusho/src/components/CPicture/CPicture.spec.js
@@ -0,0 +1,78 @@
+import { mount } from '@vue/test-utils';
+import CPicture from './CPicture';
+
+describe('CPicture', () => {
+ it('renders with the right attributes', () => {
+ const wrapper = mount(CPicture, {
+ props: {
+ src: '/image.jpg',
+ alt: 'alt',
+ },
+ });
+
+ expect(wrapper.html()).toBe(
+ ''
+ );
+ });
+
+ it('renders an empty alt by default', () => {
+ const wrapper = mount(CPicture, {
+ props: {
+ src: '/image.jpg',
+ },
+ });
+
+ expect(wrapper.html()).toBe(
+ ''
+ );
+ });
+
+ it('apply config class on img tag', () => {
+ const wrapper = mount(CPicture, {
+ global: {
+ provide: {
+ $chusho: {
+ options: {
+ components: {
+ picture: {
+ class: 'picture',
+ },
+ },
+ },
+ },
+ },
+ },
+ props: {
+ src: '/image.jpg',
+ class: 'special-picture',
+ },
+ });
+
+ expect(wrapper.find('img').classes()).toEqual([
+ 'special-picture',
+ 'picture',
+ ]);
+ });
+
+ it('renders given sources', () => {
+ const wrapper = mount(CPicture, {
+ props: {
+ src: '/image.jpg',
+ sources: [
+ {
+ srcset: 'image@2x.webp 2x, image.webp',
+ type: 'image/webp',
+ },
+ {
+ srcset: 'image@2x.jpg 2x, image.jpg',
+ type: 'image/jpeg',
+ },
+ ],
+ },
+ });
+
+ expect(wrapper.html()).toBe(
+ ''
+ );
+ });
+});
diff --git a/packages/chusho/src/components/CPicture/CPicture.ts b/packages/chusho/src/components/CPicture/CPicture.ts
new file mode 100644
index 00000000..7647dc12
--- /dev/null
+++ b/packages/chusho/src/components/CPicture/CPicture.ts
@@ -0,0 +1,57 @@
+import {
+ defineComponent,
+ h,
+ inject,
+ mergeProps,
+ PropType,
+ SourceHTMLAttributes,
+} from 'vue';
+
+import { DollarChusho } from '../../types';
+import { generateConfigClass } from '../../utils/components';
+import componentMixin from '../mixins/componentMixin';
+
+export default defineComponent({
+ name: 'CPicture',
+
+ mixins: [componentMixin],
+
+ inheritAttrs: false,
+
+ props: {
+ /**
+ * Default/fallback image URL used in the `src` attribute.
+ */
+ src: {
+ type: String,
+ required: true,
+ },
+ /**
+ * Alternative text description; leave empty if the image is not a key part of the content, otherwise describe what can be seen.
+ */
+ alt: {
+ type: String,
+ default: '',
+ },
+ /**
+ * Generate multiple `source` elements with the given attributes.
+ */
+ sources: {
+ type: Array as PropType,
+ default: () => [],
+ },
+ },
+
+ render() {
+ const pictureConfig = inject('$chusho', null)?.options
+ ?.components?.picture;
+ const elementProps: Record = mergeProps(this.$attrs, {
+ src: this.$props.src,
+ alt: this.$props.alt,
+ ...generateConfigClass(pictureConfig?.class, this.$props),
+ });
+ const sources = this.$props.sources.map((source) => h('source', source));
+
+ return h('picture', null, [...sources, h('img', elementProps)]);
+ },
+});
diff --git a/packages/chusho/src/components/CPicture/index.ts b/packages/chusho/src/components/CPicture/index.ts
new file mode 100644
index 00000000..7fc2f8d1
--- /dev/null
+++ b/packages/chusho/src/components/CPicture/index.ts
@@ -0,0 +1,3 @@
+import CPicture from './CPicture';
+
+export { CPicture };
diff --git a/packages/chusho/src/components/index.ts b/packages/chusho/src/components/index.ts
index 9059ac70..5d500548 100644
--- a/packages/chusho/src/components/index.ts
+++ b/packages/chusho/src/components/index.ts
@@ -4,3 +4,4 @@ export * from './CCollapse';
export * from './CTabs';
export * from './CDialog';
export * from './CAlert';
+export * from './CPicture';
diff --git a/packages/chusho/src/types/index.ts b/packages/chusho/src/types/index.ts
index d1fad563..27acd423 100644
--- a/packages/chusho/src/types/index.ts
+++ b/packages/chusho/src/types/index.ts
@@ -57,6 +57,9 @@ interface ComponentsOptions {
collapseContent?: ComponentCommonOptions & {
transition?: BaseTransitionProps;
};
+ picture?: {
+ class?: VueClassBinding | ClassGenerator;
+ };
}
export interface ChushoOptions {
diff --git a/packages/docs/.vuepress/config.js b/packages/docs/.vuepress/config.js
index 9bffe587..f03fc3db 100644
--- a/packages/docs/.vuepress/config.js
+++ b/packages/docs/.vuepress/config.js
@@ -41,6 +41,7 @@ module.exports = {
'components/button.md',
'components/dialog.md',
'components/icon.md',
+ 'components/picture.md',
'components/tabs.md',
'components/collapse.md',
],
diff --git a/packages/docs/guide/components/picture.md b/packages/docs/guide/components/picture.md
new file mode 100644
index 00000000..cf384e70
--- /dev/null
+++ b/packages/docs/guide/components/picture.md
@@ -0,0 +1,56 @@
+# Picture
+
+Easily generate responsive images.
+
+## Config
+
+### class
+
+Classes applied to the component `img` element, except when the prop `bare` is set to `true`. See [styling components](/guide/styling-components/).
+
+- **type:** `Array | Object | String | (props: Object) => {}`
+- **default:** `null`
+
+#### Example
+
+```js
+class: 'img-responsive'
+```
+
+## API
+
+
+
+## Examples
+
+### Simplest
+
+```vue
+
+```
+
+### With sources
+
+```vue
+
+```
+
+### With additional attributes
+
+Attributes are not applied to the `picture` element but to the `img` element.
+
+```vue
+
+```
diff --git a/packages/playground/chusho.config.js b/packages/playground/chusho.config.js
index 3e9f42ff..222a936f 100644
--- a/packages/playground/chusho.config.js
+++ b/packages/playground/chusho.config.js
@@ -33,6 +33,9 @@ export default {
height: 48,
class: 'inline-block align-middle pointer-events-none fill-current',
},
+ picture: {
+ class: 'picture',
+ },
tabs: {
class: 'tabs',
},
diff --git a/packages/playground/src/assets/images/building.jpg b/packages/playground/src/assets/images/building.jpg
new file mode 100644
index 00000000..3b2c9355
Binary files /dev/null and b/packages/playground/src/assets/images/building.jpg differ
diff --git a/packages/playground/src/assets/images/building.webp b/packages/playground/src/assets/images/building.webp
new file mode 100644
index 00000000..bc61a4ec
Binary files /dev/null and b/packages/playground/src/assets/images/building.webp differ
diff --git a/packages/playground/src/assets/images/building@2x.jpg b/packages/playground/src/assets/images/building@2x.jpg
new file mode 100644
index 00000000..6b7de319
Binary files /dev/null and b/packages/playground/src/assets/images/building@2x.jpg differ
diff --git a/packages/playground/src/assets/images/building@2x.webp b/packages/playground/src/assets/images/building@2x.webp
new file mode 100644
index 00000000..2ebe9385
Binary files /dev/null and b/packages/playground/src/assets/images/building@2x.webp differ
diff --git a/packages/playground/src/components/examples/components/picture/Default.vue b/packages/playground/src/components/examples/components/picture/Default.vue
new file mode 100644
index 00000000..afa1fd9f
--- /dev/null
+++ b/packages/playground/src/components/examples/components/picture/Default.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/packages/playground/src/components/examples/components/picture/WithSources.vue b/packages/playground/src/components/examples/components/picture/WithSources.vue
new file mode 100644
index 00000000..32af3c65
--- /dev/null
+++ b/packages/playground/src/components/examples/components/picture/WithSources.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
diff --git a/packages/playground/src/components/examples/routes.json b/packages/playground/src/components/examples/routes.json
index 83d76e8a..79e41b15 100644
--- a/packages/playground/src/components/examples/routes.json
+++ b/packages/playground/src/components/examples/routes.json
@@ -131,6 +131,19 @@
"component": "Controlled"
}
]
+ },
+ "picture": {
+ "label": "Picture",
+ "variants": [
+ {
+ "label": "Default",
+ "component": "Default"
+ },
+ {
+ "label": "With Sources",
+ "component": "WithSources"
+ }
+ ]
}
}
},