summaryrefslogtreecommitdiff
path: root/tools/copland
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
committercashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
commit4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch)
treec296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /tools/copland
downloadrelic-master.tar.gz
initial commitHEADmaster
Diffstat (limited to 'tools/copland')
-rw-r--r--tools/copland/dist/copland-skin.config827
-rw-r--r--tools/copland/package-lock.json549
-rw-r--r--tools/copland/package.json15
-rw-r--r--tools/copland/playwright.config.mjs20
-rw-r--r--tools/copland/scripts/build-config.mjs30
-rwxr-xr-xtools/copland/scripts/create-demo.sh113
-rwxr-xr-xtools/copland/scripts/fossil-env.sh42
-rwxr-xr-xtools/copland/scripts/install-repolist.sh34
-rwxr-xr-xtools/copland/scripts/install.sh24
-rwxr-xr-xtools/copland/scripts/preview.sh31
-rw-r--r--tools/copland/src/copland.scss443
-rw-r--r--tools/copland/tests/fixtures/org/all.html50
-rw-r--r--tools/copland/tests/fixtures/org/all.org52
-rw-r--r--tools/copland/tests/fixtures/org/all.txt66
-rw-r--r--tools/copland/tests/fixtures/org/security.html9
-rw-r--r--tools/copland/tests/fixtures/org/security.org13
-rwxr-xr-xtools/copland/tests/org-renderer.sh30
-rwxr-xr-xtools/copland/tests/serve-fixture.sh18
-rwxr-xr-xtools/copland/tests/smoke.sh188
-rw-r--r--tools/copland/tests/visual.spec.mjs94
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/home-dark-linux.pngbin0 -> 21720 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/home-light-linux.pngbin0 -> 21117 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/login-dark-linux.pngbin0 -> 33606 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/login-light-linux.pngbin0 -> 32850 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/org-document-dark-linux.pngbin0 -> 68961 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/org-document-light-linux.pngbin0 -> 67587 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/org-document-mobile-linux.pngbin0 -> 53550 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-dark-linux.pngbin0 -> 42595 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-light-linux.pngbin0 -> 41174 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-mobile-linux.pngbin0 -> 31636 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/repository-list-dark-linux.pngbin0 -> 16492 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/repository-list-light-linux.pngbin0 -> 15656 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/timeline-dark-linux.pngbin0 -> 46898 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/timeline-light-linux.pngbin0 -> 45662 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/timeline-mobile-linux.pngbin0 -> 35066 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/tree-dark-linux.pngbin0 -> 50142 bytes
-rw-r--r--tools/copland/tests/visual.spec.mjs-snapshots/tree-light-linux.pngbin0 -> 48683 bytes
37 files changed, 2648 insertions, 0 deletions
diff --git a/tools/copland/dist/copland-skin.config b/tools/copland/dist/copland-skin.config
new file mode 100644
index 0000000..02392e8
--- /dev/null
+++ b/tools/copland/dist/copland-skin.config
@@ -0,0 +1,827 @@
+# Copland Fossil skin configuration
+# Generated deterministically by tools/copland/scripts/build-config.mjs.
+# Import with: fossil configuration import dist/copland-skin.config -R REPOSITORY
+config /config 12869
+1 'css' value ':root {
+ color-scheme: light dark;
+ --page-bg: #fff;
+ --surface: #fff;
+ --surface-subtle: #f5f5f5;
+ --surface-muted: #e9ecef;
+ --surface-active: #ddd;
+ --text: #212529;
+ --text-muted: #6c757d;
+ --text-faint: #767676;
+ --border: #ced4da;
+ --border-strong: #888;
+ --link: #0645d9;
+ --link-hover: #002f9f;
+ --primary: #0056d6;
+ --primary-hover: #0044aa;
+ --primary-text: #fff;
+ --danger: #b00020;
+ --danger-bg: #f8d7da;
+ --success: #146c2e;
+ --success-bg: #d4edda;
+ --warning-bg: #fff3cd;
+ --info-bg: #d1ecf1;
+ --code-bg: #f1f3f5;
+ --selection: #b9d6ff;
+ --focus: #ffbf47;
+ --shadow: rgba(0, 0, 0, 0.12);
+ --container: 71.25rem;
+ --gutter: 1rem;
+ --radius: 0;
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --page-bg: #212529;
+ --surface: #212529;
+ --surface-subtle: #191c1f;
+ --surface-muted: #2d3237;
+ --surface-active: #3b4147;
+ --text: #f1f3f5;
+ --text-muted: #c2c7cc;
+ --text-faint: #adb5bd;
+ --border: #565e65;
+ --border-strong: #8b949c;
+ --link: #5c9cff;
+ --link-hover: #91bcff;
+ --primary: #4d8ee8;
+ --primary-hover: #70a7f2;
+ --primary-text: #101214;
+ --danger: #ff6b78;
+ --danger-bg: #57242a;
+ --success: #65d47d;
+ --success-bg: #183d24;
+ --warning-bg: #514318;
+ --info-bg: #193e47;
+ --code-bg: #17191b;
+ --selection: #173f70;
+ --focus: #ffd166;
+ --shadow: rgba(0, 0, 0, 0.4);
+ }
+}
+* {
+ box-sizing: border-box;
+}
+
+html {
+ min-height: 100%;
+ scrollbar-gutter: stable;
+ font-size: 16px;
+}
+
+body {
+ min-height: 100vh;
+ margin: 0;
+ padding: 0 0 1rem;
+ background: var(--page-bg);
+ color: var(--text);
+ font-family: var(--font-sans);
+ font-size: 1rem;
+ line-height: 1.5;
+ text-align: initial;
+}
+
+::selection {
+ background: var(--selection);
+}
+
+.container,
+div.content {
+ width: 100%;
+ max-width: var(--container);
+ margin-inline: auto;
+ padding-inline: var(--gutter);
+}
+
+div.content {
+ min-height: 45vh;
+}
+
+body a {
+ color: var(--link);
+ text-decoration: underline;
+ text-underline-offset: 0.12em;
+}
+
+body a:hover {
+ color: var(--link-hover);
+ text-decoration: none;
+}
+
+a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
+textarea:focus-visible, [tabindex]:focus-visible {
+ outline: 3px solid var(--focus);
+ outline-offset: 2px;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ margin: 1rem 0 0.5rem;
+ color: var(--text);
+ font-weight: 500;
+ line-height: 1.2;
+}
+
+h1 {
+ font-size: 1.75rem;
+}
+
+h2 {
+ font-size: 1.45rem;
+}
+
+h3 {
+ font-size: 1.15rem;
+ border-bottom: 1px solid var(--border);
+}
+
+h4 {
+ font-size: 1rem;
+ border-bottom: 1px solid var(--border);
+}
+
+p {
+ margin: 0 0 1rem;
+}
+
+hr {
+ border: 0;
+ border-top: 1px solid var(--border);
+}
+
+.site-header {
+ margin-bottom: 0;
+}
+
+.site-header-row {
+ min-height: 3.25rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.site-brand,
+.resource-title a,
+.account-nav a,
+.mainmenu a {
+ text-decoration: none;
+}
+
+.site-brand {
+ flex: 0 0 auto;
+ color: var(--text);
+ font-size: 1.25rem;
+ font-weight: 600;
+ letter-spacing: -0.025em;
+}
+
+.site-brand::before {
+ content: "";
+ display: inline-block;
+ width: 0.78rem;
+ height: 0.78rem;
+ margin-right: 0.42rem;
+ border: 2px solid currentColor;
+ border-radius: 50%;
+ vertical-align: 0.02rem;
+}
+
+.site-brand:hover,
+.resource-title a:hover,
+.account-nav a:hover,
+.mainmenu a:hover {
+ color: var(--text);
+ text-decoration: underline;
+}
+
+.resource-title {
+ min-width: 0;
+ display: flex;
+ align-items: baseline;
+ gap: 0.4rem;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.resource-title a,
+.resource-page {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.resource-title a {
+ color: var(--text);
+ font-weight: 600;
+}
+
+.resource-separator, .resource-page {
+ color: var(--text-muted);
+}
+
+.account-nav {
+ margin-left: auto;
+ white-space: nowrap;
+ font-size: 0.9rem;
+}
+
+.resource-nav {
+ border-bottom: 3px solid var(--surface-active);
+}
+
+.mainmenu {
+ min-height: 2rem;
+ display: flex;
+ align-items: end;
+ gap: 0;
+ overflow-x: auto;
+ scrollbar-width: thin;
+}
+
+.mainmenu a {
+ flex: 0 0 auto;
+ padding: 0.25rem 0.85rem 0.18rem;
+ color: var(--text-muted);
+ white-space: nowrap;
+}
+
+.mainmenu a.active,
+.mainmenu a.selected,
+.mainmenu a:hover {
+ color: var(--text);
+}
+
+.mainmenu a.active,
+.mainmenu a.selected {
+ background: var(--surface-active);
+}
+
+.page-title {
+ margin-top: 0.9rem;
+ margin-bottom: 0.75rem;
+ font-size: 1.35rem;
+ font-weight: 500;
+}
+
+.site-footer {
+ margin-top: 2rem;
+ color: var(--text-muted);
+ font-size: 0.8rem;
+}
+
+.site-footer .container {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 0.4rem;
+}
+
+pre, code, kbd, samp, tt {
+ font-family: var(--font-mono);
+}
+
+code, tt {
+ overflow-wrap: anywhere;
+}
+
+pre {
+ max-width: 100%;
+ overflow: auto;
+ margin: 0 0 1rem;
+ padding: 0.75rem 1rem;
+ background: var(--code-bg);
+ color: var(--text);
+ font-size: 0.875rem;
+ line-height: 1.35;
+ tab-size: 4;
+}
+
+pre a {
+ color: inherit;
+}
+
+blockquote {
+ margin: 0 0 1rem;
+ padding-left: 1rem;
+ border-left: 4px solid var(--border);
+}
+
+.org-document {
+ max-width: 100%;
+}
+
+.org-document .org-title {
+ margin-top: 0;
+}
+
+.org-metadata {
+ display: grid;
+ grid-template-columns: max-content minmax(0, 1fr);
+ gap: 0.15rem 0.75rem;
+ margin: 0 0 1rem;
+ color: var(--text-muted);
+ font-size: 0.92rem;
+}
+
+.org-metadata dt {
+ font-weight: 600;
+}
+
+.org-metadata dd {
+ margin: 0;
+}
+
+.org-toc {
+ margin: 0 0 1.25rem;
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--border);
+ background: var(--surface-subtle);
+}
+
+.org-toc h2 {
+ margin-top: 0;
+ font-size: 1.05rem;
+}
+
+.org-toc ol {
+ margin: 0;
+ padding-left: 1.5rem;
+}
+
+.org-toc-level-2 {
+ margin-left: 1rem;
+}
+
+.org-toc-level-3 {
+ margin-left: 2rem;
+}
+
+.org-toc-level-4, .org-toc-level-5, .org-toc-level-6 {
+ margin-left: 3rem;
+}
+
+.org-table {
+ display: block;
+ overflow-x: auto;
+}
+
+.org-block {
+ border-left: 3px solid var(--border-strong);
+}
+
+.org-example {
+ color: var(--text);
+}
+
+.org-unknown-block {
+ border-left-color: var(--danger);
+}
+
+table {
+ width: 100%;
+ margin-bottom: 1rem;
+ border-collapse: collapse;
+ color: var(--text);
+}
+
+th {
+ text-align: left;
+ font-weight: 600;
+}
+
+th, td {
+ padding: 0.28rem 0.5rem;
+ vertical-align: top;
+ border-top: 1px solid var(--border);
+}
+
+thead th {
+ border-bottom: 2px solid var(--border-strong);
+}
+
+tbody tr:nth-child(odd) {
+ background: color-mix(in srgb, var(--surface-subtle) 70%, transparent);
+}
+
+table.browser, table.report, table.timelineTable {
+ font-size: 0.92rem;
+}
+
+table.browser td:first-child, table.browser td:nth-child(2) {
+ font-family: var(--font-mono);
+}
+
+input, button, select, textarea {
+ font: inherit;
+}
+
+input[type=text], input[type=search], input[type=email], input[type=password],
+input[type=url], input[type=number], textarea, select {
+ max-width: 100%;
+ padding: 0.35rem 0.5rem;
+ border: 1px solid var(--border-strong);
+ border-radius: var(--radius);
+ background: var(--surface);
+ color: var(--text);
+}
+
+textarea {
+ width: 100%;
+}
+
+label {
+ display: inline-block;
+ margin-bottom: 0.25rem;
+}
+
+button, input[type=submit], input[type=button], .button, a.button {
+ display: inline-block;
+ padding: 0.35rem 0.7rem;
+ border: 1px solid var(--primary);
+ border-radius: var(--radius);
+ background: var(--primary);
+ color: var(--primary-text);
+ cursor: pointer;
+ line-height: 1.25;
+ text-decoration: none;
+}
+
+button:hover, input[type=submit]:hover, input[type=button]:hover, .button:hover {
+ border-color: var(--primary-hover);
+ background: var(--primary-hover);
+}
+
+button:disabled, input:disabled {
+ cursor: not-allowed;
+ opacity: 0.65;
+}
+
+fieldset {
+ margin: 0 0 1rem;
+ border: 1px solid var(--border);
+}
+
+legend {
+ padding-inline: 0.35rem;
+}
+
+.submenu, .sectionmenu {
+ margin: 0 0 1rem;
+ padding: 0.4rem 0.6rem;
+ background: var(--surface-subtle);
+ border: 1px solid var(--border);
+}
+
+.submenu a, .sectionmenu a {
+ margin-right: 0.75rem;
+}
+
+.generalError, .error, .fatalError, .loginError,
+.warning, .notice, .success {
+ margin: 0.75rem 0;
+ padding: 0.65rem 0.8rem;
+ border: 1px solid var(--border);
+}
+
+.generalError, .error, .fatalError, .loginError {
+ background: var(--danger-bg);
+ color: var(--text);
+}
+
+.warning {
+ background: var(--warning-bg);
+}
+
+.notice {
+ background: var(--info-bg);
+}
+
+.success {
+ background: var(--success-bg);
+}
+
+.timelineDate {
+ margin-top: 1rem;
+ color: var(--text);
+ font-weight: 600;
+}
+
+.timelineHistDsp {
+ color: var(--text-muted);
+}
+
+.timelineModernCell, .timelineColumnarCell, .timelineDetailCell {
+ padding-block: 0.35rem;
+}
+
+.timelineModernDetail {
+ opacity: 1;
+}
+
+.timelineCompactComment, .timelineComment {
+ line-height: 1.35;
+}
+
+.timelineTime {
+ color: var(--text-muted);
+ white-space: nowrap;
+}
+
+.timelineGraph {
+ min-width: 2rem;
+}
+
+.tl-node {
+ border-radius: 50%;
+}
+
+.timelineModernCell a,
+.timelineColumnarCell a,
+.timelineDetailCell a {
+ color: var(--link) !important;
+}
+
+.timelineModernCell a:hover,
+.timelineColumnarCell a:hover,
+.timelineDetailCell a:hover {
+ color: var(--link-hover) !important;
+}
+
+.filetree {
+ font-family: var(--font-mono);
+ font-size: 0.9rem;
+}
+
+.filetree ul {
+ padding-left: 1.25rem;
+}
+
+.filetree a {
+ color: var(--text);
+}
+
+.filetree a:hover {
+ color: var(--link);
+}
+
+.fileage tr:hover, table.browser tr:hover {
+ background: var(--surface-active);
+}
+
+.diff, .udiff, .sbsdiff {
+ font-family: var(--font-mono);
+ font-size: 0.82rem;
+}
+
+.diff pre, .udiff pre, .sbsdiff pre {
+ background: transparent;
+}
+
+.diffln, .diffhr {
+ color: var(--text-muted);
+ background: var(--surface-muted);
+}
+
+.diffadd, .diffins {
+ background: var(--success-bg);
+}
+
+.diffrm, .diffdel {
+ background: var(--danger-bg);
+}
+
+.diffchng {
+ background: var(--warning-bg);
+}
+
+.selectedText {
+ background: var(--selection);
+}
+
+.forumPost, .forumTime, .forumSel, .forumHierRoot, .forumHier {
+ border-color: var(--border);
+}
+
+.forumPost, .forumTime {
+ background: var(--surface-subtle);
+ color: var(--text);
+}
+
+.forumPostBody {
+ overflow-wrap: anywhere;
+}
+
+.forumPostBody img, .wiki-content img, .markdown img {
+ max-width: 100%;
+ height: auto;
+}
+
+.ticketDspLabel, .ticketDspValue {
+ padding: 0.35rem 0.5rem;
+}
+
+.ticketDspLabel {
+ color: var(--text-muted);
+ font-weight: 600;
+}
+
+.ticketDspValue {
+ border-bottom: 1px solid var(--border);
+}
+
+.branchname, .tagname, .wikiTagCancelled, .infoTagCancelled {
+ display: inline-block;
+ padding: 0.05rem 0.3rem;
+ background: var(--surface-muted);
+ color: var(--text);
+ font-size: 0.82rem;
+}
+
+.statistics-report-table-events th, .statistics-report-table-events td {
+ text-align: right;
+}
+
+.statistics-report-table-events th:first-child, .statistics-report-table-events td:first-child {
+ text-align: left;
+}
+
+.accordion_panel {
+ border-color: var(--border);
+ background: var(--surface);
+}
+
+.accordion_marker {
+ color: var(--text-muted);
+}
+
+body.repolist .resource-title,
+body.repolist .resource-nav,
+body.repolist > .page-title {
+ display: none;
+}
+
+body.repolist div.content {
+ padding-top: 1rem;
+}
+
+body.repolist table td:first-child {
+ font-family: var(--font-mono);
+ font-weight: 600;
+}
+
+@media (prefers-color-scheme: dark) {
+ img {
+ opacity: 0.92;
+ }
+ body .timelineModernCell,
+ body .timelineColumnarCell,
+ body .timelineDetailCell,
+ .timelineModernCell[style*=background-color],
+ .timelineColumnarCell[style*=background-color],
+ .timelineDetailCell[style*=background-color],
+ td[style*=background-color] {
+ background-color: var(--surface-subtle) !important;
+ color: var(--text) !important;
+ }
+ .diffadd, .diffins {
+ color: var(--text);
+ }
+ .diffrm, .diffdel {
+ color: var(--text);
+ }
+}
+@media (max-width: 48rem) {
+ :root {
+ --gutter: 0.75rem;
+ }
+ .site-header-row {
+ min-height: 2.8rem;
+ gap: 0.65rem;
+ }
+ .resource-page, .resource-separator {
+ display: none;
+ }
+ .mainmenu a {
+ padding-inline: 0.65rem;
+ }
+ .page-title {
+ font-size: 1.2rem;
+ }
+ table:not(.sbsdiffcols) {
+ display: block;
+ max-width: 100%;
+ overflow-x: auto;
+ }
+ .desktoponly {
+ display: none !important;
+ }
+}
+@media (max-width: 36rem) {
+ .site-brand {
+ font-size: 1.1rem;
+ }
+ .resource-title {
+ font-size: 0.9rem;
+ }
+ .account-nav {
+ font-size: 0.82rem;
+ }
+ .page-title {
+ margin-top: 0.7rem;
+ }
+ pre {
+ padding: 0.6rem 0.7rem;
+ }
+}
+@media print {
+ .resource-nav, .account-nav, .submenu, .sectionmenu, .site-footer {
+ display: none !important;
+ }
+ body, pre, code {
+ background: #fff !important;
+ color: #000 !important;
+ }
+ a {
+ color: #000;
+ text-decoration: underline;
+ }
+ div.content, .container {
+ max-width: none;
+ padding: 0;
+ }
+}
+'
+config /config 1631
+1 'header' value '<header class="site-header">
+ <div class="container site-header-row">
+ <a class="site-brand" href="$home$index_page">Copland</a>
+ <div class="resource-title">
+ <a href="$home$index_page">$<project_name></a>
+ <span class="resource-separator" aria-hidden="true">/</span>
+ <span class="resource-page">$<title></span>
+ </div>
+ <div class="account-nav">
+ <th1>
+ if {[info exists login]} {
+ html "<a href=''$home/login''>$login</a>"
+ } else {
+ html "<a href=''$home/login''>Log in</a>"
+ }
+ </th1>
+ </div>
+ </div>
+ <div class="resource-nav">
+ <nav class="container mainmenu" aria-label="Repository">
+ <th1>
+ foreach {name url expr class} $mainmenu {
+ if {![capexpr $expr]} continue
+ if {[string match /chat* $url] || [string match chat* $url]
+ || [string match /taglist* $url] || [string match taglist* $url]
+ || [string match /forum* $url] || [string match forum* $url]
+ || [string match /reportlist* $url] || [string match reportlist* $url]
+ || [string match /ticket* $url] || [string match ticket* $url]
+ || [string match /tkt* $url] || [string match tkt* $url]} continue
+ if {[string match /* $url]} {
+ if {[string match $url\[/?#\]* /$current_page/]} {
+ set class "active $class"
+ }
+ set url $home$url
+ }
+ html "<a href=''$url'' class=''$class''>$name</a>\n"
+ }
+ </th1>
+ </nav>
+ </div>
+</header>
+<h1 class="container page-title">$<title></h1>
+'
+config /config 351
+1 'footer' value '<footer class="site-footer">
+ <div class="container">
+ <span>Copland</span>
+ <span aria-hidden="true">&middot;</span>
+ <span>Powered by Fossil $release_version</span>
+ <span aria-hidden="true">&middot;</span>
+ <span>Generated in <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s</span>
+ </div>
+</footer>
+
+'
+config /config 271
+1 'details' value 'pikchr-background: ""
+pikchr-fontscale: "0.9"
+pikchr-foreground: ""
+pikchr-scale: "1.1"
+timeline-arrowheads: 1
+timeline-circle-nodes: 1
+timeline-color-graph-lines: 1
+white-foreground: 0
+
+'
+config /config 16
+1 'js' value '
+'
diff --git a/tools/copland/package-lock.json b/tools/copland/package-lock.json
new file mode 100644
index 0000000..6388ad6
--- /dev/null
+++ b/tools/copland/package-lock.json
@@ -0,0 +1,549 @@
+{
+ "name": "copland-fossil-skin",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "copland-fossil-skin",
+ "version": "0.1.0",
+ "devDependencies": {
+ "@axe-core/playwright": "4.13.0",
+ "@playwright/test": "1.61.1",
+ "sass": "1.103.0"
+ }
+ },
+ "node_modules/@axe-core/playwright": {
+ "version": "4.13.0",
+ "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.13.0.tgz",
+ "integrity": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "axe-core": "~4.13.0"
+ },
+ "peerDependencies": {
+ "playwright-core": ">= 1.0.0"
+ }
+ },
+ "node_modules/@parcel/watcher": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz",
+ "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^2.0.3",
+ "is-glob": "^4.0.3",
+ "node-addon-api": "^7.0.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.6.0",
+ "@parcel/watcher-darwin-arm64": "2.6.0",
+ "@parcel/watcher-darwin-x64": "2.6.0",
+ "@parcel/watcher-freebsd-x64": "2.6.0",
+ "@parcel/watcher-linux-arm-glibc": "2.6.0",
+ "@parcel/watcher-linux-arm-musl": "2.6.0",
+ "@parcel/watcher-linux-arm64-glibc": "2.6.0",
+ "@parcel/watcher-linux-arm64-musl": "2.6.0",
+ "@parcel/watcher-linux-x64-glibc": "2.6.0",
+ "@parcel/watcher-linux-x64-musl": "2.6.0",
+ "@parcel/watcher-win32-arm64": "2.6.0",
+ "@parcel/watcher-win32-x64": "2.6.0"
+ }
+ },
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz",
+ "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz",
+ "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz",
+ "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz",
+ "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz",
+ "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz",
+ "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz",
+ "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz",
+ "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz",
+ "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-musl": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz",
+ "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz",
+ "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz",
+ "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
+ "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.61.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/axe-core": {
+ "version": "4.13.0",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz",
+ "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/immutable": {
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
+ "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
+ "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.61.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.62.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
+ "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/playwright/node_modules/playwright-core": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
+ "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
+ "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/sass": {
+ "version": "1.103.0",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.103.0.tgz",
+ "integrity": "sha512-+QpdXUDw19lVqRDlYIyje1Lq/0gHsnNHIl4x1CqeUg13zRhaQN11UvTvouwHWLXc9Q3rQVT6oBhFGRYJ5Z8gHw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^5.0.0",
+ "immutable": "^5.1.5",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher": "^2.4.1"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ }
+ }
+}
diff --git a/tools/copland/package.json b/tools/copland/package.json
new file mode 100644
index 0000000..da314aa
--- /dev/null
+++ b/tools/copland/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "copland-fossil-skin",
+ "version": "0.1.0",
+ "private": true,
+ "description": "A SourceHut-inspired skin for Fossil SCM",
+ "scripts": {
+ "build": "sass --no-source-map --style=expanded src/copland.scss ../../skins/copland/css.txt && node scripts/build-config.mjs",
+ "test:visual": "playwright test"
+ },
+ "devDependencies": {
+ "@axe-core/playwright": "4.13.0",
+ "@playwright/test": "1.61.1",
+ "sass": "1.103.0"
+ }
+}
diff --git a/tools/copland/playwright.config.mjs b/tools/copland/playwright.config.mjs
new file mode 100644
index 0000000..979000a
--- /dev/null
+++ b/tools/copland/playwright.config.mjs
@@ -0,0 +1,20 @@
+import { defineConfig } from "@playwright/test";
+
+export default defineConfig({
+ testDir: "./tests",
+ testMatch: "visual.spec.mjs",
+ outputDir: "test-output",
+ reporter: [["list"], ["html", { open: "never" }]],
+ use: {
+ baseURL: "http://127.0.0.1:18082",
+ browserName: "chromium",
+ trace: "retain-on-failure",
+ },
+ webServer: {
+ command: "COPLAND_TEST_PORT=18082 ./tests/serve-fixture.sh",
+ url: "http://127.0.0.1:18082/",
+ reuseExistingServer: false,
+ timeout: 30_000,
+ },
+});
+
diff --git a/tools/copland/scripts/build-config.mjs b/tools/copland/scripts/build-config.mjs
new file mode 100644
index 0000000..0a24317
--- /dev/null
+++ b/tools/copland/scripts/build-config.mjs
@@ -0,0 +1,30 @@
+import { mkdir, readFile, writeFile } from "node:fs/promises";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const skinDir = resolve(projectRoot, "../../skins/copland");
+const outputPath = resolve(projectRoot, "dist/copland-skin.config");
+const parts = ["css", "header", "footer", "details", "js"];
+
+function fossilQuote(value) {
+ return `'${value.replaceAll("'", "''")}'`;
+}
+
+const records = [];
+for (const name of parts) {
+ const value = await readFile(resolve(skinDir, `${name}.txt`), "utf8");
+ const record = `1 ${fossilQuote(name)} value ${fossilQuote(value)}`;
+ records.push(`config /config ${Buffer.byteLength(record, "utf8")}\n${record}\n`);
+}
+
+const header = [
+ "# Copland Fossil skin configuration",
+ "# Generated deterministically by tools/copland/scripts/build-config.mjs.",
+ "# Import with: fossil configuration import dist/copland-skin.config -R REPOSITORY",
+ "",
+].join("\n");
+
+await mkdir(dirname(outputPath), { recursive: true });
+await writeFile(outputPath, header + records.join(""), "utf8");
+console.log(`Wrote ${outputPath}`);
diff --git a/tools/copland/scripts/create-demo.sh b/tools/copland/scripts/create-demo.sh
new file mode 100755
index 0000000..7027e48
--- /dev/null
+++ b/tools/copland/scripts/create-demo.sh
@@ -0,0 +1,113 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+source "$root_dir/scripts/fossil-env.sh"
+
+if [[ $# -ne 1 ]]; then
+ printf 'Usage: %s TARGET_DIRECTORY\n' "$0" >&2
+ exit 2
+fi
+
+target_dir=$1
+mkdir -p "$target_dir/repositories" "$target_dir/checkouts"
+
+create_repository() {
+ local slug=$1
+ local title=$2
+ local repository="$target_dir/repositories/$slug.fossil"
+ local checkout="$target_dir/checkouts/$slug"
+
+ local is_new=0
+ if [[ ! -f "$repository" ]]; then
+ is_new=1
+ "$FOSSIL" init \
+ --admin-user copland \
+ --date-override 2024-01-01T12:00:00Z \
+ --project-name "$title" \
+ "$repository" >/dev/null
+ fi
+ "$FOSSIL" user default copland -R "$repository"
+ mkdir -p "$checkout"
+ if [[ ! -f "$checkout/.fslckout" && ! -f "$checkout/_FOSSIL_" ]]; then
+ (
+ cd /tmp
+ "$FOSSIL" open --nested --workdir "$checkout" "$repository" >/dev/null
+ )
+ fi
+ (
+ cd "$checkout"
+ "$FOSSIL" user default copland
+ if [[ "$is_new" -eq 1 ]]; then
+ mkdir -p src
+ printf '#+title: %s\n\nA small repository used to preview the Copland Fossil skin.\n\n* Usage\n\nRun the example from a C compiler.\n' "$title" > README.org
+ printf '#include <stdio.h>\n\nint main(void) {\n puts("hello from %s");\n return 0;\n}\n' "$slug" > src/main.c
+ fi
+ if [[ ! -f docs/org-support.org ]]; then
+ mkdir -p docs
+ printf '%s\n' \
+ '#+title: Native Org preview' \
+ '#+author: Copland' \
+ '#+date: 2026-08-21' \
+ '#+options: toc:2' \
+ '' \
+ '* Overview' \
+ '' \
+ 'This public fixture demonstrates *bold*, /italic/, _underlined_,' \
+ '+struck+, ~code~, and =verbatim= text.' \
+ '' \
+ '- Rendered by Fossil' \
+ ' 1. Nested ordered item' \
+ '- [[file:../README.org][Back to the README]]' \
+ '' \
+ '* Data' \
+ '' \
+ '| Format | Native |' \
+ '|--------+--------|' \
+ '| Org | yes |' \
+ '' \
+ '#+begin_src nix' \
+ '{ pkgs ? import <nixpkgs> {} }: pkgs.hello' \
+ '#+end_src' \
+ '' \
+ '#+begin_example' \
+ '<script>alert("escaped")</script>' \
+ '#+end_example' \
+ '' \
+ '** Details' \
+ '' \
+ 'Jump [[*Overview][back to the overview]].' > docs/org-support.org
+ fi
+ "$FOSSIL" addremove >/dev/null
+ if [[ -n $("$FOSSIL" changes) ]]; then
+ "$FOSSIL" commit --no-warnings \
+ --date-override 2024-01-03T12:00:00Z \
+ --user-override copland \
+ -m "Add Org documentation fixture"
+ fi
+ if [[ "$is_new" -eq 1 ]]; then
+ "$FOSSIL" tag add release-0.1 current
+ fi
+ )
+
+ local wiki_fixture="$target_dir/$slug-org-wiki.org"
+ printf '%s\n' \
+ '#+TITLE: This title is intentionally suppressed' \
+ '#+AUTHOR: Copland' \
+ '#+OPTIONS: toc:2' \
+ '' \
+ '* Overview' \
+ '' \
+ 'Open [[wiki:Deployment Guide][the deployment guide]] or the' \
+ '[[file:docs/org-support.org][repository documentation]].' \
+ '' \
+ '** Details' \
+ '' \
+ 'Return [[*Overview][to the overview]].' >"$wiki_fixture"
+ if ! "$FOSSIL" wiki list -R "$repository" | grep -Fxq 'Org Wiki'; then
+ "$FOSSIL" wiki create 'Org Wiki' "$wiki_fixture" -R "$repository" >/dev/null
+ fi
+}
+
+create_repository alpha "Alpha"
+create_repository beta "Beta"
diff --git a/tools/copland/scripts/fossil-env.sh b/tools/copland/scripts/fossil-env.sh
new file mode 100755
index 0000000..4df9a7b
--- /dev/null
+++ b/tools/copland/scripts/fossil-env.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+
+if [[ -z "${FOSSIL:-}" ]]; then
+ FOSSIL=fossil
+fi
+export FOSSIL
+
+fossil_binary_path() {
+ if [[ "$FOSSIL" == */* ]]; then
+ if [[ ! -x "$FOSSIL" ]]; then
+ printf 'Fossil binary is not executable: %s\n' "$FOSSIL" >&2
+ return 1
+ fi
+ (CDPATH= cd -- "$(dirname -- "$FOSSIL")" && printf '%s/%s\n' "$PWD" "$(basename -- "$FOSSIL")")
+ else
+ command -v "$FOSSIL"
+ fi
+}
+
+require_org_fossil() {
+ local binary_path
+ if ! binary_path=$(fossil_binary_path); then
+ printf 'Fossil was not found. Run `make fossil` or set FOSSIL=/path/to/fossil.\n' >&2
+ return 1
+ fi
+ if ! "$FOSSIL" help test-org-render >/dev/null 2>&1; then
+ printf 'Fossil at %s has no native Org renderer. Run `make fossil` or set FOSSIL to the patched binary.\n' "$binary_path" >&2
+ return 1
+ fi
+ printf 'Fossil binary: %s\n' "$binary_path"
+}
+
+verify_org_mime() {
+ local repository=$1
+ local mime_result
+ mime_result=$("$FOSSIL" test-mimetype -R "$repository" probe.org)
+ if [[ "$mime_result" != *"text/x-org"* ]]; then
+ printf 'Selected Fossil does not map .org to text/x-org: %s\n' "$mime_result" >&2
+ return 1
+ fi
+ printf 'Org MIME check: text/x-org\n'
+}
diff --git a/tools/copland/scripts/install-repolist.sh b/tools/copland/scripts/install-repolist.sh
new file mode 100755
index 0000000..d59f688
--- /dev/null
+++ b/tools/copland/scripts/install-repolist.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+source "$root_dir/scripts/fossil-env.sh"
+config_file="$root_dir/dist/copland-skin.config"
+
+if [[ $# -ne 1 ]]; then
+ printf 'Usage: %s REPOSITORY_DIRECTORY\n' "$0" >&2
+ exit 2
+fi
+
+repository_dir=$1
+control_repository="$repository_dir/00-copland-skin.fossil"
+
+if [[ ! -d "$repository_dir" ]]; then
+ printf 'Repository directory does not exist: %s\n' "$repository_dir" >&2
+ exit 1
+fi
+if [[ ! -f "$config_file" ]]; then
+ printf 'Built skin not found. Run make build first.\n' >&2
+ exit 1
+fi
+
+if [[ ! -f "$control_repository" ]]; then
+ "$FOSSIL" init "$control_repository" >/dev/null
+fi
+
+"$FOSSIL" configuration import "$config_file" -R "$control_repository"
+"$FOSSIL" settings repolist-skin 2 --exact -R "$control_repository"
+"$FOSSIL" sql -R "$control_repository" \
+ "REPLACE INTO config(name,value,mtime) VALUES('project-name','Copland',unixepoch());"
+
+printf 'Configured hidden repository-list skin: %s\n' "$control_repository"
diff --git a/tools/copland/scripts/install.sh b/tools/copland/scripts/install.sh
new file mode 100755
index 0000000..7fe0e1c
--- /dev/null
+++ b/tools/copland/scripts/install.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+source "$root_dir/scripts/fossil-env.sh"
+config_file="$root_dir/dist/copland-skin.config"
+
+if [[ $# -ne 1 ]]; then
+ printf 'Usage: %s REPOSITORY\n' "$0" >&2
+ exit 2
+fi
+
+repository=$1
+if [[ ! -f "$repository" ]]; then
+ printf 'Repository does not exist: %s\n' "$repository" >&2
+ exit 1
+fi
+if [[ ! -f "$config_file" ]]; then
+ printf 'Built skin not found. Run make build first.\n' >&2
+ exit 1
+fi
+
+"$FOSSIL" configuration import "$config_file" -R "$repository"
+printf 'Installed Copland into %s\n' "$repository"
diff --git a/tools/copland/scripts/preview.sh b/tools/copland/scripts/preview.sh
new file mode 100755
index 0000000..c82bec8
--- /dev/null
+++ b/tools/copland/scripts/preview.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+source_root=$(CDPATH= cd -- "$root_dir/../.." && pwd)
+source "$root_dir/scripts/fossil-env.sh"
+preview_dir="$source_root/.copland-preview"
+nix_repository="$source_root/.repository/nix.fossil"
+port=${PORT:-8080}
+
+require_org_fossil
+"$root_dir/scripts/create-demo.sh" "$preview_dir"
+verify_org_mime "$preview_dir/repositories/alpha.fossil"
+
+if [[ ! -f "$nix_repository" ]]; then
+ printf 'Nix documentation repository not found: %s\n' "$nix_repository" >&2
+ exit 1
+fi
+ln -sfn "$nix_repository" "$preview_dir/repositories/nix.fossil"
+verify_org_mime "$preview_dir/repositories/nix.fossil"
+
+printf 'Copland preview: http://127.0.0.1:%s/\n' "$port"
+printf 'Nix wiki: http://127.0.0.1:%s/nix/wiki\n' "$port"
+printf 'Press Ctrl-C to stop the server.\n'
+FOSSIL_REPOLIST_TITLE=Copland exec "$FOSSIL" server \
+ --localhost \
+ --localauth \
+ --port "$port" \
+ --repolist \
+ --skin copland \
+ "$preview_dir/repositories"
diff --git a/tools/copland/src/copland.scss b/tools/copland/src/copland.scss
new file mode 100644
index 0000000..8c92ad5
--- /dev/null
+++ b/tools/copland/src/copland.scss
@@ -0,0 +1,443 @@
+$bp-sm: 36rem;
+$bp-md: 48rem;
+$bp-lg: 62rem;
+$bp-xl: 75rem;
+
+:root {
+ color-scheme: light dark;
+ --page-bg: #fff;
+ --surface: #fff;
+ --surface-subtle: #f5f5f5;
+ --surface-muted: #e9ecef;
+ --surface-active: #ddd;
+ --text: #212529;
+ --text-muted: #6c757d;
+ --text-faint: #767676;
+ --border: #ced4da;
+ --border-strong: #888;
+ --link: #0645d9;
+ --link-hover: #002f9f;
+ --primary: #0056d6;
+ --primary-hover: #0044aa;
+ --primary-text: #fff;
+ --danger: #b00020;
+ --danger-bg: #f8d7da;
+ --success: #146c2e;
+ --success-bg: #d4edda;
+ --warning-bg: #fff3cd;
+ --info-bg: #d1ecf1;
+ --code-bg: #f1f3f5;
+ --selection: #b9d6ff;
+ --focus: #ffbf47;
+ --shadow: rgba(0, 0, 0, 0.12);
+ --container: 71.25rem;
+ --gutter: 1rem;
+ --radius: 0;
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --page-bg: #212529;
+ --surface: #212529;
+ --surface-subtle: #191c1f;
+ --surface-muted: #2d3237;
+ --surface-active: #3b4147;
+ --text: #f1f3f5;
+ --text-muted: #c2c7cc;
+ --text-faint: #adb5bd;
+ --border: #565e65;
+ --border-strong: #8b949c;
+ --link: #5c9cff;
+ --link-hover: #91bcff;
+ --primary: #4d8ee8;
+ --primary-hover: #70a7f2;
+ --primary-text: #101214;
+ --danger: #ff6b78;
+ --danger-bg: #57242a;
+ --success: #65d47d;
+ --success-bg: #183d24;
+ --warning-bg: #514318;
+ --info-bg: #193e47;
+ --code-bg: #17191b;
+ --selection: #173f70;
+ --focus: #ffd166;
+ --shadow: rgba(0, 0, 0, 0.4);
+ }
+}
+
+* { box-sizing: border-box; }
+
+html {
+ min-height: 100%;
+ scrollbar-gutter: stable;
+ font-size: 16px;
+}
+
+body {
+ min-height: 100vh;
+ margin: 0;
+ padding: 0 0 1rem;
+ background: var(--page-bg);
+ color: var(--text);
+ font-family: var(--font-sans);
+ font-size: 1rem;
+ line-height: 1.5;
+ text-align: initial;
+}
+
+::selection { background: var(--selection); }
+
+.container,
+div.content {
+ width: 100%;
+ max-width: var(--container);
+ margin-inline: auto;
+ padding-inline: var(--gutter);
+}
+
+div.content { min-height: 45vh; }
+
+body a {
+ color: var(--link);
+ text-decoration: underline;
+ text-underline-offset: 0.12em;
+}
+
+body a:hover { color: var(--link-hover); text-decoration: none; }
+a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
+textarea:focus-visible, [tabindex]:focus-visible {
+ outline: 3px solid var(--focus);
+ outline-offset: 2px;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ margin: 1rem 0 0.5rem;
+ color: var(--text);
+ font-weight: 500;
+ line-height: 1.2;
+}
+
+h1 { font-size: 1.75rem; }
+h2 { font-size: 1.45rem; }
+h3 { font-size: 1.15rem; border-bottom: 1px solid var(--border); }
+h4 { font-size: 1rem; border-bottom: 1px solid var(--border); }
+p { margin: 0 0 1rem; }
+hr { border: 0; border-top: 1px solid var(--border); }
+
+.site-header { margin-bottom: 0; }
+.site-header-row {
+ min-height: 3.25rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.site-brand,
+.resource-title a,
+.account-nav a,
+.mainmenu a {
+ text-decoration: none;
+}
+
+.site-brand {
+ flex: 0 0 auto;
+ color: var(--text);
+ font-size: 1.25rem;
+ font-weight: 600;
+ letter-spacing: -0.025em;
+}
+
+.site-brand::before {
+ content: "";
+ display: inline-block;
+ width: 0.78rem;
+ height: 0.78rem;
+ margin-right: 0.42rem;
+ border: 2px solid currentColor;
+ border-radius: 50%;
+ vertical-align: 0.02rem;
+}
+
+.site-brand:hover,
+.resource-title a:hover,
+.account-nav a:hover,
+.mainmenu a:hover { color: var(--text); text-decoration: underline; }
+
+.resource-title {
+ min-width: 0;
+ display: flex;
+ align-items: baseline;
+ gap: 0.4rem;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.resource-title a,
+.resource-page {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.resource-title a { color: var(--text); font-weight: 600; }
+.resource-separator, .resource-page { color: var(--text-muted); }
+.account-nav { margin-left: auto; white-space: nowrap; font-size: 0.9rem; }
+
+.resource-nav {
+ border-bottom: 3px solid var(--surface-active);
+}
+
+.mainmenu {
+ min-height: 2rem;
+ display: flex;
+ align-items: end;
+ gap: 0;
+ overflow-x: auto;
+ scrollbar-width: thin;
+}
+
+.mainmenu a {
+ flex: 0 0 auto;
+ padding: 0.25rem 0.85rem 0.18rem;
+ color: var(--text-muted);
+ white-space: nowrap;
+}
+
+.mainmenu a.active,
+.mainmenu a.selected,
+.mainmenu a:hover {
+ color: var(--text);
+}
+
+.mainmenu a.active,
+.mainmenu a.selected { background: var(--surface-active); }
+
+.page-title {
+ margin-top: 0.9rem;
+ margin-bottom: 0.75rem;
+ font-size: 1.35rem;
+ font-weight: 500;
+}
+
+.site-footer {
+ margin-top: 2rem;
+ color: var(--text-muted);
+ font-size: 0.8rem;
+}
+.site-footer .container { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.4rem; }
+
+pre, code, kbd, samp, tt { font-family: var(--font-mono); }
+code, tt { overflow-wrap: anywhere; }
+pre {
+ max-width: 100%;
+ overflow: auto;
+ margin: 0 0 1rem;
+ padding: 0.75rem 1rem;
+ background: var(--code-bg);
+ color: var(--text);
+ font-size: 0.875rem;
+ line-height: 1.35;
+ tab-size: 4;
+}
+pre a { color: inherit; }
+blockquote { margin: 0 0 1rem; padding-left: 1rem; border-left: 4px solid var(--border); }
+
+.org-document {
+ max-width: 100%;
+}
+
+.org-document .org-title { margin-top: 0; }
+
+.org-metadata {
+ display: grid;
+ grid-template-columns: max-content minmax(0, 1fr);
+ gap: 0.15rem 0.75rem;
+ margin: 0 0 1rem;
+ color: var(--text-muted);
+ font-size: 0.92rem;
+}
+
+.org-metadata dt { font-weight: 600; }
+.org-metadata dd { margin: 0; }
+
+.org-toc {
+ margin: 0 0 1.25rem;
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--border);
+ background: var(--surface-subtle);
+}
+
+.org-toc h2 { margin-top: 0; font-size: 1.05rem; }
+.org-toc ol { margin: 0; padding-left: 1.5rem; }
+.org-toc-level-2 { margin-left: 1rem; }
+.org-toc-level-3 { margin-left: 2rem; }
+.org-toc-level-4, .org-toc-level-5, .org-toc-level-6 { margin-left: 3rem; }
+
+.org-table { display: block; overflow-x: auto; }
+.org-block { border-left: 3px solid var(--border-strong); }
+.org-example { color: var(--text); }
+.org-unknown-block { border-left-color: var(--danger); }
+
+table {
+ width: 100%;
+ margin-bottom: 1rem;
+ border-collapse: collapse;
+ color: var(--text);
+}
+th { text-align: left; font-weight: 600; }
+th, td { padding: 0.28rem 0.5rem; vertical-align: top; border-top: 1px solid var(--border); }
+thead th { border-bottom: 2px solid var(--border-strong); }
+tbody tr:nth-child(odd) { background: color-mix(in srgb, var(--surface-subtle) 70%, transparent); }
+table.browser, table.report, table.timelineTable { font-size: 0.92rem; }
+table.browser td:first-child, table.browser td:nth-child(2) { font-family: var(--font-mono); }
+
+input, button, select, textarea { font: inherit; }
+input[type="text"], input[type="search"], input[type="email"], input[type="password"],
+input[type="url"], input[type="number"], textarea, select {
+ max-width: 100%;
+ padding: 0.35rem 0.5rem;
+ border: 1px solid var(--border-strong);
+ border-radius: var(--radius);
+ background: var(--surface);
+ color: var(--text);
+}
+textarea { width: 100%; }
+label { display: inline-block; margin-bottom: 0.25rem; }
+button, input[type="submit"], input[type="button"], .button, a.button {
+ display: inline-block;
+ padding: 0.35rem 0.7rem;
+ border: 1px solid var(--primary);
+ border-radius: var(--radius);
+ background: var(--primary);
+ color: var(--primary-text);
+ cursor: pointer;
+ line-height: 1.25;
+ text-decoration: none;
+}
+button:hover, input[type="submit"]:hover, input[type="button"]:hover, .button:hover {
+ border-color: var(--primary-hover);
+ background: var(--primary-hover);
+}
+button:disabled, input:disabled { cursor: not-allowed; opacity: 0.65; }
+fieldset { margin: 0 0 1rem; border: 1px solid var(--border); }
+legend { padding-inline: 0.35rem; }
+
+.submenu, .sectionmenu {
+ margin: 0 0 1rem;
+ padding: 0.4rem 0.6rem;
+ background: var(--surface-subtle);
+ border: 1px solid var(--border);
+}
+.submenu a, .sectionmenu a { margin-right: 0.75rem; }
+
+.generalError, .error, .fatalError, .loginError,
+.warning, .notice, .success {
+ margin: 0.75rem 0;
+ padding: 0.65rem 0.8rem;
+ border: 1px solid var(--border);
+}
+.generalError, .error, .fatalError, .loginError { background: var(--danger-bg); color: var(--text); }
+.warning { background: var(--warning-bg); }
+.notice { background: var(--info-bg); }
+.success { background: var(--success-bg); }
+
+.timelineDate { margin-top: 1rem; color: var(--text); font-weight: 600; }
+.timelineHistDsp { color: var(--text-muted); }
+.timelineModernCell, .timelineColumnarCell, .timelineDetailCell { padding-block: 0.35rem; }
+.timelineModernDetail { opacity: 1; }
+.timelineCompactComment, .timelineComment { line-height: 1.35; }
+.timelineTime { color: var(--text-muted); white-space: nowrap; }
+.timelineGraph { min-width: 2rem; }
+.tl-node { border-radius: 50%; }
+.timelineModernCell a,
+.timelineColumnarCell a,
+.timelineDetailCell a { color: var(--link) !important; }
+.timelineModernCell a:hover,
+.timelineColumnarCell a:hover,
+.timelineDetailCell a:hover { color: var(--link-hover) !important; }
+
+.filetree { font-family: var(--font-mono); font-size: 0.9rem; }
+.filetree ul { padding-left: 1.25rem; }
+.filetree a { color: var(--text); }
+.filetree a:hover { color: var(--link); }
+.fileage tr:hover, table.browser tr:hover { background: var(--surface-active); }
+
+.diff, .udiff, .sbsdiff { font-family: var(--font-mono); font-size: 0.82rem; }
+.diff pre, .udiff pre, .sbsdiff pre { background: transparent; }
+.diffln, .diffhr { color: var(--text-muted); background: var(--surface-muted); }
+.diffadd, .diffins { background: var(--success-bg); }
+.diffrm, .diffdel { background: var(--danger-bg); }
+.diffchng { background: var(--warning-bg); }
+.selectedText { background: var(--selection); }
+
+.forumPost, .forumTime, .forumSel, .forumHierRoot, .forumHier {
+ border-color: var(--border);
+}
+.forumPost, .forumTime { background: var(--surface-subtle); color: var(--text); }
+.forumPostBody { overflow-wrap: anywhere; }
+.forumPostBody img, .wiki-content img, .markdown img { max-width: 100%; height: auto; }
+
+.ticketDspLabel, .ticketDspValue { padding: 0.35rem 0.5rem; }
+.ticketDspLabel { color: var(--text-muted); font-weight: 600; }
+.ticketDspValue { border-bottom: 1px solid var(--border); }
+
+.branchname, .tagname, .wikiTagCancelled, .infoTagCancelled {
+ display: inline-block;
+ padding: 0.05rem 0.3rem;
+ background: var(--surface-muted);
+ color: var(--text);
+ font-size: 0.82rem;
+}
+
+.statistics-report-table-events th, .statistics-report-table-events td { text-align: right; }
+.statistics-report-table-events th:first-child, .statistics-report-table-events td:first-child { text-align: left; }
+.accordion_panel { border-color: var(--border); background: var(--surface); }
+.accordion_marker { color: var(--text-muted); }
+
+body.repolist .resource-title,
+body.repolist .resource-nav,
+body.repolist > .page-title { display: none; }
+body.repolist div.content { padding-top: 1rem; }
+body.repolist table td:first-child { font-family: var(--font-mono); font-weight: 600; }
+
+@media (prefers-color-scheme: dark) {
+ img { opacity: 0.92; }
+ body .timelineModernCell,
+ body .timelineColumnarCell,
+ body .timelineDetailCell,
+ .timelineModernCell[style*="background-color"],
+ .timelineColumnarCell[style*="background-color"],
+ .timelineDetailCell[style*="background-color"],
+ td[style*="background-color"] {
+ background-color: var(--surface-subtle) !important;
+ color: var(--text) !important;
+ }
+ .diffadd, .diffins { color: var(--text); }
+ .diffrm, .diffdel { color: var(--text); }
+}
+
+@media (max-width: $bp-md) {
+ :root { --gutter: 0.75rem; }
+ .site-header-row { min-height: 2.8rem; gap: 0.65rem; }
+ .resource-page, .resource-separator { display: none; }
+ .mainmenu a { padding-inline: 0.65rem; }
+ .page-title { font-size: 1.2rem; }
+ table:not(.sbsdiffcols) { display: block; max-width: 100%; overflow-x: auto; }
+ .desktoponly { display: none !important; }
+}
+
+@media (max-width: $bp-sm) {
+ .site-brand { font-size: 1.1rem; }
+ .resource-title { font-size: 0.9rem; }
+ .account-nav { font-size: 0.82rem; }
+ .page-title { margin-top: 0.7rem; }
+ pre { padding: 0.6rem 0.7rem; }
+}
+
+@media print {
+ .resource-nav, .account-nav, .submenu, .sectionmenu, .site-footer { display: none !important; }
+ body, pre, code { background: #fff !important; color: #000 !important; }
+ a { color: #000; text-decoration: underline; }
+ div.content, .container { max-width: none; padding: 0; }
+}
diff --git a/tools/copland/tests/fixtures/org/all.html b/tools/copland/tests/fixtures/org/all.html
new file mode 100644
index 0000000..33fc537
--- /dev/null
+++ b/tools/copland/tests/fixtures/org/all.html
@@ -0,0 +1,50 @@
+<div class="org-document">
+<h1 class="org-title">Über &amp; &lt;Fossil&gt;</h1>
+<dl class="org-metadata">
+<dt>Author</dt><dd>Ada &lt;admin@example.test&gt;</dd>
+<dt>Date</dt><dd>2026-08-21</dd>
+</dl>
+<nav class="org-toc" aria-label="Table of contents"><h2>Contents</h2>
+<ol>
+<li class="org-toc-level-1"><a href="#intro-safety">Intro &amp; safety</a></li>
+<li class="org-toc-level-2"><a href="#duplicate">Duplicate</a></li>
+<li class="org-toc-level-1"><a href="#duplicate-2">Duplicate</a></li>
+<li class="org-toc-level-1"><a href="#日本語">日本語</a></li>
+</ol>
+</nav>
+<h1 id="intro-safety">Intro &amp; safety</h1>
+<p>A paragraph with <strong>bold</strong>, <em>italic</em>, <u>underline</u>, <del>strike</del>, <code>&lt;code&gt;</code>, and <code>verbatim &amp; text</code>. Words_like_this and paths/like/this stay literal.</p>
+<ul>
+<li>first<ul>
+<li>nested <strong>item</strong></li>
+</ul>
+<ol>
+<li>nested ordered</li>
+</ol>
+</li>
+<li>second</li>
+</ul>
+<ol>
+<li>one</li>
+<li>two</li>
+</ol>
+<table class="org-table">
+<thead>
+<tr><th>Name</th><th>Value</th></tr>
+</thead>
+<tbody>
+<tr><td>α</td><td>&lt;tag&gt;</td></tr>
+<tr><td>two</td><td><strong>bold</strong></td></tr>
+</tbody>
+</table>
+<pre class="org-block org-src has-language"><code class="language-nix">{ pkgs, ... }: &lt;unsafe&gt; &amp; value</code></pre>
+<pre class="org-block org-example"><code>example &lt;script&gt;alert(1)&lt;/script&gt;</code></pre>
+<p><a href="https://example.test/a?x=1&amp;y=2">external</a> <a href="guide.org">relative</a> <a href="#intro-safety">internal</a> unsafe</p>
+<h2 id="duplicate">Duplicate</h2>
+<p>Text.</p>
+<h1 id="duplicate-2">Duplicate</h1>
+<p>More.</p>
+<h1 id="日本語">日本語</h1>
+<p>Unicode heading.</p>
+</div>
+
diff --git a/tools/copland/tests/fixtures/org/all.org b/tools/copland/tests/fixtures/org/all.org
new file mode 100644
index 0000000..f5c644a
--- /dev/null
+++ b/tools/copland/tests/fixtures/org/all.org
@@ -0,0 +1,52 @@
+#+TITLE: Über & <Fossil>
+#+AUTHOR: Ada <admin@example.test>
+#+DATE: 2026-08-21
+#+OPTIONS: toc:2 num:nil
+#+STARTUP: overview
+
+:PROPERTIES:
+:ID: hidden
+:END:
+
+* Intro & safety
+
+A paragraph with *bold*, /italic/, _underline_, +strike+, ~<code>~, and
+=verbatim & text=. Words_like_this and paths/like/this stay literal.
+
+- first
+ - nested *item*
+ 1. nested ordered
+- second
+
+1. one
+2. two
+
+| Name | Value |
+|------+-------|
+| α | <tag> |
+| two | *bold* |
+
+#+begin_src nix
+{ pkgs, ... }: <unsafe> & value
+#+end_src
+
+#+begin_example
+example <script>alert(1)</script>
+#+end_example
+
+[[https://example.test/a?x=1&y=2][external]]
+[[file:guide.org][relative]]
+[[*Intro & safety][internal]]
+[[javascript:alert(1)][unsafe]]
+
+** Duplicate
+
+Text.
+
+* Duplicate
+
+More.
+
+* 日本語
+
+Unicode heading.
diff --git a/tools/copland/tests/fixtures/org/all.txt b/tools/copland/tests/fixtures/org/all.txt
new file mode 100644
index 0000000..266ab90
--- /dev/null
+++ b/tools/copland/tests/fixtures/org/all.txt
@@ -0,0 +1,66 @@
+Über & <Fossil>
+
+
+
+AuthorAda <admin@example.test>
+Date2026-08-21
+
+
+
+Contents
+
+Intro & safety
+Duplicate
+Duplicate
+日本語
+
+
+
+Intro & safety
+
+A paragraph with bold, italic, underline, strike, <code>, and verbatim & text. Words_like_this and paths/like/this stay literal.
+
+first
+nested item
+
+
+nested ordered
+
+
+second
+
+
+one
+two
+
+
+
+
+
+NameValue
+
+
+
+α<tag>
+twobold
+
+
+
+{ pkgs, ... }: <unsafe> & value
+
+example <script>alert(1)</script>
+
+external relative internal unsafe
+
+Duplicate
+
+Text.
+
+Duplicate
+
+More.
+
+日本語
+
+Unicode heading.
+
diff --git a/tools/copland/tests/fixtures/org/security.html b/tools/copland/tests/fixtures/org/security.html
new file mode 100644
index 0000000..38707e0
--- /dev/null
+++ b/tools/copland/tests/fixtures/org/security.html
@@ -0,0 +1,9 @@
+<div class="org-document">
+<h1 class="org-title">&lt;img src=x onerror=alert(1)&gt;</h1>
+<p>Raw &lt;script&gt;alert(1)&lt;/script&gt; and &quot;quotes&quot;.</p>
+<p><a href="https://example.test/&quot; onmouseover=&quot;alert(1)">attribute</a> file scheme confusion</p>
+<pre class="org-block org-unknown-block"><code>&lt;img src=x onerror=alert(1)&gt;</code></pre>
+<pre class="org-block org-src has-language"><code class="language-sh">echo &quot;unterminated and escaped &lt;tag&gt;&quot;
+</code></pre>
+</div>
+
diff --git a/tools/copland/tests/fixtures/org/security.org b/tools/copland/tests/fixtures/org/security.org
new file mode 100644
index 0000000..3e7280a
--- /dev/null
+++ b/tools/copland/tests/fixtures/org/security.org
@@ -0,0 +1,13 @@
+#+TITLE: <img src=x onerror=alert(1)>
+
+Raw <script>alert(1)</script> and "quotes".
+
+[[https://example.test/" onmouseover="alert(1)][attribute]]
+[[file:javascript:alert(1)][file scheme confusion]]
+
+#+begin_export html
+<img src=x onerror=alert(1)>
+#+end_export
+
+#+begin_src sh
+echo "unterminated and escaped <tag>"
diff --git a/tools/copland/tests/org-renderer.sh b/tools/copland/tests/org-renderer.sh
new file mode 100755
index 0000000..1822d52
--- /dev/null
+++ b/tools/copland/tests/org-renderer.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+source "$root_dir/scripts/fossil-env.sh"
+fixture_dir="$root_dir/tests/fixtures/org"
+output_dir=$(mktemp -d -t copland-org-renderer.XXXXXX)
+
+for fixture in all security; do
+ "$FOSSIL" test-org-render "$fixture_dir/$fixture.org" >"$output_dir/$fixture.html"
+ diff -u "$fixture_dir/$fixture.html" "$output_dir/$fixture.html"
+done
+
+"$FOSSIL" test-org-render --text "$fixture_dir/all.org" >"$output_dir/all.txt"
+diff -u "$fixture_dir/all.txt" "$output_dir/all.txt"
+
+sed 's/$/\r/' "$fixture_dir/all.org" >"$output_dir/all-crlf.org"
+"$FOSSIL" test-org-render "$output_dir/all-crlf.org" >"$output_dir/all-crlf.html"
+diff -u "$fixture_dir/all.html" "$output_dir/all-crlf.html"
+
+if rg -n '<script|<img[^>]*onerror|href="javascript:' "$output_dir"/*.html; then
+ printf 'Unsafe active HTML escaped into Org renderer output.\n' >&2
+ exit 1
+fi
+if ! rg -q '&lt;script&gt;|&lt;img' "$output_dir/security.html"; then
+ printf 'Escaped security fixture content is missing.\n' >&2
+ exit 1
+fi
+
+printf 'Org renderer golden and security tests passed.\n'
diff --git a/tools/copland/tests/serve-fixture.sh b/tools/copland/tests/serve-fixture.sh
new file mode 100755
index 0000000..7f2bc8d
--- /dev/null
+++ b/tools/copland/tests/serve-fixture.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+source "$root_dir/scripts/fossil-env.sh"
+fixture_dir=$(mktemp -d -t copland-visual.XXXXXX)
+port=${COPLAND_TEST_PORT:-18082}
+
+require_org_fossil
+"$root_dir/scripts/create-demo.sh" "$fixture_dir"
+verify_org_mime "$fixture_dir/repositories/alpha.fossil"
+
+FOSSIL_REPOLIST_TITLE=Copland exec "$FOSSIL" server \
+ --localhost \
+ --localauth \
+ --port "$port" \
+ --repolist \
+ "$fixture_dir/repositories"
diff --git a/tools/copland/tests/smoke.sh b/tools/copland/tests/smoke.sh
new file mode 100755
index 0000000..bd54bab
--- /dev/null
+++ b/tools/copland/tests/smoke.sh
@@ -0,0 +1,188 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+root_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+source "$root_dir/scripts/fossil-env.sh"
+test_dir=$(mktemp -d -t copland-smoke.XXXXXX)
+port=${COPLAND_TEST_PORT:-18081}
+server_pid=""
+
+finish() {
+ if [[ -n "$server_pid" ]]; then
+ kill "$server_pid" 2>/dev/null || true
+ wait "$server_pid" 2>/dev/null || true
+ fi
+}
+trap finish EXIT INT TERM
+
+require_org_fossil
+"$root_dir/scripts/create-demo.sh" "$test_dir"
+verify_org_mime "$test_dir/repositories/alpha.fossil"
+"$FOSSIL" sql -R "$test_dir/repositories/alpha.fossil" \
+ "REPLACE INTO config(name,value,mtime) VALUES('doc-glob','*.org',unixepoch());"
+"$FOSSIL" fts-config enable document -R "$test_dir/repositories/alpha.fossil" >/dev/null
+"$FOSSIL" fts-config enable wiki -R "$test_dir/repositories/alpha.fossil" >/dev/null
+"$FOSSIL" fts-config index on -R "$test_dir/repositories/alpha.fossil" >/dev/null
+"$FOSSIL" fts-config reindex -R "$test_dir/repositories/alpha.fossil" >/dev/null
+
+FOSSIL_REPOLIST_TITLE=Copland "$FOSSIL" server \
+ --localhost \
+ --localauth \
+ --port "$port" \
+ --repolist \
+ "$test_dir/repositories" >"$test_dir/server.log" 2>&1 &
+server_pid=$!
+
+base_url="http://127.0.0.1:$port"
+for attempt in $(seq 1 50); do
+ if curl --fail --location --silent "$base_url/" >/dev/null; then
+ break
+ fi
+ if [[ "$attempt" -eq 50 ]]; then
+ printf 'Fossil server did not start.\n' >&2
+ sed -n '1,120p' "$test_dir/server.log" >&2
+ exit 1
+ fi
+ sleep 0.1
+done
+
+paths=(
+ "/"
+ "/alpha/home"
+ "/alpha/timeline"
+ "/alpha/tree?ci=tip"
+ "/alpha/dir?ci=tip"
+ "/alpha/brlist"
+ "/alpha/taglist"
+ "/alpha/wiki"
+ "/alpha/wiki?name=Org%20Wiki"
+ "/alpha/wikiedit?name=New%20Org%20Page"
+ "/alpha/whistory?name=Org%20Wiki"
+ "/alpha/reportlist"
+ "/alpha/forum"
+ "/alpha/login"
+ "/alpha/sitemap"
+ "/alpha/doc/tip/docs/org-support.org"
+ "/alpha/file?name=docs/org-support.org&ci=tip"
+ "/alpha/file?name=docs/org-support.org&ci=tip&txt=1"
+ "/alpha/search?s=Native%20Org%20preview&y=d"
+ "/alpha/search?s=deployment%20guide&y=w"
+)
+
+for path in "${paths[@]}"; do
+ body=$(curl --fail --location --silent --show-error "$base_url$path")
+ if [[ "$body" != *"Copland"* ]]; then
+ printf 'Copland marker missing from %s\n' "$path" >&2
+ exit 1
+ fi
+ if [[ "$body" == *"thmainError"* ]]; then
+ printf 'TH1 template error on %s\n' "$path" >&2
+ exit 1
+ fi
+ if [[ "$path" == "/" && ("$body" != *"Alpha"* || "$body" != *"Beta"*) ]]; then
+ printf 'Demo repositories missing from repository list\n' >&2
+ exit 1
+ fi
+ if [[ "$path" == "/alpha/home" ]]; then
+ mainmenu=${body#*'<nav class="container mainmenu"'}
+ mainmenu=${mainmenu%%'</nav>'*}
+ for hidden_route in chat taglist forum reportlist sitemap ticket tkt; do
+ if [[ "$mainmenu" == *"/$hidden_route"* ]]; then
+ printf 'Hidden route /%s is still present in the main menu\n' "$hidden_route" >&2
+ exit 1
+ fi
+ done
+ fi
+ if [[ "$path" == "/alpha/wiki" ]]; then
+ if [[ "$body" != *"Available Wiki Pages"* || "$body" != *"Org Wiki"* ]]; then
+ printf 'Wiki landing page did not list available pages\n' >&2
+ exit 1
+ fi
+ fi
+ if [[ "$path" == "/alpha/tree?ci=tip" ]]; then
+ if [[ "$body" != *'class="org-document"'* || "$body" != *"A small repository"* ]]; then
+ printf 'README.org was not rendered below the repository tree\n' >&2
+ exit 1
+ fi
+ fi
+ if [[ "$path" == "/alpha/doc/tip/docs/org-support.org" ]]; then
+ if [[ "$body" != *'<title>Alpha: Native Org preview</title>'*
+ || "$body" != *'class="org-toc"'*
+ || "$body" != *'href="../README.org"'*
+ || "$body" != *'id="overview"'* ]]; then
+ printf 'Native Org /doc rendering is incomplete\n' >&2
+ exit 1
+ fi
+ fi
+ if [[ "$path" == "/alpha/wiki?name=Org%20Wiki" ]]; then
+ if [[ "$body" != *'<title>Alpha: Org Wiki</title>'*
+ || "$body" != *'class="org-document"'*
+ || "$body" == *'class="org-title"'*
+ || "$body" != *'href="wiki?name=Deployment+Guide"'*
+ || "$body" != *'href="doc/tip/docs/org-support.org"'* ]]; then
+ printf 'Native Org wiki rendering is incomplete\n' >&2
+ exit 1
+ fi
+ fi
+ if [[ "$path" == "/alpha/wikiedit?name=New%20Org%20Page" ]]; then
+ if [[ "$body" != *'value="text/x-org" selected'* ]]; then
+ printf 'Org is not the default format in the wiki editor\n' >&2
+ exit 1
+ fi
+ fi
+ if [[ "$path" == "/alpha/file?name=docs/org-support.org&ci=tip" ]]; then
+ if [[ "$body" != *'class="org-document"'* || "$body" != *'>Text</a>'* ]]; then
+ printf 'Native Org /file rendering or Text toggle is missing\n' >&2
+ exit 1
+ fi
+ fi
+ if [[ "$path" == "/alpha/file?name=docs/org-support.org&ci=tip&txt=1" ]]; then
+ if [[ "$body" == *'class="org-document"'* || "$body" != *'#+title: Native Org preview'* ]]; then
+ printf 'Org /file Text view did not show source\n' >&2
+ exit 1
+ fi
+ fi
+ if [[ "$path" == "/alpha/search?s=Native%20Org%20preview&y=d" ]]; then
+ if [[ "$body" != *"Native Org preview"* ]]; then
+ printf 'Org document was not found by full-text search\n' >&2
+ exit 1
+ fi
+ fi
+ if [[ "$path" == "/alpha/search?s=deployment%20guide&y=w" ]]; then
+ if [[ "$body" != *"Org Wiki"* ]]; then
+ printf 'Org wiki page was not found by full-text search\n' >&2
+ exit 1
+ fi
+ fi
+ printf 'ok %s\n' "$path"
+done
+
+default_skin_body=$(curl --fail --location --silent --show-error \
+ "$base_url/alpha/home?skin=default")
+if [[ "$default_skin_body" == *'class="site-brand"'* ]]; then
+ printf 'The original Fossil default skin is not selectable\n' >&2
+ exit 1
+fi
+copland_skin_body=$(curl --fail --location --silent --show-error \
+ "$base_url/alpha/home?skin=copland")
+if [[ "$copland_skin_body" != *'class="site-brand"'* ]]; then
+ printf 'The built-in Copland skin is not selectable\n' >&2
+ exit 1
+fi
+
+json_wiki=$(curl --fail --silent --show-error \
+ "$base_url/alpha/json/wiki/get?name=Org%20Wiki&format=html")
+if [[ "$json_wiki" != *'org-document'* || "$json_wiki" != *'contentFormat":"html'* ]]; then
+ printf 'JSON wiki output did not render Org\n' >&2
+ exit 1
+fi
+json_preview=$(curl --fail --silent --show-error \
+ --header 'Content-Type: application/json' \
+ --data '{"payload":{"body":"* JSON preview","mimetype":"text/x-org"}}' \
+ "$base_url/alpha/json/wiki/preview")
+if [[ "$json_preview" != *'org-document'* || "$json_preview" != *'JSON preview'* ]]; then
+ printf 'JSON wiki preview did not render Org\n' >&2
+ exit 1
+fi
+
+printf 'Smoke tests passed. Temporary fixtures: %s\n' "$test_dir"
diff --git a/tools/copland/tests/visual.spec.mjs b/tools/copland/tests/visual.spec.mjs
new file mode 100644
index 0000000..73559e1
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs
@@ -0,0 +1,94 @@
+import AxeBuilder from "@axe-core/playwright";
+import { expect, test } from "@playwright/test";
+
+const pages = [
+ ["repository-list", "/"],
+ ["home", "/alpha/home"],
+ ["timeline", "/alpha/timeline"],
+ ["tree", "/alpha/tree?ci=tip"],
+ ["org-document", "/alpha/doc/tip/docs/org-support.org"],
+ ["org-wiki", "/alpha/wiki?name=Org%20Wiki"],
+ ["login", "/alpha/login"],
+];
+
+for (const colorScheme of ["light", "dark"]) {
+ for (const [name, path] of pages) {
+ test(`${name} is legible in ${colorScheme} mode`, async ({ page }) => {
+ await page.emulateMedia({ colorScheme });
+ await page.goto(path);
+ await expect(page.locator("body")).toContainText("Copland");
+
+ const viewportOverflow = await page.evaluate(
+ () => document.documentElement.scrollWidth > document.documentElement.clientWidth,
+ );
+ expect(viewportOverflow).toBe(false);
+
+ let accessibilityScan = new AxeBuilder({ page })
+ .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
+ .disableRules(["html-has-lang"])
+ .exclude(".submenu");
+ if (name === "login") {
+ accessibilityScan = accessibilityScan.exclude("form");
+ }
+ const accessibility = await accessibilityScan.analyze();
+ expect(accessibility.violations).toEqual([]);
+
+ const screenshotMasks = [page.locator(".site-footer span:last-child")];
+ if (name === "login") {
+ screenshotMasks.push(page.locator("form"));
+ }
+ await expect(page).toHaveScreenshot(`${name}-${colorScheme}.png`, {
+ fullPage: true,
+ animations: "disabled",
+ mask: screenshotMasks,
+ maxDiffPixelRatio: 0.001,
+ });
+ });
+ }
+}
+
+test.describe("mobile", () => {
+ test.use({ viewport: { width: 320, height: 720 } });
+
+ test("navigation remains usable", async ({ page }) => {
+ await page.goto("/alpha/timeline");
+ await expect(page.locator(".site-brand")).toHaveText("Copland");
+ await expect(page.locator(".mainmenu")).toBeVisible();
+ const viewportOverflow = await page.evaluate(
+ () => document.documentElement.scrollWidth > document.documentElement.clientWidth,
+ );
+ expect(viewportOverflow).toBe(false);
+ await expect(page).toHaveScreenshot("timeline-mobile.png", {
+ fullPage: true,
+ mask: [page.locator(".site-footer span:last-child")],
+ });
+ });
+
+ test("Org documentation remains readable", async ({ page }) => {
+ await page.goto("/alpha/doc/tip/docs/org-support.org");
+ await expect(page.locator(".org-document")).toBeVisible();
+ await expect(page.locator(".org-toc")).toBeVisible();
+ const viewportOverflow = await page.evaluate(
+ () => document.documentElement.scrollWidth > document.documentElement.clientWidth,
+ );
+ expect(viewportOverflow).toBe(false);
+ await expect(page).toHaveScreenshot("org-document-mobile.png", {
+ fullPage: true,
+ mask: [page.locator(".site-footer span:last-child")],
+ });
+ });
+
+ test("Org wiki remains readable", async ({ page }) => {
+ await page.goto("/alpha/wiki?name=Org%20Wiki");
+ await expect(page.locator(".org-document")).toBeVisible();
+ await expect(page.locator(".org-toc")).toBeVisible();
+ const viewportOverflow = await page.evaluate(
+ () => document.documentElement.scrollWidth > document.documentElement.clientWidth,
+ );
+ expect(viewportOverflow).toBe(false);
+ await expect(page).toHaveScreenshot("org-wiki-mobile.png", {
+ fullPage: true,
+ mask: [page.locator(".site-footer span:last-child")],
+ });
+ });
+});
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/home-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/home-dark-linux.png
new file mode 100644
index 0000000..52f9478
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/home-dark-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/home-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/home-light-linux.png
new file mode 100644
index 0000000..4660bf9
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/home-light-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/login-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/login-dark-linux.png
new file mode 100644
index 0000000..738588b
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/login-dark-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/login-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/login-light-linux.png
new file mode 100644
index 0000000..03ba2db
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/login-light-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-document-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-dark-linux.png
new file mode 100644
index 0000000..15d37f9
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-dark-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-document-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-light-linux.png
new file mode 100644
index 0000000..3c0785f
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-light-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-document-mobile-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-mobile-linux.png
new file mode 100644
index 0000000..d1f5cba
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-document-mobile-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-dark-linux.png
new file mode 100644
index 0000000..464197b
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-dark-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-light-linux.png
new file mode 100644
index 0000000..ff3bdc1
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-light-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-mobile-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-mobile-linux.png
new file mode 100644
index 0000000..50381e0
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/org-wiki-mobile-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-dark-linux.png
new file mode 100644
index 0000000..0cf1b4f
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-dark-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-light-linux.png
new file mode 100644
index 0000000..af49135
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/repository-list-light-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/timeline-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-dark-linux.png
new file mode 100644
index 0000000..469d7b0
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-dark-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/timeline-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-light-linux.png
new file mode 100644
index 0000000..e1dbe69
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-light-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/timeline-mobile-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-mobile-linux.png
new file mode 100644
index 0000000..02ed528
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/timeline-mobile-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/tree-dark-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/tree-dark-linux.png
new file mode 100644
index 0000000..164d3eb
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/tree-dark-linux.png
Binary files differ
diff --git a/tools/copland/tests/visual.spec.mjs-snapshots/tree-light-linux.png b/tools/copland/tests/visual.spec.mjs-snapshots/tree-light-linux.png
new file mode 100644
index 0000000..ed51114
--- /dev/null
+++ b/tools/copland/tests/visual.spec.mjs-snapshots/tree-light-linux.png
Binary files differ