diff --git a/.agents/skills/create-thought/SKILL.md b/.agents/skills/create-thought/SKILL.md new file mode 100644 index 0000000..d33f81c --- /dev/null +++ b/.agents/skills/create-thought/SKILL.md @@ -0,0 +1,71 @@ +--- +name: create-thought +description: Adds or edits short-form thoughts under src/content/thoughts for yuler.dev. Keeps the body brief and simple. Sets date to the current day, auto-assigns up to three tags, and tightens prose, images, and links. Use when the user wants a new thought, a quick note on /thoughts, or to polish thought markdown before commit. +disable-model-invocation: false +--- + +# create-thought + +## Scope + +Thoughts live in the Astro content collection `thoughts` ([`src/content.config.ts`](../../../src/content.config.ts)): Markdown only, schema `date` (required), `tags` (optional, **max 3** strings), `images` (optional, **max 3** local images). Rendering: [`src/components/Thought.astro`](../../../src/components/Thought.astro). + +## User requirements (always apply) + +- **Date is current date** — Use the session’s authoritative calendar date (e.g. user_info “Today’s date” in Cursor). Encode `date` in frontmatter as an ISO 8601 instant, e.g. `2026-05-11T12:00:00Z`, so `z.date()` parses reliably. +- **Auto assign tags** — After the body is settled, infer **0–3** tags from themes, products, technologies, or domains mentioned. Rules below. +- **Optimize content, images, links** — Follow the optimization sections before saving. The body stays **short and simple** (see Optimize content); do not write long-form or essay-style copy unless the user explicitly asks for it. + +## File naming + +- Default path: `src/content/thoughts/YYYY-MM-DD.md` using the **same** calendar date as `date` (UTC date from that instant is fine if you keep filename and `date` consistent). +- If that file already exists, use `src/content/thoughts/YYYY-MM-DD-.md` where `` is a minimal kebab-case hint (e.g. `api-design`). + +## Frontmatter template + +```yaml +--- +date: 2026-05-11T12:00:00Z +tags: ["tag-one", "tag-two"] +--- +``` + +- Omit `tags` entirely if nothing specific fits (tags are optional). +- Omit `images` unless adding local assets (see Images). + +## Auto-assign tags + +1. **Cap:** Never more than **three** tags (schema hard limit). +2. **Shape:** Lowercase, kebab-case (`ai-agents`, `astro`), no `#` in YAML (the UI prefixes `#` when displaying). +3. **Sources:** Prefer concrete nouns from the thought (tools, frameworks, people, places) over generic words (`thought`, `note`, `update`). +4. **Dedupe:** One tag per theme; drop near-duplicates (`react` vs `reactjs` → pick one). +5. If the body is too thin to infer tags, use **zero** tags rather than vague filler. + +## Optimize content + +- **Length and shape** — A thought is a quick note, not a post. Default to a **few tight sentences** or **one short paragraph** (roughly under ~120 words) unless the user clearly wants more. Use plain, simple wording; skip background, long lists, and nested asides—those belong in `src/content/posts` or a dedicated page. +- Keep tone direct; cut hedging and repetition. +- Prefer short paragraphs; one idea per paragraph unless a list is clearer. +- Avoid a title in frontmatter (thoughts have no title field); let the opening sentence carry the hook. +- After edits, run `pnpm autocorrect` on the file when available (repo convention for content). + +## Optimize links + +- Use descriptive link text: `[Astro image docs](https://...)` not `[here](https://...)`. +- External URLs: `https://`, remove obvious tracking query params when you touch the URL. +- Same-site references: prefer root-relative paths (`/thoughts`, `/posts/...`) instead of full `https://yuler.dev/...` when linking within the site. + +## Workflow checklist + +1. Resolve **today’s date** from session context; pick `YYYY-MM-DD` and an ISO `date` value. +2. Choose path; avoid overwriting an existing thought unless the user asked to edit it. +3. Draft a **minimal** body from the user’s intent (brevity rules above); apply **content** optimizations. +4. Assign **0–3 tags** with the rules above; drop `tags` key if empty. +5. Add **`images`** only with valid relative paths and ≤3 items; otherwise omit the key. +6. Normalize **links** in the body. +7. Run `pnpm build` if you changed frontmatter shapes or image paths (catches collection errors). + +## Verification + +- `pnpm build` succeeds. +- For visual check: `pnpm dev` → `/thoughts`. diff --git a/skills/log-reporter/SKILL.md b/.agents/skills/log-reporter/SKILL.md similarity index 100% rename from skills/log-reporter/SKILL.md rename to .agents/skills/log-reporter/SKILL.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..38dc20c --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1 @@ +@../AGENTS.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index fa21537..d86c13b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,8 +19,5 @@ node_modules/ # Output pdfs/ -# AI -.claude - # Misc -screenshots \ No newline at end of file +screenshots diff --git a/AGENTS.md b/AGENTS.md index 9164e7c..b40f9ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,8 @@ +--- +name: yuler.dev +description: Agent instructions for the yuler.dev repository. +--- + # Agent instructions (yuler.dev) Personal site at https://yuler.dev — Astro 6, MDX, Tailwind CSS 4, TypeScript. Package manager: **pnpm**. @@ -19,7 +24,7 @@ Every icons should a simple astro component locate in [`src/components/icons`](. Use the `/git-commit` skill for every commit. Check in this order: -- Local: `skills/git-commit` (this repo) +- Local: `.agents/skills/git-commit` (this repo) - Global: `~/.agents/skills/git-commit` -If neither exists, install it from [skills/git-commit](https://github.com/yuler/skills/tree/main/skills/git-commit). +If neither exists, install it from the upstream [skills/git-commit](https://github.com/yuler/skills/tree/main/skills/git-commit) diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index 47dc3e3..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..af2b859 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,177 @@ +--- +version: alpha +name: yuler.dev's website design +description: Personal site — Astro, Tailwind CSS 4, Inter + monospace accents. Light, editorial cards on a neutral canvas. +colors: + primary: "#1a1a1a" + secondary: "#6b7280" + tertiary: "#3b82f6" + neutral: "#f5f5f5" + surface: "#ffffff" + border: "#e5e7eb" + border-muted: "#f3f4f6" + heatmap-low: "#f3f4f6" + heatmap-mid: "#d1d5db" + heatmap-high: "#4b5563" + heatmap-max: "#000000" +typography: + body: + fontFamily: Inter + fontSize: 1rem + fontWeight: 400 + lineHeight: 1.6 + mono-label: + fontFamily: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace + fontSize: 0.75rem + fontWeight: 500 + lineHeight: 1.25 + card-title: + fontFamily: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace + fontSize: 1.125rem + fontWeight: 600 + lineHeight: 1.375 + profile-name: + fontFamily: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace + fontSize: 1.5rem + fontWeight: 700 + lineHeight: 1.2 + post-title: + fontFamily: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace + fontSize: 1.875rem + fontWeight: 700 + lineHeight: 1.2 + prose-body: + fontFamily: Inter + fontSize: 1rem + fontWeight: 400 + lineHeight: 1.625 +rounded: + none: 0px + hairline: 2px +spacing: + xs: 4px + sm: 8px + md: 16px + lg: 24px + xl: 32px + section: 48px +components: + card: + backgroundColor: "{colors.surface}" + textColor: "{colors.primary}" + rounded: "{rounded.none}" + padding: 24px + list-row-interactive: + backgroundColor: "{colors.surface}" + textColor: "{colors.primary}" + padding: 12px + secondary-button: + backgroundColor: "{colors.border-muted}" + textColor: "#374151" + typography: "{typography.mono-label}" + padding: 12px 16px + caption-text: + backgroundColor: "{colors.surface}" + textColor: "{colors.secondary}" + typography: "{typography.body}" + padding: 0px + divider-rule: + backgroundColor: "{colors.border}" + textColor: "{colors.primary}" + height: 1px + padding: 0px + page-canvas: + backgroundColor: "{colors.neutral}" + textColor: "{colors.primary}" + padding: 0px + meta-muted: + backgroundColor: "{colors.surface}" + textColor: "{colors.secondary}" + typography: "{typography.mono-label}" + padding: 0px + heatmap-empty: + backgroundColor: "{colors.heatmap-low}" + padding: 0px + heatmap-light: + backgroundColor: "{colors.heatmap-mid}" + padding: 0px + heatmap-dark: + backgroundColor: "{colors.heatmap-high}" + padding: 0px + heatmap-full: + backgroundColor: "{colors.heatmap-max}" + padding: 0px + focus-accent: + backgroundColor: transparent + textColor: "{colors.tertiary}" + padding: 0px +--- + +## Overview + +yuler.dev is a **light, content-first** personal site: soft neutral canvas, white **surfaces** with **hairline borders**, and **monospace** for navigation, section titles, and article headings so the UI reads like a clean terminal or README. Body copy stays in **Inter** for readability. Motion is subtle (border darkens on hover, short transitions). There are almost no rounded “app” cards—corners stay **square**; optional **corner bracket** markers reinforce a drafted, editorial frame. Use this file when adding pages or components so new UI matches existing Tailwind usage and global styles in `src/styles/global.css`. + +## Colors + +- **Primary (`#1a1a1a`):** Default text on the canvas and on white surfaces; matches `body` in `global.css`. +- **Secondary (`#6b7280`):** Metadata, footer, timestamps, and de-emphasized labels (Tailwind `text-gray-500` class family). +- **Tertiary (`#3b82f6`):** Focus and selection affordances only—`::selection` and `:focus-visible` use this hue at partial opacity; do not flood large areas with blue. +- **Neutral (`#f5f5f5`):** Page background for home, posts, and workouts (`bg-[#f5f5f5]` / same as body background). +- **Surface (`#ffffff`):** All primary cards and article shells. +- **Border (`#e5e7eb`) / border-muted (`#f3f4f6`):** Default card and list borders; lighter rules for section dividers (`border-gray-100`). +- **Muted UI (`#9ca3af`, Tailwind `gray-400`):** Chevron and separator icons; decorative only, not for long text on white (contrast). Not a named YAML token—use Tailwind classes in code. +- **Heatmap scale (`heatmap-*`):** Workout contribution cells only—from empty light gray through black for intensity; today’s cell may use an inset ring, not a fifth fill color. + +## Typography + +- **Body:** Inter, 400, comfortable line height (1.6 globally). This is the default for paragraphs, descriptions, and long-form `prose`. +- **Monospace stack** (`font-mono`): Section titles (“Posts”, “Workouts”), profile name, post H1, breadcrumbs, meta lines (`date:`, `read:`), and small UI labels. Keeps the site feeling like structured logs or docs. +- **Weights:** Semibold for card section titles and post H2 in prose; bold for profile name and post title; medium for list item titles. +- **Article prose:** Use `@tailwindcss/typography` with the existing `prose-gray` overrides in `src/pages/posts/[...slug].astro`: headings monospace and gray-900, links underlined with gray decoration, blockquotes left border + gray-50 fill, tables with gray borders and mono table headers. + +## Layout + +- **Canvas:** Full-width neutral background; content centered with horizontal padding (`px-4`), vertical rhythm `py-8` / `md:py-12`. +- **Home:** `max-w-7xl` container; **3-column** grid on large screens (profile column + two-column main), **single column** on small screens; consistent `gap-4` between cards. +- **Posts list / post detail / workouts:** `max-w-4xl` for reading width on article flows; breadcrumbs above the first card. +- **Cards:** Prefer **single white panel** with `p-6` (or `md:p-8` / `lg:p-12` for long article bodies). Headers often include a bottom rule (`border-b border-gray-100 pb-4`). +- **Spacing scale:** Use Tailwind’s 4-based rhythm aligned to tokens: `gap-4`, `p-6`, `mb-8`, etc.; avoid arbitrary large gaps unless matching an existing page. + +## Elevation & Depth + +- **No drop shadows** on standard cards; depth comes from **1px borders** and hover state (`hover:border-gray-400`). +- **Corner markers:** Optional `CornerMarkers`—small L-shaped borders at card corners; use together with cards, not on plain divs. +- **Lightbox / overlays:** Follow existing `LightBox` behavior; keep backdrop consistent with rest of site (do not introduce new blur tints without updating this doc). + +## Shapes + +- **Corners:** Default **square** (`rounded-none`) for cards, code blocks, and primary panels. Small rounded corners are acceptable only where already used (e.g. tiny badges, thought tags). +- **Borders:** 1px solid gray-200 default; lighten to gray-100 for internal dividers; interactive rows use transparent border then `hover:border-gray-200`. + +## Components + +- **Card (default):** White background, `border border-gray-200`, `p-6`, `relative`, transition on border color; include `CornerMarkers` when the pattern matches profile/posts/workouts cards. +- **Card header:** Flex row, space-between; title uses `card-title` token (mono, semibold); optional count badge `text-sm bg-gray-100 text-gray-600 font-mono px-2 py-0.5`. +- **List row (link):** `p-3`, `min-h-[3.25rem]`, hover `bg-gray-50`, optional border on hover; trailing chevron `text-gray-400` with slight translate on hover. +- **Secondary button / footer link:** `bg-gray-100 text-gray-600`, hover `bg-gray-200 text-gray-900`, `font-mono`, inline-flex with icon gap (see “View more” on posts). +- **Text link (muted):** `text-xs text-gray-400`, underline with `decoration-gray-200`, hover to gray-600 and stronger decoration (workouts “View all”). +- **Tags:** `border border-gray-300 text-gray-600`, hover darkens border and text to gray-900. +- **Status chips:** Draft = gray-200/gray-600; WIP = amber-100/amber-700; align padding `text-xs`. +- **Heatmap cells:** Four gray/black steps only; “today” uses inset ring; future days muted empty state. +- **Icons:** Implement as **small Astro components** under `src/components/icons/`; stroke/fill colors follow `muted` / `secondary`, inheriting `currentColor` where possible. + +## Do's and Don'ts + +**Do** + +- Keep **Inter + monospace** split: prose body in Inter; UI chrome and headings in mono where the codebase already does. +- Reuse **gray border** vocabulary (`200` default, `100` dividers, `400` on card hover). +- Match **focus-visible** to global outline (blue tint, 2px-equivalent offset). +- Add new **icons** as dedicated Astro components in `src/components/icons/`. + +**Don’t** + +- Don’t introduce **dark mode** or new primary hues in one-off components without updating tokens and this file. +- Don’t default to **heavy rounding** or **shadow-heavy** cards; they conflict with the current editorial flat look. +- Don’t use **tertiary blue** as large fills; it’s for focus/selection semantics. +- Don’t paste **inline SVG** in random pages when an icon component pattern exists. diff --git a/src/components/DesignTokenTable.astro b/src/components/DesignTokenTable.astro new file mode 100644 index 0000000..36c1ed4 --- /dev/null +++ b/src/components/DesignTokenTable.astro @@ -0,0 +1,262 @@ +--- +type TokenGroup = 'colors' | 'typography' | 'rounded' | 'spacing' | 'components' + +interface Props { + group: TokenGroup + frontmatter: Record +} + +const { group, frontmatter } = Astro.props + +type TokenRow = { + path: string + value: string +} + +type TokenPreview = { + colorHex: string | null + pxValues: number[] + remValues: number[] + fontSize: string | null + fontFamily: string | null + fontWeight: number | null + lineHeight: string | null +} + +type TokenRowResolved = TokenRow & { preview: TokenPreview, resolvedValue: string } + +const HEX_COLOR_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i +const PX_VALUE_RE = /(\d+(?:\.\d+)?)px/g +const REM_VALUE_RE = /(\d+(?:\.\d+)?)rem/g +const FONT_WEIGHT_RE = /^\d{2,3}$/ +const TOKEN_REF_RE = /^\{([^}]+)\}$/ + +const TOKEN_GROUPS: readonly TokenGroup[] = ['colors', 'typography', 'rounded', 'spacing', 'components'] + +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function stringifyTokenValue(value: unknown): string { + if (value === null || value === undefined) + return '' + if (typeof value === 'string') + return value + if (typeof value === 'number' || typeof value === 'boolean') + return String(value) + return JSON.stringify(value) +} + +function flattenTokenObject(input: unknown, parents: string[] = []): TokenRow[] { + if (!isPlainObject(input)) + return [] + + const rows: TokenRow[] = [] + for (const [key, value] of Object.entries(input)) { + const path = [...parents, key] + if (isPlainObject(value)) { + rows.push(...flattenTokenObject(value, path)) + } + else { + rows.push({ path: path.join('.'), value: stringifyTokenValue(value) }) + } + } + return rows +} + +function getTokenPreview(tokenPath: string, value: string): TokenPreview { + const normalized = value.trim() + const colorHex = HEX_COLOR_RE.test(normalized) ? normalized : null + + const pxValues: number[] = [] + for (const match of normalized.matchAll(PX_VALUE_RE)) { + const parsed = Number.parseFloat(match[1]) + if (Number.isFinite(parsed)) + pxValues.push(parsed) + } + + const remValues: number[] = [] + for (const match of normalized.matchAll(REM_VALUE_RE)) { + const parsed = Number.parseFloat(match[1]) + if (Number.isFinite(parsed)) + remValues.push(parsed) + } + + const leafKey = tokenPath.split('.').at(-1) ?? '' + const fontSize = leafKey === 'fontSize' ? normalized : null + const remValuesForPreview = leafKey === 'fontSize' ? [] : remValues + const fontFamily = leafKey === 'fontFamily' ? normalized : null + + const fontWeight = leafKey === 'fontWeight' && FONT_WEIGHT_RE.test(normalized) + ? Number.parseInt(normalized, 10) + : null + + const lineHeight = leafKey === 'lineHeight' ? normalized : null + + return { colorHex, pxValues, remValues: remValuesForPreview, fontSize, fontFamily, fontWeight, lineHeight } +} + +const allRows: TokenRow[] = TOKEN_GROUPS.flatMap(g => + flattenTokenObject(frontmatter?.[g], [g]), +) + +const tokenValueByPath = allRows.reduce>((acc, row) => { + acc[row.path] = row.value + return acc +}, {}) + +function resolveTokenValue(rawValue: string, visited = new Set()): string { + const normalized = rawValue.trim() + const refMatch = normalized.match(TOKEN_REF_RE) + if (!refMatch) + return normalized + + const refPath = refMatch[1].trim() + if (!refPath) + return normalized + + if (visited.has(refPath)) + return normalized + + const next = tokenValueByPath[refPath] + if (!next) + return normalized + + visited.add(refPath) + return resolveTokenValue(next, visited) +} + +const groupRows: TokenRow[] = flattenTokenObject(frontmatter?.[group], [group]) + +const rows: TokenRowResolved[] = groupRows.map((row) => { + const resolvedValue = resolveTokenValue(row.value) + return { + ...row, + resolvedValue, + preview: getTokenPreview(row.path, resolvedValue), + } +}) + +const title = group[0].toUpperCase() + group.slice(1) +--- + +{rows.length > 0 && ( +
+

{title}

+
+ + + + + + + + + + {rows.map((row) => { + const preview = row.preview + return ( + + + + + + ) + })} + +
Token PathValuePreview
{row.path}{row.value} +
+ {row.resolvedValue !== row.value.trim() && ( +
+ = {row.resolvedValue} +
+ )} + {preview.fontFamily && ( +
+
+ Aa +
+ {preview.fontFamily} +
+ )} + {preview.fontSize && ( +
+
+ {preview.fontSize} +
+ {preview.fontSize} +
+ )} + + {preview.fontWeight !== null && ( +
+
+ Aa +
+ {preview.fontWeight} +
+ )} + {preview.lineHeight && ( +
+
+ A
+ a +
+ {preview.lineHeight} +
+ )} + {preview.colorHex && ( +
+
+ {preview.colorHex} +
+ )} + {preview.pxValues.length > 0 && ( +
+ {preview.pxValues.map((px) => { + const width = Math.max(2, Math.min(160, px)) + return ( +
+
+ {px}px +
+ ) + })} +
+ )} + {preview.remValues.length > 0 && ( +
+ {preview.remValues.map((rem) => { + const width = Math.max(2, Math.min(160, rem * 16)) + return ( +
+
+ {rem}rem +
+ ) + })} +
+ )} + {!preview.colorHex && preview.pxValues.length === 0 && preview.remValues.length === 0 && !preview.fontSize && !preview.fontFamily && preview.fontWeight === null && !preview.lineHeight && ( + - + )} +
+
+
+
+)} diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 82670e6..d1836b0 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -2,6 +2,9 @@ const today = new Date() --- -