fix: 릴리즈 하단 버튼을 한 줄에 세우고, 로그인을 화면으로 만든다
릴리즈 편집 맨 아래에서 저장과 공개가 서로 다른 높이에 서 있었다. `.studio-secondary-button` 는 목록 행에서 쓰려고 전역으로 `margin-top: 24px` 를 갖는데 하단 푸터에서 primary 만 그 여백을 풀어 주고 있었다 — 그래서 저장은 위에, 공개와 공개에서 내리기는 24px 아래에 앉았다. 로그인은 화면이 없었다. `ui-page` 위에 제목 한 줄과 버튼 하나가 전부라, 보호된 주소를 열었을 때 페이지처럼 보이지 않았다. 카드 하나로 세우고 eyebrow·제목·설명·행동을 갖춘 뒤, 로그인하면 어디로 돌아오는지까지 적는다 — 그것이 이 화면이 답해야 할 질문이다. 오류 화면과 다른 모양이어야 한다. 같은 모양이면 작성자는 무언가 고장 난 줄로 읽는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
This commit is contained in:
co-authored by
Claude Opus 5
parent
f9d20e8946
commit
6b9dc3f0a2
@@ -100,6 +100,102 @@ a {
|
|||||||
same reserved height: with only `main` covered the footer still sat at the
|
same reserved height: with only `main` covered the footer still sat at the
|
||||||
viewport bottom during loading and then dropped out of view when the content
|
viewport bottom during loading and then dropped out of view when the content
|
||||||
arrived, which is the whole 0.192 the page scored. */
|
arrived, which is the whole 0.192 the page scored. */
|
||||||
|
/*
|
||||||
|
로그인 관문.
|
||||||
|
|
||||||
|
이 화면은 공개 셸과 Studio 셸 양쪽에서 나온다 — 보호된 주소를 열면 그 주소가 속한 레이아웃 안에
|
||||||
|
이 자리가 대신 그려진다. 그래서 두 곳 모두에 있는 토큰만 쓴다.
|
||||||
|
|
||||||
|
카드로 세우는 이유는 이것이 "화면을 못 그렸다"가 아니라 "여기서 한 걸음 더 필요하다"이기
|
||||||
|
때문이다. 오류 화면과 같은 모양이면 작성자는 무언가 고장 난 줄로 읽는다.
|
||||||
|
*/
|
||||||
|
.auth-gate {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding-block: clamp(64px, 12vh, 140px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-gate .auth-gate__card {
|
||||||
|
width: min(100%, 34rem);
|
||||||
|
padding: clamp(32px, 5vw, 52px);
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--paper);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-gate .page-header {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-gate .page-header__eyebrow {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
color: var(--signal);
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-gate .page-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(27px, 3.2vw, 33px);
|
||||||
|
font-weight: 660;
|
||||||
|
letter-spacing: -0.035em;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-gate .page-header__description {
|
||||||
|
margin: 16px 0 0;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-gate .auth-gate__actions {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 28px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 이 버튼은 화면에서 유일한 행동이다. 폭을 채워 어디를 눌러야 하는지 묻지 않게 한다. */
|
||||||
|
.auth-gate .auth-gate__actions button {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 48px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
padding-inline: 20px;
|
||||||
|
border: 1px solid var(--signal);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--signal);
|
||||||
|
color: #fff;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 650;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-gate .auth-gate__actions button:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-gate .auth-gate__return {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--faint);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-gate .ui-terminal-error {
|
||||||
|
margin: 20px 0 0;
|
||||||
|
color: var(--warning);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.site-frame > main,
|
.site-frame > main,
|
||||||
.site-frame > .ui-page {
|
.site-frame > .ui-page {
|
||||||
/* Takes the slack so the footer stays put whether the route rendered a long
|
/* Takes the slack so the footer stays put whether the route rendered a long
|
||||||
|
|||||||
@@ -127,8 +127,14 @@
|
|||||||
.studio-app .studio-type-list strong { font-size: 18px; }
|
.studio-app .studio-type-list strong { font-size: 18px; }
|
||||||
.studio-app .studio-type-list span,
|
.studio-app .studio-type-list span,
|
||||||
.studio-app .studio-type-list small { color: var(--muted); line-height: 1.6; }
|
.studio-app .studio-type-list small { color: var(--muted); line-height: 1.6; }
|
||||||
.studio-app .studio-create-footer { display: flex; align-items: center; gap: 20px; margin-top: 28px; }
|
.studio-app .studio-create-footer { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; margin-top: 28px; }
|
||||||
.studio-app .studio-create-footer .studio-primary-button { margin: 0; }
|
/*
|
||||||
|
`.studio-secondary-button` 는 목록 행에서 쓰려고 전역으로 `margin-top: 24px` 를 갖는다. 여기서
|
||||||
|
풀어 주지 않으면 primary(저장)만 위로 붙고 secondary(공개·공개에서 내리기)는 24px 아래로
|
||||||
|
내려앉아, 같은 줄의 버튼들이 서로 다른 높이에 선다.
|
||||||
|
*/
|
||||||
|
.studio-app .studio-create-footer .studio-primary-button,
|
||||||
|
.studio-app .studio-create-footer .studio-secondary-button { margin: 0; }
|
||||||
.studio-app .studio-create-footer p { margin: 0; color: var(--muted); font-size: 13px; }
|
.studio-app .studio-create-footer p { margin: 0; color: var(--muted); font-size: 13px; }
|
||||||
.studio-app .studio-primary-button:disabled { opacity: 0.55; cursor: wait; }
|
.studio-app .studio-primary-button:disabled { opacity: 0.55; cursor: wait; }
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ const PLATFORM_KO_MESSAGES = {
|
|||||||
"route.auth.integration.description":
|
"route.auth.integration.description":
|
||||||
"외부 인증 소유자가 연결되면 이 보호 라우트를 사용할 수 있습니다.",
|
"외부 인증 소유자가 연결되면 이 보호 라우트를 사용할 수 있습니다.",
|
||||||
"route.auth.recovering.title": "세션을 복구하고 있습니다.",
|
"route.auth.recovering.title": "세션을 복구하고 있습니다.",
|
||||||
|
"route.auth.eyebrow": "SIGN IN",
|
||||||
|
"route.auth.returnTo": "로그인하면 {path} 로 돌아옵니다.",
|
||||||
"route.auth.required.title": "Studio에 로그인해 주세요.",
|
"route.auth.required.title": "Studio에 로그인해 주세요.",
|
||||||
"route.auth.recovering.description":
|
"route.auth.recovering.description":
|
||||||
"이전 로그인이 아직 살아 있는지 확인하고 있습니다. 잠시 뒤에도 이 화면이면 다시 시도해 주세요.",
|
"이전 로그인이 아직 살아 있는지 확인하고 있습니다. 잠시 뒤에도 이 화면이면 다시 시도해 주세요.",
|
||||||
@@ -274,6 +276,8 @@ const PLATFORM_EN_MESSAGES = {
|
|||||||
"route.auth.integration.description":
|
"route.auth.integration.description":
|
||||||
"This protected route is available after an external authentication owner is connected.",
|
"This protected route is available after an external authentication owner is connected.",
|
||||||
"route.auth.recovering.title": "Recovering the session.",
|
"route.auth.recovering.title": "Recovering the session.",
|
||||||
|
"route.auth.eyebrow": "SIGN IN",
|
||||||
|
"route.auth.returnTo": "You return to {path} after signing in.",
|
||||||
"route.auth.required.title": "Sign in to Studio.",
|
"route.auth.required.title": "Sign in to Studio.",
|
||||||
"route.auth.recovering.description":
|
"route.auth.recovering.description":
|
||||||
"Checking whether the previous sign-in is still valid. Try again if this screen stays.",
|
"Checking whether the previous sign-in is still valid. Try again if this screen stays.",
|
||||||
|
|||||||
@@ -241,35 +241,48 @@ function ProtectedRoute({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const recovering = decision.action === "wait-for-session";
|
const recovering = decision.action === "wait-for-session";
|
||||||
|
/*
|
||||||
|
Signing in is a screen, not a bare gate. This used to be a title and a button
|
||||||
|
dropped on `ui-page`, so opening a protected address showed no page at all —
|
||||||
|
just two stray elements. It is one card now, and it names the address the
|
||||||
|
visitor came for so they know where signing in returns them.
|
||||||
|
*/
|
||||||
return (
|
return (
|
||||||
<section className="ui-page" data-surface="authentication-required">
|
<section className="ui-page auth-gate" data-surface="authentication-required">
|
||||||
<PageHeader
|
<div className="auth-gate__card">
|
||||||
title={
|
<PageHeader
|
||||||
recovering
|
eyebrow={message("route.auth.eyebrow")}
|
||||||
? message("route.auth.recovering.title")
|
title={
|
||||||
: message("route.auth.required.title")
|
recovering
|
||||||
}
|
? message("route.auth.recovering.title")
|
||||||
description={
|
: message("route.auth.required.title")
|
||||||
recovering
|
}
|
||||||
? message("route.auth.recovering.description")
|
description={
|
||||||
: message("route.auth.required.description")
|
recovering
|
||||||
}
|
? message("route.auth.recovering.description")
|
||||||
/>
|
: message("route.auth.required.description")
|
||||||
<Button
|
}
|
||||||
disabled={pending}
|
/>
|
||||||
onClick={() => void continueSession()}
|
<div className="auth-gate__actions">
|
||||||
>
|
<Button disabled={pending} onClick={() => void continueSession()}>
|
||||||
{pending
|
{pending
|
||||||
? message("common.processing")
|
? message("common.processing")
|
||||||
: recovering
|
: recovering
|
||||||
? message("action.recoverSession")
|
? message("action.recoverSession")
|
||||||
: message("action.signIn")}
|
: message("action.signIn")}
|
||||||
</Button>
|
</Button>
|
||||||
{failed ? (
|
<p className="auth-gate__return">
|
||||||
<p className="ui-terminal-error" role="alert">
|
{message("route.auth.returnTo", {
|
||||||
{message("shell.session.actionFailed")}
|
path: `${location.pathname}${location.search}`,
|
||||||
</p>
|
})}
|
||||||
) : null}
|
</p>
|
||||||
|
</div>
|
||||||
|
{failed ? (
|
||||||
|
<p className="ui-terminal-error" role="alert">
|
||||||
|
{message("shell.session.actionFailed")}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user