forked from Keelrleek/Harbor
Harbor as of 2026-10-05: the coordination hub for the Open Sea's decomp team
Messages, threads, acknowledgements, claims, bands, tasks, review items and stop flags for a team of AI agents; the chat page and board; host hooks and MCP server; autopilot; tools/watch.py. Put under version control after the weekend's changes so they have history and can be rolled back. Local only.
This commit is contained in:
commit
dbfd1720c5
43 files changed
+12440
No files matched your search
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
+1219
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,350 @@
|
||||
/* Harbor design system: tokens, themes and components shared by the chat page and the board. The colour themes
|
||||
from BroadWorks Enhancer live in themes.css and override these tokens; this file is Harbor's own default look. */
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter"; src: url("InterVariable.woff2") format("woff2"); /* OFL, Inter-OFL.txt */
|
||||
font-weight: 100 900; font-style: normal; font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--font: "Inter", "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif;
|
||||
--font-display: "Inter", "Segoe UI Variable Display", "Segoe UI", system-ui, sans-serif;
|
||||
--mono: "Cascadia Code", "Cascadia Mono", ui-monospace, "SF Mono", Consolas, monospace;
|
||||
|
||||
--bg: #f2f4f8;
|
||||
--bg-tint: radial-gradient(1200px 600px at 100% -10%, #e6ebff 0%, transparent 60%), radial-gradient(900px 500px at -10% 110%, #e8f6f1 0%, transparent 55%);
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f7f9fc;
|
||||
--surface-3: #eef2f8;
|
||||
--text: #111a2c;
|
||||
--text-2: #364259;
|
||||
--muted: #687489;
|
||||
--faint: #9aa4b6;
|
||||
--line: #e3e8f0;
|
||||
--line-2: #d2d9e5;
|
||||
|
||||
--accent: #4361ee;
|
||||
--accent-2: #3650d6;
|
||||
--accent-soft: #edf0ff;
|
||||
--accent-ink: #2c3fb5;
|
||||
--action: #4361ee; --action-2: #3650d6; --on-action: #ffffff; /* filled buttons, the send button, your bubbles */
|
||||
--on-green: #ffffff; --on-red: #ffffff;
|
||||
--green: #097a5a; --green-soft: #e3f5ee;
|
||||
--amber: #b97710; --amber-soft: #fcf1de;
|
||||
--red: #d63f36; --red-soft: #fdebea;
|
||||
--violet: #7356f0; --violet-soft: #f0ecff;
|
||||
--blue: #2a78e4; --blue-soft: #e6f0fd;
|
||||
|
||||
--shadow-1: 0 1px 2px rgba(17, 26, 44, .05);
|
||||
--shadow-2: 0 1px 2px rgba(17, 26, 44, .04), 0 8px 24px -10px rgba(17, 26, 44, .14);
|
||||
--shadow-3: 0 24px 60px -16px rgba(17, 26, 44, .32);
|
||||
--ring: 0 0 0 3px rgba(67, 97, 238, .28);
|
||||
|
||||
--r-1: 8px; --r-2: 12px; --r-3: 16px; --r-4: 22px;
|
||||
--green-ink: color-mix(in oklab, var(--green) 66%, var(--text)); --red-ink: color-mix(in oklab, var(--red) 66%, var(--text));
|
||||
--amber-ink: color-mix(in oklab, var(--amber) 66%, var(--text)); --violet-ink: color-mix(in oklab, var(--violet) 66%, var(--text));
|
||||
--blue-ink: color-mix(in oklab, var(--blue) 66%, var(--text));
|
||||
|
||||
/* the top bar and the sidebar; a theme can colour them apart from the page */
|
||||
--header: color-mix(in srgb, var(--surface) 58%, transparent);
|
||||
--side: color-mix(in srgb, var(--surface) 72%, transparent); --side-solid: var(--surface);
|
||||
--side-text: var(--text); --side-muted: var(--muted); --side-hover: var(--surface-3); --side-line: var(--line);
|
||||
--side-active: var(--accent-soft); --side-active-text: var(--accent-ink); --side-accent: var(--accent);
|
||||
--side-green: color-mix(in oklab, var(--green) 34%, var(--side-text));
|
||||
--side-green-soft: color-mix(in srgb, var(--side-green) 16%, var(--side-solid));
|
||||
--ease: cubic-bezier(.2, .8, .2, 1);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--bg: #0b0f17;
|
||||
--bg-tint: radial-gradient(1200px 600px at 100% -10%, rgba(76, 99, 230, .16) 0%, transparent 60%), radial-gradient(900px 500px at -10% 110%, rgba(30, 160, 120, .08) 0%, transparent 55%);
|
||||
--surface: #121825; --surface-2: #161d2b; --surface-3: #1c2435;
|
||||
--text: #e9eef7; --text-2: #c3cbdb; --muted: #8b96ac; --faint: #5d687e;
|
||||
--line: #222b3c; --line-2: #2c374a;
|
||||
--accent: #7189ff; --accent-2: #8a9dff; --accent-soft: rgba(113, 137, 255, .14); --accent-ink: #b9c5ff;
|
||||
--action: #4f6af0; --action-2: #6079f5; --on-green: #0b0f17; --on-red: #0b0f17;
|
||||
--green: #33c995; --green-soft: rgba(51, 201, 149, .13);
|
||||
--amber: #f1b44c; --amber-soft: rgba(241, 180, 76, .13);
|
||||
--red: #ff6b61; --red-soft: rgba(255, 107, 97, .13);
|
||||
--violet: #a68cff; --violet-soft: rgba(166, 140, 255, .14);
|
||||
--blue: #5ba3ff; --blue-soft: rgba(91, 163, 255, .13);
|
||||
--shadow-1: 0 1px 2px rgba(0, 0, 0, .35);
|
||||
--shadow-2: 0 1px 2px rgba(0, 0, 0, .3), 0 10px 30px -12px rgba(0, 0, 0, .55);
|
||||
--shadow-3: 0 30px 70px -20px rgba(0, 0, 0, .7);
|
||||
--ring: 0 0 0 3px rgba(113, 137, 255, .35);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
--bg: #0b0f17;
|
||||
--bg-tint: radial-gradient(1200px 600px at 100% -10%, rgba(76, 99, 230, .16) 0%, transparent 60%), radial-gradient(900px 500px at -10% 110%, rgba(30, 160, 120, .08) 0%, transparent 55%);
|
||||
--surface: #121825; --surface-2: #161d2b; --surface-3: #1c2435;
|
||||
--text: #e9eef7; --text-2: #c3cbdb; --muted: #8b96ac; --faint: #5d687e;
|
||||
--line: #222b3c; --line-2: #2c374a;
|
||||
--accent: #7189ff; --accent-2: #8a9dff; --accent-soft: rgba(113, 137, 255, .14); --accent-ink: #b9c5ff;
|
||||
--action: #4f6af0; --action-2: #6079f5; --on-green: #0b0f17; --on-red: #0b0f17;
|
||||
--green: #33c995; --green-soft: rgba(51, 201, 149, .13);
|
||||
--amber: #f1b44c; --amber-soft: rgba(241, 180, 76, .13);
|
||||
--red: #ff6b61; --red-soft: rgba(255, 107, 97, .13);
|
||||
--violet: #a68cff; --violet-soft: rgba(166, 140, 255, .14);
|
||||
--blue: #5ba3ff; --blue-soft: rgba(91, 163, 255, .13);
|
||||
--shadow-1: 0 1px 2px rgba(0, 0, 0, .35);
|
||||
--shadow-2: 0 1px 2px rgba(0, 0, 0, .3), 0 10px 30px -12px rgba(0, 0, 0, .55);
|
||||
--shadow-3: 0 30px 70px -20px rgba(0, 0, 0, .7);
|
||||
--ring: 0 0 0 3px rgba(113, 137, 255, .35);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ base */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; }
|
||||
body {
|
||||
font: 14px/1.5 var(--font); color: var(--text); font-feature-settings: "cv11", "ss03";
|
||||
background-color: var(--bg); background-image: var(--bg-tint); background-attachment: fixed;
|
||||
-webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
|
||||
}
|
||||
button, input, select, textarea { font: inherit; color: inherit; }
|
||||
button { cursor: pointer; }
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
code, .mono { font-family: var(--mono); font-size: .92em; }
|
||||
code { background: var(--surface-3); padding: 1px 5px; border-radius: 5px; }
|
||||
::selection { background: var(--accent-soft); }
|
||||
:focus-visible { outline: none; box-shadow: var(--ring); border-radius: var(--r-1); }
|
||||
* { scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
|
||||
[hidden] { display: none !important; }
|
||||
svg.i { width: 16px; height: 16px; flex-shrink: 0; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
||||
svg.i.lg { width: 20px; height: 20px; }
|
||||
svg.i.sm { width: 14px; height: 14px; }
|
||||
svg.i { pointer-events: none; }
|
||||
.muted { color: var(--muted); } .faint { color: var(--faint); }
|
||||
.note { font-size: 12px; color: var(--muted); }
|
||||
.err { color: var(--red); }
|
||||
.grow { flex: 1; min-width: 0; }
|
||||
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||
.stack-v { display: flex; flex-direction: column; gap: 4px; }
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ buttons */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 34px; padding: 0 13px;
|
||||
border: 1px solid var(--line-2); background: var(--surface); color: var(--text-2); border-radius: var(--r-1);
|
||||
font-size: 13px; font-weight: 600; white-space: nowrap; box-shadow: var(--shadow-1);
|
||||
transition: background .15s, border-color .15s, color .15s, transform .08s, box-shadow .15s;
|
||||
}
|
||||
.btn:hover { background: var(--surface-2); border-color: var(--faint); color: var(--text); }
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn:disabled { opacity: .45; cursor: default; transform: none; }
|
||||
.btn.small, .btn.sm { height: 28px; padding: 0 10px; font-size: 12px; border-radius: 7px; }
|
||||
.btn.primary { background: var(--action); border-color: var(--action); color: var(--on-action); }
|
||||
.btn.primary:hover { background: var(--action-2); border-color: var(--action-2); color: var(--on-action); }
|
||||
.btn.go { background: var(--green); border-color: var(--green); color: var(--on-green); }
|
||||
.btn.go:hover { background: var(--green); filter: brightness(1.07); border-color: var(--green); color: var(--on-green); }
|
||||
.btn.stop, .btn.danger { background: var(--red); border-color: var(--red); color: var(--on-red); }
|
||||
.btn.stop:hover, .btn.danger:hover { background: var(--red); filter: brightness(1.07); border-color: var(--red); color: var(--on-red); }
|
||||
.btn.ghost { background: transparent; border-color: transparent; box-shadow: none; color: var(--muted); }
|
||||
.btn.ghost:hover { background: var(--surface-3); color: var(--text); }
|
||||
.btn.icon { width: 34px; padding: 0; }
|
||||
.btn.icon.sm { width: 28px; }
|
||||
.btn[aria-pressed="true"] { background: var(--accent-soft); border-color: transparent; color: var(--accent-ink); }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ tags and pills */
|
||||
.tag {
|
||||
display: inline-flex; align-items: center; gap: 5px; height: 21px; padding: 0 8px; border-radius: 999px;
|
||||
font-size: 11px; font-weight: 650; letter-spacing: .1px; white-space: nowrap; border: 0;
|
||||
background: var(--surface-3); color: var(--muted);
|
||||
}
|
||||
.tag.green { background: var(--green-soft); color: var(--green-ink); }
|
||||
.tag.red { background: var(--red-soft); color: var(--red-ink); }
|
||||
.tag.amber { background: var(--amber-soft); color: var(--amber-ink); }
|
||||
.tag.violet { background: var(--violet-soft); color: var(--violet-ink); }
|
||||
.tag.blue { background: var(--blue-soft); color: var(--blue-ink); }
|
||||
.tag.solid-red { background: var(--red); color: var(--on-red); }
|
||||
.tag .dot, .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; display: inline-block; flex-shrink: 0; }
|
||||
button.tag { cursor: pointer; }
|
||||
button.tag:hover { filter: brightness(.96); color: var(--accent); }
|
||||
.count-badge {
|
||||
min-width: 20px; height: 20px; padding: 0 6px; border-radius: 999px; display: inline-grid; place-items: center;
|
||||
font-size: 11px; font-weight: 700; background: var(--surface-3); color: var(--muted);
|
||||
}
|
||||
.count-badge.hot { background: var(--red); color: var(--on-red); }
|
||||
.count-badge.accent { background: var(--action); color: var(--on-action); }
|
||||
.count-badge.green { background: var(--green-soft); color: var(--green-ink); }
|
||||
a.tag:hover { text-decoration: none; filter: brightness(.96); }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ live dot */
|
||||
.live { display: inline-flex; align-items: center; gap: 7px; font-size: 12px; color: var(--muted); white-space: nowrap; }
|
||||
.live .dot { width: 8px; height: 8px; background: var(--amber); box-shadow: 0 0 0 3px var(--amber-soft); }
|
||||
.live.ok .dot, .connection.connected .dot { background: var(--green); box-shadow: 0 0 0 3px var(--green-soft); animation: breathe 2.4s ease-in-out infinite; }
|
||||
.live.bad .dot, .connection.failed .dot { background: var(--red); box-shadow: 0 0 0 3px var(--red-soft); }
|
||||
@keyframes breathe { 0%, 100% { box-shadow: 0 0 0 3px var(--green-soft); } 50% { box-shadow: 0 0 0 6px transparent; } }
|
||||
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: .35; } 100% { opacity: 1; } }
|
||||
@keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ avatars */
|
||||
.avatar {
|
||||
--avatar: #7b8797; position: relative; width: 36px; height: 36px; flex-shrink: 0; display: grid; place-items: center;
|
||||
border-radius: 12px; font-size: 11.5px; font-weight: 750; letter-spacing: .2px; color: #fff;
|
||||
background: linear-gradient(145deg, color-mix(in srgb, var(--avatar) 82%, #fff 18%), var(--avatar));
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .14), var(--shadow-1);
|
||||
}
|
||||
.avatar.sm { width: 28px; height: 28px; border-radius: 9px; font-size: 10px; }
|
||||
.avatar.lg { width: 44px; height: 44px; border-radius: 14px; font-size: 13px; }
|
||||
.avatar.xs { width: 22px; height: 22px; border-radius: 7px; font-size: 8.5px; letter-spacing: 0; }
|
||||
.avatar.xs[data-state]::after { width: 8px; height: 8px; border-width: 2px; right: -2px; bottom: -2px; }
|
||||
.avatar[data-state]::after {
|
||||
content: ""; position: absolute; right: -3px; bottom: -3px; width: 12px; height: 12px; border-radius: 50%;
|
||||
border: 2.5px solid var(--surface); background: var(--faint);
|
||||
}
|
||||
.avatar[data-state="working"]::after { background: var(--green); animation: pulse 1.6s infinite; }
|
||||
.avatar[data-state="online"]::after { background: var(--green); }
|
||||
.avatar[data-state="stopped"]::after { background: var(--red); }
|
||||
.avatar[data-state="trial"]::after { background: var(--amber); }
|
||||
.claude { --avatar: #cc8a65; } .codex { --avatar: #4688e6; } .deepseek { --avatar: #8071ce; }
|
||||
.opencode { --avatar: #39a48d; } .kimi { --avatar: #b45b9b; } .user { --avatar: #536070; }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ cards */
|
||||
.card {
|
||||
background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-3); box-shadow: var(--shadow-2);
|
||||
min-width: 0; animation: rise .35s var(--ease) both;
|
||||
}
|
||||
.card > header {
|
||||
display: flex; align-items: center; gap: 10px; padding: 14px 18px 12px; border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.card > header h2 { margin: 0; font: 650 13.5px/1.3 var(--font-display); letter-spacing: -.1px; display: flex; align-items: center; gap: 8px; }
|
||||
.card > header h2 svg.i { color: var(--muted); }
|
||||
.card > header .count { font-size: 12px; color: var(--muted); }
|
||||
.card .body { padding: 14px 18px 16px; }
|
||||
.card .body.flush { padding: 0; }
|
||||
.scroll { overflow-x: auto; }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ forms */
|
||||
input[type=text], input[type=search], select, textarea {
|
||||
background: var(--surface-2); border: 1px solid var(--line-2); border-radius: var(--r-1); padding: 7px 10px;
|
||||
font-size: 13px; min-width: 0; color: var(--text); transition: border-color .15s, box-shadow .15s, background .15s;
|
||||
}
|
||||
input[type=text]:focus, input[type=search]:focus, select:focus, textarea:focus {
|
||||
outline: none; border-color: var(--accent); box-shadow: var(--ring); background: var(--surface);
|
||||
}
|
||||
select { padding-right: 26px; appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%); background-position: calc(100% - 14px) 52%, calc(100% - 9px) 52%; background-size: 5px 5px; background-repeat: no-repeat; }
|
||||
textarea { width: 100%; resize: vertical; min-height: 72px; line-height: 1.5; }
|
||||
input::placeholder, textarea::placeholder { color: var(--faint); }
|
||||
.check { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--text-2); cursor: pointer; user-select: none; }
|
||||
.check input { accent-color: var(--action); width: 15px; height: 15px; margin: 0; }
|
||||
.switch { position: relative; display: inline-flex; align-items: center; gap: 9px; cursor: pointer; font-size: 12.5px; color: var(--text-2); user-select: none; }
|
||||
.switch input { appearance: none; width: 34px; height: 20px; border-radius: 999px; background: var(--line-2); position: relative; margin: 0; cursor: pointer; transition: background .2s; }
|
||||
.switch input::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: #fff; box-shadow: var(--shadow-1); transition: transform .2s var(--ease); }
|
||||
.switch input:checked { background: var(--action); }
|
||||
.switch input:checked::after { transform: translateX(14px); }
|
||||
.toggle-chip { position: relative; display: inline-flex; }
|
||||
.toggle-chip input { position: absolute; opacity: 0; inset: 0; cursor: pointer; margin: 0; }
|
||||
.toggle-chip span { display: inline-flex; align-items: center; gap: 5px; height: 26px; padding: 0 10px; border-radius: 999px; border: 1px solid var(--line-2); font-size: 11.5px; font-weight: 600; color: var(--muted); background: var(--surface); transition: all .15s; }
|
||||
.toggle-chip input:checked + span { background: var(--accent-soft); color: var(--accent-ink); border-color: transparent; }
|
||||
.toggle-chip input:focus-visible + span { box-shadow: var(--ring); }
|
||||
.toggle-chip.danger input:checked + span { background: var(--red-soft); color: var(--red); }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ tables */
|
||||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .7px; color: var(--faint); font-weight: 650; padding: 8px 12px; border-bottom: 1px solid var(--line); background: var(--surface-2); }
|
||||
td { padding: 10px 12px; border-top: 1px solid var(--line); vertical-align: top; overflow-wrap: anywhere; }
|
||||
tbody tr:first-child td { border-top: 0; }
|
||||
tr.click { cursor: pointer; }
|
||||
tr.click:hover td, tbody tr:hover td { background: var(--surface-2); }
|
||||
td.mono { white-space: nowrap; color: var(--muted); }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ empty states */
|
||||
.empty { display: grid; justify-items: center; gap: 6px; text-align: center; color: var(--muted); padding: 22px 12px; font-size: 13px; }
|
||||
.empty strong { color: var(--text-2); font-size: 14px; }
|
||||
.empty svg.i { width: 26px; height: 26px; color: var(--faint); stroke-width: 1.5; }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ dialogs */
|
||||
dialog {
|
||||
border: 1px solid var(--line); border-radius: var(--r-4); background: var(--surface); color: var(--text);
|
||||
padding: 0; width: min(760px, calc(100vw - 32px)); box-shadow: var(--shadow-3); max-height: 86vh;
|
||||
}
|
||||
dialog[open] { animation: rise .25s var(--ease); }
|
||||
dialog::backdrop { background: rgba(8, 12, 20, .5); backdrop-filter: blur(3px); }
|
||||
dialog > header, dialog form > header { display: flex; align-items: center; gap: 10px; padding: 16px 20px; border-bottom: 1px solid var(--line); }
|
||||
dialog h2 { margin: 0; font: 650 15px/1.3 var(--font-display); flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
||||
dialog .body { padding: 16px 20px 20px; overflow: auto; }
|
||||
dialog footer { display: flex; justify-content: flex-end; gap: 8px; padding: 14px 20px; border-top: 1px solid var(--line); background: var(--surface-2); border-radius: 0 0 var(--r-4) var(--r-4); }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ segmented radio */
|
||||
.segmented { display: inline-flex; padding: 3px; background: var(--surface-3); border-radius: 10px; gap: 2px; }
|
||||
.segmented label { position: relative; }
|
||||
.segmented input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
|
||||
.segmented span { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 8px; font-size: 12.5px; font-weight: 600; color: var(--muted); transition: all .15s; }
|
||||
.segmented input:checked + span { background: var(--surface); color: var(--text); box-shadow: var(--shadow-1); }
|
||||
.segmented input:focus-visible + span { box-shadow: var(--ring); }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ availability pill */
|
||||
.avail {
|
||||
display: inline-flex; align-items: center; gap: 8px; height: 34px; padding: 0 14px 0 12px; border-radius: 999px; white-space: nowrap;
|
||||
border: 1px solid var(--line-2); background: var(--surface); color: var(--text-2); font-size: 12.5px; font-weight: 650; box-shadow: var(--shadow-1);
|
||||
transition: background .15s, border-color .15s;
|
||||
}
|
||||
.avail::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 0 3px var(--green-soft); flex-shrink: 0; }
|
||||
.avail:hover { background: var(--surface-2); border-color: var(--faint); }
|
||||
.avail.away { color: var(--amber); border-color: color-mix(in srgb, var(--amber) 45%, var(--line-2)); }
|
||||
.avail.away::before { background: var(--amber); box-shadow: 0 0 0 3px var(--amber-soft); }
|
||||
.avail.dnd { color: var(--red); border-color: color-mix(in srgb, var(--red) 45%, var(--line-2)); }
|
||||
.avail.dnd::before { background: var(--red); box-shadow: 0 0 0 3px var(--red-soft); }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ misc shared */
|
||||
.working { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--green); font-weight: 650; }
|
||||
.working::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--green); animation: pulse 1.4s infinite; }
|
||||
.kbd { font: 600 11px var(--mono); padding: 1px 6px; border: 1px solid var(--line-2); border-bottom-width: 2px; border-radius: 5px; color: var(--muted); background: var(--surface); }
|
||||
.divider { height: 1px; background: var(--line); margin: 12px 0; }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ the sidebar's colours */
|
||||
/* Inside a sidebar the general tokens point at the theme's side colours, so everything in it follows the theme. */
|
||||
.side, .sidebar {
|
||||
--text: var(--side-text); --text-2: var(--side-text); --muted: var(--side-muted); --faint: var(--side-muted);
|
||||
--line: var(--side-line); --line-2: var(--side-line);
|
||||
--surface: var(--side-solid); --surface-2: var(--side-hover); --surface-3: var(--side-hover);
|
||||
--accent: var(--side-accent); --accent-soft: var(--side-active); --accent-ink: var(--side-active-text);
|
||||
--green: var(--side-green); --green-soft: var(--side-green-soft);
|
||||
color: var(--text); background: var(--side);
|
||||
}
|
||||
.side .segmented input:checked + span, .sidebar .segmented input:checked + span { background: var(--side-active); color: var(--side-active-text); box-shadow: none; }
|
||||
.theme-btn {
|
||||
display: inline-flex; align-items: center; gap: 8px; height: 32px; padding: 0 11px; min-width: 0; border-radius: 9px;
|
||||
border: 1px solid var(--line-2); background: var(--surface-2); color: var(--text-2); font-size: 12.5px; font-weight: 600;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: background .15s, border-color .15s;
|
||||
}
|
||||
.theme-btn:hover { background: var(--surface-3); border-color: var(--accent); }
|
||||
.theme-btn svg.i { color: var(--accent); }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ the theme picker */
|
||||
.theme-dialog { width: min(860px, calc(100vw - 32px)); }
|
||||
.theme-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 18px; }
|
||||
.theme-row .field-label { margin: 0; }
|
||||
.theme-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 172px), 1fr)); gap: 12px; }
|
||||
.tcard {
|
||||
display: flex; flex-direction: column; gap: 9px; padding: 8px 8px 10px; text-align: left; color: var(--text);
|
||||
border: 1px solid var(--line); border-radius: var(--r-2); background: var(--surface-2);
|
||||
transition: border-color .15s, transform .15s var(--ease), box-shadow .15s;
|
||||
}
|
||||
.tcard:hover { border-color: var(--line-2); transform: translateY(-2px); box-shadow: var(--shadow-2); }
|
||||
.tcard[aria-pressed="true"] { border-color: var(--accent); box-shadow: var(--ring); }
|
||||
.tname { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; padding: 0 3px; font-size: 13px; font-weight: 650; }
|
||||
.tprev {
|
||||
display: grid; grid-template-columns: 24% 1fr; grid-template-rows: 13px 1fr; height: 82px; overflow: hidden;
|
||||
border-radius: 8px; border: 1px solid var(--p-line); background: var(--p-bg);
|
||||
}
|
||||
.tprev i, .tprev b, .tprev em { display: block; font-style: normal; }
|
||||
.tprev .ps { grid-row: 1 / 3; background: var(--p-side); padding: 16px 6px 0; display: flex; flex-direction: column; gap: 6px; }
|
||||
.tprev .ps b { height: 4px; border-radius: 2px; background: var(--p-side-accent); opacity: .45; }
|
||||
.tprev .ps b:first-child { opacity: 1; }
|
||||
.tprev .ph { background: var(--p-header); border-bottom: 1px solid var(--p-line); }
|
||||
.tprev .pb { padding: 8px; display: flex; }
|
||||
.tprev .pc { flex: 1; display: flex; flex-direction: column; gap: 5px; padding: 7px; border-radius: 6px; background: var(--p-surface); border: 1px solid var(--p-line); }
|
||||
.tprev .pc b { height: 4px; width: 78%; border-radius: 2px; background: var(--p-text); opacity: .75; }
|
||||
.tprev .pc b + b { width: 52%; opacity: .4; }
|
||||
.tprev .pc em { margin-top: auto; height: 10px; width: 40%; border-radius: 5px; background: var(--p-action); }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { animation: none !important; transition: none !important; }
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="anchor" viewBox="0 0 24 24"><circle cx="12" cy="5" r="3"/><path d="M12 22V8"/><path d="M5 12H2a10 10 0 0 0 20 0h-3"/></symbol>
|
||||
<symbol id="chat" viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></symbol>
|
||||
<symbol id="thread" viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M7 8h10M7 12h6"/></symbol>
|
||||
<symbol id="layout" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2.5"/><path d="M3 9h18M9 21V9"/></symbol>
|
||||
<symbol id="home" viewBox="0 0 24 24"><path d="M3 10.5 12 3l9 7.5V20a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z"/></symbol>
|
||||
<symbol id="rocket" viewBox="0 0 24 24"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></symbol>
|
||||
<symbol id="tasks" viewBox="0 0 24 24"><path d="m3.5 6.5 1.5 1.5 3-3M3.5 13.5 5 15l3-3M11 7h9M11 14h9M11 20h9M4 20h4"/></symbol>
|
||||
<symbol id="review" viewBox="0 0 24 24"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7M6 9v12"/></symbol>
|
||||
<symbol id="activity" viewBox="0 0 24 24"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></symbol>
|
||||
<symbol id="book" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol>
|
||||
<symbol id="heart" viewBox="0 0 24 24"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/><path d="M3.2 12h6.3l.5-1 2 4.5 2-7 1.5 3.5h5.3"/></symbol>
|
||||
<symbol id="stop" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="2"/></symbol>
|
||||
<symbol id="play" viewBox="0 0 24 24"><path d="M7 4.5v15l12-7.5z"/></symbol>
|
||||
<symbol id="bell" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></symbol>
|
||||
<symbol id="bell-off" viewBox="0 0 24 24"><path d="M13.7 21a2 2 0 0 1-3.4 0M18.6 13A17.9 17.9 0 0 1 18 8M6.3 6.3A5.9 5.9 0 0 0 6 8c0 7-3 9-3 9h14M18 8a6 6 0 0 0-9.3-5M2 2l20 20"/></symbol>
|
||||
<symbol id="volume" viewBox="0 0 24 24"><path d="M11 5 6 9H2v6h4l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M19 5a10 10 0 0 1 0 14"/></symbol>
|
||||
<symbol id="volume-off" viewBox="0 0 24 24"><path d="M11 5 6 9H2v6h4l5 4z"/><path d="m22 9-6 6M16 9l6 6"/></symbol>
|
||||
<symbol id="speak" viewBox="0 0 24 24"><path d="M11 5 6 9H2v6h4l5 4z"/><path d="M15 9h6M15 12h4M15 15h6"/></symbol>
|
||||
<symbol id="mic" viewBox="0 0 24 24"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M19 10v1a7 7 0 0 1-14 0v-1M12 18v4"/></symbol>
|
||||
<symbol id="send" viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></symbol>
|
||||
<symbol id="search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7.5"/><path d="m21 21-4.6-4.6"/></symbol>
|
||||
<symbol id="pin" viewBox="0 0 24 24"><path d="M12 17v5M9 10.8V5h6v5.8a2 2 0 0 0 1.1 1.8l1.8.9A2 2 0 0 1 19 15.3V17H5v-1.7a2 2 0 0 1 1.1-1.8l1.8-.9A2 2 0 0 0 9 10.8zM8 5h8"/></symbol>
|
||||
<symbol id="users" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8"/></symbol>
|
||||
<symbol id="alert" viewBox="0 0 24 24"><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/></symbol>
|
||||
<symbol id="help" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01"/></symbol>
|
||||
<symbol id="clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></symbol>
|
||||
<symbol id="sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M6.3 17.7l-1.4 1.4M19.1 4.9l-1.4 1.4"/></symbol>
|
||||
<symbol id="moon" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z"/></symbol>
|
||||
<symbol id="monitor" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></symbol>
|
||||
<symbol id="x" viewBox="0 0 24 24"><path d="M18 6 6 18M6 6l12 12"/></symbol>
|
||||
<symbol id="copy" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></symbol>
|
||||
<symbol id="refresh" viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-2.6-6.4L21 8"/><path d="M21 3v5h-5"/></symbol>
|
||||
<symbol id="download" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></symbol>
|
||||
<symbol id="plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol>
|
||||
<symbol id="zap" viewBox="0 0 24 24"><path d="M13 2 3 14h9l-1 8 10-12h-9z"/></symbol>
|
||||
<symbol id="terminal" viewBox="0 0 24 24"><path d="m4 17 6-6-6-6M12 19h8"/></symbol>
|
||||
<symbol id="file" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></symbol>
|
||||
<symbol id="check" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></symbol>
|
||||
<symbol id="git" viewBox="0 0 24 24"><path d="M6 3v12"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol>
|
||||
<symbol id="flag" viewBox="0 0 24 24"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1zM4 22v-7"/></symbol>
|
||||
<symbol id="trophy" viewBox="0 0 24 24"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6M18 9h1.5a2.5 2.5 0 0 0 0-5H18M4 22h16M10 14.7V17c0 .6-.5 1-1 1.2C7.9 18.8 7 20.2 7 22M14 14.7V17c0 .6.5 1 1 1.2 1.1.6 2 2 2 3.8"/><path d="M18 2H6v7a6 6 0 0 0 12 0z"/></symbol>
|
||||
<symbol id="sparkle" viewBox="0 0 24 24"><path d="m12 3 1.9 5.8L20 11l-6.1 2.2L12 19l-1.9-5.8L4 11l6.1-2.2z"/></symbol>
|
||||
<symbol id="chevron" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol>
|
||||
<symbol id="menu" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></symbol>
|
||||
<symbol id="external" viewBox="0 0 24 24"><path d="M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></symbol>
|
||||
<symbol id="list" viewBox="0 0 24 24"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></symbol>
|
||||
<symbol id="moon-zz" viewBox="0 0 24 24"><path d="M20 13.5A8 8 0 1 1 10.5 4a6 6 0 0 0 9.5 9.5z"/><path d="M15 3h4l-4 4h4"/></symbol>
|
||||
<symbol id="sunrise" viewBox="0 0 24 24"><path d="M17 18a5 5 0 0 0-10 0M12 2v7M4.2 10.2l1.4 1.4M1 18h2M21 18h2M18.4 11.6l1.4-1.4M23 22H1M8 6l4-4 4 4"/></symbol>
|
||||
<symbol id="palette" viewBox="0 0 24 24"><path d="M12 22a10 10 0 1 1 10-10c0 2.8-2.2 4-4 4h-2.2a1.8 1.8 0 0 0-1.3 3.1A1.8 1.8 0 0 1 12 22z"/><circle cx="7.5" cy="11.5" r="1.3"/><circle cx="10.5" cy="7" r="1.3"/><circle cx="15.5" cy="7.5" r="1.3"/></symbol>
|
||||
<symbol id="pulse-dot" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="8"/></symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,77 @@
|
||||
/* Harbor's theme picker, shared by the chat and the board. A theme is a palette family (themes.css; Harbor's own look in
|
||||
harbor.css is the default) and a mode: system, light or dark. Both are kept in localStorage (harbor.palette,
|
||||
harbor.theme), set on <html data-palette data-theme> before the first paint by each page's head script, and kept in
|
||||
step across open tabs. Any element with data-theme-open opens the Appearance dialog; radios named "theme" set the
|
||||
mode; elements with data-theme-name show the current theme's name. */
|
||||
(function () {
|
||||
var THEMES = [
|
||||
{ id: "harbor", name: "Harbor", note: "Default" },
|
||||
{ id: "dmg", name: "DMG" }, { id: "graphite", name: "Duke" }, { id: "atlas", name: "FLUDD" },
|
||||
{ id: "rosepine", name: "Flowey" }, { id: "gruvbox", name: "Gerudo" }, { id: "contrast", name: "Kill Screen" },
|
||||
{ id: "snes", name: "Mode 7" }, { id: "terra", name: "Shovel Knight" }, { id: "nord", name: "Snowdin" },
|
||||
{ id: "solarized", name: "Solaire" }, { id: "tokyo", name: "Tokyo-to" }, { id: "dracula", name: "Vampire Killer" }
|
||||
];
|
||||
var MODES = [["auto", "System", "monitor"], ["light", "Light", "sun"], ["dark", "Dark", "moon"]];
|
||||
var root = document.documentElement, dark = matchMedia("(prefers-color-scheme: dark)"), dialog = null;
|
||||
function get(k, d) { try { var v = localStorage.getItem("harbor." + k); return v === null ? d : JSON.parse(v); } catch (e) { return d; } }
|
||||
function put(k, v) { try { localStorage.setItem("harbor." + k, JSON.stringify(v)); } catch (e) {} }
|
||||
function mode() { var m = get("theme", "auto"); return m === "light" || m === "dark" ? m : "auto"; }
|
||||
function palette() { var p = get("palette", "harbor"); return THEMES.some(function (t) { return t.id === p; }) ? p : "harbor"; }
|
||||
function side() { var m = mode(); return m === "auto" ? (dark.matches ? "dark" : "light") : m; }
|
||||
function icon(n) { return '<svg class="i sm" aria-hidden="true"><use href="static/icons.svg#' + n + '"></use></svg>'; }
|
||||
function name() { var p = palette(); return THEMES.filter(function (t) { return t.id === p; })[0].name; }
|
||||
|
||||
function apply() {
|
||||
var m = mode(), p = palette();
|
||||
if (m === "auto") delete root.dataset.theme; else root.dataset.theme = m;
|
||||
if (p === "harbor") delete root.dataset.palette; else root.dataset.palette = p;
|
||||
paint();
|
||||
}
|
||||
function paint() {
|
||||
var m = mode(), p = palette(), s = side();
|
||||
document.querySelectorAll('input[name="theme"], input[name="themeMode"]').forEach(function (r) { r.checked = r.value === m; });
|
||||
document.querySelectorAll("[data-theme-name]").forEach(function (e) { e.textContent = name(); });
|
||||
if (!dialog) return;
|
||||
dialog.querySelectorAll("[data-pick]").forEach(function (b) {
|
||||
b.setAttribute("aria-pressed", String(b.dataset.pick === p));
|
||||
b.querySelector(".tprev").dataset.p = b.dataset.pick + "-" + s;
|
||||
});
|
||||
}
|
||||
function set(key, value) { put(key, value); apply(); }
|
||||
|
||||
function build() {
|
||||
dialog = document.createElement("dialog");
|
||||
dialog.className = "theme-dialog";
|
||||
dialog.setAttribute("aria-labelledby", "themeDialogTitle");
|
||||
dialog.innerHTML =
|
||||
'<header><h2 id="themeDialogTitle">Appearance</h2><button type="button" class="btn icon sm ghost" data-theme-close aria-label="Close">' +
|
||||
'<svg class="i" aria-hidden="true"><use href="static/icons.svg#x"></use></svg></button></header>' +
|
||||
'<div class="body"><div class="theme-row"><span class="field-label">Mode</span><div class="segmented" role="radiogroup" aria-label="Mode">' +
|
||||
MODES.map(function (x) { return '<label><input type="radio" name="themeMode" value="' + x[0] + '"><span>' + icon(x[2]) + x[1] + "</span></label>"; }).join("") +
|
||||
'</div></div><div class="theme-grid" role="list">' +
|
||||
THEMES.map(function (t) {
|
||||
return '<button type="button" class="tcard" role="listitem" data-pick="' + t.id + '" aria-pressed="false"><span class="tprev" data-p="' + t.id + '-light" aria-hidden="true">' +
|
||||
'<i class="ps"><b></b><b></b><b></b></i><i class="ph"></i><span class="pb"><span class="pc"><b></b><b></b><em></em></span></span></span>' +
|
||||
'<span class="tname">' + t.name + (t.note ? ' <span class="note">' + t.note + "</span>" : "") + "</span></button>";
|
||||
}).join("") +
|
||||
'</div></div><footer><span class="grow note">Every theme has a light and a dark side; the mode picks which one you see. The choice carries over to the chat and the board.</span>' +
|
||||
'<button type="button" class="btn primary" data-theme-close>Done</button></footer>';
|
||||
document.body.appendChild(dialog);
|
||||
dialog.addEventListener("click", function (e) {
|
||||
if (e.target === dialog || e.target.closest("[data-theme-close]")) return dialog.close();
|
||||
var card = e.target.closest("[data-pick]");
|
||||
if (card) set("palette", card.dataset.pick);
|
||||
});
|
||||
}
|
||||
function open() { if (!dialog) build(); paint(); dialog.showModal(); var on = dialog.querySelector('[aria-pressed="true"]'); if (on) on.focus(); }
|
||||
|
||||
document.addEventListener("change", function (e) {
|
||||
var r = e.target;
|
||||
if (r.matches && r.matches('input[name="theme"], input[name="themeMode"]')) set("theme", r.value);
|
||||
});
|
||||
document.addEventListener("click", function (e) { if (e.target.closest && e.target.closest("[data-theme-open]")) open(); });
|
||||
window.addEventListener("storage", function (e) { if (e.key === "harbor.theme" || e.key === "harbor.palette") apply(); });
|
||||
if (dark.addEventListener) dark.addEventListener("change", paint);
|
||||
window.HarborTheme = { themes: THEMES, apply: apply, open: open, mode: mode, palette: palette, side: side };
|
||||
apply();
|
||||
})();
|
||||
Reference in new issue
Block a user