feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
|
|
@ -5,13 +5,15 @@
|
|||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="D3RO Voice - Fully local AI voice assistant. Whisper + Ollama powered, zero cloud, 100% private." />
|
||||
<meta name="theme-color" content="#f25b29" />
|
||||
<meta name="theme-color" content="#3b82f6" />
|
||||
<title>D3RO Voice — Local AI Voice Assistant</title>
|
||||
<!-- Pretendard Variable (한글+라틴 통일 서체) & JetBrains Mono (텔레메트리 전용) -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-surface-950 text-white antialiased">
|
||||
<body class="bg-surface-950 text-neutral-100 font-sans antialiased selection:bg-brand-blue/30 selection:text-white">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
|
|
|||
4
site/package-lock.json
generated
4
site/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "d3ro-voice-site",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "d3ro-voice-site",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "d3ro-voice-site",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --port 5199 --host",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview --port 5199 --host"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
|
|
|
|||
12
site/public/.well-known/assetlinks.json
Normal file
12
site/public/.well-known/assetlinks.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.d3ro.voice",
|
||||
"sha256_cert_fingerprints": [
|
||||
"01:00:19:21:DB:4F:33:40:85:CE:21:E4:B8:DE:CC:BD:71:DA:87:67:C5:6E:3B:59:83:2A:A1:C8:29:EA:0D:AB"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
345
site/public/accept-invite.css
Normal file
345
site/public/accept-invite.css
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
:root {
|
||||
--ink: #08090c;
|
||||
--panel: #11141c;
|
||||
--panel-raised: #171b25;
|
||||
--line: #303647;
|
||||
--line-soft: rgba(255, 255, 255, 0.07);
|
||||
--text: #f4f4f5;
|
||||
--muted: #a1a1aa;
|
||||
--faint: #71717a;
|
||||
--accent: #3b82f6;
|
||||
--accent-hot: #60a5fa;
|
||||
--success: #4ade80;
|
||||
--danger: #fb7185;
|
||||
font-family: "Pretendard Variable", Pretendard, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--ink);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
linear-gradient(var(--line-soft) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--line-soft) 1px, transparent 1px),
|
||||
radial-gradient(circle at 72% 18%, rgba(59, 130, 246, 0.13), transparent 34rem),
|
||||
var(--ink);
|
||||
background-size: 40px 40px, 40px 40px, auto, auto;
|
||||
}
|
||||
|
||||
button,
|
||||
a {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 32px 18px;
|
||||
}
|
||||
|
||||
.invite-panel {
|
||||
width: min(100%, 720px);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(145deg, rgba(23, 27, 37, 0.96), rgba(12, 14, 20, 0.98));
|
||||
box-shadow: 0 28px 90px rgba(0, 0, 0, 0.48), inset 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.brand-row,
|
||||
.copy-block,
|
||||
.token-card,
|
||||
.actions,
|
||||
.support-row,
|
||||
.error {
|
||||
margin-inline: clamp(22px, 7vw, 64px);
|
||||
}
|
||||
|
||||
.brand-row {
|
||||
min-height: 76px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
}
|
||||
|
||||
.brand,
|
||||
.protocol,
|
||||
.eyebrow,
|
||||
.token-card dt,
|
||||
.token-card dd {
|
||||
font-family: "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
|
||||
}
|
||||
|
||||
.brand {
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.16em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.protocol {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.signal {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 12px rgba(74, 222, 128, 0.72);
|
||||
}
|
||||
|
||||
.route-line {
|
||||
height: 54px;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 1fr auto;
|
||||
align-items: center;
|
||||
padding-inline: clamp(22px, 7vw, 64px);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
}
|
||||
|
||||
.route-node {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 50%;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.route-node--active {
|
||||
border-color: var(--accent-hot);
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 18px rgba(59, 130, 246, 0.72);
|
||||
}
|
||||
|
||||
.route-track {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, var(--accent), var(--line));
|
||||
}
|
||||
|
||||
.copy-block {
|
||||
padding-top: clamp(42px, 8vw, 72px);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 16px;
|
||||
color: var(--accent-hot);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 600px;
|
||||
margin: 0;
|
||||
font-size: clamp(34px, 7.2vw, 60px);
|
||||
font-weight: 790;
|
||||
letter-spacing: -0.045em;
|
||||
line-height: 1.08;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.description {
|
||||
max-width: 590px;
|
||||
margin: 26px 0 0;
|
||||
color: #d4d4d8;
|
||||
font-size: 16px;
|
||||
line-height: 1.72;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.description--en {
|
||||
margin-top: 8px;
|
||||
color: var(--faint);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.token-card {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1px;
|
||||
margin-top: 38px;
|
||||
padding: 1px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: var(--line);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.token-card div {
|
||||
min-width: 0;
|
||||
padding: 18px;
|
||||
background: var(--panel-raised);
|
||||
}
|
||||
|
||||
.token-card dt {
|
||||
color: var(--faint);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.13em;
|
||||
}
|
||||
|
||||
.token-card dd {
|
||||
margin: 8px 0 0;
|
||||
overflow: hidden;
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.token-card dd[data-state="valid"] {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.token-card dd[data-state="invalid"] {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 18px;
|
||||
padding: 13px 14px;
|
||||
border: 1px solid rgba(251, 113, 133, 0.35);
|
||||
border-radius: 8px;
|
||||
color: #fecdd3;
|
||||
background: rgba(251, 113, 133, 0.08);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1.25fr 1fr;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.button {
|
||||
min-height: 52px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 0 18px;
|
||||
font-weight: 750;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: border-color 160ms ease, background 160ms ease, color 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
.button--primary {
|
||||
color: #fff;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 10px 30px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.button--primary:hover {
|
||||
background: var(--accent-hot);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.button--secondary {
|
||||
color: var(--text);
|
||||
border-color: var(--line);
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
}
|
||||
|
||||
.button--secondary:hover {
|
||||
border-color: #535f7f;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.button[aria-disabled="true"],
|
||||
.button:disabled {
|
||||
opacity: 0.42;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.button:focus-visible,
|
||||
.brand:focus-visible,
|
||||
.support-row a:focus-visible {
|
||||
outline: 3px solid rgba(255, 115, 66, 0.78);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.support-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-top: 38px;
|
||||
padding-block: 22px 28px;
|
||||
border-top: 1px solid var(--line-soft);
|
||||
color: var(--faint);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.support-row a {
|
||||
color: #d4d4d8;
|
||||
font-weight: 650;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.shell {
|
||||
align-items: start;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.invite-panel {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.brand-row {
|
||||
min-height: 66px;
|
||||
}
|
||||
|
||||
.protocol {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.token-card,
|
||||
.actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.support-row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
76
site/public/accept-invite.html
Normal file
76
site/public/accept-invite.html
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<meta name="theme-color" content="#08090c" />
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; base-uri 'none'; connect-src 'none'; font-src 'self'; form-action 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; style-src 'self'"
|
||||
/>
|
||||
<title>D3RO Voice — 팀 초대 열기</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="stylesheet" href="/accept-invite.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<section class="invite-panel" aria-labelledby="invite-title">
|
||||
<header class="brand-row">
|
||||
<a class="brand" href="/" aria-label="D3RO Voice 홈">D3RO VOICE</a>
|
||||
<span class="protocol"><span class="signal" aria-hidden="true"></span>SECURE INVITE</span>
|
||||
</header>
|
||||
|
||||
<div class="route-line" aria-hidden="true">
|
||||
<span class="route-node route-node--active"></span>
|
||||
<span class="route-track"></span>
|
||||
<span class="route-node"></span>
|
||||
<span class="route-track"></span>
|
||||
<span class="route-node"></span>
|
||||
</div>
|
||||
|
||||
<div class="copy-block">
|
||||
<p class="eyebrow">TEAM ACCESS / MOBILE HANDOFF</p>
|
||||
<h1 id="invite-title">D3RO Voice 앱에서<br />팀 초대를 확인해.</h1>
|
||||
<p class="description">
|
||||
초대 수락은 로그인한 계정과 서버 권한을 확인한 뒤에만 완료돼. 이 페이지는 초대 토큰을
|
||||
저장하거나 수락 결과를 만들지 않아.
|
||||
</p>
|
||||
<p class="description description--en" lang="en">
|
||||
Acceptance is completed only after the app verifies your signed-in account and server permissions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<dl class="token-card" aria-label="초대 링크 상태">
|
||||
<div>
|
||||
<dt>LINK STATUS</dt>
|
||||
<dd id="invite-status" aria-live="polite">검증 중</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>TOKEN FINGERPRINT</dt>
|
||||
<dd id="token-fingerprint">—</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<p id="invite-error" class="error" role="alert" hidden></p>
|
||||
|
||||
<div class="actions">
|
||||
<a id="open-app" class="button button--primary" href="#" aria-disabled="true">
|
||||
D3RO Voice 앱 열기
|
||||
</a>
|
||||
<button id="copy-link" class="button button--secondary" type="button" disabled>
|
||||
초대 링크 복사
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<footer class="support-row">
|
||||
<span>앱이 설치되지 않았어?</span>
|
||||
<a href="/download.html">안전한 설치 파일 받기</a>
|
||||
</footer>
|
||||
</section>
|
||||
</main>
|
||||
<script src="/accept-invite.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
37
site/public/accept-invite.js
Normal file
37
site/public/accept-invite.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
(() => {
|
||||
'use strict'
|
||||
|
||||
const tokenPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
const status = document.getElementById('invite-status')
|
||||
const fingerprint = document.getElementById('token-fingerprint')
|
||||
const error = document.getElementById('invite-error')
|
||||
const openApp = document.getElementById('open-app')
|
||||
const copyLink = document.getElementById('copy-link')
|
||||
const token = new URLSearchParams(window.location.search).get('token')?.trim() ?? ''
|
||||
|
||||
if (!tokenPattern.test(token)) {
|
||||
status.textContent = '사용할 수 없는 링크'
|
||||
status.dataset.state = 'invalid'
|
||||
error.textContent = '초대 링크가 없거나 형식이 올바르지 않아. 새 초대 링크를 요청해.'
|
||||
error.hidden = false
|
||||
return
|
||||
}
|
||||
|
||||
status.textContent = '형식 검증 완료 · 서버 확인 대기'
|
||||
status.dataset.state = 'valid'
|
||||
fingerprint.textContent = `${token.slice(0, 8)}…${token.slice(-4)}`
|
||||
openApp.href = `d3ro-voice://accept-invite?token=${encodeURIComponent(token)}`
|
||||
openApp.removeAttribute('aria-disabled')
|
||||
copyLink.disabled = false
|
||||
|
||||
copyLink.addEventListener('click', async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(window.location.href)
|
||||
copyLink.textContent = '복사했어'
|
||||
window.setTimeout(() => { copyLink.textContent = '초대 링크 복사' }, 1800)
|
||||
} catch {
|
||||
error.textContent = '브라우저가 복사를 허용하지 않았어. 주소 표시줄에서 링크를 직접 복사해.'
|
||||
error.hidden = false
|
||||
}
|
||||
})
|
||||
})()
|
||||
76
site/public/accept-invite/index.html
Normal file
76
site/public/accept-invite/index.html
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<meta name="theme-color" content="#08090c" />
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; base-uri 'none'; connect-src 'none'; font-src 'self'; form-action 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; style-src 'self'"
|
||||
/>
|
||||
<title>D3RO Voice — 팀 초대 열기</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="stylesheet" href="/accept-invite.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<section class="invite-panel" aria-labelledby="invite-title">
|
||||
<header class="brand-row">
|
||||
<a class="brand" href="/" aria-label="D3RO Voice 홈">D3RO VOICE</a>
|
||||
<span class="protocol"><span class="signal" aria-hidden="true"></span>SECURE INVITE</span>
|
||||
</header>
|
||||
|
||||
<div class="route-line" aria-hidden="true">
|
||||
<span class="route-node route-node--active"></span>
|
||||
<span class="route-track"></span>
|
||||
<span class="route-node"></span>
|
||||
<span class="route-track"></span>
|
||||
<span class="route-node"></span>
|
||||
</div>
|
||||
|
||||
<div class="copy-block">
|
||||
<p class="eyebrow">TEAM ACCESS / MOBILE HANDOFF</p>
|
||||
<h1 id="invite-title">D3RO Voice 앱에서<br />팀 초대를 확인해.</h1>
|
||||
<p class="description">
|
||||
초대 수락은 로그인한 계정과 서버 권한을 확인한 뒤에만 완료돼. 이 페이지는 초대 토큰을
|
||||
저장하거나 수락 결과를 만들지 않아.
|
||||
</p>
|
||||
<p class="description description--en" lang="en">
|
||||
Acceptance is completed only after the app verifies your signed-in account and server permissions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<dl class="token-card" aria-label="초대 링크 상태">
|
||||
<div>
|
||||
<dt>LINK STATUS</dt>
|
||||
<dd id="invite-status" aria-live="polite">검증 중</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>TOKEN FINGERPRINT</dt>
|
||||
<dd id="token-fingerprint">—</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<p id="invite-error" class="error" role="alert" hidden></p>
|
||||
|
||||
<div class="actions">
|
||||
<a id="open-app" class="button button--primary" href="#" aria-disabled="true">
|
||||
D3RO Voice 앱 열기
|
||||
</a>
|
||||
<button id="copy-link" class="button button--secondary" type="button" disabled>
|
||||
초대 링크 복사
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<footer class="support-row">
|
||||
<span>앱이 설치되지 않았어?</span>
|
||||
<a href="/download.html">안전한 설치 파일 받기</a>
|
||||
</footer>
|
||||
</section>
|
||||
</main>
|
||||
<script src="/accept-invite.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
44
site/public/delete-account/index.html
Normal file
44
site/public/delete-account/index.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="description" content="D3RO Voice 계정 및 데이터 삭제 요청">
|
||||
<title>D3RO Voice 계정 삭제</title>
|
||||
<link rel="stylesheet" href="/legal.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<a class="brand" href="/"><span class="brand-mark"></span>D3RO VOICE</a>
|
||||
<main>
|
||||
<p class="eyebrow">Account deletion</p>
|
||||
<h1>계정 및 데이터 삭제</h1>
|
||||
<p class="lead">D3RO Voice 계정과 연결된 데이터를 앱 안에서 즉시 삭제하거나, 앱에 접근할 수 없는 경우 외부 요청을 제출할 수 있다.</p>
|
||||
|
||||
<h2>앱에서 삭제</h2>
|
||||
<ol>
|
||||
<li>D3RO Voice에 로그인한다.</li>
|
||||
<li><strong>설정 → 계정 → 계정 삭제</strong>를 연다.</li>
|
||||
<li>안내를 확인하고 삭제를 승인한다.</li>
|
||||
</ol>
|
||||
<div class="notice"><strong>활성 유료 구독이 있다면 먼저 Google Play의 결제 및 정기 결제에서 구독을 취소해야 한다.</strong> 앱 계정 삭제만으로 스토어 구독이 자동 취소되지는 않는다.</div>
|
||||
|
||||
<h2>앱 없이 삭제 요청</h2>
|
||||
<p>아래 버튼은 YUN CHAN의 D3RO Voice 개인정보 담당자에게 이메일 요청을 연다. 계정에 사용한 이메일 주소를 발신 주소로 사용하고, 본문에 계정 이메일을 적어야 한다. 계정 소유 확인을 마친 뒤 삭제를 처리한다.</p>
|
||||
<div class="actions">
|
||||
<a class="button" href="mailto:yunchan8804@gmail.com?subject=D3RO%20Voice%20account%20deletion%20request&body=Account%20email%3A%0A%0AI%20request%20permanent%20deletion%20of%20my%20D3RO%20Voice%20account%20and%20associated%20data.">외부 삭제 요청 보내기</a>
|
||||
</div>
|
||||
|
||||
<h2>삭제되는 데이터</h2>
|
||||
<p>인증 계정, 프로필, 전사 기록, 회의, 메모, 템플릿, 생성 문서, 팀 소속, 업로드 파일, 기기 및 푸시 등록 등 계정에 연결된 서비스 데이터를 삭제한다.</p>
|
||||
|
||||
<h2>제한적으로 보관될 수 있는 데이터</h2>
|
||||
<p>법적 의무, 결제 분쟁, 보안 또는 부정 사용 방지를 위해 필요한 최소한의 결제·감사 기록은 관련 목적과 의무가 끝날 때까지 제한적으로 보관될 수 있다. 해당 기록은 서비스 개인화나 광고에 사용하지 않는다.</p>
|
||||
|
||||
<h2>삭제 후</h2>
|
||||
<p>삭제는 되돌릴 수 없다. 같은 이메일로 새 계정을 만들더라도 삭제된 콘텐츠는 복구되지 않는다. 기기에 남은 캐시와 녹음 대기열은 앱의 개인정보 정리 절차에서 제거된다.</p>
|
||||
</main>
|
||||
<footer><span>© 2026 YUN CHAN · D3RO Voice</span><nav><a href="/privacy/">개인정보처리방침</a><a href="/terms/">이용약관</a><a href="/delete-account/">계정 삭제</a></nav></footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,489 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ko" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>D3RO Voice — Official Download Center & Release History</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Geist:wght@300;400;500;600;700;800;900&family=Pretendard:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
d3ro: {
|
||||
void: '#05070d',
|
||||
base: '#090d19',
|
||||
card: '#0f172a',
|
||||
cardHover: '#141e36',
|
||||
border: '#1e293b',
|
||||
hairline: 'rgba(255, 255, 255, 0.08)',
|
||||
accent: '#38bdf8',
|
||||
accentHover: '#0ea5e9',
|
||||
accentMuted: 'rgba(56, 189, 248, 0.12)',
|
||||
green: '#22c55e',
|
||||
amber: '#f59e0b',
|
||||
text: {
|
||||
bright: '#ffffff',
|
||||
primary: '#f1f5f9',
|
||||
secondary: '#94a3b8',
|
||||
dim: '#64748b'
|
||||
}
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Geist', 'Pretendard', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
|
||||
mono: ['Fira Code', 'monospace']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
background-color: #05070d;
|
||||
color: #f1f5f9;
|
||||
font-family: 'Geist', 'Pretendard', sans-serif;
|
||||
background-image:
|
||||
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(56, 189, 248, 0.12), transparent 70%),
|
||||
radial-gradient(circle at 100% 100%, rgba(15, 23, 42, 0.8), transparent 40%);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
.glass-surface {
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
.glass-surface-interactive {
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.glass-surface-interactive:hover {
|
||||
border-color: rgba(56, 189, 248, 0.35);
|
||||
background: rgba(20, 30, 54, 0.85);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.7), 0 0 30px -10px rgba(56, 189, 248, 0.2);
|
||||
}
|
||||
.pill-glow {
|
||||
box-shadow: 0 0 20px -3px rgba(56, 189, 248, 0.4);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen flex flex-col antialiased selection:bg-sky-500/30 selection:text-sky-200">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="sticky top-0 z-50 glass-surface border-b border-white/5 px-6 py-3.5 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3.5">
|
||||
<div class="w-8 h-8 rounded-lg bg-gradient-to-tr from-sky-500 to-blue-600 flex items-center justify-center font-black text-white text-xs shadow-md shadow-sky-500/30">
|
||||
D3
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-extrabold tracking-tight text-sm text-white">D3RO VOICE</span>
|
||||
<span class="px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-sky-500/10 text-sky-400 border border-sky-500/25">v1.0.0 STABLE</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="hidden md:flex items-center gap-6 text-xs font-medium text-slate-400">
|
||||
<a href="/" class="hover:text-white transition-colors">Overview</a>
|
||||
<a href="#changelog" class="hover:text-white transition-colors">Changelog</a>
|
||||
<a href="#integrity" class="hover:text-white transition-colors">SHA-256 Verifier</a>
|
||||
<a href="https://git.chanpaca.net/yunchan/d3ro-voice" target="_blank" class="flex items-center gap-1 text-sky-400 hover:text-sky-300 font-mono">
|
||||
<i data-lucide="git-branch" class="w-3.5 h-3.5"></i> Forgejo Git
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="http://admin.chanpaca.net:3001" target="_blank" class="px-3.5 py-1.5 rounded-lg glass-surface hover:border-sky-500/40 text-sky-300 text-xs font-mono font-semibold flex items-center gap-1.5 transition-all">
|
||||
<i data-lucide="layout-dashboard" class="w-3.5 h-3.5"></i> Admin CRM
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Hero & Primary OS Download Section -->
|
||||
<main class="flex-1 max-w-6xl mx-auto w-full px-6 pt-12 pb-24">
|
||||
|
||||
<!-- Hero Header (Strict 2-line headline, <20 words subtext, no clutter) -->
|
||||
<div class="text-center max-w-2xl mx-auto mb-10">
|
||||
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-sky-500/10 border border-sky-500/25 text-sky-400 text-xs font-mono font-semibold mb-4">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-sky-400 animate-pulse"></span>
|
||||
OFFICIAL RELEASE • ZERO-LATENCY LOCAL WHISPER
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl sm:text-4xl md:text-5xl font-black tracking-tight text-white mb-3">
|
||||
Download <span class="bg-gradient-to-r from-sky-400 to-blue-500 bg-clip-text text-transparent">D3RO Voice</span> for Desktop
|
||||
</h1>
|
||||
<p class="text-sm sm:text-base text-slate-400 leading-relaxed">
|
||||
100% on-device local transcription, intelligent meeting minutes, and multi-network ad rewards.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Primary Hero Download Box (Auto-detected OS card) -->
|
||||
<div class="max-w-xl mx-auto glass-surface p-6 sm:p-8 rounded-2xl border border-sky-500/30 relative overflow-hidden mb-16 shadow-2xl shadow-sky-950/40">
|
||||
<div class="absolute -right-16 -top-16 w-48 h-48 bg-sky-500/10 rounded-full blur-3xl pointer-events-none"></div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4 mb-6">
|
||||
<div class="flex items-center gap-3.5">
|
||||
<div id="osIcon" class="w-12 h-12 rounded-xl bg-sky-500/15 border border-sky-500/30 flex items-center justify-center text-sky-400">
|
||||
<i data-lucide="monitor" class="w-6 h-6"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div id="detectedOsTitle" class="font-bold text-base sm:text-lg text-white">Windows 64-bit Installer</div>
|
||||
<div id="detectedOsMeta" class="text-xs text-slate-400 font-mono">D3RO-Voice-Setup-1.0.0-x64.exe • 102 MB • NSIS</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="px-2.5 py-1 rounded-md text-[10px] font-mono font-bold bg-emerald-500/15 text-emerald-400 border border-emerald-500/30 uppercase tracking-wider">
|
||||
Recommended
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Single Primary Action Button (No Wrap, High Contrast WCAG AA) -->
|
||||
<a id="primaryDownloadBtn" href="/releases/1.0.0/D3RO-Voice-Setup-1.0.0-x64.exe" download class="w-full py-3.5 px-6 rounded-xl bg-sky-400 hover:bg-sky-300 text-slate-950 font-bold text-sm flex items-center justify-center gap-2 transition-all transform active:scale-[0.98] shadow-lg shadow-sky-500/20">
|
||||
<i data-lucide="download" class="w-4 h-4"></i>
|
||||
<span id="downloadBtnText">Download for Windows (v1.0.0)</span>
|
||||
</a>
|
||||
|
||||
<!-- Quick Verify Strip -->
|
||||
<div class="mt-5 pt-4 border-t border-white/5 flex items-center justify-between text-xs text-slate-400">
|
||||
<div class="flex items-center gap-1.5 font-mono truncate max-w-[320px]">
|
||||
<span>SHA256:</span>
|
||||
<span class="text-slate-300 truncate">b0ac051443151a2e34e8...</span>
|
||||
<button onclick="copyHash('b0ac051443151a2e34e8192f1fcf795586bbafca6eb21f01a79170c079a53ba2')" class="text-sky-400 hover:text-sky-300 underline font-sans text-[11px] ml-1">Copy</button>
|
||||
</div>
|
||||
<span class="text-emerald-400 text-[11px] font-mono flex items-center gap-1">
|
||||
<i data-lucide="shield-check" class="w-3.5 h-3.5"></i> Signed & Verified
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Platform Packages Grid (Bento Structure with Asymmetry) -->
|
||||
<div class="mb-20">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-white">All Platform Releases</h2>
|
||||
<p class="text-xs text-slate-400">Optimized standalone packages for desktop operating systems.</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs font-mono text-slate-400">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-400"></span> Channel: latest.yml Active
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
|
||||
<!-- Windows Card -->
|
||||
<div class="glass-surface-interactive p-6 rounded-2xl flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-9 h-9 rounded-lg bg-sky-500/10 border border-sky-500/20 flex items-center justify-center text-sky-400">
|
||||
<i data-lucide="layout-grid" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<span class="text-[11px] font-mono text-slate-400">Windows 10 / 11 x64</span>
|
||||
</div>
|
||||
|
||||
<h3 class="font-bold text-base text-white mb-1">Windows</h3>
|
||||
<p class="text-xs text-slate-400 mb-5 leading-relaxed">
|
||||
NSIS one-click installer with background delta updates and DirectML GPU acceleration.
|
||||
</p>
|
||||
|
||||
<div class="space-y-2 mb-6">
|
||||
<a href="/releases/1.0.0/D3RO-Voice-Setup-1.0.0-x64.exe" class="flex items-center justify-between p-2.5 rounded-lg bg-white/[0.03] hover:bg-sky-500/10 border border-white/5 hover:border-sky-500/30 transition-all text-xs text-white">
|
||||
<div class="flex items-center gap-2 font-medium">
|
||||
<i data-lucide="download" class="w-3.5 h-3.5 text-sky-400"></i>
|
||||
<span>Setup Installer (.exe)</span>
|
||||
</div>
|
||||
<span class="font-mono text-slate-400 text-[11px]">102 MB</span>
|
||||
</a>
|
||||
<a href="/releases/1.0.0/D3RO-Voice-Setup-1.0.0-x64.exe.blockmap" class="flex items-center justify-between p-2 rounded-lg bg-white/[0.02] hover:bg-white/[0.04] border border-white/5 text-[11px] text-slate-400">
|
||||
<span class="font-mono">Blockmap (.blockmap)</span>
|
||||
<span class="text-[10px]">105 KB</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-3 border-t border-white/5 text-[11px] font-mono text-slate-400 flex justify-between">
|
||||
<span>Min: 4GB RAM / 1GB Disk</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- macOS Card -->
|
||||
<div class="glass-surface-interactive p-6 rounded-2xl flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-9 h-9 rounded-lg bg-purple-500/10 border border-purple-500/20 flex items-center justify-center text-purple-400">
|
||||
<i data-lucide="apple" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<span class="text-[11px] font-mono text-slate-400">macOS 12.0+</span>
|
||||
</div>
|
||||
|
||||
<h3 class="font-bold text-base text-white mb-1">macOS (Apple Silicon & Intel)</h3>
|
||||
<p class="text-xs text-slate-400 mb-5 leading-relaxed">
|
||||
Metal accelerated build for Apple Silicon M1/M2/M3/M4 and universal Intel DMG.
|
||||
</p>
|
||||
|
||||
<div class="space-y-2 mb-6">
|
||||
<a href="/releases/1.0.0/D3RO-Voice-1.0.0-arm64.dmg" class="flex items-center justify-between p-2.5 rounded-lg bg-white/[0.03] hover:bg-purple-500/10 border border-white/5 hover:border-purple-500/30 transition-all text-xs text-white">
|
||||
<div class="flex items-center gap-2 font-medium">
|
||||
<i data-lucide="download" class="w-3.5 h-3.5 text-purple-400"></i>
|
||||
<span>Apple Silicon DMG (.dmg)</span>
|
||||
</div>
|
||||
<span class="font-mono text-slate-400 text-[11px]">98 MB</span>
|
||||
</a>
|
||||
<a href="/releases/1.0.0/D3RO-Voice-1.0.0-arm64-mac.zip" class="flex items-center justify-between p-2 rounded-lg bg-white/[0.02] hover:bg-white/[0.04] border border-white/5 text-[11px] text-slate-400">
|
||||
<span class="font-mono">Portable Zip (.zip)</span>
|
||||
<span class="text-[10px]">96 MB</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-3 border-t border-white/5 text-[11px] font-mono text-slate-400 flex justify-between">
|
||||
<span>Metal Acceleration Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NAS & Server Card -->
|
||||
<div class="glass-surface-interactive p-6 rounded-2xl flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-9 h-9 rounded-lg bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400">
|
||||
<i data-lucide="server" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<span class="text-[11px] font-mono text-slate-400">Docker / Synology NAS</span>
|
||||
</div>
|
||||
|
||||
<h3 class="font-bold text-base text-white mb-1">Synology NAS & Docker</h3>
|
||||
<p class="text-xs text-slate-400 mb-5 leading-relaxed">
|
||||
Self-hosted private deployment package for NAS Container Manager and CRM services.
|
||||
</p>
|
||||
|
||||
<div class="space-y-2 mb-6">
|
||||
<a href="https://git.chanpaca.net/yunchan/d3ro-voice/src/branch/main/docker-compose.nas.yml" target="_blank" class="flex items-center justify-between p-2.5 rounded-lg bg-white/[0.03] hover:bg-emerald-500/10 border border-white/5 hover:border-emerald-500/30 transition-all text-xs text-white">
|
||||
<div class="flex items-center gap-2 font-medium">
|
||||
<i data-lucide="file-code" class="w-3.5 h-3.5 text-emerald-400"></i>
|
||||
<span>docker-compose.nas.yml</span>
|
||||
</div>
|
||||
<span class="font-mono text-emerald-400 text-[11px]">Source →</span>
|
||||
</a>
|
||||
<a href="https://git.chanpaca.net/yunchan/d3ro-voice/src/branch/main/docs/deployment/nas-deployment-guide.md" target="_blank" class="flex items-center justify-between p-2 rounded-lg bg-white/[0.02] hover:bg-white/[0.04] border border-white/5 text-[11px] text-slate-400">
|
||||
<span>NAS Deployment Manual</span>
|
||||
<span class="text-[10px] text-slate-400">Guide →</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-3 border-t border-white/5 text-[11px] font-mono text-slate-400 flex justify-between">
|
||||
<span>DSM 7.2+ Container Manager</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client-Side SHA-256 Verifier (Security & Integrity) -->
|
||||
<div id="integrity" class="glass-surface p-6 sm:p-8 rounded-2xl border border-sky-500/20 mb-20">
|
||||
<div class="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 text-sky-400 text-xs font-mono font-bold mb-1">
|
||||
<i data-lucide="hash" class="w-4 h-4"></i>
|
||||
CLIENT-SIDE CRYPTOGRAPHIC VERIFICATION
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-white">SHA-256 Binary Integrity Verifier</h3>
|
||||
<p class="text-xs text-slate-400">Drag & drop your downloaded installer file to compute and compare hash client-side.</p>
|
||||
</div>
|
||||
|
||||
<button onclick="document.getElementById('fileVerifierInput').click()" class="px-3.5 py-2 rounded-lg bg-sky-500/10 hover:bg-sky-500/20 text-sky-300 border border-sky-500/30 text-xs font-semibold flex items-center gap-2 transition-all">
|
||||
<i data-lucide="file-check" class="w-4 h-4"></i> Select File to Verify
|
||||
</button>
|
||||
<input type="file" id="fileVerifierInput" class="hidden" onchange="handleFileVerify(event)">
|
||||
</div>
|
||||
|
||||
<div id="dropZone" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" ondrop="handleFileDrop(event)" class="border-2 border-dashed border-slate-700/80 rounded-xl p-6 sm:p-8 text-center transition-all bg-black/20">
|
||||
<div id="verifyIdleState">
|
||||
<i data-lucide="upload-cloud" class="w-8 h-8 text-slate-500 mx-auto mb-2"></i>
|
||||
<p class="text-xs font-medium text-slate-300 mb-1">Drop installer (.exe / .dmg / .zip) here</p>
|
||||
<p class="text-[11px] text-slate-500 font-mono">Calculated locally in browser via WebCrypto API (no upload)</p>
|
||||
</div>
|
||||
|
||||
<div id="verifyResultState" class="hidden text-left space-y-3 font-mono text-xs">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-300 font-bold" id="verifyFileName">D3RO-Voice-Setup-1.0.0-x64.exe</span>
|
||||
<span id="verifyBadge" class="px-2.5 py-0.5 rounded text-[11px] font-bold"></span>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg bg-black/40 border border-white/5 space-y-1.5">
|
||||
<div class="text-slate-400">Computed Hash: <span class="text-sky-300" id="calculatedHash">-</span></div>
|
||||
<div class="text-slate-400">Official Hash: <span class="text-emerald-400" id="officialHash">b0ac051443151a2e34e8192f1fcf795586bbafca6eb21f01a79170c079a53ba2</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Release Changelog Timeline -->
|
||||
<div id="changelog" class="mb-16">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-white">Release Changelog & History</h2>
|
||||
<p class="text-xs text-slate-400">Complete version archives and feature milestones.</p>
|
||||
</div>
|
||||
<a href="https://git.chanpaca.net/yunchan/d3ro-voice/releases" target="_blank" class="text-xs text-sky-400 hover:text-sky-300 font-mono underline flex items-center gap-1">
|
||||
Full Git Tags Archive →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- v1.0.0 Item -->
|
||||
<div class="glass-surface p-6 rounded-2xl border-l-4 border-l-sky-400">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-lg font-bold text-white font-mono">v1.0.0</span>
|
||||
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-mono font-bold bg-sky-500/20 text-sky-300 border border-sky-500/30">
|
||||
LATEST STABLE
|
||||
</span>
|
||||
<span class="text-xs text-slate-500 font-mono">2026-08-20</span>
|
||||
</div>
|
||||
<a href="/releases/1.0.0/D3RO-Voice-Setup-1.0.0-x64.exe" class="px-3 py-1.5 rounded-lg bg-sky-500/10 hover:bg-sky-500/20 text-sky-300 border border-sky-500/30 text-xs font-semibold flex items-center gap-1.5 transition-all">
|
||||
<i data-lucide="download" class="w-3.5 h-3.5"></i> Installer (.exe)
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5 text-xs text-slate-300 leading-relaxed mb-4">
|
||||
<p class="font-semibold text-white">Highlights & Features:</p>
|
||||
<ul class="list-disc list-inside space-y-1 ml-2 text-slate-400">
|
||||
<li><strong class="text-sky-300">10+ Ad Mediation System</strong>: Parallel header bidding auction (EthicalAds, Carbon, GAM, Playwire, AppLovin, Unity).</li>
|
||||
<li><strong class="text-sky-300">Free Tier Rewarded Token Refill</strong>: 15s commercial playback grants +50 Cloud AI tokens.</li>
|
||||
<li><strong class="text-sky-300">Forgejo CI/CD & Synology NAS Packaging</strong>: Multi-platform automated packaging and Docker CRM.</li>
|
||||
<li><strong class="text-sky-300">100% Local Whisper Engine</strong>: Zero-latency offline speech transcription with hardware acceleration.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="p-2.5 rounded-lg bg-black/30 font-mono text-[11px] text-slate-400 flex items-center justify-between">
|
||||
<span class="truncate">SHA-256: b0ac051443151a2e34e8192f1fcf795586bbafca6eb21f01a79170c079a53ba2</span>
|
||||
<span class="text-sky-400">102 MB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- v0.2.1-alpha Item -->
|
||||
<div class="glass-surface p-5 rounded-xl border-l-4 border-l-slate-700 opacity-80">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="text-base font-bold text-white font-mono">v0.2.1-alpha</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono bg-white/5 text-slate-400 border border-white/10">PRE-RELEASE</span>
|
||||
<span class="text-xs text-slate-500 font-mono">2026-08-15</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400">
|
||||
Multi-provider cloud STT drivers dispatch (Deepgram, AssemblyAI, Groq) and cryptographic license verification engine.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="glass-surface border-t border-white/5 py-6 px-6 text-xs text-slate-500">
|
||||
<div class="max-w-6xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||
<div>
|
||||
<span class="font-bold text-white">D3RO Voice</span> • Copyright © 2026 D3RO. All rights reserved.
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/privacy" class="hover:text-white transition-colors">Privacy Policy</a>
|
||||
<a href="/terms" class="hover:text-white transition-colors">Terms</a>
|
||||
<a href="https://git.chanpaca.net" target="_blank" class="text-sky-400 hover:text-sky-300 font-mono">git.chanpaca.net</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
|
||||
const OFFICIAL_HASH = 'b0ac051443151a2e34e8192f1fcf795586bbafca6eb21f01a79170c079a53ba2';
|
||||
|
||||
// Auto OS Detection
|
||||
(function detectOS() {
|
||||
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||
const titleEl = document.getElementById('detectedOsTitle');
|
||||
const metaEl = document.getElementById('detectedOsMeta');
|
||||
const btnEl = document.getElementById('primaryDownloadBtn');
|
||||
const btnTextEl = document.getElementById('downloadBtnText');
|
||||
const iconEl = document.getElementById('osIcon');
|
||||
|
||||
if (userAgent.includes('mac') || userAgent.includes('darwin')) {
|
||||
titleEl.textContent = 'macOS Apple Silicon Installer';
|
||||
metaEl.textContent = 'D3RO-Voice-1.0.0-arm64.dmg • 98 MB • Apple Silicon (M1/M2/M3/M4)';
|
||||
btnEl.href = '/releases/1.0.0/D3RO-Voice-1.0.0-arm64.dmg';
|
||||
btnTextEl.textContent = 'Download for macOS (v1.0.0)';
|
||||
iconEl.innerHTML = '<i data-lucide="apple" class="w-6 h-6"></i>';
|
||||
} else if (userAgent.includes('linux')) {
|
||||
titleEl.textContent = 'Linux Universal Package';
|
||||
metaEl.textContent = 'D3RO-Voice-1.0.0.AppImage • 105 MB • AppImage';
|
||||
btnEl.href = '/releases/1.0.0/D3RO-Voice-1.0.0.AppImage';
|
||||
btnTextEl.textContent = 'Download for Linux (v1.0.0)';
|
||||
}
|
||||
lucide.createIcons();
|
||||
})();
|
||||
|
||||
function copyHash(hash) {
|
||||
navigator.clipboard.writeText(hash).then(() => {
|
||||
alert('SHA-256 Hash copied to clipboard:\n' + hash);
|
||||
});
|
||||
}
|
||||
|
||||
function handleDragOver(e) {
|
||||
e.preventDefault();
|
||||
document.getElementById('dropZone').classList.add('border-sky-400', 'bg-sky-500/10');
|
||||
}
|
||||
|
||||
function handleDragLeave(e) {
|
||||
e.preventDefault();
|
||||
document.getElementById('dropZone').classList.remove('border-sky-400', 'bg-sky-500/10');
|
||||
}
|
||||
|
||||
function handleFileDrop(e) {
|
||||
e.preventDefault();
|
||||
document.getElementById('dropZone').classList.remove('border-sky-400', 'bg-sky-500/10');
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) calculateFileHash(files[0]);
|
||||
}
|
||||
|
||||
function handleFileVerify(e) {
|
||||
const files = e.target.files;
|
||||
if (files.length > 0) calculateFileHash(files[0]);
|
||||
}
|
||||
|
||||
async function calculateFileHash(file) {
|
||||
document.getElementById('verifyIdleState').classList.add('hidden');
|
||||
document.getElementById('verifyResultState').classList.remove('hidden');
|
||||
document.getElementById('verifyFileName').textContent = file.name + ' (' + (file.size / (1024*1024)).toFixed(1) + ' MB)';
|
||||
document.getElementById('calculatedHash').textContent = 'Computing SHA-256 hash locally...';
|
||||
|
||||
const badge = document.getElementById('verifyBadge');
|
||||
badge.textContent = 'Hashing...';
|
||||
badge.className = 'px-2.5 py-0.5 rounded text-[11px] font-bold bg-amber-500/20 text-amber-300 border border-amber-500/30';
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
document.getElementById('calculatedHash').textContent = hashHex;
|
||||
|
||||
if (hashHex === OFFICIAL_HASH) {
|
||||
badge.textContent = '✓ OFFICIAL MATCH (GENUINE)';
|
||||
badge.className = 'px-2.5 py-0.5 rounded text-[11px] font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30';
|
||||
} else {
|
||||
badge.textContent = '✓ LOCAL HASH COMPUTED';
|
||||
badge.className = 'px-2.5 py-0.5 rounded text-[11px] font-bold bg-sky-500/20 text-sky-300 border border-sky-500/30';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta http-equiv="refresh" content="0; url=/#download" />
|
||||
<link rel="canonical" href="https://d3ro.chanpaca.net/#download" />
|
||||
<title>D3RO Voice 릴리스 준비</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>D3RO Voice 릴리스 준비</h1>
|
||||
<p>설치 파일은 서명과 업데이트 경로 검증이 끝난 뒤 공식 페이지에서 제공합니다.</p>
|
||||
<p><a href="/#download">공식 릴리스 준비 페이지로 이동</a></p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="14" fill="#19191b"/>
|
||||
<circle cx="32" cy="28" r="8" fill="none" stroke="#f25b29" stroke-width="3"/>
|
||||
<rect x="29" y="36" width="6" height="10" rx="3" fill="#f25b29"/>
|
||||
<path d="M20 32 Q20 44 32 44 Q44 44 44 32" fill="none" stroke="#f25b29" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<line x1="32" y1="44" x2="32" y2="52" stroke="#f25b29" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<line x1="26" y1="52" x2="38" y2="52" stroke="#f25b29" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<circle cx="32" cy="28" r="8" fill="none" stroke="#3b82f6" stroke-width="3"/>
|
||||
<rect x="29" y="36" width="6" height="10" rx="3" fill="#3b82f6"/>
|
||||
<path d="M20 32 Q20 44 32 44 Q44 44 44 32" fill="none" stroke="#3b82f6" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<line x1="32" y1="44" x2="32" y2="52" stroke="#3b82f6" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<line x1="26" y1="52" x2="38" y2="52" stroke="#3b82f6" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 592 B After Width: | Height: | Size: 592 B |
111
site/public/legal.css
Normal file
111
site/public/legal.css
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
:root {
|
||||
color-scheme: dark;
|
||||
font-family: Pretendard, "Noto Sans KR", system-ui, -apple-system, sans-serif;
|
||||
background: #090b0f;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(circle at 80% 0%, rgba(59, 130, 246, 0.11), transparent 34rem),
|
||||
#090b0f;
|
||||
}
|
||||
|
||||
a { color: #ff8a62; }
|
||||
a:hover { color: #ffad91; }
|
||||
|
||||
.shell {
|
||||
width: min(920px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 32px 0 72px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
color: #f8fafc;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: .08em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 3px;
|
||||
background: #3b82f6;
|
||||
box-shadow: 0 0 18px rgba(59, 130, 246, .5);
|
||||
}
|
||||
|
||||
main {
|
||||
margin-top: 28px;
|
||||
padding: clamp(24px, 5vw, 52px);
|
||||
border: 1px solid rgba(255,255,255,.09);
|
||||
border-radius: 24px;
|
||||
background: rgba(20, 23, 30, .92);
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,.35);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 12px;
|
||||
color: #ff8a62;
|
||||
font: 700 11px/1.4 "JetBrains Mono", monospace;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 { margin: 0; font-size: clamp(32px, 6vw, 54px); line-height: 1.05; }
|
||||
h2 { margin: 40px 0 12px; font-size: 21px; }
|
||||
h3 { margin: 24px 0 8px; font-size: 16px; }
|
||||
p, li { color: #c7cbd4; font-size: 15px; line-height: 1.8; }
|
||||
ul, ol { padding-left: 22px; }
|
||||
|
||||
.lead { max-width: 720px; color: #e6e8ed; font-size: 17px; }
|
||||
.meta { color: #828895; font-size: 13px; }
|
||||
|
||||
.notice {
|
||||
margin: 24px 0;
|
||||
padding: 18px 20px;
|
||||
border-left: 3px solid #3b82f6;
|
||||
border-radius: 8px;
|
||||
background: rgba(59, 130, 246, .08);
|
||||
}
|
||||
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; }
|
||||
.button {
|
||||
display: inline-flex;
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 18px;
|
||||
border-radius: 12px;
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
.button:hover { background: #ff6b38; color: #fff; }
|
||||
.button.secondary { background: #282d37; color: #f3f4f6; }
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 18px;
|
||||
justify-content: space-between;
|
||||
padding: 22px 4px 0;
|
||||
color: #777e8b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
footer nav { display: flex; flex-wrap: wrap; gap: 14px; }
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.shell { width: min(100% - 20px, 920px); padding-top: 18px; }
|
||||
main { border-radius: 18px; }
|
||||
}
|
||||
58
site/public/privacy/index.html
Normal file
58
site/public/privacy/index.html
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="description" content="D3RO Voice 개인정보처리방침">
|
||||
<title>D3RO Voice 개인정보처리방침</title>
|
||||
<link rel="stylesheet" href="/legal.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<a class="brand" href="/"><span class="brand-mark"></span>D3RO VOICE</a>
|
||||
<main>
|
||||
<p class="eyebrow">Privacy policy</p>
|
||||
<h1>개인정보처리방침</h1>
|
||||
<p class="lead">YUN CHAN이 제공하는 D3RO Voice는 음성 기록, 전사, 문서화, 동기화 및 팀 협업 기능을 제공하기 위해 필요한 범위에서 데이터를 처리한다.</p>
|
||||
<p class="meta">시행일: 2026년 8월 21일 · 최종 수정: 2026년 8월 24일</p>
|
||||
|
||||
<h2>1. 처리하는 데이터</h2>
|
||||
<ul>
|
||||
<li><strong>계정:</strong> 이메일, 표시 이름, 인증 제공자 식별자, 계정 및 구독 상태</li>
|
||||
<li><strong>사용자 콘텐츠:</strong> 사용자가 선택하거나 녹음한 음성, 전사문, 메모, 회의, 템플릿, 생성 문서와 팀 데이터</li>
|
||||
<li><strong>기기 및 알림:</strong> 앱 버전, 기기 등록 정보, 푸시 토큰, 권한 및 오류 상태</li>
|
||||
<li><strong>결제 및 광고 검증:</strong> 스토어 상품·거래 식별자, 구독 상태, 보상형 광고 검증 식별자. 완전한 카드 번호는 D3RO Voice가 저장하지 않는다.</li>
|
||||
<li><strong>AI 응답 신고:</strong> 사용자가 신고 화면에서 확인하고 제출한 AI 응답 일부(최대 4,000자), 신고 사유, 선택 입력한 설명(최대 500자), 신고 대상 생성 영수증, 처리 상태와 검토·조치 기록</li>
|
||||
<li><strong>운영·보안:</strong> 사용량, 요청 결과, 감사 기록, 부정 사용 방지와 장애 진단에 필요한 제한된 로그</li>
|
||||
</ul>
|
||||
|
||||
<h2>2. 이용 목적</h2>
|
||||
<p>로그인과 계정 관리, 음성 전사·AI 처리, 기기 간 동기화, 팀 협업, 구매·구독 복원, Free 요금제 광고 및 보상 검증, 보안·오류 대응, 사용자 요청 이행에 이용한다. 사용자가 선택하지 않은 파일을 임의로 업로드하지 않는다.</p>
|
||||
|
||||
<h2>3. 외부 처리자와 전송</h2>
|
||||
<p>기능 제공을 위해 Supabase(인증·데이터·파일), Google Play/Firebase/FCM/AdMob(배포·결제·알림·광고), 사용자가 선택한 결제 처리자, 구성된 STT·AI 제공자가 데이터를 처리할 수 있다. 음성 또는 텍스트는 해당 클라우드 기능을 사용한 경우에만 필요한 범위에서 전송된다. 로컬 전사 기능을 선택하면 지원되는 처리는 기기 안에서 수행된다.</p>
|
||||
|
||||
<h2>4. 광고</h2>
|
||||
<p>Free 요금제에는 Google AdMob 광고가 표시될 수 있다. Google Mobile Ads SDK는 광고 기능이 작동할 때 IP 주소(IP 기반 대략적 위치 추정에 이용될 수 있음), 앱 실행·탭·동영상 조회 같은 앱 상호작용, 앱 및 SDK의 성능·오류·진단 정보, Android 광고 ID·앱 세트 ID 등 광고·기기·계정 식별자를 기본적으로 자동 수집하여 Google로 전송할 수 있다. 이 데이터는 광고 제공, 분석, 사기 및 부정 사용 방지 목적으로 처리된다. 광고 동의와 개인정보 옵션은 지원 지역의 요구사항에 따라 앱에서 제공한다. Pro 및 Pro+ 유효 entitlement에서는 앱이 광고 요청을 만들지 않는다.</p>
|
||||
|
||||
<h2>5. 보관과 삭제</h2>
|
||||
<p>계정 데이터는 서비스를 제공하는 동안 보관한다. 사용자가 계정을 삭제하면 인증 계정과 연결된 콘텐츠·기기 등록을 삭제한다. 다만 결제, 분쟁, 보안 및 법적 의무 이행에 필요한 최소 기록은 관련 의무가 끝날 때까지 제한적으로 보관할 수 있으며, 그 목적 외에는 사용하지 않는다.</p>
|
||||
<p>AI 생성 영수증은 신고할 수 있는 응답과 계정을 확인하기 위해 계정 식별자, 생성 유형, 모델, 발급·만료 시각만 저장하며 원래 프롬프트나 AI 응답 원문을 저장하지 않는다. 영수증은 발급 후 30일 동안 유효하며, 만료되면 권한이 제한된 서비스 전용 정리 절차의 삭제 대상이 된다. 신고 증거와 검토 기록은 상태가 보류 또는 검토 중인 동안 해결 시점까지 보관한다. 신고가 조치 완료 또는 기각으로 해결되면 180일의 이의 제기·부정 사용 대응 기간을 거친 뒤 같은 서비스 전용 정리 절차의 삭제 대상이 된다. 계정이 삭제되면 신고자 및 검토자 계정 식별자는 신고 기록에서 분리되며, 해결 전 신고 또는 앞서 설명한 제한 보관기간이 남은 신고 증거는 해당 목적을 위해 해당 계정 식별자와 분리된 상태로 보관될 수 있다.</p>
|
||||
|
||||
<h2>6. AI 응답 신고와 데이터 최소화</h2>
|
||||
<p>지원되는 AI 응답을 신고할 때 사용자는 신고 대상 응답 일부를 화면에서 미리 확인하고, 신고 사유와 선택 설명을 직접 제출한다. 앱은 신고 과정에서 음성, 전체 대화, 원래 프롬프트를 자동으로 첨부하거나 전송하지 않는다. 신고 데이터는 신고 접수, 정책·안전 검토, 조치, 이의 제기와 부정 사용 방지에만 이용하며, 일반 애플리케이션 로그에 신고 응답 원문을 복사하지 않는다.</p>
|
||||
|
||||
<h2>7. 보호 조치와 사용자 권리</h2>
|
||||
<p>전송 구간 암호화, 사용자별 접근 통제, 최소 권한, 민감 로컬 캐시 암호화, 보안 감사와 삭제 경계를 적용한다. 사용자는 앱에서 자신의 데이터를 조회·내보내기·수정·삭제할 수 있다.</p>
|
||||
|
||||
<h2>8. 계정 삭제와 문의</h2>
|
||||
<p>앱의 <strong>설정 → 계정 → 계정 삭제</strong>를 이용하거나, 앱을 사용할 수 없는 경우 외부 삭제 요청 페이지를 이용할 수 있다. 개인정보 문의는 YUN CHAN의 D3RO Voice 담당자에게 보낼 수 있다.</p>
|
||||
<div class="actions">
|
||||
<a class="button" href="/delete-account/">계정 삭제 요청</a>
|
||||
<a class="button secondary" href="mailto:yunchan8804@gmail.com?subject=D3RO%20Voice%20privacy%20inquiry">개인정보 문의</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer><span>© 2026 YUN CHAN · D3RO Voice</span><nav><a href="/privacy/">개인정보처리방침</a><a href="/terms/">이용약관</a><a href="/delete-account/">계정 삭제</a></nav></footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
68
site/public/terms/index.html
Normal file
68
site/public/terms/index.html
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="description" content="D3RO Voice 이용약관">
|
||||
<title>D3RO Voice 이용약관</title>
|
||||
<link rel="stylesheet" href="/legal.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<a class="brand" href="/"><span class="brand-mark"></span>D3RO VOICE</a>
|
||||
<main>
|
||||
<p class="eyebrow">Terms of service</p>
|
||||
<h1>이용약관</h1>
|
||||
<p class="lead">이 약관은 YUN CHAN이 제공하는 D3RO Voice 앱과 관련 서비스의 이용 조건을 정한다.</p>
|
||||
<p class="meta">시행일: 2026년 8월 21일 · 최종 수정: 2026년 8월 24일</p>
|
||||
|
||||
<h2>1. 서비스와 계정</h2>
|
||||
<p>D3RO Voice는 음성 녹음·가져오기, 전사, AI 처리, 문서화, 동기화, 팀 협업과 구독 기능을 제공한다. 사용자는 정확한 계정 정보를 유지하고 자신의 인증 수단과 기기를 보호해야 한다.</p>
|
||||
|
||||
<h2>2. 사용자 콘텐츠</h2>
|
||||
<p>사용자는 자신이 입력하거나 업로드한 콘텐츠에 대한 권리를 유지한다. 서비스를 제공하고 사용자가 요청한 처리를 수행하는 데 필요한 범위에서만 D3RO Voice가 콘텐츠를 처리하도록 허용한다. 타인의 권리나 법률을 침해하는 콘텐츠를 업로드해서는 안 된다.</p>
|
||||
|
||||
<h2>3. 전사와 AI 결과</h2>
|
||||
<p>자동 전사와 AI 생성 결과는 부정확하거나 불완전하거나 부적절할 수 있으며, 항상 사실 또는 YUN CHAN의 견해를 나타내는 것은 아니다. 사용자는 결과를 사용하거나 공유하기 전에 정확성, 적법성과 적합성을 직접 검토해야 한다. 의료·법률·재무 또는 안전에 중대한 결정을 위한 전문 조언이나 긴급 서비스의 대체 수단으로 사용해서는 안 된다. 지원되는 AI 응답에서 유해하거나 부적절한 결과를 발견하면 앱의 신고 기능을 이용할 수 있다.</p>
|
||||
|
||||
<h2>4. 요금제, 구매와 광고</h2>
|
||||
<ul>
|
||||
<li>Free 요금제에는 광고와 사용량 제한이 적용될 수 있다.</li>
|
||||
<li>유료 구독의 가격, 갱신, 취소와 환불은 구매 화면 및 Google Play 또는 해당 결제 처리자의 조건에 따른다.</li>
|
||||
<li>구독 취소와 계정 삭제는 별개다. 활성 구독이 있다면 스토어에서 먼저 취소해야 계정 삭제가 가능할 수 있다.</li>
|
||||
<li>광고 보상은 서버 검증이 완료된 경우에만 부여된다.</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. 허용되지 않는 사용</h2>
|
||||
<p>다음 콘텐츠를 생성·업로드·공유하거나 이를 목적으로 서비스를 이용해서는 안 된다.</p>
|
||||
<ul>
|
||||
<li>불법 행위 또는 범죄를 조장하거나 수행하기 위한 콘텐츠</li>
|
||||
<li>아동 성적 학대·착취, 미성년자의 성적 대상화 또는 이를 묘사·조장하는 콘텐츠</li>
|
||||
<li>비동의 성적 콘텐츠, 음란물 또는 성적 착취를 조장하는 콘텐츠</li>
|
||||
<li>보호되는 특성을 이유로 한 혐오·차별, 괴롭힘, 위협 또는 표적 학대를 조장하는 콘텐츠</li>
|
||||
<li>폭력, 테러, 자해 또는 타해를 조장하거나 구체적으로 돕는 콘텐츠</li>
|
||||
<li>사기, 스팸, 피싱, 악성코드 또는 기만적 행위를 위한 콘텐츠</li>
|
||||
<li>개인정보, 초상권, 저작권 등 타인의 권리 또는 비밀을 침해하는 콘텐츠</li>
|
||||
</ul>
|
||||
<p>타인 계정 침해, 서비스·결제·광고 검증 우회, 악성코드 배포, 과도한 자동 요청, 역설계로 보안 조치를 우회하는 행위도 금지한다.</p>
|
||||
|
||||
<h2>6. 신고, 검토와 조치</h2>
|
||||
<p>사용자는 지원되는 AI 응답 화면의 신고 기능으로 유해성, 성적 콘텐츠, 혐오, 폭력, 자해, 허위정보, 개인정보 침해, 스팸 또는 그 밖의 문제를 알릴 수 있다. 신고는 운영 검토 대상으로 접수되며, 확인 결과와 관련 법률·정책에 따라 신고를 기각하거나 콘텐츠·기능·계정 이용을 제한하는 등 필요한 조치를 할 수 있다. 선의로 제출한 신고에는 불이익을 주지 않지만, 타인을 괴롭히거나 보복하기 위한 신고, 고의로 허위인 신고 또는 반복적인 신고 기능 남용은 제한할 수 있다.</p>
|
||||
|
||||
<h2>7. 변경, 중단과 책임</h2>
|
||||
<p>보안, 법률, 제공자 정책 또는 서비스 개선을 위해 기능을 변경할 수 있다. 합리적인 범위에서 중요한 변경을 알린다. 외부 네트워크·스토어·AI 제공자의 장애로 일부 기능이 일시적으로 제한될 수 있으며, D3RO Voice는 이를 성공으로 가장하지 않고 명시적인 오류로 처리한다.</p>
|
||||
|
||||
<h2>8. 종료와 데이터</h2>
|
||||
<p>사용자는 언제든 구독을 취소하거나 계정을 삭제할 수 있다. 중대한 약관 위반이나 보안 위험이 있는 경우 관련 법률과 정책에 따라 서비스 이용을 제한할 수 있다. 데이터 처리와 삭제는 개인정보처리방침을 따른다.</p>
|
||||
|
||||
<h2>9. 준거와 문의</h2>
|
||||
<p>강행 법규가 달리 정하지 않는 한 대한민국 법률을 적용한다. 서비스 또는 약관 문의는 아래 연락 경로를 이용한다.</p>
|
||||
<div class="actions">
|
||||
<a class="button" href="mailto:yunchan8804@gmail.com?subject=D3RO%20Voice%20terms%20inquiry">문의하기</a>
|
||||
<a class="button secondary" href="/privacy/">개인정보처리방침</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer><span>© 2026 YUN CHAN · D3RO Voice</span><nav><a href="/privacy/">개인정보처리방침</a><a href="/terms/">이용약관</a><a href="/delete-account/">계정 삭제</a></nav></footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -4,23 +4,23 @@ import { Led } from './Led'
|
|||
interface BadgeProps {
|
||||
children: ReactNode
|
||||
led?: boolean
|
||||
ledColor?: 'amber' | 'green' | 'red'
|
||||
ledColor?: 'blue' | 'green' | 'red'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Badge({ children, led = false, ledColor = 'amber', className = '' }: BadgeProps) {
|
||||
export function Badge({ children, led = false, ledColor = 'blue', className = '' }: BadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={`
|
||||
inline-flex items-center gap-2 px-3 py-1
|
||||
font-mono text-nano uppercase tracking-widest
|
||||
text-neutral-400 bg-surface-700/50 border border-white/[0.06]
|
||||
rounded-sm select-none
|
||||
text-xs font-medium
|
||||
text-neutral-300 bg-surface-800/90 border border-white/10
|
||||
rounded-full shadow-sm select-none
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{led && <Led color={ledColor} size="sm" />}
|
||||
{children}
|
||||
<span>{children}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ interface DataPointProps {
|
|||
export function DataPoint({ label, value, unit, className = '' }: DataPointProps) {
|
||||
return (
|
||||
<div className={`flex flex-col ${className}`}>
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-neutral-500 mb-1">
|
||||
<span className="text-xs font-normal text-neutral-400 mb-1">
|
||||
{label}
|
||||
</span>
|
||||
<span className="font-display text-2xl md:text-3xl font-bold text-neutral-100 tracking-tight">
|
||||
<span className="text-2xl sm:text-3xl font-medium text-neutral-50 tracking-tight flex items-baseline">
|
||||
{value}
|
||||
{unit && (
|
||||
<span className="text-sm font-mono text-neutral-500 ml-1">{unit}</span>
|
||||
<span className="text-xs font-normal text-brand-blue ml-1.5">{unit}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ interface GlowButtonProps {
|
|||
href?: string
|
||||
download?: string
|
||||
variant?: 'primary' | 'secondary'
|
||||
size?: 'md' | 'lg'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export function GlowButton({
|
||||
|
|
@ -16,33 +17,34 @@ export function GlowButton({
|
|||
variant = 'primary',
|
||||
size = 'md',
|
||||
className = '',
|
||||
onClick,
|
||||
}: GlowButtonProps) {
|
||||
const base = 'glow-btn inline-flex items-center justify-center font-mono text-sm font-medium tracking-wide uppercase rounded-panel select-none'
|
||||
const base = 'glow-btn inline-flex items-center justify-center font-medium tracking-normal select-none rounded-xl transition-all duration-200 leading-normal'
|
||||
|
||||
const variants = {
|
||||
primary: 'bg-brand-amber text-white hover:bg-brand-amber-light',
|
||||
secondary: 'bg-surface-500 text-neutral-200 border border-white/[0.08] hover:border-brand-amber/30',
|
||||
primary: 'bg-brand-blue text-white hover:bg-brand-blue-light shadow-glow-sm hover:shadow-glow-md active:scale-[0.98]',
|
||||
secondary: 'bg-surface-700/80 text-neutral-100 border border-white/10 hover:border-brand-blue/40 hover:bg-surface-600 hover:text-white active:scale-[0.98]',
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
md: 'px-6 py-3 text-xs',
|
||||
lg: 'px-8 py-4 text-sm',
|
||||
sm: 'px-4.5 py-2 text-xs',
|
||||
md: 'px-6 py-2.5 sm:py-3 text-xs sm:text-sm font-medium',
|
||||
lg: 'px-8 sm:px-10 py-3.5 sm:py-4 text-sm sm:text-base font-medium',
|
||||
}
|
||||
|
||||
const cls = `${base} ${variants[variant]} ${sizes[size]} ${className}`
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<a href={href} download={download} className={cls}>
|
||||
<span className="relative z-10">{children}</span>
|
||||
<a href={href} download={download} onClick={onClick} className={cls}>
|
||||
<span className="relative z-10 flex items-center gap-2.5">{children}</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={cls}>
|
||||
<span className="relative z-10">{children}</span>
|
||||
<button type="button" onClick={onClick} className={cls}>
|
||||
<span className="relative z-10 flex items-center gap-2.5">{children}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,16 +5,15 @@ interface InstrumentCardProps {
|
|||
className?: string
|
||||
hover?: boolean
|
||||
}
|
||||
|
||||
export function InstrumentCard({ children, className = '', hover = true }: InstrumentCardProps) {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
relative noise-texture
|
||||
bg-surface-600 border border-white/[0.06]
|
||||
rounded-card shadow-chassis
|
||||
p-5 md:p-6
|
||||
${hover ? 'transition-all duration-250 hover:border-brand-amber/20 hover:shadow-glow-sm' : ''}
|
||||
bg-surface-800/80 border border-white/10
|
||||
rounded-2xl shadow-glass-card
|
||||
p-6 md:p-7 backdrop-blur-xl
|
||||
${hover ? 'transition-all duration-300 hover:border-brand-blue/35 hover:bg-surface-700/85 hover:shadow-glow-sm' : ''}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -21,32 +21,34 @@ export function LanguageSwitcher() {
|
|||
return (
|
||||
<div ref={ref} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1.5 font-mono text-nano uppercase tracking-widest text-neutral-400 hover:text-white border border-white/[0.06] rounded-panel transition-colors"
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-xs font-medium text-neutral-300 hover:text-white bg-surface-800/80 hover:bg-surface-700 border border-white/10 hover:border-brand-blue/40 rounded-lg transition-all shadow-sm"
|
||||
aria-label="Change language"
|
||||
>
|
||||
<GlobeIcon />
|
||||
{current?.label ?? 'EN'}
|
||||
<span>{current?.label ?? 'EN'}</span>
|
||||
<ChevronIcon open={open} />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute right-0 top-full mt-2 w-40 py-1 bg-surface-700 border border-white/[0.08] rounded-card shadow-lg z-50 max-h-80 overflow-y-auto">
|
||||
<div className="absolute right-0 top-full mt-2 w-44 py-1.5 bg-surface-800/95 backdrop-blur-2xl border border-white/10 rounded-xl shadow-2xl z-50 max-h-80 overflow-y-auto">
|
||||
{LOCALES.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setLocale(l.code)
|
||||
setOpen(false)
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 font-mono text-xs flex items-center justify-between transition-colors ${
|
||||
className={`w-full text-left px-3.5 py-2 text-xs flex items-center justify-between transition-colors ${
|
||||
l.code === locale
|
||||
? 'text-brand-amber bg-brand-amber/[0.08]'
|
||||
: 'text-neutral-400 hover:text-white hover:bg-surface-600'
|
||||
? 'text-brand-blue bg-brand-blue/10 font-medium'
|
||||
: 'text-neutral-300 hover:text-white hover:bg-surface-700/80 font-normal'
|
||||
}`}
|
||||
>
|
||||
<span>{l.nativeName}</span>
|
||||
<span className="text-nano text-neutral-600 uppercase">{l.label}</span>
|
||||
<span className="text-[11px] text-neutral-400 font-normal">{l.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -57,7 +59,7 @@ export function LanguageSwitcher() {
|
|||
|
||||
function GlobeIcon() {
|
||||
return (
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg className="w-3.5 h-3.5 text-brand-blue" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M2 12h20" />
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
||||
|
|
@ -68,7 +70,7 @@ function GlobeIcon() {
|
|||
function ChevronIcon({ open }: { open: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
className={`w-3 h-3 transition-transform ${open ? 'rotate-180' : ''}`}
|
||||
className={`w-3 h-3 text-neutral-400 transition-transform ${open ? 'rotate-180' : ''}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
interface LedProps {
|
||||
color?: 'amber' | 'green' | 'red'
|
||||
color?: 'blue' | 'green' | 'red'
|
||||
pulse?: boolean
|
||||
size?: 'sm' | 'md'
|
||||
className?: string
|
||||
}
|
||||
|
||||
const colorMap = {
|
||||
amber: {
|
||||
bg: 'bg-brand-amber',
|
||||
shadow: 'shadow-[0_0_6px_rgba(242,91,41,0.8)]',
|
||||
blue: {
|
||||
bg: 'bg-brand-blue',
|
||||
shadow: 'shadow-[0_0_6px_rgba(59,130,246,0.8)]',
|
||||
},
|
||||
green: {
|
||||
bg: 'bg-emerald-400',
|
||||
|
|
@ -25,7 +25,7 @@ const sizeMap = {
|
|||
md: 'w-2 h-2',
|
||||
} as const
|
||||
|
||||
export function Led({ color = 'amber', pulse = false, size = 'sm', className = '' }: LedProps) {
|
||||
export function Led({ color = 'blue', pulse = false, size = 'sm', className = '' }: LedProps) {
|
||||
const c = colorMap[color]
|
||||
const s = sizeMap[size]
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -8,21 +8,22 @@ interface SectionHeaderProps {
|
|||
export function SectionHeader({ index, title, subtitle, className = '' }: SectionHeaderProps) {
|
||||
return (
|
||||
<div className={`mb-12 md:mb-16 ${className}`}>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber">
|
||||
<div className="flex items-center gap-3 mb-3.5">
|
||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium text-brand-blue bg-brand-blue/10 border border-brand-blue/25 shadow-sm">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-brand-blue" />
|
||||
{index}
|
||||
</span>
|
||||
<div className="h-px flex-1 bg-white/[0.06]" />
|
||||
<div className="h-px flex-1 bg-gradient-to-r from-brand-blue/20 via-white/[0.08] to-transparent" />
|
||||
</div>
|
||||
<h2
|
||||
className="font-display text-display font-bold text-neutral-50 tracking-tight break-keep"
|
||||
className="text-2xl sm:text-3xl md:text-4xl font-medium text-neutral-50 tracking-tight leading-[1.22] break-keep"
|
||||
style={{ wordBreak: 'keep-all', overflowWrap: 'break-word' }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
{subtitle && (
|
||||
<p
|
||||
className="mt-3 max-w-xl text-base text-neutral-400 leading-relaxed break-keep"
|
||||
className="mt-3 max-w-2xl text-sm sm:text-base text-neutral-300 leading-relaxed font-normal break-keep"
|
||||
style={{ wordBreak: 'keep-all', overflowWrap: 'break-word' }}
|
||||
>
|
||||
{subtitle}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export function WaveBars({ className = '' }: WaveBarsProps) {
|
|||
<div
|
||||
key={i}
|
||||
ref={(el) => { barsRef.current[i] = el }}
|
||||
className="w-[3px] rounded-full bg-brand-amber/60"
|
||||
className="w-[3px] rounded-full bg-brand-blue/60"
|
||||
style={{ height: 4, transition: 'height 60ms ease-out' }}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@ export const OS_CONFIGS: Record<ClientOSType, ClientOSInfo> = {
|
|||
os: 'android',
|
||||
osName: 'Android',
|
||||
isMobile: true,
|
||||
downloadUrl: 'https://git.chanpaca.net/attachments/0b015367-dd8b-488c-8cc0-4db413b51792',
|
||||
downloadFilename: 'd3ro-voice-v1.0.0-signed.apk',
|
||||
badgeText: 'ANDROID APK v1.0.0',
|
||||
buttonLabelKo: 'Android용 무료 다운로드 (APK)',
|
||||
buttonLabelEn: 'Download for Android (APK)',
|
||||
subtextKo: 'v1.0.0 · 공식 서명 APK (47.5MB) · Google Play 호환',
|
||||
subtextEn: 'v1.0.0 · Official Signed APK (47.5MB) · Play Protected',
|
||||
downloadUrl: '#download',
|
||||
downloadFilename: '',
|
||||
badgeText: 'ANDROID RELEASE PENDING',
|
||||
buttonLabelKo: 'Android 공식 배포 준비 중',
|
||||
buttonLabelEn: 'Android release unavailable',
|
||||
subtextKo: '서명된 배포 증거가 확인될 때까지 다운로드를 제공하지 않습니다.',
|
||||
subtextEn: 'Download stays unavailable until signed release evidence is verified.',
|
||||
},
|
||||
ios: {
|
||||
os: 'ios',
|
||||
|
|
@ -34,23 +34,23 @@ export const OS_CONFIGS: Record<ClientOSType, ClientOSInfo> = {
|
|||
isMobile: true,
|
||||
downloadUrl: '#download',
|
||||
downloadFilename: '',
|
||||
badgeText: 'iOS TESTFLIGHT / PWA',
|
||||
buttonLabelKo: 'iOS 앱 / TestFlight 시작',
|
||||
buttonLabelEn: 'Download for iOS (TestFlight)',
|
||||
subtextKo: 'v1.0.0 · iPhone & iPad 지원 · PWA 즉시 실행',
|
||||
subtextEn: 'v1.0.0 · iPhone & iPad Supported · Instant PWA',
|
||||
badgeText: 'iOS DISTRIBUTION UNAVAILABLE',
|
||||
buttonLabelKo: 'iOS 배포 미지원',
|
||||
buttonLabelEn: 'iOS distribution unavailable',
|
||||
subtextKo: '현재 검증된 App Store 또는 TestFlight 배포가 없습니다.',
|
||||
subtextEn: 'No verified App Store or TestFlight distribution is available.',
|
||||
},
|
||||
windows: {
|
||||
os: 'windows',
|
||||
osName: 'Windows',
|
||||
isMobile: false,
|
||||
downloadUrl: 'https://git.chanpaca.net/attachments/c281a224-2538-496a-bf74-bc188938c734',
|
||||
downloadFilename: 'D3RO-Voice-Setup-1.0.0-x64.exe',
|
||||
badgeText: 'WINDOWS x64 v1.0.0',
|
||||
buttonLabelKo: 'Windows용 무료 다운로드',
|
||||
buttonLabelEn: 'Download for Windows (x64)',
|
||||
subtextKo: 'v1.0.0 · x64 공식 인스톨러 (102MB) · 온디바이스',
|
||||
subtextEn: 'v1.0.0 · x64 Official Setup (102MB) · On-Device',
|
||||
downloadUrl: '#download',
|
||||
downloadFilename: '',
|
||||
badgeText: 'DESKTOP 1.1.0 PREPARATION',
|
||||
buttonLabelKo: 'Windows 릴리스 준비 중',
|
||||
buttonLabelEn: 'Windows release in preparation',
|
||||
subtextKo: '서명과 설치·업데이트 검증이 끝난 뒤 공식 설치 파일을 제공합니다.',
|
||||
subtextEn: 'The installer will be published after signing, installation, and update verification.',
|
||||
},
|
||||
macos: {
|
||||
os: 'macos',
|
||||
|
|
@ -58,11 +58,11 @@ export const OS_CONFIGS: Record<ClientOSType, ClientOSInfo> = {
|
|||
isMobile: false,
|
||||
downloadUrl: '#download',
|
||||
downloadFilename: '',
|
||||
badgeText: 'MACOS UNIVERSAL',
|
||||
buttonLabelKo: 'macOS용 무료 다운로드',
|
||||
buttonLabelEn: 'Download for macOS (Universal)',
|
||||
subtextKo: 'v1.0.0 · Apple Silicon (M1/M2/M3/M4) & Intel 지원',
|
||||
subtextEn: 'v1.0.0 · Apple Silicon & Intel Universal',
|
||||
badgeText: 'MACOS APPLE SILICON PREPARATION',
|
||||
buttonLabelKo: 'macOS 릴리스 준비 중',
|
||||
buttonLabelEn: 'macOS release in preparation',
|
||||
subtextKo: 'Apple Silicon용 빌드는 서명과 설치 검증을 마친 뒤 제공합니다.',
|
||||
subtextEn: 'The Apple Silicon build will be published after signing and installation verification.',
|
||||
},
|
||||
linux: {
|
||||
os: 'linux',
|
||||
|
|
@ -70,11 +70,11 @@ export const OS_CONFIGS: Record<ClientOSType, ClientOSInfo> = {
|
|||
isMobile: false,
|
||||
downloadUrl: '#download',
|
||||
downloadFilename: '',
|
||||
badgeText: 'LINUX APPIMAGE / DEB',
|
||||
buttonLabelKo: 'Linux용 무료 다운로드',
|
||||
buttonLabelEn: 'Download for Linux (AppImage)',
|
||||
subtextKo: 'v1.0.0 · Ubuntu / Debian / Arch 지원',
|
||||
subtextEn: 'v1.0.0 · Ubuntu / Debian / Arch Supported',
|
||||
badgeText: 'LINUX PACKAGE UNAVAILABLE',
|
||||
buttonLabelKo: 'Linux 배포 미지원',
|
||||
buttonLabelEn: 'Linux package unavailable',
|
||||
subtextKo: '현재 검증된 AppImage 또는 DEB 패키지가 없습니다.',
|
||||
subtextEn: 'No verified AppImage or DEB package is available.',
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,60 +4,78 @@
|
|||
|
||||
/* ── Base Reset & Globals ──────────────────────────────── */
|
||||
@layer base {
|
||||
:root {
|
||||
--font-sans: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", sans-serif;
|
||||
--font-display: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
||||
--font-mono: "JetBrains Mono", "Fira Code", ui-monospace, monospace;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: rgba(242, 91, 41, 0.3);
|
||||
color: #fafafa;
|
||||
background-color: rgba(59, 130, 246, 0.35);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #0d0d0f;
|
||||
background: #08090c;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #333338;
|
||||
border-radius: 3px;
|
||||
background: #222838;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #08090c;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4a4b50;
|
||||
background: #363f57;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
background-color: #08090c;
|
||||
color: #f4f4f5;
|
||||
}
|
||||
|
||||
/* Accessibility Focus Rings */
|
||||
:focus-visible {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Grid Background Pattern ──────────────────────────── */
|
||||
.bg-grid {
|
||||
background-image:
|
||||
linear-gradient(rgba(242, 91, 41, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(242, 91, 41, 0.03) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
linear-gradient(rgba(59, 130, 246, 0.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(59, 130, 246, 0.035) 1px, transparent 1px);
|
||||
background-size: 64px 64px;
|
||||
}
|
||||
|
||||
.bg-grid-dense {
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
/* ── CRT Screen Effect (CSS only) ─────────────────────── */
|
||||
/* ── CRT Screen Effect (Crisp High-Contrast) ─────────── */
|
||||
.crt-screen {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #050605 0%, #0a0b0a 100%);
|
||||
border: 1px solid rgba(242, 91, 41, 0.1);
|
||||
background: linear-gradient(180deg, #05070a 0%, #0a0e14 100%);
|
||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||
box-shadow:
|
||||
inset 0 2px 8px rgba(0, 0, 0, 0.8),
|
||||
0 0 30px rgba(242, 91, 41, 0.05);
|
||||
inset 0 2px 10px rgba(0, 0, 0, 0.9),
|
||||
0 0 32px rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
.crt-screen::before {
|
||||
|
|
@ -68,8 +86,8 @@
|
|||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(0, 0, 0, 0.15) 2px,
|
||||
rgba(0, 0, 0, 0.15) 4px
|
||||
rgba(0, 0, 0, 0.2) 2px,
|
||||
rgba(0, 0, 0, 0.2) 4px
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
|
|
@ -81,9 +99,9 @@
|
|||
inset: 0;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(242, 91, 41, 0.02) 0%,
|
||||
rgba(59, 130, 246, 0.03) 0%,
|
||||
transparent 50%,
|
||||
rgba(242, 91, 41, 0.01) 100%
|
||||
rgba(59, 130, 246, 0.015) 100%
|
||||
);
|
||||
animation: scan 8s linear infinite;
|
||||
pointer-events: none;
|
||||
|
|
@ -98,7 +116,7 @@
|
|||
/* ── Crosshair Decorations ─────────────────────────────── */
|
||||
.crosshair-box {
|
||||
--ch-size: 16px;
|
||||
--ch-color: rgba(242, 91, 41, 0.35);
|
||||
--ch-color: rgba(59, 130, 246, 0.4);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
|
@ -109,8 +127,8 @@
|
|||
left: -1px;
|
||||
width: var(--ch-size);
|
||||
height: var(--ch-size);
|
||||
border-top: 1px solid var(--ch-color);
|
||||
border-left: 1px solid var(--ch-color);
|
||||
border-top: 1.5px solid var(--ch-color);
|
||||
border-left: 1.5px solid var(--ch-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
|
@ -121,8 +139,8 @@
|
|||
right: -1px;
|
||||
width: var(--ch-size);
|
||||
height: var(--ch-size);
|
||||
border-bottom: 1px solid var(--ch-color);
|
||||
border-right: 1px solid var(--ch-color);
|
||||
border-bottom: 1.5px solid var(--ch-color);
|
||||
border-right: 1.5px solid var(--ch-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +152,7 @@
|
|||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.025;
|
||||
opacity: 0.03;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
background-repeat: repeat;
|
||||
background-size: 256px 256px;
|
||||
|
|
@ -142,6 +160,24 @@
|
|||
border-radius: inherit;
|
||||
}
|
||||
|
||||
/* ── Glass Surfaces & Double Bezel ─────────────────────── */
|
||||
.glass-panel {
|
||||
background: rgba(19, 22, 31, 0.75);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 12px 32px -4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.glass-panel-hover {
|
||||
transition: all 250ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.glass-panel-hover:hover {
|
||||
border-color: rgba(59, 130, 246, 0.35);
|
||||
background: rgba(26, 30, 43, 0.85);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12), 0 0 20px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
/* ── Glow Button ───────────────────────────────────────── */
|
||||
.glow-btn {
|
||||
position: relative;
|
||||
|
|
@ -152,7 +188,7 @@
|
|||
content: '';
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
background: linear-gradient(135deg, rgba(242, 91, 41, 0.4), rgba(242, 91, 41, 0) 60%);
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.45), rgba(59, 130, 246, 0) 65%);
|
||||
border-radius: inherit;
|
||||
opacity: 0;
|
||||
transition: opacity 250ms;
|
||||
|
|
@ -162,46 +198,60 @@
|
|||
opacity: 1;
|
||||
}
|
||||
.glow-btn:hover {
|
||||
box-shadow: 0 0 20px rgba(242, 91, 41, 0.3), 0 0 40px rgba(242, 91, 41, 0.1);
|
||||
box-shadow: 0 0 24px rgba(59, 130, 246, 0.35), 0 0 48px rgba(59, 130, 246, 0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.glow-btn:active {
|
||||
transform: translateY(0);
|
||||
transform: translateY(0) scale(0.98);
|
||||
}
|
||||
|
||||
/* ── Accordion ─────────────────────────────────────────── */
|
||||
.accordion-content {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition: grid-template-rows 300ms ease;
|
||||
transition: grid-template-rows 250ms cubic-bezier(0.16, 1, 0.3, 1), opacity 200ms ease;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.accordion-content.open {
|
||||
grid-template-rows: 1fr;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
.accordion-content > div {
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── Reveal Animation ──────────────────────────────────── */
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transform: translateY(16px);
|
||||
}
|
||||
.reveal.visible {
|
||||
animation: fade-in-up 0.6s ease-out forwards;
|
||||
animation: fade-in-up 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
0% { opacity: 0; transform: translateY(20px); }
|
||||
0% { opacity: 0; transform: translateY(16px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ── Utilities ─────────────────────────────────────────── */
|
||||
@layer base {
|
||||
/* 의미적 강조는 세미볼드까지 — 볼드(700) 헤드라인 금지 (한글 과중 방지) */
|
||||
strong, b { font-weight: 600; }
|
||||
}
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
.break-keep {
|
||||
word-break: keep-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.border-hairline {
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,24 +10,24 @@ export function CTA() {
|
|||
const clientOS = useClientOS()
|
||||
|
||||
return (
|
||||
<section className="relative py-28 md:py-36 overflow-hidden">
|
||||
<section className="relative py-24 md:py-32 overflow-hidden">
|
||||
{/* Background radial glow */}
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[500px] bg-brand-amber/[0.045] rounded-full blur-[140px] pointer-events-none" />
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[700px] h-[550px] bg-brand-blue/[0.06] rounded-full blur-[160px] pointer-events-none" />
|
||||
|
||||
<Container className="relative text-center">
|
||||
<Crosshair className="inline-block mb-8">
|
||||
<WaveBars className="h-9 px-3" />
|
||||
<Crosshair className="inline-block mb-7">
|
||||
<WaveBars className="h-8 px-4" />
|
||||
</Crosshair>
|
||||
|
||||
<h2
|
||||
className="font-display text-display font-extrabold text-neutral-50 mb-6 max-w-3xl mx-auto tracking-tight break-keep"
|
||||
className="text-3xl sm:text-4xl md:text-5xl font-medium text-neutral-50 mb-5 max-w-3xl mx-auto tracking-tight leading-[1.2] break-keep"
|
||||
style={{ wordBreak: 'keep-all', overflowWrap: 'break-word' }}
|
||||
>
|
||||
{t.cta.title1} <span className="text-brand-amber">{t.cta.title2}</span>
|
||||
{t.cta.title1} <span className="text-brand-blue font-medium">{t.cta.title2}</span>
|
||||
</h2>
|
||||
|
||||
<p
|
||||
className="text-base md:text-lg text-neutral-400 max-w-xl mx-auto mb-8 leading-relaxed break-keep"
|
||||
className="text-base sm:text-lg text-neutral-300 max-w-xl mx-auto mb-7 leading-relaxed font-normal break-keep"
|
||||
style={{ wordBreak: 'keep-all', overflowWrap: 'break-word' }}
|
||||
>
|
||||
{t.cta.subtitle1}
|
||||
|
|
@ -36,40 +36,40 @@ export function CTA() {
|
|||
</p>
|
||||
|
||||
{/* Hotkey Hint Box */}
|
||||
<div className="inline-flex items-center gap-2 mb-10 px-4 py-2 rounded-xl bg-surface-800/80 border border-white/[0.08] shadow-inner">
|
||||
<span className="font-mono text-nano text-neutral-400 uppercase tracking-widest">
|
||||
Push-to-Talk:
|
||||
<div className="inline-flex items-center gap-3 mb-9 px-4.5 py-2.5 rounded-2xl bg-surface-800/90 border border-white/10 shadow-glass-card">
|
||||
<span className="text-xs text-neutral-400 font-normal">
|
||||
푸시투톡 단축키:
|
||||
</span>
|
||||
<div className="flex items-center gap-1 font-mono text-xs font-semibold text-neutral-200">
|
||||
<span className="px-2 py-0.5 rounded bg-surface-600 border border-white/[0.1]">Ctrl</span>
|
||||
<span className="text-neutral-500">+</span>
|
||||
<span className="px-2 py-0.5 rounded bg-surface-600 border border-white/[0.1]">Shift</span>
|
||||
<span className="text-neutral-500">+</span>
|
||||
<span className="px-2.5 py-0.5 rounded bg-brand-amber/20 border border-brand-amber/40 text-brand-amber">Space</span>
|
||||
<div className="flex items-center gap-1.5 font-mono text-xs font-medium text-neutral-200">
|
||||
<span className="px-2.5 py-1 rounded-md bg-surface-700 border border-white/10 shadow-sm">Ctrl</span>
|
||||
<span className="text-neutral-400">+</span>
|
||||
<span className="px-2.5 py-1 rounded-md bg-surface-700 border border-white/10 shadow-sm">Shift</span>
|
||||
<span className="text-neutral-400">+</span>
|
||||
<span className="px-3 py-1 rounded-md bg-brand-blue/20 border border-brand-blue/40 text-brand-blue shadow-sm">Space</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="flex flex-col items-center gap-3.5">
|
||||
<GlowButton
|
||||
href={clientOS.downloadUrl}
|
||||
download={clientOS.downloadFilename || undefined}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="shadow-glow-md hover:shadow-glow-lg"
|
||||
className="shadow-glow-md hover:shadow-glow-lg text-base sm:text-lg font-medium px-10 sm:px-12 py-4.5 rounded-2xl"
|
||||
>
|
||||
{locale === 'ko' ? clientOS.buttonLabelKo : clientOS.buttonLabelEn}
|
||||
</GlowButton>
|
||||
<div className="flex items-center gap-2 font-mono text-xs text-neutral-400">
|
||||
<div className="flex items-center gap-2 text-xs text-neutral-400 font-normal">
|
||||
<span>{locale === 'ko' ? clientOS.subtextKo : clientOS.subtextEn}</span>
|
||||
<span>·</span>
|
||||
<a href="#download" className="underline hover:text-brand-amber transition-colors">
|
||||
{locale === 'ko' ? '다른 OS 다운로드' : 'Other Platforms'}
|
||||
<a href="#download" className="underline text-brand-blue hover:text-brand-blue-light transition-colors font-medium">
|
||||
{locale === 'ko' ? '릴리스 준비 상태' : 'Release readiness'}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="font-mono text-nano text-neutral-500 mt-8 uppercase tracking-widest">
|
||||
{t.cta.systemReq} · 100% OPEN ARCHITECTURE
|
||||
<p className="text-xs text-neutral-400 mt-9 font-normal">
|
||||
{t.cta.systemReq} · 100% 오픈 아키텍처
|
||||
</p>
|
||||
</Container>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,42 +1,17 @@
|
|||
import { useState, useEffect, type ChangeEvent, type DragEvent } from 'react'
|
||||
import { useState, type ChangeEvent, type DragEvent } from 'react'
|
||||
import { Container } from '../components/Container'
|
||||
import { SectionHeader } from '../components/SectionHeader'
|
||||
import { Led } from '../components/Led'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
const OFFICIAL_HASH = 'b0ac051443151a2e34e8192f1fcf795586bbafca6eb21f01a79170c079a53ba2'
|
||||
|
||||
export function Download() {
|
||||
const { locale } = useI18n()
|
||||
const isKo = locale === 'ko'
|
||||
|
||||
const [detectedOs, setDetectedOs] = useState<'windows' | 'mac' | 'linux'>('windows')
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [verifyStatus, setVerifyStatus] = useState<'idle' | 'computing' | 'match' | 'custom'>('idle')
|
||||
const [verifyStatus, setVerifyStatus] = useState<'idle' | 'computing' | 'custom'>('idle')
|
||||
const [computedHash, setComputedHash] = useState('')
|
||||
const [fileName, setFileName] = useState('')
|
||||
const [isDragOver, setIsDragOver] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const ua = window.navigator.userAgent.toLowerCase()
|
||||
if (ua.includes('mac') || ua.includes('darwin')) {
|
||||
setDetectedOs('mac')
|
||||
} else if (ua.includes('linux')) {
|
||||
setDetectedOs('linux')
|
||||
} else {
|
||||
setDetectedOs('windows')
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const copyHash = (hash: string) => {
|
||||
navigator.clipboard.writeText(hash).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
}
|
||||
|
||||
const computeFileHash = async (file: File) => {
|
||||
setFileName(`${file.name} (${(file.size / (1024 * 1024)).toFixed(1)} MB)`)
|
||||
setVerifyStatus('computing')
|
||||
|
|
@ -48,11 +23,7 @@ export function Download() {
|
|||
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
|
||||
|
||||
setComputedHash(hashHex)
|
||||
if (hashHex.toLowerCase() === OFFICIAL_HASH.toLowerCase()) {
|
||||
setVerifyStatus('match')
|
||||
} else {
|
||||
setVerifyStatus('custom')
|
||||
}
|
||||
setVerifyStatus('custom')
|
||||
} catch {
|
||||
setVerifyStatus('custom')
|
||||
}
|
||||
|
|
@ -85,287 +56,238 @@ export function Download() {
|
|||
}
|
||||
|
||||
return (
|
||||
<section id="download" className="py-24 md:py-32 bg-surface-900/60 border-t border-white/[0.04] relative">
|
||||
<section id="download" className="py-20 md:py-28 bg-surface-900/60 border-t border-white/[0.04] relative">
|
||||
<Container>
|
||||
<SectionHeader
|
||||
index="06 / DOWNLOAD"
|
||||
title={isKo ? 'D3RO Voice 다운로드 및 배포 센터' : 'Download D3RO Voice Desktop'}
|
||||
title={isKo ? 'D3RO Voice 릴리스 준비 센터' : 'D3RO Voice Release Preparation'}
|
||||
subtitle={
|
||||
isKo
|
||||
? '100% 로컬 Whisper 전사 엔진과 AI 음성 비서를 데스크톱에 무료로 설치하세요. 클라우드 전송 없이 프라이버시가 완벽히 보장됩니다.'
|
||||
: 'Zero-latency on-device speech assistant with offline Whisper model. Instant installation for Windows, macOS, and private servers.'
|
||||
? '다음 안정 버전 1.1.0의 설치 파일, 서명, 업데이트 경로를 검증하고 있습니다. 검증이 끝나기 전에는 바이너리를 제공하지 않습니다.'
|
||||
: 'Version 1.1.0 installers, signatures, and update paths are being verified. No binary is offered before verification completes.'
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Primary Recommended OS Download Card */}
|
||||
<div className="max-w-3xl mx-auto mb-16 p-8 md:p-10 rounded-2xl bg-surface-800/90 border border-brand-amber/40 shadow-glow-md relative overflow-hidden backdrop-blur-md">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-brand-amber/10 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="max-w-3xl mx-auto mb-16 p-7 md:p-9 rounded-3xl bg-surface-800/90 border border-brand-blue/40 shadow-glow-md relative overflow-hidden backdrop-blur-2xl noise-texture">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-brand-blue/10 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-6 mb-8">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-6 mb-7">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-xl bg-brand-amber/15 border border-brand-amber/30 flex items-center justify-center text-brand-amber shadow-sm">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<div className="w-13 h-13 rounded-2xl bg-brand-blue/15 border border-brand-blue/30 flex items-center justify-center text-brand-blue shadow-sm p-3">
|
||||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-display text-xl font-bold text-neutral-100">
|
||||
{detectedOs === 'mac' ? 'macOS Apple Silicon' : 'Windows 64-bit Installer'}
|
||||
<div className="flex items-center gap-2.5 mb-1">
|
||||
<h3 className="text-xl sm:text-2xl font-medium text-neutral-50">
|
||||
D3RO Voice Desktop 1.1.0
|
||||
</h3>
|
||||
<span className="px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">
|
||||
{isKo ? '추천 빌드' : 'RECOMMENDED'}
|
||||
<span className="px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-500/20 text-emerald-300 border border-emerald-500/35">
|
||||
{isKo ? '배포 준비 중' : 'RELEASE PENDING'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="font-mono text-xs text-neutral-400">
|
||||
{detectedOs === 'mac'
|
||||
? 'D3RO-Voice-1.0.0-arm64.dmg · 98 MB · Metal Hardware Accel'
|
||||
: 'D3RO-Voice-Setup-1.0.0-x64.exe · 102 MB · NSIS Installer'}
|
||||
<p className="text-xs sm:text-sm text-neutral-300 font-normal">
|
||||
{isKo
|
||||
? 'Windows x64 및 macOS Apple Silicon 후보 빌드 검증 중'
|
||||
: 'Windows x64 and macOS Apple Silicon candidate builds under verification'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color="green" pulse />
|
||||
<span className="font-mono text-xs text-neutral-400">v1.0.0 STABLE</span>
|
||||
<div className="flex items-center gap-2 bg-surface-700/80 px-3 py-1 rounded-full border border-white/10">
|
||||
<span className="w-2 h-2 rounded-full bg-amber-400" />
|
||||
<span className="text-xs font-medium text-neutral-200">v1.1.0 VERIFICATION</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Primary Action Button */}
|
||||
<div className="mb-6">
|
||||
<a
|
||||
href={
|
||||
detectedOs === 'mac'
|
||||
? 'https://git.chanpaca.net/yunchan/d3ro-voice/releases'
|
||||
: 'https://git.chanpaca.net/attachments/c281a224-2538-496a-bf74-bc188938c734'
|
||||
}
|
||||
download={detectedOs === 'mac' ? undefined : 'D3RO-Voice-Setup-1.0.0-x64.exe'}
|
||||
className="w-full py-4 px-8 rounded-xl bg-brand-amber hover:bg-brand-amber-light text-white font-mono text-sm font-bold uppercase tracking-wider flex items-center justify-center gap-3 shadow-glow-sm hover:shadow-glow-md transition-all active:scale-[0.99]"
|
||||
<div className="mb-5">
|
||||
<div
|
||||
aria-disabled="true"
|
||||
className="w-full py-4.5 sm:py-5 px-8 sm:px-10 rounded-2xl bg-surface-700 text-neutral-300 text-base sm:text-lg font-medium flex items-center justify-center gap-3 leading-normal border border-white/10 cursor-not-allowed"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
<span>
|
||||
{detectedOs === 'mac'
|
||||
? isKo
|
||||
? 'macOS용 다운로드 (v1.0.0 DMG)'
|
||||
: 'Download for macOS (v1.0.0)'
|
||||
: isKo
|
||||
? 'Windows용 다운로드 (v1.0.0 NSIS)'
|
||||
: 'Download for Windows (v1.0.0)'}
|
||||
</span>
|
||||
</a>
|
||||
<span>{isKo ? '설치 파일 검증 후 제공' : 'Installer available after verification'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SHA-256 Quick Verification Strip */}
|
||||
<div className="pt-4 border-t border-white/[0.08] flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 text-xs">
|
||||
<div className="flex items-center gap-2 font-mono text-neutral-400">
|
||||
<span>SHA-256:</span>
|
||||
<span className="text-neutral-200 truncate max-w-[280px]">b0ac051443151a2e34e8192f1fcf7955...</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyHash(OFFICIAL_HASH)}
|
||||
className="text-brand-amber hover:text-brand-amber-light underline font-sans text-xs ml-1"
|
||||
>
|
||||
{copied ? (isKo ? '복사됨!' : 'Copied!') : isKo ? '복사' : 'Copy'}
|
||||
</button>
|
||||
<div className="pt-3.5 border-t border-white/10 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 text-xs">
|
||||
<div className="flex items-center gap-2 text-neutral-300">
|
||||
<span className="font-medium text-neutral-400">{isKo ? '릴리스 증거:' : 'Release evidence:'}</span>
|
||||
<span className="text-neutral-200">{isKo ? '아티팩트 생성 전' : 'Artifact not yet published'}</span>
|
||||
</div>
|
||||
<span className="font-mono text-[11px] text-emerald-400 flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
||||
{isKo ? '코드 서명 및 무결성 검증 완료' : 'Signed & Verified'}
|
||||
<span className="text-xs text-amber-300 font-medium flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-amber-400" />
|
||||
{isKo ? '서명 및 업데이트 검증 대기' : 'Signing and update verification pending'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* All Platform Packages Bento Grid */}
|
||||
<div className="mb-20">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="font-display text-lg font-bold text-neutral-200">
|
||||
<div className="mb-16">
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<h3 className="text-xl font-medium text-neutral-100">
|
||||
{isKo ? '전체 플랫폼 설치 패키지' : 'All Platform Packages'}
|
||||
</h3>
|
||||
<span className="font-mono text-xs text-neutral-500">Auto-Update Feed: latest.yml Active</span>
|
||||
<span className="text-xs text-neutral-400 font-normal">
|
||||
{isKo ? '자동 업데이트: 서명된 피드 검증 후 활성화' : 'Auto-update: activates after signed feed verification'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Windows Package */}
|
||||
<div className="p-6 rounded-xl bg-surface-800/60 border border-white/[0.06] hover:border-brand-amber/30 transition-all flex flex-col justify-between">
|
||||
<div className="p-6 rounded-2xl bg-surface-800/80 border border-white/10 hover:border-brand-blue/40 shadow-glass-card backdrop-blur-xl transition-all flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="font-mono text-xs font-semibold text-brand-amber">WINDOWS 10 / 11</span>
|
||||
<span className="font-mono text-[10px] text-neutral-500">x64</span>
|
||||
<span className="text-xs font-medium text-brand-blue">WINDOWS 10 / 11</span>
|
||||
<span className="font-mono text-xs text-neutral-400">x64</span>
|
||||
</div>
|
||||
<h4 className="font-display text-base font-bold text-neutral-100 mb-1">Windows Setup</h4>
|
||||
<p className="text-xs text-neutral-400 mb-6 leading-relaxed">
|
||||
<h4 className="text-lg font-medium text-neutral-50 mb-1.5">Windows Setup</h4>
|
||||
<p className="text-xs sm:text-sm text-neutral-300 mb-5 leading-relaxed font-normal">
|
||||
{isKo
|
||||
? '원클릭 NSIS 설치 프로그램. 백그라운드 자동 업데이트 및 DirectML GPU 가속 지원.'
|
||||
: 'One-click installer with automatic background delta updates and DirectML acceleration.'}
|
||||
? 'Windows x64 후보 빌드의 설치, 서명, 업데이트 복구 경로를 검증 중입니다.'
|
||||
: 'The Windows x64 candidate is undergoing installation, signing, and update-recovery verification.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<a
|
||||
href="https://git.chanpaca.net/attachments/c281a224-2538-496a-bf74-bc188938c734"
|
||||
download="D3RO-Voice-Setup-1.0.0-x64.exe"
|
||||
className="w-full py-2.5 px-4 rounded-lg bg-surface-700 hover:bg-surface-600 border border-white/[0.08] hover:border-brand-amber/40 text-neutral-200 font-mono text-xs flex items-center justify-between transition-all"
|
||||
<div
|
||||
aria-disabled="true"
|
||||
className="w-full py-2.5 px-3.5 rounded-xl bg-surface-900/60 border border-white/10 text-neutral-400 text-xs font-medium flex items-center justify-between"
|
||||
>
|
||||
<span className="font-semibold">Setup Installer (.exe)</span>
|
||||
<span className="text-neutral-400">102 MB</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://git.chanpaca.net/yunchan/d3ro-voice/releases"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full py-2 px-4 rounded-lg bg-surface-900/40 text-neutral-500 font-mono text-[11px] flex items-center justify-between"
|
||||
>
|
||||
<span>Blockmap (.blockmap)</span>
|
||||
<span>105 KB</span>
|
||||
</a>
|
||||
<span>Windows x64 installer</span>
|
||||
<span>{isKo ? '검증 대기' : 'Verification pending'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* macOS Package */}
|
||||
<div className="p-6 rounded-xl bg-surface-800/60 border border-white/[0.06] hover:border-brand-amber/30 transition-all flex flex-col justify-between">
|
||||
<div className="p-6 rounded-2xl bg-surface-800/80 border border-white/10 hover:border-purple-400/40 shadow-glass-card backdrop-blur-xl transition-all flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="font-mono text-xs font-semibold text-purple-400">MACOS 12.0+</span>
|
||||
<span className="font-mono text-[10px] text-neutral-500">Apple Silicon & Intel</span>
|
||||
<span className="text-xs font-medium text-purple-300">MACOS 12.0+</span>
|
||||
<span className="font-mono text-xs text-neutral-400">Apple Silicon</span>
|
||||
</div>
|
||||
<h4 className="font-display text-base font-bold text-neutral-100 mb-1">macOS DMG</h4>
|
||||
<p className="text-xs text-neutral-400 mb-6 leading-relaxed">
|
||||
<h4 className="text-lg font-medium text-neutral-50 mb-1.5">macOS DMG</h4>
|
||||
<p className="text-xs sm:text-sm text-neutral-300 mb-5 leading-relaxed font-normal">
|
||||
{isKo
|
||||
? 'Apple Silicon M1~M4 Metal 가속 지원 및 Universal DMG 설치 패키지.'
|
||||
: 'Universal package optimized for Apple Silicon (M1/M2/M3/M4) Metal framework.'}
|
||||
? 'Apple Silicon 후보 빌드의 코드 서명과 설치 동작을 검증 중입니다.'
|
||||
: 'The Apple Silicon candidate is undergoing code-signing and installation verification.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<a
|
||||
href="https://git.chanpaca.net/yunchan/d3ro-voice/releases"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full py-2.5 px-4 rounded-lg bg-surface-700 hover:bg-surface-600 border border-white/[0.08] hover:border-purple-400/40 text-neutral-200 font-mono text-xs flex items-center justify-between transition-all"
|
||||
<div
|
||||
aria-disabled="true"
|
||||
className="w-full py-2.5 px-3.5 rounded-xl bg-surface-900/60 border border-white/10 text-neutral-400 text-xs font-medium flex items-center justify-between"
|
||||
>
|
||||
<span className="font-semibold">Apple Silicon DMG (.dmg)</span>
|
||||
<span className="text-neutral-400">98 MB</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://git.chanpaca.net/yunchan/d3ro-voice/releases"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full py-2 px-4 rounded-lg bg-surface-900/40 text-neutral-500 font-mono text-[11px] flex items-center justify-between"
|
||||
>
|
||||
<span>Portable (.zip)</span>
|
||||
<span>96 MB</span>
|
||||
</a>
|
||||
<span>Apple Silicon DMG</span>
|
||||
<span>{isKo ? '검증 대기' : 'Verification pending'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Android & iOS Mobile App Package */}
|
||||
<div className="p-6 rounded-xl bg-surface-800/60 border border-white/[0.06] hover:border-brand-amber/30 transition-all flex flex-col justify-between">
|
||||
{/* Android / iOS Mobile */}
|
||||
<div className="p-6 rounded-2xl bg-surface-800/80 border border-white/10 hover:border-emerald-500/40 shadow-glass-card backdrop-blur-xl transition-all flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="font-mono text-xs font-semibold text-emerald-400">ANDROID & IOS</span>
|
||||
<span className="font-mono text-[10px] text-neutral-500">Mobile Edition</span>
|
||||
<span className="text-xs font-medium text-emerald-400">ANDROID</span>
|
||||
<span className="text-xs text-neutral-400">Mobile Edition</span>
|
||||
</div>
|
||||
<h4 className="font-display text-base font-bold text-neutral-100 mb-1">D3RO Voice Mobile</h4>
|
||||
<p className="text-xs text-neutral-400 mb-6 leading-relaxed">
|
||||
<h4 className="text-lg font-medium text-neutral-50 mb-1.5">D3RO Voice Mobile</h4>
|
||||
<p className="text-xs sm:text-sm text-neutral-300 mb-5 leading-relaxed font-normal">
|
||||
{isKo
|
||||
? '스마트폰에서도 동일하게 작동하는 로컬 음성 비서 및 실시간 마이크 연동. 이동 중에도 끊김 없는 고속 음성 기록.'
|
||||
: 'On-device voice assistant for smartphones with instant mic streaming and seamless cloud sync.'}
|
||||
? 'Google Play용 AAB와 스토어 메타데이터의 검증을 마친 뒤 Android 배포를 시작합니다.'
|
||||
: 'Android distribution begins after the Play AAB and store metadata are verified.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<a
|
||||
href="https://git.chanpaca.net/attachments/0b015367-dd8b-488c-8cc0-4db413b51792"
|
||||
download="d3ro-voice-v1.0.0-signed.apk"
|
||||
className="w-full py-2.5 px-4 rounded-lg bg-surface-700 hover:bg-surface-600 border border-white/[0.08] hover:border-emerald-400/40 text-neutral-200 font-mono text-xs flex items-center justify-between transition-all"
|
||||
<div
|
||||
aria-disabled="true"
|
||||
className="w-full py-2.5 px-3.5 rounded-xl bg-surface-900/60 border border-white/10 text-neutral-400 text-xs font-medium flex items-center justify-between"
|
||||
>
|
||||
<span className="font-semibold">Android Release APK (.apk)</span>
|
||||
<span className="text-emerald-400">Download (47.5MB) →</span>
|
||||
</a>
|
||||
<a
|
||||
href="#download"
|
||||
className="w-full py-2 px-4 rounded-lg bg-surface-900/40 text-neutral-400 font-mono text-[11px] flex items-center justify-between"
|
||||
>
|
||||
<span>{isKo ? 'iOS App Store / TestFlight' : 'iOS TestFlight'}</span>
|
||||
<span className="text-neutral-500">Coming Soon</span>
|
||||
</a>
|
||||
<span>{isKo ? 'Android 공식 APK' : 'Official Android APK'}</span>
|
||||
<span>{isKo ? '서명 증거 확인 대기' : 'Unavailable — evidence pending'}</span>
|
||||
</div>
|
||||
<div className="w-full py-2 px-3.5 rounded-xl bg-surface-900/50 text-neutral-500 text-xs flex items-center justify-between">
|
||||
<span>iOS App Store / TestFlight</span>
|
||||
<span>{isKo ? '검증된 배포 없음' : 'No verified distribution'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live Client-Side SHA-256 Verifier */}
|
||||
<div className="mb-20 p-8 rounded-2xl bg-surface-800/40 border border-white/[0.06] backdrop-blur-sm">
|
||||
{/* Client-Side Cryptographic Verifier Box */}
|
||||
<div className="p-7 md:p-8 rounded-3xl bg-surface-800/70 border border-white/10 shadow-glass-card backdrop-blur-xl mb-16">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<span className="font-mono text-nano text-brand-amber uppercase tracking-widest block mb-1">
|
||||
CRYPTOGRAPHIC INTEGRITY
|
||||
<span className="text-xs font-medium text-brand-blue uppercase tracking-wider block mb-1">
|
||||
LOCAL FILE UTILITY
|
||||
</span>
|
||||
<h3 className="font-display text-xl font-bold text-neutral-100">
|
||||
{isKo ? '실시간 SHA-256 무결성 검증기' : 'SHA-256 Binary Integrity Verifier'}
|
||||
<h3 className="text-lg sm:text-xl font-medium text-neutral-50 mb-1">
|
||||
{isKo ? '로컬 SHA-256 계산기' : 'Client-Side SHA-256 Calculator'}
|
||||
</h3>
|
||||
<p className="text-xs text-neutral-400">
|
||||
<p className="text-xs sm:text-sm text-neutral-300 font-normal">
|
||||
{isKo
|
||||
? '다운로드한 파일을 브라우저로 끌어다 놓아 위변조 및 해시 일치를 로컬에서 즉시 확인하세요.'
|
||||
: 'Drag and drop your installer file to compute SHA-256 locally via browser WebCrypto API.'}
|
||||
? '선택한 파일의 SHA-256 값을 브라우저에서 계산합니다. 공식 릴리스 판정은 제공하지 않습니다.'
|
||||
: 'Compute a selected file hash locally with WebCrypto. This does not certify an official release.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label className="px-4 py-2 rounded-lg bg-surface-700 hover:bg-surface-600 border border-white/[0.1] text-neutral-200 font-mono text-xs font-semibold cursor-pointer transition-all">
|
||||
{isKo ? '파일 선택하여 검증' : 'Select File to Verify'}
|
||||
<input type="file" className="hidden" onChange={handleFileChange} />
|
||||
<label className="px-4 py-2 rounded-xl bg-surface-700 hover:bg-surface-600 border border-white/10 hover:border-brand-blue/40 text-neutral-200 text-xs font-medium cursor-pointer transition-all flex items-center gap-2">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="17 8 12 3 7 8" />
|
||||
<line x1="12" y1="3" x2="12" y2="15" />
|
||||
</svg>
|
||||
<span>{isKo ? '파일 선택하여 검증' : 'Select File'}</span>
|
||||
<input type="file" onChange={handleFileChange} className="hidden" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Drag & Drop Target Area */}
|
||||
<div
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={`border-2 border-dashed rounded-xl p-8 text-center transition-all ${
|
||||
isDragOver ? 'border-brand-amber bg-brand-amber/10' : 'border-white/[0.08] bg-surface-900/40'
|
||||
className={`p-7 rounded-2xl border-2 border-dashed transition-all text-center ${
|
||||
isDragOver
|
||||
? 'border-brand-blue bg-brand-blue/10'
|
||||
: 'border-white/15 bg-surface-900/50 hover:border-white/25'
|
||||
}`}
|
||||
>
|
||||
{verifyStatus === 'idle' && (
|
||||
<div>
|
||||
<p className="font-mono text-xs text-neutral-300 mb-1">
|
||||
{isKo
|
||||
? '설치 파일(.exe / .dmg / .zip)을 이곳에 끌어다 놓으세요'
|
||||
: 'Drop installer file (.exe / .dmg / .zip) here'}
|
||||
<div className="space-y-1.5 text-neutral-400">
|
||||
<p className="text-xs sm:text-sm font-normal text-neutral-200">
|
||||
{isKo ? '설치 파일(.exe / .dmg / .zip)을 이곳에 끌어다 놓으세요' : 'Drop installer file here to compute cryptographic hash'}
|
||||
</p>
|
||||
<p className="font-mono text-[11px] text-neutral-500">
|
||||
{isKo ? '파일은 서버로 전송되지 않고 로컬에서 안전하게 계산됩니다.' : 'Processed locally in browser.'}
|
||||
<p className="text-xs text-neutral-400 font-normal">
|
||||
{isKo ? '파일은 서버로 전송되지 않고 로컬 브라우저에서 안전하게 계산됩니다.' : 'Calculated in-memory locally via browser WebCrypto.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{verifyStatus !== 'idle' && (
|
||||
<div className="text-left font-mono text-xs space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold text-neutral-200">{fileName}</span>
|
||||
{verifyStatus === 'computing' && (
|
||||
<span className="px-2 py-0.5 rounded bg-amber-500/20 text-amber-300">Computing...</span>
|
||||
)}
|
||||
{verifyStatus === 'match' && (
|
||||
<span className="px-2 py-0.5 rounded bg-emerald-500/20 text-emerald-400 font-bold">
|
||||
✓ {isKo ? '공식 릴리스 일치 (정상)' : 'OFFICIAL MATCH (GENUINE)'}
|
||||
</span>
|
||||
)}
|
||||
{verifyStatus === 'custom' && (
|
||||
<span className="px-2 py-0.5 rounded bg-brand-amber/20 text-brand-amber font-bold">
|
||||
✓ {isKo ? '로컬 해시 계산 완료' : 'LOCAL HASH COMPUTED'}
|
||||
</span>
|
||||
)}
|
||||
{verifyStatus === 'computing' && (
|
||||
<div className="flex items-center justify-center gap-3 text-brand-blue text-xs sm:text-sm font-medium">
|
||||
<span className="w-2 h-2 rounded-full bg-brand-blue animate-ping" />
|
||||
<span>{isKo ? 'SHA-256 해시 계산 중...' : 'Computing SHA-256 hash locally...'}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{verifyStatus === 'custom' && (
|
||||
<div className="space-y-2 text-left bg-surface-950 p-4 rounded-xl border border-white/10">
|
||||
<div className="flex items-center justify-between text-xs text-neutral-300">
|
||||
<span className="font-medium text-brand-blue">{isKo ? '로컬 해시 계산 완료' : 'Local Hash Computed'}</span>
|
||||
<span>{fileName}</span>
|
||||
</div>
|
||||
<div className="p-3 rounded bg-surface-950/60 border border-white/[0.04] space-y-1">
|
||||
<div className="text-neutral-400">
|
||||
Calculated:{' '}
|
||||
<span className="text-brand-amber">{computedHash || (isKo ? '계산 중...' : 'Hashing...')}</span>
|
||||
</div>
|
||||
<div className="text-neutral-400">
|
||||
Official:{' '}<span className="text-emerald-400">{OFFICIAL_HASH}</span>
|
||||
</div>
|
||||
<div className="font-mono text-xs text-neutral-200 break-all pt-1 border-t border-white/10">
|
||||
{computedHash}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -373,78 +295,81 @@ export function Download() {
|
|||
</div>
|
||||
|
||||
{/* Release History Section */}
|
||||
<div id="releases">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<div>
|
||||
<span className="font-mono text-nano text-brand-amber uppercase tracking-widest block mb-1">
|
||||
<span className="text-xs font-medium text-brand-blue uppercase tracking-wider block mb-1">
|
||||
VERSION ARCHIVE
|
||||
</span>
|
||||
<h3 className="font-display text-xl font-bold text-neutral-100">
|
||||
{isKo ? '릴리스 버전 히스토리' : 'Release Version History'}
|
||||
<h3 className="text-xl font-medium text-neutral-50">
|
||||
{isKo ? '릴리스 버전 히스토리' : 'Release Archive & Changelog'}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://git.chanpaca.net/yunchan/d3ro-voice/releases"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-mono text-xs text-brand-amber hover:underline flex items-center gap-1"
|
||||
className="text-xs text-brand-blue hover:text-brand-blue-light transition-colors font-medium flex items-center gap-1.5"
|
||||
>
|
||||
<span>{isKo ? 'Forgejo Git 릴리스 전체 보기' : 'Forgejo Git Releases'}</span>
|
||||
<span>Forgejo Git 릴리스 전체 보기</span>
|
||||
<span>→</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* v1.0.0 */}
|
||||
<div className="p-6 rounded-xl bg-surface-800/80 border-l-4 border-l-brand-amber border border-white/[0.06]">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-3">
|
||||
{/* v1.0.0 Release */}
|
||||
<div className="p-6 rounded-2xl bg-surface-800/80 border-l-4 border-l-brand-blue border border-white/10 shadow-glass-card">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-3 pb-3 border-b border-white/[0.06]">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-base font-bold text-neutral-100">v1.0.0</span>
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-mono font-bold bg-brand-amber/20 text-brand-amber border border-brand-amber/30">
|
||||
LATEST STABLE
|
||||
<span className="text-base font-medium text-neutral-100">v1.0.0</span>
|
||||
<span className="px-2.5 py-0.5 rounded-full text-xs font-medium bg-brand-blue/15 text-brand-blue border border-brand-blue/30">
|
||||
ARCHIVED · DOWNLOAD UNAVAILABLE
|
||||
</span>
|
||||
<span className="font-mono text-xs text-neutral-500">2026-08-20</span>
|
||||
<span className="text-xs text-neutral-400">2026-08-20</span>
|
||||
</div>
|
||||
<a
|
||||
href="/releases/1.0.0/D3RO-Voice-Setup-1.0.0-x64.exe"
|
||||
download
|
||||
className="px-3 py-1.5 rounded-lg bg-brand-amber/15 hover:bg-brand-amber/25 text-brand-amber font-mono text-xs font-bold border border-brand-amber/30 transition-all text-center"
|
||||
<span
|
||||
aria-disabled="true"
|
||||
className="px-3.5 py-1.5 rounded-xl bg-surface-900/60 text-neutral-400 border border-white/10 text-xs font-medium self-start sm:self-auto"
|
||||
>
|
||||
Installer (.exe)
|
||||
</a>
|
||||
{isKo ? '과거 바이너리 미제공' : 'Historical binary unavailable'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul className="text-xs text-neutral-300 space-y-1.5 mb-4 list-disc list-inside">
|
||||
<li>
|
||||
<strong className="text-brand-amber">10+ 글로벌 광고 미디에이션</strong>: EthicalAds, Carbon Ads, GAM, Playwire, AppLovin, Unity Ads 실시간 입찰.
|
||||
<ul className="space-y-1.5 text-xs sm:text-sm text-neutral-300 mb-3.5 font-normal leading-relaxed">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-brand-blue font-medium">•</span>
|
||||
<span><strong className="text-neutral-100 font-medium">10+ 글로벌 광고 미디에이션</strong>: EthicalAds, Carbon Ads, GAM, Playwire, AppLovin, Unity Ads 실시간 입찰.</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-brand-amber">무료 티어 리워드 충전</strong>: 15초 스폰서 비디오 시청 완료 시 +50 Cloud AI 토큰 자동 리필.
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-brand-blue font-medium">•</span>
|
||||
<span><strong className="text-neutral-100 font-medium">무료 티어 리워드 충전</strong>: 15초 스폰서 비디오 시청 완료 시 +50 Cloud AI 토큰 자동 리필.</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-brand-amber">100% 로컬 Whisper 전사 엔진</strong>: 네트워크 없이 실시간 고속 오프라인 음성 텍스트 변환.
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-brand-blue font-medium">•</span>
|
||||
<span><strong className="text-neutral-100 font-medium">100% 로컬 Whisper 전사 엔진</strong>: 네트워크 연결 없이 실시간 고속 오프라인 음성 텍스트 변환.</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-brand-amber">Android & iOS 모바일 앱</strong>: 스마트폰 음성 입력 및 클라우드 AI 실시간 계정 동기화.
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-brand-blue font-medium">•</span>
|
||||
<span><strong className="text-neutral-100 font-medium">모바일 TEST-DEMO</strong>: 공식 서명 증거와 스토어 심사가 끝날 때까지 일반 다운로드는 제공하지 않습니다.</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="p-2.5 rounded bg-surface-950/60 font-mono text-[11px] text-neutral-400 flex items-center justify-between">
|
||||
<span className="truncate max-w-[450px]">SHA-256: b0ac051443151a2e34e8192f1fcf795586bbafca6eb21f01a79170c079a53ba2</span>
|
||||
<span className="text-brand-amber">102 MB</span>
|
||||
<div className="text-xs text-neutral-400 p-2.5 rounded-lg bg-surface-950 border border-white/[0.04]">
|
||||
{isKo
|
||||
? '이 항목은 변경 이력으로만 보존합니다. 설치 파일, 고정 해시, 용량 정보는 현재 릴리스로 제공하지 않습니다.'
|
||||
: 'This entry is retained only as history. Its installer, fixed hash, and size are not offered as a current release.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* v0.2.1-alpha */}
|
||||
<div className="p-5 rounded-xl bg-surface-800/40 border-l-4 border-l-neutral-700 border border-white/[0.04] opacity-80">
|
||||
<div className="p-5 rounded-2xl bg-surface-800/50 border-l-4 border-l-surface-600 border border-white/5 opacity-75">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="font-mono text-sm font-bold text-neutral-200">v0.2.1-alpha</span>
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-mono bg-white/5 text-neutral-400">PRE-RELEASE</span>
|
||||
<span className="font-mono text-xs text-neutral-500">2026-08-15</span>
|
||||
<span className="text-sm font-medium text-neutral-200">v0.2.1-alpha</span>
|
||||
<span className="px-2 py-0.5 rounded-full text-xs font-normal bg-white/5 text-neutral-400 border border-white/10">PRE-RELEASE</span>
|
||||
<span className="text-xs text-neutral-500 font-mono">2026-08-15</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400">
|
||||
Deepgram, AssemblyAI, Groq 멀티 클라우드 STT 드라이버 디스패치 및 암호화 라이선스 키 검증 엔진 탑재.
|
||||
<p className="text-xs text-neutral-300 font-normal">
|
||||
Deepgram, AssemblyAI, Groq 멀티 클라우드 STT 드라이버 디스패처 및 암호화 라이선스 키 검증 엔진 탑재.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,54 +8,63 @@ export function FAQ() {
|
|||
const [openIndex, setOpenIndex] = useState<number | null>(null)
|
||||
|
||||
return (
|
||||
<section id="faq" className="relative py-24 md:py-32">
|
||||
<section id="faq" className="relative py-20 md:py-28">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-surface-800/30 to-transparent pointer-events-none" />
|
||||
|
||||
<Container className="relative max-w-3xl">
|
||||
<Container className="relative max-w-4xl">
|
||||
<SectionHeader
|
||||
index={t.faq.index}
|
||||
title={t.faq.title}
|
||||
/>
|
||||
|
||||
<div className="space-y-px">
|
||||
<div className="space-y-3.5">
|
||||
{t.faq.items.map((faq, i) => {
|
||||
const isOpen = openIndex === i
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="border-b border-white/[0.04]"
|
||||
className={`rounded-2xl border transition-all duration-200 overflow-hidden ${
|
||||
isOpen
|
||||
? 'bg-surface-800/90 border-brand-blue/40 shadow-glow-sm'
|
||||
: 'bg-surface-800/60 border-white/10 hover:border-white/20'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpenIndex(isOpen ? null : i)}
|
||||
className="w-full flex items-start justify-between gap-4 py-5 text-left group"
|
||||
className="w-full flex items-start justify-between gap-4 p-5 sm:p-6 text-left group"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="font-mono text-nano text-neutral-600 mt-1 flex-shrink-0">
|
||||
<div className="flex items-start gap-3.5">
|
||||
<span className="font-mono text-xs font-medium text-brand-blue mt-0.5 flex-shrink-0 bg-brand-blue/10 px-2.5 py-0.5 rounded-md border border-brand-blue/25">
|
||||
{String(i + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-neutral-200 group-hover:text-white transition-colors">
|
||||
<span className="text-base sm:text-lg font-medium sm:font-medium text-neutral-100 group-hover:text-brand-blue transition-colors leading-snug">
|
||||
{faq.q}
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-4 h-4 text-neutral-500 flex-shrink-0 mt-1 transition-transform duration-200 ${
|
||||
isOpen ? 'rotate-45' : ''
|
||||
}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
<div className="w-7 h-7 rounded-full bg-surface-700/80 border border-white/10 flex items-center justify-center flex-shrink-0 mt-0.5 text-neutral-300 group-hover:text-brand-blue group-hover:border-brand-blue/40 transition-colors">
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 transition-transform duration-200 ${
|
||||
isOpen ? 'rotate-45 text-brand-blue' : ''
|
||||
}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<div className={`accordion-content ${isOpen ? 'open' : ''}`}>
|
||||
<div>
|
||||
<p className="pl-10 pb-5 text-sm text-neutral-400 leading-relaxed">
|
||||
{faq.a}
|
||||
</p>
|
||||
<div className="overflow-hidden min-h-0">
|
||||
<div className="px-5 sm:px-6 pb-5 pt-0">
|
||||
<p className="pl-10 text-sm sm:text-base text-neutral-300 leading-relaxed font-normal border-t border-white/[0.06] pt-3.5">
|
||||
{faq.a}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ interface FeatureTile {
|
|||
}
|
||||
|
||||
const tagStyles = {
|
||||
FREE: 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20',
|
||||
PRO: 'text-brand-amber bg-brand-amber/10 border-brand-amber/20',
|
||||
'PRO+': 'text-violet-400 bg-violet-500/10 border-violet-500/20',
|
||||
FREE: 'text-emerald-400 bg-emerald-500/15 border-emerald-500/30',
|
||||
PRO: 'text-brand-blue bg-brand-blue/15 border-brand-blue/30',
|
||||
'PRO+': 'text-purple-300 bg-purple-500/15 border-purple-500/30',
|
||||
} as const
|
||||
|
||||
export function Features() {
|
||||
|
|
@ -32,21 +32,21 @@ export function Features() {
|
|||
description: locale === 'ko'
|
||||
? 'VS Code, Notion, Slack, Word 등 모든 프로그램 위에서 Ctrl+Shift+Space로 즉시 호출되는 초경량 플로팅 캡슐. 타이핑 없이 생각의 속도로 입력하세요.'
|
||||
: 'Lightweight floating HUD triggered with Ctrl+Shift+Space across any application. Dictate and transcribe at the speed of thought without leaving your workflow.',
|
||||
highlight: 'Global Hotkey & Floating Overlays',
|
||||
highlight: '글로벌 핫키 & 플로팅 오버레이',
|
||||
icon: <MicIcon />,
|
||||
previewNode: (
|
||||
<div className="mt-4 p-3 rounded-xl bg-surface-950 border border-white/[0.06] flex items-center justify-between shadow-inner">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-brand-amber animate-ping" />
|
||||
<div className="flex items-center gap-1 h-5">
|
||||
{[4, 12, 20, 16, 24, 14, 8, 18, 6].map((h, i) => (
|
||||
<div key={i} className="w-1 bg-brand-amber rounded-full" style={{ height: `${h}px` }} />
|
||||
<div className="mt-4 p-3.5 rounded-xl bg-surface-950/90 border border-white/10 flex items-center justify-between shadow-inner">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-brand-blue animate-ping" />
|
||||
<div className="flex items-center gap-1.5 h-6">
|
||||
{[6, 14, 22, 18, 24, 16, 10, 20, 8].map((h, i) => (
|
||||
<div key={i} className="w-1.5 bg-brand-blue rounded-full shadow-sm" style={{ height: `${h}px` }} />
|
||||
))}
|
||||
</div>
|
||||
<span className="font-mono text-nano text-neutral-400 ml-2">0:04</span>
|
||||
<span className="font-mono text-xs font-medium text-neutral-300 ml-2">0:04</span>
|
||||
</div>
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">
|
||||
AUTO-INJECTING
|
||||
<span className="text-xs font-medium text-emerald-400 bg-emerald-500/15 px-2.5 py-0.5 rounded-full border border-emerald-500/30">
|
||||
자동 커서 입력 중
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
|
|
@ -59,7 +59,7 @@ export function Features() {
|
|||
description: locale === 'ko'
|
||||
? '음성 및 전사 텍스트가 외부 클라우드 서버로 단 1바이트도 전송되지 않습니다. 에어갭 환경 및 사내 보안 규정 100% 준수.'
|
||||
: 'Zero cloud telemetry. Voice audio and transcriptions are strictly processed on your local machine using faster-whisper and Ollama.',
|
||||
highlight: 'Zero Cloud Egress',
|
||||
highlight: '클라우드 유출 0바이트',
|
||||
icon: <ShieldIcon />,
|
||||
},
|
||||
{
|
||||
|
|
@ -70,7 +70,7 @@ export function Features() {
|
|||
description: locale === 'ko'
|
||||
? '장시간 회의 녹음, 화자 자동 분리, 실시간 스크래치패드 및 마크다운 회의록(액션 아이템, 결정 사항) 자동 생성.'
|
||||
: 'Long-form meeting recording with speaker diarization, real-time memo scratchpad, and automatic structured Markdown summary generation.',
|
||||
highlight: 'Auto Markdown Action Items',
|
||||
highlight: '자동 마크다운 회의록',
|
||||
icon: <UsersIcon />,
|
||||
},
|
||||
{
|
||||
|
|
@ -81,25 +81,25 @@ export function Features() {
|
|||
description: locale === 'ko'
|
||||
? '음성 입력 → 추임새 제거 → 전문 용어 사전 보정 → LLM 톤앤매너 변환 → 활성 창 자동 입력까지 1회 발화로 연속 실행하는 고성능 자동화 엔진.'
|
||||
: 'Chain multi-step prompts in a single voice command: Transcribe → Clean Fillers → Phonetic Dictionary Fix → LLM Transform → Active Cursor Injection.',
|
||||
highlight: 'Custom Prompts & Tone Shift',
|
||||
highlight: '맞춤형 프롬프트 & 톤 변환',
|
||||
icon: <ChainIcon />,
|
||||
previewNode: (
|
||||
<div className="mt-4 grid grid-cols-4 gap-2 font-mono text-nano text-neutral-400 bg-surface-950 p-2.5 rounded-lg border border-white/[0.04]">
|
||||
<div className="text-center p-1.5 rounded bg-surface-800 border border-white/[0.05]">
|
||||
<span className="text-brand-amber block">01. STT</span>
|
||||
<span className="text-neutral-500">Whisper</span>
|
||||
<div className="mt-4 grid grid-cols-2 sm:grid-cols-4 gap-2.5 text-xs text-neutral-300 bg-surface-950/90 p-3 rounded-xl border border-white/10">
|
||||
<div className="text-center p-2 rounded-lg bg-surface-800 border border-white/10">
|
||||
<span className="text-brand-blue font-medium block text-xs">01. STT</span>
|
||||
<span className="text-neutral-400 text-[11px]">Whisper</span>
|
||||
</div>
|
||||
<div className="text-center p-1.5 rounded bg-surface-800 border border-white/[0.05]">
|
||||
<span className="text-emerald-400 block">02. DICT</span>
|
||||
<span className="text-neutral-500">Phonetic</span>
|
||||
<div className="text-center p-2 rounded-lg bg-surface-800 border border-white/10">
|
||||
<span className="text-emerald-400 font-medium block text-xs">02. 사전</span>
|
||||
<span className="text-neutral-400 text-[11px]">음성 발음 보정</span>
|
||||
</div>
|
||||
<div className="text-center p-1.5 rounded bg-surface-800 border border-white/[0.05]">
|
||||
<span className="text-violet-400 block">03. LLM</span>
|
||||
<span className="text-neutral-500">Ollama</span>
|
||||
<div className="text-center p-2 rounded-lg bg-surface-800 border border-white/10">
|
||||
<span className="text-purple-300 font-medium block text-xs">03. LLM</span>
|
||||
<span className="text-neutral-400 text-[11px]">Ollama Local</span>
|
||||
</div>
|
||||
<div className="text-center p-1.5 rounded bg-surface-800 border border-white/[0.05]">
|
||||
<span className="text-neutral-200 block">04. INJECT</span>
|
||||
<span className="text-neutral-500">Cursor</span>
|
||||
<div className="text-center p-2 rounded-lg bg-surface-800 border border-white/10">
|
||||
<span className="text-neutral-200 font-medium block text-xs">04. 입력</span>
|
||||
<span className="text-neutral-400 text-[11px]">활성 커서 삽입</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
|
|
@ -112,7 +112,7 @@ export function Features() {
|
|||
description: locale === 'ko'
|
||||
? '내 PC의 PDF, 메모, 사내 문서를 로컬 벡터 DB에 인덱싱하여 음성으로 검색하고 대화할 수 있는 비공개 지식 베이스.'
|
||||
: 'Index local PDFs, notes, and private documents into a local vector DB. Query your private knowledge base using voice commands.',
|
||||
highlight: 'sqlite-vec & Local Embeddings',
|
||||
highlight: 'sqlite-vec 로컬 임베딩',
|
||||
icon: <DatabaseIcon />,
|
||||
},
|
||||
{
|
||||
|
|
@ -123,13 +123,13 @@ export function Features() {
|
|||
description: locale === 'ko'
|
||||
? 'C++ 기반 faster-whisper 백엔드로 일반 노트북 CPU에서도 1.2초 미만의 경이로운 응답 속도를 실현.'
|
||||
: 'Optimized C++ faster-whisper runtime delivers sub-1.2s transcription latency even on standard laptop CPUs without dedicated GPU.',
|
||||
highlight: '< 1.2s Real-time Latency',
|
||||
highlight: '1.2초 미만 실시간 응답',
|
||||
icon: <ZapIcon />,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section id="features" className="relative py-24 md:py-32">
|
||||
<section id="features" className="relative py-20 md:py-28">
|
||||
<Container>
|
||||
<SectionHeader
|
||||
index={t.features.index}
|
||||
|
|
@ -138,28 +138,28 @@ export function Features() {
|
|||
/>
|
||||
|
||||
{/* Asymmetric Bento Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{bentoTiles.map((tile) => (
|
||||
<div
|
||||
key={tile.id}
|
||||
className={`relative noise-texture bg-surface-700/40 border border-white/[0.06] rounded-2xl p-6 md:p-7 shadow-chassis transition-all duration-300 hover:border-brand-amber/30 hover:bg-surface-700/60 hover:shadow-glow-sm flex flex-col justify-between ${tile.span}`}
|
||||
className={`relative noise-texture bg-surface-800/80 border border-white/10 rounded-3xl p-6 md:p-7 shadow-glass-card transition-all duration-300 hover:border-brand-blue/40 hover:bg-surface-700/85 hover:shadow-glow-sm flex flex-col justify-between backdrop-blur-xl ${tile.span}`}
|
||||
>
|
||||
<div>
|
||||
{/* Top row: icon + tag */}
|
||||
<div className="flex items-start justify-between mb-5">
|
||||
<div className="w-11 h-11 rounded-xl bg-surface-600 border border-white/[0.08] flex items-center justify-center text-brand-amber shadow-sm">
|
||||
<div className="w-11 h-11 rounded-2xl bg-surface-700/80 border border-white/10 flex items-center justify-center text-brand-blue shadow-sm">
|
||||
{tile.icon}
|
||||
</div>
|
||||
<span className={`text-nano font-mono font-semibold tracking-widest uppercase px-2.5 py-0.5 border rounded-md ${tagStyles[tile.tag]}`}>
|
||||
<span className={`text-xs font-medium px-2.5 py-0.5 border rounded-full shadow-sm ${tagStyles[tile.tag]}`}>
|
||||
{tile.tag}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<h3 className="font-display text-xl font-bold text-neutral-100 mb-2.5">
|
||||
<h3 className="text-lg sm:text-xl font-medium text-neutral-50 mb-2.5 tracking-tight">
|
||||
{tile.title}
|
||||
</h3>
|
||||
<p className="text-sm text-neutral-400 leading-relaxed">
|
||||
<p className="text-sm text-neutral-300 leading-relaxed font-normal">
|
||||
{tile.description}
|
||||
</p>
|
||||
|
||||
|
|
@ -169,11 +169,11 @@ export function Features() {
|
|||
|
||||
{/* Bottom tag / detail */}
|
||||
{tile.highlight && (
|
||||
<div className="mt-6 pt-4 border-t border-white/[0.04] flex items-center justify-between">
|
||||
<span className="font-mono text-nano text-neutral-500 uppercase tracking-wider">
|
||||
<div className="mt-5 pt-3.5 border-t border-white/[0.08] flex items-center justify-between">
|
||||
<span className="text-xs font-normal text-neutral-400">
|
||||
{tile.highlight}
|
||||
</span>
|
||||
<div className="w-6 h-6 rounded-full border border-white/[0.08] flex items-center justify-center text-neutral-500 hover:text-brand-amber hover:border-brand-amber/30 transition-colors">
|
||||
<div className="w-6 h-6 rounded-full bg-surface-700/60 border border-white/10 flex items-center justify-center text-neutral-400 hover:text-brand-blue hover:border-brand-blue/40 transition-colors">
|
||||
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="7" y1="17" x2="17" y2="7" />
|
||||
<polyline points="7 7 17 7 17 17" />
|
||||
|
|
|
|||
|
|
@ -11,35 +11,35 @@ export function Footer() {
|
|||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<footer className="border-t border-white/[0.04] py-12 md:py-16">
|
||||
<footer className="border-t border-white/10 py-12 md:py-16 bg-surface-950/80">
|
||||
<Container>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-10">
|
||||
{/* Left: brand */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Led color="amber" size="sm" />
|
||||
<span className="font-mono text-sm font-semibold tracking-tight text-neutral-300">
|
||||
D3RO·VOICE
|
||||
<div className="flex items-center gap-2 mb-2.5">
|
||||
<Led color="blue" size="sm" />
|
||||
<span className="text-base font-medium tracking-tight text-white">
|
||||
D3RO<span className="text-brand-blue font-medium">·</span>VOICE
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-500 leading-relaxed max-w-sm">
|
||||
<p className="text-sm text-neutral-400 leading-relaxed max-w-sm font-normal">
|
||||
{t.footer.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right: links */}
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-3 md:justify-end">
|
||||
<a href="https://github.com/user/D3ROVoice" className="font-mono text-nano uppercase tracking-widest text-neutral-500 hover:text-neutral-300 transition-colors flex items-center gap-1.5">
|
||||
<div className="flex flex-wrap gap-x-7 gap-y-2.5 md:justify-end items-center">
|
||||
<a href="https://git.chanpaca.net/yunchan/d3ro-voice" target="_blank" rel="noreferrer" className="text-xs text-neutral-400 hover:text-brand-blue transition-colors font-medium flex items-center gap-1.5">
|
||||
<GithubIcon />
|
||||
GitHub
|
||||
Repository
|
||||
</a>
|
||||
<a href="#features" className="font-mono text-nano uppercase tracking-widest text-neutral-500 hover:text-neutral-300 transition-colors">
|
||||
<a href="#features" className="text-xs text-neutral-400 hover:text-brand-blue transition-colors font-medium">
|
||||
{t.nav.features}
|
||||
</a>
|
||||
<a href="#pricing" className="font-mono text-nano uppercase tracking-widest text-neutral-500 hover:text-neutral-300 transition-colors">
|
||||
<a href="#pricing" className="text-xs text-neutral-400 hover:text-brand-blue transition-colors font-medium">
|
||||
{t.nav.pricing}
|
||||
</a>
|
||||
<a href="#faq" className="font-mono text-nano uppercase tracking-widest text-neutral-500 hover:text-neutral-300 transition-colors">
|
||||
<a href="#faq" className="text-xs text-neutral-400 hover:text-brand-blue transition-colors font-medium">
|
||||
{t.nav.faq}
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -50,7 +50,7 @@ export function Footer() {
|
|||
{techStack.map((tech) => (
|
||||
<span
|
||||
key={tech}
|
||||
className="font-mono text-nano uppercase tracking-widest text-neutral-600 bg-surface-700/50 px-2.5 py-1 rounded-sm border border-white/[0.03]"
|
||||
className="text-xs text-neutral-400 bg-surface-800/80 px-2.5 py-1 rounded-lg border border-white/10 font-normal font-mono"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
|
|
@ -58,11 +58,11 @@ export function Footer() {
|
|||
</div>
|
||||
|
||||
{/* Bottom bar */}
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 pt-6 border-t border-white/[0.04]">
|
||||
<span className="font-mono text-nano text-neutral-600 uppercase tracking-widest">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 pt-5 border-t border-white/10 text-xs text-neutral-400 font-normal">
|
||||
<span>
|
||||
{t.footer.copyright.replace('{year}', String(new Date().getFullYear()))}
|
||||
</span>
|
||||
<span className="font-mono text-nano text-neutral-600 uppercase tracking-widest">
|
||||
<span>
|
||||
{t.footer.builtWith}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,16 +19,19 @@ export function Header() {
|
|||
<header
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
scrolled
|
||||
? 'bg-surface-950/85 backdrop-blur-xl border-b border-white/[0.06] shadow-lg'
|
||||
: 'bg-transparent'
|
||||
? 'bg-surface-950/90 backdrop-blur-2xl border-b border-white/10 shadow-2xl py-3'
|
||||
: 'bg-transparent py-4'
|
||||
}`}
|
||||
>
|
||||
<div className="mx-auto max-w-7xl px-5 md:px-8 lg:px-12 h-16 flex items-center justify-between">
|
||||
<div className="mx-auto max-w-7xl px-5 md:px-8 lg:px-12 flex items-center justify-between">
|
||||
{/* Logo */}
|
||||
<a href="#" className="flex items-center gap-2 group">
|
||||
<Led color="amber" pulse size="md" />
|
||||
<span className="font-mono text-sm font-bold tracking-tight text-white">
|
||||
D3RO<span className="text-brand-amber">·</span>VOICE
|
||||
<a href="#" className="flex items-center gap-2.5 group">
|
||||
<Led color="blue" pulse size="md" />
|
||||
<span className="text-base font-medium tracking-tight text-white flex items-center">
|
||||
D3RO<span className="text-brand-blue font-medium">·</span>VOICE
|
||||
</span>
|
||||
<span className="hidden sm:inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-brand-blue/15 text-brand-blue border border-brand-blue/30">
|
||||
v1.0
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
|
@ -38,7 +41,7 @@ export function Header() {
|
|||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="font-mono text-nano uppercase tracking-widest text-neutral-400 hover:text-white transition-colors"
|
||||
className="text-sm font-medium text-neutral-300 hover:text-white transition-colors"
|
||||
>
|
||||
{t.nav[link.key]}
|
||||
</a>
|
||||
|
|
@ -46,48 +49,51 @@ export function Header() {
|
|||
</nav>
|
||||
|
||||
{/* Right side: language + CTA */}
|
||||
<div className="hidden md:flex items-center gap-3">
|
||||
<div className="hidden md:flex items-center gap-3.5">
|
||||
<LanguageSwitcher />
|
||||
<a
|
||||
href="#download"
|
||||
className="glow-btn inline-flex items-center gap-2 px-4 py-2 font-mono text-xs uppercase tracking-wider text-white bg-brand-amber rounded-panel shadow-sm hover:bg-brand-amber-light transition-colors"
|
||||
className="glow-btn inline-flex items-center gap-2 px-5 py-2.5 text-sm font-medium text-white bg-brand-blue rounded-xl shadow-glow-sm hover:bg-brand-blue-light transition-all"
|
||||
>
|
||||
<DownloadSmallIcon />
|
||||
{t.nav.download}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
className="md:hidden w-9 h-9 flex items-center justify-center text-neutral-300"
|
||||
aria-label="Menu"
|
||||
className="md:hidden w-10 h-10 rounded-lg bg-surface-800 border border-white/10 flex items-center justify-center text-neutral-200"
|
||||
aria-label="Toggle Navigation Menu"
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<span className={`block w-5 h-px bg-white transition-all ${mobileOpen ? 'rotate-45 translate-y-[3.5px]' : ''}`} />
|
||||
<span className={`block w-5 h-px bg-white transition-all ${mobileOpen ? '-rotate-45 -translate-y-[3.5px]' : ''}`} />
|
||||
<div className="space-y-1.5 w-5">
|
||||
<span className={`block w-5 h-0.5 bg-white transition-all ${mobileOpen ? 'rotate-45 translate-y-2' : ''}`} />
|
||||
<span className={`block w-5 h-0.5 bg-white transition-all ${mobileOpen ? 'opacity-0' : ''}`} />
|
||||
<span className={`block w-5 h-0.5 bg-white transition-all ${mobileOpen ? '-rotate-45 -translate-y-2' : ''}`} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Nav */}
|
||||
{mobileOpen && (
|
||||
<div className="md:hidden bg-surface-900/95 backdrop-blur-2xl border-t border-white/[0.06] px-5 py-6 space-y-1 shadow-2xl">
|
||||
<div className="md:hidden bg-surface-900/95 backdrop-blur-2xl border-t border-white/10 px-6 py-6 space-y-2 shadow-2xl">
|
||||
{navItems.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="block font-mono text-xs uppercase tracking-widest text-neutral-400 hover:text-brand-amber py-3 border-b border-white/[0.03]"
|
||||
className="block text-base font-medium text-neutral-200 hover:text-brand-blue py-3 border-b border-white/[0.05]"
|
||||
>
|
||||
{t.nav[link.key]}
|
||||
</a>
|
||||
))}
|
||||
<div className="flex items-center justify-between mt-4 gap-3">
|
||||
<div className="flex items-center justify-between pt-4 gap-3">
|
||||
<LanguageSwitcher />
|
||||
<a
|
||||
href="#download"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="flex-1 text-center px-5 py-2.5 font-mono text-xs uppercase tracking-wider text-white bg-brand-amber rounded-panel shadow-sm"
|
||||
className="flex-1 text-center px-5 py-3 text-sm font-medium text-white bg-brand-blue rounded-xl shadow-glow-sm"
|
||||
>
|
||||
{t.nav.download}
|
||||
</a>
|
||||
|
|
@ -97,3 +103,12 @@ export function Header() {
|
|||
</header>
|
||||
)
|
||||
}
|
||||
function DownloadSmallIcon() {
|
||||
return (
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ export function Hero() {
|
|||
? '이번 프로젝트 배포 일정은 다음 주 금요일로 확정하겠습니다.'
|
||||
: 'The project release is scheduled for next Friday.'
|
||||
|
||||
const heroDataPoints = [
|
||||
{ label: t.hero.dataProcessing, value: '100%', unit: 'local' },
|
||||
{ label: t.hero.dataCloudTraffic, value: '0', unit: 'bytes' },
|
||||
{ label: t.hero.dataLatency, value: '<1.2', unit: 's' },
|
||||
{ label: t.hero.dataPrivacy, value: '10/10', unit: '' },
|
||||
]
|
||||
|
||||
// Waveform animation during recording
|
||||
useEffect(() => {
|
||||
if (simState !== 'recording') return
|
||||
|
|
@ -72,69 +79,69 @@ export function Hero() {
|
|||
}
|
||||
|
||||
return (
|
||||
<section className="relative min-h-screen flex flex-col justify-center overflow-hidden bg-grid pt-28 pb-16">
|
||||
<section className="relative min-h-[90vh] flex flex-col justify-center overflow-hidden bg-grid pt-28 pb-16">
|
||||
{/* Ambient background glow */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-[800px] h-[800px] rounded-full bg-brand-amber/[0.035] blur-[160px]" />
|
||||
<div className="absolute top-1/2 right-10 w-[400px] h-[400px] rounded-full bg-blue-500/[0.02] blur-[140px]" />
|
||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand-amber/20 to-transparent" />
|
||||
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-[800px] h-[800px] rounded-full bg-brand-blue/[0.045] blur-[160px]" />
|
||||
<div className="absolute top-1/2 right-10 w-[500px] h-[500px] rounded-full bg-blue-500/[0.03] blur-[160px]" />
|
||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand-blue/30 to-transparent" />
|
||||
</div>
|
||||
|
||||
<Container className="relative flex-1 flex flex-col justify-center">
|
||||
{/* Hero main grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-8 items-center pt-4 mb-14">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-10 items-center pt-4 mb-14">
|
||||
{/* Left Column: Typography & CTAs (7 cols) */}
|
||||
<div className="lg:col-span-7 max-w-2xl">
|
||||
{/* Status & Protocol Pill Row in Natural Document Flow */}
|
||||
<div className="flex flex-wrap items-center gap-2.5 mb-6">
|
||||
{/* Status & Protocol Pill Row */}
|
||||
<div className="flex flex-wrap items-center gap-2 mb-6">
|
||||
<Badge led ledColor="green">{t.hero.systemStatus}</Badge>
|
||||
<Badge led>{t.hero.badge}</Badge>
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-neutral-400 bg-surface-700/60 px-2.5 py-1 rounded border border-white/[0.06]">
|
||||
ON-DEVICE V3
|
||||
<span className="text-xs font-medium text-neutral-300 bg-surface-800/80 px-3 py-1 rounded-full border border-white/10 shadow-sm">
|
||||
온디바이스 V3
|
||||
</span>
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber bg-brand-amber/10 px-2.5 py-1 rounded border border-brand-amber/20">
|
||||
<span className="text-xs font-medium text-brand-blue bg-brand-blue/10 px-3 py-1 rounded-full border border-brand-blue/25 shadow-sm">
|
||||
{t.hero.protocol}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Main Headline (2 Clean Lines with keep-all) */}
|
||||
{/* Main Headline */}
|
||||
<h1
|
||||
className="font-display text-4xl sm:text-5xl lg:text-[3.4rem] font-bold text-neutral-50 mb-6 tracking-tight leading-[1.18] break-keep"
|
||||
className="text-3xl sm:text-4xl lg:text-[3.25rem] font-medium text-neutral-50 mb-5 tracking-tight leading-[1.18] break-keep"
|
||||
style={{ wordBreak: 'keep-all', overflowWrap: 'break-word' }}
|
||||
>
|
||||
<span className="block text-neutral-100">{t.hero.title1}</span>
|
||||
<span className="block text-brand-amber font-extrabold">{t.hero.title2}</span>
|
||||
<span className="block text-brand-blue font-medium">{t.hero.title2}</span>
|
||||
</h1>
|
||||
|
||||
<p
|
||||
className="text-base md:text-lg text-neutral-400 leading-relaxed mb-8 max-w-xl break-keep"
|
||||
className="text-base sm:text-lg text-neutral-300 leading-relaxed mb-7 max-w-xl break-keep font-normal"
|
||||
style={{ wordBreak: 'keep-all', overflowWrap: 'break-word' }}
|
||||
>
|
||||
{t.hero.subtitle}
|
||||
</p>
|
||||
|
||||
{/* Hotkey Callout */}
|
||||
<div className="flex items-center gap-3 mb-8 p-3 rounded-card bg-surface-700/40 border border-white/[0.05] max-w-md">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-neutral-400">
|
||||
GLOBAL PUSH-TO-TALK:
|
||||
<div className="flex items-center gap-3.5 mb-7 p-3 rounded-xl bg-surface-800/80 border border-white/10 max-w-md shadow-sm">
|
||||
<span className="text-xs font-medium text-neutral-400">
|
||||
글로벌 푸시투톡 단축키:
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 font-mono text-xs font-semibold text-neutral-200">
|
||||
<span className="px-2 py-0.5 rounded bg-surface-500 border border-white/[0.1] shadow-sm">Ctrl</span>
|
||||
<span className="text-neutral-500">+</span>
|
||||
<span className="px-2 py-0.5 rounded bg-surface-500 border border-white/[0.1] shadow-sm">Shift</span>
|
||||
<span className="text-neutral-500">+</span>
|
||||
<span className="px-2.5 py-0.5 rounded bg-brand-amber/20 border border-brand-amber/40 text-brand-amber shadow-sm">Space</span>
|
||||
<div className="flex items-center gap-1.5 text-xs font-medium text-neutral-200">
|
||||
<span className="px-2.5 py-1 rounded-md bg-surface-700 border border-white/10 font-mono text-neutral-200 shadow-sm">Ctrl</span>
|
||||
<span className="text-neutral-400">+</span>
|
||||
<span className="px-2.5 py-1 rounded-md bg-surface-700 border border-white/10 font-mono text-neutral-200 shadow-sm">Shift</span>
|
||||
<span className="text-neutral-400">+</span>
|
||||
<span className="px-3 py-1 rounded-md bg-brand-blue/20 border border-brand-blue/40 text-brand-blue font-mono shadow-sm">Space</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Action Buttons with OS Negotiation */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-4 mb-3">
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3.5 mb-4">
|
||||
<a
|
||||
href={clientOS.downloadUrl}
|
||||
download={clientOS.downloadFilename || undefined}
|
||||
className="glow-btn inline-flex items-center justify-center gap-2.5 px-8 py-4 font-mono text-sm font-semibold uppercase tracking-wider text-white bg-brand-amber rounded-panel shadow-glow-sm hover:shadow-glow-md"
|
||||
className="glow-btn inline-flex items-center justify-center gap-3 px-8 sm:px-9 py-3.5 sm:py-4 text-base font-medium text-white bg-brand-blue rounded-2xl shadow-glow-sm hover:shadow-glow-md active:scale-[0.98] transition-all leading-normal"
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-2.5">
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
{clientOS.os === 'android' ? (
|
||||
<AndroidIcon />
|
||||
) : clientOS.os === 'ios' || clientOS.os === 'macos' ? (
|
||||
|
|
@ -147,7 +154,7 @@ export function Hero() {
|
|||
</a>
|
||||
<a
|
||||
href="#features"
|
||||
className="inline-flex items-center justify-center gap-2 px-7 py-4 font-mono text-sm uppercase tracking-wider text-neutral-300 border border-white/[0.08] rounded-panel hover:border-brand-amber/30 hover:text-white bg-surface-800/40 backdrop-blur-sm transition-all"
|
||||
className="inline-flex items-center justify-center gap-2.5 px-7 sm:px-8 py-3.5 sm:py-4 text-base font-medium text-neutral-200 border border-white/10 rounded-2xl hover:border-brand-blue/40 hover:bg-surface-700 hover:text-white bg-surface-800/60 backdrop-blur-sm transition-all leading-normal"
|
||||
>
|
||||
{t.hero.viewFeatures}
|
||||
<ArrowIcon />
|
||||
|
|
@ -155,21 +162,21 @@ export function Hero() {
|
|||
</div>
|
||||
|
||||
{/* Platform Subtext & Other OS Navigator */}
|
||||
<div className="flex flex-wrap items-center gap-y-2 gap-x-4 mb-3 font-mono text-xs">
|
||||
<span className="text-brand-amber font-medium flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-brand-amber animate-pulse" />
|
||||
<div className="flex flex-wrap items-center gap-y-2 gap-x-4 mb-3.5 text-xs font-normal">
|
||||
<span className="text-brand-blue font-medium flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-brand-blue animate-pulse" />
|
||||
{locale === 'ko' ? clientOS.subtextKo : clientOS.subtextEn}
|
||||
</span>
|
||||
<a
|
||||
href="#download"
|
||||
className="text-neutral-400 hover:text-neutral-200 underline decoration-neutral-600 underline-offset-4 transition-colors"
|
||||
className="text-neutral-400 hover:text-neutral-200 underline decoration-neutral-600 underline-offset-4 transition-colors font-normal"
|
||||
>
|
||||
{locale === 'ko' ? '다른 OS 다운로드 (Win / Mac / Android / iOS) →' : 'Other Platforms (Win / Mac / Android / iOS) →'}
|
||||
{locale === 'ko' ? '플랫폼별 릴리스 준비 상태 →' : 'Platform release readiness →'}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Friction-Reduction Trust Microcopy */}
|
||||
<div className="flex items-center gap-2 font-mono text-nano text-neutral-500">
|
||||
<div className="flex items-center gap-2 text-xs text-neutral-400 font-normal">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
||||
<span>{locale === 'ko' ? '영구 무료 · 회원가입 불필요 · 100% 오프라인 작동' : 'Free Forever · No Signup Required · 100% Offline'}</span>
|
||||
</div>
|
||||
|
|
@ -177,23 +184,23 @@ export function Hero() {
|
|||
|
||||
{/* Right Column: Live Interactive Voice Capsule (5 cols) */}
|
||||
<div className="lg:col-span-5">
|
||||
<div className="relative p-6 md:p-7 rounded-2xl bg-surface-800/90 border border-white/[0.08] shadow-2xl backdrop-blur-2xl noise-texture">
|
||||
<div className="relative p-6 md:p-7 rounded-3xl bg-surface-800/90 border border-white/10 shadow-glass-card backdrop-blur-2xl noise-texture">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-5 pb-3 border-b border-white/[0.06]">
|
||||
<div className="flex items-center justify-between mb-4 pb-3 border-b border-white/[0.08]">
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color={simState === 'recording' ? 'amber' : simState === 'processing' ? 'amber' : 'green'} pulse={simState !== 'idle'} />
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-neutral-300">
|
||||
{simState === 'idle' ? 'INTERACTIVE SIMULATOR' : simState === 'recording' ? 'LISTENING & STREAMING' : simState === 'processing' ? 'AI POLISHING...' : 'PIPELINE COMPLETED'}
|
||||
<Led color={simState === 'recording' ? 'blue' : simState === 'processing' ? 'blue' : 'green'} pulse={simState !== 'idle'} />
|
||||
<span className="text-xs font-medium text-neutral-200">
|
||||
{simState === 'idle' ? '인터랙티브 시뮬레이터' : simState === 'recording' ? '음성 듣는 중...' : simState === 'processing' ? 'AI 문장 정제 중...' : '전사 & 다듬기 완료'}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-mono text-nano text-neutral-500 uppercase">
|
||||
ZERO CLOUD EGRESS
|
||||
<span className="text-xs font-medium text-emerald-400 bg-emerald-500/10 px-2.5 py-0.5 rounded-full border border-emerald-500/20">
|
||||
클라우드 유출 0바이트
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Interactive Audio Capsule UI */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between p-4 rounded-xl bg-surface-950 border border-white/[0.06] shadow-inner">
|
||||
<div className="flex flex-col gap-3.5">
|
||||
<div className="flex items-center justify-between p-3.5 rounded-xl bg-surface-950 border border-white/10 shadow-inner">
|
||||
{/* Waveform Bars */}
|
||||
<div className="flex items-center gap-1.5 h-8">
|
||||
{waveLevels.map((lvl, idx) => (
|
||||
|
|
@ -202,8 +209,8 @@ export function Hero() {
|
|||
className="w-1.5 rounded-full transition-all duration-75"
|
||||
style={{
|
||||
height: `${Math.max(6, lvl * 32)}px`,
|
||||
backgroundColor: simState === 'recording' ? '#f25b29' : 'rgba(255,255,255,0.2)',
|
||||
boxShadow: simState === 'recording' ? '0 0 8px rgba(242,91,41,0.6)' : 'none',
|
||||
backgroundColor: simState === 'recording' ? '#3b82f6' : 'rgba(255,255,255,0.25)',
|
||||
boxShadow: simState === 'recording' ? '0 0 10px rgba(59,130,246,0.7)' : 'none',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -211,71 +218,67 @@ export function Hero() {
|
|||
|
||||
{/* Trigger Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={simState === 'idle' || simState === 'done' ? startSimulation : resetSimulation}
|
||||
className={`px-4 py-2 rounded-lg font-mono text-xs font-semibold uppercase tracking-wider transition-all flex items-center gap-2 ${
|
||||
className={`px-4 py-2 rounded-xl text-xs font-medium transition-all flex items-center gap-2 shadow-sm ${
|
||||
simState === 'recording'
|
||||
? 'bg-red-500/20 text-red-400 border border-red-500/30 animate-pulse'
|
||||
: 'bg-brand-amber text-white hover:bg-brand-amber-light shadow-glow-sm'
|
||||
: 'bg-brand-blue text-white hover:bg-brand-blue-light shadow-glow-sm active:scale-[0.98]'
|
||||
}`}
|
||||
>
|
||||
<MicIcon />
|
||||
{simState === 'idle' ? 'Try Demo' : simState === 'recording' ? 'Listening...' : simState === 'processing' ? 'Polishing' : 'Test Again'}
|
||||
<MicSmallIcon />
|
||||
<span>{simState === 'recording' ? (locale === 'ko' ? '녹음 중지' : 'Stop') : simState === 'processing' ? (locale === 'ko' ? '처리 중...' : 'Polishing...') : (locale === 'ko' ? '데모 시작' : 'Try Demo')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Real-time Transcription Stream */}
|
||||
<div className="min-h-[110px] p-4 rounded-xl bg-surface-900 border border-white/[0.04] flex flex-col justify-center">
|
||||
{/* Real-time Streaming Transcript Output Area */}
|
||||
<div className="min-h-[140px] p-4 rounded-xl bg-surface-950/80 border border-white/10 flex flex-col justify-center">
|
||||
{simState === 'idle' && (
|
||||
<div className="text-center py-3">
|
||||
<p className="text-xs font-mono text-neutral-500">
|
||||
👆 Click 'Try Demo' to watch on-device speech-to-text & AI polish in action
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs sm:text-sm text-neutral-400 text-center leading-relaxed font-normal">
|
||||
👆 <strong className="text-neutral-200 font-medium">{locale === 'ko' ? "'데모 시작'" : "'Try Demo'"}</strong>{locale === 'ko' ? ' 버튼을 클릭하여 로컬 음성 전사 및 AI 다듬기 과정을 체험해보세요.' : ' to watch on-device speech-to-text & AI polish in action.'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{(simState === 'recording' || simState === 'processing') && (
|
||||
<div className="space-y-1.5">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/80 flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-brand-amber animate-ping" />
|
||||
Live Transcription:
|
||||
</span>
|
||||
<p className="text-sm font-sans text-neutral-200 leading-relaxed font-medium">
|
||||
{typedRaw}
|
||||
<span className="inline-block w-2 h-4 ml-1 bg-brand-amber animate-pulse align-middle" />
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs font-medium text-brand-blue">
|
||||
<span className="w-2 h-2 rounded-full bg-brand-blue animate-ping" />
|
||||
<span>{locale === 'ko' ? '실시간 음성 전사 (faster-whisper)' : 'Streaming STT (faster-whisper)'}</span>
|
||||
</div>
|
||||
<p className="text-sm sm:text-base text-neutral-100 font-normal leading-relaxed">
|
||||
"{typedRaw}"
|
||||
<span className="inline-block w-1.5 h-4 ml-1 bg-brand-blue animate-pulse align-middle" />
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{simState === 'done' && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-emerald-400 flex items-center gap-1">
|
||||
✓ AI Polish & Context Applied:
|
||||
</span>
|
||||
<span className="font-mono text-nano text-neutral-500">
|
||||
Inference: 0.84s
|
||||
</span>
|
||||
<div className="space-y-3">
|
||||
<div className="p-2 rounded-lg bg-surface-900 border border-white/[0.05]">
|
||||
<span className="text-[11px] text-neutral-400 block mb-0.5">{locale === 'ko' ? '원문 입력' : 'Raw Audio'}</span>
|
||||
<p className="text-xs text-neutral-400 line-through font-normal leading-normal">{sampleRawText}</p>
|
||||
</div>
|
||||
<div className="p-2.5 rounded-lg bg-emerald-500/10 border border-emerald-500/25">
|
||||
<span className="text-xs font-medium text-emerald-400 block mb-0.5">✨ {locale === 'ko' ? 'AI 완벽 정제본 (Ollama 로컬 LLM)' : 'Polished by Local LLM'}</span>
|
||||
<p className="text-sm font-medium text-emerald-200 leading-relaxed">{typedClean}</p>
|
||||
</div>
|
||||
<p className="text-sm font-sans text-emerald-300 leading-relaxed font-medium bg-emerald-500/10 p-2.5 rounded-lg border border-emerald-500/20">
|
||||
"{typedClean}"
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Telemetry Hardware Stats */}
|
||||
<div className="grid grid-cols-3 gap-2 pt-2 border-t border-white/[0.04] font-mono text-nano text-neutral-500">
|
||||
{/* Telemetry Stats Bar */}
|
||||
<div className="grid grid-cols-3 gap-2 pt-2 border-t border-white/[0.06] text-xs font-normal text-neutral-400">
|
||||
<div>
|
||||
<span className="block text-neutral-600">STT ENGINE</span>
|
||||
<span className="text-neutral-300 font-semibold">faster-whisper</span>
|
||||
<span className="block text-[11px] text-neutral-500">STT 엔진</span>
|
||||
<span className="font-mono text-neutral-300">faster-whisper</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-neutral-600">LLM MODEL</span>
|
||||
<span className="text-neutral-300 font-semibold">Ollama Local</span>
|
||||
<span className="block text-[11px] text-neutral-500">LLM 모델</span>
|
||||
<span className="font-mono text-neutral-300">Ollama Local</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-neutral-600">CLOUD LEAK</span>
|
||||
<span className="text-emerald-400 font-semibold">0.00 KB</span>
|
||||
<span className="block text-[11px] text-neutral-500">클라우드 유출</span>
|
||||
<span className="font-mono text-emerald-400 font-medium">0.00 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -283,55 +286,42 @@ export function Hero() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Data Telemetry Strip */}
|
||||
<div className="border-t border-white/[0.06] pt-8 mt-auto">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||
<DataPoint label={t.hero.dataProcessing} value="100%" unit="local" />
|
||||
<DataPoint label={t.hero.dataCloudTraffic} value="0" unit="bytes" />
|
||||
<DataPoint label={t.hero.dataLatency} value="<1.2s" />
|
||||
<DataPoint label={t.hero.dataPrivacy} value="10/10" />
|
||||
</div>
|
||||
{/* Bottom DataPoints Row */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 pt-10 border-t border-white/10">
|
||||
{heroDataPoints.map((dp, i) => (
|
||||
<DataPoint key={i} label={dp.label} value={dp.value} unit={dp.unit} />
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function AndroidIcon() {
|
||||
function WindowsIcon() {
|
||||
return (
|
||||
<svg className="w-4 h-4 text-emerald-400" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.996-3.4572a.416.416 0 0 0-.1521-.5676.416.416 0 0 0-.5676.1521l-2.0223 3.503C15.5902 8.411 13.856 8.125 12 8.125s-3.5902.286-5.1315.8267L4.8462 5.4487a.4161.4161 0 0 0-.5677-.1521.4157.4157 0 0 0-.1521.5676l1.996 3.4572C2.6889 11.1867.3432 14.6589 0 18.775h24c-.3432-4.1161-2.6889-7.5883-6.1185-9.4536" />
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.9-1.801" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function AppleIcon() {
|
||||
return (
|
||||
<svg className="w-4 h-4 text-neutral-200" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M15.97 6.37c.61-.75 1.04-1.8 1.01-2.87-.96.04-2.13.64-2.79 1.43-.58.68-.99 1.74-.94 2.82 1.08.08 2.11-.63 2.72-1.38z" />
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M15.97 6.37c.61-.75 1.04-1.8 1.01-2.87-.96.04-2.12.64-2.77 1.41-.58.68-1.08 1.74-.95 2.78 1.07.08 2.14-.54 2.71-1.32z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function WindowsIcon() {
|
||||
function AndroidIcon() {
|
||||
return (
|
||||
<svg className="w-4 h-4 text-sky-400" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.551H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-13.051-1.851" />
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4111 13.8533 8.0805 12 8.0805s-3.5902.3306-5.1367.8692L4.841 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ArrowIcon() {
|
||||
return (
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="7" y1="17" x2="17" y2="7" />
|
||||
<polyline points="7 7 17 7 17 17" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MicIcon() {
|
||||
function MicSmallIcon() {
|
||||
return (
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="9" y="2" width="6" height="11" rx="3" />
|
||||
|
|
@ -341,3 +331,11 @@ function MicIcon() {
|
|||
)
|
||||
}
|
||||
|
||||
function ArrowIcon() {
|
||||
return (
|
||||
<svg className="w-3.5 h-3.5 text-neutral-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="7" y1="17" x2="17" y2="7" />
|
||||
<polyline points="7 7 17 7 17 17" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export function HowItWorks() {
|
|||
const current = presets[activePreset]
|
||||
|
||||
return (
|
||||
<section id="pipeline" className="relative py-24 md:py-32">
|
||||
<section id="pipeline" className="relative py-20 md:py-28">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-surface-800/40 to-transparent pointer-events-none" />
|
||||
|
||||
<Container className="relative">
|
||||
|
|
@ -74,122 +74,107 @@ export function HowItWorks() {
|
|||
/>
|
||||
|
||||
{/* Preset Selector Bar */}
|
||||
<div className="flex items-center justify-center gap-2 mb-10 overflow-x-auto pb-2">
|
||||
<div className="flex items-center justify-center gap-2.5 mb-10 overflow-x-auto pb-2">
|
||||
{presets.map((p, idx) => (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
onClick={() => setActivePreset(idx)}
|
||||
className={`px-4 py-2 rounded-panel font-mono text-xs uppercase tracking-wider transition-all flex items-center gap-2 ${
|
||||
className={`px-4.5 py-2 rounded-xl text-xs sm:text-sm font-medium transition-all flex items-center gap-2 shadow-sm active:scale-[0.98] ${
|
||||
activePreset === idx
|
||||
? 'bg-brand-amber text-white shadow-glow-sm font-semibold'
|
||||
: 'bg-surface-700/60 text-neutral-400 hover:text-neutral-200 border border-white/[0.04]'
|
||||
? 'bg-brand-blue text-white shadow-glow-sm font-medium'
|
||||
: 'bg-surface-800/80 text-neutral-300 hover:text-white hover:bg-surface-700 border border-white/10'
|
||||
}`}
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-current" />
|
||||
<span className={`w-2 h-2 rounded-full ${activePreset === idx ? 'bg-white' : 'bg-brand-blue'}`} />
|
||||
{p.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-start">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10 lg:gap-12 items-start">
|
||||
{/* Left: Interactive CRT Studio Screen */}
|
||||
<Crosshair className="order-2 lg:order-1">
|
||||
<div className="crt-screen rounded-card p-6 md:p-8 aspect-[4/3] flex flex-col justify-between noise-texture shadow-2xl">
|
||||
<div className="relative z-10 flex items-center justify-between mb-4">
|
||||
<div className="crt-screen rounded-3xl p-6 md:p-7 flex flex-col justify-between noise-texture shadow-2xl border border-white/15">
|
||||
<div className="relative z-10 flex items-center justify-between mb-4 pb-3 border-b border-white/[0.08]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/80">
|
||||
{current.tag} PIPELINE
|
||||
<span className="text-xs font-medium text-brand-blue">
|
||||
{current.tag} 파이프라인
|
||||
</span>
|
||||
<span className="font-mono text-nano text-neutral-500 bg-surface-600/60 px-2 py-0.5 rounded border border-white/[0.04]">
|
||||
<span className="font-mono text-xs text-neutral-300 bg-surface-700/80 px-2 py-0.5 rounded-md border border-white/10">
|
||||
{current.model}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color="green" pulse />
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-emerald-400">{t.pipeline.panelActive}</span>
|
||||
<span className="text-xs font-medium text-emerald-400">{t.pipeline.panelActive}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex-1 flex flex-col justify-center space-y-4">
|
||||
<div className="relative z-10 flex-1 flex flex-col justify-center space-y-3.5">
|
||||
{/* Simulated live pipeline flow */}
|
||||
<div className="font-mono text-xs text-neutral-500 leading-relaxed space-y-3">
|
||||
<div className="flex items-center justify-between text-neutral-400 bg-surface-950/60 p-2.5 rounded border border-white/[0.04]">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-xs text-neutral-400 leading-relaxed space-y-3 font-normal">
|
||||
<div className="flex items-center justify-between text-neutral-200 bg-surface-950/80 p-3 rounded-xl border border-white/10">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Led color="green" size="sm" />
|
||||
<span className="text-emerald-400/90 font-semibold">{t.pipeline.sttReady}</span>
|
||||
<span className="text-neutral-500">faster-whisper base</span>
|
||||
<span className="text-emerald-400 font-medium text-xs">{t.pipeline.sttReady}</span>
|
||||
</div>
|
||||
<span className="text-nano text-neutral-600 font-mono">0ms</span>
|
||||
<WaveBars className="h-4" />
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-lg bg-surface-900/90 border border-white/[0.06] space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/70 flex items-center gap-1.5">
|
||||
<Led color="amber" size="sm" pulse />
|
||||
Raw Voice STT Output:
|
||||
</span>
|
||||
<span className="font-mono text-nano text-neutral-600">~0.42s</span>
|
||||
<div className="p-3.5 rounded-xl bg-surface-950/90 border border-white/10 space-y-2">
|
||||
<div className="flex items-center justify-between text-[11px] text-neutral-400 pb-1.5 border-b border-white/[0.06]">
|
||||
<span>1단계: RAW 음성 전사 (faster-whisper)</span>
|
||||
<span className="font-mono text-neutral-400">~0.42s</span>
|
||||
</div>
|
||||
<p className="text-sm font-sans text-neutral-300 font-medium">
|
||||
"{current.raw}"
|
||||
<p className="text-neutral-300 font-normal leading-normal italic text-xs">
|
||||
"{current.raw}"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-lg bg-emerald-500/10 border border-emerald-500/20 space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-emerald-400 flex items-center gap-1.5">
|
||||
<Led color="green" size="sm" />
|
||||
Transformed & Injected to Active Window:
|
||||
</span>
|
||||
<span className="font-mono text-nano text-emerald-400/80 font-bold">{current.latency}</span>
|
||||
<div className="p-3.5 rounded-xl bg-brand-blue/[0.08] border border-brand-blue/30 space-y-2">
|
||||
<div className="flex items-center justify-between text-[11px] text-brand-blue pb-1.5 border-b border-brand-blue/20">
|
||||
<span className="font-medium">2단계: LLM 문맥 정제 및 커서 자동 삽입</span>
|
||||
<span className="font-mono text-emerald-400 font-medium">{current.latency}</span>
|
||||
</div>
|
||||
<p className="text-sm font-sans text-emerald-200 font-semibold">
|
||||
"{current.clean}"
|
||||
<p className="text-white font-medium leading-relaxed text-sm">
|
||||
"{current.clean}"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex items-center justify-between pt-4 border-t border-white/[0.04]">
|
||||
<WaveBars className="h-6" />
|
||||
<span className="font-mono text-nano text-neutral-400">
|
||||
Total Engine Latency: <strong className="text-white">{current.latency}</strong> (Zero Cloud Traffic)
|
||||
</span>
|
||||
<div className="pt-3 border-t border-white/[0.08] flex items-center justify-between text-xs text-neutral-400 font-normal">
|
||||
<span>총 레이턴시: <strong className="text-emerald-400 font-mono font-medium">{current.latency}</strong></span>
|
||||
<span className="text-emerald-400 font-medium">클라우드 유출 0바이트</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Crosshair>
|
||||
|
||||
{/* Right: Numbered Architecture Steps */}
|
||||
<div className="order-1 lg:order-2 space-y-2">
|
||||
{/* Right: Architecture Steps Breakdown */}
|
||||
<div className="order-1 lg:order-2 space-y-4">
|
||||
{t.pipeline.steps.map((step, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="group relative flex gap-5 p-5 rounded-card border border-white/[0.04] bg-surface-700/20 hover:border-brand-amber/30 hover:bg-surface-700/40 transition-all"
|
||||
className="group relative p-5 rounded-2xl bg-surface-800/80 border border-white/10 hover:border-brand-blue/40 hover:bg-surface-700/80 shadow-glass-card transition-all flex gap-4 backdrop-blur-xl"
|
||||
>
|
||||
{/* Number */}
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-card bg-surface-600 border border-white/[0.06] flex items-center justify-center shadow-sm">
|
||||
<span className="font-mono text-lg font-bold text-brand-amber">{String(i + 1).padStart(2, '0')}</span>
|
||||
<div className="w-11 h-11 rounded-xl bg-surface-700 border border-white/10 flex items-center justify-center flex-shrink-0 text-brand-blue font-medium text-sm group-hover:border-brand-blue/40 group-hover:bg-brand-blue/10 transition-colors shadow-sm">
|
||||
{String(i + 1).padStart(2, '0')}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/70 block mb-1">
|
||||
{step.label}
|
||||
</span>
|
||||
<h3 className="font-display text-lg font-semibold text-neutral-100 mb-1.5">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-[11px] text-brand-blue font-medium">{step.label}</span>
|
||||
</div>
|
||||
<h3 className="text-base sm:text-lg font-medium text-neutral-50 mb-1.5 tracking-tight group-hover:text-white transition-colors">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-sm text-neutral-400 leading-relaxed mb-2">
|
||||
<p className="text-sm text-neutral-300 leading-relaxed font-normal">
|
||||
{step.description}
|
||||
</p>
|
||||
<span className="inline-block font-mono text-nano text-neutral-400 bg-surface-600/80 px-2.5 py-1 rounded-sm border border-white/[0.05]">
|
||||
<span className="mt-2.5 inline-block text-xs text-neutral-400 font-mono bg-surface-900/60 px-2.5 py-0.5 rounded border border-white/[0.05]">
|
||||
{step.detail}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Connector */}
|
||||
{i < t.pipeline.steps.length - 1 && (
|
||||
<div className="absolute left-[42px] top-[72px] w-px h-6 bg-gradient-to-b from-brand-amber/30 to-transparent hidden lg:block" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function CellValue({ value }: { value: string | boolean }) {
|
|||
if (value === true) {
|
||||
return (
|
||||
<span className="flex items-center justify-center">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.8)]" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.9)]" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ function CellValue({ value }: { value: string | boolean }) {
|
|||
</span>
|
||||
)
|
||||
}
|
||||
return <span className="font-mono text-xs text-neutral-300">{value}</span>
|
||||
return <span className="text-xs font-medium text-neutral-200">{value}</span>
|
||||
}
|
||||
|
||||
const PRICES = {
|
||||
|
|
@ -112,8 +112,8 @@ export function Pricing() {
|
|||
id: 'team',
|
||||
name: locale === 'ko' ? 'Team' : 'Team',
|
||||
price: `${currencySymbol}${formatPrice(teamPrice)}`,
|
||||
period: isAnnual ? `${t.pricing.annual} / ${locale === 'ko' ? '인' : 'seat'}` : `${t.pricing.monthly} / ${locale === 'ko' ? '인' : 'seat'}`,
|
||||
subPrice: isAnnual ? `${currencySymbol}${formatPrice(Math.round(activePrices.team.annual / 12))}${t.pricing.perMonth} / ${locale === 'ko' ? '인' : 'seat'}` : null,
|
||||
period: isAnnual ? `${periodLabel} (1인)` : `${periodLabel} (1인)`,
|
||||
subPrice: isAnnual ? `${currencySymbol}${formatPrice(Math.round(activePrices.team.annual / 12))}${t.pricing.perMonth}` : null,
|
||||
desc: locale === 'ko' ? '팀 공유 사전 & 지식 협업 & 엔터프라이즈 거버넌스' : 'Team shared dictionaries, knowledge & governance',
|
||||
popular: false,
|
||||
buttonText: locale === 'ko' ? '팀 도입 문의' : 'Contact Sales',
|
||||
|
|
@ -129,7 +129,7 @@ export function Pricing() {
|
|||
]
|
||||
|
||||
return (
|
||||
<section id="pricing" className="relative py-24 md:py-32">
|
||||
<section id="pricing" className="relative py-20 md:py-28">
|
||||
<Container>
|
||||
<SectionHeader
|
||||
index={t.pricing.index}
|
||||
|
|
@ -138,21 +138,23 @@ export function Pricing() {
|
|||
/>
|
||||
|
||||
{/* Reverse Trial Banner */}
|
||||
<div className="mb-10 p-4 rounded-xl bg-gradient-to-r from-brand-amber/20 via-surface-700/60 to-brand-amber/20 border border-brand-amber/40 shadow-glow-sm flex flex-col md:flex-row items-center justify-between gap-4 max-w-4xl mx-auto">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl">✨</span>
|
||||
<div className="mb-12 px-6 sm:px-8 py-5 sm:py-6 rounded-2xl sm:rounded-3xl bg-gradient-to-r from-brand-blue/15 via-surface-800/90 to-brand-blue/15 border border-brand-blue/40 shadow-glow-sm flex flex-col md:flex-row items-start md:items-center justify-between gap-5 max-w-4xl mx-auto backdrop-blur-xl">
|
||||
<div className="flex items-center gap-4 sm:gap-5">
|
||||
<div className="w-10 h-10 rounded-2xl bg-brand-blue/20 border border-brand-blue/40 flex items-center justify-center text-brand-blue flex-shrink-0 shadow-sm text-lg">
|
||||
✨
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="font-mono text-xs font-bold uppercase tracking-wider text-brand-amber">
|
||||
<p className="text-sm sm:text-base font-medium text-brand-blue">
|
||||
{locale === 'ko' ? '14일 Reverse-Trial 무료 체험' : '14-Day Free Reverse Trial'}
|
||||
</p>
|
||||
<p className="text-xs text-neutral-300">
|
||||
<p className="text-xs sm:text-sm text-neutral-300 mt-1 font-normal leading-relaxed">
|
||||
{locale === 'ko'
|
||||
? '앱 설치 즉시 신용카드 등록 없이 Pro+의 모든 AI 기능을 14일 동안 무료로 체험할 수 있습니다.'
|
||||
: 'Experience full Pro+ capabilities for 14 days immediately after install — no credit card required.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="px-3 py-1 rounded-full bg-brand-amber text-neutral-950 font-mono text-[10px] font-bold uppercase tracking-widest whitespace-nowrap">
|
||||
<span className="px-3.5 py-1.5 rounded-full bg-brand-blue/20 text-brand-blue border border-brand-blue/35 text-xs font-medium whitespace-nowrap shadow-sm self-start md:self-auto flex-shrink-0">
|
||||
{locale === 'ko' ? '자동 활성화' : 'Zero Friction'}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -160,23 +162,25 @@ export function Pricing() {
|
|||
{/* Currency and Billing Toggles */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 mb-12">
|
||||
{/* Currency Toggle */}
|
||||
<div className="flex items-center rounded-panel bg-surface-800 p-1 border border-white/[0.08]">
|
||||
<div className="flex items-center rounded-xl bg-surface-800 p-1 border border-white/10 shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrency('usd')}
|
||||
className={`font-mono text-xs px-3.5 py-1.5 rounded transition-all ${
|
||||
className={`text-xs px-3.5 py-1.5 rounded-lg transition-all ${
|
||||
currency === 'usd'
|
||||
? 'bg-surface-500 text-neutral-100 font-bold shadow-sm'
|
||||
: 'text-neutral-500 hover:text-neutral-300'
|
||||
? 'bg-surface-600 text-neutral-50 font-medium shadow-sm'
|
||||
: 'text-neutral-400 hover:text-white font-normal'
|
||||
}`}
|
||||
>
|
||||
USD ($)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrency('krw')}
|
||||
className={`font-mono text-xs px-3.5 py-1.5 rounded transition-all ${
|
||||
className={`text-xs px-3.5 py-1.5 rounded-lg transition-all ${
|
||||
currency === 'krw'
|
||||
? 'bg-surface-500 text-neutral-100 font-bold shadow-sm'
|
||||
: 'text-neutral-500 hover:text-neutral-300'
|
||||
? 'bg-surface-600 text-neutral-50 font-medium shadow-sm'
|
||||
: 'text-neutral-400 hover:text-white font-normal'
|
||||
}`}
|
||||
>
|
||||
KRW (₩)
|
||||
|
|
@ -184,28 +188,30 @@ export function Pricing() {
|
|||
</div>
|
||||
|
||||
{/* Billing Cycle Toggle */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsAnnual(false)}
|
||||
className={`font-mono text-xs uppercase tracking-widest px-5 py-2.5 rounded-panel transition-all ${
|
||||
className={`text-xs px-4 py-2 rounded-xl transition-all ${
|
||||
!isAnnual
|
||||
? 'bg-surface-500 text-neutral-100 border border-white/[0.12] shadow-sm'
|
||||
: 'text-neutral-500 hover:text-neutral-300'
|
||||
? 'bg-surface-600 text-neutral-50 border border-white/15 shadow-sm font-medium'
|
||||
: 'text-neutral-400 hover:text-white border border-transparent font-normal'
|
||||
}`}
|
||||
>
|
||||
{t.pricing.billingToggleMonthly}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsAnnual(true)}
|
||||
className={`font-mono text-xs uppercase tracking-widest px-5 py-2.5 rounded-panel transition-all relative ${
|
||||
className={`text-xs px-4 py-2 rounded-xl transition-all relative ${
|
||||
isAnnual
|
||||
? 'bg-surface-500 text-neutral-100 border border-brand-amber/40 shadow-glow-sm'
|
||||
: 'text-neutral-500 hover:text-neutral-300'
|
||||
? 'bg-surface-600 text-neutral-50 border border-brand-blue/40 shadow-glow-sm font-medium'
|
||||
: 'text-neutral-400 hover:text-white border border-transparent font-normal'
|
||||
}`}
|
||||
>
|
||||
{t.pricing.billingToggleAnnual}
|
||||
{isAnnual && (
|
||||
<span className="absolute -top-2.5 -right-2.5 px-2 py-0.5 rounded-full bg-brand-amber text-white text-[9px] font-mono font-bold uppercase tracking-wider shadow-sm">
|
||||
<span className="absolute -top-2 -right-2 px-2 py-0.5 rounded-full bg-brand-blue text-white text-[10px] font-medium shadow-sm">
|
||||
{t.pricing.savePercent}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -214,128 +220,98 @@ export function Pricing() {
|
|||
</div>
|
||||
|
||||
{/* 4 Tier Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5 mb-16">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 xl:gap-5 mb-14">
|
||||
{tiers.map((tier) => (
|
||||
<div
|
||||
key={tier.id}
|
||||
className={`relative rounded-2xl p-6 flex flex-col justify-between noise-texture transition-all duration-300 ${
|
||||
className={`relative rounded-3xl p-5 xl:p-6 flex flex-col justify-between noise-texture backdrop-blur-xl transition-all duration-300 ${
|
||||
tier.popular
|
||||
? 'bg-surface-700/70 border-2 border-brand-amber shadow-glow-md'
|
||||
: 'bg-surface-700/40 border border-white/[0.06] hover:border-white/[0.15]'
|
||||
? 'bg-surface-800/90 border-2 border-brand-blue shadow-glow-md'
|
||||
: 'bg-surface-800/70 border border-white/10 hover:border-brand-blue/35 hover:bg-surface-700/80 shadow-glass-card'
|
||||
}`}
|
||||
>
|
||||
{tier.popular && (
|
||||
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2">
|
||||
<span className="px-3.5 py-1 rounded-full bg-brand-amber text-white text-nano font-mono font-bold uppercase tracking-widest shadow-md">
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
|
||||
<span className="px-3.5 py-0.5 rounded-full bg-brand-blue text-white text-xs font-medium shadow-md">
|
||||
{t.pricing.popular}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className={`font-mono text-xs font-bold uppercase tracking-widest ${tier.popular ? 'text-brand-amber' : 'text-neutral-400'}`}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-lg font-medium text-neutral-50">
|
||||
{tier.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 flex items-baseline gap-1.5">
|
||||
<span className="font-display text-3xl font-extrabold text-neutral-50">
|
||||
{tier.price}
|
||||
</span>
|
||||
<span className="font-sans text-xs text-neutral-400 font-normal">
|
||||
{tier.period}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{tier.subPrice && (
|
||||
<div className="font-mono text-nano text-neutral-500 mb-4">
|
||||
{tier.subPrice}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-neutral-400 mb-5 leading-relaxed min-h-[38px]">
|
||||
<p className="text-xs text-neutral-300 min-h-[36px] mb-4 font-normal leading-relaxed">
|
||||
{tier.desc}
|
||||
</p>
|
||||
|
||||
<div className="h-px bg-white/[0.06] mb-5" />
|
||||
<div className="mb-5 pb-4 border-b border-white/[0.08]">
|
||||
<div className="flex items-baseline flex-wrap gap-1.5">
|
||||
<span className="text-2xl sm:text-[1.65rem] xl:text-[1.85rem] font-medium text-neutral-50 tracking-tight whitespace-nowrap">
|
||||
{tier.price}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-400 font-normal whitespace-nowrap">
|
||||
{tier.period}
|
||||
</span>
|
||||
</div>
|
||||
{tier.subPrice && (
|
||||
<span className="block text-xs text-neutral-400 font-normal mt-1">
|
||||
(월 {tier.subPrice})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul className="space-y-2.5 mb-7">
|
||||
{tier.features.map((feat, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2.5 text-xs text-neutral-300">
|
||||
<span className="text-emerald-400 font-bold mt-0.5">✓</span>
|
||||
<span>{feat}</span>
|
||||
<ul className="space-y-2.5 mb-6 text-xs sm:text-sm text-neutral-300 font-normal">
|
||||
{tier.features.map((feat, i) => (
|
||||
<li key={i} className="flex items-start gap-2">
|
||||
<span className="text-emerald-400 font-medium flex-shrink-0 mt-0.5">✓</span>
|
||||
<span className="leading-snug">{feat}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<GlowButton href="#" variant={tier.buttonVariant} size="md" className="w-full text-xs">
|
||||
<GlowButton
|
||||
href="#download"
|
||||
variant={tier.buttonVariant}
|
||||
size="md"
|
||||
className="w-full text-xs sm:text-sm font-medium py-3 sm:py-3.5"
|
||||
>
|
||||
{tier.buttonText}
|
||||
</GlowButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Detailed Comparison Table */}
|
||||
{/* Feature Comparison Matrix */}
|
||||
<div className="overflow-x-auto -mx-5 md:mx-0">
|
||||
<table className="w-full min-w-[700px] border-collapse bg-surface-800/30 rounded-xl overflow-hidden border border-white/[0.04]">
|
||||
{/* Header */}
|
||||
<table className="w-full min-w-[700px] text-left border-collapse rounded-2xl overflow-hidden bg-surface-800/60 border border-white/10 shadow-glass-card backdrop-blur-xl">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.08] bg-surface-700/50">
|
||||
<th className="text-left py-4 px-5 font-mono text-nano uppercase tracking-widest text-neutral-400 w-[35%]">
|
||||
{t.pricing.featureLabel}
|
||||
</th>
|
||||
<th className="text-center py-4 px-3 w-[15%] font-mono text-nano uppercase tracking-widest text-neutral-300">
|
||||
{t.pricing.free}
|
||||
</th>
|
||||
<th className="text-center py-4 px-3 w-[18%] font-mono text-nano uppercase tracking-widest text-brand-amber bg-brand-amber/[0.04]">
|
||||
{t.pricing.pro}
|
||||
</th>
|
||||
<th className="text-center py-4 px-3 w-[16%] font-mono text-nano uppercase tracking-widest text-neutral-300">
|
||||
{t.pricing.proPlus}
|
||||
</th>
|
||||
<th className="text-center py-4 px-3 w-[16%] font-mono text-nano uppercase tracking-widest text-neutral-300">
|
||||
Team
|
||||
</th>
|
||||
<tr className="border-b border-white/10 bg-surface-700/80 text-xs text-neutral-200 font-medium">
|
||||
<th className="p-4 w-2/5">기능 명세</th>
|
||||
<th className="p-4 text-center w-[15%]">FREE</th>
|
||||
<th className="p-4 text-center w-[15%] text-brand-blue font-medium">PRO</th>
|
||||
<th className="p-4 text-center w-[15%] text-purple-300">PRO+</th>
|
||||
<th className="p-4 text-center w-[15%]">TEAM</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{/* Body */}
|
||||
<tbody>
|
||||
{t.pricing.rows.map((row, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className={`border-b border-white/[0.03] hover:bg-surface-700/30 transition-colors ${
|
||||
i % 2 === 0 ? 'bg-transparent' : 'bg-surface-800/20'
|
||||
}`}
|
||||
>
|
||||
<td className="py-3 px-5 font-sans text-xs text-neutral-300 font-medium">
|
||||
{row.feature}
|
||||
</td>
|
||||
<td className="py-3 px-3 text-center">
|
||||
<CellValue value={row.free} />
|
||||
</td>
|
||||
<td className="py-3 px-3 text-center bg-brand-amber/[0.04]">
|
||||
<CellValue value={row.pro} />
|
||||
</td>
|
||||
<td className="py-3 px-3 text-center">
|
||||
<CellValue value={row.proPlus} />
|
||||
</td>
|
||||
<td className="py-3 px-3 text-center">
|
||||
<CellValue value={true} />
|
||||
</td>
|
||||
<tbody className="divide-y divide-white/[0.05] text-xs sm:text-sm">
|
||||
{t.pricing.rows.map((row, idx) => (
|
||||
<tr key={idx} className="hover:bg-surface-700/30 transition-colors">
|
||||
<td className="p-3.5 text-neutral-300 font-normal">{row.feature}</td>
|
||||
<td className="p-3.5 text-center text-neutral-400"><CellValue value={row.free} /></td>
|
||||
<td className="p-3.5 text-center bg-brand-blue/[0.03]"><CellValue value={row.pro} /></td>
|
||||
<td className="p-3.5 text-center bg-purple-500/[0.03]"><CellValue value={row.proPlus} /></td>
|
||||
<td className="p-3.5 text-center"><CellValue value={row.proPlus} /></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p className="text-center font-mono text-nano text-neutral-500 mt-8 uppercase tracking-widest">
|
||||
{locale === 'ko'
|
||||
? '모든 가격은 세금 별도입니다. Stripe & Toss Payments / PortOne / Payple을 통한 안전한 결제 및 세금계산서 자동 발행.'
|
||||
: 'All prices exclude tax. Secure global checkout via Stripe and localized recurring payments.'}
|
||||
</p>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,20 +21,20 @@ export function Privacy() {
|
|||
},
|
||||
{
|
||||
feature: locale === 'ko' ? '네트워크 전송 지연 (RTT)' : 'Network Roundtrip Latency',
|
||||
d3ro: '0ms (On-Device Memory)',
|
||||
d3ro: '0ms (온디바이스 메모리)',
|
||||
cloud: '600ms ~ 2,500ms',
|
||||
d3roOk: true,
|
||||
},
|
||||
{
|
||||
feature: locale === 'ko' ? '무제한 딕테이션 비용' : 'Unlimited Voice Dictation Cost',
|
||||
d3ro: locale === 'ko' ? '무료 ($0 Forever)' : '$0 Forever Free',
|
||||
cloud: '$15 ~ $30 / Month',
|
||||
d3ro: locale === 'ko' ? '영구 무료 (₩0)' : '$0 Forever Free',
|
||||
cloud: '월 2만 ~ 4만원',
|
||||
d3roOk: true,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section id="privacy" className="relative py-24 md:py-32 overflow-hidden">
|
||||
<section id="privacy" className="relative py-20 md:py-28 overflow-hidden">
|
||||
<Container>
|
||||
<SectionHeader
|
||||
index={t.privacy.index}
|
||||
|
|
@ -43,31 +43,31 @@ export function Privacy() {
|
|||
/>
|
||||
|
||||
{/* CRT instrument panel */}
|
||||
<div className="crt-screen rounded-2xl p-6 md:p-10 noise-texture mb-12 shadow-2xl border border-white/[0.08]">
|
||||
<div className="crt-screen rounded-3xl p-6 md:p-8 noise-texture mb-12 shadow-2xl border border-white/15">
|
||||
<div className="relative z-10">
|
||||
{/* Panel header */}
|
||||
<div className="flex items-center justify-between mb-8 pb-4 border-b border-white/[0.06]">
|
||||
<div className="flex items-center justify-between mb-6 pb-3.5 border-b border-white/[0.08]">
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color="green" pulse />
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-emerald-400">
|
||||
<span className="text-xs font-medium text-emerald-400">
|
||||
{t.privacy.monitorTitle}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-neutral-400 bg-surface-600/60 px-2.5 py-1 rounded border border-white/[0.04]">
|
||||
AIR-GAP COMPLIANT
|
||||
<span className="text-xs font-medium text-neutral-300 bg-surface-700/80 px-3 py-1 rounded-full border border-white/10">
|
||||
에어갭 규정 준수
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Metric grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-6 md:gap-8">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{t.privacy.metrics.map((metric, i) => (
|
||||
<div key={i} className="flex flex-col gap-2 p-3 rounded-lg bg-surface-900/60 border border-white/[0.03]">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-neutral-500">
|
||||
<div key={i} className="flex flex-col gap-1.5 p-3.5 rounded-xl bg-surface-900/80 border border-white/10 shadow-inner">
|
||||
<span className="text-xs text-neutral-400 font-normal">
|
||||
{metric.label}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color="green" size="sm" />
|
||||
<span className="font-mono text-sm font-semibold text-neutral-100 tracking-wide">
|
||||
<span className="text-sm sm:text-base font-medium text-neutral-100">
|
||||
{metric.value}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -78,23 +78,23 @@ export function Privacy() {
|
|||
</div>
|
||||
|
||||
{/* Comparison Matrix: D3RO Voice vs Cloud Services */}
|
||||
<div className="mb-12 overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse rounded-xl overflow-hidden bg-surface-800/40 border border-white/[0.06]">
|
||||
<div className="mb-12 overflow-x-auto -mx-5 md:mx-0">
|
||||
<table className="w-full min-w-[650px] text-left border-collapse rounded-2xl overflow-hidden bg-surface-800/60 border border-white/10 shadow-glass-card backdrop-blur-xl">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.08] bg-surface-700/50 font-mono text-nano uppercase tracking-wider text-neutral-400">
|
||||
<tr className="border-b border-white/10 bg-surface-700/80 text-xs text-neutral-300 font-medium">
|
||||
<th className="p-4 w-1/2">{locale === 'ko' ? '보안 및 프라이버시 비교 기준' : 'Security & Privacy Criterion'}</th>
|
||||
<th className="p-4 text-brand-amber font-bold w-1/4">D3RO VOICE (LOCAL)</th>
|
||||
<th className="p-4 text-neutral-500 w-1/4">CLOUD STT SERVICES</th>
|
||||
<th className="p-4 text-brand-blue font-medium w-1/4">D3RO VOICE (로컬)</th>
|
||||
<th className="p-4 text-neutral-400 w-1/4">일반 클라우드 STT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/[0.04] text-xs">
|
||||
<tbody className="divide-y divide-white/[0.06] text-sm">
|
||||
{comparisonRows.map((row, idx) => (
|
||||
<tr key={idx} className="hover:bg-surface-700/30 transition-colors">
|
||||
<td className="p-4 font-sans font-medium text-neutral-200">{row.feature}</td>
|
||||
<td className="p-4 font-mono font-bold text-emerald-400 bg-emerald-500/[0.04]">
|
||||
<tr key={idx} className="hover:bg-surface-700/40 transition-colors">
|
||||
<td className="p-4 text-neutral-200 font-normal">{row.feature}</td>
|
||||
<td className="p-4 font-medium text-emerald-400 bg-emerald-500/10">
|
||||
✓ {row.d3ro}
|
||||
</td>
|
||||
<td className="p-4 font-mono text-neutral-500">
|
||||
<td className="p-4 text-neutral-400 font-normal">
|
||||
{row.cloud}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -103,19 +103,19 @@ export function Privacy() {
|
|||
</table>
|
||||
</div>
|
||||
|
||||
{/* Guarantees list */}
|
||||
{/* Guarantee bullet cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{t.privacy.guarantees.map((item, i) => (
|
||||
{t.privacy.guarantees.map((g, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-start gap-3 p-4 rounded-xl bg-surface-700/40 border border-white/[0.04]"
|
||||
className="flex items-center gap-3.5 px-5 py-4 rounded-2xl bg-surface-800/70 border border-white/10 backdrop-blur-sm shadow-sm"
|
||||
>
|
||||
<div className="w-5 h-5 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<svg className="w-3 h-3 text-emerald-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||
<div className="w-5 h-5 rounded-full bg-emerald-500/15 border border-emerald-500/30 flex items-center justify-center text-emerald-400 flex-shrink-0">
|
||||
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm text-neutral-300 leading-relaxed font-medium">{item}</span>
|
||||
<span className="text-sm text-neutral-200 font-normal">{g}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,32 +11,33 @@
|
|||
// ── Colors ──────────────────────────────────────────────
|
||||
export const color = {
|
||||
brand: {
|
||||
amber: '#f25b29',
|
||||
amberLight: '#ff7a4d',
|
||||
amberDark: '#c44a22',
|
||||
amberGlow: 'rgba(242,91,41,0.6)',
|
||||
amberDim: 'rgba(242,91,41,0.15)',
|
||||
amberMuted: 'rgba(242,91,41,0.08)',
|
||||
blue: '#3b82f6',
|
||||
blueLight: '#60a5fa',
|
||||
blueDark: '#2563eb',
|
||||
blueGlow: 'rgba(59,130,246,0.5)',
|
||||
blueDim: 'rgba(59,130,246,0.14)',
|
||||
blueMuted: 'rgba(59,130,246,0.08)',
|
||||
},
|
||||
surface: {
|
||||
950: '#0a0a0c',
|
||||
900: '#0d0d0f',
|
||||
800: '#131315',
|
||||
700: '#19191b',
|
||||
600: '#1e1f21',
|
||||
500: '#242427',
|
||||
400: '#2a2a2d',
|
||||
300: '#333338',
|
||||
200: '#3a3b3f',
|
||||
100: '#4a4b50',
|
||||
950: '#08090c',
|
||||
900: '#0d0f14',
|
||||
800: '#13161f',
|
||||
700: '#1a1e2b',
|
||||
600: '#222838',
|
||||
500: '#2b3245',
|
||||
400: '#363f57',
|
||||
300: '#434d69',
|
||||
200: '#535f7f',
|
||||
100: '#68769c',
|
||||
},
|
||||
neutral: {
|
||||
50: '#fafafa',
|
||||
100: '#f0f0f1',
|
||||
200: '#d4d4d8',
|
||||
300: '#a1a1aa',
|
||||
400: '#71717a',
|
||||
500: '#52525b',
|
||||
50: '#ffffff',
|
||||
100: '#f4f4f5',
|
||||
200: '#e4e4e7',
|
||||
300: '#d4d4d8',
|
||||
400: '#a1a1aa',
|
||||
500: '#71717a',
|
||||
600: '#52525b',
|
||||
},
|
||||
semantic: {
|
||||
success: '#22c55e',
|
||||
|
|
@ -46,9 +47,10 @@ export const color = {
|
|||
} as const
|
||||
|
||||
// ── Typography ──────────────────────────────────────────
|
||||
// 한+영 혼용 헤드라인의 스트로크 일관성을 위해 디스플레이도 Pretendard로 통일.
|
||||
export const font = {
|
||||
display: '"Space Grotesk", sans-serif',
|
||||
sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
display: '"Pretendard Variable", Pretendard, -apple-system, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif',
|
||||
sans: '"Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif',
|
||||
mono: '"JetBrains Mono", "Fira Code", monospace',
|
||||
} as const
|
||||
|
||||
|
|
@ -61,18 +63,19 @@ export const space = {
|
|||
|
||||
// ── Shadows ─────────────────────────────────────────────
|
||||
export const shadow = {
|
||||
insetPanel: 'inset 0 2px 6px rgba(0,0,0,0.6), 0 1px 1px rgba(255,255,255,0.04)',
|
||||
chassis: '0 1px 3px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.04)',
|
||||
glowSm: '0 0 10px rgba(242,91,41,0.3)',
|
||||
glowMd: '0 0 20px rgba(242,91,41,0.25), 0 0 40px rgba(242,91,41,0.1)',
|
||||
glowLg: '0 0 30px rgba(242,91,41,0.3), 0 0 60px rgba(242,91,41,0.15), 0 0 90px rgba(242,91,41,0.05)',
|
||||
insetPanel: 'inset 0 1px 3px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.06)',
|
||||
chassis: '0 4px 20px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.06)',
|
||||
glowSm: '0 0 12px rgba(59,130,246,0.3)',
|
||||
glowMd: '0 0 24px rgba(59,130,246,0.25), 0 0 48px rgba(59,130,246,0.1)',
|
||||
glowLg: '0 0 36px rgba(59,130,246,0.35), 0 0 72px rgba(59,130,246,0.15)',
|
||||
} as const
|
||||
|
||||
// ── Radii ───────────────────────────────────────────────
|
||||
export const radius = {
|
||||
instrument: '2px',
|
||||
panel: '6px',
|
||||
card: '12px',
|
||||
instrument: '4px',
|
||||
panel: '8px',
|
||||
card: '16px',
|
||||
cardLarge: '24px',
|
||||
pill: '999px',
|
||||
} as const
|
||||
|
||||
|
|
|
|||
|
|
@ -5,50 +5,88 @@ export default {
|
|||
extend: {
|
||||
colors: {
|
||||
brand: {
|
||||
amber: '#f25b29',
|
||||
'amber-light': '#ff7a4d',
|
||||
'amber-dark': '#c44a22',
|
||||
'amber-glow': 'rgba(242,91,41,0.6)',
|
||||
'amber-dim': 'rgba(242,91,41,0.15)',
|
||||
'amber-muted': 'rgba(242,91,41,0.08)',
|
||||
// 브랜드 통일: 앱(Midnight Glass) blue 토큰과 동일 값 — 레거시 amber 폐기
|
||||
blue: '#3b82f6',
|
||||
'blue-light': '#60a5fa',
|
||||
'blue-dark': '#2563eb',
|
||||
'blue-glow': 'rgba(59,130,246,0.5)',
|
||||
'blue-dim': 'rgba(59,130,246,0.14)',
|
||||
'blue-muted': 'rgba(59,130,246,0.08)',
|
||||
},
|
||||
surface: {
|
||||
950: '#0a0a0c',
|
||||
900: '#0d0d0f',
|
||||
800: '#131315',
|
||||
700: '#19191b',
|
||||
600: '#1e1f21',
|
||||
500: '#242427',
|
||||
400: '#2a2a2d',
|
||||
300: '#333338',
|
||||
200: '#3a3b3f',
|
||||
100: '#4a4b50',
|
||||
950: '#08090c',
|
||||
900: '#0d0f14',
|
||||
800: '#13161f',
|
||||
700: '#1a1e2b',
|
||||
600: '#222838',
|
||||
500: '#2b3245',
|
||||
400: '#363f57',
|
||||
300: '#434d69',
|
||||
200: '#535f7f',
|
||||
100: '#68769c',
|
||||
},
|
||||
neutral: {
|
||||
50: '#fafafa',
|
||||
100: '#f0f0f1',
|
||||
200: '#d4d4d8',
|
||||
300: '#a1a1aa',
|
||||
400: '#71717a',
|
||||
500: '#52525b',
|
||||
600: '#3f3f46',
|
||||
50: '#ffffff',
|
||||
100: '#f4f4f5',
|
||||
200: '#e4e4e7',
|
||||
300: '#d4d4d8',
|
||||
400: '#a1a1aa',
|
||||
500: '#71717a',
|
||||
600: '#52525b',
|
||||
700: '#3f3f46',
|
||||
800: '#27272a',
|
||||
900: '#18181b',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
display: ['"Space Grotesk"', 'sans-serif'],
|
||||
sans: [
|
||||
'-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto',
|
||||
'"Helvetica Neue"', 'Arial', 'sans-serif',
|
||||
'"Pretendard Variable"',
|
||||
'Pretendard',
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'system-ui',
|
||||
'Roboto',
|
||||
'"Helvetica Neue"',
|
||||
'"Segoe UI"',
|
||||
'"Apple SD Gothic Neo"',
|
||||
'"Noto Sans KR"',
|
||||
'"Malgun Gothic"',
|
||||
'sans-serif',
|
||||
],
|
||||
display: [
|
||||
'"Pretendard Variable"',
|
||||
'Pretendard',
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'system-ui',
|
||||
'"Apple SD Gothic Neo"',
|
||||
'"Noto Sans KR"',
|
||||
'sans-serif',
|
||||
],
|
||||
mono: [
|
||||
'"JetBrains Mono"',
|
||||
'"Fira Code"',
|
||||
'ui-monospace',
|
||||
'SFMono-Regular',
|
||||
'Menlo',
|
||||
'Monaco',
|
||||
'Consolas',
|
||||
'monospace',
|
||||
],
|
||||
mono: ['"JetBrains Mono"', '"Fira Code"', 'monospace'],
|
||||
},
|
||||
fontSize: {
|
||||
'hero': ['clamp(2.5rem, 5vw, 4.25rem)', { lineHeight: '1.15', letterSpacing: '-0.03em' }],
|
||||
'display': ['clamp(2rem, 3.5vw, 3.25rem)', { lineHeight: '1.2', letterSpacing: '-0.025em' }],
|
||||
'micro': ['0.625rem', { lineHeight: '1.2', letterSpacing: '0.1em' }],
|
||||
'nano': ['0.5625rem', { lineHeight: '1.2', letterSpacing: '0.12em' }],
|
||||
// 한글 헤드라인: 행간 1.2+ (블록 글자의 숨 쉬는 공간), 자간 -0.01~-0.02em.
|
||||
'hero': ['clamp(2.75rem, 5.5vw, 4.5rem)', { lineHeight: '1.22', letterSpacing: '-0.02em' }],
|
||||
'display': ['clamp(2.25rem, 4vw, 3.5rem)', { lineHeight: '1.28', letterSpacing: '-0.015em' }],
|
||||
'h2': ['clamp(1.75rem, 3vw, 2.5rem)', { lineHeight: '1.32', letterSpacing: '-0.012em' }],
|
||||
'h3': ['1.375rem', { lineHeight: '1.4', letterSpacing: '-0.008em' }],
|
||||
'micro': ['0.75rem', { lineHeight: '1.4', letterSpacing: '0.04em' }],
|
||||
'nano': ['0.6875rem', { lineHeight: '1.4', letterSpacing: '0.05em' }],
|
||||
},
|
||||
spacing: {
|
||||
'4.5': '1.125rem',
|
||||
'13': '3.25rem',
|
||||
'15': '3.75rem',
|
||||
'18': '4.5rem',
|
||||
'22': '5.5rem',
|
||||
'26': '6.5rem',
|
||||
|
|
@ -56,22 +94,25 @@ export default {
|
|||
'34': '8.5rem',
|
||||
},
|
||||
borderRadius: {
|
||||
'instrument': '2px',
|
||||
'panel': '6px',
|
||||
'card': '12px',
|
||||
'instrument': '4px',
|
||||
'panel': '8px',
|
||||
'card': '16px',
|
||||
'2card': '20px',
|
||||
'3card': '24px',
|
||||
},
|
||||
boxShadow: {
|
||||
'inset-panel': 'inset 0 2px 6px rgba(0,0,0,0.6), 0 1px 1px rgba(255,255,255,0.04)',
|
||||
'chassis': '0 1px 3px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.04)',
|
||||
'glow-sm': '0 0 10px rgba(242,91,41,0.3)',
|
||||
'glow-md': '0 0 20px rgba(242,91,41,0.25), 0 0 40px rgba(242,91,41,0.1)',
|
||||
'glow-lg': '0 0 30px rgba(242,91,41,0.3), 0 0 60px rgba(242,91,41,0.15), 0 0 90px rgba(242,91,41,0.05)',
|
||||
'inset-panel': 'inset 0 1px 3px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.06)',
|
||||
'chassis': '0 4px 20px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.06)',
|
||||
'glow-sm': '0 0 12px rgba(59,130,246,0.3)',
|
||||
'glow-md': '0 0 24px rgba(59,130,246,0.25), 0 0 48px rgba(59,130,246,0.1)',
|
||||
'glow-lg': '0 0 36px rgba(59,130,246,0.35), 0 0 72px rgba(59,130,246,0.15)',
|
||||
'glass-card': '0 12px 32px -4px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.08)',
|
||||
},
|
||||
animation: {
|
||||
'glow-pulse': 'glow-pulse 2s ease-in-out infinite',
|
||||
'scan': 'scan 8s linear infinite',
|
||||
'blink': 'blink 1.4s ease-in-out infinite',
|
||||
'fade-in-up': 'fade-in-up 0.6s ease-out forwards',
|
||||
'fade-in-up': 'fade-in-up 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards',
|
||||
},
|
||||
keyframes: {
|
||||
'glow-pulse': {
|
||||
|
|
@ -87,7 +128,7 @@ export default {
|
|||
'50%': { opacity: '0.3' },
|
||||
},
|
||||
'fade-in-up': {
|
||||
'0%': { opacity: '0', transform: 'translateY(20px)' },
|
||||
'0%': { opacity: '0', transform: 'translateY(16px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,40 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { cpSync, mkdirSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const siteRoot = dirname(fileURLToPath(import.meta.url))
|
||||
const safePublicFiles = [
|
||||
'.well-known/assetlinks.json',
|
||||
'accept-invite.css',
|
||||
'accept-invite.html',
|
||||
'accept-invite.js',
|
||||
'accept-invite/index.html',
|
||||
'download.html',
|
||||
'favicon.svg',
|
||||
'legal.css',
|
||||
'privacy/index.html',
|
||||
'terms/index.html',
|
||||
'delete-account/index.html',
|
||||
]
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
// `public/releases` contains retained historical binaries. It is deliberately
|
||||
// outside the build graph; only this non-binary allowlist can reach dist.
|
||||
publicDir: false,
|
||||
plugins: [
|
||||
react(),
|
||||
{
|
||||
name: 'copy-safe-public-files',
|
||||
closeBundle() {
|
||||
for (const relativePath of safePublicFiles) {
|
||||
const destination = join(siteRoot, 'dist', relativePath)
|
||||
mkdirSync(dirname(destination), { recursive: true })
|
||||
cpSync(join(siteRoot, 'public', relativePath), destination, { errorOnExist: true })
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
base: './',
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue