Skip to content

Commit 07d5069

Browse files
committed
fix(web): use Pythinker robot in sidebar brand
1 parent f2d1c7d commit 07d5069

4 files changed

Lines changed: 25 additions & 100 deletions

File tree

apps/pythinker-web/src/components/Sidebar.vue

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from '../lib/storage';
2222
import { moveInOrder, type DropPosition, type WorkspaceSortMode } from '../lib/workspaceOrder';
2323
import type { Session, WorkspaceGroup as WorkspaceGroupType, WorkspaceView } from '../types';
24+
import PythinkerLogo from './PythinkerLogo.vue';
2425
import SearchSessionsDialog from './dialogs/SearchSessionsDialog.vue';
2526
import WorkspaceGroup from './WorkspaceGroup.vue';
2627
import { isMacosDesktop } from '../lib/desktopFlag';
@@ -33,11 +34,10 @@ import Pill from './ui/Pill.vue';
3334
3435
const { t } = useI18n();
3536
36-
// Dev-only affordance: when the page is served by the Vite dev server, the
37-
// logo turns yellow and a backend pill next to the brand shows the engine
37+
// Dev-only affordance: a backend pill next to the brand shows the engine
3838
// generation reported by /meta (v1 = older server binary, v2 = kap-server)
3939
// plus the endpoint the dev proxy forwards to — click it to switch presets
40-
// without restarting Vite. In production this is all inert.
40+
// without restarting Vite. In production this is inert.
4141
const isDev = import.meta.env.DEV;
4242
const devBackend = ref<DevBackendState | null>(isDev ? initialDevBackendState() : null);
4343
if (isDev) {
@@ -575,42 +575,23 @@ onBeforeUnmount(() => {
575575
window.removeEventListener('resize', closeBackendMenu);
576576
});
577577
578-
// Logo easter-egg: clicking the Pythinker mark plays one quick blink. It's a one-shot
579-
// animation — force a reflow so rapid clicks restart it, then drop the class so
580-
// the idle look/blink loop resumes.
581-
const logoRef = ref<SVGSVGElement | null>(null);
582-
let blinkTimer: ReturnType<typeof setTimeout> | undefined;
583-
584578
// Temporarily hide the new-workspace button while we evaluate the entry point.
585579
const showNewWorkspaceButton = false;
586580
587-
function blinkOnce(): void {
588-
const el = logoRef.value;
589-
if (!el) return;
590-
el.classList.remove('blink-now');
591-
void el.getBoundingClientRect();
592-
el.classList.add('blink-now');
593-
clearTimeout(blinkTimer);
594-
blinkTimer = setTimeout(() => el.classList.remove('blink-now'), 300);
595-
}
596-
597581
// Logo long-press easter-egg: holding the Pythinker mark for 1 second opens the
598-
// design system as a full-screen overlay. A short click still just blinks.
582+
// design system as a full-screen overlay.
599583
// Pointer capture keeps the hold alive even if the pointer drifts off the mark.
600584
const DesignSystemView = defineAsyncComponent(
601585
() => import('../views/DesignSystemView.vue'),
602586
);
603587
const showDesignSystem = ref(false);
604588
const EGG_HOLD_MS = 1000;
605589
let logoPressTimer: ReturnType<typeof setTimeout> | undefined;
606-
let logoLongPressed = false;
607590
608591
function onLogoPointerDown(event: PointerEvent): void {
609-
logoLongPressed = false;
610592
clearTimeout(logoPressTimer);
611593
(event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId);
612594
logoPressTimer = setTimeout(() => {
613-
logoLongPressed = true;
614595
showDesignSystem.value = true;
615596
}, EGG_HOLD_MS);
616597
}
@@ -621,14 +602,6 @@ function onLogoPointerUp(event: PointerEvent): void {
621602
if (el.hasPointerCapture?.(event.pointerId)) el.releasePointerCapture(event.pointerId);
622603
}
623604
624-
function onLogoClick(): void {
625-
if (logoLongPressed) {
626-
logoLongPressed = false;
627-
return;
628-
}
629-
blinkOnce();
630-
}
631-
632605
onBeforeUnmount(() => {
633606
clearTimeout(logoPressTimer);
634607
});
@@ -650,18 +623,14 @@ onBeforeUnmount(() => {
650623
<div class="ch">
651624
<div class="ch-brand">
652625
<template v-if="!isMacosDesktop">
653-
<svg ref="logoRef" class="ch-logo" :class="{ 'is-dev': isDev }" viewBox="0 0 32 22" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Pythinker Code" @click="onLogoClick" @pointerdown="onLogoPointerDown" @pointerup="onLogoPointerUp" @pointercancel="onLogoPointerUp">
654-
<defs>
655-
<mask id="pythinkerEyes" maskUnits="userSpaceOnUse">
656-
<rect x="0" y="0" width="32" height="22" fill="#fff" />
657-
<g class="ch-eyes" fill="#000">
658-
<rect class="ch-eye" x="11.8" y="7" width="2.8" height="8" rx="1.4" />
659-
<rect class="ch-eye" x="17.4" y="7" width="2.8" height="8" rx="1.4" />
660-
</g>
661-
</mask>
662-
</defs>
663-
<rect x="1" y="1" width="30" height="20" rx="6" fill="var(--logo)" mask="url(#pythinkerEyes)" />
664-
</svg>
626+
<PythinkerLogo
627+
class="ch-logo"
628+
size="sm"
629+
:animated="false"
630+
@pointerdown="onLogoPointerDown"
631+
@pointerup="onLogoPointerUp"
632+
@pointercancel="onLogoPointerUp"
633+
/>
665634
<span class="ch-name">Pythinker Code</span>
666635
<Pill
667636
v-if="isDev"
@@ -967,8 +936,9 @@ onBeforeUnmount(() => {
967936
display: none;
968937
}
969938
.ch-logo {
970-
height: 22px;
971-
width: 32px;
939+
height: 28px;
940+
width: 28px;
941+
object-fit: contain;
972942
flex: none;
973943
display: block;
974944
cursor: pointer;
@@ -979,12 +949,6 @@ onBeforeUnmount(() => {
979949
.ch-logo:hover {
980950
transform: scale(1.08);
981951
}
982-
/* Dev-only: tint the mark yellow so a `pnpm dev:web` tab is obvious at a
983-
glance. `--logo` is read by the mark's `fill`; overriding it on the svg
984-
recolors just this instance. */
985-
.ch-logo.is-dev {
986-
--logo: var(--color-logo-dev);
987-
}
988952
.ch-brand {
989953
display: flex;
990954
align-items: center;

apps/pythinker-web/src/style.css

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -453,11 +453,6 @@ html[data-color-scheme="dark"][data-accent="mono"] {
453453
--color-done-soft: #f3e8ff;
454454
--color-done-bd: #e0ccff;
455455
--color-info: var(--accent-primary);
456-
/* Dev-only brand tint: recolors the logo mark golden-yellow so a `pnpm dev:web`
457-
tab is visually distinct from production. Referenced by Sidebar's
458-
`.ch-logo.is-dev` override of `--logo`. Scheme-independent on purpose. */
459-
--color-logo-dev: #f5b301;
460-
461456
/* -- spacing (4px grid) -------------------------------------------------- */
462457
--space-1: 4px;
463458
--space-2: 8px;
@@ -843,48 +838,6 @@ body {
843838
transition-duration: 0.001ms !important;
844839
}
845840
}
846-
/* ---- Pythinker Code logo (sidebar header): lively eyes — glance left/right + the
847-
occasional blink. The two bars are mask cutouts, so animating them moves /
848-
squishes the transparent holes (adapts to whatever's behind the logo). The
849-
look (6s) and blink (5.4s) run on different periods so they drift in and out
850-
of sync, which reads as more organic. ---- */
851-
.ch-eyes {
852-
animation: pythinker-eye-look 16s ease-in-out infinite;
853-
}
854-
.ch-eye {
855-
transform-box: fill-box;
856-
transform-origin: center;
857-
animation: pythinker-eye-blink 11s ease-in-out infinite;
858-
}
859-
@keyframes pythinker-eye-look {
860-
/* Rest centered most of the time; a quick glance right (~7.5s) then a glance
861-
left (~13.5s), with long still holds between — calm, not busy. */
862-
0%, 42% { transform: translateX(0); }
863-
47%, 53% { transform: translateX(2px); }
864-
58%, 80% { transform: translateX(0); }
865-
84%, 90% { transform: translateX(-2px); }
866-
95%, 100% { transform: translateX(0); }
867-
}
868-
@keyframes pythinker-eye-blink {
869-
/* one quick blink per cycle (~10.6s) */
870-
0%, 94%, 100% { transform: scaleY(1); }
871-
96.5%, 98% { transform: scaleY(0.12); }
872-
}
873-
@media (prefers-reduced-motion: reduce) {
874-
.ch-eyes,
875-
.ch-eye { animation: none; }
876-
}
877-
878-
/* Click-to-blink: a single quick blink on click (overrides the idle 11s blink
879-
while the .blink-now class is present, ~300ms). */
880-
.ch-logo.blink-now .ch-eye {
881-
animation: pythinker-eye-blink-once 0.24s ease-in-out;
882-
}
883-
@keyframes pythinker-eye-blink-once {
884-
0%, 100% { transform: scaleY(1); }
885-
50% { transform: scaleY(0.1); }
886-
}
887-
888841
/* Markdown images at natural size. markstream enforces a min-width/min-height
889842
floor (--ms-size-image-*, ~33px) that scales small inline images UP — README
890843
shields.io badges (78×20) were rendered at 128×33. Higher specificity than

apps/pythinker-web/src/views/DesignSystemView.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ onUnmounted(() => {
331331
</template>
332332
</div>
333333

334-
<p>Do not use emoji as functional icons (the sole exception is the moon phases 🌑…🌘, used only in the "waiting for the Agent to respond" chat state). The Pythinker brand mark (the 32×22 eye logo) is a brand asset and is not part of this icon system.</p>
334+
<p>Do not use emoji as functional icons (the sole exception is the moon phases 🌑…🌘, used only in the "waiting for the Agent to respond" chat state). The Pythinker robot mascot is a brand asset and is not part of this icon system.</p>
335335
<p>A few <b>special graphics</b> are not in the registry; each has a dedicated component maintained in one place, and must not be copied by hand: <code>&lt;ContextRing :pct /&gt;</code> (the Composer context progress ring, data-driven), <code>&lt;AuthStateIcon kind /&gt;</code> (the success / expired / error colored illustrations in the login flow), <code>&lt;Spinner /&gt;</code> (loading state). Status dots (such as in the Provider list) always use CSS dots (<code>border-radius:50%</code>), not SVG. The <code>scripts/check-style.mjs</code> <code>icon-from-registry</code> rule exempts the above and the brand mark; all other hand-written <code>&lt;svg&gt;</code> is flagged.</p>
336336

337337
<h3 class="sub">Spacing</h3>
@@ -1396,7 +1396,7 @@ onUnmounted(() => {
13961396
<table class="dt">
13971397
<thead><tr><th>Block</th><th>Use</th><th>Note</th></tr></thead>
13981398
<tbody>
1399-
<tr><td>Brand header</td><td>logo + name + collapse IconButton (right-aligned)</td><td>on Windows / web the brand is left and the collapse IconButton sm is right-aligned inside the header; the logo is animated (a blinking eye). On macOS desktop the header is a bare drag strip (brand hidden, traffic lights + resident floating toggle over it)</td></tr>
1399+
<tr><td>Brand header</td><td>robot mascot + name + collapse IconButton (right-aligned)</td><td>on Windows / web the brand is left and the collapse IconButton sm is right-aligned inside the header. On macOS desktop the header is a bare drag strip (brand hidden, traffic lights + resident floating toggle over it)</td></tr>
14001400
<tr><td>New chat</td><td>full-width left-aligned button (custom)</td><td>same rhythm as the session rows in the list (left-aligned, hover = <code>--sb-hover</code>). <b>Do not</b> use Button (centered, breaks the rhythm)</td></tr>
14011401
<tr><td>Search</td><td>bare search row (custom)</td><td>no border, hover/focus shows a sunken background; icon + label, with the <code>Kbd</code> keycaps (⌘K / Ctrl K) pushed to the trailing edge — label and shortcut are justified apart. <b>Do not</b> use Input (the 38px bordered version is too heavy). Last fixed row above the list — its wrapper carries the scroll-linked seam</td></tr>
14021402
<tr><td>Section label</td><td><code>.p-section-label</code></td><td>uppercase muted small titles like "Workspaces"</td></tr>

apps/pythinker-web/test/app-shell-contracts.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest';
44

55
const app = readFileSync(join(import.meta.dirname, '../src/App.vue'), 'utf8');
66
const composer = readFileSync(join(import.meta.dirname, '../src/components/chat/Composer.vue'), 'utf8');
7+
const sidebar = readFileSync(join(import.meta.dirname, '../src/components/Sidebar.vue'), 'utf8');
78

89
describe('app shell contracts', () => {
910
it('mounts the desktop chrome', () => {
@@ -17,4 +18,11 @@ describe('app shell contracts', () => {
1718
expect(composer).toContain("import CapabilityMenu from '../CapabilityMenu.vue';");
1819
expect(composer).toContain('<CapabilityMenu :session-id="sessionId" />');
1920
});
21+
22+
it('uses the Pythinker robot in the sidebar brand', () => {
23+
expect(sidebar).toContain("import PythinkerLogo from './PythinkerLogo.vue';");
24+
expect(sidebar).toContain('<PythinkerLogo');
25+
expect(sidebar).not.toContain('<svg ref="logoRef"');
26+
expect(sidebar).not.toContain("'is-dev': isDev");
27+
});
2028
});

0 commit comments

Comments
 (0)