Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions app/components/demo/PolaroidCardDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script setup lang="ts">
import { PolaroidCard, type PolaroidCardImage } from "~/registry/blocks/polaroid-card";

const images: Array<{
image: PolaroidCardImage;
caption?: string;
rotation: "left" | "right" | "none";
interactive?: boolean;
}> = [
{
image: {
src: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=320&q=80",
alt: "Cabin beside a lake",
},
caption: "Weekend cabin",
rotation: "left",
},
{
image: {
src: "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?auto=format&fit=crop&w=320&q=80",
alt: "Desert road at sunset",
},
rotation: "right",
},
{
image: {
src: "https://images.unsplash.com/photo-1495567720989-cebdbdd97913?auto=format&fit=crop&w=320&q=80",
alt: "Mountain ridge above clouds",
},
caption: "Static layout",
rotation: "none",
interactive: false,
},
];
</script>

<template>
<div class="overflow-hidden rounded-xl border bg-muted/30 p-8">
<div class="mx-auto flex max-w-2xl flex-col items-center gap-8 sm:flex-row sm:justify-center sm:gap-6">
<PolaroidCard
v-for="item in images"
:key="item.image.src"
:image="item.image"
:caption="item.caption"
:rotation="item.rotation"
:interactive="item.interactive"
/>
</div>
</div>
</template>
66 changes: 66 additions & 0 deletions app/registry/blocks/polaroid-card/PolaroidCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { computed } from "vue";
import { cn } from "@/lib/utils";

export interface PolaroidCardImage {
src: string;
alt: string;
}

export interface PolaroidCardProps {
/** Image source and accessible alt text supplied by the consuming app */
image: PolaroidCardImage;
/** Optional handwritten-style caption. Falls back to image.alt when omitted. */
caption?: string;
/** Fixed card rotation for standalone placement */
rotation?: "left" | "right" | "none";
/** Enable hover lift and straightening interaction */
interactive?: boolean;
/** Additional CSS classes for the root card */
class?: HTMLAttributes["class"];
}

const props = withDefaults(defineProps<PolaroidCardProps>(), {
rotation: "left",
interactive: true,
});

const captionText = computed(() => props.caption ?? props.image.alt);

const rotationClass = computed(() => {
if (props.rotation === "right") return "rotate-5";
if (props.rotation === "none") return "rotate-0";
return "-rotate-5";
});

const interactiveClass = computed(() => {
if (!props.interactive) return "";
if (props.rotation === "right") return "hover:z-10 hover:translate-x-4 hover:scale-105 hover:rotate-0";
if (props.rotation === "left") return "hover:z-10 hover:-translate-x-4 hover:scale-105 hover:rotate-0";
return "hover:z-10 hover:scale-105";
});
</script>

<template>
<figure
data-slot="polaroid-card"
:data-rotation="rotation"
:data-interactive="interactive"
:class="cn(
'inline-flex flex-col bg-white p-2 text-black drop-shadow-2xl transition-transform duration-300 ease-in-out',
rotationClass,
interactiveClass,
props.class,
)"
>
<img
:src="image.src"
:alt="image.alt"
class="aspect-square size-32 object-cover"
>
<figcaption class="mt-2 w-32 text-center font-serif text-xs font-medium text-black">
{{ captionText }}
</figcaption>
</figure>
</template>
1 change: 1 addition & 0 deletions app/registry/blocks/polaroid-card/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as PolaroidCard, type PolaroidCardImage, type PolaroidCardProps } from "./PolaroidCard.vue";
87 changes: 87 additions & 0 deletions content/docs/2.components/10.polaroid-card.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
title: PolaroidCard
description: Render a tilted photo card with app-owned image data and explicit presentation controls.
category: content
---

::component-preview
---
name: PolaroidCardDemo
---
::

## Installation

```bash
npx shadcn-vue@latest add "https://ui.stackhacker.io/r/polaroid-card.json"
```

## Usage

```vue
<script setup lang="ts">
import { PolaroidCard, type PolaroidCardImage } from "@/components/polaroid-card";

const image: PolaroidCardImage = {
src: "/images/studio-desk.jpg",
alt: "Studio desk with open notebooks",
};
</script>

<template>
<PolaroidCard
:image="image"
caption="Studio notes"
rotation="right"
/>
</template>
```

## App-Owned Media

`PolaroidCard` owns the photo-paper styling, rotation, caption rendering, and optional hover movement. Your app owns image storage, image optimization, alt text, captions, ordering, links, lightboxes, and gallery layout.

When `caption` is omitted, the component renders `image.alt` as the visible caption. Pass an explicit caption when the display label should differ from the accessible image description.

Use `:interactive="false"` when the card appears in a dense static layout or inside another interactive surface.

## Examples

### Default

::component-preview
---
name: PolaroidCardDemo
---
::

### Static Card

```vue
<PolaroidCard
:image="image"
rotation="none"
:interactive="false"
/>
```

## API Reference

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `image` | `PolaroidCardImage` | — | Image source and accessible alt text supplied by your app. |
| `caption` | `string` | `image.alt` | Optional visible caption. Falls back to the image alt text when omitted. |
| `rotation` | `"left" \| "right" \| "none"` | `"left"` | Fixed card rotation for standalone placement. |
| `interactive` | `boolean` | `true` | Enables hover lift, translation, scale, and straightening. |
| `class` | `HTMLAttributes["class"]` | — | Additional CSS classes for the root card. |

### Types

```ts
interface PolaroidCardImage {
src: string;
alt: string;
}
```
4 changes: 4 additions & 0 deletions scripts/registry-verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ const expectedItems: Record<string, ExpectedRegistryItem> = {
dependencies: [],
registryDependencies: [],
},
"polaroid-card": {
dependencies: [],
registryDependencies: [],
},
"docs-pager": {
dependencies: ["@lucide/vue"],
registryDependencies: [],
Expand Down
Loading