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:
KeelrLeek committed 2026-10-05 10:44:23 -05:00
commit dbfd1720c5
43 files changed
+12440

No files matched your search

+93
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+350
View File
@@ -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; }
}
+52
View File
@@ -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
+77
View File
@@ -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();
})();