Compare commits
10 Commits
aacd5b890f
...
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| d8d152e249 | |||
| 75ea9b7416 | |||
| cd36fc5723 | |||
| 3987a4e43e | |||
| 5ca66f3ed7 | |||
| c36d146fef | |||
| 3ac68a88d0 | |||
| 94b02198dd | |||
| b6c769eecc | |||
| bf8fef9190 |
+1
-1
@@ -1,5 +1,5 @@
|
||||
# SQLite database file (mounted on a persistent volume in Docker)
|
||||
DATABASE_URL="file:/app/data/askedout.db"
|
||||
DATABASE_URL="file:/app/data/lovelope.db"
|
||||
|
||||
# Public base URL (used in QR codes and share links)
|
||||
APP_URL="http://localhost:3000"
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
name: Publish Docker image
|
||||
|
||||
# Two channels, not one-image-per-commit:
|
||||
# - push to `dev` -> rolling `:dev` tag, for testing in-progress work
|
||||
# - push tag `v*` -> `:latest` + the version tag, for actual releases
|
||||
# Plain commits/merges to master/main build nothing on their own.
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
branches: [dev]
|
||||
tags: ['v*']
|
||||
|
||||
permissions:
|
||||
@@ -20,6 +24,24 @@ jobs:
|
||||
- name: Compute lowercase image name
|
||||
run: echo "IMAGE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Compute image tags
|
||||
run: |
|
||||
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||
VERSION="${{ github.ref_name }}"
|
||||
TAGS="${{ vars.REGISTRY_HOST }}/${IMAGE_NAME}:latest
|
||||
${{ vars.REGISTRY_HOST }}/${IMAGE_NAME}:${VERSION}
|
||||
ghcr.io/${IMAGE_NAME}:latest
|
||||
ghcr.io/${IMAGE_NAME}:${VERSION}"
|
||||
else
|
||||
TAGS="${{ vars.REGISTRY_HOST }}/${IMAGE_NAME}:dev
|
||||
ghcr.io/${IMAGE_NAME}:dev"
|
||||
fi
|
||||
{
|
||||
echo "TAGS<<TAGS_EOF"
|
||||
echo "$TAGS"
|
||||
echo "TAGS_EOF"
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: Log in to Gitea container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -27,11 +49,16 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
${{ vars.REGISTRY_HOST }}/${{ env.IMAGE_NAME }}:latest
|
||||
${{ vars.REGISTRY_HOST }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
tags: ${{ env.TAGS }}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
tags: ['v*']
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Compute lowercase image name
|
||||
run: echo "IMAGE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ env.IMAGE_NAME }}:latest
|
||||
ghcr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
+4
-4
@@ -28,7 +28,7 @@ RUN npm run build
|
||||
|
||||
# ── Stage 3: runner ────────────────────────────────────────────────────────────
|
||||
FROM node:20-alpine AS runner
|
||||
RUN apk add --no-cache libc6-compat openssl
|
||||
RUN apk add --no-cache libc6-compat openssl su-exec
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
@@ -48,11 +48,11 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
RUN mkdir -p /app/data && chown nextjs:nodejs /app/data
|
||||
|
||||
USER nextjs
|
||||
COPY --chmod=755 docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
|
||||
EXPOSE 3000
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
# Run migrations then start the server
|
||||
CMD ["sh", "-c", "npx prisma migrate deploy && node server.js"]
|
||||
# Fix ownership of the bind-mounted /app/data volume, then drop to nextjs
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
|
||||
+61
-3
@@ -46,10 +46,68 @@ Types: `feat`, `fix`, `refactor`, `style`, `docs`, `chore`, `test`.
|
||||
- Never force-push `main`. Force-pushing a feature branch you own (after a
|
||||
rebase) is fine; never force-push a branch someone else might be using.
|
||||
|
||||
## Tags / releases
|
||||
## Releases & Docker images
|
||||
|
||||
Tag production releases as `v<major>.<minor>.<patch>` (semver) once this
|
||||
project has a deployment cadence worth marking. Not required for every merge.
|
||||
CI (`.gitea/workflows/docker-publish.yml`) never builds an image from a
|
||||
plain commit or merge to `main`/`master` — only from two deliberate
|
||||
triggers, each producing tags on both the Gitea registry and GHCR:
|
||||
|
||||
| Trigger | Produces | Purpose |
|
||||
|------------------|--------------------------|-------------------------------------|
|
||||
| push to `dev` | `:dev` | test a build before releasing |
|
||||
| push a `v*` tag | `:latest` + `:vX.Y.Z` | ship an actual release |
|
||||
|
||||
### Testing a build (`dev`)
|
||||
|
||||
`dev` is a disposable pointer, not a branch with history of its own — it
|
||||
should always match `main`/`master` exactly. Bring it up to date and push
|
||||
whenever you want a fresh test image:
|
||||
|
||||
```
|
||||
git checkout dev
|
||||
git merge main --ff-only
|
||||
git push origin dev
|
||||
```
|
||||
|
||||
If `dev` has drifted and won't fast-forward (e.g. after a rebase, or a
|
||||
commit landed only on `dev` by mistake), reset it instead of merging —
|
||||
`dev` has no unique history worth preserving:
|
||||
|
||||
```
|
||||
git checkout dev
|
||||
git reset --hard main
|
||||
git push origin dev --force-with-lease
|
||||
```
|
||||
|
||||
### Shipping a release (tag)
|
||||
|
||||
1. Bump the version in `package.json` on `main`/`master` (a plain commit —
|
||||
builds nothing on its own):
|
||||
```
|
||||
# edit package.json: "version": "X.Y.Z"
|
||||
git add package.json
|
||||
git commit -m "chore: bump version to X.Y.Z"
|
||||
git push origin main
|
||||
```
|
||||
2. Tag that commit and push the tag — this is what actually triggers the
|
||||
release build:
|
||||
```
|
||||
git tag vX.Y.Z
|
||||
git push origin vX.Y.Z
|
||||
```
|
||||
3. Never move or re-push an existing tag once it's out (it may already be
|
||||
pulled/deployed). If you need to fix something after tagging, bump to
|
||||
the next patch version and tag again instead.
|
||||
|
||||
Use `v<major>.<minor>.<patch>` (semver) for tags.
|
||||
|
||||
### Required repo secrets/variables (Gitea → Settings → Actions)
|
||||
|
||||
- `vars.REGISTRY_HOST` — the Gitea instance hostname (no protocol), used to
|
||||
tag/push to the built-in Gitea container registry.
|
||||
- `secrets.REGISTRY_TOKEN` — a Gitea PAT with `write:package` scope.
|
||||
- `secrets.GHCR_TOKEN` — a GitHub PAT with `write:packages` scope, used to
|
||||
also push to `ghcr.io`.
|
||||
|
||||
## What not to commit
|
||||
|
||||
|
||||
@@ -10,7 +10,40 @@ No accounts, no friction, just a sweet little link. Everything personal is encry
|
||||
|
||||
## Quick start
|
||||
|
||||
You need [Docker](https://www.docker.com/) installed. That's it.
|
||||
You need [Docker](https://www.docker.com/) installed. That's it — no clone required, the image is published on GHCR.
|
||||
|
||||
Create a `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
image: ghcr.io/jeangaston/lovelope:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
DATABASE_URL: "file:/app/data/lovelope.db"
|
||||
APP_URL: "http://localhost:3000"
|
||||
GIPHY_API_KEY: "${GIPHY_API_KEY:-}"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Open **http://localhost:3000**. Your data lives in `./data/lovelope.db` on your machine.
|
||||
|
||||
Want GIF search to work? Grab a free key at [developers.giphy.com](https://developers.giphy.com/), export it as `GIPHY_API_KEY` before running `docker compose up` (or drop it in a `.env` file next to `docker-compose.yml`), then restart. Without it, the app still runs fine, GIF search is just disabled.
|
||||
|
||||
Pin a specific version instead of always tracking `latest` by using a release tag, e.g. `ghcr.io/jeangaston/lovelope:v1.0.0`.
|
||||
|
||||
### Build from source
|
||||
|
||||
If you'd rather build the image yourself instead of pulling from GHCR:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/jeangaston/lovelope.git && cd lovelope
|
||||
@@ -18,9 +51,7 @@ cp .env.example .env
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Open **http://localhost:3000**. Everything (the app and its database) runs in one container; your data lives in `./data/askedout.db` on your machine.
|
||||
|
||||
Want GIF search to work? Grab a free key at [developers.giphy.com](https://developers.giphy.com/) and put it in `.env` as `GIPHY_API_KEY`, then restart. Without it, the app still runs fine, GIF search is just disabled.
|
||||
This uses the [docker-compose.yml](docker-compose.yml) in the repo, which builds from the local [Dockerfile](Dockerfile) instead of pulling a published image.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -52,7 +83,7 @@ Browsers never send the part after `#` to any server, so that key never reaches
|
||||
|
||||
| Variable | Required | Description |
|
||||
|---|---|---|
|
||||
| `DATABASE_URL` | ✅ | SQLite file path, default `file:/app/data/askedout.db` works out of the box with Docker |
|
||||
| `DATABASE_URL` | ✅ | SQLite file path, default `file:/app/data/lovelope.db` works out of the box with Docker |
|
||||
| `APP_URL` | ✅ | Base URL used to build share links, e.g. `http://localhost:3000` |
|
||||
| `GIPHY_API_KEY` | optional | Enables GIF search. Free key at [developers.giphy.com](https://developers.giphy.com/), leave blank to disable |
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ services:
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
DATABASE_URL: "file:/app/data/askedout.db"
|
||||
DATABASE_URL: "file:/app/data/lovelope.db"
|
||||
APP_URL: "http://localhost:3000"
|
||||
NODE_ENV: "development"
|
||||
GIPHY_API_KEY: "${GIPHY_API_KEY:-}"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
chown -R nextjs:nodejs /app/data
|
||||
|
||||
exec su-exec nextjs sh -c "npx prisma migrate deploy && node server.js"
|
||||
Generated
+1
-25
@@ -7,12 +7,11 @@
|
||||
"": {
|
||||
"name": "lovelope",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.15.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.23",
|
||||
"@radix-ui/react-dialog": "^1.1.23",
|
||||
"@radix-ui/react-label": "^2.1.15",
|
||||
"@radix-ui/react-separator": "^1.1.15",
|
||||
"@radix-ui/react-slot": "^1.3.3",
|
||||
"@radix-ui/react-switch": "^1.3.7",
|
||||
"@radix-ui/react-tabs": "^1.1.21",
|
||||
@@ -1358,29 +1357,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz",
|
||||
"integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz",
|
||||
|
||||
+1
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lovelope",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
@@ -17,9 +17,7 @@
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.15.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.23",
|
||||
"@radix-ui/react-dialog": "^1.1.23",
|
||||
"@radix-ui/react-label": "^2.1.15",
|
||||
"@radix-ui/react-separator": "^1.1.15",
|
||||
"@radix-ui/react-slot": "^1.3.3",
|
||||
"@radix-ui/react-switch": "^1.3.7",
|
||||
"@radix-ui/react-tabs": "^1.1.21",
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -1,121 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
Reference in New Issue
Block a user