feat(ui): ReplyPier 品牌、中文界面与前端构建产物
替换品牌文案与图标,补齐窄屏布局和加载失败提示,确认主要旅程可操作。
No files matched your search
@@ -0,0 +1,143 @@
|
||||
{#
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
A single help-center article: breadcrumb trail (which must stay in step with the
|
||||
BreadcrumbList JSON-LD), rich Markdown body, "Was this helpful?"
|
||||
feedback, and related articles. `article.body_html` is the ONLY value rendered
|
||||
`| safe` — it is pre-sanitized to an HTML allowlist by help_center_content;
|
||||
every other value stays autoescaped.
|
||||
#}{% extends "help_center/base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="hc-body">
|
||||
|
||||
<!-- ---- SIDEBAR ---- -->
|
||||
<aside class="hc-aside">
|
||||
{% if topics %}
|
||||
<div class="hc-aside-label">BROWSE TOPICS</div>
|
||||
<div class="hc-topics">
|
||||
{% for topic in topics %}
|
||||
<a class="hc-topic{% if topic.active %} active{% endif %}" href="{{ base_path }}/?topic={{ topic.category | urlencode }}" style="--ic: {{ topic.color }};">
|
||||
<span class="hc-topic-icon">
|
||||
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"></path></svg>
|
||||
</span>
|
||||
<span class="hc-topic-name">{{ topic.category }}</span>
|
||||
<span class="hc-topic-count">{{ topic.count }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if widget_id %}
|
||||
<div class="hc-help-card">
|
||||
<h3>Can't find it?</h3>
|
||||
<p>Ask our assistant — it answers instantly and can bring in a human.</p>
|
||||
<button class="hc-cta-btn" type="button" data-chat style="padding: 9px 14px; font-size: 13px; border-radius: 10px; background: var(--brand); color: var(--brand-ink);">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.5 8.5 0 0 1-12.3 7.6L3 21l1.9-5.7A8.5 8.5 0 1 1 21 11.5z"></path></svg>
|
||||
Start a chat
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</aside>
|
||||
|
||||
<!-- ---- ARTICLE ---- -->
|
||||
<main class="hc-main">
|
||||
<article class="hc-article" style="--ic: {{ article.color }};">
|
||||
<nav class="hc-crumbs" aria-label="Breadcrumb">
|
||||
{%- for crumb in breadcrumbs %}
|
||||
{%- if not loop.first %}<span class="hc-crumb-sep" aria-hidden="true">›</span>{% endif %}
|
||||
{%- if crumb.href %}
|
||||
<a href="{{ crumb.href }}">{{ crumb.label }}</a>
|
||||
{%- else %}
|
||||
<span class="hc-crumb-current" aria-current="page" title="{{ crumb.label }}">{{ crumb.label }}</span>
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
</nav>
|
||||
<div class="hc-article-meta">
|
||||
<span class="hc-article-tag">{{ article.category }}</span>
|
||||
<span class="hc-article-time">{{ article.read_time }}</span>
|
||||
</div>
|
||||
<h1>{{ article.question }}</h1>
|
||||
<div class="hc-article-card">
|
||||
<div class="hc-article-body">{{ article.body_html | safe }}</div>
|
||||
{% if article.feedback_url %}
|
||||
<div class="hc-feedback" data-feedback data-feedback-url="{{ article.feedback_url }}">
|
||||
<span class="hc-feedback-q">Was this helpful?</span>
|
||||
<button class="hc-vote" type="button" data-vote="yes">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 11v9H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1z"></path><path d="M7 11l4-8a2 2 0 0 1 3 1.8V9h4.5a2 2 0 0 1 2 2.4l-1.4 7A2 2 0 0 1 17 20H7"></path></svg>
|
||||
Yes
|
||||
</button>
|
||||
<button class="hc-vote" type="button" data-vote="no">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 13V4h3a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1z"></path><path d="M17 13l-4 8a2 2 0 0 1-3-1.8V15H5.5a2 2 0 0 1-2-2.4l1.4-7A2 2 0 0 1 7 4h10"></path></svg>
|
||||
No
|
||||
</button>
|
||||
<span class="hc-thanks" hidden>Thanks for the feedback!</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if article.related %}
|
||||
<div class="hc-related">
|
||||
<div class="hc-related-label">RELATED ARTICLES</div>
|
||||
<div class="hc-related-grid">
|
||||
{% for r in article.related %}
|
||||
<a class="hc-related-card" href="{{ base_path }}{{ r.href }}" style="--ic: {{ r.color }};">
|
||||
<span class="hc-related-top">
|
||||
<span class="hc-related-icon">
|
||||
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"></path></svg>
|
||||
</span>
|
||||
<span style="color:#c4c8d2; display:flex;"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17L17 7M17 7H8M17 7v9"></path></svg></span>
|
||||
</span>
|
||||
<span class="hc-related-q">{{ r.question }}</span>
|
||||
<span class="hc-related-time">
|
||||
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3v5h5"></path><path d="M6 3h9l5 5v13H6z"></path></svg>
|
||||
{{ r.read_time }}
|
||||
</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_scripts %}
|
||||
<script>
|
||||
(function () {
|
||||
// per-article "Was this helpful?" — records the vote, then acknowledges.
|
||||
var fb = document.querySelector('[data-feedback]');
|
||||
if (!fb) return;
|
||||
var voted = false;
|
||||
fb.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('[data-vote]');
|
||||
if (!btn || voted) return;
|
||||
voted = true;
|
||||
var helpful = btn.getAttribute('data-vote') === 'yes';
|
||||
fb.querySelectorAll('.hc-vote').forEach(function (v) { v.classList.remove('on-yes', 'on-no'); });
|
||||
btn.classList.add(helpful ? 'on-yes' : 'on-no');
|
||||
var thanks = fb.querySelector('.hc-thanks');
|
||||
if (thanks) thanks.hidden = false;
|
||||
// Explicit URL, not location.pathname: an article that kept its original
|
||||
// URL through a migration is served at a path with no /feedback route.
|
||||
fetch(fb.getAttribute('data-feedback-url'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ helpful: helpful })
|
||||
}).catch(function () {}); // best-effort; the UI already acknowledged
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,420 @@
|
||||
{#
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Shared chrome for the public help center (list page + article page): head/meta,
|
||||
all CSS, header, contact CTA, footer, and the chat-widget embed. Pages fill the
|
||||
`hero`, `body` and `page_scripts` blocks.
|
||||
|
||||
Every org/FAQ value is escaped by Jinja2 autoescaping. The ONE exception is the
|
||||
article body, which is pre-sanitized to an HTML allowlist by
|
||||
app.services.help_center_content and rendered with `| safe`; never mark any other
|
||||
org content safe. Colors: only `--brand`/`--brand-ink` come from the server
|
||||
(validated hex); every other tone is derived in CSS via color-mix.
|
||||
#}<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ title }}</title>
|
||||
<meta name="description" content="{{ description }}">
|
||||
<link rel="canonical" href="{{ canonical_url }}">
|
||||
{% if favicon_url %}<link rel="icon" href="{{ favicon_url }}">{% endif %}
|
||||
<meta property="og:title" content="{{ title }}">
|
||||
<meta property="og:description" content="{{ description }}">
|
||||
<meta property="og:type" content="{{ og_type | default('website') }}">
|
||||
<meta property="og:url" content="{{ canonical_url }}">
|
||||
{% if og_site_name %}<meta property="og:site_name" content="{{ og_site_name }}">{% endif %}
|
||||
{# og:image must be absolute (scrapers don't resolve relative URLs) — the
|
||||
route absolutizes it. Without an image the card degrades to a text
|
||||
summary rather than showing a broken thumbnail. #}
|
||||
{% if og_image %}
|
||||
<meta property="og:image" content="{{ og_image }}">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:image" content="{{ og_image }}">
|
||||
{% else %}
|
||||
<meta name="twitter:card" content="summary">
|
||||
{% endif %}
|
||||
<meta name="twitter:title" content="{{ title }}">
|
||||
<meta name="twitter:description" content="{{ description }}">
|
||||
<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=Sora:wght@400;500;600;700&family=Instrument+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--brand: {{ brand_color }};
|
||||
--brand-ink: {{ brand_ink }};
|
||||
--brand-tint: color-mix(in srgb, var(--brand) 9%, #fff);
|
||||
--brand-soft: color-mix(in srgb, var(--brand) 30%, #fff);
|
||||
--brand-line: color-mix(in srgb, var(--brand) 40%, #fff);
|
||||
--hero-bg: linear-gradient(180deg, color-mix(in srgb, var(--brand) 10%, #fff), #f5f6f9 80%);
|
||||
--hero-glow: radial-gradient(ellipse, color-mix(in srgb, var(--brand) 12%, transparent), transparent 68%);
|
||||
--cta-bg: linear-gradient(120deg, var(--brand), color-mix(in srgb, var(--brand) 78%, #fff));
|
||||
--cta-sub: color-mix(in srgb, var(--brand-ink) 72%, transparent);
|
||||
--sans: 'Instrument Sans', system-ui, -apple-system, sans-serif;
|
||||
--display: 'Sora', var(--sans);
|
||||
--mono: 'JetBrains Mono', ui-monospace, monospace;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
/* Author display rules (e.g. .hc-card { display:flex }) otherwise beat the UA
|
||||
[hidden] rule, so the hidden attribute must win explicitly for JS toggling. */
|
||||
[hidden] { display: none !important; }
|
||||
html, body { margin: 0; padding: 0; background: #f5f6f9; }
|
||||
body { color: #1a1b25; font-family: var(--sans); -webkit-font-smoothing: antialiased; min-height: 100vh; }
|
||||
a { color: var(--brand); }
|
||||
::selection { background: var(--brand-soft); color: #1a1b25; }
|
||||
@keyframes hc-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes hc-blink { 0%, 100% { opacity: .3; } 50% { opacity: 1; } }
|
||||
@keyframes hc-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||||
|
||||
/* ---------- header ---------- */
|
||||
.hc-header { background: #fff; border-bottom: 1px solid #ececf2; position: sticky; top: 0; z-index: 40; }
|
||||
.hc-header-in { max-width: 1120px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 15px 28px; }
|
||||
.hc-brand { display: flex; align-items: center; gap: 11px; text-decoration: none; color: inherit; min-width: 0; }
|
||||
.hc-mark { width: 30px; height: 30px; border-radius: 9px; background: var(--brand); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.hc-mark span { width: 12px; height: 12px; border-radius: 3px; background: var(--brand-ink); transform: rotate(45deg); }
|
||||
.hc-logo { height: 30px; width: auto; display: block; flex-shrink: 0; }
|
||||
.hc-name { font-family: var(--display); font-weight: 700; font-size: 19px; letter-spacing: -.02em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.hc-kicker { font-size: 14px; color: #8a90a0; font-weight: 500; padding-left: 12px; border-left: 1px solid #e4e6ec; white-space: nowrap; }
|
||||
.hc-nav { display: flex; align-items: center; gap: 20px; font-size: 14.5px; flex-shrink: 0; }
|
||||
.hc-nav a { color: #5a6172; text-decoration: none; }
|
||||
.hc-nav a:hover { color: #1a1b25; }
|
||||
.hc-nav a.cta { padding: 9px 17px; background: var(--brand); color: var(--brand-ink); border-radius: 10px; font-weight: 600; }
|
||||
|
||||
/* ---------- hero ---------- */
|
||||
.hc-hero { position: relative; overflow: hidden; background: var(--hero-bg); padding: 58px 28px 46px; }
|
||||
.hc-hero-glow { position: absolute; top: -90px; left: 50%; transform: translateX(-50%); width: 640px; height: 340px; max-width: 100%; background: var(--hero-glow); pointer-events: none; }
|
||||
.hc-hero-in { position: relative; max-width: 720px; margin: 0 auto; text-align: center; }
|
||||
.hc-hero h1 { font-family: var(--display); font-weight: 700; font-size: 38px; line-height: 1.1; letter-spacing: -.03em; margin: 0 0 12px; }
|
||||
.hc-hero p.lead { font-size: 16.5px; color: #5a6172; margin: 0 0 26px; }
|
||||
.hc-searchbar { display: flex; align-items: center; gap: 12px; background: #fff; border: 1px solid #e4e6ec; border-radius: 15px; padding: 6px 6px 6px 18px; box-shadow: 0 12px 34px rgba(40, 36, 90, .08); max-width: 600px; margin: 0 auto; }
|
||||
.hc-searchbar svg { flex-shrink: 0; }
|
||||
.hc-searchbar input { flex: 1; min-width: 0; background: transparent; border: none; outline: none; font-size: 16px; color: #1a1b25; font-family: var(--sans); padding: 12px 0; }
|
||||
.hc-searchbar input::placeholder { color: #9aa0ad; }
|
||||
.hc-clear { flex-shrink: 0; width: 34px; height: 34px; border-radius: 9px; background: #f1f2f6; border: none; color: #8a90a0; cursor: pointer; font-size: 14px; display: none; }
|
||||
.hc-popular { display: flex; align-items: center; justify-content: center; gap: 9px; margin-top: 18px; font-size: 13.5px; color: #8a90a0; flex-wrap: wrap; }
|
||||
.hc-chip { padding: 5px 12px; background: #fff; border: 1px solid #e4e6ec; border-radius: 999px; font-size: 13px; color: #4a5163; cursor: pointer; font-family: var(--sans); text-decoration: none; }
|
||||
.hc-chip:hover { border-color: var(--brand-soft); }
|
||||
|
||||
/* ---------- body layout ---------- */
|
||||
.hc-body { max-width: 1120px; margin: 0 auto; padding: 40px 28px 20px; display: grid; grid-template-columns: 250px 1fr; gap: 36px; align-items: start; }
|
||||
.hc-aside { position: sticky; top: 82px; }
|
||||
.hc-aside-label { font-family: var(--mono); font-size: 11px; letter-spacing: .09em; color: #9aa0ad; padding: 0 6px; margin-bottom: 12px; }
|
||||
.hc-topics { display: flex; flex-direction: column; gap: 4px; }
|
||||
.hc-topic { display: flex; align-items: center; gap: 11px; width: 100%; padding: 9px 10px; border-radius: 12px; cursor: pointer; font-family: var(--sans); border: 1px solid transparent; background: transparent; color: #4a5163; transition: background .15s; text-decoration: none; text-align: left; }
|
||||
.hc-topic:hover { background: #fff; }
|
||||
.hc-topic.active { border-color: var(--brand-soft); background: var(--brand-tint); color: #1a1b25; }
|
||||
.hc-topic-icon { width: 30px; height: 30px; flex-shrink: 0; border-radius: 9px; display: flex; align-items: center; justify-content: center; background: color-mix(in srgb, var(--ic) 12%, #fff); color: var(--ic); }
|
||||
.hc-topic-name { flex: 1; min-width: 0; text-align: left; font-size: 14.5px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.hc-topic.active .hc-topic-name { font-weight: 600; }
|
||||
.hc-topic-count { flex-shrink: 0; font-family: var(--mono); font-size: 12px; color: #b4b9c4; }
|
||||
.hc-topic.active .hc-topic-count { color: var(--brand); }
|
||||
.hc-help-card { margin-top: 22px; padding: 18px; background: #fff; border: 1px solid #ececf2; border-radius: 16px; box-shadow: 0 2px 10px rgba(40, 36, 90, .04); }
|
||||
.hc-help-card h3 { font-family: var(--display); font-weight: 600; font-size: 14.5px; margin: 0 0 5px; }
|
||||
.hc-help-card p { font-size: 13px; color: #8a90a0; line-height: 1.5; margin: 0 0 13px; }
|
||||
|
||||
.hc-main { min-width: 0; }
|
||||
|
||||
/* ---------- AI answer ---------- */
|
||||
.hc-ai-wrap { margin-bottom: 24px; }
|
||||
.hc-ask-btn { display: inline-flex; align-items: center; gap: 8px; padding: 11px 18px; background: #fff; border: 1px solid var(--brand-soft); color: var(--brand); border-radius: 12px; font-size: 14px; font-weight: 600; cursor: pointer; font-family: var(--sans); }
|
||||
.hc-ask-btn:hover { background: var(--brand-tint); }
|
||||
.hc-ai { background: #fff; border: 1px solid var(--brand-soft); border-radius: 18px; padding: 22px 24px; box-shadow: 0 12px 34px rgba(40, 36, 90, .06); animation: hc-rise .3s ease; }
|
||||
.hc-ai-head { display: flex; align-items: center; gap: 10px; margin-bottom: 13px; flex-wrap: wrap; }
|
||||
.hc-badge { display: inline-flex; align-items: center; gap: 7px; padding: 5px 11px; background: var(--brand-tint); border: 1px solid var(--brand-soft); border-radius: 999px; font-family: var(--mono); font-size: 11px; font-weight: 500; color: var(--brand); letter-spacing: .04em; }
|
||||
.hc-dots { display: inline-flex; gap: 3px; }
|
||||
.hc-dots span { width: 4px; height: 4px; border-radius: 50%; background: var(--brand); animation: hc-blink 1s ease-in-out infinite; }
|
||||
.hc-dots span:nth-child(2) { animation-delay: .18s; }
|
||||
.hc-dots span:nth-child(3) { animation-delay: .36s; }
|
||||
.hc-ai-note { font-size: 12px; color: #9aa0ad; }
|
||||
.hc-ai-answer { font-size: 16px; line-height: 1.65; color: #2a303e; margin: 0; white-space: pre-line; }
|
||||
.hc-ai-foot { display: flex; align-items: center; justify-content: flex-end; gap: 16px; flex-wrap: wrap; padding-top: 14px; margin-top: 14px; border-top: 1px solid #f1f2f6; }
|
||||
.hc-skeleton { display: flex; flex-direction: column; gap: 9px; }
|
||||
.hc-skeleton span { height: 12px; border-radius: 6px; background: linear-gradient(90deg, #eef0f4, #f6f7f9, #eef0f4); background-size: 200% 100%; animation: hc-shimmer 1.3s linear infinite; }
|
||||
|
||||
/* ---------- content header ---------- */
|
||||
.hc-head { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; }
|
||||
.hc-head-icon { width: 38px; height: 38px; flex-shrink: 0; border-radius: 11px; display: flex; align-items: center; justify-content: center; background: color-mix(in srgb, var(--ic) 12%, #fff); color: var(--ic); }
|
||||
.hc-head h2 { font-family: var(--display); font-weight: 600; font-size: 22px; letter-spacing: -.02em; margin: 0; }
|
||||
.hc-head-sub { font-size: 13.5px; color: #8a90a0; margin-top: 2px; }
|
||||
|
||||
/* ---------- FAQ list (link cards) ---------- */
|
||||
.hc-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
.hc-card { display: flex; align-items: center; justify-content: space-between; gap: 16px; background: #fff; border: 1px solid #ececf2; border-radius: 14px; padding: 16px 20px; text-decoration: none; box-shadow: 0 1px 5px rgba(40, 36, 90, .03); transition: border-color .18s, transform .18s; }
|
||||
.hc-card:hover { border-color: var(--brand-soft); transform: translateY(-1px); }
|
||||
.hc-card-main { min-width: 0; flex: 1; }
|
||||
.hc-tag { display: none; font-family: var(--mono); font-size: 10.5px; padding: 3px 8px; border-radius: 6px; background: color-mix(in srgb, var(--ic) 12%, #fff); color: var(--ic); margin-bottom: 8px; }
|
||||
.hc-qtext { display: block; font-size: 15.5px; font-weight: 600; color: #1a1b25; line-height: 1.4; }
|
||||
.hc-preview { display: block; font-size: 13.5px; color: #8a90a0; line-height: 1.5; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.hc-card-meta { flex-shrink: 0; display: flex; align-items: center; gap: 12px; }
|
||||
.hc-read { font-size: 12px; color: #b4b9c4; white-space: nowrap; }
|
||||
.hc-chev { color: #8a90a0; display: flex; }
|
||||
|
||||
/* ---------- grouped browse: every category with its own articles ---------- */
|
||||
.hc-groups { display: flex; flex-direction: column; gap: 30px; }
|
||||
.hc-group { scroll-margin-top: 90px; }
|
||||
.hc-group-head { margin-bottom: 14px; }
|
||||
.hc-group-head h2 { font-size: 19px; }
|
||||
|
||||
.hc-empty { text-align: center; padding: 56px 20px; background: #fff; border: 1px solid #ececf2; border-radius: 18px; }
|
||||
.hc-empty-icon { width: 52px; height: 52px; margin: 0 auto 18px; border-radius: 14px; background: #f5f6f9; border: 1px solid #ececf2; display: flex; align-items: center; justify-content: center; color: #9aa0ad; }
|
||||
.hc-empty h3 { font-family: var(--display); font-weight: 600; font-size: 19px; margin: 0 0 8px; }
|
||||
.hc-empty p { font-size: 15px; color: #8a90a0; margin: 0 0 20px; }
|
||||
|
||||
/* ---------- article page ---------- */
|
||||
/* Breadcrumb trail. The markup mirrors the BreadcrumbList JSON-LD exactly —
|
||||
change one and change the other, or the structured data stops matching
|
||||
the visible content. The current page is text, not a link. */
|
||||
.hc-crumbs { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-bottom: 20px; font-family: var(--sans); font-size: 13.5px; color: #8a90a0; }
|
||||
.hc-crumbs a { color: #8a90a0; text-decoration: none; }
|
||||
.hc-crumbs a:hover { color: #4a5163; text-decoration: underline; }
|
||||
.hc-crumb-sep { color: #c4c8d2; }
|
||||
/* The article title can be long; keep the trail to one tidy line. */
|
||||
.hc-crumb-current { color: #4a5163; max-width: min(100%, 340px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.hc-article-meta { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
|
||||
.hc-article-tag { font-family: var(--mono); font-size: 11px; padding: 4px 10px; border-radius: 7px; background: color-mix(in srgb, var(--ic) 12%, #fff); color: var(--ic); }
|
||||
.hc-article-time { font-size: 13px; color: #9aa0ad; }
|
||||
.hc-article h1 { font-family: var(--display); font-weight: 700; font-size: 30px; line-height: 1.2; letter-spacing: -.025em; margin: 0 0 22px; }
|
||||
.hc-article-card { background: #fff; border: 1px solid #ececf2; border-radius: 18px; padding: 30px 32px; box-shadow: 0 2px 12px rgba(40, 36, 90, .04); }
|
||||
|
||||
/* rendered Markdown body */
|
||||
.hc-article-body { font-size: 15.5px; color: #3a4152; line-height: 1.72; }
|
||||
/* A short, single-paragraph answer reads larger so it doesn't look lost in
|
||||
the card's whitespace. */
|
||||
.hc-article-body > p:only-child { font-size: 18px; line-height: 1.6; color: #2a3040; }
|
||||
.hc-article-body > :first-child { margin-top: 0; }
|
||||
.hc-article-body > :last-child { margin-bottom: 0; }
|
||||
.hc-article-body p { margin: 0 0 16px; }
|
||||
.hc-article-body h1, .hc-article-body h2, .hc-article-body h3, .hc-article-body h4 { font-family: var(--display); font-weight: 600; letter-spacing: -.015em; color: #1a1b25; margin: 28px 0 12px; line-height: 1.3; }
|
||||
.hc-article-body h1 { font-size: 22px; }
|
||||
.hc-article-body h2 { font-size: 19px; }
|
||||
.hc-article-body h3 { font-size: 16.5px; }
|
||||
.hc-article-body h4 { font-size: 15px; }
|
||||
.hc-article-body a { color: var(--brand); font-weight: 600; text-decoration: none; }
|
||||
.hc-article-body a:hover { text-decoration: underline; }
|
||||
.hc-article-body img { max-width: 100%; height: auto; display: block; border-radius: 14px; border: 1px solid #ececf2; margin: 8px 0 22px; }
|
||||
.hc-article-body ul, .hc-article-body ol { margin: 6px 0 22px; padding-left: 24px; }
|
||||
.hc-article-body li { margin-bottom: 8px; }
|
||||
.hc-article-body blockquote { margin: 6px 0 22px; padding: 14px 18px; background: var(--brand-tint); border: 1px solid var(--brand-soft); border-left: 3px solid var(--brand); border-radius: 12px; color: #3a4152; }
|
||||
.hc-article-body blockquote p { margin: 0; }
|
||||
.hc-article-body code { font-family: var(--mono); font-size: 13px; background: #f1f2f6; border-radius: 5px; padding: 1px 6px; }
|
||||
.hc-article-body pre { background: #f1f2f6; border-radius: 12px; padding: 14px 16px; overflow-x: auto; margin: 6px 0 22px; }
|
||||
.hc-article-body pre code { background: none; padding: 0; }
|
||||
.hc-article-body table { border-collapse: collapse; width: 100%; margin: 6px 0 22px; font-size: 14px; }
|
||||
.hc-article-body th, .hc-article-body td { border: 1px solid #ececf2; padding: 8px 11px; text-align: left; }
|
||||
.hc-article-body th { background: #f5f6f9; font-weight: 600; }
|
||||
.hc-article-body hr { border: none; border-top: 1px solid #ececf2; margin: 24px 0; }
|
||||
|
||||
.hc-feedback { display: flex; align-items: center; gap: 14px; margin-top: 10px; padding-top: 20px; border-top: 1px solid #f1f2f6; flex-wrap: wrap; }
|
||||
.hc-feedback-q { font-size: 13.5px; color: #8a90a0; }
|
||||
.hc-vote { display: inline-flex; align-items: center; gap: 6px; padding: 7px 13px; border-radius: 9px; font-size: 13px; cursor: pointer; font-family: var(--sans); background: #f5f6f9; border: 1px solid #e4e6ec; color: #4a5163; }
|
||||
.hc-vote.on-yes { background: var(--brand-tint); border-color: var(--brand-soft); color: var(--brand); }
|
||||
.hc-vote.on-no { background: #fdecea; border-color: #f3c6be; color: #cf5b38; }
|
||||
.hc-thanks { font-size: 12.5px; color: var(--brand); font-weight: 600; }
|
||||
|
||||
/* related articles */
|
||||
.hc-related { margin-top: 34px; }
|
||||
.hc-related-label { font-family: var(--mono); font-size: 11px; letter-spacing: .09em; color: #9aa0ad; margin-bottom: 14px; }
|
||||
.hc-related-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
||||
.hc-related-card { display: flex; flex-direction: column; gap: 14px; height: 100%; padding: 18px; background: #fff; border: 1px solid #ececf2; border-radius: 15px; text-decoration: none; box-shadow: 0 1px 5px rgba(40, 36, 90, .03); transition: border-color .18s, transform .18s, box-shadow .18s; }
|
||||
.hc-related-card:hover { border-color: var(--brand-soft); transform: translateY(-2px); box-shadow: 0 12px 26px rgba(40, 36, 90, .08); }
|
||||
.hc-related-top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.hc-related-icon { width: 34px; height: 34px; border-radius: 10px; background: color-mix(in srgb, var(--ic) 12%, #fff); color: var(--ic); display: flex; align-items: center; justify-content: center; }
|
||||
.hc-related-q { display: block; font-family: var(--display); font-size: 15px; font-weight: 600; color: #1a1b25; line-height: 1.35; letter-spacing: -.01em; }
|
||||
.hc-related-time { display: inline-flex; align-items: center; gap: 6px; margin-top: auto; font-size: 12.5px; color: #9aa0ad; }
|
||||
|
||||
/* ---------- contact CTA ---------- */
|
||||
.hc-cta-shell { max-width: 1120px; margin: 0 auto; padding: 0 28px; }
|
||||
.hc-cta { margin: 24px 0 56px; background: var(--cta-bg); border-radius: 24px; padding: 44px; display: flex; align-items: center; justify-content: space-between; gap: 30px; flex-wrap: wrap; color: var(--brand-ink); scroll-margin-top: 90px; }
|
||||
.hc-cta h2 { font-family: var(--display); font-weight: 700; font-size: 26px; letter-spacing: -.02em; margin: 0 0 8px; }
|
||||
.hc-cta p { font-size: 16px; color: var(--cta-sub); margin: 0; max-width: 440px; line-height: 1.55; }
|
||||
.hc-cta-btn { display: inline-flex; align-items: center; gap: 10px; padding: 15px 26px; background: var(--brand-ink); color: var(--brand); border: none; border-radius: 13px; font-size: 15.5px; font-weight: 600; cursor: pointer; font-family: var(--sans); white-space: nowrap; text-decoration: none; }
|
||||
|
||||
/* ---------- footer ---------- */
|
||||
.hc-footer { border-top: 1px solid #ececf2; background: #fff; }
|
||||
.hc-footer-in { max-width: 1120px; margin: 0 auto; padding: 30px 28px; display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap; }
|
||||
.hc-footer-org { display: flex; align-items: center; gap: 11px; }
|
||||
.hc-footer-org .hc-mark { width: 26px; height: 26px; border-radius: 8px; }
|
||||
.hc-footer-org .hc-mark span { width: 10px; height: 10px; }
|
||||
.hc-footer-org .hc-name { font-size: 15px; }
|
||||
.hc-powered { display: inline-flex; align-items: center; gap: 8px; text-decoration: none; padding: 7px 13px; border: 1px solid #ececf2; border-radius: 999px; font-size: 12.5px; color: #8a90a0; }
|
||||
.hc-powered strong { display: inline-flex; align-items: center; gap: 6px; color: #1a1b25; font-weight: 600; }
|
||||
.hc-rp-mark { width: 15px; height: 15px; border-radius: 5px 5px 5px 1px; background: #c9f24e; display: inline-flex; align-items: center; justify-content: center; gap: 1.5px; }
|
||||
.hc-rp-mark i { width: 2px; height: 2px; border-radius: 50%; background: #0b0c10; }
|
||||
|
||||
/* progressive enhancement: only hide things once JS takes over */
|
||||
.js .hc-tag { display: none; }
|
||||
|
||||
@media (max-width: 860px) {
|
||||
/* Compact hero so the topics and articles start higher up. */
|
||||
.hc-hero { padding: 30px 20px 22px; }
|
||||
.hc-hero h1 { font-size: 25px; margin-bottom: 6px; }
|
||||
.hc-hero p.lead { font-size: 14.5px; margin-bottom: 16px; }
|
||||
.hc-searchbar { padding: 5px 5px 5px 14px; }
|
||||
.hc-searchbar input { padding: 11px 0; font-size: 15.5px; }
|
||||
/* The 2-column topic grid below is the category picker on mobile, so the
|
||||
duplicate "Popular" chips just add height. */
|
||||
.hc-popular { display: none; }
|
||||
|
||||
.hc-body { grid-template-columns: 1fr; gap: 16px; padding: 18px 16px 16px; }
|
||||
.hc-aside { position: static; min-width: 0; }
|
||||
.hc-main { min-width: 0; }
|
||||
.hc-aside-label { margin-bottom: 10px; }
|
||||
|
||||
/* Topics: a compact 3-row band that scrolls left/right. Fixed-width
|
||||
auto-columns + overflow-x keep the scroll contained to this strip (the
|
||||
page never scrolls sideways), so it stays short instead of a tall grid. */
|
||||
.hc-topics {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-rows: repeat(3, auto);
|
||||
grid-auto-columns: 46%;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-x: contain;
|
||||
scroll-snap-type: x proximity;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
padding: 0 2px 4px;
|
||||
margin: 0 -2px;
|
||||
}
|
||||
.hc-topics::-webkit-scrollbar { display: none; }
|
||||
.hc-topic { width: auto; min-width: 0; padding: 10px 11px; gap: 9px; scroll-snap-align: start; }
|
||||
.hc-topic-icon { width: 26px; height: 26px; }
|
||||
.hc-topic-name { font-size: 13.5px; }
|
||||
|
||||
/* Redundant on mobile — the "Still need a hand?" CTA below covers it. */
|
||||
.hc-help-card { display: none; }
|
||||
/* While searching, get the whole browse-topics column out of the way so
|
||||
the results (and the Ask-AI card) sit right under the search box,
|
||||
instead of on a "second page" below every category. Also tighten the
|
||||
hero/body padding so results sit close to the search box. */
|
||||
body.hc-searching .hc-aside { display: none; }
|
||||
body.hc-searching .hc-hero { padding-bottom: 8px; }
|
||||
body.hc-searching .hc-body { padding-top: 8px; }
|
||||
.hc-cta { padding: 30px; }
|
||||
.hc-article-card { padding: 24px 20px; }
|
||||
.hc-related-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<!-- ============ HEADER ============ -->
|
||||
<header class="hc-header">
|
||||
<div class="hc-header-in">
|
||||
<a class="hc-brand" href="{{ base_path }}/">
|
||||
{% if logo_url %}
|
||||
<img class="hc-logo" src="{{ logo_url }}" alt="{{ row.organization.name }} logo">
|
||||
{% else %}
|
||||
<span class="hc-mark"><span></span></span>
|
||||
{% endif %}
|
||||
<span class="hc-name">{{ row.organization.name }}</span>
|
||||
<span class="hc-kicker">Help Center</span>
|
||||
</a>
|
||||
<nav class="hc-nav">
|
||||
{% for link in header_links %}<a href="{{ link.url }}" rel="noopener">{{ link.label }}</a>{% endfor %}
|
||||
{% if row.cta_enabled and row.cta_text and row.cta_url %}<a class="cta" href="{{ row.cta_url }}" rel="noopener">{{ row.cta_text }}</a>{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% block hero %}{% endblock %}
|
||||
|
||||
{% block body %}{% endblock %}
|
||||
|
||||
<!-- ============ CONTACT CTA ============ -->
|
||||
{% set cta_shown = row.cta_enabled and row.cta_text and row.cta_url %}
|
||||
{% if widget_id or cta_shown %}
|
||||
<div class="hc-cta-shell">
|
||||
<section class="hc-cta" id="contact">
|
||||
<div>
|
||||
<h2>Still need a hand?</h2>
|
||||
<p>{% if widget_id %}Our support assistant answers instantly, any time of day — and connects you to a human whenever you need one.{% else %}Reach out and our team will get back to you.{% endif %}</p>
|
||||
</div>
|
||||
{% if widget_id %}
|
||||
<button class="hc-cta-btn" type="button" data-chat>
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.5 8.5 0 0 1-12.3 7.6L3 21l1.9-5.7A8.5 8.5 0 1 1 21 11.5z"></path></svg>
|
||||
Start a chat
|
||||
</button>
|
||||
{% elif cta_shown %}
|
||||
<a class="hc-cta-btn" href="{{ row.cta_url }}" rel="noopener">{{ row.cta_text }}</a>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- ============ FOOTER ============ -->
|
||||
<footer class="hc-footer">
|
||||
<div class="hc-footer-in">
|
||||
<div class="hc-footer-org">
|
||||
{% if logo_url %}
|
||||
<img class="hc-logo" src="{{ logo_url }}" alt="{{ row.organization.name }} logo">
|
||||
{% else %}
|
||||
<span class="hc-mark"><span></span></span>
|
||||
{% endif %}
|
||||
<span class="hc-name">{{ row.organization.name }}</span>
|
||||
</div>
|
||||
<a class="hc-powered" href="https://replypier.site" target="_blank" rel="noopener">
|
||||
<span>Powered by</span>
|
||||
<strong><span class="hc-rp-mark"><i></i><i></i><i></i></span> ReplyPier</strong>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{% if json_ld %}
|
||||
<script type="application/ld+json">{{ json_ld | tojson }}</script>
|
||||
{% endif %}
|
||||
|
||||
<!-- open the real embedded ReplyPier widget -->
|
||||
<script>
|
||||
(function () {
|
||||
// The widget script is async and builds its launcher (#replypier-button)
|
||||
// a moment after load, so a click can land before it exists — poll briefly
|
||||
// and click as soon as it appears instead of silently doing nothing.
|
||||
function openChat() {
|
||||
var tries = 0;
|
||||
(function attempt() {
|
||||
var btn = document.getElementById('replypier-button');
|
||||
if (btn) { btn.click(); return; }
|
||||
if (tries++ < 40) { setTimeout(attempt, 150); return; } // ~6s
|
||||
if (window.console) console.warn('[ReplyPier] chat widget is not available on this page.');
|
||||
})();
|
||||
}
|
||||
[].slice.call(document.querySelectorAll('[data-chat]')).forEach(function (b) {
|
||||
b.addEventListener('click', openChat);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% block page_scripts %}{% endblock %}
|
||||
|
||||
{% if widget_id %}
|
||||
<script>
|
||||
window.replypierId = {{ widget_id | tojson }};
|
||||
// Must be set, exactly like the dashboard's install snippet does: the loader
|
||||
// otherwise falls back to the API URL baked in at build time (the vendor
|
||||
// cloud), so a self-hosted help center would ask the wrong backend for its
|
||||
// widget and the visitor would just see "Chat Unavailable".
|
||||
window.replypierBaseUrl = {{ widget_api_url | tojson }};
|
||||
(function () {
|
||||
var s = document.createElement('script');
|
||||
s.src = {{ widget_script_url | tojson }};
|
||||
s.async = true;
|
||||
document.body.appendChild(s);
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,425 @@
|
||||
{#
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Help-center landing: hero + search, topic sidebar, and a list of FAQ cards that
|
||||
link to their article pages (/a/{slug}). Fully server-rendered for SEO; the JS is
|
||||
progressive enhancement only (topic filtering, client-side search, optional
|
||||
Ask-AI fetch). Card previews/read-times are computed server-side.
|
||||
#}{% extends "help_center/base.html" %}
|
||||
|
||||
{% block hero %}
|
||||
<section class="hc-hero" id="top">
|
||||
<div class="hc-hero-glow"></div>
|
||||
<div class="hc-hero-in">
|
||||
<h1>How can we help?</h1>
|
||||
<p class="lead">Search our knowledge base, or pick a topic to browse.</p>
|
||||
<form class="hc-searchbar" method="get" action="{{ base_path }}/" role="search" onsubmit="return false;">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#9aa0ad" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
<input id="hc-search" type="search" name="q" value="{{ search }}" placeholder="Search for answers…" maxlength="200" autocomplete="off">
|
||||
<button class="hc-clear" id="hc-clear" type="button" aria-label="Clear search">✕</button>
|
||||
</form>
|
||||
{% if groups %}
|
||||
<div class="hc-popular">
|
||||
<span>Popular:</span>
|
||||
{% for category, faqs in groups[:4] %}
|
||||
<button class="hc-chip" type="button" data-jump="{{ category }}">{{ category }}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% set first_cat = groups[0][0] if groups else '' %}
|
||||
<div class="hc-body">
|
||||
|
||||
<!-- ---- SIDEBAR ---- -->
|
||||
<aside class="hc-aside">
|
||||
{% if groups %}
|
||||
<div class="hc-aside-label">BROWSE TOPICS</div>
|
||||
<div class="hc-topics">
|
||||
{% for category, faqs in groups %}
|
||||
<button class="hc-topic{% if loop.first %} active{% endif %}" type="button"
|
||||
data-category="{{ category }}" data-count="{{ faqs | length }}"
|
||||
data-color="{{ colors[category] }}"
|
||||
style="--ic: {{ colors[category] }};">
|
||||
<span class="hc-topic-icon">
|
||||
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"></path></svg>
|
||||
</span>
|
||||
<span class="hc-topic-name">{{ category }}</span>
|
||||
<span class="hc-topic-count">{{ faqs | length }}</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if widget_id %}
|
||||
<div class="hc-help-card">
|
||||
<h3>Can't find it?</h3>
|
||||
<p>Ask our assistant — it answers instantly and can bring in a human.</p>
|
||||
<button class="hc-cta-btn" type="button" data-chat style="padding: 9px 14px; font-size: 13px; border-radius: 10px; background: var(--brand); color: var(--brand-ink);">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.5 8.5 0 0 1-12.3 7.6L3 21l1.9-5.7A8.5 8.5 0 1 1 21 11.5z"></path></svg>
|
||||
Start a chat
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</aside>
|
||||
|
||||
<!-- ---- CONTENT ---- -->
|
||||
<main class="hc-main">
|
||||
|
||||
{% if ask_enabled %}
|
||||
<div class="hc-ai-wrap" id="hc-ai-wrap" hidden>
|
||||
<button class="hc-ask-btn" id="hc-ask-btn" type="button">
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.9 4.7L18.5 9l-4.6 1.3L12 15l-1.9-4.7L5.5 9l4.6-1.3z"></path></svg>
|
||||
<span id="hc-ask-label">Ask AI for an answer</span>
|
||||
</button>
|
||||
<div class="hc-ai" id="hc-ai-loading" hidden>
|
||||
<div class="hc-ai-head">
|
||||
<span class="hc-badge"><span class="hc-dots"><span></span><span></span><span></span></span> GENERATING ANSWER</span>
|
||||
<span class="hc-ai-note">Reading the knowledge base…</span>
|
||||
</div>
|
||||
<div class="hc-skeleton"><span style="width:96%"></span><span style="width:88%"></span><span style="width:64%"></span></div>
|
||||
</div>
|
||||
<div class="hc-ai" id="hc-ai-card" hidden>
|
||||
<div class="hc-ai-head">
|
||||
<span class="hc-badge">
|
||||
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.9 4.7L18.5 9l-4.6 1.3L12 15l-1.9-4.7L5.5 9l4.6-1.3z"></path></svg>
|
||||
AI ANSWER
|
||||
</span>
|
||||
</div>
|
||||
<p class="hc-ai-answer" id="hc-ai-answer"></p>
|
||||
{% if widget_id %}
|
||||
<div class="hc-ai-foot">
|
||||
<button class="hc-cta-btn" type="button" data-chat style="padding: 9px 15px; font-size: 13.5px; border-radius: 10px; background: var(--brand); color: var(--brand-ink);">
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.5 8.5 0 0 1-12.3 7.6L3 21l1.9-5.7A8.5 8.5 0 1 1 21 11.5z"></path></svg>
|
||||
Ask a follow-up in chat
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Search-results header (shown only while a query is active). -->
|
||||
<div class="hc-head" id="hc-head" hidden>
|
||||
<div class="hc-head-icon" id="hc-head-icon" style="--ic: #6d5bd0;">
|
||||
<svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
</div>
|
||||
<div style="min-width:0;">
|
||||
<h2 id="hc-head-title">Search results</h2>
|
||||
<div class="hc-head-sub" id="hc-head-sub"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flat, relevance-ranked results — cards move here while searching. -->
|
||||
<div class="hc-list" id="hc-search-results" hidden></div>
|
||||
|
||||
<!-- Browse view: every category with its own articles (Option B). On
|
||||
desktop all sections show and the sidebar jump-scrolls to them; on
|
||||
mobile the topic grid filters to one section at a time. -->
|
||||
<div class="hc-groups" id="hc-groups">
|
||||
{% for category, faqs in groups %}
|
||||
<section class="hc-group" id="cat-{{ loop.index0 }}" data-category="{{ category }}" style="--ic: {{ colors[category] }};">
|
||||
<div class="hc-head hc-group-head">
|
||||
<div class="hc-head-icon">
|
||||
<svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"></path></svg>
|
||||
</div>
|
||||
<div style="min-width:0;">
|
||||
<h2>{{ category }}</h2>
|
||||
<div class="hc-head-sub">{{ faqs | length }} {{ 'article' if faqs | length == 1 else 'articles' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hc-list">
|
||||
{% for faq in faqs %}
|
||||
<a class="hc-card" href="{{ base_path }}{{ faq.href }}" data-category="{{ category }}"
|
||||
data-title="{{ faq.search_title }}" data-search="{{ faq.search_text }}"
|
||||
style="--ic: {{ colors[category] }};">
|
||||
<span class="hc-card-main">
|
||||
<span class="hc-tag">{{ category }}</span>
|
||||
<span class="hc-qtext">{{ faq.question }}</span>
|
||||
<span class="hc-preview">{{ faq.preview }}</span>
|
||||
</span>
|
||||
<span class="hc-card-meta">
|
||||
<span class="hc-read">{{ faq.read_time }}</span>
|
||||
<span class="hc-chev">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="hc-empty" id="hc-empty"{% if groups %} hidden{% endif %}>
|
||||
<div class="hc-empty-icon">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
</div>
|
||||
<h3 id="hc-empty-title">{% if groups %}No results{% else %}No published FAQs yet{% endif %}</h3>
|
||||
<p id="hc-empty-sub">{% if groups %}Try a different search{% if widget_id %}, or chat with us — our assistant can help right away{% endif %}.{% else %}Check back soon.{% endif %}</p>
|
||||
{% if widget_id %}
|
||||
<button class="hc-cta-btn" type="button" data-chat style="background: var(--brand); color: var(--brand-ink);">
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.5 8.5 0 0 1-12.3 7.6L3 21l1.9-5.7A8.5 8.5 0 1 1 21 11.5z"></path></svg>
|
||||
Ask a question
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_scripts %}
|
||||
<script>
|
||||
(function () {
|
||||
document.body.classList.add('js');
|
||||
|
||||
var input = document.getElementById('hc-search');
|
||||
var clearBtn = document.getElementById('hc-clear');
|
||||
var topics = [].slice.call(document.querySelectorAll('.hc-topic'));
|
||||
var groupsWrap = document.getElementById('hc-groups');
|
||||
var groups = [].slice.call(document.querySelectorAll('.hc-group'));
|
||||
var cards = [].slice.call(document.querySelectorAll('.hc-card'));
|
||||
// Remember each card's home group + document order so search (which moves
|
||||
// matched cards into a flat ranked list) can put them back on clear.
|
||||
cards.forEach(function (c, i) { c._home = c.parentNode; c._idx = i; });
|
||||
var results = document.getElementById('hc-search-results');
|
||||
var head = document.getElementById('hc-head');
|
||||
var headTitle = document.getElementById('hc-head-title');
|
||||
var headSub = document.getElementById('hc-head-sub');
|
||||
var empty = document.getElementById('hc-empty');
|
||||
var emptyTitle = document.getElementById('hc-empty-title');
|
||||
var aiWrap = document.getElementById('hc-ai-wrap');
|
||||
var mq = window.matchMedia('(max-width: 860px)');
|
||||
var activeCat = topics.length ? topics[0].getAttribute('data-category') : null;
|
||||
|
||||
function resetAi() {
|
||||
if (!aiWrap) return;
|
||||
aiWrap.hidden = true;
|
||||
var c = document.getElementById('hc-ai-card'); if (c) c.hidden = true;
|
||||
var l = document.getElementById('hc-ai-loading'); if (l) l.hidden = true;
|
||||
var b = document.getElementById('hc-ask-btn'); if (b) b.hidden = false;
|
||||
}
|
||||
|
||||
function sectionFor(cat) {
|
||||
return groups.filter(function (g) { return g.getAttribute('data-category') === cat; })[0];
|
||||
}
|
||||
|
||||
// Return every card to its home group in original document order.
|
||||
function restoreCards() {
|
||||
cards.slice().sort(function (a, b) { return a._idx - b._idx; }).forEach(function (c) {
|
||||
c.hidden = false;
|
||||
if (c.parentNode !== c._home) c._home.appendChild(c);
|
||||
});
|
||||
}
|
||||
|
||||
// Desktop shows every category section; mobile shows one at a time.
|
||||
function applyGroupVisibility() {
|
||||
groups.forEach(function (g) {
|
||||
g.hidden = mq.matches && g.getAttribute('data-category') !== activeCat;
|
||||
});
|
||||
}
|
||||
|
||||
function markActive() {
|
||||
topics.forEach(function (x) { x.classList.toggle('active', x.getAttribute('data-category') === activeCat); });
|
||||
}
|
||||
|
||||
// ----- browse: grouped category sections -----
|
||||
function showBrowse() {
|
||||
document.body.classList.remove('hc-searching');
|
||||
restoreCards();
|
||||
if (results) results.hidden = true;
|
||||
if (head) head.hidden = true;
|
||||
if (empty) empty.hidden = !!groups.length;
|
||||
if (groupsWrap) groupsWrap.hidden = false;
|
||||
markActive();
|
||||
applyGroupVisibility();
|
||||
resetAi();
|
||||
}
|
||||
|
||||
// Pick a category: mobile filters to its section, desktop scrolls to it.
|
||||
function selectCategory(cat) {
|
||||
if (input) input.value = '';
|
||||
activeCat = cat;
|
||||
showBrowse();
|
||||
if (mq.matches) {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else {
|
||||
var sec = sectionFor(cat);
|
||||
if (sec) sec.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
// ----- search across all categories -----
|
||||
// Token-based relevance search: every query term must match somewhere
|
||||
// (word-prefix or substring), title hits outrank body hits, and results
|
||||
// are re-ordered by score — so "close my" finds "How to close my account".
|
||||
function termScore(term, title, titleWords, body) {
|
||||
var best = 0;
|
||||
for (var i = 0; i < titleWords.length; i++) {
|
||||
if (titleWords[i] === term) return 8; // exact title word
|
||||
if (titleWords[i].indexOf(term) === 0) best = 5; // title word prefix
|
||||
}
|
||||
if (!best && title.indexOf(term) !== -1) best = 3; // inside a title word
|
||||
if (!best) {
|
||||
if (body.indexOf(' ' + term) !== -1 || body.indexOf(term) === 0) best = 2; // body word start
|
||||
else if (body.indexOf(term) !== -1) best = 1; // anywhere in body
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// Partial-match semantics: a card qualifies when ANY term matches, but
|
||||
// cards matching MORE terms always rank above cards matching fewer —
|
||||
// so "close my" still surfaces "How to close your Atoa account" (no
|
||||
// literal "my") below any article containing both words.
|
||||
function cardScore(card, terms) {
|
||||
var title = card.getAttribute('data-title') || '';
|
||||
var body = card.getAttribute('data-search') || '';
|
||||
var titleWords = title.split(/\s+/);
|
||||
var matched = 0;
|
||||
var total = 0;
|
||||
for (var i = 0; i < terms.length; i++) {
|
||||
var s = termScore(terms[i], title, titleWords, body);
|
||||
if (s) { matched++; total += s; }
|
||||
}
|
||||
// Matched-term count dominates; per-term quality breaks ties.
|
||||
return matched ? matched * 100 + total : 0;
|
||||
}
|
||||
|
||||
function search(raw) {
|
||||
var q = (raw || '').trim().toLowerCase();
|
||||
if (clearBtn) clearBtn.style.display = q ? 'block' : 'none';
|
||||
document.body.classList.toggle('hc-searching', !!q);
|
||||
if (!q) { showBrowse(); return; }
|
||||
if (groupsWrap) groupsWrap.hidden = true;
|
||||
topics.forEach(function (x) { x.classList.remove('active'); });
|
||||
var terms = q.split(/\s+/);
|
||||
var matches = [];
|
||||
cards.forEach(function (card) {
|
||||
var score = cardScore(card, terms);
|
||||
card.hidden = !score;
|
||||
var tag = card.querySelector('.hc-tag');
|
||||
if (tag) tag.style.display = score ? 'inline-block' : '';
|
||||
if (score) matches.push({ card: card, score: score });
|
||||
});
|
||||
matches.sort(function (a, b) { return b.score - a.score; });
|
||||
matches.forEach(function (m) { results.appendChild(m.card); });
|
||||
if (results) results.hidden = false;
|
||||
var count = matches.length;
|
||||
if (head) {
|
||||
head.hidden = false;
|
||||
headTitle.textContent = 'Search results';
|
||||
headSub.textContent = count + (count === 1 ? ' answer' : ' answers') + ' for “' + raw.trim() + '”';
|
||||
}
|
||||
if (empty) {
|
||||
empty.hidden = count > 0;
|
||||
if (emptyTitle) emptyTitle.textContent = 'No results for “' + raw.trim() + '”';
|
||||
}
|
||||
if (aiWrap) {
|
||||
resetAi();
|
||||
aiWrap.hidden = false;
|
||||
var label = document.getElementById('hc-ask-label');
|
||||
if (label) label.textContent = 'Ask AI: “' + raw.trim() + '”';
|
||||
}
|
||||
}
|
||||
|
||||
// Honor ?topic=… so links from an article page land on the right category.
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var wantTopic = params.get('topic');
|
||||
|
||||
if (input) {
|
||||
input.addEventListener('input', function () { search(input.value); });
|
||||
if (input.value) search(input.value);
|
||||
else if (wantTopic && topics.some(function (t) { return t.getAttribute('data-category') === wantTopic; })) selectCategory(wantTopic);
|
||||
else showBrowse();
|
||||
} else {
|
||||
showBrowse();
|
||||
}
|
||||
if (clearBtn) clearBtn.addEventListener('click', function () { input.value = ''; input.focus(); search(''); });
|
||||
|
||||
topics.forEach(function (t) {
|
||||
t.addEventListener('click', function () { selectCategory(t.getAttribute('data-category')); });
|
||||
});
|
||||
[].slice.call(document.querySelectorAll('[data-jump]')).forEach(function (chip) {
|
||||
chip.addEventListener('click', function () { selectCategory(chip.getAttribute('data-jump')); });
|
||||
});
|
||||
|
||||
// Re-apply per-breakpoint group visibility when crossing 860px (not searching).
|
||||
mq.addEventListener('change', function () {
|
||||
if (!document.body.classList.contains('hc-searching')) applyGroupVisibility();
|
||||
});
|
||||
|
||||
// Desktop scroll-spy: highlight the category whose section is at the top of
|
||||
// the viewport as you scroll through the grouped list.
|
||||
var spyScheduled = false;
|
||||
function syncActiveToScroll() {
|
||||
spyScheduled = false;
|
||||
if (mq.matches || document.body.classList.contains('hc-searching')) return;
|
||||
var current = null;
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
if (groups[i].getBoundingClientRect().top - 100 <= 0) current = groups[i];
|
||||
else break;
|
||||
}
|
||||
if (!current) current = groups[0];
|
||||
if (current) {
|
||||
var cat = current.getAttribute('data-category');
|
||||
if (cat !== activeCat) { activeCat = cat; markActive(); }
|
||||
}
|
||||
}
|
||||
window.addEventListener('scroll', function () {
|
||||
if (!spyScheduled) { spyScheduled = true; window.requestAnimationFrame(syncActiveToScroll); }
|
||||
}, { passive: true });
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% if ask_enabled %}
|
||||
<script>
|
||||
(function () {
|
||||
var askBtn = document.getElementById('hc-ask-btn');
|
||||
var input = document.getElementById('hc-search');
|
||||
var loading = document.getElementById('hc-ai-loading');
|
||||
var card = document.getElementById('hc-ai-card');
|
||||
var answerEl = document.getElementById('hc-ai-answer');
|
||||
if (!askBtn) return;
|
||||
askBtn.addEventListener('click', function () {
|
||||
var question = (input.value || '').trim();
|
||||
if (question.length < 3) { input.focus(); return; }
|
||||
askBtn.hidden = true;
|
||||
card.hidden = true;
|
||||
loading.hidden = false;
|
||||
fetch('{{ base_path }}/ask', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ question: question })
|
||||
}).then(function (r) {
|
||||
if (!r.ok) throw new Error(r.status);
|
||||
return r.json();
|
||||
}).then(function (data) {
|
||||
loading.hidden = true;
|
||||
answerEl.textContent = data.answer; // text only — never innerHTML
|
||||
card.hidden = false;
|
||||
}).catch(function (err) {
|
||||
loading.hidden = true;
|
||||
answerEl.textContent = String(err.message) === '429'
|
||||
? 'Too many questions — please try again in a minute.'
|
||||
: 'Sorry, I could not answer that right now.';
|
||||
card.hidden = false;
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content">
|
||||
<meta name="theme-color" content="#0B0C10">
|
||||
<script>
|
||||
/* Applies the saved theme before first paint.
|
||||
useTheme.ts sets data-theme and theme-color too, but only once the bundle
|
||||
has parsed and run — so a light-theme user saw the dark shell, and dark
|
||||
browser chrome, flash on every launch. That is most obvious in the
|
||||
installed PWA, where the launch goes straight from a dark splash into the
|
||||
app. The manifest's own background_color cannot help: it is static and
|
||||
already matches the default (dark) theme.
|
||||
Keep the key and the two colours in step with src/composables/useTheme.ts
|
||||
and src/config/themeColors.ts — a static document cannot import them. */
|
||||
(function () {
|
||||
try {
|
||||
var stored = localStorage.getItem('rp-theme') || 'dark';
|
||||
var resolved = stored === 'system'
|
||||
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
||||
: (stored === 'light' ? 'light' : 'dark');
|
||||
document.documentElement.setAttribute('data-theme', resolved);
|
||||
var meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) meta.setAttribute('content', resolved === 'dark' ? '#0B0C10' : '#EEF0F4');
|
||||
} catch (e) {
|
||||
/* Private mode can throw on localStorage; the dark default already applies. */
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<!-- iOS installed-PWA (Add to Home Screen) -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="ReplyPier">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Instrument+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap">
|
||||
<title>ReplyPier · 智能客服工作台</title>
|
||||
<meta name="shopify-api-key" content="<%= VITE_SHOPIFY_API_KEY %>" />
|
||||
<!-- Shopify App Bridge, and the Polaris web components the Shopify views use.
|
||||
Only loaded inside the Shopify admin (host/shop query params present);
|
||||
outside it App Bridge throws "missing required configuration fields: shop"
|
||||
and both scripts are ~200KB of render-blocking script nothing else uses.
|
||||
async = false keeps them in order and ahead of the app bundle. Appending
|
||||
them beats document.write, which Chrome may block on a slow connection -
|
||||
that would leave the embedded admin with no Polaris and an unstyled UI. -->
|
||||
<script>
|
||||
(function () {
|
||||
var params = new URLSearchParams(window.location.search)
|
||||
if (!params.has('host') && !params.has('shop')) return
|
||||
var sources = [
|
||||
'https://cdn.shopify.com/shopifycloud/app-bridge.js',
|
||||
'https://cdn.shopify.com/shopifycloud/polaris.js'
|
||||
]
|
||||
sources.forEach(function (src) {
|
||||
var script = document.createElement('script')
|
||||
script.src = src
|
||||
script.async = false
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="/config.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="24px" height="24px" fill-rule="nonzero"><g fill="#000000" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(10.66667,10.66667)"><path d="M18,21h-12c-0.6,0 -1,-0.4 -1,-1v-8c0,-1.7 1.3,-3 3,-3h8c1.7,0 3,1.3 3,3v8c0,0.6 -0.4,1 -1,1z" opacity="0.3"></path><path d="M12,2c-1.10457,0 -2,0.89543 -2,2c0,1.10457 0.89543,2 2,2c1.10457,0 2,-0.89543 2,-2c0,-1.10457 -0.89543,-2 -2,-2zM9.5,13c-0.82843,0 -1.5,0.67157 -1.5,1.5c0,0.82843 0.67157,1.5 1.5,1.5c0.82843,0 1.5,-0.67157 1.5,-1.5c0,-0.82843 -0.67157,-1.5 -1.5,-1.5zM14.5,13c-0.82843,0 -1.5,0.67157 -1.5,1.5c0,0.82843 0.67157,1.5 1.5,1.5c0.82843,0 1.5,-0.67157 1.5,-1.5c0,-0.82843 -0.67157,-1.5 -1.5,-1.5z"></path><path d="M11,4h2v6h-2z"></path><path d="M18,22h-12c-1.1,0 -2,-0.9 -2,-2v-8c0,-2.2 1.8,-4 4,-4h8c2.2,0 4,1.8 4,4v8c0,1.1 -0.9,2 -2,2zM18,20v1v-1zM8,10c-1.1,0 -2,0.9 -2,2v8h12v-8c0,-1.1 -0.9,-2 -2,-2z"></path><path d="M9,18h6v3h-6zM2,16h3v2h-3zM19,16h3v2h-3z"></path></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 3V19C3 20.1046 3.89543 21 5 21H21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M7 14L11 10L15 14L21 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="7" cy="14" r="2" fill="currentColor"/>
|
||||
<circle cx="11" cy="10" r="2" fill="currentColor"/>
|
||||
<circle cx="15" cy="14" r="2" fill="currentColor"/>
|
||||
<circle cx="21" cy="8" r="2" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 592 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 226 B |
@@ -0,0 +1,10 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background Circle -->
|
||||
<circle cx="20" cy="20" r="20" fill="#f34611"/>
|
||||
|
||||
<!-- Head -->
|
||||
<circle cx="20" cy="16" r="7" fill="white"/>
|
||||
|
||||
<!-- Body -->
|
||||
<path d="M20 25C15.8 25 8 27.2 8 31.4V35H32V31.4C32 27.2 24.2 25 20 25Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 373 B |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 237 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 50 50">
|
||||
<path d="M 25 4.5 C 15.204 4.5 5.9439688 11.985969 3.9179688 21.542969 C 3.9119687 21.571969 3.9200156 21.599906 3.9160156 21.628906 C 1.5620156 23.233906 -0.04296875 26.383 -0.04296875 30 C -0.04296875 35.238 3.3210312 39.5 7.4570312 39.5 C 7.7850313 39.5 8.0913438 39.339313 8.2773438 39.070312 C 8.4643437 38.800312 8.5065781 38.456438 8.3925781 38.148438 C 8.3775781 38.110438 6.9550781 34.244 6.9550781 29.5 C 6.9550781 24.506 8.3091719 22.022187 8.3261719 21.992188 C 8.5011719 21.683187 8.4983125 21.305047 8.3203125 20.998047 C 8.1433125 20.689047 7.8130313 20.5 7.4570312 20.5 C 7.0350313 20.5 6.62275 20.554625 6.21875 20.640625 C 8.58675 12.613625 16.57 6.5 25 6.5 C 32.992 6.5 40.688641 12.044172 43.431641 19.576172 C 43.133641 19.530172 42.831438 19.5 42.523438 19.5 C 42.169438 19.5 41.841109 19.689094 41.662109 19.996094 C 41.482109 20.302094 41.481297 20.683187 41.654297 20.992188 C 41.668297 21.016188 43.023437 23.5 43.023438 28.5 C 43.023438 32.44 42.045078 35.767641 41.705078 36.806641 C 40.558078 37.740641 38.815344 39.034297 36.777344 40.154297 C 36.016344 39.305297 34.839391 38.871437 33.650391 39.148438 L 31.867188 39.560547 C 31.024188 39.753547 30.308609 40.262094 29.849609 40.996094 C 29.391609 41.728094 29.245453 42.598453 29.439453 43.439453 C 29.783453 44.935453 31.11975 45.949219 32.59375 45.949219 C 32.83275 45.949219 33.074359 45.923188 33.318359 45.867188 L 35.103516 45.457031 C 35.945516 45.264031 36.661141 44.752531 37.119141 44.019531 C 37.503141 43.406531 37.653984 42.700187 37.583984 41.992188 C 39.728984 40.830188 41.570453 39.483422 42.814453 38.482422 C 46.814453 38.286422 50.023438 34.114 50.023438 29 C 50.023438 25.237 48.284437 21.989172 45.773438 20.451172 C 45.769438 20.376172 45.777859 20.301563 45.755859 20.226562 C 43.152859 11.113563 34.423 4.5 25 4.5 z M 12 19 C 11.447 19 11 19.447 11 20 L 11 32 C 11 32.553 11.447 33 12 33 L 28.044922 33 C 27.540922 34.057 26.743578 35.482375 26.142578 36.484375 C 25.941578 36.819375 25.954828 37.2405 26.173828 37.5625 C 26.360828 37.8395 26.673 38 27 38 C 27.055 38 27.109063 37.995328 27.164062 37.986328 C 33.351062 36.955328 38.412 32.95125 38.625 32.78125 C 38.862 32.59125 39 32.304 39 32 L 39 20 C 39 19.447 38.553 19 38 19 L 12 19 z M 18.5 23 C 19.902 23 21 24.317 21 26 C 21 27.683 19.902 29 18.5 29 C 17.098 29 16 27.683 16 26 C 16 24.317 17.098 23 18.5 23 z M 31.5 23 C 32.902 23 34 24.317 34 26 C 34 27.683 32.902 29 31.5 29 C 30.098 29 29 27.683 29 26 C 29 24.317 30.098 23 31.5 23 z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,119 @@
|
||||
/* ReplyPier base — global resets & keyframes */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
--vt-c-indigo: #2c3e50;
|
||||
--background-base-rgb: 11,12,16;
|
||||
--background-soft-rgb: 14,15,20;
|
||||
--background-dark-rgb: 0,0,0;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
html, body {
|
||||
min-height: 100vh;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1.6;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Display headings */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: var(--font-weight-bold);
|
||||
letter-spacing: var(--tracking-display);
|
||||
color: var(--text);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Inputs inherit font */
|
||||
input, textarea, select, button {
|
||||
font-family: var(--font-sans);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
input::placeholder, textarea::placeholder {
|
||||
color: var(--faint);
|
||||
}
|
||||
|
||||
/* Text selection */
|
||||
::selection {
|
||||
background: #C9F24E;
|
||||
color: #0B0C10;
|
||||
}
|
||||
|
||||
[data-theme="light"] ::selection {
|
||||
background: #4C7A0B;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Scrollbars */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--o10);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--o16);
|
||||
}
|
||||
|
||||
/* Global focus ring */
|
||||
:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--ring-focus);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* Keyframe animations */
|
||||
@keyframes rp-spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes rp-pulse {
|
||||
0%, 100% { opacity: .4; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes rp-aurora {
|
||||
0% { transform: translate3d(-4%,-2%,0) scale(1); }
|
||||
50% { transform: translate3d(4%,3%,0) scale(1.12); }
|
||||
100% { transform: translate3d(-4%,-2%,0) scale(1); }
|
||||
}
|
||||
|
||||
@keyframes rp-float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-12px); }
|
||||
}
|
||||
|
||||
@keyframes button-loading-spinner {
|
||||
from { transform: rotate(0turn); }
|
||||
to { transform: rotate(1turn); }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#9CA3AF">
|
||||
<path d="M12 2C6.477 2 2 5.582 2 10c0 2.136 1.01 4.08 2.636 5.546.139.127.22.31.22.504v3.45l4.329-2.164c.175-.088.37-.135.568-.135h.494C11.447 17.73 12.947 18 14.5 18c5.523 0 10-3.582 10-8s-4.477-8-10-8zm0 14c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 351 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Amazon CloudWatch</title><rect x="1" y="1" width="22" height="22" rx="4.5" fill="#E7157B"/><path d="M3.6 13.2h3.4l1.9-4.9 2.7 7.4 1.9-4.6 1.2 2.1h5.7" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 329 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="50px" height="50px" fill-rule="nonzero"><g fill="#000000" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.12,5.12)"><path d="M41.5,0c-4.68248,0 -8.5,3.81752 -8.5,8.5c0,0.98423 0.29088,1.87869 0.59961,2.75586l-7.53516,7.5332c-1.41239,1.41239 -1.41239,3.73214 0,5.14453c1.41239,1.41239 3.7341,1.41239 5.14648,0l7.53125,-7.5332c0.8779,0.30938 1.77344,0.59961 2.75781,0.59961c4.68248,0 8.5,-3.81752 8.5,-8.5c0,-0.93143 -0.15402,-1.82719 -0.42969,-2.66211l-0.49609,-1.50195l-1.14453,1.09375l-4.66211,4.45313l-2.45703,-0.69336l-0.69336,-2.45703l5.54687,-5.80859l-1.50391,-0.49414c-0.83396,-0.27422 -1.72872,-0.42969 -2.66016,-0.42969zM41.5,2c0.11179,0 0.21006,0.04507 0.32031,0.05078l-3.9375,4.125l1.30664,4.63477l4.63281,1.30664l4.125,-3.93945c0.00579,0.11102 0.05273,0.20972 0.05273,0.32227c0,3.60152 -2.89848,6.5 -6.5,6.5c-0.95719,0 -1.85793,-0.21152 -2.68164,-0.58594l-0.63086,-0.28711l-8.39258,8.39258c-0.64761,0.64761 -1.66879,0.64761 -2.31641,0c-0.64761,-0.64761 -0.64761,-1.66879 0,-2.31641l8.39258,-8.39062l-0.28711,-0.63086c-0.37353,-0.82282 -0.58398,-1.72445 -0.58398,-2.68164c0,-3.60152 2.89848,-6.5 6.5,-6.5zM8,8c-2.206,0 -4,1.794 -4,4v22c0,0.73222 0.21188,1.4091 0.55664,2h-2.55664c-0.55226,0.00006 -0.99994,0.44774 -1,1c0,2.74952 2.25048,5 5,5h38c2.74952,0 5,-2.25048 5,-5c-0.00006,-0.55226 -0.44774,-0.99994 -1,-1h-2.55664c0.34476,-0.5909 0.55664,-1.26778 0.55664,-2v-16.02344c-0.635,0.302 -1.303,0.53994 -2,0.71094v15.3125c0,1.103 -0.897,2 -2,2h-34c-1.103,0 -2,-0.897 -2,-2v-22c0,-1.103 0.897,-2 2,-2h23.11914c-0.07,-0.491 -0.11914,-0.989 -0.11914,-1.5c0,-0.169 0.01739,-0.333 0.02539,-0.5zM3.41211,38h4.58789h34h4.58789c-0.43709,1.11247 -1.31204,2 -2.58789,2h-38c-1.27585,0 -2.1508,-0.88753 -2.58789,-2z"></path></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="50px" height="50px" fill-rule="nonzero"><g fill="#000000" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.12,5.12)"><path d="M7,4c-2.74609,0 -5,2.25391 -5,5v24c0,2.74609 2.25391,5 5,5h4.09375c0.13672,1.20313 0.12109,2.31641 -0.1875,3.25c-0.37891,1.14844 -1.08594,2.11328 -2.40625,2.90625c-0.37109,0.23438 -0.54297,0.68359 -0.42969,1.10547c0.11328,0.42188 0.49219,0.72266 0.92969,0.73828c4.24219,0 9.10547,-2.21484 11.5625,-8h22.4375c2.74609,0 5,-2.25391 5,-5v-24c0,-2.74609 -2.25391,-5 -5,-5zM7,6h36c1.65625,0 3,1.34375 3,3v24c0,1.65625 -1.34375,3 -3,3h-23c-0.41797,0 -0.79297,0.26172 -0.9375,0.65625c-1.55469,4.24219 -4.33203,6.26172 -7.21875,7c0.39063,-0.55859 0.76172,-1.14844 0.96875,-1.78125c0.51953,-1.57812 0.47656,-3.30469 0.15625,-5.0625c-0.08984,-0.46484 -0.49219,-0.80469 -0.96875,-0.8125h-5c-1.65625,0 -3,-1.34375 -3,-3v-24c0,-1.65625 1.34375,-3 3,-3z"></path></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" width="30px" height="30px"> <path d="M 14.984375 2.4863281 A 1.0001 1.0001 0 0 0 14 3.5 L 14 4 L 8.5 4 A 1.0001 1.0001 0 0 0 7.4863281 5 L 6 5 A 1.0001 1.0001 0 1 0 6 7 L 24 7 A 1.0001 1.0001 0 1 0 24 5 L 22.513672 5 A 1.0001 1.0001 0 0 0 21.5 4 L 16 4 L 16 3.5 A 1.0001 1.0001 0 0 0 14.984375 2.4863281 z M 6 9 L 7.7929688 24.234375 C 7.9109687 25.241375 8.7633438 26 9.7773438 26 L 20.222656 26 C 21.236656 26 22.088031 25.241375 22.207031 24.234375 L 24 9 L 6 9 z"/></svg>
|
||||
|
After Width: | Height: | Size: 560 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 320 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Elasticsearch</title><path fill="#00BFB3" d="M13.394 0C8.683 0 4.609 2.716 2.644 6.667h15.641a4.77 4.77 0 0 0 3.073-1.11c.446-.375.864-.785 1.247-1.243l.001-.002A11.974 11.974 0 0 0 13.394 0zM1.804 8.889a12.009 12.009 0 0 0 0 6.222h14.7a3.111 3.111 0 1 0 0-6.222zm.84 8.444C4.61 21.283 8.684 24 13.395 24c3.701 0 7.011-1.677 9.212-4.312l-.001-.002a9.958 9.958 0 0 0-1.247-1.243 4.77 4.77 0 0 0-3.073-1.11z"/></svg>
|
||||
|
After Width: | Height: | Size: 493 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
||||
<circle cx="120" cy="120" r="120" fill="#6366F1"/>
|
||||
<rect x="52" y="78" width="136" height="88" rx="12" fill="#FFF"/>
|
||||
<path d="M58 88l62 46 62-46" fill="none" stroke="#6366F1" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 318 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Grafana</title><path fill="#F46800" d="M23.02 10.59a8.578 8.578 0 0 0-.862-3.034 8.911 8.911 0 0 0-1.789-2.445c.337-1.342-.413-2.505-.413-2.505-1.292-.08-2.113.4-2.416.62-.052-.02-.102-.044-.154-.064-.22-.089-.446-.172-.677-.247-.231-.073-.47-.14-.711-.197a9.867 9.867 0 0 0-.875-.161C14.557.753 12.94 0 12.94 0c-1.804 1.145-2.147 2.744-2.147 2.744l-.018.093c-.098.029-.2.057-.298.088-.138.042-.275.094-.413.143-.138.055-.275.107-.41.166a8.869 8.869 0 0 0-1.557.87l-.063-.029c-2.497-.955-4.716.195-4.716.195-.203 2.658.996 4.33 1.235 4.636a11.608 11.608 0 0 0-.607 2.635C1.636 12.677.953 15.014.953 15.014c1.926 2.214 4.171 2.351 4.171 2.351.003-.002.006-.002.006-.005.285.509.615.994.986 1.446.156.19.32.371.488.548-.704 2.009.099 3.68.099 3.68 2.144.08 3.553-.937 3.849-1.173a9.784 9.784 0 0 0 3.164.501h.08l.055-.003.107-.002.103-.005.003.002c1.01 1.44 2.788 1.646 2.788 1.646 1.264-1.332 1.337-2.653 1.337-2.94v-.058c0-.02-.003-.039-.003-.06.265-.187.52-.387.758-.6a7.875 7.875 0 0 0 1.415-1.7c1.43.083 2.437-.885 2.437-.885-.236-1.49-1.085-2.216-1.264-2.354l-.018-.013-.016-.013a.217.217 0 0 1-.031-.02c.008-.092.016-.18.02-.27.011-.162.016-.323.016-.48v-.253l-.005-.098-.008-.135a1.891 1.891 0 0 0-.01-.13c-.003-.042-.008-.083-.013-.125l-.016-.124-.018-.122a6.215 6.215 0 0 0-2.032-3.73 6.015 6.015 0 0 0-3.222-1.46 6.292 6.292 0 0 0-.85-.048l-.107.002h-.063l-.044.003-.104.008a4.777 4.777 0 0 0-3.335 1.695c-.332.4-.592.84-.768 1.297a4.594 4.594 0 0 0-.312 1.817l.003.091c.005.055.007.11.013.164a3.615 3.615 0 0 0 .698 1.82 3.53 3.53 0 0 0 1.827 1.282c.33.098.66.14.971.137.039 0 .078 0 .114-.002l.063-.003c.02 0 .041-.003.062-.003.034-.002.065-.007.099-.01.007 0 .018-.003.028-.003l.031-.005.06-.008a1.18 1.18 0 0 0 .112-.02c.036-.008.072-.013.109-.024a2.634 2.634 0 0 0 .914-.415c.028-.02.056-.041.085-.065a.248.248 0 0 0 .039-.35.244.244 0 0 0-.309-.06l-.078.042c-.09.044-.184.083-.283.116a2.476 2.476 0 0 1-.475.096c-.028.003-.054.006-.083.006l-.083.002c-.026 0-.054 0-.08-.002l-.102-.006h-.012l-.024.006c-.016-.003-.031-.003-.044-.006-.031-.002-.06-.007-.091-.01a2.59 2.59 0 0 1-.724-.213 2.557 2.557 0 0 1-.667-.438 2.52 2.52 0 0 1-.805-1.475 2.306 2.306 0 0 1-.029-.444l.006-.122v-.023l.002-.031c.003-.021.003-.04.005-.06a3.163 3.163 0 0 1 1.352-2.29 3.12 3.12 0 0 1 .937-.43 2.946 2.946 0 0 1 .776-.101h.06l.07.002.045.003h.026l.07.005a4.041 4.041 0 0 1 1.635.49 3.94 3.94 0 0 1 1.602 1.662 3.77 3.77 0 0 1 .397 1.414l.005.076.003.075c.002.026.002.05.002.075 0 .024.003.052 0 .07v.065l-.002.073-.008.174a6.195 6.195 0 0 1-.08.639 5.1 5.1 0 0 1-.267.927 5.31 5.31 0 0 1-.624 1.13 5.052 5.052 0 0 1-3.237 2.014 4.82 4.82 0 0 1-.649.066l-.039.003h-.287a6.607 6.607 0 0 1-1.716-.265 6.776 6.776 0 0 1-3.4-2.274 6.75 6.75 0 0 1-.746-1.15 6.616 6.616 0 0 1-.714-2.596l-.005-.083-.002-.02v-.056l-.003-.073v-.096l-.003-.104v-.07l.003-.163c.008-.22.026-.45.054-.678a8.707 8.707 0 0 1 .28-1.355c.128-.444.286-.872.473-1.277a7.04 7.04 0 0 1 1.456-2.1 5.925 5.925 0 0 1 .953-.763c.169-.111.343-.213.524-.306.089-.05.182-.091.273-.135.047-.02.093-.042.138-.062a7.177 7.177 0 0 1 .714-.267l.145-.045c.049-.015.098-.026.148-.041.098-.029.197-.052.296-.076.049-.013.1-.02.15-.033l.15-.032.151-.028.076-.013.075-.01.153-.024c.057-.01.114-.013.171-.023l.169-.021c.036-.003.073-.008.106-.01l.073-.008.036-.003.042-.002c.057-.003.114-.008.171-.01l.086-.006h.023l.037-.003.145-.007a7.999 7.999 0 0 1 1.708.125 7.917 7.917 0 0 1 2.048.68 8.253 8.253 0 0 1 1.672 1.09l.09.077.089.078c.06.052.114.107.171.159.057.052.112.106.166.16.052.055.107.107.159.164a8.671 8.671 0 0 1 1.41 1.978c.012.026.028.052.04.078l.04.078.075.156c.023.051.05.1.07.153l.065.15a8.848 8.848 0 0 1 .45 1.34.19.19 0 0 0 .201.142.186.186 0 0 0 .172-.184c.01-.246.002-.532-.024-.856z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="50px" height="50px" fill-rule="nonzero"><g fill="#ffffff" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.12,5.12)"><path d="M24.99805,6.00195c-0.35375,0 -0.70564,0.12033 -0.99414,0.36133l-16.82422,14.05664c-0.212,0.178 -0.24145,0.49308 -0.06445,0.70508c0.179,0.212 0.49308,0.2405 0.70508,0.0625l2.17969,-1.82031v16.63281c0,1.657 1.343,3 3,3h24c1.657,0 3,-1.343 3,-3v-16.63281l2.17969,1.82227c0.093,0.078 0.20731,0.11523 0.32031,0.11523c0.143,0 0.28577,-0.05973 0.38477,-0.17773c0.177,-0.212 0.14755,-0.52808 -0.06445,-0.70508l-2.82031,-2.35547v-7.06641c0,-0.552 -0.448,-1 -1,-1h-1c-0.552,0 -1,0.448 -1,1v4.55859l-11.00586,-9.19531c-0.2885,-0.241 -0.64234,-0.36133 -0.99609,-0.36133zM24.99805,7.00391c0.12825,0 0.25683,0.04336 0.36133,0.13086l13.64063,11.39844v17.4668c0,1.105 -0.895,2 -2,2h-7v-11.33203c0,-0.921 -0.74602,-1.66797 -1.66602,-1.66797h-6.66602c-0.921,0 -1.66797,0.74697 -1.66797,1.66797v11.33203h-7c-1.105,0 -2,-0.895 -2,-2v-17.46875l13.63867,-11.39648c0.1045,-0.0875 0.23112,-0.13086 0.35938,-0.13086z"></path></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M17.2 8.9V6.1a2 2 0 1 0-1.6 0v2.8a5.6 5.6 0 0 0-2.6 1.1L7.6 6.2a2.2 2.2 0 1 0-1.1 1.3l5.2 3.7a5.6 5.6 0 1 0 5.5-2.3Zm-.8 8.9a3.2 3.2 0 1 1 0-6.4 3.2 3.2 0 0 1 0 6.4Z" fill="#FF7A59"/>
|
||||
<path d="m8.3 15.6-2 1.9a1.8 1.8 0 1 0 1.2 1.2l2-1.9a5.7 5.7 0 0 1-1.2-1.2Z" fill="#FF7A59"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 371 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="30px" height="30px" fill-rule="nonzero"><g fill="#000000" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(8.53333,8.53333)"><path d="M14,2v8.14063c1.722,0.446 3,1.99738 3,3.85938c0,0.631 -0.15892,1.22095 -0.41992,1.75195l2.61523,2.2793c0.098,-0.021 0.19969,-0.03125 0.30469,-0.03125c0.828,0 1.5,0.672 1.5,1.5c0,0.828 -0.672,1.5 -1.5,1.5c-0.775,0 -1.40638,-0.5907 -1.48437,-1.3457l-2.73047,-2.37891c-0.909,0.636 -2.08689,0.91289 -3.33789,0.58789c-1.399,-0.362 -2.51984,-1.51773 -2.83984,-2.92773c-0.514,-2.26 0.88858,-4.27492 2.89258,-4.79492v-7.95117c-5.119,0.942 -9,5.41955 -9,10.81055c0,4.026 2.25142,6.79659 3.73242,8.18359c0.81,0.758 1.26758,1.81092 1.26758,2.91992v3.89648h10v-1.55859c0,-0.861 0.55019,-1.62448 1.36719,-1.89648l2.58203,-0.85937c1.225,-0.41 2.05078,-1.55666 2.05078,-2.84766v-3.09375l2.26758,-0.78125c0.43262,-0.12014 0.73213,-0.5139 0.73242,-0.96289c-0.00069,-0.23009 -0.08071,-0.4529 -0.22656,-0.63086l-0.02734,-0.0332c-0.00952,-0.01128 -0.01929,-0.02235 -0.0293,-0.0332l-2.75586,-3.30469c-0.40743,-5.60491 -4.22408,-9.99805 -9.96094,-9.99805zM13,12c-1.10457,0 -2,0.89543 -2,2c0,1.10457 0.89543,2 2,2c1.10457,0 2,-0.89543 2,-2c0,-1.10457 -0.89543,-2 -2,-2z"></path></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.0425 12.2025C17.0139 9.34808 19.4159 7.95105 19.5279 7.88465C18.2523 6.06609 16.2641 5.80104 15.5675 5.77504C13.8824 5.59836 12.2523 6.79092 11.3935 6.79092C10.5151 6.79092 9.18511 5.79705 7.77308 5.82905C5.93451 5.86105 4.22497 6.91695 3.28739 8.54686C1.35364 11.8747 2.79168 16.7843 4.64625 19.5907C5.56783 20.9647 6.64973 22.4968 8.09977 22.4448C9.51779 22.3868 10.0576 21.5344 11.7687 21.5344C13.4602 21.5344 13.9641 22.4448 15.4401 22.4128C16.9642 22.3868 17.9018 21.0127 18.7874 19.6287C19.8433 18.0508 20.2752 16.5027 20.2952 16.4247C20.2552 16.4087 17.0745 15.1522 17.0425 12.2025Z" fill="black"/>
|
||||
<path d="M14.7278 3.6875C15.4884 2.75592 16.0083 1.47536 15.8643 0.179688C14.7478 0.227687 13.3878 0.931695 12.5992 1.83927C11.8947 2.64766 11.2708 3.96022 11.4388 5.2208C12.6824 5.3208 13.9392 4.60108 14.7278 3.6875Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 999 B |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.7071 4.29289C14.0976 4.68342 14.0976 5.31658 13.7071 5.70711L7.70711 11.7071C7.31658 12.0976 6.68342 12.0976 6.29289 11.7071L2.29289 7.70711C1.90237 7.31658 1.90237 6.68342 2.29289 6.29289C2.68342 5.90237 3.31658 5.90237 3.70711 6.29289L7 9.58579L12.2929 4.29289C12.6834 3.90237 13.3166 3.90237 13.7071 4.29289Z" fill="#10B981"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 487 B |
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.8055 10.0415H21V10H12V14H17.6515C16.827 16.3285 14.6115 18 12 18C8.6865 18 6 15.3135 6 12C6 8.6865 8.6865 6 12 6C13.5295 6 14.921 6.577 15.9805 7.5195L18.809 4.691C17.023 3.0265 14.634 2 12 2C6.4775 2 2 6.4775 2 12C2 17.5225 6.4775 22 12 22C17.5225 22 22 17.5225 22 12C22 11.3295 21.931 10.675 21.8055 10.0415Z" fill="#FFC107"/>
|
||||
<path d="M3.15302 7.3455L6.43852 9.755C7.32752 7.554 9.48052 6 12 6C13.5295 6 14.921 6.577 15.9805 7.5195L18.809 4.691C17.023 3.0265 14.634 2 12 2C8.15902 2 4.82802 4.1685 3.15302 7.3455Z" fill="#FF3D00"/>
|
||||
<path d="M12 22C14.583 22 16.93 21.0115 18.7045 19.404L15.6095 16.785C14.5718 17.5742 13.3038 18.0011 12 18C9.39897 18 7.19047 16.3415 6.35847 14.027L3.09747 16.5395C4.75247 19.778 8.11347 22 12 22Z" fill="#4CAF50"/>
|
||||
<path d="M21.8055 10.0415H21V10H12V14H17.6515C17.2571 15.1082 16.5467 16.0766 15.608 16.7855L15.6095 16.7845L18.7045 19.4035C18.4855 19.6025 22 17 22 12C22 11.3295 21.931 10.675 21.8055 10.0415Z" fill="#1976D2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
||||
<defs>
|
||||
<radialGradient id="ig-grad" cx="0.3" cy="1.07" r="1.3">
|
||||
<stop offset="0" stop-color="#fdf497"/>
|
||||
<stop offset="0.05" stop-color="#fdf497"/>
|
||||
<stop offset="0.45" stop-color="#fd5949"/>
|
||||
<stop offset="0.6" stop-color="#d6249f"/>
|
||||
<stop offset="0.9" stop-color="#285AEB"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect x="28" y="28" width="184" height="184" rx="44" fill="url(#ig-grad)"/>
|
||||
<rect x="66" y="66" width="108" height="108" rx="30" fill="none" stroke="#FFF" stroke-width="12"/>
|
||||
<circle cx="120" cy="120" r="27" fill="none" stroke="#FFF" stroke-width="12"/>
|
||||
<circle cx="153" cy="87" r="8.5" fill="#FFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 721 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 11H9V5H4V11ZM4 18H9V12H4V18ZM10 18H15V12H10V18ZM16 18H21V12H16V18ZM10 11H15V5H10V11ZM16 5V11H21V5H16Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 242 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><defs><linearGradient id="jira-original-a" gradientUnits="userSpaceOnUse" x1="22.034" y1="9.773" x2="17.118" y2="14.842" gradientTransform="scale(4)"><stop offset=".176" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient><linearGradient id="jira-original-b" gradientUnits="userSpaceOnUse" x1="16.641" y1="15.564" x2="10.957" y2="21.094" gradientTransform="scale(4)"><stop offset=".176" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient></defs><path d="M108.023 16H61.805c0 11.52 9.324 20.848 20.847 20.848h8.5v8.226c0 11.52 9.328 20.848 20.848 20.848V19.977A3.98 3.98 0 00108.023 16zm0 0" fill="#2684ff"/><path d="M85.121 39.04H38.902c0 11.519 9.325 20.847 20.844 20.847h8.504v8.226c0 11.52 9.328 20.848 20.848 20.848V43.016a3.983 3.983 0 00-3.977-3.977zm0 0" fill="url(#jira-original-a)"/><path d="M62.219 62.078H16c0 11.524 9.324 20.848 20.848 20.848h8.5v8.23c0 11.52 9.328 20.844 20.847 20.844V66.059a3.984 3.984 0 00-3.976-3.98zm0 0" fill="url(#jira-original-b)"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#9CA3AF">
|
||||
<path d="M12 2L2 6v12l10 4 10-4V6L12 2zm0 2.09l7 2.81v8.19l-7 2.81V4.09zM5 15.18V7l5 2v8.18l-5-2z"/>
|
||||
<path d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 233 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
||||
<rect x="20" y="20" width="200" height="200" rx="48" fill="#06C755"/>
|
||||
<path d="M120 62c-38.7 0-70 25.6-70 57.2 0 28.3 25.1 52 59 56.5 2.3.5 5.4 1.5 6.2 3.5.7 1.8.5 4.5.2 6.3l-1 6c-.3 1.8-1.4 7 6.2 3.8s40.9-24.1 55.8-41.2c10.3-11.3 13.6-22.8 13.6-34.9 0-31.6-31.3-57.2-70-57.2z" fill="#FFF"/>
|
||||
<path d="M92 105v32M78 137h14m24-32v32m14-32v32m0-16l16 16m0-32l-16 16m40-16v32h16" fill="none" stroke="#06C755" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 547 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="32" height="32" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="20" y="20" width="80" height="80" rx="22" fill="#C9F24E"/>
|
||||
<circle cx="42" cy="60" r="6" fill="#0B0C10"/>
|
||||
<circle cx="60" cy="60" r="6" fill="#0B0C10"/>
|
||||
<circle cx="78" cy="60" r="6" fill="#0B0C10"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 322 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="20" y="20" width="80" height="80" rx="22" fill="#C9F24E"/>
|
||||
<circle cx="42" cy="60" r="6" fill="#0B0C10"/>
|
||||
<circle cx="60" cy="60" r="6" fill="#0B0C10"/>
|
||||
<circle cx="78" cy="60" r="6" fill="#0B0C10"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 324 B |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
||||
<defs>
|
||||
<radialGradient id="msgr-grad" cx="0.19" cy="0.99" r="1.08">
|
||||
<stop offset="0" stop-color="#09f"/>
|
||||
<stop offset="0.6" stop-color="#a033ff"/>
|
||||
<stop offset="0.93" stop-color="#ff5280"/>
|
||||
<stop offset="1" stop-color="#ff7061"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<path fill="url(#msgr-grad)" d="M120 28c-52.3 0-92 38.3-92 90 0 27 10.9 50.4 28.7 66.6a7.3 7.3 0 0 1 2.5 5.2l.5 16.3a7.3 7.3 0 0 0 10.3 6.5l18.2-8a7.4 7.4 0 0 1 4.9-.4 100 100 0 0 0 26.9 3.6c52.3 0 92-38.3 92-89.8S172.3 28 120 28z"/>
|
||||
<path fill="#FFF" d="M64.8 144.3l27-42.9a13.8 13.8 0 0 1 20-3.7l21.5 16.1a5.5 5.5 0 0 0 6.7 0L169 91.8c3.9-2.9 8.9 1.7 6.3 5.8l-27 42.8a13.8 13.8 0 0 1-20 3.7l-21.5-16.1a5.5 5.5 0 0 0-6.7 0l-29 22c-3.9 3-8.9-1.6-6.3-5.7z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 824 B |
@@ -0,0 +1 @@
|
||||
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path d="M 25 0 C 22.800781 0 21 1.800781 21 4 C 21 4.515625 21.101563 5.015625 21.28125 5.46875 C 15.65625 6.929688 12 11.816406 12 18 C 12 25.832031 10.078125 29.398438 8.25 31.40625 C 7.335938 32.410156 6.433594 33.019531 5.65625 33.59375 C 5.265625 33.878906 4.910156 34.164063 4.59375 34.53125 C 4.277344 34.898438 4 35.421875 4 36 C 4 37.375 4.84375 38.542969 6.03125 39.3125 C 7.21875 40.082031 8.777344 40.578125 10.65625 40.96875 C 13.09375 41.472656 16.101563 41.738281 19.40625 41.875 C 19.15625 42.539063 19 43.253906 19 44 C 19 47.300781 21.699219 50 25 50 C 28.300781 50 31 47.300781 31 44 C 31 43.25 30.847656 42.535156 30.59375 41.875 C 33.898438 41.738281 36.90625 41.472656 39.34375 40.96875 C 41.222656 40.578125 42.78125 40.082031 43.96875 39.3125 C 45.15625 38.542969 46 37.375 46 36 C 46 35.421875 45.722656 34.898438 45.40625 34.53125 C 45.089844 34.164063 44.734375 33.878906 44.34375 33.59375 C 43.566406 33.019531 42.664063 32.410156 41.75 31.40625 C 39.921875 29.398438 38 25.832031 38 18 C 38 11.820313 34.335938 6.9375 28.71875 5.46875 C 28.898438 5.015625 29 4.515625 29 4 C 29 1.800781 27.199219 0 25 0 Z M 25 2 C 26.117188 2 27 2.882813 27 4 C 27 5.117188 26.117188 6 25 6 C 23.882813 6 23 5.117188 23 4 C 23 2.882813 23.882813 2 25 2 Z M 27.34375 7.1875 C 32.675781 8.136719 36 12.257813 36 18 C 36 26.167969 38.078125 30.363281 40.25 32.75 C 41.335938 33.941406 42.433594 34.6875 43.15625 35.21875 C 43.515625 35.484375 43.785156 35.707031 43.90625 35.84375 C 44.027344 35.980469 44 35.96875 44 36 C 44 36.625 43.710938 37.082031 42.875 37.625 C 42.039063 38.167969 40.679688 38.671875 38.9375 39.03125 C 35.453125 39.753906 30.492188 40 25 40 C 19.507813 40 14.546875 39.753906 11.0625 39.03125 C 9.320313 38.671875 7.960938 38.167969 7.125 37.625 C 6.289063 37.082031 6 36.625 6 36 C 6 35.96875 5.972656 35.980469 6.09375 35.84375 C 6.214844 35.707031 6.484375 35.484375 6.84375 35.21875 C 7.566406 34.6875 8.664063 33.941406 9.75 32.75 C 11.921875 30.363281 14 26.167969 14 18 C 14 12.261719 17.328125 8.171875 22.65625 7.21875 C 23.320313 7.707031 24.121094 8 25 8 C 25.886719 8 26.679688 7.683594 27.34375 7.1875 Z M 21.5625 41.9375 C 22.683594 41.960938 23.824219 42 25 42 C 26.175781 42 27.316406 41.960938 28.4375 41.9375 C 28.792969 42.539063 29 43.25 29 44 C 29 46.222656 27.222656 48 25 48 C 22.777344 48 21 46.222656 21 44 C 21 43.242188 21.199219 42.539063 21.5625 41.9375 Z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="24px" height="24px" fill-rule="nonzero"><g fill="#000000" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(10.66667,10.66667)"><path d="M4,3c-1.09306,0 -2,0.90694 -2,2v16h14h6v-12c0,-1.09306 -0.90694,-2 -2,-2h-4v-2c0,-1.09306 -0.90694,-2 -2,-2zM4,5h10v14h-10zM6,7v2h2v-2zM10,7v2h2v-2zM16,9h4v10h-4v-2h2v-2h-2v-2h2v-2h-2zM6,11v2h2v-2zM10,11v2h2v-2zM6,15v2h2v-2zM10,15v2h2v-2z"></path></g></g></svg>
|
||||
|
After Width: | Height: | Size: 747 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M13.4 3.5c-1.7 0-2.9.7-3.5 1.5-.1-.5-.4-1.2-1.6-1.2H5.2v3.2h1.3c.2 0 .3.1.3.3v13.2h3.6v-5.2c.6.6 1.7 1.1 3 1.1 3.3 0 5.6-2.6 5.6-6.5 0-3.9-2.2-6.4-5.6-6.4Zm-.7 9.9c-1.5 0-2.4-1.5-2.4-3.4 0-2.6 1.4-3.5 2.5-3.5 1.4 0 2.5 1.4 2.5 3.5 0 2.4-1.3 3.4-2.6 3.4Z" fill="#017737"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 363 B |
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="48px" height="48px" fill-rule="nonzero"><g fill="#000000" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(4,4)"><path d="M53.41,10.59c0.56,0.56 0.74,1.39 0.46,2.13l-16.23,42c-0.3,0.76 -1.035,1.27 -1.855,1.28c-0.8,0 -1.545,-0.48 -1.855,-1.22l-4.566,-10.848c-0.521,-1.238 -0.386,-2.654 0.359,-3.771l9.007,-13.501c0.6,-0.91 -0.48,-1.99 -1.39,-1.39l-13.501,9.006c-1.117,0.745 -2.534,0.88 -3.771,0.359l-10.848,-4.565c-0.75,-0.32 -1.23,-1.06 -1.22,-1.87c0.01,-0.82 0.52,-1.54 1.28,-1.84l42,-16.23c0.74,-0.28 1.57,-0.1 2.13,0.46z"></path></g></g></svg>
|
||||
|
After Width: | Height: | Size: 896 B |
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Sentry</title><path fill="#8D5CF5" d="M13.91 2.505c-.873-1.448-2.972-1.448-3.844 0L6.904 7.92a15.478 15.478 0 0 1 8.53 12.811h-2.221A13.301 13.301 0 0 0 5.784 9.814l-2.926 5.06a7.65 7.65 0 0 1 4.435 5.848H2.194a.365.365 0 0 1-.298-.534l1.413-2.402a5.16 5.16 0 0 0-1.614-.913L.296 19.275a2.182 2.182 0 0 0 .812 2.999 2.24 2.24 0 0 0 1.086.288h6.983a9.322 9.322 0 0 0-3.845-8.318l1.11-1.922a11.47 11.47 0 0 1 4.95 10.24h5.915a17.242 17.242 0 0 0-7.885-15.28l2.244-3.845a.37.37 0 0 1 .504-.13c.255.14 9.75 16.708 9.928 16.9a.365.365 0 0 1-.327.543h-2.287c.029.612.029 1.223 0 1.831h2.297a2.206 2.206 0 0 0 1.922-3.31z"/></svg>
|
||||
|
After Width: | Height: | Size: 702 B |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="-18 0 292 292" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M223.774 57.34c-.201-1.46-1.48-2.268-2.537-2.357-1.055-.088-23.383-1.743-23.383-1.743s-15.507-15.395-17.209-17.099c-1.703-1.703-5.029-1.185-6.32-.805-.19.056-3.388 1.043-8.678 2.68-5.18-14.906-14.322-28.604-30.405-28.604-.444 0-.901.018-1.358.044C129.31 3.407 123.644.779 118.75.779c-37.465 0-55.364 46.835-60.976 70.635-14.558 4.511-24.9 7.718-26.221 8.133-8.126 2.549-8.383 2.805-9.45 10.462C21.3 95.806.038 260.235.038 260.235l165.678 31.042 89.77-19.42S223.973 58.8 223.775 57.34zM156.49 40.848l-14.019 4.339c.005-.988.01-1.96.01-3.023 0-9.264-1.286-16.723-3.349-22.636 8.287 1.04 13.806 10.469 17.358 21.32zm-27.638-19.483c2.304 5.773 3.802 14.058 3.802 25.238 0 .572-.005 1.095-.01 1.624-9.117 2.824-19.024 5.89-28.953 8.966 5.575-21.516 16.025-31.908 25.161-35.828zm-11.131-10.537c1.617 0 3.246.549 4.805 1.622-12.007 5.65-24.877 19.88-30.312 48.297l-22.886 7.088C75.694 46.16 90.81 10.828 117.72 10.828z" fill="#95BF46"/><path d="M221.237 54.983c-1.055-.088-23.383-1.743-23.383-1.743s-15.507-15.395-17.209-17.099c-.637-.634-1.496-.959-2.394-1.099l-12.527 256.233 89.762-19.418S223.972 58.8 223.774 57.34c-.201-1.46-1.48-2.268-2.537-2.357" fill="#5E8E3E"/><path d="M135.242 104.585l-11.069 32.926s-9.698-5.176-21.586-5.176c-17.428 0-18.305 10.937-18.305 13.693 0 15.038 39.2 20.8 39.2 56.024 0 27.713-17.577 45.558-41.277 45.558-28.44 0-42.984-17.7-42.984-17.7l7.615-25.16s14.95 12.835 27.565 12.835c8.243 0 11.596-6.49 11.596-11.232 0-19.616-32.16-20.491-32.16-52.724 0-27.129 19.472-53.382 58.778-53.382 15.145 0 22.627 4.338 22.627 4.338" fill="#FFF"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,27 @@
|
||||
<svg class="background-illustration" viewBox="0 0 800 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background Elements -->
|
||||
<path d="M0 0 L800 0 L800 600 L0 600 Z" fill="#FEF2F2"/>
|
||||
<path d="M600 100 Q 750 150 700 300 T 600 500 L 800 600 L 800 0 L 600 100Z" fill="#FEE2E2" opacity="0.5"/>
|
||||
<path d="M650 150 Q 800 200 750 350 T 650 550 L 850 650 L 850 50 L 650 150Z" fill="#FECACA" opacity="0.3"/>
|
||||
|
||||
<!-- Chat Bubbles -->
|
||||
<g transform="translate(100, 150) scale(0.8)">
|
||||
<!-- AI Chat Bubble -->
|
||||
<rect x="50" y="50" width="300" height="80" rx="20" fill="#f34611" opacity="0.9"/>
|
||||
<text x="80" y="95" fill="white" font-family="Inter, sans-serif" font-size="16">Hello! How can I assist you today?</text>
|
||||
|
||||
<!-- User Chat Bubble -->
|
||||
<rect x="150" y="150" width="280" height="80" rx="20" fill="white" stroke="#f34611" stroke-width="2"/>
|
||||
<text x="180" y="195" fill="#374151" font-family="Inter, sans-serif" font-size="16">I need help with my account</text>
|
||||
|
||||
<!-- AI Response Bubble -->
|
||||
<rect x="50" y="250" width="320" height="80" rx="20" fill="#f34611" opacity="0.9"/>
|
||||
<text x="80" y="295" fill="white" font-family="Inter, sans-serif" font-size="16">I'll guide you through the process</text>
|
||||
</g>
|
||||
|
||||
<!-- Decorative Elements -->
|
||||
<circle cx="600" cy="200" r="8" fill="#f34611" opacity="0.6"/>
|
||||
<circle cx="650" cy="250" r="6" fill="#f34611" opacity="0.4"/>
|
||||
<circle cx="700" cy="180" r="10" fill="#f34611" opacity="0.5"/>
|
||||
<circle cx="580" cy="300" r="7" fill="#f34611" opacity="0.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.2 32.4C19.2 34.2 17.7 35.7 15.9 35.7C14.1 35.7 12.6 34.2 12.6 32.4C12.6 30.6 14.1 29.1 15.9 29.1H19.2V32.4Z" fill="#E01E5A"/>
|
||||
<path d="M20.7 32.4C20.7 30.6 22.2 29.1 24 29.1C25.8 29.1 27.3 30.6 27.3 32.4V40.5C27.3 42.3 25.8 43.8 24 43.8C22.2 43.8 20.7 42.3 20.7 40.5V32.4Z" fill="#E01E5A"/>
|
||||
<path d="M24 19.2C22.2 19.2 20.7 17.7 20.7 15.9C20.7 14.1 22.2 12.6 24 12.6C25.8 12.6 27.3 14.1 27.3 15.9V19.2H24Z" fill="#36C5F0"/>
|
||||
<path d="M24 20.7C25.8 20.7 27.3 22.2 27.3 24C27.3 25.8 25.8 27.3 24 27.3H15.9C14.1 27.3 12.6 25.8 12.6 24C12.6 22.2 14.1 20.7 15.9 20.7H24Z" fill="#36C5F0"/>
|
||||
<path d="M35.7 24C35.7 22.2 37.2 20.7 39 20.7C40.8 20.7 42.3 22.2 42.3 24C42.3 25.8 40.8 27.3 39 27.3H35.7V24Z" fill="#2EB67D"/>
|
||||
<path d="M34.2 24C34.2 25.8 32.7 27.3 30.9 27.3C29.1 27.3 27.6 25.8 27.6 24V15.9C27.6 14.1 29.1 12.6 30.9 12.6C32.7 12.6 34.2 14.1 34.2 15.9V24Z" fill="#2EB67D"/>
|
||||
<path d="M30.9 35.7C32.7 35.7 34.2 37.2 34.2 39C34.2 40.8 32.7 42.3 30.9 42.3C29.1 42.3 27.6 40.8 27.6 39V35.7H30.9Z" fill="#ECB22E"/>
|
||||
<path d="M30.9 34.2C29.1 34.2 27.6 32.7 27.6 30.9C27.6 29.1 29.1 27.6 30.9 27.6H39C40.8 27.6 42.3 29.1 42.3 30.9C42.3 32.7 40.8 34.2 39 34.2H30.9Z" fill="#ECB22E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
||||
<circle cx="120" cy="120" r="120" fill="#F22F46"/>
|
||||
<path d="M120 58c-38.7 0-70 26.9-70 60 0 18.8 10.1 35.6 26 46.6V196l27.8-15.5c5.2 1 10.7 1.5 16.2 1.5 38.7 0 70-26.9 70-60s-31.3-64-70-64z" fill="#FFF"/>
|
||||
<circle cx="90" cy="120" r="9" fill="#F22F46"/>
|
||||
<circle cx="120" cy="120" r="9" fill="#F22F46"/>
|
||||
<circle cx="150" cy="120" r="9" fill="#F22F46"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 431 B |
@@ -0,0 +1,54 @@
|
||||
@import './design-tokens.css';
|
||||
|
||||
/* Reset */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Base Styles */
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--text-color);
|
||||
background: var(--background-color);
|
||||
line-height: 1.6;
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--text-base);
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 { font-size: var(--text-4xl); }
|
||||
h2 { font-size: var(--text-3xl); }
|
||||
h3 { font-size: var(--text-2xl); }
|
||||
h4 { font-size: var(--text-xl); }
|
||||
h5 { font-size: var(--text-lg); }
|
||||
h6 { font-size: var(--text-base); }
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Focus Styles */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
/* Buttons */
|
||||
.btn {
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
padding: var(--space-sm) var(--space-lg);
|
||||
border-radius: var(--radius-md);
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast),
|
||||
transform var(--transition-fast), border-color var(--transition-fast), opacity var(--transition-fast);
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-sm);
|
||||
line-height: 1.4;
|
||||
background: var(--accent-solid);
|
||||
color: #0B0C10;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--ring-focus);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent-solid);
|
||||
color: #0B0C10;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--o06);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--o14);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--o10);
|
||||
border-color: var(--o20);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--text3);
|
||||
border: 1px solid var(--o12);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--o06);
|
||||
color: var(--text);
|
||||
border-color: var(--o20);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: transparent;
|
||||
color: var(--c-coral);
|
||||
border: 1px solid rgba(255,138,115,.3);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: rgba(255,138,115,.08);
|
||||
border-color: rgba(255,138,115,.5);
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.btn.loading {
|
||||
position: relative;
|
||||
color: transparent !important;
|
||||
}
|
||||
|
||||
.btn.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -8px 0 0 -8px;
|
||||
border: 2px solid rgba(11,12,16,.4);
|
||||
border-right-color: #0B0C10;
|
||||
border-radius: 50%;
|
||||
animation: button-loading-spinner 0.75s linear infinite;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: var(--space-lg);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 9px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text3);
|
||||
font-size: 13.5px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-base);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--o12);
|
||||
border-radius: 12px;
|
||||
color: var(--text);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--faint);
|
||||
}
|
||||
|
||||
.form-input:hover {
|
||||
border-color: var(--o20);
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-input:focus-visible {
|
||||
border-color: var(--accent-ink);
|
||||
box-shadow: var(--ring-focus);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-input:disabled {
|
||||
background: var(--o04);
|
||||
color: var(--muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-input.error,
|
||||
.form-input[aria-invalid='true'] {
|
||||
border-color: var(--error-color);
|
||||
}
|
||||
|
||||
.form-input.error:focus,
|
||||
.form-input[aria-invalid='true']:focus {
|
||||
box-shadow: 0 0 0 3px var(--error-bg);
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--o08);
|
||||
padding: var(--space-xl);
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-full {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
/* Stepper */
|
||||
.progress-steps {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-2xl);
|
||||
margin-bottom: var(--space-2xl);
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: var(--space-md);
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--o05);
|
||||
border: 2px solid var(--o14);
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-family: var(--font-display);
|
||||
transition: background-color var(--transition-normal), border-color var(--transition-normal),
|
||||
color var(--transition-normal);
|
||||
}
|
||||
|
||||
.step-indicator.active {
|
||||
background: rgba(201,242,78,.1);
|
||||
border-color: var(--accent-ink);
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
|
||||
.step-indicator.completed {
|
||||
background: var(--accent-solid);
|
||||
border-color: var(--accent-ink);
|
||||
color: #0B0C10;
|
||||
}
|
||||
|
||||
.step-details h3 {
|
||||
font-size: var(--text-base);
|
||||
font-family: var(--font-display);
|
||||
margin-bottom: var(--space-xs);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.step-details p {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.progress-steps {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.step-details {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal buttons.
|
||||
Global rather than scoped because BaseModal takes its actions through a
|
||||
slot, and slot content is styled by the parent — three modals were each
|
||||
carrying a byte-identical private copy of these rules.
|
||||
Distinct from .btn above, which is the app's louder primary action: these
|
||||
are the quieter Cancel/Confirm pair a dialog footer wants. */
|
||||
.modal-btn {
|
||||
padding: 9px 16px;
|
||||
border-radius: var(--radius-btn, 8px);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--background-soft);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.modal-btn-primary {
|
||||
background: var(--accent-solid);
|
||||
color: var(--on-accent-solid);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.modal-btn-danger {
|
||||
background: var(--c-danger, var(--danger));
|
||||
color: var(--on-accent-solid);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.modal-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Native checkboxes/radios follow the design accent instead of the browser
|
||||
default (blue), so form controls match the theme app-wide. */
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
accent-color: var(--accent-ink);
|
||||
}
|
||||
|
||||
/* Nav section heading — shared by the desktop sidebar and the mobile More
|
||||
sheet so the two menus can't drift apart visually. */
|
||||
.nav-section-heading {
|
||||
color: var(--faint);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
/* ── Responsive table → cards ────────────────────────────────────────────
|
||||
Desktop keeps its own grid template; below 768px each row becomes a card:
|
||||
the primary cell and a trailing badge share line 1, and the remaining
|
||||
values wrap onto line 2 carrying their own labels (the table header is
|
||||
hidden, so each value has to name itself).
|
||||
|
||||
Markup: .rcards-head on the header row, .rcards-row on each row,
|
||||
.rcards-primary / .rcards-badge on the line-1 cells, and .rcards-meta
|
||||
wrapping a .rcards-label + .rcards-value pair on every other cell.
|
||||
|
||||
The consuming component must scope its desktop grid to
|
||||
@media (min-width: 769px) — a scoped SFC rule carries a [data-v-*]
|
||||
attribute and would otherwise outrank these global classes. */
|
||||
.rcards-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.rcards-head {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rcards-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px 12px;
|
||||
}
|
||||
|
||||
/* Zero-height full-width break: pushes the meta cells to their own line
|
||||
without assuming how wide the badge renders */
|
||||
.rcards-row::after {
|
||||
content: '';
|
||||
order: 3;
|
||||
flex: 1 0 100%;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.rcards-primary {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.rcards-badge {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.rcards-meta {
|
||||
order: 4;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.rcards-label {
|
||||
display: inline;
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--faint);
|
||||
}
|
||||
|
||||
/* text-overflow needs its own block box — it does nothing on a flex item */
|
||||
.rcards-value {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
/* ReplyPier Design Tokens — dark-first, lime accent */
|
||||
:root {
|
||||
/* Surfaces */
|
||||
--bg: #0B0C10;
|
||||
--bg2: #0E0F14;
|
||||
--surface: #13151C;
|
||||
--topbar: rgba(11,12,16,.8);
|
||||
|
||||
/* Accent */
|
||||
--accent-ink: #C9F24E; /* accent for text / borders / icons (darkens in light mode) */
|
||||
--accent-solid: #C9F24E; /* accent for FILLED surfaces (stays lime in both themes) */
|
||||
--on-accent-solid: #0B0C10; /* text/icon on --accent-solid (dark in both themes) */
|
||||
--c-lime: #C9F24E;
|
||||
--c-teal: #5FE3D6;
|
||||
--c-purple: #9D8CFF;
|
||||
--c-coral: #FF8A73;
|
||||
|
||||
/* Contrast inks — text/icon placed on top of a fill */
|
||||
--on-accent: #0B0C10; /* on lime accent */
|
||||
--on-light: #0B0C10; /* on any light fill */
|
||||
--on-dark: #F5F6F8; /* on any dark/coloured fill (theme-invariant) */
|
||||
|
||||
/* Elevated surfaces / header gradient */
|
||||
--bg-elevated: #101119;
|
||||
--bg-deep: #0A0B0E;
|
||||
--header-gradient: linear-gradient(180deg, var(--bg-elevated), var(--bg));
|
||||
--scrim: rgba(6,7,10,.72);
|
||||
|
||||
/* Mode pills (agent editor) */
|
||||
--mode-ai-bg: #3B82F6;
|
||||
--mode-ai-fg: #FFFFFF;
|
||||
--mode-wf-bg: #9D8CFF;
|
||||
--mode-wf-fg: #0B0C10;
|
||||
--pill-idle-bg: var(--o05);
|
||||
--pill-idle-fg: var(--muted);
|
||||
|
||||
/* Extended status / chart palette */
|
||||
--c-danger: #FF6B6B;
|
||||
--c-warn: #E6A23C;
|
||||
--c-info: #8FB8FF;
|
||||
--c-positive: #7CE38B;
|
||||
--c-neutral: #8A93A3;
|
||||
--c-online: #8FEDE3; /* online label text */
|
||||
--c-pro: #C5BDFF; /* PRO badge text */
|
||||
|
||||
/* Theme-aware tints (derived from base palette, auto-adjust per theme) */
|
||||
--accent-bg-06: color-mix(in srgb, var(--accent-ink) 6%, transparent);
|
||||
--accent-bg-08: color-mix(in srgb, var(--accent-ink) 8%, transparent);
|
||||
--accent-bg-12: color-mix(in srgb, var(--accent-ink) 12%, transparent);
|
||||
--accent-border: color-mix(in srgb, var(--accent-ink) 30%, transparent);
|
||||
--teal-bg-10: color-mix(in srgb, var(--c-teal) 10%, transparent);
|
||||
--teal-bg: color-mix(in srgb, var(--c-teal) 12%, transparent);
|
||||
--teal-border: color-mix(in srgb, var(--c-teal) 22%, transparent);
|
||||
--purple-bg: color-mix(in srgb, var(--c-purple) 15%, transparent);
|
||||
--purple-border: color-mix(in srgb, var(--c-purple) 28%, transparent);
|
||||
--coral-bg: color-mix(in srgb, var(--c-coral) 12%, transparent);
|
||||
--coral-border: color-mix(in srgb, var(--c-coral) 30%, transparent);
|
||||
|
||||
/* Controls */
|
||||
--toggle-knob: #FFFFFF;
|
||||
--toggle-track-off: var(--o12);
|
||||
--toggle-on-teal: var(--c-teal);
|
||||
--toggle-on-accent: var(--accent-ink);
|
||||
|
||||
/* Inbox message bubbles */
|
||||
--bubble-customer-bg: var(--o05);
|
||||
--bubble-customer-fg: var(--text2);
|
||||
--bubble-ai-bg: var(--teal-bg-10);
|
||||
--bubble-ai-border: var(--teal-border);
|
||||
--bubble-ai-fg: var(--text2);
|
||||
--bubble-agent-bg: var(--accent-ink);
|
||||
--bubble-agent-fg: var(--on-accent);
|
||||
|
||||
/* Brand gradients / avatars */
|
||||
--grad-purple-teal: linear-gradient(135deg, var(--c-purple), var(--c-teal));
|
||||
--grad-lime-teal: linear-gradient(135deg, var(--accent-ink), var(--c-teal));
|
||||
--grad-generate: linear-gradient(135deg, var(--c-purple), #7C6AE6);
|
||||
|
||||
/* Text scale */
|
||||
--text: #F5F6F8;
|
||||
--text2: #E7EAF0;
|
||||
--text3: #C7CCD6;
|
||||
--muted: #9CA3B0;
|
||||
/* Small text on every surface (down to #1A1C23) must reach WCAG AA 4.5:1. */
|
||||
--muted2: #868D9C;
|
||||
--faint: #7F8696;
|
||||
--code: #D7DBE3;
|
||||
|
||||
/* White-opacity tokens — borders, dividers, overlays */
|
||||
--o03: rgba(255,255,255,.03);
|
||||
--o04: rgba(255,255,255,.04);
|
||||
--o05: rgba(255,255,255,.05);
|
||||
--o06: rgba(255,255,255,.06);
|
||||
--o07: rgba(255,255,255,.07);
|
||||
--o08: rgba(255,255,255,.08);
|
||||
--o10: rgba(255,255,255,.1);
|
||||
--o12: rgba(255,255,255,.12);
|
||||
--o14: rgba(255,255,255,.14);
|
||||
--o16: rgba(255,255,255,.16);
|
||||
--o20: rgba(255,255,255,.2);
|
||||
--o25: rgba(255,255,255,.25);
|
||||
--o30: rgba(255,255,255,.3);
|
||||
|
||||
/* Semantic */
|
||||
--success-color: #0f9d6e;
|
||||
--error-color: #DC2626;
|
||||
--error-dark: #b91c1c;
|
||||
--warning-color: #D97706;
|
||||
--info-color: #2563EB;
|
||||
--success-bg: rgba(15,157,110,.1);
|
||||
--error-bg: rgba(220,38,38,.1);
|
||||
--warning-bg: rgba(217,119,6,.1);
|
||||
--info-bg: rgba(37,99,235,.1);
|
||||
|
||||
/* Fonts */
|
||||
--font-family: 'Instrument Sans', system-ui, -apple-system, sans-serif;
|
||||
--font-sans: 'Instrument Sans', system-ui, -apple-system, sans-serif;
|
||||
--font-display: 'Space Grotesk', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
|
||||
/* Font weights */
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
/* Letter spacing */
|
||||
--tracking-display: -0.02em;
|
||||
--tracking-tight: -0.01em;
|
||||
--tracking-wide: 0.04em;
|
||||
|
||||
/* Font sizes */
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.875rem;
|
||||
--text-base: 1rem;
|
||||
--text-lg: 1.125rem;
|
||||
--text-xl: 1.25rem;
|
||||
--text-2xl: 1.5rem;
|
||||
--text-3xl: 1.875rem;
|
||||
--text-4xl: 2.25rem;
|
||||
|
||||
/* Spacing */
|
||||
--space-xs: 0.25rem;
|
||||
--space-sm: 0.5rem;
|
||||
--space-md: 1rem;
|
||||
--space-lg: 1.5rem;
|
||||
--space-xl: 2rem;
|
||||
--space-2xl: 3rem;
|
||||
--space-3xl: 4rem;
|
||||
|
||||
/* Border radius */
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 16px;
|
||||
--radius-full: 9999px;
|
||||
/* Design-spec radii */
|
||||
--radius-chip: 10px;
|
||||
--radius-btn: 11px;
|
||||
--radius-input: 12px;
|
||||
--radius-card: 18px;
|
||||
--radius-card-lg: 20px;
|
||||
--radius-pill: 999px;
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 0.18s cubic-bezier(0.4,0,0.2,1);
|
||||
--transition-normal: 0.25s cubic-bezier(0.4,0,0.2,1);
|
||||
--transition-slow: 0.4s cubic-bezier(0.4,0,0.2,1);
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,.15);
|
||||
--shadow-md: 0 4px 12px rgba(0,0,0,.2);
|
||||
--shadow-lg: 0 20px 50px rgba(0,0,0,.5);
|
||||
|
||||
/* Focus ring */
|
||||
--ring-focus: 0 0 0 3px rgba(201,242,78,.15);
|
||||
|
||||
/* Icon filters (for SVG img elements on dark bg) */
|
||||
--icon-filter: brightness(0) invert(1);
|
||||
--icon-opacity: 0.5;
|
||||
--icon-active-opacity: 0.9;
|
||||
|
||||
/* Mobile app shell */
|
||||
--bottom-nav-height: 56px;
|
||||
--safe-top: env(safe-area-inset-top, 0px);
|
||||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
|
||||
/* Overlay stacking order — every fixed overlay must use one of these */
|
||||
--z-topbar: 50;
|
||||
--z-bottom-nav: 900;
|
||||
--z-drawer: 1000; /* notification drawer, sidebar overlay */
|
||||
--z-fullscreen-page: 1050; /* mobile chat-info "Details" page */
|
||||
--z-prompt: 1090; /* push/install prompts */
|
||||
--z-sheet: 1100; /* More bottom sheet */
|
||||
--z-support-chat: 1150; /* hosted support chat drawer (read from JS) */
|
||||
--z-modal: 2000;
|
||||
|
||||
/* Breakpoints — keep in sync with useBreakpoint.ts (JS can't read these
|
||||
at media-query time: --breakpoint-sm = MOBILE, --breakpoint-md = TABLET) */
|
||||
--breakpoint-xs: 480px;
|
||||
--breakpoint-sm: 768px;
|
||||
--breakpoint-md: 1024px;
|
||||
--breakpoint-lg: 1280px;
|
||||
--breakpoint-xl: 1366px;
|
||||
--breakpoint-xxl: 1440px;
|
||||
--breakpoint-xxxl: 1600px;
|
||||
|
||||
/* Legacy aliases (keep for component compatibility) */
|
||||
--primary-color: var(--accent-ink);
|
||||
--primary-dark: #b3d943;
|
||||
--background-color: var(--bg);
|
||||
--background-base: var(--surface);
|
||||
--background-soft: var(--bg2);
|
||||
--background-mute: var(--o08);
|
||||
--hover-bg: var(--o05);
|
||||
--border-color: var(--o10);
|
||||
--border-color-hover: var(--o16);
|
||||
--text-primary: var(--text);
|
||||
--text-secondary: var(--text3);
|
||||
--text-color: var(--text);
|
||||
--text-heading: var(--text);
|
||||
--text-muted: var(--muted);
|
||||
--text-placeholder: var(--faint);
|
||||
--code-bg: var(--surface);
|
||||
--primary-soft: rgba(201,242,78,.1);
|
||||
|
||||
/* Legacy aliases used by the agent editor (resolve via flipping tokens) */
|
||||
--accent-color: var(--accent-ink);
|
||||
--background-alt: var(--bg2);
|
||||
--background-muted: var(--o05);
|
||||
--radius-xl: 20px;
|
||||
--success: var(--success-color);
|
||||
--success-dark: var(--success-color);
|
||||
--success-light: color-mix(in srgb, var(--success-color) 14%, transparent);
|
||||
--success-color-soft: color-mix(in srgb, var(--success-color) 12%, transparent);
|
||||
--warning: var(--warning-color);
|
||||
--warning-light: color-mix(in srgb, var(--warning-color) 14%, transparent);
|
||||
--error-light: var(--coral-bg);
|
||||
--error-soft: var(--coral-bg);
|
||||
--error-color-soft: var(--coral-bg);
|
||||
--primary-color-filter: none;
|
||||
}
|
||||
|
||||
/* Light theme override */
|
||||
[data-theme="light"] {
|
||||
--bg: #EEF0F4;
|
||||
--bg2: #FFFFFF;
|
||||
--surface: #FFFFFF;
|
||||
--topbar: rgba(255,255,255,.85);
|
||||
|
||||
/* Accent and status inks are used as text on white and on their own pale
|
||||
tints: darkened until they reach WCAG AA 4.5:1 on both. */
|
||||
--accent-ink: #44700A;
|
||||
--c-lime: #44700A;
|
||||
--c-teal: #097171;
|
||||
--c-purple: #6454BF;
|
||||
--c-coral: #AC4829;
|
||||
--warning-color: #9C5604;
|
||||
|
||||
--text: #171A21;
|
||||
--text2: #222732;
|
||||
--text3: #3C424E;
|
||||
--muted: #5C6473;
|
||||
/* Small text on every surface (down to #EEF0F4) must reach WCAG AA 4.5:1. */
|
||||
--muted2: #626977;
|
||||
--faint: #646B79;
|
||||
--code: #3C424E;
|
||||
|
||||
--o03: rgba(17,21,30,.03);
|
||||
--o04: rgba(17,21,30,.04);
|
||||
--o05: rgba(17,21,30,.05);
|
||||
--o06: rgba(17,21,30,.06);
|
||||
--o07: rgba(17,21,30,.075);
|
||||
--o08: rgba(17,21,30,.08);
|
||||
--o10: rgba(17,21,30,.09);
|
||||
--o12: rgba(17,21,30,.1);
|
||||
--o14: rgba(17,21,30,.11);
|
||||
--o16: rgba(17,21,30,.12);
|
||||
--o20: rgba(17,21,30,.14);
|
||||
--o25: rgba(17,21,30,.18);
|
||||
--o30: rgba(17,21,30,.2);
|
||||
|
||||
/* Icon filters for light theme — SVGs are already dark, no inversion needed */
|
||||
--icon-filter: brightness(1);
|
||||
--icon-opacity: 0.6;
|
||||
--icon-active-opacity: 0.85;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(17,21,30,.05);
|
||||
--shadow-md: 0 4px 12px rgba(17,21,30,.08);
|
||||
--shadow-lg: 0 20px 50px rgba(17,21,30,.15);
|
||||
|
||||
--ring-focus: 0 0 0 3px rgba(76,122,11,.2);
|
||||
--primary-soft: rgba(76,122,11,.08);
|
||||
|
||||
/* Contrast inks — accent is dark-olive in light, so text on it must be white */
|
||||
--on-accent: #FFFFFF;
|
||||
--on-light: #0B0C10;
|
||||
--on-dark: #F5F6F8;
|
||||
|
||||
/* Elevated surfaces / scrim (gradients & tints referencing base tokens auto-recompute) */
|
||||
--bg-elevated: #FFFFFF;
|
||||
--bg-deep: #F4F6FA;
|
||||
--scrim: rgba(17,21,30,.4);
|
||||
|
||||
/* Mode pills — darker fills need light text in light theme */
|
||||
--mode-ai-bg: #2563EB;
|
||||
--mode-ai-fg: #FFFFFF;
|
||||
--mode-wf-bg: #6D5BD0;
|
||||
--mode-wf-fg: #FFFFFF;
|
||||
|
||||
/* Extended status / chart palette tuned for light bg: each reaches WCAG AA
|
||||
4.5:1 as text on white and on its own pale tint. */
|
||||
--c-danger: #B33F3F;
|
||||
--c-warn: #8F5E18;
|
||||
--c-info: #225BD8;
|
||||
--c-positive: #187742;
|
||||
--c-neutral: #606773;
|
||||
--c-online: #097171;
|
||||
--c-pro: #6454BF;
|
||||
|
||||
/* Generate gradient — darker end stop for light */
|
||||
--grad-generate: linear-gradient(135deg, var(--c-purple), #5B49C9);
|
||||
|
||||
/* Legacy aliases */
|
||||
--primary-color: var(--accent-ink);
|
||||
--primary-dark: #3d6209;
|
||||
--background-color: var(--bg);
|
||||
--background-base: var(--surface);
|
||||
--background-soft: var(--bg2);
|
||||
--background-mute: var(--o08);
|
||||
--hover-bg: var(--o05);
|
||||
--border-color: var(--o10);
|
||||
--border-color-hover: var(--o16);
|
||||
--text-primary: var(--text);
|
||||
--text-secondary: var(--text3);
|
||||
--text-color: var(--text);
|
||||
--text-heading: var(--text);
|
||||
--text-muted: var(--muted);
|
||||
--text-placeholder: var(--faint);
|
||||
--code-bg: var(--surface);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@import './design-tokens.css';
|
||||
@import './base.css';
|
||||
@import './components.css';
|
||||
@import './utilities.css';
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/* Layout */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-xl);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-xl);
|
||||
}
|
||||
|
||||
/* Responsive containers */
|
||||
@media (max-width: 1366px) {
|
||||
.container,
|
||||
.container-fluid {
|
||||
max-width: 1200px;
|
||||
padding: 0 var(--space-lg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.container,
|
||||
.container-fluid {
|
||||
padding: 0 var(--space-md);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container,
|
||||
.container-fluid {
|
||||
padding: 0 var(--space-sm);
|
||||
}
|
||||
}
|
||||
|
||||
/* Flexbox */
|
||||
.flex { display: flex; }
|
||||
.flex-col { flex-direction: column; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-center { justify-content: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.gap-sm { gap: var(--space-sm); }
|
||||
.gap-md { gap: var(--space-md); }
|
||||
.gap-lg { gap: var(--space-lg); }
|
||||
|
||||
/* Spacing */
|
||||
.m-0 { margin: 0; }
|
||||
.mt-lg { margin-top: var(--space-lg); }
|
||||
.mb-lg { margin-bottom: var(--space-lg); }
|
||||
.mx-auto { margin-left: auto; margin-right: auto; }
|
||||
|
||||
/* Text */
|
||||
.text-center { text-align: center; }
|
||||
.text-sm { font-size: var(--text-sm); }
|
||||
.text-lg { font-size: var(--text-lg); }
|
||||
.font-bold { font-weight: 600; }
|
||||
.opacity-80 { opacity: 0.8; }
|
||||
|
||||
/* Responsive visibility utilities */
|
||||
.hidden { display: none !important; }
|
||||
.visible { display: block !important; }
|
||||
|
||||
/* Show/hide on specific screen sizes */
|
||||
@media (max-width: 1366px) {
|
||||
.hidden-laptop-13 { display: none !important; }
|
||||
.visible-laptop-13 { display: block !important; }
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.hidden-laptop-small { display: none !important; }
|
||||
.visible-laptop-small { display: block !important; }
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.hidden-tablet { display: none !important; }
|
||||
.visible-tablet { display: block !important; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hidden-mobile { display: none !important; }
|
||||
.visible-mobile { display: block !important; }
|
||||
}
|
||||
|
||||
/* Responsive spacing utilities for 13-inch laptops */
|
||||
@media (max-width: 1366px) {
|
||||
.p-responsive-13 { padding: var(--space-md) !important; }
|
||||
.px-responsive-13 { padding-left: var(--space-md) !important; padding-right: var(--space-md) !important; }
|
||||
.py-responsive-13 { padding-top: var(--space-md) !important; padding-bottom: var(--space-md) !important; }
|
||||
.m-responsive-13 { margin: var(--space-md) !important; }
|
||||
.mx-responsive-13 { margin-left: var(--space-md) !important; margin-right: var(--space-md) !important; }
|
||||
.my-responsive-13 { margin-top: var(--space-md) !important; margin-bottom: var(--space-md) !important; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 4H3C1.89543 4 1 4.89543 1 6V18C1 19.1046 1.89543 20 3 20H21C22.1046 20 23 19.1046 23 18V6C23 4.89543 22.1046 4 21 4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1 10H23" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 15H7.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 15H13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 701 B |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
||||
<defs>
|
||||
<linearGradient id="tg-grad" x1="0.667" y1="0.167" x2="0.417" y2="0.75">
|
||||
<stop offset="0" stop-color="#37aee2"/>
|
||||
<stop offset="1" stop-color="#1e96c8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="120" cy="120" r="120" fill="url(#tg-grad)"/>
|
||||
<path fill="#c8daea" d="M98 175c-3.888 0-3.227-1.468-4.568-5.17L82 132.207 170 80"/>
|
||||
<path fill="#a9c9dd" d="M98 175c3 0 4.325-1.372 6-3l16-15.558-19.958-12.035"/>
|
||||
<path fill="#f6fbfe" d="M100.04 144.41l48.36 35.729c5.519 3.045 9.501 1.468 10.876-5.123l19.685-92.763c2.015-8.08-3.08-11.746-8.36-9.349l-115.59 44.571c-7.89 3.165-7.843 7.567-1.438 9.528l29.663 9.259 68.673-43.325c3.242-1.966 6.218-.91 3.776 1.258"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 761 B |
|
After Width: | Height: | Size: 509 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="32px" height="32px" fill-rule="nonzero"><g fill="#000000" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(8,8)"><path d="M16,5c-3.85547,0 -7,3.14453 -7,7c0,2.41016 1.23047,4.55078 3.09375,5.8125c-3.56641,1.53125 -6.09375,5.07031 -6.09375,9.1875h2c0,-2.89062 1.52734,-5.40625 3.8125,-6.8125c0.67188,1.64844 2.30859,2.8125 4.1875,2.8125c1.87891,0 3.51563,-1.16406 4.1875,-2.8125c2.28516,1.40625 3.8125,3.92188 3.8125,6.8125h2c0,-4.11719 -2.52734,-7.65625 -6.09375,-9.1875c1.86328,-1.26172 3.09375,-3.40234 3.09375,-5.8125c0,-3.85547 -3.14453,-7 -7,-7zM16,7c2.77344,0 5,2.22656 5,5c0,2.77344 -2.22656,5 -5,5c-2.77344,0 -5,-2.22656 -5,-5c0,-2.77344 2.22656,-5 5,-5zM16,19c0.82031,0 1.60156,0.11719 2.34375,0.34375c-0.34766,0.96484 -1.25391,1.65625 -2.34375,1.65625c-1.08984,0 -1.99609,-0.69141 -2.34375,-1.65625c0.74219,-0.22656 1.52344,-0.34375 2.34375,-0.34375z"></path></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" width="30px" height="30px">
|
||||
<path d="M 15 5 C 6.081703 5 0.32098813 14.21118 0.21679688 14.378906 A 1 1 0 0 0 0.21679688 15.621094 C 0.32098813 15.78882 6.081703 25 15 25 C 23.918297 25 29.679012 15.78882 29.783203 15.621094 A 1 1 0 0 0 29.783203 14.378906 C 29.679012 14.21118 23.918297 5 15 5 z M 15 8 C 18.866 8 22 11.134 22 15 C 22 18.866 18.866 22 15 22 C 11.134 22 8 18.866 8 15 C 8 11.134 11.134 8 15 8 z M 15 12 A 3 3 0 0 0 12 15 A 3 3 0 0 0 15 18 A 3 3 0 0 0 18 15 A 3 3 0 0 0 15 12 z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 589 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
||||
<circle cx="120" cy="120" r="120" fill="#25D366"/>
|
||||
<path fill="#FFF" d="M164.5 75.2A62.4 62.4 0 0 0 120.1 56.8c-34.6 0-62.8 28.2-62.8 62.8 0 11.1 2.9 21.9 8.4 31.4L56.8 183l32.8-8.6a62.7 62.7 0 0 0 30.4 7.8h.1c34.6 0 62.8-28.2 62.8-62.8a62.4 62.4 0 0 0-18.4-44.2zm-44.4 96.6h-.1a52.1 52.1 0 0 1-26.6-7.3l-1.9-1.1-19.7 5.2 5.3-19.2-1.3-2a52 52 0 0 1-8-27.8c0-28.8 23.4-52.2 52.3-52.2a52 52 0 0 1 52.2 52.3c0 28.8-23.4 52.1-52.2 52.1zm28.7-39.1c-1.6-.8-9.3-4.6-10.7-5.1s-2.5-.8-3.5.8-4 5.1-5 6.2-1.8 1.2-3.4.4a42.8 42.8 0 0 1-12.6-7.8 47.6 47.6 0 0 1-8.7-10.9c-.9-1.6-.1-2.4.7-3.2s1.6-1.8 2.4-2.8a10.8 10.8 0 0 0 1.5-2.6 2.9 2.9 0 0 0-.1-2.7c-.4-.8-3.5-8.5-4.8-11.7s-2.6-2.6-3.5-2.7h-3a5.8 5.8 0 0 0-4.2 2c-1.4 1.5-5.5 5.4-5.5 13.1s5.6 15.2 6.4 16.3 11.1 16.9 26.8 23.7a90.6 90.6 0 0 0 8.9 3.3 21.5 21.5 0 0 0 9.9.6c3-.4 9.3-3.8 10.6-7.5s1.3-6.8.9-7.5-1.4-1.1-3-1.9z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 942 B |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1"/>
|
||||
<rect x="14" y="3" width="7" height="7" rx="1"/>
|
||||
<rect x="3" y="14" width="7" height="7" rx="1"/>
|
||||
<rect x="14" y="14" width="7" height="7" rx="1"/>
|
||||
<circle cx="6.5" cy="6.5" r="1.5" fill="currentColor" stroke="none"/>
|
||||
<path d="M16 6h3"/>
|
||||
<path d="M17.5 4.5v3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 492 B |
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 4C12.954 4 4 12.954 4 24C4 35.046 12.954 44 24 44C35.046 44 44 35.046 44 24C44 12.954 35.046 4 24 4Z" fill="#03363D"/>
|
||||
<path d="M15.5 14H32.5V22.5L24 14L15.5 22.5V14Z" fill="white"/>
|
||||
<path d="M15.5 34H32.5V25.5L24 34L15.5 25.5V34Z" fill="white"/>
|
||||
<path d="M34 15.5V32.5H25.5L34 24L25.5 15.5H34Z" fill="white"/>
|
||||
<path d="M14 15.5V32.5H22.5L14 24L22.5 15.5H14Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 501 B |
@@ -0,0 +1,75 @@
|
||||
<!--
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup lang="ts" name="InstallPrompt">
|
||||
import { computed } from 'vue'
|
||||
import { usePWAInstall } from '@/composables/usePWAInstall'
|
||||
import { isShopifyEmbedded } from '@/pwa/register'
|
||||
|
||||
const { canInstall, needsManualInstall, promptInstall } = usePWAInstall()
|
||||
|
||||
// Dashed hint card per the design: native prompt where supported, share-sheet
|
||||
// instructions on iOS, hidden when installed/unsupported/embedded.
|
||||
const visible = computed(() => !isShopifyEmbedded() && (canInstall.value || needsManualInstall.value))
|
||||
|
||||
const handleClick = () => {
|
||||
if (canInstall.value) promptInstall()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button v-if="visible" type="button" class="install-hint" @click="handleClick">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="install-icon" aria-hidden="true"><path d="M12 15V3m0 0L8 7m4-4l4 4"/><path d="M5 13v5a3 3 0 003 3h8a3 3 0 003-3v-5"/></svg>
|
||||
<span class="install-text">
|
||||
安装 ReplyPier —
|
||||
<span v-if="needsManualInstall" class="install-sub">分享 → 添加到主屏幕</span>
|
||||
<span v-else class="install-sub">像原生应用一样使用</span>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.install-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
width: 100%;
|
||||
padding: 13px 15px;
|
||||
border-radius: 14px;
|
||||
border: 1px dashed var(--o10);
|
||||
background: var(--o03);
|
||||
color: var(--text3);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 12.5px;
|
||||
line-height: 1.35;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.install-hint:hover {
|
||||
background: var(--o05);
|
||||
}
|
||||
|
||||
.install-icon {
|
||||
color: var(--c-teal);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.install-sub {
|
||||
color: var(--muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt(): Promise<void>
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
|
||||
}
|
||||
|
||||
// Module-level: beforeinstallprompt often fires before any component mounts,
|
||||
// so the listener must be attached at import time.
|
||||
const deferredPrompt = ref<BeforeInstallPromptEvent | null>(null)
|
||||
const installed = ref(false)
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
event.preventDefault()
|
||||
deferredPrompt.value = event as BeforeInstallPromptEvent
|
||||
})
|
||||
window.addEventListener('appinstalled', () => {
|
||||
installed.value = true
|
||||
deferredPrompt.value = null
|
||||
})
|
||||
}
|
||||
|
||||
export function usePWAInstall() {
|
||||
const isStandalone = computed(
|
||||
() =>
|
||||
installed.value ||
|
||||
(typeof window !== 'undefined' &&
|
||||
(window.matchMedia?.('(display-mode: standalone)').matches ||
|
||||
(navigator as unknown as { standalone?: boolean }).standalone === true))
|
||||
)
|
||||
|
||||
const isIOS = typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent)
|
||||
|
||||
// Native prompt available (Chrome/Edge/Android)
|
||||
const canInstall = computed(() => !isStandalone.value && !!deferredPrompt.value)
|
||||
|
||||
// iOS never fires beforeinstallprompt — install is manual via the share sheet
|
||||
const needsManualInstall = computed(() => !isStandalone.value && isIOS)
|
||||
|
||||
const promptInstall = async (): Promise<boolean> => {
|
||||
const prompt = deferredPrompt.value
|
||||
if (!prompt) return false
|
||||
await prompt.prompt()
|
||||
const choice = await prompt.userChoice
|
||||
if (choice.outcome === 'accepted') {
|
||||
deferredPrompt.value = null
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return { isStandalone, isIOS, canInstall, needsManualInstall, promptInstall }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Navigations the service worker must NOT answer with the SPA shell.
|
||||
*
|
||||
* Lives outside sw.ts so it can be tested without ServiceWorker globals.
|
||||
*
|
||||
* Workbox matches these against `url.pathname + url.search`, **not** the
|
||||
* pathname alone (see workbox-routing/NavigationRoute `_match`). An anchored
|
||||
* `$` therefore stops matching the moment a provider appends a query string —
|
||||
* which is precisely the case that matters here, since OAuth providers always
|
||||
* redirect back with `?code=…`.
|
||||
*/
|
||||
export const NAVIGATION_DENYLIST: RegExp[] = [
|
||||
/^\/api\//,
|
||||
/^\/widget/,
|
||||
/^\/webclient/,
|
||||
/^\/shopify/,
|
||||
// Anything carrying a file extension is a real file served from public/,
|
||||
// never an SPA route — no route in the router has one.
|
||||
//
|
||||
// This is what keeps meta-oauth-callback.html working: Meta redirects the
|
||||
// login popup there, and the page exists only to postMessage the code back
|
||||
// to its opener. Answer it with index.html and the SPA boots instead, fails
|
||||
// to route the path, lands on the dashboard, and the connect dies silently.
|
||||
// Being precached does not save it — a precache lookup does not ignore
|
||||
// `?code=&state=`, so the request misses the entry and reaches the fallback.
|
||||
//
|
||||
// `[^?]*` keeps the extension match on the path, so a dot inside a query
|
||||
// string (`/settings?q=a.b`) is not mistaken for a filename.
|
||||
/^[^?]*\.[a-z0-9]+(\?|$)/i,
|
||||
]
|
||||
|
||||
/**
|
||||
* Whether this navigation should bypass the SPA shell. Mirrors workbox's own
|
||||
* denylist check, so tests exercise the real matching semantics.
|
||||
*/
|
||||
export function isDeniedNavigation(pathnameAndSearch: string): boolean {
|
||||
return NAVIGATION_DENYLIST.some((pattern) => pattern.test(pathnameAndSearch))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The message contract between the service worker (src/sw.ts) and the page
|
||||
* (src/composables/useNotifications.ts). Both sides import from here so a
|
||||
* rename can never silently break background toasts or notification taps.
|
||||
*/
|
||||
export const SW_MESSAGE = {
|
||||
/** SW → page: a background push arrived; page shows the in-app toast state */
|
||||
BACKGROUND_NOTIFICATION: 'BACKGROUND_NOTIFICATION',
|
||||
/** SW → page: user tapped a notification the SW couldn't navigate() itself */
|
||||
OPEN_CONVERSATION: 'OPEN_CONVERSATION',
|
||||
} as const
|
||||
|
||||
/** The one place the conversation deep-link URL is spelled. */
|
||||
export const CONVERSATIONS_PATH = '/conversations'
|
||||
|
||||
export const conversationSessionUrl = (sessionId?: string | null): string =>
|
||||
sessionId ? `${CONVERSATIONS_PATH}?session=${encodeURIComponent(sessionId)}` : '/'
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
|
||||
// Never register inside the Shopify admin iframe or on /shopify/* routes —
|
||||
// the embedded app has its own lifecycle and must not install an app shell.
|
||||
export const isShopifyEmbedded = (): boolean => {
|
||||
try {
|
||||
return window.top !== window.self || window.location.pathname.startsWith('/shopify')
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How long to wait for the new worker to take control before forcing a reload.
|
||||
* Activation is normally well under a second; this only covers a worker that
|
||||
* fails to activate or a browser that withholds controllerchange, so the user
|
||||
* is never stranded on the old build.
|
||||
*/
|
||||
const UPDATE_TAKEOVER_TIMEOUT_MS = 4000
|
||||
|
||||
/**
|
||||
* Offer the new build rather than forcing it: an agent mid-reply should not
|
||||
* have the page reloaded under them. vue-sonner is imported lazily so the
|
||||
* registration path stays off the startup critical path.
|
||||
*/
|
||||
async function promptForUpdate(reload: (reloadPage?: boolean) => Promise<void>) {
|
||||
try {
|
||||
const { toast } = await import('vue-sonner')
|
||||
const id = toast('ReplyPier 有新版本可用', {
|
||||
description: '刷新页面即可使用最新版本。',
|
||||
duration: Number.POSITIVE_INFINITY,
|
||||
action: {
|
||||
label: '刷新',
|
||||
onClick: () => {
|
||||
// Do NOT reload here. registerSW's updateSW ignores its reloadPage
|
||||
// argument and only posts SKIP_WAITING, without awaiting activation
|
||||
// — it resolves within a tick. The page refresh is done by the
|
||||
// `controlling` listener registerSW arms just before it calls us.
|
||||
//
|
||||
// Reloading here raced that handshake and always won: the document
|
||||
// came back under the OLD worker with the new one still in `waiting`,
|
||||
// so the next load re-dispatched `waiting` and the prompt returned.
|
||||
// That is why the toast reappeared on every release and no amount of
|
||||
// clicking Reload cleared it.
|
||||
toast.dismiss(id)
|
||||
void reload()
|
||||
window.setTimeout(() => window.location.reload(), UPDATE_TAKEOVER_TIMEOUT_MS)
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to show update prompt:', err)
|
||||
}
|
||||
}
|
||||
|
||||
export function setupPWA() {
|
||||
if (!('serviceWorker' in navigator) || isShopifyEmbedded()) return
|
||||
|
||||
// Clients from pre-PWA deployments still hold the Firebase-only worker that
|
||||
// Firebase's getToken() self-registered; drop it so only one SW owns scope /.
|
||||
navigator.serviceWorker
|
||||
.getRegistrations()
|
||||
.then((registrations) => {
|
||||
registrations.forEach((registration) => {
|
||||
const scriptUrl =
|
||||
registration.active?.scriptURL ||
|
||||
registration.waiting?.scriptURL ||
|
||||
registration.installing?.scriptURL ||
|
||||
''
|
||||
if (scriptUrl.endsWith('firebase-messaging-sw.js')) {
|
||||
registration.unregister()
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
const updateSW = registerSW({
|
||||
immediate: true,
|
||||
onNeedRefresh() {
|
||||
promptForUpdate(updateSW)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* The single app SW registration — passed to Firebase's getToken() so it never
|
||||
* self-registers a second worker. navigator.serviceWorker.ready never settles
|
||||
* when no SW gets registered (vite dev, or a failed registration), so race it
|
||||
* against a timeout instead of hanging callers forever.
|
||||
*/
|
||||
// Generous enough for a cold first install on low-end devices; a miss only
|
||||
// delays token registration to the next visit.
|
||||
const SW_READY_TIMEOUT_MS = 8000
|
||||
|
||||
export async function getSWRegistration(): Promise<ServiceWorkerRegistration | undefined> {
|
||||
if (!('serviceWorker' in navigator) || isShopifyEmbedded()) return undefined
|
||||
return Promise.race([
|
||||
navigator.serviceWorker.ready,
|
||||
new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), SW_READY_TIMEOUT_MS)),
|
||||
])
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
/**
|
||||
* Single app service worker: precaching/app-shell (installable PWA) plus
|
||||
* Firebase Cloud Messaging background push. Replaces the old standalone
|
||||
* public/firebase-messaging-sw.js — Firebase is bundled (the nginx CSP blocks
|
||||
* importScripts from the gstatic CDN) and its config is inlined at build time
|
||||
* (exactly what the generated swenv.js used to provide).
|
||||
*/
|
||||
|
||||
import { precacheAndRoute, cleanupOutdatedCaches, createHandlerBoundToURL } from 'workbox-precaching'
|
||||
import { NavigationRoute, registerRoute } from 'workbox-routing'
|
||||
import { NetworkFirst, CacheFirst } from 'workbox-strategies'
|
||||
import { clientsClaim } from 'workbox-core'
|
||||
import { initializeApp } from 'firebase/app'
|
||||
import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw'
|
||||
import { SW_MESSAGE, conversationSessionUrl } from './pwa/pushContract'
|
||||
import { NAVIGATION_DENYLIST } from './pwa/navigationDenylist'
|
||||
|
||||
declare let self: ServiceWorkerGlobalScope
|
||||
|
||||
// A new worker waits until the page asks it to take over, so an agent is
|
||||
// never reloaded mid-reply. The page prompts and then posts SKIP_WAITING.
|
||||
// Calling skipWaiting() unconditionally here activated the new worker while
|
||||
// the open tab kept running the previous bundle's CSS and JS — the app looked
|
||||
// stale until someone happened to reload.
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data?.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting()
|
||||
}
|
||||
})
|
||||
clientsClaim()
|
||||
|
||||
precacheAndRoute(self.__WB_MANIFEST)
|
||||
cleanupOutdatedCaches()
|
||||
|
||||
// SPA navigation fallback. What it must not swallow lives in
|
||||
// ./pwa/navigationDenylist, which is testable without ServiceWorker globals.
|
||||
registerRoute(
|
||||
new NavigationRoute(createHandlerBoundToURL('index.html'), {
|
||||
denylist: NAVIGATION_DENYLIST,
|
||||
}),
|
||||
)
|
||||
|
||||
// Runtime config is substituted per-environment at container start, so it must
|
||||
// never be precached. NetworkFirst (not StaleWhileRevalidate): online clients
|
||||
// must always boot on the CURRENT environment's config — a stale cached copy
|
||||
// could keep credentialed traffic pointed at a rotated-away endpoint. The
|
||||
// cache only serves offline boots.
|
||||
registerRoute(
|
||||
({ url }) => url.origin === self.location.origin && url.pathname === '/config.js',
|
||||
new NetworkFirst({ cacheName: 'runtime-config', networkTimeoutSeconds: 3 }),
|
||||
)
|
||||
|
||||
registerRoute(
|
||||
({ url }) => url.origin === 'https://fonts.googleapis.com' || url.origin === 'https://fonts.gstatic.com',
|
||||
new CacheFirst({ cacheName: 'google-fonts' }),
|
||||
)
|
||||
|
||||
// ---- Firebase Cloud Messaging (background push) ----
|
||||
|
||||
// Pull in the same runtime config the page uses. Published images are built
|
||||
// once and configured per deployment at container start, so import.meta.env is
|
||||
// undefined there — without this, self-hosted installs get no background push.
|
||||
// config.js assigns to `self`, which exists in worker scope; importScripts is
|
||||
// synchronous and bypasses the SW's own fetch handling.
|
||||
try {
|
||||
self.importScripts('/config.js')
|
||||
} catch {
|
||||
// Absent in `vite dev` (config.js is served but not always present) and on
|
||||
// any deployment that bakes its config at build time — the env fallbacks
|
||||
// below cover both.
|
||||
}
|
||||
|
||||
const runtimeConfig = (self as unknown as { APP_CONFIG?: Record<string, string> }).APP_CONFIG ?? {}
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: runtimeConfig.FIREBASE_API_KEY || import.meta.env.VITE_FIREBASE_API_KEY,
|
||||
authDomain: runtimeConfig.FIREBASE_AUTH_DOMAIN || import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: runtimeConfig.FIREBASE_PROJECT_ID || import.meta.env.VITE_FIREBASE_PROJECT_ID,
|
||||
messagingSenderId:
|
||||
runtimeConfig.FIREBASE_MESSAGING_SENDER_ID || import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: runtimeConfig.FIREBASE_APP_ID || import.meta.env.VITE_FIREBASE_APP_ID,
|
||||
storageBucket:
|
||||
runtimeConfig.FIREBASE_STORAGE_BUCKET || import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
|
||||
measurementId:
|
||||
runtimeConfig.FIREBASE_MEASUREMENT_ID || import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
|
||||
}
|
||||
|
||||
if (firebaseConfig.apiKey) {
|
||||
const messaging = getMessaging(initializeApp(firebaseConfig))
|
||||
|
||||
onBackgroundMessage(messaging, (payload) => {
|
||||
// Messages are data-only (title/body in data): a notification payload
|
||||
// would make the Firebase SDK auto-display a duplicate copy that also
|
||||
// swallows clicks. The notification?. fallbacks keep older senders working.
|
||||
const title = payload.data?.title || payload.notification?.title || 'ReplyPier'
|
||||
const body = payload.data?.body || payload.notification?.body
|
||||
const sessionId = payload.data?.session_id
|
||||
|
||||
self.registration.showNotification(title, {
|
||||
body,
|
||||
// icon = the large icon in the expanded tray, which may be full colour.
|
||||
// badge = the status-bar icon, which Android builds from the ALPHA CHANNEL
|
||||
// alone: it throws the colours away and tints the silhouette. Pointing this
|
||||
// at the colour PWA icon is why it rendered as a grey blob — the whole
|
||||
// square is opaque, so the silhouette is a square.
|
||||
icon: '/pwa-192x192.png',
|
||||
badge: '/notification-badge-96.png',
|
||||
// One stacked notification per conversation (or per notification otherwise)
|
||||
tag: sessionId || payload.data?.notification_id || undefined,
|
||||
data: payload.data,
|
||||
})
|
||||
|
||||
// Let any open windows refresh their in-app notification state
|
||||
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => {
|
||||
clients.forEach((client) =>
|
||||
client.postMessage({ eventType: SW_MESSAGE.BACKGROUND_NOTIFICATION, data: payload.data }),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Deep-link straight into the conversation on tap (default action)
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close()
|
||||
|
||||
const url = conversationSessionUrl(event.notification.data?.session_id)
|
||||
|
||||
event.waitUntil(
|
||||
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(async (clients) => {
|
||||
// Prefer the focused window so a background tab isn't yanked to the session
|
||||
const client = clients.find((c) => c.focused) ?? clients[0]
|
||||
if (client) {
|
||||
await client.focus()
|
||||
try {
|
||||
// navigate() rejects for windows this SW doesn't control
|
||||
// (hard-reloaded or first-visit tabs) — fall back to messaging the
|
||||
// page so the in-app router performs the navigation.
|
||||
await client.navigate(url)
|
||||
} catch {
|
||||
client.postMessage({ eventType: SW_MESSAGE.OPEN_CONVERSATION, url })
|
||||
}
|
||||
return
|
||||
}
|
||||
return self.clients.openWindow(url)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Procedural "aurora" gradient orbs used as an avatar alternative to an uploaded photo.
|
||||
// Colors are hardcoded hex (not CSS vars) so the orb renders identically in the dashboard,
|
||||
// the preview panel, and the isolated widget bundle (which has no access to app design tokens).
|
||||
|
||||
interface OrbPalette {
|
||||
stops: string
|
||||
glow: string
|
||||
}
|
||||
|
||||
// Each entry is a visibly distinct colored orb the user can choose from.
|
||||
const ORB_PALETTES: OrbPalette[] = [
|
||||
{ stops: '#9D8CFF, #5FE3D6, #C9F24E', glow: 'rgba(157,140,255,0.45)' }, // aurora (default)
|
||||
{ stops: '#FF8A73, #9D8CFF, #5FE3D6', glow: 'rgba(255,138,115,0.40)' }, // coral
|
||||
{ stops: '#5FE3D6, #C9F24E, #9D8CFF', glow: 'rgba(95,227,214,0.40)' }, // teal
|
||||
{ stops: '#C9F24E, #5FE3D6, #FF8A73', glow: 'rgba(201,242,78,0.35)' }, // lime
|
||||
{ stops: '#6EA8FF, #9D8CFF, #5FE3D6', glow: 'rgba(110,168,255,0.42)' }, // blue
|
||||
{ stops: '#FF7AC6, #9D8CFF, #6EA8FF', glow: 'rgba(255,122,198,0.42)' }, // pink
|
||||
{ stops: '#FF8A73, #FFC857, #FF7AC6', glow: 'rgba(255,200,87,0.40)' }, // sunset
|
||||
{ stops: '#7C5CFF, #B388FF, #5FE3D6', glow: 'rgba(124,92,255,0.45)' }, // violet
|
||||
{ stops: '#0EA5A5, #5FE3D6, #C9F24E', glow: 'rgba(14,165,165,0.40)' }, // emerald
|
||||
{ stops: '#F34611, #FF8A73, #FFC857', glow: 'rgba(243,70,17,0.38)' }, // ember
|
||||
]
|
||||
|
||||
// Number of selectable orb variants (for rendering the picker grid).
|
||||
export const ORB_PALETTE_COUNT = ORB_PALETTES.length
|
||||
|
||||
// Stable palette index from a seed (agent name) so the same agent always gets the same orb
|
||||
// when no variant has been explicitly chosen.
|
||||
export const orbIndexForSeed = (seed: string): number =>
|
||||
(seed || '').split('').reduce((a, c) => a + c.charCodeAt(0), 0) % ORB_PALETTES.length
|
||||
|
||||
// Inline style object for a specific orb variant by index.
|
||||
export const getOrbStyleAt = (index: number): Record<string, string> => {
|
||||
const p = ORB_PALETTES[((index % ORB_PALETTES.length) + ORB_PALETTES.length) % ORB_PALETTES.length]
|
||||
return {
|
||||
background: `
|
||||
radial-gradient(circle at 32% 28%, rgba(255,255,255,0.22) 0%, transparent 42%),
|
||||
radial-gradient(circle at 68% 72%, rgba(0,0,0,0.25) 0%, transparent 38%),
|
||||
radial-gradient(ellipse at 50% 50%, ${p.stops})
|
||||
`.trim(),
|
||||
boxShadow: `0 4px 28px ${p.glow}, inset 0 1px 0 rgba(255,255,255,0.15)`,
|
||||
borderRadius: '50%',
|
||||
}
|
||||
}
|
||||
|
||||
// Inline style for the orb, keyed by a seed (used as the stable default).
|
||||
export const getOrbStyle = (seed: string): Record<string, string> => getOrbStyleAt(orbIndexForSeed(seed))
|
||||
|
||||
// Resolve the orb style for an agent: an explicitly chosen variant wins, otherwise
|
||||
// fall back to the seed-derived default. `variant` is whatever is stored in
|
||||
// customization_metadata.orb_variant (may be undefined / non-numeric).
|
||||
export const resolveOrbStyle = (seed: string, variant: unknown): Record<string, string> => {
|
||||
const idx = typeof variant === 'number' && Number.isFinite(variant) ? variant : orbIndexForSeed(seed)
|
||||
return getOrbStyleAt(idx)
|
||||
}
|
||||
|
||||
// Render an orb variant as a self-contained SVG data URI. Storing this in
|
||||
// `photo_url` lets every existing avatar render site (agent list, widget header,
|
||||
// previews) show the orb with no orb-specific logic — they just render the image.
|
||||
export const orbSvgDataUri = (seed: string, variant?: unknown): string => {
|
||||
const idx = typeof variant === 'number' && Number.isFinite(variant) ? variant : orbIndexForSeed(seed)
|
||||
const p = ORB_PALETTES[((idx % ORB_PALETTES.length) + ORB_PALETTES.length) % ORB_PALETTES.length]
|
||||
const [c1, c2, c3] = p.stops.split(',').map((s) => s.trim())
|
||||
const svg =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">' +
|
||||
'<defs>' +
|
||||
'<radialGradient id="o" cx="50%" cy="50%" r="70%">' +
|
||||
`<stop offset="0%" stop-color="${c1}"/>` +
|
||||
`<stop offset="50%" stop-color="${c2}"/>` +
|
||||
`<stop offset="100%" stop-color="${c3}"/>` +
|
||||
'</radialGradient>' +
|
||||
'<radialGradient id="h" cx="32%" cy="28%" r="45%">' +
|
||||
'<stop offset="0%" stop-color="#ffffff" stop-opacity="0.45"/>' +
|
||||
'<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>' +
|
||||
'</radialGradient>' +
|
||||
'<radialGradient id="s" cx="68%" cy="72%" r="42%">' +
|
||||
'<stop offset="0%" stop-color="#000000" stop-opacity="0.3"/>' +
|
||||
'<stop offset="100%" stop-color="#000000" stop-opacity="0"/>' +
|
||||
'</radialGradient>' +
|
||||
'</defs>' +
|
||||
'<circle cx="50" cy="50" r="50" fill="url(#o)"/>' +
|
||||
'<circle cx="50" cy="50" r="50" fill="url(#h)"/>' +
|
||||
'<circle cx="50" cy="50" r="50" fill="url(#s)"/>' +
|
||||
'</svg>'
|
||||
return `data:image/svg+xml,${encodeURIComponent(svg)}`
|
||||
}
|
||||
|
||||
// Terminal-style avatar mark: a monospace ">" prompt on a dark, FULL-BLEED tile tinted
|
||||
// with the accent (matches the Terminal chat theme). Full-bleed so it fully covers the
|
||||
// avatar container at every render site — circular (header/message) or square — with no
|
||||
// transparent corners. Stored in `photo_url` exactly like the orb, so every avatar
|
||||
// render site shows it with no mark-specific logic.
|
||||
export const TERMINAL_MARK_ACCENT = '#C9F24E'
|
||||
// Only accept simple, safe colour tokens (hex / rgb[a] / hsl[a] / named) before
|
||||
// embedding into the SVG, so a stray quote in a stored value can't break out of the
|
||||
// attribute. Anything else falls back to the Terminal lime.
|
||||
const SAFE_COLOR = /^(#[0-9a-f]{3,8}|rgba?\([\d.,\s%]+\)|hsla?\([\d.,\s%]+\)|[a-z]+)$/i
|
||||
export const terminalMarkSvgDataUri = (accent: string = TERMINAL_MARK_ACCENT): string => {
|
||||
const c = accent && SAFE_COLOR.test(accent.trim()) ? accent.trim() : TERMINAL_MARK_ACCENT
|
||||
const svg =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">' +
|
||||
// Full-bleed dark base guarantees coverage in any avatar shape.
|
||||
'<rect width="100" height="100" fill="#0B0C10"/>' +
|
||||
`<rect width="100" height="100" fill="${c}" fill-opacity="0.16"/>` +
|
||||
// Inset accent frame for the terminal look.
|
||||
`<rect x="14" y="14" width="72" height="72" rx="14" fill="none" stroke="${c}" stroke-opacity="0.6" stroke-width="4"/>` +
|
||||
`<text x="50" y="52" font-family="'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace" font-size="52" font-weight="700" fill="${c}" text-anchor="middle" dominant-baseline="central">></text>` +
|
||||
'</svg>'
|
||||
return `data:image/svg+xml,${encodeURIComponent(svg)}`
|
||||
}
|
||||