visual improvements
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node --import tsx src/app.ts",
|
||||
"dev": "tsx watch src/app.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/app.js"
|
||||
},
|
||||
|
||||
+100
-23
@@ -39,6 +39,11 @@ const allowedOrigins = Array.from(
|
||||
);
|
||||
|
||||
const dateStringSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
||||
const chartColorSchema = z.string().regex(/^#[0-9a-fA-F]{6}$/);
|
||||
const photoDataUrlSchema = z
|
||||
.string()
|
||||
.regex(/^data:image\/(?:png|jpeg|jpg|webp|gif);base64,[A-Za-z0-9+/=]+$/)
|
||||
.max(1_500_000);
|
||||
|
||||
const birdSchema = z.object({
|
||||
name: z.string().trim().min(1).max(120),
|
||||
@@ -46,6 +51,8 @@ const birdSchema = z.object({
|
||||
species: z.string().trim().min(1).max(120),
|
||||
dateOfBirth: dateStringSchema.optional().or(z.literal('')),
|
||||
gotchaDay: dateStringSchema.optional().or(z.literal('')),
|
||||
chartColor: chartColorSchema.optional(),
|
||||
photoDataUrl: photoDataUrlSchema.optional().or(z.literal('')),
|
||||
});
|
||||
|
||||
const weightSchema = z.object({
|
||||
@@ -68,6 +75,8 @@ type BirdRow = {
|
||||
species: string;
|
||||
date_of_birth: string | null;
|
||||
gotcha_day: string | null;
|
||||
chart_color: string;
|
||||
photo_data_url: string | null;
|
||||
created_at: string;
|
||||
latest_weight_grams: string | null;
|
||||
latest_recorded_on: string | null;
|
||||
@@ -111,7 +120,7 @@ app.use(
|
||||
legacyHeaders: false,
|
||||
}),
|
||||
);
|
||||
app.use(express.json({ limit: '300kb' }));
|
||||
app.use(express.json({ limit: '2mb' }));
|
||||
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
|
||||
|
||||
const emptyToNull = (value?: string) => {
|
||||
@@ -126,11 +135,27 @@ const normalizeBird = (row: BirdRow) => ({
|
||||
species: row.species,
|
||||
dateOfBirth: row.date_of_birth,
|
||||
gotchaDay: row.gotcha_day,
|
||||
chartColor: row.chart_color,
|
||||
photoDataUrl: row.photo_data_url,
|
||||
createdAt: row.created_at,
|
||||
latestWeightGrams: row.latest_weight_grams ? Number(row.latest_weight_grams) : null,
|
||||
latestRecordedOn: row.latest_recorded_on,
|
||||
});
|
||||
|
||||
const birdSelectFields = `
|
||||
birds.id,
|
||||
birds.name,
|
||||
birds.tag_id,
|
||||
birds.species,
|
||||
birds.date_of_birth::text,
|
||||
birds.gotcha_day::text,
|
||||
birds.chart_color,
|
||||
birds.photo_data_url,
|
||||
birds.created_at,
|
||||
latest.weight_grams AS latest_weight_grams,
|
||||
latest.recorded_on::text AS latest_recorded_on
|
||||
`;
|
||||
|
||||
const normalizeWeight = (row: WeightRow) => ({
|
||||
id: row.id,
|
||||
birdId: row.bird_id,
|
||||
@@ -159,12 +184,16 @@ const ensureSchema = async () => {
|
||||
species VARCHAR(120) NOT NULL,
|
||||
date_of_birth DATE,
|
||||
gotcha_day DATE,
|
||||
chart_color VARCHAR(7) NOT NULL DEFAULT '#cb3a35',
|
||||
photo_data_url TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
ALTER TABLE birds
|
||||
ADD COLUMN IF NOT EXISTS date_of_birth DATE,
|
||||
ADD COLUMN IF NOT EXISTS gotcha_day DATE;
|
||||
ADD COLUMN IF NOT EXISTS gotcha_day DATE,
|
||||
ADD COLUMN IF NOT EXISTS chart_color VARCHAR(7) NOT NULL DEFAULT '#cb3a35',
|
||||
ADD COLUMN IF NOT EXISTS photo_data_url TEXT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS weight_records (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -197,15 +226,7 @@ const ensureSchema = async () => {
|
||||
const getBirdById = async (birdId: string) => {
|
||||
const result = await pool.query<BirdRow>(
|
||||
`SELECT
|
||||
birds.id,
|
||||
birds.name,
|
||||
birds.tag_id,
|
||||
birds.species,
|
||||
birds.date_of_birth::text,
|
||||
birds.gotcha_day::text,
|
||||
birds.created_at,
|
||||
latest.weight_grams AS latest_weight_grams,
|
||||
latest.recorded_on::text AS latest_recorded_on
|
||||
${birdSelectFields}
|
||||
FROM birds
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT weight_grams, recorded_on
|
||||
@@ -229,15 +250,7 @@ app.get('/api/birds', async (_req: Request, res: Response, next: NextFunction) =
|
||||
try {
|
||||
const result = await pool.query<BirdRow>(`
|
||||
SELECT
|
||||
birds.id,
|
||||
birds.name,
|
||||
birds.tag_id,
|
||||
birds.species,
|
||||
birds.date_of_birth::text,
|
||||
birds.gotcha_day::text,
|
||||
birds.created_at,
|
||||
latest.weight_grams AS latest_weight_grams,
|
||||
latest.recorded_on::text AS latest_recorded_on
|
||||
${birdSelectFields}
|
||||
FROM birds
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT weight_grams, recorded_on
|
||||
@@ -265,15 +278,17 @@ app.post('/api/birds', async (req: Request, res: Response, next: NextFunction) =
|
||||
|
||||
try {
|
||||
const result = await pool.query<BirdRow>(
|
||||
`INSERT INTO birds (name, tag_id, species, date_of_birth, gotcha_day)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, name, tag_id, species, date_of_birth::text, gotcha_day::text, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
|
||||
`INSERT INTO birds (name, tag_id, species, date_of_birth, gotcha_day, chart_color, photo_data_url)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, name, tag_id, species, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
|
||||
[
|
||||
parsed.data.name,
|
||||
parsed.data.tagId,
|
||||
parsed.data.species,
|
||||
emptyToNull(parsed.data.dateOfBirth),
|
||||
emptyToNull(parsed.data.gotchaDay),
|
||||
parsed.data.chartColor ?? '#cb3a35',
|
||||
emptyToNull(parsed.data.photoDataUrl),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -288,6 +303,68 @@ app.post('/api/birds', async (req: Request, res: Response, next: NextFunction) =
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/birds/:birdId', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const parsed = birdSchema.safeParse(req.body);
|
||||
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ error: 'Invalid bird payload', details: parsed.error.flatten() });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await pool.query<BirdRow>(
|
||||
`UPDATE birds
|
||||
SET name = $2,
|
||||
tag_id = $3,
|
||||
species = $4,
|
||||
date_of_birth = $5,
|
||||
gotcha_day = $6,
|
||||
chart_color = $7,
|
||||
photo_data_url = $8
|
||||
WHERE id = $1
|
||||
RETURNING id, name, tag_id, species, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, created_at,
|
||||
(
|
||||
SELECT weight_grams::text
|
||||
FROM weight_records
|
||||
WHERE bird_id = birds.id
|
||||
ORDER BY recorded_on DESC
|
||||
LIMIT 1
|
||||
) AS latest_weight_grams,
|
||||
(
|
||||
SELECT recorded_on::text
|
||||
FROM weight_records
|
||||
WHERE bird_id = birds.id
|
||||
ORDER BY recorded_on DESC
|
||||
LIMIT 1
|
||||
) AS latest_recorded_on`,
|
||||
[
|
||||
req.params.birdId,
|
||||
parsed.data.name,
|
||||
parsed.data.tagId,
|
||||
parsed.data.species,
|
||||
emptyToNull(parsed.data.dateOfBirth),
|
||||
emptyToNull(parsed.data.gotchaDay),
|
||||
parsed.data.chartColor ?? '#cb3a35',
|
||||
emptyToNull(parsed.data.photoDataUrl),
|
||||
],
|
||||
);
|
||||
|
||||
if (!result.rowCount) {
|
||||
res.status(404).json({ error: 'Bird not found.' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ bird: normalizeBird(result.rows[0]) });
|
||||
} catch (error) {
|
||||
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
|
||||
res.status(409).json({ error: 'That tag ID is already in use.' });
|
||||
return;
|
||||
}
|
||||
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/birds/:birdId', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const result = await pool.query<{ id: string }>('DELETE FROM birds WHERE id = $1 RETURNING id', [req.params.birdId]);
|
||||
|
||||
@@ -5,7 +5,6 @@ RUN npm install
|
||||
COPY tsconfig*.json ./
|
||||
COPY vite.config.ts ./
|
||||
COPY index.html ./
|
||||
COPY public ./public
|
||||
COPY src ./src
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "dev", "--", "--host"]
|
||||
|
||||
+656
-300
File diff suppressed because it is too large
Load Diff
+204
-45
@@ -11,10 +11,12 @@
|
||||
--accent-teal: #2f8f98;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(203, 58, 53, 0.24), transparent 24%),
|
||||
radial-gradient(circle at 88% 22%, rgba(39, 105, 179, 0.22), transparent 20%),
|
||||
radial-gradient(circle at bottom right, rgba(35, 138, 90, 0.24), transparent 28%),
|
||||
linear-gradient(180deg, #fff3e8 0%, #f6ead7 55%, #eef7f2 100%);
|
||||
radial-gradient(circle at 14% 10%, rgba(222, 124, 58, 0.28), transparent 22%),
|
||||
radial-gradient(circle at 82% 12%, rgba(53, 136, 110, 0.26), transparent 20%),
|
||||
radial-gradient(circle at 24% 84%, rgba(221, 179, 78, 0.2), transparent 22%),
|
||||
radial-gradient(circle at 86% 78%, rgba(43, 118, 92, 0.24), transparent 24%),
|
||||
radial-gradient(circle at 62% 54%, rgba(48, 114, 160, 0.14), transparent 16%),
|
||||
linear-gradient(180deg, #fef5e7 0%, #e9ddba 46%, #d9eadf 100%);
|
||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
@@ -34,11 +36,27 @@ body,
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--ink);
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
opacity: 0.32;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 720 720'%3E%3Crect width='720' height='720' fill='none'/%3E%3Cg fill='none' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3Cg stroke='%236db7b4' transform='translate(72 88) rotate(-18)'%3E%3Cpath d='M0 0 L-24 -42'/%3E%3Cpath d='M0 0 L12 -50'/%3E%3Cpath d='M0 0 L44 -18'/%3E%3C/g%3E%3Cg stroke='%2396c489' transform='translate(248 70) rotate(14)'%3E%3Cpath d='M0 0 L-28 -38'/%3E%3Cpath d='M0 0 L4 -52'/%3E%3Cpath d='M0 0 L40 -30'/%3E%3C/g%3E%3Cg stroke='%23f5a347' transform='translate(430 92) rotate(-10)'%3E%3Cpath d='M0 0 L-30 -32'/%3E%3Cpath d='M0 0 L10 -50'/%3E%3Cpath d='M0 0 L46 -14'/%3E%3C/g%3E%3Cg stroke='%23888690' transform='translate(604 64) rotate(20)'%3E%3Cpath d='M0 0 L-26 -36'/%3E%3Cpath d='M0 0 L2 -54'/%3E%3Cpath d='M0 0 L36 -34'/%3E%3C/g%3E%3Cg stroke='%23b5c948' transform='translate(126 220) rotate(8)'%3E%3Cpath d='M0 0 L-22 -46'/%3E%3Cpath d='M0 0 L6 -54'/%3E%3Cpath d='M0 0 L42 -26'/%3E%3C/g%3E%3Cg stroke='%236db7b4' transform='translate(330 246) rotate(-24)'%3E%3Cpath d='M0 0 L-28 -34'/%3E%3Cpath d='M0 0 L0 -56'/%3E%3Cpath d='M0 0 L38 -30'/%3E%3C/g%3E%3Cg stroke='%23888690' transform='translate(520 224) rotate(18)'%3E%3Cpath d='M0 0 L-20 -46'/%3E%3Cpath d='M0 0 L6 -52'/%3E%3Cpath d='M0 0 L34 -36'/%3E%3C/g%3E%3Cg stroke='%2396c489' transform='translate(654 220) rotate(-14)'%3E%3Cpath d='M0 0 L-34 -26'/%3E%3Cpath d='M0 0 L-4 -54'/%3E%3Cpath d='M0 0 L34 -38'/%3E%3C/g%3E%3Cg stroke='%23f5a347' transform='translate(62 402) rotate(22)'%3E%3Cpath d='M0 0 L-30 -28'/%3E%3Cpath d='M0 0 L2 -54'/%3E%3Cpath d='M0 0 L38 -26'/%3E%3C/g%3E%3Cg stroke='%23888690' transform='translate(250 386) rotate(-18)'%3E%3Cpath d='M0 0 L-24 -42'/%3E%3Cpath d='M0 0 L10 -50'/%3E%3Cpath d='M0 0 L40 -22'/%3E%3C/g%3E%3Cg stroke='%2396c489' transform='translate(438 410) rotate(10)'%3E%3Cpath d='M0 0 L-28 -38'/%3E%3Cpath d='M0 0 L4 -54'/%3E%3Cpath d='M0 0 L44 -24'/%3E%3C/g%3E%3Cg stroke='%236db7b4' transform='translate(610 392) rotate(-20)'%3E%3Cpath d='M0 0 L-26 -34'/%3E%3Cpath d='M0 0 L8 -54'/%3E%3Cpath d='M0 0 L40 -18'/%3E%3C/g%3E%3Cg stroke='%23b5c948' transform='translate(122 564) rotate(16)'%3E%3Cpath d='M0 0 L-30 -30'/%3E%3Cpath d='M0 0 L2 -56'/%3E%3Cpath d='M0 0 L44 -20'/%3E%3C/g%3E%3Cg stroke='%23888690' transform='translate(330 576) rotate(-12)'%3E%3Cpath d='M0 0 L-26 -40'/%3E%3Cpath d='M0 0 L10 -52'/%3E%3Cpath d='M0 0 L40 -28'/%3E%3C/g%3E%3Cg stroke='%23f5a347' transform='translate(520 566) rotate(24)'%3E%3Cpath d='M0 0 L-28 -34'/%3E%3Cpath d='M0 0 L2 -54'/%3E%3Cpath d='M0 0 L42 -24'/%3E%3C/g%3E%3Cg stroke='%2396c489' transform='translate(678 586) rotate(-18)'%3E%3Cpath d='M0 0 L-26 -38'/%3E%3Cpath d='M0 0 L6 -54'/%3E%3Cpath d='M0 0 L36 -28'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||
background-position: center center;
|
||||
background-repeat: repeat;
|
||||
background-size: 360px 360px;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
@@ -51,7 +69,8 @@ button:disabled {
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
border: 1px solid rgba(39, 105, 179, 0.16);
|
||||
@@ -62,7 +81,8 @@ textarea {
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: rgba(39, 105, 179, 0.62);
|
||||
box-shadow: 0 0 0 4px rgba(39, 105, 179, 0.14);
|
||||
@@ -73,11 +93,25 @@ textarea {
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
max-width: 1280px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: 240px minmax(0, 1fr);
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.content-shell {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.side-nav {
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
display: grid;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.stack-grid {
|
||||
@@ -107,22 +141,6 @@ textarea {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-card::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: auto -8% -42% auto;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
border-radius: 50%;
|
||||
background:
|
||||
radial-gradient(circle at 35% 35%, rgba(240, 182, 63, 0.62), transparent 26%),
|
||||
radial-gradient(circle at 58% 44%, rgba(35, 138, 90, 0.52), transparent 32%),
|
||||
radial-gradient(circle at 72% 62%, rgba(39, 105, 179, 0.5), transparent 30%),
|
||||
radial-gradient(circle at 42% 74%, rgba(203, 58, 53, 0.52), transparent 32%);
|
||||
pointer-events: none;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.hero-card h1,
|
||||
.panel h2 {
|
||||
margin: 0;
|
||||
@@ -135,21 +153,18 @@ textarea {
|
||||
}
|
||||
|
||||
.page-tabs {
|
||||
display: flex;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1.25rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.page-tab {
|
||||
border: 1px solid rgba(39, 105, 179, 0.14);
|
||||
border-radius: 999px;
|
||||
padding: 0.7rem 1.1rem;
|
||||
border-radius: 18px;
|
||||
padding: 0.95rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.54);
|
||||
color: var(--ink);
|
||||
transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.page-tab.active {
|
||||
@@ -265,11 +280,49 @@ textarea {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bird-list {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.bird-card-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.85rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bird-avatar,
|
||||
.profile-photo {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
object-fit: cover;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(39, 105, 179, 0.16);
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
}
|
||||
|
||||
.profile-photo {
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
border-radius: 28px;
|
||||
}
|
||||
|
||||
.placeholder-avatar {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: linear-gradient(135deg, rgba(203, 58, 53, 0.14), rgba(39, 105, 179, 0.18));
|
||||
color: var(--accent-red);
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bird-card {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
@@ -311,17 +364,8 @@ textarea {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.chart-card::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
border-radius: 50%;
|
||||
background:
|
||||
radial-gradient(circle, rgba(39, 105, 179, 0.14), transparent 58%);
|
||||
pointer-events: none;
|
||||
.overview-chart-card::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.overview-chart-card {
|
||||
@@ -334,6 +378,22 @@ textarea {
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.chart-grid-line {
|
||||
stroke: rgba(39, 105, 179, 0.16);
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 4 6;
|
||||
}
|
||||
|
||||
.chart-axis-line {
|
||||
stroke: rgba(31, 42, 42, 0.24);
|
||||
stroke-width: 1.2;
|
||||
}
|
||||
|
||||
.chart-axis-label {
|
||||
fill: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.chart-footer,
|
||||
.recent-list,
|
||||
.detail-grid,
|
||||
@@ -361,6 +421,27 @@ textarea {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.profile-hero {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(180deg, rgba(255, 252, 247, 0.92), rgba(240, 248, 244, 0.84));
|
||||
border: 1px solid rgba(39, 105, 179, 0.1);
|
||||
}
|
||||
|
||||
.profile-copy {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.profile-copy h3 {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.inline-form {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -399,6 +480,11 @@ textarea {
|
||||
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.large-swatch {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.detail-card span,
|
||||
.summary-card span {
|
||||
color: var(--muted);
|
||||
@@ -443,6 +529,19 @@ label {
|
||||
background: linear-gradient(135deg, #b7312d, #1f5e9f);
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
border: 1px solid rgba(39, 105, 179, 0.18);
|
||||
border-radius: 18px;
|
||||
padding: 0.95rem 1.2rem;
|
||||
color: var(--ink);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
box-shadow: 0 10px 22px rgba(39, 105, 179, 0.1);
|
||||
}
|
||||
|
||||
.secondary-button:hover {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.danger-button {
|
||||
border: 1px solid rgba(171, 44, 44, 0.18);
|
||||
border-radius: 18px;
|
||||
@@ -470,13 +569,73 @@ label {
|
||||
color: #922728;
|
||||
}
|
||||
|
||||
.picker-list {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.color-preview-card {
|
||||
display: flex;
|
||||
gap: 0.9rem;
|
||||
align-items: center;
|
||||
padding: 0.95rem 1rem;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(180deg, rgba(255, 252, 247, 0.9), rgba(240, 248, 244, 0.82));
|
||||
border: 1px solid rgba(39, 105, 179, 0.1);
|
||||
}
|
||||
|
||||
.picker-chip {
|
||||
border: 1px solid rgba(39, 105, 179, 0.16);
|
||||
border-radius: 999px;
|
||||
padding: 0.65rem 0.95rem;
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.picker-chip.active {
|
||||
border-color: transparent;
|
||||
background: linear-gradient(135deg, rgba(203, 58, 53, 0.94), rgba(39, 105, 179, 0.94));
|
||||
color: #fffdf9;
|
||||
}
|
||||
|
||||
.photo-editor {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(180deg, rgba(255, 252, 247, 0.9), rgba(240, 248, 244, 0.82));
|
||||
border: 1px solid rgba(39, 105, 179, 0.1);
|
||||
}
|
||||
|
||||
.photo-preview-shell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.photo-copy {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.file-picker input[type="file"] {
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.app-shell,
|
||||
.hero-card,
|
||||
.dashboard-grid,
|
||||
.forms-grid,
|
||||
.hero-stats,
|
||||
.chart-footer,
|
||||
.inline-form {
|
||||
.inline-form,
|
||||
.profile-hero,
|
||||
.photo-editor {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -484,7 +643,7 @@ label {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.page-tabs {
|
||||
margin-top: 1rem;
|
||||
.side-nav {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user