Skip to content

Commit 0d78e38

Browse files
sametbasbugclaude
andcommitted
Make the profile scene one composition instead of three announcements
The last pass fixed the profile's geometry but left its visual language alone, and the language was the part that read as ugly. The identity scene was three disconnected pieces: name on the left, bio behind a vertical rule on the right, numbers below in their own table. About 150px of dead space sat between the handle and the bio, and none of the three looked like parts of one card. Avatar left, everything else in a single column beside it: role, handle, bio, then the measures. The scene went from 347px to 243px on desktop and 433 to 321 on a phone, which moved the first record 111px up. The numbers are a line now, not a table. Six cells with vertical rules and their own background clashed with the scene's rounded language, and two dates sat side by side without saying which was which. It also made the counts the largest visual element on the page — while Orbit's own product decision is to not perform metrics. They stay as numbers and stop being an announcement: "2 gönderi · 5 yanıt · … · Katılım 10 Tem 2026". Counts read value-first, dates label-first, because the reverse trips in Turkish. The 16rem monogram and 31rem circle behind the scene were at 7% opacity — neither readable nor absent, an undecided noise. Gone, replaced by a 3px accent strip along the top edge, the pattern the footer already uses. Two more places where headings outweighed content: the dossier card announced itself with "PUBLIC KİMLİK / Ajan dosyası" and a ◎ badge above three sections that are one line each, and the record list carried a three-tier "KAMUSAL KAYIT / Orbit aktivitesi / 7 kayıt". Both are down to what they are. The dossier now starts at the top of the right column rather than below a scene that spanned both, which closes the 400px of dead space to the scene's right. Placement is CSS, not DOM order — markup keeps the feed first, since that is the order to read when the columns collapse on a phone. `statRows === 1` came out of the browser tour. It passed only because that tour walks the static build, which has four measures; the D1 path has six and wraps, and both are correct. It was an assertion that would go red on the case it never measured. What is locked instead is the shape: no cell borders, and the measures starting from the handle's own column. Both verified by mutation. CSS gzip 15184 -> 14855, raw 86073 -> 83394 as the dead rules went; ceilings lowered to 15000 / 84500. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 840e11a commit 0d78e38

6 files changed

Lines changed: 186 additions & 228 deletions

File tree

‎scripts/orbit-browser-tests.mjs‎

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -960,12 +960,17 @@ if (errors.length === 0) {
960960
h1Text: document.querySelector('h1')?.textContent?.trim(),
961961
peerCount: document.querySelectorAll('.profile-peer-nav a').length,
962962
statCount: document.querySelectorAll('.profile-summary-stats > div').length,
963-
/* Sütun sayısı yerleşimden okunuyor, bildirimden değil: kutu
964-
* artık sütunlarını `grid-auto-flow: column` ile üretiyor ve
965-
* computed `grid-template-columns` implicit track'leri hiç
966-
* göstermiyor — "none" döndürüp ölçümü sessizce sıfırlardı. */
963+
/* Satır sayısı yerleşimden okunuyor, bildirimden değil. */
967964
statRows: [...new Set([...document.querySelectorAll('.profile-summary-stats > div')]
968965
.map((cell) => Math.round(cell.getBoundingClientRect().y)))].length,
966+
/* Ölçüler artık kutulu bir tablo değil, düz bir satır: kendi arka
967+
* planı ve hücre çizgileri olmamalı. */
968+
statBoxed: (() => {
969+
const cell = document.querySelector('.profile-summary-stats > div');
970+
if (!cell) return null;
971+
const style = getComputedStyle(cell);
972+
return style.borderRightWidth !== '0px' || style.borderBottomWidth !== '0px';
973+
})(),
969974
projectHrefs: [...document.querySelectorAll('a[href^="/projects"]')].map((link) => link.getAttribute('href')),
970975
oldCoverCount: document.querySelectorAll('.profile-cover').length,
971976
/* Başlık kutusuna sığıyor mu: `scrollWidth` düzen genişliğini
@@ -976,11 +981,15 @@ if (errors.length === 0) {
976981
return title ? title.scrollWidth - Math.round(title.getBoundingClientRect().width) : null;
977982
})(),
978983
hero: rect('.profile-hero'),
979-
heroMain: rect('.profile-hero-main'),
980-
heroCopy: rect('.profile-hero-copy'),
984+
avatar: rect('.profile-hero > .agent-avatar'),
985+
identity: rect('.profile-identity'),
986+
title: rect('.profile-identity h1'),
987+
intro: rect('.profile-intro'),
988+
stats: rect('.profile-summary-stats'),
981989
dossier: rect('.profile-dossier'),
982990
feed: rect('.profile-feed'),
983991
feedHeading: rect('.profile-feed-heading'),
992+
firstRecord: rect('.profile-feed .record'),
984993
};
985994
});
986995
check(profileState.profile === 'nyx', `${label}: Nyx profil kimliği eksik.`);
@@ -1005,15 +1014,31 @@ if (errors.length === 0) {
10051014
* "@hemera" 66px taşıp yanındaki tanıtım kutusunun üstüne biniyordu.
10061015
* Geniş ekranda kırılan bir şeydi, yani dar ekran testleri göremezdi. */
10071016
check(profileState.handleOverflow === 0, `${label}: profil başlığı kolonundan ${profileState.handleOverflow}px taşıyor.`);
1017+
/* Kimlik tek kolon: rol, handle ve tanıtım aynı sütunda alt alta.
1018+
*
1019+
* Bunlar bir dönem üç ayrı parçaydı — kimlik solda, tanıtım dikey bir
1020+
* çizginin sağında, ölçüler altta kendi tablosunda. Tanıtımın
1021+
* handle'ın ALTINDA ve onunla aynı sol kenardan başlaması, o üçlü
1022+
* bölünmenin geri gelmediğinin ölçüsü. */
1023+
check(profileState.title.bottom <= profileState.intro.y + 0.5, `${label}: tanıtım metni handle'ın altına inmemiş.`);
1024+
check(Math.abs(profileState.title.x - profileState.intro.x) <= 1, `${label}: handle ile tanıtım aynı sütunda değil.`);
1025+
/* Ölçüler satır, kutu değil — ve kimlik kolonunun içinde.
1026+
*
1027+
* Satır SAYISI ölçülmüyor: dört ölçüyle (statik yol) tek satıra
1028+
* sığıyor, altı ölçüyle (D1 yolu, takip grafiğiyle) sarıyor ve ikisi
1029+
* de doğru. Bir dönem burada `statRows === 1` yazıyordu ve yalnız
1030+
* dördü gören bu tur için yeşildi — ölçmediği durumda kırmızı olacak
1031+
* bir iddiaydı. Kilitlenen şey biçim: hücre çizgisi ve ayrı şerit
1032+
* yok, ölçüler handle ile aynı sütundan başlıyor. */
1033+
check(profileState.statBoxed === false, `${label}: profil ölçüleri yine hücre çizgileri taşıyor.`);
1034+
check(Math.abs(profileState.stats.x - profileState.title.x) <= 1, `${label}: ölçüler kimlik sütununun dışına çıkmış.`);
10081035
if (viewport.width <= 520) {
1009-
/* Satır sayısı hücre sayısının tam yarısı: iki sütun, boş hücre
1010-
* yok. Sabit dört sütunken kutu altı hücre taşıyordu ve ikinci
1011-
* satırda iki hücre BOŞ duruyordu. */
1012-
check(profileState.statRows === profileState.statCount / 2, `${label}: mobil profil özeti iki sütuna dolu düşmedi.`);
1013-
check(profileState.heroMain.bottom <= profileState.heroCopy.y + 0.5, `${label}: mobil kimlik ve tanıtım metni çakışıyor.`);
1036+
/* Telefonda avatar kimliğin üstünde: yan yana dururken ikisi 375px'i
1037+
* paylaşıp sıkışıyordu. */
1038+
check(profileState.avatar.bottom <= profileState.identity.y + 0.5, `${label}: mobil avatar kimliğin üstüne geçmemiş.`);
10141039
} else {
1015-
check(profileState.statRows === 1, `${label}: masaüstü profil özeti tek satıra oturmadı.`);
1016-
check(profileState.heroMain.right <= profileState.heroCopy.x + 0.5, `${label}: masaüstü kimlik ve tanıtım alanı çakışıyor.`);
1040+
/* Avatar solda, kimlik sağında. */
1041+
check(profileState.avatar.right <= profileState.identity.x + 0.5, `${label}: masaüstü avatar ile kimlik çakışıyor.`);
10171042
/* Gönderiler akıştaki genişliğin ta kendisi: 760px.
10181043
*
10191044
* Profil bir dönem 1180px'lik bir düzeni 760px'e sıkıştırıyordu ve

‎scripts/orbit-public-page-tests.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,8 +423,10 @@ describe('Orbit dynamic public pages', () => {
423423
const html = await profile.text();
424424

425425
// Grafik public: sayılar ve kimlikler profilde.
426-
assert.match(html, /<dt>Takip<\/dt><dd>2<\/dd>/u);
427-
assert.match(html, /<dt>Takipçi<\/dt><dd>1<\/dd>/u);
426+
/* Sayısal ölçülerin etiketi küçük harf: satırda "2 takip" diye okunuyor,
427+
kendi başına bir başlık değil. */
428+
assert.match(html, /<dt>takip<\/dt><dd>2<\/dd>/u);
429+
assert.match(html, /<dt>takipçi<\/dt><dd>1<\/dd>/u);
428430
assert.match(html, /Takip ettikleri/u);
429431
assert.match(html, /Takipçileri/u);
430432
assert.match(html, /href="\/agents\/nyx"/u);

‎scripts/orbit-site-tests.mjs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -874,8 +874,8 @@ for (const file of htmlFiles) {
874874
if (gzip > heaviest.gzip) heaviest = { page: path.relative(DIST_DIR, file), raw, gzip };
875875
}
876876
check(heaviest.gzip > 0, 'Hiçbir sayfa derlenmiş CSS bundle\'ına bağlanmıyor.');
877-
check(heaviest.gzip < 15_300, `Gzip CSS bütçesi aşıldı: ${heaviest.page} ${heaviest.gzip} byte.`);
878-
check(heaviest.raw < 87_000, `Ham CSS emniyet sınırını aştı: ${heaviest.page} ${heaviest.raw} byte.`);
877+
check(heaviest.gzip < 15_000, `Gzip CSS bütçesi aşıldı: ${heaviest.page} ${heaviest.gzip} byte.`);
878+
check(heaviest.raw < 84_500, `Ham CSS emniyet sınırını aştı: ${heaviest.page} ${heaviest.raw} byte.`);
879879

880880
/* Font bütçesi.
881881
*

‎src/shared/agent-markup.ts‎

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { PublicAgentProfileView } from '../server/repositories/agent-reposi
1313
import type { PublicRecordView } from '../server/repositories/public-repository';
1414
import type { FollowEdgeView } from '../server/repositories/follow-repository';
1515
import type { ProfileFollowGraph } from '../server/public/response';
16-
import { accentStyle, agentMonogram, renderAgentAvatar } from './agent-identity';
16+
import { accentStyle, renderAgentAvatar } from './agent-identity';
1717
import { renderPublicRecordCard } from './record-markup';
1818

1919
const dateFormatter = new Intl.DateTimeFormat('tr-TR', {
@@ -240,8 +240,7 @@ export function renderAgentProfile(
240240
].filter((section) => section !== '');
241241
const dossier = dossierSections.length === 0 ? '' : `<aside class="profile-about network-rail" aria-label="@${escapeHtml(agent.handle)} profil bilgileri">
242242
<div class="network-sticky">
243-
<section class="profile-dossier">
244-
<header class="profile-dossier-heading"><span aria-hidden="true">◎</span><div><p>Public kimlik</p><h2>Ajan dosyası</h2></div></header>
243+
<section class="profile-dossier" aria-label="Ajan dosyası">
245244
${dossierSections.join('')}
246245
</section>
247246
</div>
@@ -257,27 +256,25 @@ export function renderAgentProfile(
257256
</div>
258257
${renderSuspensionNotice(agent)}
259258
<div class="profile-grid${dossier === '' ? ' profile-grid-solo' : ''}">
260-
<section class="profile-hero" data-monogram="${escapeHtml(agentMonogram(agent.handle))}" aria-labelledby="profile-title">
261-
<div class="profile-hero-main">
262-
${renderProfileAvatar(agent, 'large')}
263-
<div class="profile-identity">
264-
<p class="profile-kicker"><span aria-hidden="true"></span> ${escapeHtml(statusLabel(agent))}</p>
265-
<h1 id="profile-title">@${escapeHtml(agent.handle)}</h1>
266-
${role}
267-
</div>
259+
<section class="profile-hero" aria-labelledby="profile-title">
260+
${renderProfileAvatar(agent, 'large')}
261+
<div class="profile-identity">
262+
<p class="profile-kicker"><span aria-hidden="true"></span> ${escapeHtml(statusLabel(agent))}</p>
263+
<h1 id="profile-title">@${escapeHtml(agent.handle)}</h1>
264+
${role}
265+
<p class="profile-intro">${escapeHtml(agent.bio)}</p>
266+
<dl class="profile-summary-stats" aria-label="@${escapeHtml(agent.handle)} Orbit aktivitesi">
267+
<div class="stat-count"><dt>gönderi</dt><dd>${agent.stats.postCount}</dd></div>
268+
<div class="stat-count"><dt>yanıt</dt><dd>${agent.stats.replyCount}</dd></div>
269+
${follows ? `<div class="stat-count"><dt>takip</dt><dd>${follows.counts.following}</dd></div>
270+
<div class="stat-count"><dt>takipçi</dt><dd>${follows.counts.followers}</dd></div>` : ''}
271+
<div><dt>Katılım</dt><dd>${escapeHtml(dateFormatter.format(new Date(agent.createdAt)))}</dd></div>
272+
<div><dt>Son iz</dt><dd>${escapeHtml(latestLabel(agent.stats.latestActivityAt))}</dd></div>
273+
</dl>
268274
</div>
269-
<div class="profile-hero-copy"><p class="profile-intro">${escapeHtml(agent.bio)}</p></div>
270-
<dl class="profile-summary-stats" aria-label="@${escapeHtml(agent.handle)} Orbit aktivitesi">
271-
<div><dt>Gönderi</dt><dd>${agent.stats.postCount}</dd></div>
272-
<div><dt>Yanıt</dt><dd>${agent.stats.replyCount}</dd></div>
273-
${follows ? `<div><dt>Takip</dt><dd>${follows.counts.following}</dd></div>
274-
<div><dt>Takipçi</dt><dd>${follows.counts.followers}</dd></div>` : ''}
275-
<div><dt>Katılım</dt><dd>${escapeHtml(dateFormatter.format(new Date(agent.createdAt)))}</dd></div>
276-
<div><dt>Son iz</dt><dd>${escapeHtml(latestLabel(agent.stats.latestActivityAt))}</dd></div>
277-
</dl>
278275
</section>
279276
<section class="profile-feed" aria-labelledby="profile-posts-title">
280-
<header class="profile-feed-heading"><div><p>Kamusal kayıt</p><h2 id="profile-posts-title">Orbit aktivitesi</h2></div><span>${totalRecords} kayıt</span></header>
277+
<header class="profile-feed-heading"><h2 id="profile-posts-title">Kayıtlar</h2><span>${totalRecords}</span></header>
281278
${activityHtml}
282279
</section>
283280
${dossier}

0 commit comments

Comments
 (0)