Cart line item
One product in a cart: picture, name, chosen variant, quantity, line total and a remove button.
Install
If this is the first component in the project, set up once. This writes ermine.json and copies the theme stylesheet. The installation guide covers Vue, Nuxt and Astro.
npx ermineui initpnpm dlx ermineui inityarn dlx ermineui initbunx ermineui initThen add the component:
npx ermineui add cart-line-itempnpm dlx ermineui add cart-line-itemyarn dlx ermineui add cart-line-itembunx ermineui add cart-line-itemThat copies 2 files into src/components/ui/cart-line-item/, and the 2 components it is built from. Files you already have are kept.
Use
import { CartLineItem } from '@/components/ui/cart-line-item'<RadioGroup v-model="delivery" aria-label="Delivery">
<OptionCard value="standard" title="Standard" description="3 to 5 working days"><template #end>Free</template></OptionCard>
<OptionCard value="express" title="Express" description="1 to 2 working days"><template #end>$12.00</template></OptionCard>
</RadioGroup>
<CartLineItem v-model:quantity="item.quantity" :title="item.title" :image="item.image" options="Sand / 350 ml" :price="24" :max="6" @remove="remove(item)" />
<OrderSummary :subtotal="144" :discount="10" discount-label="SAVE10" :shipping="0" :tax="11.52">
<template #footer><Button size="lg">Checkout</Button></template>
</OrderSummary>In Nuxt, with the auto-import setting, the tags work in templates without the import. In Astro, use it inside a Vue island.
What gets copied
| File | Where it goes |
|---|---|
CartLineItem.vue | src/components/ui/cart-line-item/CartLineItem.vue |
index.ts | src/components/ui/cart-line-item/index.ts |
It is built from: Number input, Price. These are added with it.
The folders are the suggested ones. ermine.json says where yours are.
Source
This is exactly what the command copies. To install by hand, create each file below and the helpers listed above.
CartLineItem.vue 193 lines
<script setup lang="ts">
import type { Component } from 'vue'
import { NumberInput } from '../number-input'
import { formatMoney, Price } from '../price'
withDefaults(
defineProps<{
title: string
/** Where the product's own page is. */
href?: string
image?: string
/** Leave empty when the picture shows nothing the title does not already say. */
imageAlt?: string
/** The variant that was chosen, e.g. "Sand / Large". */
options?: string
/** Price of one. Amounts are in whole units of the currency. */
price: number
/** The price of one before a reduction. */
compareAt?: number
currency?: string
locale?: string
/** The most that can be bought, e.g. what is left in stock. */
max?: number
/** Show the quantity as text with nothing to change: for an order that is being paid for, or already placed. */
readonly?: boolean
/** Tag or component for the link, e.g. NuxtLink. */
linkAs?: string | Component
}>(),
{ imageAlt: '', currency: 'USD', locale: 'en-US', linkAs: 'a' },
)
defineEmits<{
/** The "Remove" button was pressed. Take the item out of your cart. */
remove: []
}>()
/** How many of this item. */
const quantity = defineModel<number>('quantity', { default: 1 })
/** An emptied field counts as one until a number is typed, rather than as nothing. */
function setQuantity(next: number | undefined) {
quantity.value = next === undefined ? 1 : Math.max(1, Math.round(next))
}
</script>
<template>
<div class="er-cart-line-item" :data-readonly="readonly ? '' : undefined">
<div v-if="image" class="er-cart-line-item-media"><img :src="image" :alt="imageAlt" loading="lazy" /></div>
<div class="er-cart-line-item-main">
<div class="er-cart-line-item-title">
<component :is="linkAs" v-if="href" :href="href">{{ title }}</component>
<template v-else>{{ title }}</template>
</div>
<p v-if="options" class="er-cart-line-item-options">{{ options }}</p>
<Price :amount="price" :compare-at="compareAt" :currency="currency" :locale="locale" size="sm" :unit="quantity > 1 ? 'each' : undefined" />
<!-- A note about this item: "Only 2 left", "Arrives Friday". -->
<slot />
</div>
<div class="er-cart-line-item-end">
<strong class="er-cart-line-item-total">{{ formatMoney(price * quantity, currency, locale) }}</strong>
<p v-if="readonly" class="er-cart-line-item-options">Qty {{ quantity }}</p>
<template v-else>
<NumberInput :model-value="quantity" :min="1" :max="max" :aria-label="`Quantity of ${title}`" @update:model-value="setQuantity" />
<button type="button" class="er-cart-line-item-remove" @click="$emit('remove')">Remove<span class="er-cart-line-item-hidden"> {{ title }}</span></button>
</template>
</div>
</div>
</template>
<style>
@layer components {
.er-cart-line-item {
display: flex;
gap: 0.75rem 1rem;
min-width: 0;
}
.er-cart-line-item-media {
flex-shrink: 0;
width: 4.5rem;
height: 4.5rem;
border: 1px solid var(--er-border);
border-radius: var(--er-radius-md);
background: var(--er-muted);
overflow: hidden;
}
.er-cart-line-item[data-readonly] .er-cart-line-item-media {
width: 3.5rem;
height: 3.5rem;
}
.er-cart-line-item-media img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.er-cart-line-item-main {
display: grid;
flex: 1;
align-content: start;
gap: 0.125rem;
min-width: 0;
}
.er-cart-line-item-title {
font-size: 0.875rem;
font-weight: var(--er-weight-medium);
line-height: 1.25rem;
}
.er-cart-line-item-title a {
color: inherit;
text-decoration: none;
}
.er-cart-line-item-title a:hover {
text-decoration: underline;
text-underline-offset: 0.2em;
}
.er-cart-line-item-title a:focus-visible {
border-radius: var(--er-radius-sm);
outline: 2px solid var(--er-ring);
outline-offset: 2px;
}
.er-cart-line-item-options {
margin: 0;
color: var(--er-muted-foreground);
font-size: 0.8125rem;
line-height: 1.25rem;
}
.er-cart-line-item-end {
display: grid;
flex-shrink: 0;
align-content: start;
justify-items: end;
gap: 0.375rem;
}
.er-cart-line-item-total {
font-size: 0.875rem;
font-weight: var(--er-weight-semibold);
font-variant-numeric: tabular-nums;
line-height: 1.25rem;
}
.er-cart-line-item-end .er-number-input {
width: 6.5rem;
height: var(--er-control-sm);
}
.er-cart-line-item-end .er-number-input button {
width: var(--er-control-sm);
}
.er-cart-line-item-remove {
padding: 0;
border: 0;
border-radius: var(--er-radius-sm);
background: transparent;
color: var(--er-muted-foreground);
font: inherit;
font-size: 0.8125rem;
text-decoration: underline;
text-underline-offset: 0.2em;
cursor: pointer;
}
.er-cart-line-item-remove:hover {
color: var(--er-foreground);
}
.er-cart-line-item-remove:focus-visible {
outline: 2px solid var(--er-ring);
outline-offset: 2px;
}
.er-cart-line-item-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
}
</style>index.ts 3 lines
/** Cart line item — one product in a cart: picture, name, chosen variant, quantity, line total and a remove button. */
export { default as CartLineItem } from './CartLineItem.vue'