chore: add vite-plus as node toolchain (#1806)

This commit is contained in:
Kyle Mendell
2026-10-04 18:53:18 +02:00
committed by GitHub
parent 4d2a5e423e
commit 69362ff8b8
154 changed files with 2177 additions and 1922 deletions
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "pocket-id",
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
"image": "ghcr.io/voidzero-dev/vite-plus:1.0.0",
"features": {
"ghcr.io/devcontainers/features/go:1": {
"version": "1.27"
@@ -12,12 +12,14 @@
"golang.go",
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode"
"oxc.oxc-vscode",
"VoidZero.vite-plus-extension-pack"
]
}
},
"containerEnv": {
"HOST": "0.0.0.0"
},
"postCreateCommand": "npm install --prefix frontend && cd backend && go mod download"
"remoteUser": "vp",
"postCreateCommand": "vp install && cd backend && go mod download"
}
+6 -2
View File
@@ -27,8 +27,12 @@ jobs:
with:
fetch-depth: 0
- name: Setup Node.js and pnpm
uses: pnpm/setup@fbda4c85fc2e1e08721cd8763afea8f48d60f024 # v3.0.0
- name: Setup Vite+
uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1
with:
node-version-file: .node-version
cache: true
run-install: false
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
+9 -7
View File
@@ -49,8 +49,12 @@ jobs:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js and pnpm
uses: pnpm/setup@fbda4c85fc2e1e08721cd8763afea8f48d60f024 # v3.0.0
- name: Setup Vite+
uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1
with:
node-version-file: .node-version
cache: true
run-install: false
- name: Set up Depot CLI
uses: depot/setup-action@91bc8495a33ebfc504ffc89e5674379ccf23c29c # v1.7.2
@@ -136,12 +140,11 @@ jobs:
run: docker load < /tmp/francis-image.tar
- name: Install test dependencies
run: pnpm --filter pocket-id-tests install --frozen-lockfile
run: vp -C tests install --frozen-lockfile
- name: Install Playwright Browsers
working-directory: ./tests
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium
run: vp -C tests exec playwright install --with-deps chromium
- name: Run Docker containers
working-directory: ./tests/setup
@@ -184,8 +187,7 @@ jobs:
fi
- name: Run Playwright tests
working-directory: ./tests
run: pnpm exec playwright test
run: vp -C tests exec playwright test
- name: Upload Test Report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+14 -6
View File
@@ -68,13 +68,17 @@ jobs:
echo "tag=v$version" >> "$GITHUB_OUTPUT"
echo "Preparing release v$version from $GITHUB_SHA." >> "$GITHUB_STEP_SUMMARY"
- name: Setup Node.js and pnpm
- name: Setup Vite+
if: steps.version.outputs.tag != ''
uses: pnpm/setup@fbda4c85fc2e1e08721cd8763afea8f48d60f024 # v3.0.0
uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1
with:
node-version-file: .node-version
cache: true
run-install: false
- name: Install dependencies
if: steps.version.outputs.tag != ''
run: pnpm install --frozen-lockfile
run: vp install --frozen-lockfile
- name: Update version and changelog
if: steps.version.outputs.tag != ''
@@ -86,7 +90,7 @@ jobs:
printf '%s\n' "$VERSION" > .version
jq --arg version "$VERSION" '.version = $version' frontend/package.json > "$RUNNER_TEMP/package.json"
mv "$RUNNER_TEMP/package.json" frontend/package.json
pnpm --dir frontend exec prettier --write package.json
vp fmt frontend/package.json
git cliff --prepend CHANGELOG.md --tag "$RELEASE_TAG" --unreleased
- name: Create bot app token
@@ -166,8 +170,12 @@ jobs:
gh release create "$RELEASE_TAG" --verify-tag --title "$RELEASE_TAG" --notes-file "$RUNNER_TEMP/release-notes.md" --draft
fi
- name: Setup Node.js and pnpm
uses: pnpm/setup@fbda4c85fc2e1e08721cd8763afea8f48d60f024 # v3.0.0
- name: Setup Vite+
uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1
with:
node-version-file: .node-version
cache: true
run-install: false
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
+9 -7
View File
@@ -35,19 +35,21 @@ jobs:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js and pnpm
uses: pnpm/setup@fbda4c85fc2e1e08721cd8763afea8f48d60f024 # v3.0.0
- name: Setup Vite+
uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1
with:
node-version-file: .node-version
cache: true
run-install: false
- name: Install dependencies
run: pnpm --filter pocket-id-frontend install --frozen-lockfile
run: vp -C frontend install --frozen-lockfile
- name: Build Pocket ID Frontend
working-directory: frontend
run: pnpm --filter pocket-id-frontend build
run: vp -C frontend run build
- name: Add svelte-check problem matcher
run: echo "::add-matcher::.github/svelte-check-matcher.json"
- name: Run svelte-check
working-directory: frontend
run: pnpm --filter pocket-id-frontend check
run: vp -C frontend run check
+4 -1
View File
@@ -65,4 +65,7 @@ dist/
/backend/go.work
/backend/go.work.sum
.deepsec/
.deepsec/
# Vite+ generated git hook dispatcher
.vite-hooks/_
.vitest/
+2 -2
View File
@@ -7,8 +7,8 @@ project_name: pocket-id
before:
hooks:
- pnpm install --frozen-lockfile
- pnpm -C frontend build
- vp install --frozen-lockfile
- vp -C frontend run build
- mkdir -p .tmp
- mv backend/frontend/dist/cyclonedx/frontend.cdx.json .tmp/frontend.cdx.json
- rm -r backend/frontend/dist/cyclonedx
+1
View File
@@ -0,0 +1 @@
vp staged
+2 -1
View File
@@ -3,6 +3,7 @@
"golang.go",
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode"
"oxc.oxc-vscode",
"VoidZero.vite-plus-extension-pack"
]
}
+1 -1
View File
@@ -20,7 +20,7 @@
"request": "launch",
"envFile": "${workspaceFolder}/frontend/.env",
"cwd": "${workspaceFolder}/frontend",
"runtimeExecutable": "npm",
"runtimeExecutable": "vp",
"runtimeArgs": [
"run",
"dev"
+5 -1
View File
@@ -1,4 +1,8 @@
{
"go.buildTags": "e2etest unit",
"prettier.documentSelectors": ["**/*.svelte"]
"editor.defaultFormatter": "oxc.oxc-vscode",
"oxc.fmt.disableNestedConfig": true,
"[svelte]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
}
}
+2 -2
View File
@@ -19,8 +19,8 @@ golangci-lint run # lint (config: backend/.golangci.yml - includes build tags)
# frontend/ (or root)
pnpm check # svelte-check — the ONLY frontend type gate (no unit tests exist)
pnpm lint # prettier --check && eslint (note: not enforced by CI)
pnpm format # prettier --write — REQUIRED before opening a PR
pnpm lint # vp check — oxlint + oxfmt check via Vite+ (note: not enforced by CI)
pnpm format # vp fmt — REQUIRED before opening a PR
```
End-to-end (needs Docker; **stop any local backend on `:1411` first** — see gotchas):
+13 -14
View File
@@ -1,4 +1,4 @@
# syntax=docker/dockerfile:1.7
# syntax=docker/dockerfile:1
# This file uses multi-stage builds to build the application from source, including the front-end
@@ -6,21 +6,20 @@
ARG BUILD_TAGS=""
# Stage 1: Build Frontend
FROM node:24-alpine AS frontend-builder
RUN corepack enable
FROM --platform=$BUILDPLATFORM ghcr.io/voidzero-dev/vite-plus:1.0.0 AS frontend-builder
ENV CI=true
WORKDIR /app
WORKDIR /build
COPY --chown=vp:vp package.json pnpm-workspace.yaml pnpm-lock.yaml .node-version ./
COPY --chown=vp:vp --parents frontend/package.json tests/package.json email-templates/package.json ./
RUN --mount=type=cache,id=pocket-id-pnpm,target=/home/vp/.local/share/pnpm/store,sharing=locked,uid=1000,gid=1000 \
vp -C frontend install --frozen-lockfile --prefer-offline
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY frontend/package.json ./frontend/
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm --filter pocket-id-frontend install --frozen-lockfile
COPY --chown=vp:vp ./frontend ./frontend/
COPY ./frontend ./frontend/
RUN --mount=type=cache,target=/build/frontend/node_modules/.vite \
--mount=type=cache,target=/build/frontend/.svelte-kit \
BUILD_OUTPUT_PATH=dist pnpm --filter pocket-id-frontend run build
RUN --mount=type=cache,target=/app/frontend/node_modules/.vite,uid=1000,gid=1000 \
--mount=type=cache,target=/app/frontend/.svelte-kit,uid=1000,gid=1000 \
BUILD_OUTPUT_PATH=dist vp -C frontend run build
# Stage 2: Build Backend
FROM golang:1.27.1-alpine AS backend-builder
@@ -31,7 +30,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY ./backend ./
COPY --from=frontend-builder /build/frontend/dist ./frontend/dist
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
COPY .version .version
WORKDIR /build/cmd
+45 -49
View File
@@ -1,78 +1,74 @@
import { render } from "react-email";
import * as fs from "node:fs";
import * as path from "node:path";
import { render } from 'react-email';
import * as fs from 'node:fs';
import * as path from 'node:path';
const outputDir = "../backend/resources/email-templates";
const outputDir = '../backend/resources/email-templates';
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
fs.mkdirSync(outputDir, { recursive: true });
}
function getTemplateName(filename: string): string {
return filename.replace(".tsx", "");
return filename.replace('.tsx', '');
}
async function buildTemplateFile(
Component: any,
templateName: string,
isPlainText: boolean
) {
const rendered = await render(
Component(Component.TemplateProps),
isPlainText ? { plainText: true } : {},
);
async function buildTemplateFile(Component: any, templateName: string, isPlainText: boolean) {
const rendered = await render(
Component(Component.TemplateProps),
isPlainText ? { plainText: true } : {}
);
// Normalize quotes
const normalized = rendered.replace(/&quot;/g, '"');
// Normalize quotes
const normalized = rendered.replace(/&quot;/g, '"');
const goTemplate = `{{define "root"}}${normalized}{{end}}`;
const suffix = isPlainText ? "_text.tmpl" : "_html.tmpl";
const templatePath = path.join(outputDir, `${templateName}${suffix}`);
const goTemplate = `{{define "root"}}${normalized}{{end}}`;
const suffix = isPlainText ? '_text.tmpl' : '_html.tmpl';
const templatePath = path.join(outputDir, `${templateName}${suffix}`);
fs.writeFileSync(templatePath, goTemplate);
fs.writeFileSync(templatePath, goTemplate);
}
async function discoverAndBuildTemplates() {
console.log("Discovering and building email templates...");
console.log('Discovering and building email templates...');
const emailsDir = "./emails";
const files = fs.readdirSync(emailsDir);
const emailsDir = './emails';
const files = fs.readdirSync(emailsDir);
for (const file of files) {
if (!file.endsWith(".tsx")) continue;
for (const file of files) {
if (!file.endsWith('.tsx')) continue;
const templateName = getTemplateName(file);
const modulePath = `./${emailsDir}/${file}`;
const templateName = getTemplateName(file);
const modulePath = `./${emailsDir}/${file}`;
console.log(`Building ${templateName}...`);
console.log(`Building ${templateName}...`);
try {
const module = await import(modulePath);
const Component = module.default || module[Object.keys(module)[0]];
try {
const module = await import(modulePath);
const Component = module.default || module[Object.keys(module)[0]];
if (!Component) {
console.error(`✗ No component found in ${file}`);
continue;
}
if (!Component) {
console.error(`✗ No component found in ${file}`);
continue;
}
if (!Component.TemplateProps) {
console.error(`✗ No TemplateProps found in ${file}`);
continue;
}
if (!Component.TemplateProps) {
console.error(`✗ No TemplateProps found in ${file}`);
continue;
}
await buildTemplateFile(Component, templateName, false); // HTML
await buildTemplateFile(Component, templateName, true); // Text
await buildTemplateFile(Component, templateName, false); // HTML
await buildTemplateFile(Component, templateName, true); // Text
console.log(`✓ Built ${templateName}`);
} catch (error) {
console.error(`✗ Error building ${templateName}:`, error);
}
}
console.log(`✓ Built ${templateName}`);
} catch (error) {
console.error(`✗ Error building ${templateName}:`, error);
}
}
}
async function main() {
await discoverAndBuildTemplates();
console.log("All templates built successfully!");
await discoverAndBuildTemplates();
console.log('All templates built successfully!');
}
main().catch(console.error);
+67 -86
View File
@@ -1,119 +1,100 @@
import {
Body,
Column,
Container,
Head,
Html,
Img,
Link,
Preview,
Row,
Section,
} from "react-email";
import type { SharedProps } from "../props";
import { colors, fonts, radius } from "./theme";
import { Body, Column, Container, Head, Html, Img, Link, Preview, Row, Section } from 'react-email';
import type { SharedProps } from '../props';
import { colors, fonts, radius } from './theme';
interface BaseTemplateProps extends SharedProps {
preview?: string;
children: React.ReactNode;
preview?: string;
children: React.ReactNode;
}
export const BaseTemplate = ({
logoURL,
appName,
appURL,
preview,
children,
logoURL,
appName,
appURL,
preview,
children
}: BaseTemplateProps) => (
<Html lang="en">
<Head>
<meta name="color-scheme" content="light" />
<meta name="supported-color-schemes" content="light" />
<style dangerouslySetInnerHTML={{ __html: fontFaceCss(appURL) }} />
</Head>
{preview && <Preview>{preview}</Preview>}
<Body style={bodyStyle}>
<Container style={containerStyle}>
<Section style={headerStyle} data-skip-in-text="true">
<Row>
<Column style={logoColumnStyle}>
<Link href={appURL}>
<Img
src={logoURL}
width="28"
height="28"
alt={appName}
style={logoStyle}
/>
</Link>
</Column>
<Column>
<Link href={appURL} style={appNameStyle}>
{appName}
</Link>
</Column>
</Row>
</Section>
<Html lang="en">
<Head>
<meta name="color-scheme" content="light" />
<meta name="supported-color-schemes" content="light" />
<style dangerouslySetInnerHTML={{ __html: fontFaceCss(appURL) }} />
</Head>
{preview && <Preview>{preview}</Preview>}
<Body style={bodyStyle}>
<Container style={containerStyle}>
<Section style={headerStyle} data-skip-in-text="true">
<Row>
<Column style={logoColumnStyle}>
<Link href={appURL}>
<Img src={logoURL} width="28" height="28" alt={appName} style={logoStyle} />
</Link>
</Column>
<Column>
<Link href={appURL} style={appNameStyle}>
{appName}
</Link>
</Column>
</Row>
</Section>
<Section>
<Row>
<Column style={cardStyle}>{children}</Column>
</Row>
</Section>
</Container>
</Body>
</Html>
<Section>
<Row>
<Column style={cardStyle}>{children}</Column>
</Row>
</Section>
</Container>
</Body>
</Html>
);
// The heading font is self-hosted by the app, so it loads from the same origin as the logo and works without third-party requests
// Clients without web font support fall back to the serif stack declared on the headings
const fontFaceCss = (appURL: string) =>
`@font-face{font-family:'Gloock';font-style:normal;font-weight:400;mso-font-alt:'Georgia';src:url(${appURL}/fonts/Gloock-Regular.woff) format('woff');}`;
`@font-face{font-family:'Gloock';font-style:normal;font-weight:400;mso-font-alt:'Georgia';src:url(${appURL}/fonts/Gloock-Regular.woff) format('woff');}`;
const bodyStyle = {
margin: 0,
padding: "32px 16px",
backgroundColor: colors.background,
fontFamily: fonts.sans,
color: colors.text,
margin: 0,
padding: '32px 16px',
backgroundColor: colors.background,
fontFamily: fonts.sans,
color: colors.text
};
const containerStyle = {
width: "100%",
maxWidth: "480px",
margin: "0 auto",
width: '100%',
maxWidth: '480px',
margin: '0 auto'
};
const headerStyle = {
marginBottom: "20px",
marginBottom: '20px'
};
const logoColumnStyle = {
width: "36px",
verticalAlign: "middle",
width: '36px',
verticalAlign: 'middle'
};
const logoStyle = {
display: "block",
width: "28px",
height: "28px",
borderRadius: "6px",
display: 'block',
width: '28px',
height: '28px',
borderRadius: '6px'
};
const appNameStyle = {
fontFamily: fonts.serif,
fontSize: "20px",
lineHeight: "28px",
color: colors.foreground,
textDecoration: "none",
fontFamily: fonts.serif,
fontSize: '20px',
lineHeight: '28px',
color: colors.foreground,
textDecoration: 'none'
};
const cardStyle = {
backgroundColor: colors.card,
border: `1px solid ${colors.border}`,
borderRadius: radius.card,
padding: "32px",
textAlign: "left" as const,
backgroundColor: colors.card,
border: `1px solid ${colors.border}`,
borderRadius: radius.card,
padding: '32px',
textAlign: 'left' as const
};
+21 -21
View File
@@ -1,33 +1,33 @@
import { Button as EmailButton, Section } from "react-email";
import { colors, fonts, radius } from "./theme";
import { Button as EmailButton, Section } from 'react-email';
import { colors, fonts, radius } from './theme';
interface ButtonProps {
href: string;
children: React.ReactNode;
href: string;
children: React.ReactNode;
}
export const Button = ({ href, children }: ButtonProps) => (
<Section style={containerStyle}>
<EmailButton href={href} style={buttonStyle}>
{children}
</EmailButton>
</Section>
<Section style={containerStyle}>
<EmailButton href={href} style={buttonStyle}>
{children}
</EmailButton>
</Section>
);
const containerStyle = {
margin: "24px 0 8px 0",
textAlign: "center" as const,
margin: '24px 0 8px 0',
textAlign: 'center' as const
};
const buttonStyle = {
display: "inline-block",
padding: "12px 28px",
backgroundColor: colors.primary,
color: colors.primaryForeground,
borderRadius: radius.pill,
fontFamily: fonts.sans,
fontSize: "14px",
lineHeight: "20px",
fontWeight: 500,
textDecoration: "none",
display: 'inline-block',
padding: '12px 28px',
backgroundColor: colors.primary,
color: colors.primaryForeground,
borderRadius: radius.pill,
fontFamily: fonts.sans,
fontSize: '14px',
lineHeight: '20px',
fontWeight: 500,
textDecoration: 'none'
};
+15 -15
View File
@@ -1,24 +1,24 @@
import { Heading } from "react-email";
import { colors, fonts } from "./theme";
import { Heading } from 'react-email';
import { colors, fonts } from './theme';
interface CardHeaderProps {
title: string;
title: string;
}
export default function CardHeader({ title }: CardHeaderProps) {
return (
<Heading as="h1" style={titleStyle}>
{title}
</Heading>
);
return (
<Heading as="h1" style={titleStyle}>
{title}
</Heading>
);
}
const titleStyle = {
margin: "0 0 16px 0",
fontFamily: fonts.serif,
fontSize: "26px",
lineHeight: "32px",
fontWeight: 400,
letterSpacing: "-0.01em",
color: colors.foreground,
margin: '0 0 16px 0',
fontFamily: fonts.serif,
fontSize: '26px',
lineHeight: '32px',
fontWeight: 400,
letterSpacing: '-0.01em',
color: colors.foreground
};
+24 -24
View File
@@ -1,39 +1,39 @@
import { Column, Row, Section, Text } from "react-email";
import { colors, fonts, radius } from "./theme";
import { Column, Row, Section, Text } from 'react-email';
import { colors, fonts, radius } from './theme';
interface CodeBoxProps {
code: string;
code: string;
}
export const CodeBox = ({ code }: CodeBoxProps) => (
<Section style={boxStyle}>
<Row>
<Column style={cellStyle}>
<Text style={codeStyle}>{code}</Text>
</Column>
</Row>
</Section>
<Section style={boxStyle}>
<Row>
<Column style={cellStyle}>
<Text style={codeStyle}>{code}</Text>
</Column>
</Row>
</Section>
);
const boxStyle = {
margin: "8px 0 0 0",
backgroundColor: colors.muted,
border: `1px solid ${colors.border}`,
borderRadius: radius.box,
margin: '8px 0 0 0',
backgroundColor: colors.muted,
border: `1px solid ${colors.border}`,
borderRadius: radius.box
};
const cellStyle = {
padding: "20px 16px",
textAlign: "center" as const,
padding: '20px 16px',
textAlign: 'center' as const
};
const codeStyle = {
margin: 0,
fontFamily: fonts.mono,
fontSize: "32px",
lineHeight: "40px",
fontWeight: 600,
letterSpacing: "8px",
paddingLeft: "8px",
color: colors.foreground,
margin: 0,
fontFamily: fonts.mono,
fontSize: '32px',
lineHeight: '40px',
fontWeight: 600,
letterSpacing: '8px',
paddingLeft: '8px',
color: colors.foreground
};
+30 -30
View File
@@ -1,54 +1,54 @@
import { Column, Row, Section } from "react-email";
import { colors, radius } from "./theme";
import { Column, Row, Section } from 'react-email';
import { colors, radius } from './theme';
interface DetailsListProps {
items: { label: string; value: string }[];
items: { label: string; value: string }[];
}
// A key-value list inside a muted box, used for sign-in details and similar metadata
// Each value sits below its label so long values never fight the label for width on narrow screens
// Every item is its own Row so the plain-text build separates entries with a blank line
export const DetailsList = ({ items }: DetailsListProps) => (
<Section style={boxStyle}>
{items.map((item, index) => {
const isLast = index === items.length - 1;
return (
<Row key={item.label}>
<Column style={isLast ? cellStyle : { ...cellStyle, ...dividerStyle }}>
<span style={labelStyle}>{item.label}</span>
<br />
<span style={valueStyle}>{item.value}</span>
</Column>
</Row>
);
})}
</Section>
<Section style={boxStyle}>
{items.map((item, index) => {
const isLast = index === items.length - 1;
return (
<Row key={item.label}>
<Column style={isLast ? cellStyle : { ...cellStyle, ...dividerStyle }}>
<span style={labelStyle}>{item.label}</span>
<br />
<span style={valueStyle}>{item.value}</span>
</Column>
</Row>
);
})}
</Section>
);
const boxStyle = {
margin: "8px 0 0 0",
backgroundColor: colors.muted,
border: `1px solid ${colors.border}`,
borderRadius: radius.box,
margin: '8px 0 0 0',
backgroundColor: colors.muted,
border: `1px solid ${colors.border}`,
borderRadius: radius.box
};
const cellStyle = {
padding: "12px 16px",
padding: '12px 16px'
};
const labelStyle = {
fontSize: "12px",
lineHeight: "18px",
color: colors.mutedForeground,
fontSize: '12px',
lineHeight: '18px',
color: colors.mutedForeground
};
const valueStyle = {
fontSize: "14px",
lineHeight: "20px",
fontWeight: 500,
color: colors.foreground,
fontSize: '14px',
lineHeight: '20px',
fontWeight: 500,
color: colors.foreground
};
const dividerStyle = {
borderBottom: `1px solid ${colors.border}`,
borderBottom: `1px solid ${colors.border}`
};
+14 -14
View File
@@ -1,29 +1,29 @@
import { Text } from "react-email";
import { colors } from "./theme";
import { Text } from 'react-email';
import { colors } from './theme';
interface TextProps {
children: React.ReactNode;
style?: React.CSSProperties;
children: React.ReactNode;
style?: React.CSSProperties;
}
export const Paragraph = ({ children, style }: TextProps) => (
<Text style={{ ...paragraphStyle, ...style }}>{children}</Text>
<Text style={{ ...paragraphStyle, ...style }}>{children}</Text>
);
export const Muted = ({ children, style }: TextProps) => (
<Text style={{ ...mutedStyle, ...style }}>{children}</Text>
<Text style={{ ...mutedStyle, ...style }}>{children}</Text>
);
const paragraphStyle = {
margin: "0 0 16px 0",
fontSize: "15px",
lineHeight: "24px",
color: colors.text,
margin: '0 0 16px 0',
fontSize: '15px',
lineHeight: '24px',
color: colors.text
};
const mutedStyle = {
margin: "16px 0 0 0",
fontSize: "13px",
lineHeight: "20px",
color: colors.mutedForeground,
margin: '16px 0 0 0',
fontSize: '13px',
lineHeight: '20px',
color: colors.mutedForeground
};
+15 -15
View File
@@ -1,25 +1,25 @@
// Design tokens mirrored from the frontend's app.css (light theme) so emails match the app
// Email clients have no CSS variable or oklch support, so the values are the resolved hex equivalents
export const colors = {
background: "#f5f5f5",
card: "#ffffff",
border: "#e5e5e5",
muted: "#fafafa",
foreground: "#0a0a0a",
text: "#404040",
mutedForeground: "#737373",
primary: "#171717",
primaryForeground: "#ffffff",
background: '#f5f5f5',
card: '#ffffff',
border: '#e5e5e5',
muted: '#fafafa',
foreground: '#0a0a0a',
text: '#404040',
mutedForeground: '#737373',
primary: '#171717',
primaryForeground: '#ffffff'
};
export const fonts = {
sans: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
serif: "Gloock, Georgia, 'Times New Roman', serif",
mono: "'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace",
sans: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
serif: "Gloock, Georgia, 'Times New Roman', serif",
mono: "'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace"
};
export const radius = {
card: "24px",
box: "16px",
pill: "9999px",
card: '24px',
box: '16px',
pill: '9999px'
};
@@ -1,61 +1,50 @@
import { BaseTemplate } from "../components/base-template";
import CardHeader from "../components/card-header";
import { Muted, Paragraph } from "../components/text";
import {
type SharedProps,
sharedPreviewProps,
sharedTemplateProps,
} from "../props";
import { BaseTemplate } from '../components/base-template';
import CardHeader from '../components/card-header';
import { Muted, Paragraph } from '../components/text';
import { type SharedProps, sharedPreviewProps, sharedTemplateProps } from '../props';
interface ApiKeyExpiringData {
name: string;
apiKeyName: string;
expiresAt: string;
name: string;
apiKeyName: string;
expiresAt: string;
}
interface ApiKeyExpiringEmailProps extends SharedProps {
data: ApiKeyExpiringData;
data: ApiKeyExpiringData;
}
export const ApiKeyExpiringEmail = ({
data,
...props
}: ApiKeyExpiringEmailProps) => (
<BaseTemplate
{...props}
preview={`Your API key ${data.apiKeyName} expires on ${data.expiresAt}`}
>
<CardHeader title="API key expiring soon" />
<Paragraph>Hello {data.name},</Paragraph>
<Paragraph>
Your API key <strong>{data.apiKeyName}</strong> will expire on{" "}
<strong>{data.expiresAt}</strong>. Anything that uses this key will stop
working once it expires.
</Paragraph>
export const ApiKeyExpiringEmail = ({ data, ...props }: ApiKeyExpiringEmailProps) => (
<BaseTemplate {...props} preview={`Your API key ${data.apiKeyName} expires on ${data.expiresAt}`}>
<CardHeader title="API key expiring soon" />
<Paragraph>Hello {data.name},</Paragraph>
<Paragraph>
Your API key <strong>{data.apiKeyName}</strong> will expire on{' '}
<strong>{data.expiresAt}</strong>. Anything that uses this key will stop working once it
expires.
</Paragraph>
<Muted>
To keep access, create a new API key in your {props.appName} account
settings before then.
</Muted>
</BaseTemplate>
<Muted>
To keep access, create a new API key in your {props.appName} account settings before then.
</Muted>
</BaseTemplate>
);
export default ApiKeyExpiringEmail;
ApiKeyExpiringEmail.TemplateProps = {
...sharedTemplateProps,
data: {
name: "{{.Data.Name}}",
apiKeyName: "{{.Data.ApiKeyName}}",
expiresAt: '{{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}',
},
...sharedTemplateProps,
data: {
name: '{{.Data.Name}}',
apiKeyName: '{{.Data.ApiKeyName}}',
expiresAt: '{{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}'
}
};
ApiKeyExpiringEmail.PreviewProps = {
...sharedPreviewProps,
data: {
name: "Elias",
apiKeyName: "CI deploy key",
expiresAt: "2026-01-30 12:00:00 UTC",
},
...sharedPreviewProps,
data: {
name: 'Elias',
apiKeyName: 'CI deploy key',
expiresAt: '2026-01-30 12:00:00 UTC'
}
};
+40 -51
View File
@@ -1,72 +1,61 @@
import { Link } from "react-email";
import { BaseTemplate } from "../components/base-template";
import { Button } from "../components/button";
import CardHeader from "../components/card-header";
import { Muted, Paragraph } from "../components/text";
import { colors } from "../components/theme";
import {
type SharedProps,
sharedPreviewProps,
sharedTemplateProps,
} from "../props";
import { Link } from 'react-email';
import { BaseTemplate } from '../components/base-template';
import { Button } from '../components/button';
import CardHeader from '../components/card-header';
import { Muted, Paragraph } from '../components/text';
import { colors } from '../components/theme';
import { type SharedProps, sharedPreviewProps, sharedTemplateProps } from '../props';
interface EmailVerificationData {
userFullName: string;
verificationLink: string;
userFullName: string;
verificationLink: string;
}
interface EmailVerificationProps extends SharedProps {
data: EmailVerificationData;
data: EmailVerificationData;
}
export const EmailVerification = ({
data,
...props
}: EmailVerificationProps) => (
<BaseTemplate
{...props}
preview={`Confirm the email address for your ${props.appName} account`}
>
<CardHeader title="Verify your email address" />
<Paragraph>Hello {data.userFullName},</Paragraph>
<Paragraph>
Click the button below to confirm the email address for your{" "}
{props.appName} account. This link expires in 24 hours.
</Paragraph>
export const EmailVerification = ({ data, ...props }: EmailVerificationProps) => (
<BaseTemplate {...props} preview={`Confirm the email address for your ${props.appName} account`}>
<CardHeader title="Verify your email address" />
<Paragraph>Hello {data.userFullName},</Paragraph>
<Paragraph>
Click the button below to confirm the email address for your {props.appName} account. This
link expires in 24 hours.
</Paragraph>
<Button href={data.verificationLink}>Verify email address</Button>
<Button href={data.verificationLink}>Verify email address</Button>
<Muted style={{ marginTop: "32px" }}>
Or if you don't like clicking buttons, open this link:
<br />
<Link href={data.verificationLink} style={linkStyle}>
{data.verificationLink}
</Link>
</Muted>
</BaseTemplate>
<Muted style={{ marginTop: '32px' }}>
Or if you don't like clicking buttons, open this link:
<br />
<Link href={data.verificationLink} style={linkStyle}>
{data.verificationLink}
</Link>
</Muted>
</BaseTemplate>
);
export default EmailVerification;
const linkStyle = {
color: colors.mutedForeground,
textDecoration: "underline",
wordBreak: "break-all" as const,
color: colors.mutedForeground,
textDecoration: 'underline',
wordBreak: 'break-all' as const
};
EmailVerification.TemplateProps = {
...sharedTemplateProps,
data: {
userFullName: "{{.Data.UserFullName}}",
verificationLink: "{{.Data.VerificationLink}}",
},
...sharedTemplateProps,
data: {
userFullName: '{{.Data.UserFullName}}',
verificationLink: '{{.Data.VerificationLink}}'
}
};
EmailVerification.PreviewProps = {
...sharedPreviewProps,
data: {
userFullName: "Tim Cook",
verificationLink:
"https://id.example.com/user/verify-email?code=abcdefg12345",
},
...sharedPreviewProps,
data: {
userFullName: 'Tim Cook',
verificationLink: 'https://id.example.com/user/verify-email?code=abcdefg12345'
}
};
@@ -1,70 +1,63 @@
import { BaseTemplate } from "../components/base-template";
import CardHeader from "../components/card-header";
import { DetailsList } from "../components/details-list";
import { Muted, Paragraph } from "../components/text";
import {
type SharedProps,
sharedPreviewProps,
sharedTemplateProps,
} from "../props";
import { BaseTemplate } from '../components/base-template';
import CardHeader from '../components/card-header';
import { DetailsList } from '../components/details-list';
import { Muted, Paragraph } from '../components/text';
import { type SharedProps, sharedPreviewProps, sharedTemplateProps } from '../props';
interface SignInData {
location: string;
ipAddress: string;
device: string;
dateTime: string;
location: string;
ipAddress: string;
device: string;
dateTime: string;
}
interface NewSignInEmailProps extends SharedProps {
data: SignInData;
data: SignInData;
}
export const NewSignInEmail = ({ data, ...props }: NewSignInEmailProps) => (
<BaseTemplate
{...props}
preview={`A new sign-in to your ${props.appName} account was detected`}
>
<CardHeader title="New sign-in detected" />
<Paragraph>
Your {props.appName} account was recently accessed from a new IP address
or browser. If this was you, no further action is needed.
</Paragraph>
<BaseTemplate {...props} preview={`A new sign-in to your ${props.appName} account was detected`}>
<CardHeader title="New sign-in detected" />
<Paragraph>
Your {props.appName} account was recently accessed from a new IP address or browser. If this
was you, no further action is needed.
</Paragraph>
<DetailsList
items={[
{ label: "Approximate location", value: data.location },
{ label: "IP address", value: data.ipAddress },
{ label: "Device", value: data.device },
{ label: "Time", value: data.dateTime },
]}
/>
<DetailsList
items={[
{ label: 'Approximate location', value: data.location },
{ label: 'IP address', value: data.ipAddress },
{ label: 'Device', value: data.device },
{ label: 'Time', value: data.dateTime }
]}
/>
<Muted>
If you don't recognize this activity, review the passkeys in your{" "}
{props.appName} account settings and remove any you don't recognize.
</Muted>
</BaseTemplate>
<Muted>
If you don't recognize this activity, review the passkeys in your {props.appName} account
settings and remove any you don't recognize.
</Muted>
</BaseTemplate>
);
export default NewSignInEmail;
NewSignInEmail.TemplateProps = {
...sharedTemplateProps,
data: {
location:
"{{if and .Data.City .Data.Country}}{{.Data.City}}, {{.Data.Country}}{{else if .Data.Country}}{{.Data.Country}}{{else}}Unknown{{end}}",
ipAddress: "{{.Data.IPAddress}}",
device: "{{.Data.Device}}",
dateTime: '{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}',
},
...sharedTemplateProps,
data: {
location:
'{{if and .Data.City .Data.Country}}{{.Data.City}}, {{.Data.Country}}{{else if .Data.Country}}{{.Data.Country}}{{else}}Unknown{{end}}',
ipAddress: '{{.Data.IPAddress}}',
device: '{{.Data.Device}}',
dateTime: '{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}'
}
};
NewSignInEmail.PreviewProps = {
...sharedPreviewProps,
data: {
location: "San Francisco, USA",
ipAddress: "203.0.113.42",
device: "Chrome on macOS",
dateTime: "January 2, 2026 at 3:04 PM UTC",
},
...sharedPreviewProps,
data: {
location: 'San Francisco, USA',
ipAddress: '203.0.113.42',
device: 'Chrome on macOS',
dateTime: 'January 2, 2026 at 3:04 PM UTC'
}
};
+46 -57
View File
@@ -1,79 +1,68 @@
import { Link } from "react-email";
import { BaseTemplate } from "../components/base-template";
import { Button } from "../components/button";
import CardHeader from "../components/card-header";
import { CodeBox } from "../components/code-box";
import { Muted, Paragraph } from "../components/text";
import { colors } from "../components/theme";
import {
type SharedProps,
sharedPreviewProps,
sharedTemplateProps,
} from "../props";
import { Link } from 'react-email';
import { BaseTemplate } from '../components/base-template';
import { Button } from '../components/button';
import CardHeader from '../components/card-header';
import { CodeBox } from '../components/code-box';
import { Muted, Paragraph } from '../components/text';
import { colors } from '../components/theme';
import { type SharedProps, sharedPreviewProps, sharedTemplateProps } from '../props';
interface OneTimeAccessData {
code: string;
loginLink: string;
buttonCodeLink: string;
expirationString: string;
code: string;
loginLink: string;
buttonCodeLink: string;
expirationString: string;
}
interface OneTimeAccessEmailProps extends SharedProps {
data: OneTimeAccessData;
data: OneTimeAccessData;
}
export const OneTimeAccessEmail = ({
data,
...props
}: OneTimeAccessEmailProps) => (
<BaseTemplate
{...props}
preview={`Your ${props.appName} login code is ${data.code}`}
>
<CardHeader title="Your login code" />
<Paragraph>
Use the code below to sign in to {props.appName}. It expires in{" "}
{data.expirationString}.
</Paragraph>
export const OneTimeAccessEmail = ({ data, ...props }: OneTimeAccessEmailProps) => (
<BaseTemplate {...props} preview={`Your ${props.appName} login code is ${data.code}`}>
<CardHeader title="Your login code" />
<Paragraph>
Use the code below to sign in to {props.appName}. It expires in {data.expirationString}.
</Paragraph>
<CodeBox code={data.code} />
<CodeBox code={data.code} />
<Button href={data.buttonCodeLink}>Sign in</Button>
<Button href={data.buttonCodeLink}>Sign in</Button>
<Muted style={{ marginTop: "32px" }}>
Or open{" "}
<Link href={data.loginLink} style={linkStyle}>
{data.loginLink}
</Link>{" "}
and enter the code manually.
</Muted>
</BaseTemplate>
<Muted style={{ marginTop: '32px' }}>
Or open{' '}
<Link href={data.loginLink} style={linkStyle}>
{data.loginLink}
</Link>{' '}
and enter the code manually.
</Muted>
</BaseTemplate>
);
export default OneTimeAccessEmail;
const linkStyle = {
color: colors.mutedForeground,
textDecoration: "underline",
wordBreak: "break-all" as const,
color: colors.mutedForeground,
textDecoration: 'underline',
wordBreak: 'break-all' as const
};
OneTimeAccessEmail.TemplateProps = {
...sharedTemplateProps,
data: {
code: "{{.Data.Code}}",
loginLink: "{{.Data.LoginLink}}",
buttonCodeLink: "{{.Data.LoginLinkWithCode}}",
expirationString: "{{.Data.ExpirationString}}",
},
...sharedTemplateProps,
data: {
code: '{{.Data.Code}}',
loginLink: '{{.Data.LoginLink}}',
buttonCodeLink: '{{.Data.LoginLinkWithCode}}',
expirationString: '{{.Data.ExpirationString}}'
}
};
OneTimeAccessEmail.PreviewProps = {
...sharedPreviewProps,
data: {
code: "123456",
loginLink: "https://id.example.com/lc",
buttonCodeLink: "https://id.example.com/lc/123456",
expirationString: "15 minutes",
},
...sharedPreviewProps,
data: {
code: '123456',
loginLink: 'https://id.example.com/lc',
buttonCodeLink: 'https://id.example.com/lc/123456',
expirationString: '15 minutes'
}
};
+10 -14
View File
@@ -1,25 +1,21 @@
import { BaseTemplate } from "../components/base-template";
import CardHeader from "../components/card-header";
import { Paragraph } from "../components/text";
import {
type SharedProps,
sharedPreviewProps,
sharedTemplateProps,
} from "../props";
import { BaseTemplate } from '../components/base-template';
import CardHeader from '../components/card-header';
import { Paragraph } from '../components/text';
import { type SharedProps, sharedPreviewProps, sharedTemplateProps } from '../props';
export const TestEmail = (props: SharedProps) => (
<BaseTemplate {...props} preview="Your email setup is working correctly">
<CardHeader title="Test email" />
<Paragraph>Your email setup is working correctly!</Paragraph>
</BaseTemplate>
<BaseTemplate {...props} preview="Your email setup is working correctly">
<CardHeader title="Test email" />
<Paragraph>Your email setup is working correctly!</Paragraph>
</BaseTemplate>
);
export default TestEmail;
TestEmail.TemplateProps = {
...sharedTemplateProps,
...sharedTemplateProps
};
TestEmail.PreviewProps = {
...sharedPreviewProps,
...sharedPreviewProps
};
+22 -22
View File
@@ -1,24 +1,24 @@
{
"name": "pocketid-email-templates",
"version": "1.0.0",
"scripts": {
"preinstall": "npx only-allow pnpm",
"build": "tsx build.ts",
"build:watch": "tsx watch build.ts",
"dev": "email dev --port 3030",
"export": "email export"
},
"dependencies": {
"react": "^19.3.0",
"react-dom": "^19.3.0",
"react-email": "6.11.0"
},
"devDependencies": {
"@react-email/preview-server": "5.2.10",
"@react-email/ui": "6.11.0",
"@types/node": "^25.9.7",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"tsx": "^4.23.13"
}
"name": "pocketid-email-templates",
"version": "1.0.0",
"scripts": {
"preinstall": "npx only-allow pnpm",
"build": "tsx build.ts",
"build:watch": "tsx watch build.ts",
"dev": "email dev --port 3030",
"export": "email export"
},
"dependencies": {
"react": "^19.3.0",
"react-dom": "^19.3.0",
"react-email": "6.11.0"
},
"devDependencies": {
"@react-email/preview-server": "5.2.10",
"@react-email/ui": "6.11.0",
"@types/node": "^25.9.7",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"tsx": "^4.23.13"
}
}
+9 -9
View File
@@ -1,17 +1,17 @@
export interface SharedProps {
logoURL: string;
appName: string;
appURL: string;
logoURL: string;
appName: string;
appURL: string;
}
export const sharedPreviewProps: SharedProps = {
logoURL: "https://pocket-id.org/img/logo.png",
appName: "Pocket ID",
appURL: "https://id.example.com",
logoURL: 'https://pocket-id.org/img/logo.png',
appName: 'Pocket ID',
appURL: 'https://id.example.com'
};
export const sharedTemplateProps: SharedProps = {
logoURL: "{{.LogoURL}}",
appName: "{{.AppName}}",
appURL: "{{.AppURL}}",
logoURL: '{{.LogoURL}}',
appName: '{{.AppName}}',
appURL: '{{.AppURL}}'
};
+18 -24
View File
@@ -1,25 +1,19 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["node"]
},
"include": [
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules",
"dist"
]
}
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["node"]
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "dist"]
}
-9
View File
@@ -1,9 +0,0 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
# Compiled files
.svelte-kit/
build/
src/lib/paraglide/messages
-8
View File
@@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss", "prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
-40
View File
@@ -1,40 +0,0 @@
import js from '@eslint/js';
import ts from 'typescript-eslint';
import svelte from 'eslint-plugin-svelte';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/', 'src/lib/paraglide/', 'src/paraglide/']
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
// Pocket ID does not support deployments under a SvelteKit base path
'svelte/no-navigation-without-resolve': 'off'
}
}
];
+11 -18
View File
@@ -3,15 +3,19 @@
"version": "2.17.0",
"private": true,
"type": "module",
"imports": {
"#lib/*": "./src/lib/*"
},
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "vite dev --port 3000",
"build": "vite build",
"preview": "vite preview --port 3000",
"dev": "vp dev --port 3000",
"build": "vp build",
"preview": "vp preview --port 3000",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
"lint": "vp lint",
"format": "vp fmt",
"format:check": "vp fmt --check"
},
"dependencies": {
"@inlang/paraglide-js-svelte": "^1.0.3",
@@ -31,7 +35,6 @@
"zod": "^4.4.3"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@inlang/paraglide-js": "^2.18.0",
"@inlang/plugin-m-function-matcher": "^2.2.6",
"@inlang/plugin-message-format": "^4.4.0",
@@ -43,14 +46,7 @@
"@types/node": "^25.9.0",
"@types/qrcode": "^1.5.6",
"bits-ui": "^2.18.1",
"eslint": "^10.4.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.17.1",
"globals": "^17.6.0",
"mode-watcher": "^1.1.0",
"prettier": "^3.8.3",
"prettier-plugin-svelte": "^3.5.2",
"prettier-plugin-tailwindcss": "^0.8.0",
"rollup": "^4.63.3",
"rollup-plugin-sbom": "4.0.0",
"shadcn-svelte": "^1.3.0",
@@ -61,10 +57,7 @@
"tailwindcss": "^4.3.0",
"tw-animate-css": "^1.4.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.4",
"vite": "^8.0.16"
},
"imports": {
"#lib/*": "./src/lib/*"
"vite": "catalog:",
"vite-plus": "catalog:"
}
}
@@ -70,7 +70,7 @@
<button
{...props}
class={cn(
'w-full flex justify-start items-center font-normal h-9 bg-input/50 rounded-full p-3',
'bg-input/50 flex h-9 w-full items-center justify-start rounded-full p-3 font-normal',
!value && 'text-muted-foreground'
)}
aria-label={m.select_a_date()}
@@ -81,9 +81,9 @@
variant="outline"
role="combobox"
aria-expanded={open}
class="h-auto min-h-10 w-full bg-input/30!"
class="bg-input/30! h-auto min-h-10 w-full"
>
<div class="flex items-center justify-between w-full">
<div class="flex w-full items-center justify-between">
<div class="flex flex-wrap items-center gap-1">
{#if selectedItems.length > 0}
{#each selectedLabels as label, i (i)}
@@ -72,7 +72,7 @@
{...props}
class={restProps.class}
>
<span class="flex justify-between w-full">
<span class="flex w-full justify-between">
{items.find((item) => item.value === value)?.label || selectText}
<LucideChevronDown class="size-4 shrink-0 opacity-50" />
</span>
@@ -23,8 +23,8 @@
<div
class=" w-full {isAuthPage
? 'absolute top-0 z-10 mt-3 lg:mt-8 pr-2 lg:pr-3'
: 'pt-3 bg-muted/40 dark:bg-background '}"
? 'absolute top-0 z-10 mt-3 pr-2 lg:mt-8 lg:pr-3'
: 'bg-muted/40 dark:bg-background pt-3 '}"
>
<div
class="{!isAuthPage
@@ -35,8 +35,8 @@
{#if !isAuthPage}
<a href="/" class="flex items-center transition-opacity hover:opacity-80">
<Logo class="size-8" />
<Separator orientation="vertical" class="h-5! bg-neutral-600 ml-2 mr-3" />
<h1 class="text-2xl font-gloock" data-testid="application-name">
<Separator orientation="vertical" class="mr-3 ml-2 h-5! bg-neutral-600" />
<h1 class="font-gloock text-2xl" data-testid="application-name">
{m.settings()}
</h1>
</a>
@@ -14,13 +14,13 @@
<DropdownMenu.Trigger class={buttonVariants({ variant: 'ghost', size: 'icon' })}>
<SunIcon
class="h-[1.2rem] w-[1.2rem] !transition-all {isDark
? '-rotate-90 scale-0'
: 'rotate-0 scale-100'}"
? 'scale-0 -rotate-90'
: 'scale-100 rotate-0'}"
/>
<MoonIcon
class="absolute h-[1.2rem] w-[1.2rem] !transition-all {isDark
? 'rotate-0 scale-100'
: 'rotate-90 scale-0'}"
? 'scale-100 rotate-0'
: 'scale-0 rotate-90'}"
/>
<span class="sr-only">Toggle theme</span>
</DropdownMenu.Trigger>
@@ -58,7 +58,7 @@
: 'justify-center'}"
>
<div
class="relative z-10 flex h-full w-full max-w-[650px] 2xl:max-w-[800px] p-16 {cn(
class="relative z-10 flex h-full w-full max-w-[650px] p-16 2xl:max-w-[800px] {cn(
showAlternativeSignInMethodButton && 'pb-0'
)}"
>
@@ -88,7 +88,7 @@
src={cachedBackgroundImage.getUrl()}
class="{cn(
animate && 'animate-bg-zoom'
)} h-screen object-cover w-[calc(100vw-650px)] 2xl:w-[calc(100vw-800px)]"
)} h-screen w-[calc(100vw-650px)] object-cover 2xl:w-[calc(100vw-800px)]"
alt={m.login_background()}
/>
</div>
@@ -102,7 +102,7 @@
<Card.Root
class={{
'mx-3 w-full max-w-md': true,
'bg-transparent border-0': !backgroundImageExists
'border-0 bg-transparent': !backgroundImageExists
}}
>
<Card.CardContent
@@ -35,7 +35,7 @@
const showProviderIcon = $derived(!!providerIconUrl && !iconFailed);
</script>
<Item.Root variant="transparent" class="hover:bg-muted transition-colors py-3 px-0 sm:px-4">
<Item.Root variant="transparent" class="hover:bg-muted px-0 py-3 transition-colors sm:px-4">
<Item.Media class="bg-muted text-muted-foreground size-11 rounded-xl">
{#if showProviderIcon}
<img
@@ -13,8 +13,8 @@
const SvelteComponent = $derived(icon);
</script>
<Item.Root class="py-1.5 px-0">
<Item.Media class="bg-muted !self-center rounded-lg p-2 !translate-y-0 h-full">
<Item.Root class="px-0 py-1.5">
<Item.Media class="bg-muted h-full !translate-y-0 !self-center rounded-lg p-2">
<SvelteComponent class="size-5" />
</Item.Media>
<Item.Content class="text-start">
@@ -41,7 +41,7 @@
class="order-dashed"
data-testid={`facet-${title.toLowerCase()}-trigger`}
>
<span class="flex gap-2 items-center">
<span class="flex items-center gap-2">
<ListFilterIcon />
{title}
</span>
@@ -243,7 +243,7 @@
class="h-12 w-full justify-start px-4 font-medium hover:bg-transparent"
onclick={() => onSort(column.column)}
>
<span class="flex items-center w-full">
<span class="flex w-full items-center">
{column.label}
<ChevronDown
class={cn(
@@ -24,7 +24,7 @@
data-slot="alert-dialog-content"
data-size={size}
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-popover text-popover-foreground ring-foreground/5 dark:ring-foreground/10 gap-6 rounded-4xl p-6 shadow-xl ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-md group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none',
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-popover text-popover-foreground ring-foreground/5 dark:ring-foreground/10 group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-6 rounded-4xl p-6 shadow-xl ring-1 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-md',
className
)}
{...restProps}
@@ -13,7 +13,7 @@
bind:ref
data-slot="alert-dialog-overlay"
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/30 duration-100 supports-backdrop-filter:backdrop-blur-sm fixed inset-0 z-50',
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/30 duration-100 supports-backdrop-filter:backdrop-blur-sm',
className
)}
{...restProps}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="alert-description"
class={cn(
'text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3',
'text-muted-foreground [&_a]:hover:text-foreground text-sm text-balance md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4',
className
)}
{...restProps}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="alert-title"
class={cn(
'font-medium group-has-[>svg]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3',
'[&_a]:hover:text-foreground font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3',
className
)}
{...restProps}
@@ -13,7 +13,7 @@
bind:ref
data-slot="avatar-fallback"
class={cn(
'bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs',
'bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs',
className
)}
{...restProps}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="avatar-group-count"
class={cn(
'bg-muted text-muted-foreground size-8 rounded-full text-sm group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2',
'bg-muted text-muted-foreground ring-background relative flex size-8 shrink-0 items-center justify-center rounded-full text-sm ring-2 group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
className
)}
{...restProps}
@@ -12,6 +12,6 @@
<AvatarPrimitive.Image
bind:ref
data-slot="avatar-image"
class={cn('rounded-full aspect-square size-full object-cover', className)}
class={cn('aspect-square size-full rounded-full object-cover', className)}
{...restProps}
/>
@@ -19,7 +19,7 @@
data-slot="avatar"
data-size={size}
class={cn(
'size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 after:border-border group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten',
'after:border-border group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten',
className
)}
{...restProps}
@@ -51,7 +51,7 @@ get along, so we shut typescript up by casting `value` to `never`.
{weekdayFormat}
{disableDaysOutsideMonth}
class={cn(
'p-3 [--cell-radius:var(--radius-4xl)] [--cell-size:--spacing(8)] bg-background group/calendar in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent',
'bg-background group/calendar p-3 [--cell-radius:var(--radius-4xl)] [--cell-size:--spacing(8)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent',
className
)}
{locale}
@@ -13,7 +13,7 @@
<p
bind:this={ref}
data-slot="card-description"
class={cn('text-muted-foreground text-sm mt-1', className)}
class={cn('text-muted-foreground mt-1 text-sm', className)}
{...restProps}
>
{@render children?.()}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="card-footer"
class={cn(
'rounded-b-4xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center',
'flex items-center rounded-b-4xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4',
className
)}
{...restProps}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="card-header"
class={cn(
'gap-1.5 rounded-t-4xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]',
'group/card-header @container/card-header grid auto-rows-min items-start gap-1.5 rounded-t-4xl px-6 group-data-[size=sm]/card:px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4',
className
)}
{...restProps}
@@ -16,7 +16,7 @@
data-slot="card"
data-size={size}
class={cn(
'bg-card text-card-foreground border-foreground/5 dark:border-foreground/10 gap-6 overflow-hidden rounded-4xl py-6 text-sm shadow-sm border-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-4xl *:[img:last-child]:rounded-b-4xl group/card flex flex-col',
'bg-card text-card-foreground border-foreground/5 dark:border-foreground/10 group/card flex flex-col gap-6 overflow-hidden rounded-4xl border-1 py-6 text-sm shadow-sm has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-4xl *:[img:last-child]:rounded-b-4xl',
className
)}
{...restProps}
@@ -17,7 +17,7 @@
bind:ref
data-slot="checkbox"
class={cn(
'bg-input/90 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[5px] border border-transparent transition-shadow group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50',
'bg-input/90 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 peer relative flex size-4 shrink-0 items-center justify-center rounded-[5px] border border-transparent transition-shadow outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3',
className
)}
bind:checked
@@ -27,7 +27,7 @@
{#snippet children({ checked, indeterminate })}
<div
data-slot="checkbox-indicator"
class="[&>svg]:size-3.5 grid place-content-center text-current transition-none"
class="grid place-content-center text-current transition-none [&>svg]:size-3.5"
>
{#if checked}
<CheckIcon />
@@ -33,7 +33,7 @@
<Dialog.Description>{description}</Dialog.Description>
</Dialog.Header>
<Dialog.Content
class={cn('rounded-4xl! p-0 top-1/3 translate-y-0 overflow-hidden p-0', className)}
class={cn('top-1/3 translate-y-0 overflow-hidden rounded-4xl! p-0 p-0', className)}
{showCloseButton}
{portalProps}
>
@@ -18,7 +18,7 @@
bind:ref
data-slot="command-group"
class={cn(
'text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden py-2 px-1 **:[[cmdk-group-heading]]:px-3 **:[[cmdk-group-heading]]:py-2 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium',
'text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden px-1 py-2 **:[[cmdk-group-heading]]:px-3 **:[[cmdk-group-heading]]:py-2 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium',
className
)}
value={value ?? heading ?? `----${useId()}`}
@@ -13,7 +13,7 @@
bind:ref
data-slot="command-list"
class={cn(
'no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto',
'no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none',
className
)}
{...restProps}
@@ -21,7 +21,7 @@
bind:ref
data-slot="command"
class={cn(
'bg-popover text-popover-foreground rounded-4xl p-1 flex size-full flex-col overflow-hidden',
'bg-popover text-popover-foreground flex size-full flex-col overflow-hidden rounded-4xl p-1',
className
)}
{...restProps}
@@ -28,7 +28,7 @@
bind:ref
data-slot="dialog-content"
class={cn(
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/5 dark:ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-6 rounded-4xl p-6 text-sm shadow-xl ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none',
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/5 dark:ring-foreground/10 fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-4xl p-6 text-sm shadow-xl ring-1 duration-100 outline-none sm:max-w-md',
className
)}
{...restProps}
@@ -18,7 +18,7 @@
<div
bind:this={ref}
data-slot="dialog-footer"
class={cn('gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
class={cn('flex flex-col-reverse gap-2 gap-2 sm:flex-row sm:justify-end', className)}
{...restProps}
>
{@render children?.()}
@@ -13,7 +13,7 @@
<div
bind:this={ref}
data-slot="dialog-header"
class={cn('gap-1.5 flex flex-col', className)}
class={cn('flex flex-col gap-1.5', className)}
{...restProps}
>
{@render children?.()}
@@ -13,7 +13,7 @@
bind:ref
data-slot="dialog-overlay"
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/30 duration-100 supports-backdrop-filter:backdrop-blur-sm fixed inset-0 isolate z-50',
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 isolate z-50 bg-black/30 duration-100 supports-backdrop-filter:backdrop-blur-sm',
className
)}
{...restProps}
@@ -23,14 +23,14 @@
bind:indeterminate
data-slot="dropdown-menu-checkbox-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2.5 rounded-2xl py-2 pr-8 pl-3 text-sm font-medium data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2.5 rounded-2xl py-2 pr-8 pl-3 text-sm font-medium outline-hidden select-none data-inset:pl-9.5 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<span
class="absolute right-2 flex items-center justify-center pointer-events-none"
class="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-checkbox-item-indicator"
>
{#if indeterminate}
@@ -23,7 +23,7 @@
{sideOffset}
{align}
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 dark:ring-foreground/10 text-popover-foreground min-w-48 rounded-3xl p-1.5 shadow-lg ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!',
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 dark:ring-foreground/10 text-popover-foreground data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 bg-popover/70 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground! relative z-50 min-w-48 animate-none! overflow-x-hidden overflow-y-auto rounded-3xl p-1.5 shadow-lg ring-1 duration-100 outline-none before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 data-closed:overflow-hidden',
className
)}
{...restProps}
@@ -20,7 +20,7 @@
data-inset={inset}
data-variant={variant}
class={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2.5 rounded-2xl px-3 py-1.5 text-sm font-medium data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-pointer items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground group/dropdown-menu-item relative flex cursor-pointer items-center gap-2.5 rounded-2xl px-3 py-1.5 text-sm font-medium outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-9.5 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
@@ -15,14 +15,14 @@
bind:ref
data-slot="dropdown-menu-radio-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2.5 rounded-2xl py-2 pr-8 pl-3 text-sm font-medium data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2.5 rounded-2xl py-2 pr-8 pl-3 text-sm font-medium outline-hidden select-none data-inset:pl-9.5 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span
class="absolute right-2 flex items-center justify-center pointer-events-none"
class="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
{#if checked}
@@ -13,7 +13,7 @@
bind:ref
data-slot="dropdown-menu-sub-content"
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 dark:ring-foreground/10 text-popover-foreground min-w-36 rounded-3xl p-1.5 shadow-lg ring-1 duration-100 w-auto animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!',
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 dark:ring-foreground/10 text-popover-foreground bg-popover/70 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground! relative w-auto min-w-36 animate-none! rounded-3xl p-1.5 shadow-lg ring-1 duration-100 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150',
className
)}
{...restProps}
@@ -19,7 +19,7 @@
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
class={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-2xl px-3 py-1.5 text-sm font-medium data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground flex cursor-default items-center gap-2 rounded-2xl px-3 py-1.5 text-sm font-medium outline-hidden select-none data-inset:pl-9.5 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="empty-content"
class={cn(
'gap-4 text-sm flex w-full max-w-sm min-w-0 flex-col items-center text-balance',
'flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance',
className
)}
{...restProps}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="empty-description"
class={cn(
'text-sm/relaxed text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
'text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
className
)}
{...restProps}
@@ -13,7 +13,7 @@
<div
bind:this={ref}
data-slot="empty-header"
class={cn('gap-2 flex max-w-sm flex-col items-center', className)}
class={cn('flex max-w-sm flex-col items-center gap-2', className)}
{...restProps}
>
{@render children?.()}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="empty"
class={cn(
'gap-4 rounded-2xl border-dashed p-12 flex w-full min-w-0 flex-1 flex-col items-center justify-center text-center text-balance',
'flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-2xl border-dashed p-12 text-center text-balance',
className
)}
{...restProps}
@@ -13,7 +13,7 @@
<div
bind:this={ref}
data-slot="field-content"
class={cn('gap-1 group/field-content flex flex-1 flex-col leading-snug', className)}
class={cn('group/field-content flex flex-1 flex-col gap-1 leading-snug', className)}
{...restProps}
>
{@render children?.()}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="field-description"
class={cn(
'text-muted-foreground text-left text-sm [[data-variant=legend]+&]:-mt-1.5 leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
'text-muted-foreground text-left text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance [[data-variant=legend]+&]:-mt-1.5',
'last:mt-0 nth-last-2:-mt-1',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
className
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="field-group"
class={cn(
'gap-7 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4 group/field-group @container/field-group flex w-full flex-col',
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4',
className
)}
{...restProps}
@@ -18,7 +18,7 @@
bind:ref
data-slot="field-label"
class={cn(
'has-data-checked:bg-input/30 gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-2xl has-[>[data-slot=field]]:border *:data-[slot=field]:p-4 group/field-label peer/field-label flex w-fit leading-snug',
'has-data-checked:bg-input/30 group/field-label peer/field-label flex w-fit gap-2 leading-snug leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-2xl has-[>[data-slot=field]]:border *:data-[slot=field]:p-4',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col',
required && "after:text-red-500 after:content-['*']",
className
@@ -20,13 +20,13 @@
bind:this={ref}
data-slot="field-separator"
data-content={hasContent}
class={cn('-my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2 relative', className)}
class={cn('relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2', className)}
{...restProps}
>
<Separator class="absolute inset-0 top-1/2" />
{#if children}
<span
class="text-muted-foreground px-2 bg-background relative mx-auto block w-fit"
class="text-muted-foreground bg-background relative mx-auto block w-fit px-2"
data-slot="field-separator-content"
>
{@render children()}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="field-set"
class={cn(
'gap-6 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 flex flex-col',
'flex flex-col gap-6 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
className
)}
{...restProps}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="field-label"
class={cn(
'gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50 flex w-fit items-center leading-snug',
'flex w-fit items-center gap-2 text-sm leading-snug leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
className
)}
{...restProps}
@@ -15,7 +15,7 @@
bind:ref
data-slot="input-group-control"
class={cn(
'rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent flex-1',
'flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent',
className
)}
bind:value
@@ -13,7 +13,7 @@
<span
bind:this={ref}
class={cn(
"text-muted-foreground gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none",
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
@@ -15,7 +15,7 @@
bind:ref
data-slot="input-group-control"
class={cn(
'rounded-none border-0 bg-transparent py-2.5 shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent flex-1 resize-none',
'flex-1 resize-none rounded-none border-0 bg-transparent py-2.5 shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent',
className
)}
bind:value
@@ -15,7 +15,7 @@
data-slot="input-group"
role="group"
class={cn(
'group/input-group bg-input/50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 h-9 rounded-4xl border border-transparent transition-[color,box-shadow,background-color] in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-data-[align=block-end]:rounded-3xl has-data-[align=block-start]:rounded-3xl has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[textarea]:rounded-2xl has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto',
'group/input-group bg-input/50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 relative flex h-9 w-full min-w-0 items-center rounded-4xl border border-transparent transition-[color,box-shadow,background-color] outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-data-[align=block-end]:rounded-3xl has-data-[align=block-start]:rounded-3xl has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[textarea]:rounded-2xl has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5',
className
)}
{...props}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="input-otp-group"
class={cn(
'gap-1.5 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive rounded-3xl has-aria-invalid:ring-3 flex items-center',
'has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive flex items-center gap-1.5 rounded-3xl has-aria-invalid:ring-3',
className
)}
{...restProps}
@@ -16,7 +16,7 @@
bind:this={ref}
data-slot="input-otp-separator"
role="separator"
class={cn("[&_svg:not([class*='size-'])]:size-4 flex items-center", className)}
class={cn("flex items-center [&_svg:not([class*='size-'])]:size-4", className)}
{...restProps}
>
{#if children}
@@ -15,7 +15,7 @@
bind:ref
data-slot="input-otp-slot"
class={cn(
'w-10 h-13 rounded-sm bg-input/50 border-input data-active:border-ring data-active:ring-ring/30 data-active:aria-invalid:ring-destructive/20 dark:data-active:aria-invalid:ring-destructive/40 aria-invalid:border-destructive border text-sm transition-all outline-none data-active:ring-3 relative flex items-center justify-center data-active:z-10',
'bg-input/50 border-input data-active:border-ring data-active:ring-ring/30 data-active:aria-invalid:ring-destructive/20 dark:data-active:aria-invalid:ring-destructive/40 aria-invalid:border-destructive relative flex h-13 w-10 items-center justify-center rounded-sm border text-sm transition-all outline-none data-active:z-10 data-active:ring-3',
className
)}
{...restProps}
@@ -25,9 +25,7 @@
<div
class="cn-input-otp-caret pointer-events-none absolute inset-0 flex items-center justify-center"
>
<div
class="animate-caret-blink bg-foreground h-4 w-px duration-1000 bg-foreground h-4 w-px"
></div>
<div class="animate-caret-blink bg-foreground bg-foreground h-4 w-px duration-1000"></div>
</div>
{/if}
</InputOTPPrimitive.Cell>
@@ -19,7 +19,7 @@
data-lpignore="true"
data-1p-ignore="true"
class={cn(
'cn-input-otp-input gap-2 flex items-center disabled:cursor-not-allowed has-disabled:opacity-50',
'cn-input-otp-input flex items-center gap-2 disabled:cursor-not-allowed has-disabled:opacity-50',
className
)}
{...restProps}
@@ -25,7 +25,7 @@
bind:this={ref}
data-slot={dataSlot}
class={cn(
'bg-input/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-3xl border border-transparent px-3 py-1 text-base transition-[color,box-shadow,background-color] file:h-7 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
'bg-input/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 file:text-foreground placeholder:text-muted-foreground h-9 w-full min-w-0 rounded-3xl border border-transparent px-3 py-1 text-base transition-[color,box-shadow,background-color] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
className
)}
type="file"
@@ -38,7 +38,7 @@
bind:this={ref}
data-slot={dataSlot}
class={cn(
'bg-input/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-3xl border border-transparent px-3 py-1 text-base transition-[color,box-shadow,background-color] file:h-7 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
'bg-input/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 file:text-foreground placeholder:text-muted-foreground h-9 w-full min-w-0 rounded-3xl border border-transparent px-3 py-1 text-base transition-[color,box-shadow,background-color] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
className
)}
{type}
@@ -13,7 +13,7 @@
<div
bind:this={ref}
data-slot="item-actions"
class={cn('gap-2 flex items-center', className)}
class={cn('flex items-center gap-2', className)}
{...restProps}
>
{@render children?.()}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="item-content"
class={cn(
'gap-1 group-data-[size=xs]/item:gap-0.5 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none',
'flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0.5 [&+[data-slot=item-content]]:flex-none',
className
)}
{...restProps}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="item-description"
class={cn(
'text-muted-foreground text-left text-sm [&>a:hover]:text-primary line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4',
'text-muted-foreground [&>a:hover]:text-primary line-clamp-2 text-left text-sm font-normal [&>a]:underline [&>a]:underline-offset-4',
className
)}
{...restProps}
@@ -13,7 +13,7 @@
<div
bind:this={ref}
data-slot="item-footer"
class={cn('gap-2 flex basis-full items-center justify-between', className)}
class={cn('flex basis-full items-center justify-between gap-2', className)}
{...restProps}
>
{@render children?.()}
@@ -15,7 +15,7 @@
role="list"
data-slot="item-group"
class={cn(
'gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2 group/item-group flex w-full flex-col',
'group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2',
className
)}
{...restProps}
@@ -13,7 +13,7 @@
<div
bind:this={ref}
data-slot="item-header"
class={cn('gap-2 flex basis-full items-center justify-between', className)}
class={cn('flex basis-full items-center justify-between gap-2', className)}
{...restProps}
>
{@render children?.()}
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="item-title"
class={cn(
'gap-2 text-sm leading-snug font-semibold underline-offset-4 line-clamp-1 flex w-fit items-center',
'line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-semibold underline-offset-4',
className
)}
{...restProps}
@@ -14,7 +14,7 @@
bind:ref
data-slot="label"
class={cn(
'gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed',
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
className
)}
{...restProps}
@@ -13,7 +13,7 @@
<ul
bind:this={ref}
data-slot="pagination-content"
class={cn('gap-1 flex items-center', className)}
class={cn('flex items-center gap-1', className)}
{...restProps}
>
{@render children?.()}

Some files were not shown because too many files have changed in this diff Show More