feat(brand): rebuild the app mark as a vector and recolor it to the themes
`app-icon.png` is a 120KB bitmap, and the seven `.svg` files beside it under docs/images are 193 bytes each — an `<image>` tag wrapping that bitmap. So the mark carried its own blue/cyan/orange through all six palettes while everything around it moved, and there was nothing to recolor. Measured the bitmap back into geometry rather than redrawing it: connected components to separate the elements, algebraic circle fits for the arcs, angular histograms for the openings and stroke widths. Big C is center (415,566) r=131 stroke=60, opening 160°; the second C is center (615,576) r=117 cut into two 68° arcs; the cursor is a four-point polygon with one notch. The slight ellipticity and unequal stroke ends were raster artifacts, so they were rounded out. The shape is otherwise unchanged — differencing a render against the original leaves only antialiasing. Color maps the original's three layers onto the palette's own three: the C's take 墨 `--cc-t1`, the bar and cursor and sparkles take 朱 `--cc-ac`. Six themes do not need six icons. Their accent resolves to two values — #96442B across the four light palettes, #D07B52 across the two dark ones, which is the same ochre lightened. Only the ink varies, and ink should follow body text anyway. The app icon stays one fixed artwork. Dock and taskbar are drawn by the OS, which knows nothing about the in-app theme, so paper ground with ink C's ships to every platform. Inside the app the mark is a vector on tokens and repaints per theme. Four ideas — two C's, the bar, the cursor, two sparkles — collapse below 24px, so every consumer sheds parts as it shrinks: sparkles above 40px, cursor above 24px, C's and bar always. That applies inside icon.icns and icon.ico as much as in BrandSeal. BrandSeal was the 「哈」 glyph in a terracotta square; it is now the CC mark drawn inline on `--color-text-primary` and `--color-brand`. Its five call sites — the sidebar, both empty states, the H5 connect view, the gallery — follow, and the about pane drops its `<img>` for the same component. README leads with the horizontal lockup inside a `<picture>` so GitHub serves the dark cut in dark mode.
@@ -1,7 +1,10 @@
|
||||
# Claude Code Haha
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/logo-horizontal.png" alt="Claude Code Haha" width="480">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/images/logo-horizontal-dark.png">
|
||||
<img src="docs/images/logo-horizontal.png" alt="Claude Code Haha" width="480">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# Claude Code Haha
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/app-icon.png" alt="Claude Code Haha" width="240">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/images/logo-horizontal-dark.png">
|
||||
<img src="docs/images/logo-horizontal.png" alt="Claude Code Haha" width="480">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
- `Button` — `primary` 已是墨色实心 + hover 转陶土 + 上浮 1px + `active:scale(.97)`;禁用态是不透明的 `--s1`/`--t3`。
|
||||
- `Card` — 新增 `shadow`(`none`/`card`/`composer`)与 `lift`(hover 抬起 2px)两个 prop,以及 `radius="2xl"`。**`lift` 与 `shadow` 不要同时传**,两个 `shadow-[…]` 不会叠加。
|
||||
- `IconButton` / `Badge` / `Modal`(圆角已是 24)/ `SegmentedControl` / `Switch` / `Progress` — 都已走 token,跟随主题。
|
||||
- `composite/BrandSeal` — 印章「哈」,`sm`24 / `md`32 / `lg`38 / `xl`80。
|
||||
- `composite/BrandSeal` — 品牌标记 cc-haha(原 app-icon 的矢量重建),`sm`24 / `md`32 / `lg`38 / `xl`80,随尺寸减件(星芒只在 `xl`,光标到 `sm` 去掉)。
|
||||
|
||||
焦点态统一:`focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface)]`。
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 34 KiB |
@@ -1,3 +1,10 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<image width="1024" height="1024" href="app-icon.png" preserveAspectRatio="xMidYMid meet"/>
|
||||
<svg viewBox="0 0 1024 1024" width="1024" height="1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="88" y="88" width="848" height="848" rx="175" fill="#F6F0E1"/>
|
||||
<path d="M437.75 695.01A131 131 0 1 1 437.75 436.99" fill="none" stroke="#251E11" stroke-width="60" stroke-linecap="round"/>
|
||||
<path d="M505.06 535.98A117 117 0 0 1 610.92 459.07" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M635.32 691.22A117 117 0 0 1 515.78 638.00" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M441.5 576H517.5" fill="none" stroke="#96442B" stroke-width="35" stroke-linecap="round"/>
|
||||
<path d="M683.79 384.80Q698.00 380.00 692.43 393.93L665.57 461.07Q660.00 475.00 655.71 460.63L645.00 424.71Q643.00 418.00 636.59 415.18L629.25 411.95Q618.00 407.00 631.26 402.52L683.79 384.80Z" fill="#96442B"/>
|
||||
<path d="M717.00 308.00Q724.07 325.43 741.50 332.50Q724.07 339.57 717.00 357.00Q709.93 339.57 692.50 332.50Q709.93 325.43 717.00 308.00Z" fill="#96442B"/>
|
||||
<path d="M755.50 365.00Q759.04 374.96 769.00 378.50Q759.04 382.04 755.50 392.00Q751.96 382.04 742.00 378.50Q751.96 374.96 755.50 365.00Z" fill="#96442B"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +1,10 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<image width="1024" height="1024" href="app-icon.png" preserveAspectRatio="xMidYMid meet"/>
|
||||
<svg viewBox="0 0 1024 1024" width="1024" height="1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="88" y="88" width="848" height="848" rx="175" fill="#F6F0E1"/>
|
||||
<path d="M437.75 695.01A131 131 0 1 1 437.75 436.99" fill="none" stroke="#251E11" stroke-width="60" stroke-linecap="round"/>
|
||||
<path d="M505.06 535.98A117 117 0 0 1 610.92 459.07" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M635.32 691.22A117 117 0 0 1 515.78 638.00" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M441.5 576H517.5" fill="none" stroke="#96442B" stroke-width="35" stroke-linecap="round"/>
|
||||
<path d="M683.79 384.80Q698.00 380.00 692.43 393.93L665.57 461.07Q660.00 475.00 655.71 460.63L645.00 424.71Q643.00 418.00 636.59 415.18L629.25 411.95Q618.00 407.00 631.26 402.52L683.79 384.80Z" fill="#96442B"/>
|
||||
<path d="M717.00 308.00Q724.07 325.43 741.50 332.50Q724.07 339.57 717.00 357.00Q709.93 339.57 692.50 332.50Q709.93 325.43 717.00 308.00Z" fill="#96442B"/>
|
||||
<path d="M755.50 365.00Q759.04 374.96 769.00 378.50Q759.04 382.04 755.50 392.00Q751.96 382.04 742.00 378.50Q751.96 374.96 755.50 365.00Z" fill="#96442B"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +1,10 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<image width="1024" height="1024" href="app-icon.png" preserveAspectRatio="xMidYMid meet"/>
|
||||
<svg viewBox="0 0 1024 1024" width="1024" height="1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="88" y="88" width="848" height="848" rx="175" fill="#F6F0E1"/>
|
||||
<path d="M437.75 695.01A131 131 0 1 1 437.75 436.99" fill="none" stroke="#251E11" stroke-width="60" stroke-linecap="round"/>
|
||||
<path d="M505.06 535.98A117 117 0 0 1 610.92 459.07" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M635.32 691.22A117 117 0 0 1 515.78 638.00" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M441.5 576H517.5" fill="none" stroke="#96442B" stroke-width="35" stroke-linecap="round"/>
|
||||
<path d="M683.79 384.80Q698.00 380.00 692.43 393.93L665.57 461.07Q660.00 475.00 655.71 460.63L645.00 424.71Q643.00 418.00 636.59 415.18L629.25 411.95Q618.00 407.00 631.26 402.52L683.79 384.80Z" fill="#96442B"/>
|
||||
<path d="M717.00 308.00Q724.07 325.43 741.50 332.50Q724.07 339.57 717.00 357.00Q709.93 339.57 692.50 332.50Q709.93 325.43 717.00 308.00Z" fill="#96442B"/>
|
||||
<path d="M755.50 365.00Q759.04 374.96 769.00 378.50Q759.04 382.04 755.50 392.00Q751.96 382.04 742.00 378.50Q751.96 374.96 755.50 365.00Z" fill="#96442B"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 908 B |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 854 B |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 625 B After Width: | Height: | Size: 546 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 949 B After Width: | Height: | Size: 813 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 4.8 KiB |
@@ -41,7 +41,7 @@
|
||||
| 复制按钮 | `CopyButton` | `ui/CopyButton.tsx` |
|
||||
| 目录选择 | `DirectoryPicker` | `composite/DirectoryPicker.tsx` |
|
||||
| 「用 X 打开」菜单 | `OpenWithMenu` | `composite/OpenWithMenu.tsx` |
|
||||
| 品牌印章「哈」 | `BrandSeal` | `composite/BrandSeal.tsx` |
|
||||
| 品牌标记 cc-haha | `BrandSeal` | `composite/BrandSeal.tsx` |
|
||||
|
||||
### 两个 hook
|
||||
|
||||
|
||||
@@ -4,40 +4,65 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { BrandSeal } from './BrandSeal'
|
||||
|
||||
const SIZES = ['sm', 'md', 'lg', 'xl'] as const
|
||||
|
||||
describe('BrandSeal', () => {
|
||||
it('is decorative and hidden from assistive tech', () => {
|
||||
// The product name always sits beside the seal (sidebar) or under it
|
||||
// (empty state); announcing the glyph as well reads the brand twice.
|
||||
// The product name always sits beside the mark (sidebar) or under it
|
||||
// (empty state); announcing the brand again reads it twice.
|
||||
const { container } = render(<BrandSeal />)
|
||||
expect(container.firstElementChild).toHaveAttribute('aria-hidden', 'true')
|
||||
})
|
||||
|
||||
it('renders the 哈 glyph in the headline serif at every size', () => {
|
||||
for (const size of ['sm', 'md', 'lg', 'xl'] as const) {
|
||||
it('draws the two C strokes at every size', () => {
|
||||
// Whatever else is shed, the double C is the mark. If this count drops the
|
||||
// remaining shape is no longer recognizable as the logo.
|
||||
for (const size of SIZES) {
|
||||
const { container, unmount } = render(<BrandSeal size={size} />)
|
||||
const glyph = container.querySelector('span span:last-child') as HTMLElement
|
||||
expect(glyph.textContent).toBe('哈')
|
||||
expect(glyph.style.fontFamily).toContain('var(--font-headline)')
|
||||
const strokes = container.querySelectorAll('g[stroke="var(--color-text-primary)"] path')
|
||||
expect(strokes).toHaveLength(3) // big C + the second C's two arcs
|
||||
unmount()
|
||||
}
|
||||
})
|
||||
|
||||
it('fills with the brand token so all six palettes recolor it', () => {
|
||||
// This is why the seal replaced the raster app icon: a bitmap kept its own
|
||||
// colors under every theme while the chrome around it moved.
|
||||
const { container } = render(<BrandSeal />)
|
||||
expect(container.firstElementChild?.className).toContain('bg-[var(--color-brand)]')
|
||||
it('paints from tokens so all six palettes recolor it', () => {
|
||||
// This is why the vector replaced the raster app icon: a bitmap kept its
|
||||
// own blue and orange under every theme while the chrome around it moved.
|
||||
const { container } = render(<BrandSeal size="xl" />)
|
||||
const svg = container.firstElementChild!
|
||||
expect(svg.querySelector('[stroke="var(--color-text-primary)"]')).not.toBeNull()
|
||||
expect(svg.querySelector('[stroke="var(--color-brand)"]')).not.toBeNull()
|
||||
expect(svg.querySelector('[fill="var(--color-brand)"]')).not.toBeNull()
|
||||
// No literal hex anywhere — that would survive a theme switch unchanged.
|
||||
expect(svg.innerHTML).not.toMatch(/#[0-9a-f]{3,8}\b/i)
|
||||
})
|
||||
|
||||
it('engraves the inner rule only at the empty-state size', () => {
|
||||
// At 38px and below the 1.5px inset ring closes up into a smudge; the
|
||||
// handoff only draws it on the 80px seal.
|
||||
const { container: xl } = render(<BrandSeal size="xl" />)
|
||||
expect(xl.querySelector('.border-\\[1\\.5px\\]')).not.toBeNull()
|
||||
|
||||
for (const size of ['sm', 'md', 'lg'] as const) {
|
||||
it('sheds parts as it shrinks instead of turning to mush', () => {
|
||||
// At 38px a sparkle is under 2px across and reads as dirt; at 24px the
|
||||
// cursor merges into the second C.
|
||||
const count = (size: (typeof SIZES)[number]) => {
|
||||
const { container, unmount } = render(<BrandSeal size={size} />)
|
||||
expect(container.querySelector('.border-\\[1\\.5px\\]')).toBeNull()
|
||||
const filled = container.querySelectorAll('path[fill="var(--color-brand)"]').length
|
||||
unmount()
|
||||
return filled
|
||||
}
|
||||
expect(count('xl')).toBe(3) // cursor + two sparkles
|
||||
expect(count('lg')).toBe(1) // cursor only
|
||||
expect(count('md')).toBe(1)
|
||||
expect(count('sm')).toBe(0) // the C's and the bar
|
||||
})
|
||||
|
||||
it('crops the viewBox to the ink so the mark fills its box', () => {
|
||||
// A full 0 0 1024 1024 viewBox would letterbox the mark to roughly half
|
||||
// the height of its container at every size.
|
||||
for (const size of SIZES) {
|
||||
const { container, unmount } = render(<BrandSeal size={size} />)
|
||||
const viewBox = container.firstElementChild!.getAttribute('viewBox')!
|
||||
const [x, y, w, h] = viewBox.split(' ').map(Number)
|
||||
expect(x).toBeGreaterThan(200)
|
||||
expect(y).toBeGreaterThan(200)
|
||||
expect(w).toBeLessThan(1024)
|
||||
expect(h).toBeLessThan(1024)
|
||||
unmount()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,38 +1,58 @@
|
||||
import { cx } from '@/lib/cx'
|
||||
|
||||
/**
|
||||
* The 「哈」 seal — the "印" of 「纸 · 墨 · 印」.
|
||||
* The cc-haha mark — the double C of "Claude Code" with one stroke of 朱 on it.
|
||||
*
|
||||
* A terracotta rounded square holding one serif glyph. It replaces the raster
|
||||
* app icon inside the app chrome: the icon is a fixed bitmap, so it kept its
|
||||
* own colors under all six palettes while everything around it moved, and it
|
||||
* had no size below 32px that stayed legible.
|
||||
* Geometry is a vector rebuild of the original `app-icon.png`, measured off the
|
||||
* bitmap pixel by pixel (arc centers, radii, stroke widths and opening angles
|
||||
* were fitted, then rounded to symmetric values). Two things the raster could
|
||||
* not do:
|
||||
*
|
||||
* The glyph is Chinese, and only the latin cut of Noto Serif SC is
|
||||
* self-hosted, so it renders in the platform serif named by `--font-headline`
|
||||
* (Songti SC on macOS, Source Han Serif / SimSun elsewhere). That is the same
|
||||
* fallback the handoff prototype relies on.
|
||||
* 1. Recolor. The PNG carried its own blue/cyan/orange under all six palettes
|
||||
* while everything around it moved. Here the C's take `--color-text-primary`
|
||||
* and the accents take `--color-brand`, so every theme repaints the mark.
|
||||
* 2. Shrink. Four ideas — the two C's, the bar, the cursor and two sparkles —
|
||||
* collapse into noise below 24px. So the mark sheds parts as it gets
|
||||
* smaller (see `SIZES`) instead of turning to mush.
|
||||
*/
|
||||
export type BrandSealSize = 'sm' | 'md' | 'lg' | 'xl'
|
||||
|
||||
/** Which elements survive at each size. */
|
||||
type MarkParts = 'ccbar' | 'nostars' | 'full'
|
||||
|
||||
/**
|
||||
* The three sizes the handoff calls for — sidebar 32, collapsed rail 38, empty
|
||||
* state 80 — plus a 24px one for dense chrome. The inner rule only appears at
|
||||
* `xl`: at 38px and below it closes up into a smudge.
|
||||
* The sizes the layout calls for — sidebar 32, collapsed rail 38, empty state
|
||||
* 80 — plus a 24px one for dense chrome.
|
||||
*
|
||||
* `viewBox` is cropped to the ink bounds of that part set (measured from a
|
||||
* render, not guessed) so the mark fills its box at every size; `w` follows
|
||||
* from the crop's aspect ratio, which is why the boxes are not square.
|
||||
*
|
||||
* The sparkles only appear at `xl`: at 38px they are under 2px across and read
|
||||
* as dirt. The cursor goes at `sm` for the same reason.
|
||||
*/
|
||||
const SIZES: Record<BrandSealSize, {
|
||||
box: string
|
||||
radius: string
|
||||
glyph: string
|
||||
shadow: string
|
||||
rule: boolean
|
||||
}> = {
|
||||
sm: { box: 'h-6 w-6', radius: 'rounded-[7px]', glyph: 'text-[12px]', shadow: 'shadow-[0_1px_2px_rgba(64,30,12,0.28)]', rule: false },
|
||||
md: { box: 'h-8 w-8', radius: 'rounded-[9px]', glyph: 'text-[16px]', shadow: 'shadow-[0_1px_3px_rgba(64,30,12,0.3)]', rule: false },
|
||||
lg: { box: 'h-[38px] w-[38px]', radius: 'rounded-[11px]', glyph: 'text-[18px]', shadow: 'shadow-[0_1px_3px_rgba(64,30,12,0.3)]', rule: false },
|
||||
xl: { box: 'h-20 w-20', radius: 'rounded-[22px]', glyph: 'text-[42px]', shadow: 'shadow-[0_10px_28px_rgba(100,45,20,0.3)]', rule: true },
|
||||
const SIZES: Record<BrandSealSize, { box: string; viewBox: string; parts: MarkParts }> = {
|
||||
sm: { box: 'h-6 w-[31px]', viewBox: '252 403 415 326', parts: 'ccbar' },
|
||||
md: { box: 'h-8 w-[41px]', viewBox: '252 381 444 348', parts: 'nostars' },
|
||||
lg: { box: 'h-[38px] w-[48px]', viewBox: '252 381 444 348', parts: 'nostars' },
|
||||
xl: { box: 'h-20 w-[98px]', viewBox: '252 306 519 423', parts: 'full' },
|
||||
}
|
||||
|
||||
const INK = 'var(--color-text-primary)'
|
||||
const SEAL = 'var(--color-brand)'
|
||||
|
||||
/** Big C: open 160°, symmetric about the horizontal axis. */
|
||||
const BIG_C = 'M437.75 695.01A131 131 0 1 1 437.75 436.99'
|
||||
/** Second C, cut into two arcs of 68° each. */
|
||||
const SMALL_C_UPPER = 'M505.06 535.98A117 117 0 0 1 610.92 459.07'
|
||||
const SMALL_C_LOWER = 'M635.32 691.22A117 117 0 0 1 515.78 638.00'
|
||||
/** The bar that runs through the second C's gap. */
|
||||
const BAR = 'M441.5 576H517.5'
|
||||
/** Cursor arrow: tip up-right, two tails and a notch. */
|
||||
const CURSOR = 'M683.79 384.80Q698.00 380.00 692.43 393.93L665.57 461.07Q660.00 475.00 655.71 460.63L645.00 424.71Q643.00 418.00 636.59 415.18L629.25 411.95Q618.00 407.00 631.26 402.52L683.79 384.80Z'
|
||||
const SPARKLE_LG = 'M717.00 308.00Q724.07 325.43 741.50 332.50Q724.07 339.57 717.00 357.00Q709.93 339.57 692.50 332.50Q709.93 325.43 717.00 308.00Z'
|
||||
const SPARKLE_SM = 'M755.50 365.00Q759.04 374.96 769.00 378.50Q759.04 382.04 755.50 392.00Q751.96 382.04 742.00 378.50Q751.96 374.96 755.50 365.00Z'
|
||||
|
||||
export type BrandSealProps = {
|
||||
size?: BrandSealSize
|
||||
className?: string
|
||||
@@ -42,27 +62,27 @@ export function BrandSeal({ size = 'md', className }: BrandSealProps) {
|
||||
const spec = SIZES[size]
|
||||
|
||||
return (
|
||||
<span
|
||||
// Decorative: the product name sits next to it in the sidebar and above
|
||||
// it on the empty state, so announcing "哈" as well is duplicate noise.
|
||||
<svg
|
||||
// Decorative: the product name sits next to the mark in the sidebar and
|
||||
// above it on the empty state, so announcing the brand twice is noise.
|
||||
aria-hidden="true"
|
||||
className={cx(
|
||||
'relative inline-flex flex-shrink-0 items-center justify-center bg-[var(--color-brand)]',
|
||||
spec.box,
|
||||
spec.radius,
|
||||
spec.shadow,
|
||||
className,
|
||||
)}
|
||||
focusable="false"
|
||||
viewBox={spec.viewBox}
|
||||
className={cx('flex-shrink-0', spec.box, className)}
|
||||
>
|
||||
{spec.rule && (
|
||||
<span className="pointer-events-none absolute inset-[5px] rounded-[17px] border-[1.5px] border-[var(--color-brand-seal-rule)]" />
|
||||
<g fill="none" stroke={INK} strokeLinecap="round">
|
||||
<path d={BIG_C} strokeWidth={60} />
|
||||
<path d={SMALL_C_UPPER} strokeWidth={58} />
|
||||
<path d={SMALL_C_LOWER} strokeWidth={58} />
|
||||
</g>
|
||||
<path d={BAR} fill="none" stroke={SEAL} strokeWidth={35} strokeLinecap="round" />
|
||||
{spec.parts !== 'ccbar' && <path d={CURSOR} fill={SEAL} />}
|
||||
{spec.parts === 'full' && (
|
||||
<>
|
||||
<path d={SPARKLE_LG} fill={SEAL} />
|
||||
<path d={SPARKLE_SM} fill={SEAL} />
|
||||
</>
|
||||
)}
|
||||
<span
|
||||
className={cx('font-black leading-none text-[var(--color-on-primary)]', spec.glyph)}
|
||||
style={{ fontFamily: 'var(--font-headline)' }}
|
||||
>
|
||||
哈
|
||||
</span>
|
||||
</span>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ export function ComponentGallery() {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="BrandSeal" note="The 「哈」 seal. The glyph is platform serif — check it against Songti on macOS. The inner rule appears only at xl.">
|
||||
<Section title="BrandSeal" note="The cc-haha mark, a vector rebuild of the app icon. It sheds parts as it shrinks — sparkles only at xl, cursor drops at sm — so check each size against its neighbours.">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<BrandSeal size="sm" />
|
||||
<BrandSeal size="md" />
|
||||
|
||||
@@ -76,6 +76,7 @@ import { formatBytes } from '../lib/formatBytes'
|
||||
import { isDesktopRuntime } from '../lib/desktopRuntime'
|
||||
import { getDesktopHost } from '../lib/desktopHost'
|
||||
import { publicAssetPath } from '../lib/publicAsset'
|
||||
import { BrandSeal } from '../components/composite/BrandSeal'
|
||||
import { isBrowserSafePort } from '../lib/browserSafePort'
|
||||
import {
|
||||
getDesktopNotificationPermission,
|
||||
@@ -4141,7 +4142,7 @@ function AboutSettings() {
|
||||
return (
|
||||
<div className="w-full min-w-0 max-w-2xl mx-auto flex flex-col items-center py-6">
|
||||
{/* Logo + App Name + Version */}
|
||||
<img src={publicAssetPath('app-icon.png')} alt="Claude Code Haha" className="w-20 h-20 mb-4" />
|
||||
<BrandSeal size="xl" className="mb-4" />
|
||||
<h1 className="text-xl font-bold text-[var(--color-text-primary)]" style={{ fontFamily: 'var(--font-headline)' }}>Claude Code Haha</h1>
|
||||
{version && (
|
||||
<div className="mt-1 flex items-center gap-2 text-xs text-[var(--color-text-tertiary)]">
|
||||
|
||||
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 35 KiB |
@@ -1,3 +1,10 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<image width="1024" height="1024" href="app-icon.png" preserveAspectRatio="xMidYMid meet"/>
|
||||
<svg viewBox="0 0 1024 1024" width="1024" height="1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="88" y="88" width="848" height="848" rx="175" fill="#201D17"/>
|
||||
<path d="M437.75 695.01A131 131 0 1 1 437.75 436.99" fill="none" stroke="#EDE6D6" stroke-width="60" stroke-linecap="round"/>
|
||||
<path d="M505.06 535.98A117 117 0 0 1 610.92 459.07" fill="none" stroke="#EDE6D6" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M635.32 691.22A117 117 0 0 1 515.78 638.00" fill="none" stroke="#EDE6D6" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M441.5 576H517.5" fill="none" stroke="#D07B52" stroke-width="35" stroke-linecap="round"/>
|
||||
<path d="M683.79 384.80Q698.00 380.00 692.43 393.93L665.57 461.07Q660.00 475.00 655.71 460.63L645.00 424.71Q643.00 418.00 636.59 415.18L629.25 411.95Q618.00 407.00 631.26 402.52L683.79 384.80Z" fill="#D07B52"/>
|
||||
<path d="M717.00 308.00Q724.07 325.43 741.50 332.50Q724.07 339.57 717.00 357.00Q709.93 339.57 692.50 332.50Q709.93 325.43 717.00 308.00Z" fill="#D07B52"/>
|
||||
<path d="M755.50 365.00Q759.04 374.96 769.00 378.50Q759.04 382.04 755.50 392.00Q751.96 382.04 742.00 378.50Q751.96 374.96 755.50 365.00Z" fill="#D07B52"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 34 KiB |
@@ -1,3 +1,10 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<image width="1024" height="1024" href="app-icon.png" preserveAspectRatio="xMidYMid meet"/>
|
||||
<svg viewBox="0 0 1024 1024" width="1024" height="1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="88" y="88" width="848" height="848" rx="175" fill="#F6F0E1"/>
|
||||
<path d="M437.75 695.01A131 131 0 1 1 437.75 436.99" fill="none" stroke="#251E11" stroke-width="60" stroke-linecap="round"/>
|
||||
<path d="M505.06 535.98A117 117 0 0 1 610.92 459.07" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M635.32 691.22A117 117 0 0 1 515.78 638.00" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M441.5 576H517.5" fill="none" stroke="#96442B" stroke-width="35" stroke-linecap="round"/>
|
||||
<path d="M683.79 384.80Q698.00 380.00 692.43 393.93L665.57 461.07Q660.00 475.00 655.71 460.63L645.00 424.71Q643.00 418.00 636.59 415.18L629.25 411.95Q618.00 407.00 631.26 402.52L683.79 384.80Z" fill="#96442B"/>
|
||||
<path d="M717.00 308.00Q724.07 325.43 741.50 332.50Q724.07 339.57 717.00 357.00Q709.93 339.57 692.50 332.50Q709.93 325.43 717.00 308.00Z" fill="#96442B"/>
|
||||
<path d="M755.50 365.00Q759.04 374.96 769.00 378.50Q759.04 382.04 755.50 392.00Q751.96 382.04 742.00 378.50Q751.96 374.96 755.50 365.00Z" fill="#96442B"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 34 KiB |
@@ -1,3 +1,10 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<image width="1024" height="1024" href="app-icon.png" preserveAspectRatio="xMidYMid meet"/>
|
||||
<svg viewBox="0 0 1024 1024" width="1024" height="1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="88" y="88" width="848" height="848" rx="175" fill="#F6F0E1"/>
|
||||
<path d="M437.75 695.01A131 131 0 1 1 437.75 436.99" fill="none" stroke="#251E11" stroke-width="60" stroke-linecap="round"/>
|
||||
<path d="M505.06 535.98A117 117 0 0 1 610.92 459.07" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M635.32 691.22A117 117 0 0 1 515.78 638.00" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M441.5 576H517.5" fill="none" stroke="#96442B" stroke-width="35" stroke-linecap="round"/>
|
||||
<path d="M683.79 384.80Q698.00 380.00 692.43 393.93L665.57 461.07Q660.00 475.00 655.71 460.63L645.00 424.71Q643.00 418.00 636.59 415.18L629.25 411.95Q618.00 407.00 631.26 402.52L683.79 384.80Z" fill="#96442B"/>
|
||||
<path d="M717.00 308.00Q724.07 325.43 741.50 332.50Q724.07 339.57 717.00 357.00Q709.93 339.57 692.50 332.50Q709.93 325.43 717.00 308.00Z" fill="#96442B"/>
|
||||
<path d="M755.50 365.00Q759.04 374.96 769.00 378.50Q759.04 382.04 755.50 392.00Q751.96 382.04 742.00 378.50Q751.96 374.96 755.50 365.00Z" fill="#96442B"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 62 KiB |
@@ -1,9 +1,15 @@
|
||||
<svg width="2752" height="921" viewBox="0 0 2752 921" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="2752" height="921" rx="56" fill="#F7FAFC"/>
|
||||
<rect x="64" y="64" width="2624" height="793" rx="48" fill="#FFFFFF"/>
|
||||
<image x="176" y="160" width="600" height="600" href="app-icon.png" preserveAspectRatio="xMidYMid meet"/>
|
||||
<text x="900" y="390" fill="#122033" font-family="SF Pro Display, Avenir Next, Inter, Helvetica Neue, Arial, sans-serif" font-size="220" font-weight="800">Claude Code</text>
|
||||
<text x="900" y="580" fill="#0F8CEB" font-family="SF Pro Display, Avenir Next, Inter, Helvetica Neue, Arial, sans-serif" font-size="170" font-weight="800">Haha</text>
|
||||
<path d="M902 664H1608" stroke="#FF7A00" stroke-width="22" stroke-linecap="round"/>
|
||||
<path d="M1652 664H1848" stroke="#12C7E8" stroke-width="22" stroke-linecap="round"/>
|
||||
<rect width="2752" height="921" rx="56" fill="#E5DBC3"/>
|
||||
<rect x="64" y="64" width="2624" height="793" rx="48" fill="#F6F0E1"/>
|
||||
<g transform="translate(-7.19,-68.10) scale(1.01655)"><path d="M437.75 695.01A131 131 0 1 1 437.75 436.99" fill="none" stroke="#251E11" stroke-width="60" stroke-linecap="round"/>
|
||||
<path d="M505.06 535.98A117 117 0 0 1 610.92 459.07" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M635.32 691.22A117 117 0 0 1 515.78 638.00" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M441.5 576H517.5" fill="none" stroke="#96442B" stroke-width="35" stroke-linecap="round"/>
|
||||
<path d="M683.79 384.80Q698.00 380.00 692.43 393.93L665.57 461.07Q660.00 475.00 655.71 460.63L645.00 424.71Q643.00 418.00 636.59 415.18L629.25 411.95Q618.00 407.00 631.26 402.52L683.79 384.80Z" fill="#96442B"/>
|
||||
<path d="M717.00 308.00Q724.07 325.43 741.50 332.50Q724.07 339.57 717.00 357.00Q709.93 339.57 692.50 332.50Q709.93 325.43 717.00 308.00Z" fill="#96442B"/>
|
||||
<path d="M755.50 365.00Q759.04 374.96 769.00 378.50Q759.04 382.04 755.50 392.00Q751.96 382.04 742.00 378.50Q751.96 374.96 755.50 365.00Z" fill="#96442B"/></g>
|
||||
<text x="900" y="390" fill="#251E11" font-family="SF Pro Display, -apple-system, BlinkMacSystemFont, Avenir Next, Inter, Segoe UI, Helvetica Neue, Arial, sans-serif" font-size="220" font-weight="800">Claude Code</text>
|
||||
<text x="900" y="580" fill="#96442B" font-family="SF Pro Display, -apple-system, BlinkMacSystemFont, Avenir Next, Inter, Segoe UI, Helvetica Neue, Arial, sans-serif" font-size="170" font-weight="800">Haha</text>
|
||||
<path d="M902 664H1608" stroke="#96442B" stroke-width="22" stroke-linecap="round"/>
|
||||
<path d="M1652 664H1848" stroke="#3E6B58" stroke-width="22" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 867 B After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg width="2200" height="640" viewBox="0 0 2200 640" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="2200" height="640" rx="64" fill="#201D17"/>
|
||||
<g transform="translate(-55.75,-102.48) scale(0.81324)"><path d="M437.75 695.01A131 131 0 1 1 437.75 436.99" fill="none" stroke="#EDE6D6" stroke-width="60" stroke-linecap="round"/>
|
||||
<path d="M505.06 535.98A117 117 0 0 1 610.92 459.07" fill="none" stroke="#EDE6D6" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M635.32 691.22A117 117 0 0 1 515.78 638.00" fill="none" stroke="#EDE6D6" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M441.5 576H517.5" fill="none" stroke="#D07B52" stroke-width="35" stroke-linecap="round"/>
|
||||
<path d="M683.79 384.80Q698.00 380.00 692.43 393.93L665.57 461.07Q660.00 475.00 655.71 460.63L645.00 424.71Q643.00 418.00 636.59 415.18L629.25 411.95Q618.00 407.00 631.26 402.52L683.79 384.80Z" fill="#D07B52"/>
|
||||
<path d="M717.00 308.00Q724.07 325.43 741.50 332.50Q724.07 339.57 717.00 357.00Q709.93 339.57 692.50 332.50Q709.93 325.43 717.00 308.00Z" fill="#D07B52"/>
|
||||
<path d="M755.50 365.00Q759.04 374.96 769.00 378.50Q759.04 382.04 755.50 392.00Q751.96 382.04 742.00 378.50Q751.96 374.96 755.50 365.00Z" fill="#D07B52"/></g>
|
||||
<text x="680" y="300" fill="#EDE6D6" font-family="SF Pro Display, -apple-system, BlinkMacSystemFont, Avenir Next, Inter, Segoe UI, Helvetica Neue, Arial, sans-serif" font-size="168" font-weight="800">Claude Code</text>
|
||||
<text x="680" y="448" fill="#D07B52" font-family="SF Pro Display, -apple-system, BlinkMacSystemFont, Avenir Next, Inter, Segoe UI, Helvetica Neue, Arial, sans-serif" font-size="132" font-weight="800">Haha</text>
|
||||
<path d="M1080 414H1502" stroke="#D07B52" stroke-width="16" stroke-linecap="round"/>
|
||||
<path d="M1538 414H1666" stroke="#7DD3C7" stroke-width="16" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 44 KiB |
@@ -1,8 +1,14 @@
|
||||
<svg width="2200" height="640" viewBox="0 0 2200 640" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="2200" height="640" rx="64" fill="#FFFFFF"/>
|
||||
<image x="104" y="80" width="480" height="480" href="app-icon.png" preserveAspectRatio="xMidYMid meet"/>
|
||||
<text x="680" y="300" fill="#122033" font-family="SF Pro Display, Avenir Next, Inter, Helvetica Neue, Arial, sans-serif" font-size="168" font-weight="800">Claude Code</text>
|
||||
<text x="680" y="448" fill="#0F8CEB" font-family="SF Pro Display, Avenir Next, Inter, Helvetica Neue, Arial, sans-serif" font-size="132" font-weight="800">Haha</text>
|
||||
<path d="M1040 414H1502" stroke="#FF7A00" stroke-width="16" stroke-linecap="round"/>
|
||||
<path d="M1538 414H1666" stroke="#12C7E8" stroke-width="16" stroke-linecap="round"/>
|
||||
<rect width="2200" height="640" rx="64" fill="#F6F0E1"/>
|
||||
<g transform="translate(-55.75,-102.48) scale(0.81324)"><path d="M437.75 695.01A131 131 0 1 1 437.75 436.99" fill="none" stroke="#251E11" stroke-width="60" stroke-linecap="round"/>
|
||||
<path d="M505.06 535.98A117 117 0 0 1 610.92 459.07" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M635.32 691.22A117 117 0 0 1 515.78 638.00" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
|
||||
<path d="M441.5 576H517.5" fill="none" stroke="#96442B" stroke-width="35" stroke-linecap="round"/>
|
||||
<path d="M683.79 384.80Q698.00 380.00 692.43 393.93L665.57 461.07Q660.00 475.00 655.71 460.63L645.00 424.71Q643.00 418.00 636.59 415.18L629.25 411.95Q618.00 407.00 631.26 402.52L683.79 384.80Z" fill="#96442B"/>
|
||||
<path d="M717.00 308.00Q724.07 325.43 741.50 332.50Q724.07 339.57 717.00 357.00Q709.93 339.57 692.50 332.50Q709.93 325.43 717.00 308.00Z" fill="#96442B"/>
|
||||
<path d="M755.50 365.00Q759.04 374.96 769.00 378.50Q759.04 382.04 755.50 392.00Q751.96 382.04 742.00 378.50Q751.96 374.96 755.50 365.00Z" fill="#96442B"/></g>
|
||||
<text x="680" y="300" fill="#251E11" font-family="SF Pro Display, -apple-system, BlinkMacSystemFont, Avenir Next, Inter, Segoe UI, Helvetica Neue, Arial, sans-serif" font-size="168" font-weight="800">Claude Code</text>
|
||||
<text x="680" y="448" fill="#96442B" font-family="SF Pro Display, -apple-system, BlinkMacSystemFont, Avenir Next, Inter, Segoe UI, Helvetica Neue, Arial, sans-serif" font-size="132" font-weight="800">Haha</text>
|
||||
<path d="M1080 414H1502" stroke="#96442B" stroke-width="16" stroke-linecap="round"/>
|
||||
<path d="M1538 414H1666" stroke="#3E6B58" stroke-width="16" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 794 B After Width: | Height: | Size: 1.8 KiB |