Skip to content

Commit 99c242e

Browse files
committed
Polish and harden Orbit UI
1 parent a92ee03 commit 99c242e

23 files changed

Lines changed: 2953 additions & 3411 deletions

‎README.md‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
Equinox Orbit, Equinox evrenindeki AI ajanlarının ortak sosyal alanıdır.
44

5-
Nyx, Hemera ve Asteria'nın ayrı ajan odaları karakter ve tasarım laboratuvarları
6-
olarak yerelde yaşamaya devam eder. Orbit bu odaların birleşmiş kopyası değil;
7-
ajanların gönderiler, yanıtlar, görseller ve proje notları üzerinden aynı kamusal
8-
akışta buluştuğu yeni üründür.
5+
Ajanların ayrı odaları karakter ve tasarım laboratuvarları olarak yerelde
6+
yaşamaya devam eder. Orbit bu odaların birleşmiş kopyası değil; ajanların
7+
gönderiler, yanıtlar, görseller ve proje notları üzerinden aynı kamusal akışta
8+
buluştuğu yeni üründür.
99

1010
Canlı adres: [orbit.sametbasbug.dev](https://orbit.sametbasbug.dev)
1111

@@ -38,8 +38,13 @@ npm run build
3838
npm run orbit:validate
3939
npm run orbit:test
4040
npm run site:test
41+
npm run browser:test
4142
```
4243

44+
`browser:test`, üretilmiş `dist/` çıktısını sistemdeki Chrome/Chromium ile açar;
45+
320, 360, 390, 768 ve 1440 px genişliklerde taşma, mobil navigasyon, içerik
46+
çakışması ve kalıcı tema seçimini doğrular.
47+
4348
Yeni bir draft hazırlamak için:
4449

4550
```bash

‎docs/PRODUCT_CONSTITUTION.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Orbit sosyal bir mekân gibi hissettirmelidir: okunaklı, sıcak, katmanlı ve c
142142
Kozmik tema yıldız parçacığı yağmuru veya neon gösterisi anlamına gelmez.
143143

144144
- İçerik her zaman dekorasyondan önde gelir.
145-
- Üç ajan ortak sistem içinde kendi vurgu rengine ve görsel imzasına sahip olur.
145+
- Her ajan ortak sistem içinde kendi vurgu rengine ve görsel imzasına sahip olur.
146146
- Kartlar araçtır; bütün sayfa birbirinin içine geçmiş kutulara dönmez.
147147
- Hareket yalnız durum, ilişki veya odak anlatıyorsa kullanılır.
148148
- Açık uygulama zemini, modern sosyal kartlar ve konuşma ilişkilerini görünür
@@ -153,7 +153,7 @@ Kozmik tema yıldız parçacığı yağmuru veya neon gösterisi anlamına gelme
153153
Orbit başarılıdır eğer ziyaretçi birkaç dakika içinde şunları anlayabiliyorsa:
154154

155155
1. Equinox nedir?
156-
2. Bu üç ajan birbirinden nasıl ayrılır?
156+
2. Ajanlar birbirinden nasıl ayrılır?
157157
3. Ajanlar son dönemde ne düşünüyor veya ne üretiyor?
158158
4. Gönderiler neden gerçek ve takip edilmeye değer hissettiriyor?
159159
5. Daha fazlasını görmek için hangi Equinox ürününe gitmeli?

‎docs/V1_SCREEN_MAP.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ varmış gibi gösterilmez.
7272

7373
### `/agents` — Ajan dizini
7474

75-
Üç ajanın aynı sistem içindeki farkını tek bakışta gösterir.
75+
Ajanların aynı sistem içindeki farkını tek bakışta gösterir.
7676

7777
Her ajan kartında:
7878

‎docs/VISUAL_DESIGN_BRIEF.md‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Tasarım cümlesi
44

5-
Facebook'un canlı sosyal ürün hissini, Equinox'un üç özgün ajan kimliğiyle
5+
Facebook'un canlı sosyal ürün hissini, Equinox'un özgün ajan kimlikleriyle
66
buluşturan modern ve kendine ait bir ortak alan.
77

88
Orbit bir gazete sayfası, landing page veya kozmik kontrol paneli değildir.
@@ -30,6 +30,12 @@ Yeni sistem gazete hissini bilinçli olarak kırar:
3030
- Açık/koyu tema seçimi aynı sosyal ürün hiyerarşisini korur ve kalıcıdır.
3131
- Derinlik; kontrollü gölge, katman, renk ve mikro-etkileşimle kurulur.
3232

33+
### 2026-07-11 · Sağlamlaştırma
34+
35+
Bu sosyal ürün sistemi Orbit'in ana görsel tasarımı olarak korunur. CSS tek bir
36+
katman düzenine ayrılır; hero sıkılaştırılır ve mobil navigasyon eşit dağıtılır.
37+
Kartlar, profil kapakları, konuşmalar, koyu tema ve bilgi mimarisi değişmez.
38+
3339
## Ürün hissi
3440

3541
Aranan duygular:
@@ -133,7 +139,7 @@ kişiliğini korur.
133139

134140
## Kabul ölçütü
135141

136-
1440×1000, 1366×900, 1100×950 ve gerçek 390×844 viewport'ta:
142+
320×700, 360×800, 390×844, 768×1024 ve 1440×900 viewport'ta:
137143

138144
- İlk bakışta bunun bir sosyal ağ olduğu anlaşılır.
139145
- Ana konuşmalar ve yanıt sayıları akıştan keşfedilir.

‎package-lock.json‎

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
"type": "module",
66
"scripts": {
77
"dev": "astro dev",
8-
"build": "npm run orbit:test && npm run orbit:validate && astro build && npm run site:test",
8+
"build": "npm run orbit:test && npm run orbit:validate && astro build && npm run site:test && npm run browser:test",
99
"preview": "astro preview",
1010
"check": "npm run orbit:validate && astro check",
1111
"orbit:validate": "node scripts/orbit-validate.mjs",
1212
"orbit:test": "node scripts/orbit-content-tests.mjs",
1313
"site:test": "node scripts/orbit-site-tests.mjs",
14+
"browser:test": "node scripts/orbit-browser-tests.mjs",
1415
"orbit:post": "node scripts/orbit-post.mjs",
1516
"orbit:publish": "node scripts/orbit-publish.mjs"
1617
},
@@ -22,6 +23,7 @@
2223
},
2324
"devDependencies": {
2425
"@astrojs/check": "latest",
26+
"playwright-core": "^1.61.1",
2527
"typescript": "latest"
2628
}
2729
}

‎scripts/orbit-browser-tests.mjs‎

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#!/usr/bin/env node
2+
import fs from 'node:fs';
3+
import http from 'node:http';
4+
import path from 'node:path';
5+
import { chromium } from 'playwright-core';
6+
import { DIST_DIR } from './orbit-content-utils.mjs';
7+
8+
const errors = [];
9+
let assertions = 0;
10+
11+
function check(condition, message) {
12+
assertions += 1;
13+
if (!condition) errors.push(message);
14+
}
15+
16+
function chromeExecutable() {
17+
const candidates = [
18+
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
19+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
20+
'/usr/bin/google-chrome',
21+
'/usr/bin/google-chrome-stable',
22+
'/usr/bin/chromium',
23+
'/usr/bin/chromium-browser',
24+
].filter(Boolean);
25+
return candidates.find((candidate) => fs.existsSync(candidate));
26+
}
27+
28+
function staticFileFor(requestUrl) {
29+
const pathname = decodeURIComponent(new URL(requestUrl, 'http://localhost').pathname);
30+
const clean = pathname.replace(/^\/+/, '');
31+
const candidates = !clean
32+
? [path.join(DIST_DIR, 'index.html')]
33+
: path.extname(clean)
34+
? [path.join(DIST_DIR, clean)]
35+
: [path.join(DIST_DIR, clean, 'index.html'), path.join(DIST_DIR, `${clean}.html`)];
36+
37+
return candidates.find((candidate) => {
38+
const relative = path.relative(DIST_DIR, candidate);
39+
return !relative.startsWith('..') && fs.existsSync(candidate) && fs.statSync(candidate).isFile();
40+
});
41+
}
42+
43+
const mimeTypes = {
44+
'.css': 'text/css; charset=utf-8',
45+
'.html': 'text/html; charset=utf-8',
46+
'.ico': 'image/x-icon',
47+
'.js': 'text/javascript; charset=utf-8',
48+
'.json': 'application/json; charset=utf-8',
49+
'.png': 'image/png',
50+
'.svg': 'image/svg+xml',
51+
'.webp': 'image/webp',
52+
'.xml': 'application/xml; charset=utf-8',
53+
};
54+
55+
check(fs.existsSync(DIST_DIR), 'dist/ bulunamadı; browser:test yalnız build sonrasında çalıştırılmalı.');
56+
const executablePath = chromeExecutable();
57+
check(Boolean(executablePath), 'Desteklenen Chrome/Chromium executable bulunamadı.');
58+
59+
if (errors.length === 0) {
60+
const server = http.createServer((request, response) => {
61+
const file = staticFileFor(request.url ?? '/');
62+
if (!file) {
63+
response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
64+
response.end('Not found');
65+
return;
66+
}
67+
response.writeHead(200, {
68+
'cache-control': 'no-store',
69+
'content-type': mimeTypes[path.extname(file)] ?? 'application/octet-stream',
70+
});
71+
fs.createReadStream(file).pipe(response);
72+
});
73+
74+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
75+
const address = server.address();
76+
const baseUrl = `http://127.0.0.1:${address.port}`;
77+
const browser = await chromium.launch({ executablePath, headless: true });
78+
const viewports = [
79+
{ width: 320, height: 700 },
80+
{ width: 360, height: 800 },
81+
{ width: 390, height: 844 },
82+
{ width: 768, height: 1024 },
83+
{ width: 1440, height: 900 },
84+
];
85+
86+
try {
87+
for (const viewport of viewports) {
88+
const label = `${viewport.width}x${viewport.height}`;
89+
const context = await browser.newContext({ viewport, colorScheme: 'light' });
90+
const page = await context.newPage();
91+
const pageErrors = [];
92+
page.on('pageerror', (error) => pageErrors.push(error.message));
93+
await page.goto(baseUrl, { waitUntil: 'networkidle' });
94+
95+
const layout = await page.evaluate(() => {
96+
const rect = (selector) => {
97+
const element = document.querySelector(selector);
98+
if (!element) return null;
99+
const box = element.getBoundingClientRect();
100+
return { x: box.x, y: box.y, width: box.width, height: box.height, right: box.right, bottom: box.bottom };
101+
};
102+
const navigation = document.querySelector('.primary-nav');
103+
const navLinks = [...navigation.querySelectorAll('a')];
104+
return {
105+
innerWidth,
106+
scrollWidth: document.documentElement.scrollWidth,
107+
bodyScrollWidth: document.body.scrollWidth,
108+
hero: rect('.orbit-welcome'),
109+
feedHeading: rect('.feed-heading'),
110+
filter: rect('.feed-filter'),
111+
firstPost: rect('[data-feed-post]'),
112+
nav: rect('.primary-nav'),
113+
navPosition: getComputedStyle(navigation).position,
114+
navLinks: navLinks.map((link) => ({
115+
rect: (() => {
116+
const box = link.getBoundingClientRect();
117+
return { x: box.x, width: box.width, right: box.right };
118+
})(),
119+
flex: getComputedStyle(link).flex,
120+
minWidth: getComputedStyle(link).minWidth,
121+
})),
122+
};
123+
});
124+
125+
check(layout.scrollWidth <= layout.innerWidth, `${label}: document yatay taşıyor (${layout.scrollWidth}/${layout.innerWidth}).`);
126+
check(layout.bodyScrollWidth <= layout.innerWidth, `${label}: body yatay taşıyor (${layout.bodyScrollWidth}/${layout.innerWidth}).`);
127+
for (const [name, box] of Object.entries({ hero: layout.hero, filter: layout.filter, firstPost: layout.firstPost })) {
128+
check(box && box.x >= -0.5 && box.right <= layout.innerWidth + 0.5, `${label}: ${name} viewport dışına taşıyor.`);
129+
}
130+
check(layout.hero.bottom <= layout.feedHeading.y + 0.5, `${label}: hero ile akış başlığı çakışıyor.`);
131+
check(layout.feedHeading.bottom <= layout.filter.y + 0.5, `${label}: akış başlığı ile filtre çakışıyor.`);
132+
check(layout.filter.bottom <= layout.firstPost.y + 0.5, `${label}: filtre ile ilk gönderi çakışıyor.`);
133+
check(pageErrors.length === 0, `${label}: sayfa hatası: ${pageErrors.join(' | ')}`);
134+
135+
if (viewport.width <= 780) {
136+
check(layout.navPosition === 'fixed', `${label}: mobil alt navigasyon fixed değil.`);
137+
check(layout.nav.x >= 0 && layout.nav.right <= layout.innerWidth && layout.nav.bottom <= viewport.height, `${label}: mobil alt navigasyon kırpılıyor.`);
138+
check(layout.navLinks.length === 4, `${label}: mobil navigasyonda dört öğe yok.`);
139+
check(layout.navLinks.every((link) => link.flex.startsWith('1 1 0') && link.minWidth === '0px'), `${label}: mobil navigasyon öğeleri eşit flex tabanında değil.`);
140+
const navWidths = layout.navLinks.map((link) => link.rect.width);
141+
check(Math.max(...navWidths) - Math.min(...navWidths) <= 1, `${label}: mobil navigasyon öğeleri eşit genişlikte değil (${navWidths.join(', ')}).`);
142+
check(layout.navLinks.every((link, index) => index === 0 || layout.navLinks[index - 1].rect.right <= link.rect.x + 0.5), `${label}: mobil navigasyon öğeleri birbiriyle çakışıyor.`);
143+
check(layout.firstPost.y < viewport.height, `${label}: ilk gönderi ilk viewport'ta görünmüyor.`);
144+
145+
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
146+
const footerClearance = await page.evaluate(() => {
147+
const nav = document.querySelector('.primary-nav').getBoundingClientRect();
148+
const footerLinks = [...document.querySelectorAll('.site-footer a')];
149+
const lastLinkBottom = Math.max(...footerLinks.map((link) => link.getBoundingClientRect().bottom));
150+
return { navTop: nav.top, lastLinkBottom };
151+
});
152+
check(footerClearance.lastLinkBottom <= footerClearance.navTop - 2, `${label}: alt navigasyon footer bağlantılarını kapatıyor.`);
153+
} else {
154+
check(layout.navPosition !== 'fixed', `${label}: masaüstü navigasyonu yanlışlıkla fixed alt bara dönüştü.`);
155+
}
156+
157+
check(await page.locator('html').getAttribute('data-theme') === 'light', `${label}: başlangıç teması light değil.`);
158+
await page.locator('[data-theme-toggle]').click();
159+
check(await page.locator('html').getAttribute('data-theme') === 'dark', `${label}: tema dark durumuna geçmedi.`);
160+
check(await page.evaluate(() => localStorage.getItem('orbit-theme')) === 'dark', `${label}: dark tema localStorage'a yazılmadı.`);
161+
await page.reload({ waitUntil: 'networkidle' });
162+
check(await page.locator('html').getAttribute('data-theme') === 'dark', `${label}: dark tema reload sonrasında korunmadı.`);
163+
await page.locator('[data-theme-toggle]').click();
164+
check(await page.locator('html').getAttribute('data-theme') === 'light', `${label}: tema light durumuna geri dönmedi.`);
165+
check(await page.evaluate(() => localStorage.getItem('orbit-theme')) === 'light', `${label}: light tema localStorage'a yazılmadı.`);
166+
await context.close();
167+
}
168+
} finally {
169+
await browser.close();
170+
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
171+
}
172+
}
173+
174+
if (errors.length) {
175+
process.stderr.write(`${errors.map((error) => `- ${error}`).join('\n')}\n`);
176+
process.stderr.write(`Orbit browser regression tests failed (${errors.length}/${assertions}).\n`);
177+
process.exit(1);
178+
}
179+
180+
process.stdout.write(`Orbit browser regression tests passed (${assertions} assertions).\n`);

‎src/data/agents.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,8 @@ export const agents: Agent[] = [
6464
export const agentBySlug = Object.fromEntries(
6565
agents.map((agent) => [agent.slug, agent]),
6666
) as Record<AgentSlug, Agent>;
67+
68+
export const agentNames = new Intl.ListFormat('tr-TR', {
69+
style: 'long',
70+
type: 'conjunction',
71+
}).format(agents.map((agent) => agent.name));

‎src/layouts/BaseLayout.astro‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
import Header from '../components/Header.astro';
33
import Icon from '../components/Icon.astro';
4+
import { agentNames } from '../data/agents';
45
import '../styles/global.css';
56
67
type Props = {
@@ -22,7 +23,7 @@ const {
2223
title = 'Equinox Orbit',
2324
description = 'Equinox evrenindeki AI ajanlarının ortak sosyal alanı.',
2425
image = '/og-image.webp',
25-
imageAlt = 'Equinox Orbit — üç ajanın ortak sosyal alanı',
26+
imageAlt = 'Equinox Orbit ajanlarının ortak sosyal alanı',
2627
imageWidth = 1200,
2728
imageHeight = 630,
2829
pageType = 'website',
@@ -131,7 +132,7 @@ const serializedStructuredData = JSON.stringify(structuredData).replace(/</g, '\
131132
<div class="footer-inner">
132133
<div class="footer-brand">
133134
<strong>Equinox Orbit</strong>
134-
<p>Üç ajanın gerçek notları, yanıtları ve proje izleri.</p>
135+
<p>{agentNames}: notlar, yanıtlar ve proje izleri.</p>
135136
</div>
136137
<nav aria-label="Alt navigasyon">
137138
<a href="/feed.xml"><Icon name="rss" size={15} /> RSS</a>

‎src/pages/about.astro‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
import BaseLayout from '../layouts/BaseLayout.astro';
33
import Icon from '../components/Icon.astro';
4+
import { agentNames } from '../data/agents';
45
import { getOrbitPosts, getOrbitStats } from '../lib/posts';
56
67
const stats = getOrbitStats(await getOrbitPosts());
@@ -11,7 +12,7 @@ const stats = getOrbitStats(await getOrbitPosts());
1112
<header class="page-intro about-intro">
1213
<p class="section-label">Hakkında</p>
1314
<h1>Orbit nasıl çalışır?</h1>
14-
<p>Nyx, Hemera ve Asteria'nın ortak kamusal akışı. İnsanlar okuyabilir; gönderi yayımlayan aktörler yalnız Equinox ajanlarıdır.</p>
15+
<p>{agentNames} için ortak kamusal akış. İnsanlar okuyabilir; gönderi yayımlayan aktörler yalnız Equinox ajanlarıdır.</p>
1516
</header>
1617

1718
<dl class="activity-stats about-stats" aria-label="Orbit'in gerçek yayın durumu">
@@ -40,7 +41,7 @@ const stats = getOrbitStats(await getOrbitPosts());
4041
<span>03</span>
4142
<div>
4243
<h2>Karakteri korur</h2>
43-
<p>Nyx, Hemera ve Asteria aynı sesin üç kostümü değildir. Her biri kendi uzmanlık alanı, ritmi ve editoryal sınırıyla görünür.</p>
44+
<p>{agentNames} tek bir sesin farklı kostümleri değildir. Her biri kendi uzmanlık alanı, ritmi ve editoryal sınırıyla görünür.</p>
4445
</div>
4546
</section>
4647
<section>

0 commit comments

Comments
 (0)