This commit is contained in:
2026-10-11 11:06:58 -07:00
parent ad9a41036f
commit f28898089e
25 changed files with 3320 additions and 453 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@ body.is-404 .error-container {
body.is-404 .error-code {
margin: 0 0 16px;
color: var(--text-heading);
color: var(--text);
font-size: clamp(74px, 14vw, 112px);
font-weight: 700;
line-height: 1;
@@ -42,7 +42,7 @@ body.is-404 .error-code {
body.is-404 .error-title {
margin: 0 0 24px;
color: var(--text-heading);
color: var(--text);
font-size: 18px;
font-weight: 800;
letter-spacing: 0.12em;

View File

@@ -29,145 +29,6 @@
color: inherit;
}
#search-modal {
position: fixed;
inset: 0;
z-index: 100;
display: none;
}
#search-modal.open {
display: block;
}
.search-panel {
position: relative;
width: min(640px, calc(100vw - 24px));
margin: 52px auto 0;
border-radius: 20px;
overflow: hidden;
background: var(--search-panel-bg) !important;
border: 1px solid var(--border);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.search-input-row {
display: flex;
align-items: center;
gap: 12px;
padding: 18px 20px;
}
.search-input-row .ico {
width: 18px;
height: 18px;
color: var(--muted);
}
#search-input {
flex: 1;
border: none;
outline: none;
background: transparent;
color: var(--text-heading);
font: inherit;
font-size: 18px;
}
#search-results {
max-height: 40vh;
overflow-y: auto;
padding: 0 6px 8px;
}
.search-label {
padding: 8px 14px;
font-size: 12px;
color: var(--muted);
}
.sr {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 12px;
border-radius: 12px;
}
.sr>.ico {
margin-top: 3px;
color: var(--muted);
}
.sr.on {
background: var(--hover-bg);
}
.sr-body {
min-width: 0;
}
.sr .t {
font-size: 14px;
font-weight: 600;
line-height: 1.35;
color: var(--text-heading);
}
.sr .s {
display: -webkit-box;
margin-top: 2px;
font-size: 12.5px;
line-height: 1.4;
color: var(--muted);
overflow: hidden;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.sr .p {
margin-top: 2px;
font-size: 12px;
line-height: 1.35;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sr .sep {
margin: 0 6px;
opacity: 0.7;
}
.sr .hit {
font-weight: 700;
color: var(--text-heading);
}
.search-footer {
display: flex;
gap: 18px;
padding: 12px 20px;
font-size: 12px;
color: var(--muted);
border-top: 1px solid var(--border-subtle);
}
.search-footer .close-hint {
margin-left: auto;
}
.search-footer kbd {
margin-right: 4px;
padding: 1px 5px;
border-radius: 4px;
font: 11px var(--font);
}
.code-wrap {
position: relative;
}
@@ -254,7 +115,7 @@
}
.cg-copy-btn:hover {
color: var(--text-heading);
color: var(--text);
}
.code-group-panel {
@@ -456,13 +317,11 @@ main.prose blockquote p {
#search-trigger kbd {
display: none;
}
.search-panel {
margin-top: 50px;
border-radius: 30px;
}
.search-footer {
display: none;
}
}
main.prose h1,
main.prose h2,
main.prose h3,
main.prose h4 {
cursor: pointer;
}

262
assets/styling/features.css Normal file
View File

@@ -0,0 +1,262 @@
.bm-btn {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
padding: 0;
border: none;
border-radius: 50%;
background: none;
color: var(--muted);
cursor: pointer;
transition: color 0.15s ease, background 0.15s ease;
}
.bm-btn .ico {
width: 18px;
height: 18px;
}
.bm-btn:hover {
background: var(--hover-bg);
color: var(--text);
}
.bm-btn.on {
color: var(--text);
}
.bm-btn.on .ico {
fill: currentColor;
}
.personal-card {
margin: 18px 0 8px;
padding: 16px 18px;
border-radius: 22px;
background: var(--callout-note-bg);
}
.personal-card label {
display: block;
margin-bottom: 10px;
font-size: 14px;
font-weight: 600;
color: var(--text);
}
.pc-row {
display: flex;
gap: 8px;
}
.pc-row input {
flex: 1;
min-width: 0;
height: 36px;
padding: 0 14px;
border: 1px solid var(--border);
border-radius: 100px;
background: var(--search-bg);
color: var(--text);
font: inherit;
font-size: 13.5px;
outline: none;
}
.pc-row input.bad {
border-color: #ef4444;
}
.pc-btn {
height: 36px;
padding: 0 18px;
border: none;
border-radius: 100px;
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
font: inherit;
font-size: 13.5px;
font-weight: 600;
cursor: pointer;
}
main.prose .pc-hint {
margin: 8px 0 0;
font-size: 12.5px;
line-height: 1.5;
color: var(--muted);
}
.rec-badge {
display: inline-flex;
align-items: center;
margin-left: 8px;
padding: 2px 10px;
border-radius: 100px;
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
font-size: 11.5px;
font-weight: 700;
letter-spacing: 0.02em;
vertical-align: middle;
white-space: nowrap;
}
tbody tr.rec-row td {
background: var(--hover-bg);
}
tbody tr.rec-row td:first-child {
padding-left: 12px;
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
}
tbody tr.rec-row td:last-child {
padding-right: 12px;
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
}
#footer-note {
margin: 0 0 16px;
font-size: 13.5px;
color: var(--muted);
}
.changelog,
.contributors {
margin: 20px 0;
}
.contributors {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
main.prose .muted-line {
margin: 0;
color: var(--muted);
}
.cl-item {
padding: 16px 0;
border-bottom: 1px solid var(--border-subtle);
}
.cl-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
font-size: 12.5px;
color: var(--muted);
}
.cl-tag {
padding: 2px 10px;
border-radius: 100px;
background: var(--hover-bg);
color: var(--text);
font-weight: 600;
}
.cl-title {
margin-top: 8px;
font-size: 16px;
font-weight: 700;
color: var(--text);
}
main.prose .cl-notes {
margin: 6px 0 0;
font-size: 14px;
}
.cl-link {
display: inline-block;
margin-top: 8px;
font-size: 13px;
}
.ct-chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 0px 14px 0px 5px;
border-radius: 100px;
background: var(--hover-bg);
color: var(--text);
;
font-size: 13.5px;
margin-right: 10px;
height: 40px;
align-items: center;
}
.ct-chip em {
font-style: normal;
font-size: 12px;
color: var(--muted);
}
.ct-avatar {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
font-size: 12px;
font-weight: 700;
}
.ct-avatar-img {
width: 26px;
height: 26px;
}
.confetti-canvas {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 3000;
}
body:not(.is-home) main.prose {
zoom: var(--text-scale, 1);
}
body.no-deco .cobweb,
body.no-deco .spider-container,
body.no-deco .snowfall,
body.no-deco .leaf-fall,
body.no-deco .light-string {
display: none !important;
}
body.no-video .video-bg,
body.no-video .body-blurred-video,
body.no-video .header-video {
display: none !important;
}
body.no-motion *,
body.no-motion *::before,
body.no-motion *::after {
animation: none !important;
transition: none !important;
scroll-behavior: auto !important;
}
@media (max-width: 900px) {
.rec-badge {
margin-left: 0 !important;
margin-top: 10px !important;
}
}

View File

@@ -6,6 +6,7 @@
z-index: 41;
backdrop-filter: blur(10px) saturate(1.2);
-webkit-backdrop-filter: blur(10px) saturate(1.2);
background: var(--bg-card);
width: 50%;
border-radius: 30px;
}
@@ -99,11 +100,11 @@
}
#navbar .tabs a:hover {
color: var(--text-heading);
color: var(--text);
}
#navbar .tabs a.active {
color: var(--text-heading);
color: var(--text);
font-weight: 600;
}
@@ -155,19 +156,19 @@
}
#sidebar li a.active {
color: var(--text-heading);
color: var(--text);
}
#sidebar li a:hover {
background: var(--hover-bg);
color: var(--text-heading);
color: var(--text);
}
#content-col {
flex: 1;
min-width: 0;
display: flex;
padding: 16px 20px 20px;
padding: 16px max(20px, env(safe-area-inset-right)) 20px max(20px, env(safe-area-inset-left));
gap: 92px;
max-width: 100%;
box-sizing: border-box;
@@ -178,7 +179,7 @@
flex: 1;
min-width: 0;
max-width: 720px;
margin-top: 130px;
margin-top: 120px;
border-radius: 12px;
width: 100%;
box-sizing: border-box;
@@ -213,7 +214,7 @@ footer#site-footer {
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--text-heading);
color: var(--text);
transition: opacity 0.15s ease;
}
@@ -248,7 +249,7 @@ footer#site-footer {
}
.footer-socials a:hover {
color: var(--text-heading);
color: var(--text);
}
.footer-icon {
@@ -361,6 +362,7 @@ footer#site-footer {
border-radius: 10px;
background: none;
color: var(--text);
;
font: inherit;
font-size: 13.5px;
cursor: pointer;
@@ -397,11 +399,11 @@ footer#site-footer {
#theme-menu button.on {
font-weight: 600;
color: var(--text-heading);
color: var(--text);
}
#theme-menu button.on .ico {
color: var(--text-heading);
color: var(--text);
}
#theme-menu button.on .check {
@@ -479,6 +481,7 @@ footer#site-footer {
border-radius: 8px;
background: none;
color: var(--text);
;
font: inherit;
font-size: 13.5px;
cursor: pointer;
@@ -505,15 +508,16 @@ footer#site-footer {
overflow: hidden;
text-overflow: ellipsis;
color: var(--text);
;
}
#theme-seg-menu button.on span {
color: var(--text-heading);
color: var(--text);
font-weight: 600;
}
#theme-seg-menu button.on .ico {
color: var(--text-heading);
color: var(--text);
}
#theme-seg-menu button .check {
@@ -526,7 +530,7 @@ footer#site-footer {
#theme-seg-menu button.on .check {
visibility: visible;
color: var(--text-heading);
color: var(--text);
}
#theme-seg-menu button:hover {
@@ -535,7 +539,7 @@ footer#site-footer {
#theme-seg-menu button.on {
font-weight: 600;
color: var(--text-heading);
color: var(--text);
}
#theme-seg-menu .check {
@@ -564,7 +568,7 @@ footer#site-footer {
#theme-seg>button.on {
background: var(--hover-bg);
color: var(--text-heading);
color: var(--text);
}
#mobile-bar {
@@ -645,7 +649,7 @@ footer#site-footer {
}
.mobile-crumbs .crumb-heading {
color: var(--text-heading);
color: var(--text);
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
@@ -667,7 +671,7 @@ footer#site-footer {
border: none;
background: var(--hover-bg);
border-radius: 100px;
color: var(--text-heading);
color: var(--text);
font: inherit;
font-size: 15px;
cursor: pointer;
@@ -728,11 +732,11 @@ footer#site-footer {
.mt-menu .mt-item:hover {
background: var(--hover-bg);
color: var(--text-heading);
color: var(--text);
}
.mt-menu .mt-item.on {
color: var(--text-heading);
color: var(--text);
font-weight: 600;
}
@@ -754,12 +758,6 @@ footer#site-footer {
display: none;
}
body.is-home #sidebar,
body.is-home #toc,
body.is-home #mobile-bar {
display: none !important;
}
body.is-home #layout {
display: block;
width: 100%;
@@ -805,14 +803,23 @@ body.is-home main.prose {
align-items: center;
}
body.is-home #toc,
body.is-home #sidebar,
body.is-home #mobile-bar {
display: none !important;
}
@media (max-width: 900px) {
:root {
--header-height: 60px;
}
#header-container {
width: 91%;
border-radius: 25px;
width: 90%;
border-radius: 30px;
backdrop-filter: blur(15px) saturate(1.2);
-webkit-backdrop-filter: blur(15px) saturate(1.2);
top: max(6px, env(safe-area-inset-top));
}
#navbar .tabs,
@@ -865,7 +872,7 @@ body.is-home main.prose {
}
#sidebar #theme-seg>button.on {
color: var(--text-heading);
color: var(--text);
font-weight: 900;
}
@@ -939,6 +946,7 @@ body.is-home main.prose {
height: 60px;
margin: 0 -16px 12px;
padding: 0 16px;
margin-top: max(6px, env(safe-area-inset-top));
}
#sidebar .side-head .brand {
@@ -959,3 +967,43 @@ body.is-home main.prose {
margin-right: 4px;
}
}
@media (orientation: landscape) and (max-height: 550px) {
:root {
--header-height: 52px;
}
#header-container {
top: max(6px, env(safe-area-inset-top));
width: min(600px, 88vw);
border-radius: 20px;
}
#navbar .nav-top {
height: 46px;
}
#navbar .tabs {
height: 38px;
}
#navbar .tabs a {
padding: 4px 10px;
font-size: 13px;
}
#main-wrap {
margin-top: 100px;
}
#sidebar {
top: calc(var(--header-height) + 16px);
max-height: calc(100vh - var(--header-height) - 16px);
margin-top: 10px;
}
#mobile-bar {
padding: 6px 14px;
font-size: 13px;
}
}

858
assets/styling/overlay.css Normal file
View File

@@ -0,0 +1,858 @@
#search-modal {
position: fixed;
inset: 0;
z-index: 100;
display: none;
}
#search-modal.open {
display: block;
}
.search-panel {
position: relative;
width: min(640px, calc(100vw - 24px));
margin: 52px auto 0;
border-radius: 20px;
overflow: hidden;
background: var(--search-panel-bg) !important;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.search-input-row {
display: flex;
align-items: center;
gap: 12px;
padding: 18px 20px;
}
.search-input-row .ico {
width: 18px;
height: 18px;
color: var(--muted);
}
#search-input {
flex: 1;
border: none;
outline: none;
background: transparent;
color: var(--text);
font: inherit;
font-size: 18px;
}
#search-results {
max-height: 40vh;
overflow-y: auto;
padding: 0 6px 8px;
}
.search-label {
padding: 8px 14px;
font-size: 12px;
color: var(--muted);
}
.sr {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 12px;
border-radius: 12px;
}
.sr>.ico {
margin-top: 3px;
color: var(--muted);
}
.sr.on {
background: var(--hover-bg);
}
.sr-body {
min-width: 0;
}
.sr .t {
font-size: 14px;
font-weight: 600;
line-height: 1.35;
color: var(--text);
}
.sr .s {
display: -webkit-box;
margin-top: 2px;
font-size: 12.5px;
line-height: 1.4;
color: var(--muted);
overflow: hidden;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.sr .p {
margin-top: 2px;
font-size: 12px;
line-height: 1.35;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sr .sep {
margin: 0 6px;
opacity: 0.7;
}
.sr .hit {
font-weight: 700;
color: var(--text);
}
.search-footer {
display: flex;
gap: 18px;
padding: 12px 20px;
font-size: 12px;
color: var(--muted);
border-top: 1px solid var(--border-subtle);
}
.search-footer .close-hint {
margin-left: auto;
}
.search-footer kbd {
margin-right: 4px;
padding: 1px 5px;
border-radius: 4px;
font: 11px var(--font);
}
@media (max-width: 900px) {
.search-panel {
margin-top: 50px;
border-radius: 30px;
}
.search-footer {
display: none;
}
}
.sheet-overlay {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.35s cubic-bezier(0.16, 1, 0.3, 1);
}
.sheet-overlay.active {
opacity: 1;
pointer-events: auto;
}
.sheet-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.sheet-panel {
position: relative;
width: min(500px, calc(100vw - 32px));
background: var(--bg-modal);
border-radius: 28px;
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
padding: 32px 28px 28px;
box-sizing: border-box;
transform: scale(0.92) translateY(16px);
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
display: flex;
flex-direction: column;
}
.sheet-overlay.active .sheet-panel {
transform: scale(1) translateY(0);
}
.sheet-handle-bar {
display: none;
}
.sheet-title {
font-size: 22px;
font-weight: 700;
color: var(--text);
margin: 0 0 10px;
line-height: 1.3;
text-align: center;
}
.sheet-desc {
font-size: 14.5px;
color: var(--muted);
line-height: 1.6;
margin: 0 0 24px;
text-align: center;
}
.sheet-actions {
display: flex;
align-items: center;
gap: 12px;
justify-content: center;
}
.sheet-btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 22px;
border-radius: 100px;
background: var(--btn-primary-bg);
color: var(--btn-primary-text) !important;
font-size: 14px;
font-weight: 600;
text-decoration: none !important;
transition: transform 0.15s ease, opacity 0.15s ease;
}
.sheet-btn-primary:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.sheet-btn-never {
padding: 10px 18px;
border: none;
background: none;
color: var(--muted);
font: inherit;
font-size: 13.5px;
font-weight: 500;
cursor: pointer;
border-radius: 100px;
transition: color 0.15s ease, background 0.15s ease;
}
.sheet-btn-never:hover {
color: var(--text);
background: var(--hover-bg);
}
@media (max-width: 900px) {
.sheet-overlay {
align-items: flex-end;
}
.sheet-panel {
width: 100%;
max-width: 100%;
border-radius: 28px 28px 0 0;
border-bottom: none;
padding: 14px 22px 32px;
transform: translateY(100%);
transition: transform 0.45s cubic-bezier(0.32, 0.72, 0, 1);
touch-action: none;
max-height: 90vh;
}
.sheet-panel.dragging {
transition: none !important;
}
.sheet-overlay.active .sheet-panel {
transform: translateY(0);
}
.sheet-handle-bar {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 6px 0 16px;
cursor: grab;
}
.sheet-handle {
width: 40px;
height: 5px;
border-radius: 999px;
background: var(--muted);
opacity: 0.45;
}
.sheet-actions {
flex-direction: column;
width: 100%;
gap: 8px;
}
.sheet-btn-primary {
width: 100%;
}
.sheet-btn-never {
width: 100%;
text-align: center;
}
}
@media (orientation: landscape) and (max-height: 550px) {
.sheet-panel {
max-height: 94vh;
overflow-y: auto;
padding: 10px max(24px, env(safe-area-inset-right)) 16px max(24px, env(safe-area-inset-left));
}
.sheet-title {
font-size: 18px;
margin-bottom: 6px;
}
.sheet-desc {
font-size: 13px;
margin-bottom: 12px;
line-height: 1.45;
}
.sheet-actions {
flex-direction: row;
gap: 10px;
}
.sheet-btn-primary,
.sheet-btn-never {
width: auto;
padding: 8px 18px;
font-size: 13px;
}
}
.image-lightbox {
position: fixed;
inset: 0;
z-index: 2000;
background: transparent;
backdrop-filter: blur(0px);
-webkit-backdrop-filter: blur(0px);
opacity: 0;
pointer-events: none;
transition: background 0.42s cubic-bezier(0.2, 0.9, 0.3, 1),
backdrop-filter 0.42s cubic-bezier(0.2, 0.9, 0.3, 1),
opacity 0.25s ease;
}
.image-lightbox.active {
background: var(--bg-modal);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
opacity: 1;
pointer-events: auto;
}
.lightbox-controls {
position: fixed;
top: max(16px, env(safe-area-inset-top));
right: max(16px, env(safe-area-inset-right));
display: flex;
align-items: center;
gap: 3px;
z-index: 2003;
background: var(--bg-menu);
border-radius: 100px;
padding: 3px 6px;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
opacity: 0;
transform: translateY(-8px) scale(0.96);
transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1) 0.08s,
transform 0.3s cubic-bezier(0.16, 1, 0.3, 1) 0.08s;
}
.image-lightbox.active .lightbox-controls {
opacity: 1;
transform: translateY(0) scale(1);
}
.lightbox-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: transparent;
color: var(--muted);
font-family: var(--font);
font-size: 16px;
line-height: 1;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.lightbox-btn:hover {
background: var(--hover-bg);
color: var(--text);
}
.lightbox-btn svg {
display: block;
width: 16px;
height: 16px;
fill: currentColor;
flex-shrink: 0;
}
.lightbox-img {
position: fixed;
object-fit: contain;
border-radius: 16px;
transition: top 0.44s cubic-bezier(0.2, 0.9, 0.3, 1),
left 0.44s cubic-bezier(0.2, 0.9, 0.3, 1),
width 0.44s cubic-bezier(0.2, 0.9, 0.3, 1),
height 0.44s cubic-bezier(0.2, 0.9, 0.3, 1),
border-radius 0.44s cubic-bezier(0.2, 0.9, 0.3, 1),
transform 0.25s ease;
z-index: 2002;
transform-origin: center center;
will-change: top, left, width, height, transform;
}
main.prose img {
cursor: zoom-in;
transition: opacity 0.2s ease, transform 0.2s ease;
}
main.prose img:hover {
opacity: 0.95;
}
#settings-trigger,
.side-settings {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 0;
border: none;
border-radius: 10px;
background: none;
color: var(--muted);
cursor: pointer;
}
#settings-trigger .ico,
.side-settings .ico {
width: 18px;
height: 18px;
}
#settings-trigger:hover,
.side-settings:hover {
background: var(--hover-bg);
color: var(--text);
}
.settings-panel {
position: relative;
display: flex;
flex-direction: column;
padding: 0;
overflow: hidden;
}
.st-top,
.st-bottom {
position: absolute;
left: 0;
right: 0;
z-index: 3;
isolation: isolate;
}
.st-top::before,
.st-bottom::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
backdrop-filter: blur(18px) saturate(1.2);
-webkit-backdrop-filter: blur(18px) saturate(1.2);
}
.st-top {
top: 0;
padding: 26px 28px 30px;
}
.st-top::before {
background: linear-gradient(to bottom, var(--bg-menu) 40%, transparent);
background: linear-gradient(to bottom, color-mix(in srgb, var(--bg-menu) 94%, transparent), color-mix(in srgb, var(--bg-menu) 80%, transparent) 60%, transparent);
-webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent);
mask-image: linear-gradient(to bottom, #000 55%, transparent);
}
.st-top .sheet-title {
margin: 0 0 6px;
font-size: 20px;
}
.st-top .sheet-desc {
margin: 0;
font-size: 13.5px;
line-height: 1.5;
}
.st-bottom {
bottom: 0;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
padding: 36px 28px calc(20px + env(safe-area-inset-bottom));
}
.st-bottom::before {
background: linear-gradient(to top, var(--bg-menu) 40%, transparent);
background: linear-gradient(to top, color-mix(in srgb, var(--bg-menu) 94%, transparent), color-mix(in srgb, var(--bg-menu) 80%, transparent) 60%, transparent);
-webkit-mask-image: linear-gradient(to top, #000 55%, transparent);
mask-image: linear-gradient(to top, #000 55%, transparent);
}
.st-bottom .sheet-btn-primary {
border: none;
font-family: inherit;
cursor: pointer;
}
.st-bottom .sheet-btn-primary.done {
opacity: 0.85;
}
.st-scroll {
flex: 1;
min-height: 0;
padding: var(--st-top, 110px) 28px var(--st-bottom, 84px);
overflow-y: auto;
overscroll-behavior: contain;
touch-action: pan-y;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
.st-group {
margin-bottom: 18px;
}
.st-heading {
margin: 0 0 8px 6px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.st-card {
border-radius: 20px;
background: var(--hover-bg);
overflow: hidden;
}
.st-row,
.st-field {
display: flex;
gap: 14px;
padding: 14px 16px;
border-top: 1px solid var(--border-subtle);
cursor: default;
}
.st-card>:first-child {
border-top: none;
}
.st-row {
align-items: center;
justify-content: space-between;
}
.st-row.stack {
flex-direction: column;
align-items: stretch;
}
label.st-row {
cursor: pointer;
}
.st-field {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.st-text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.st-label {
font-size: 14px;
font-weight: 600;
line-height: 1.35;
color: var(--text);
}
.st-hint {
font-size: 12.5px;
line-height: 1.45;
color: var(--muted);
}
.st-hint.err {
color: #ef4444;
}
.st-input {
width: 100%;
height: 40px;
padding: 0 16px;
border: none;
border-radius: 100px;
background: var(--search-bg);
color: var(--text);
font: inherit;
font-size: 14px;
outline: none;
transition: border-color 0.15s ease;
}
.st-input::placeholder {
color: var(--muted);
opacity: 0.8;
}
.st-input:focus {
border-color: var(--text);
}
.st-input.bad {
border-color: #ef4444;
}
.st-swatches {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.st-swatch {
display: block;
position: relative;
flex-shrink: 0;
width: 26px;
height: 26px;
padding: 0;
border: none;
border-radius: 50%;
background: var(--sw, var(--hover-bg));
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.st-swatch:hover {
transform: scale(1.12);
}
.st-swatch.on {
box-shadow: 0 0 0 2px var(--bg-menu), 0 0 0 4px var(--text);
;
}
.st-swatch.reset {
background: linear-gradient(135deg, transparent 46%, var(--muted) 46%, var(--muted) 54%, transparent 54%), var(--search-bg);
box-shadow: inset 0 0 0 1.5px var(--muted);
}
.st-swatch.reset.on {
box-shadow: inset 0 0 0 1.5px var(--muted), 0 0 0 2px var(--bg-menu), 0 0 0 4px var(--text);
;
}
.st-swatch.custom {
background: conic-gradient(from 0deg, #ff5c5c, #f4b942, #5fd68f, #8ad1ff, #c084fc, #ff5c5c);
}
.st-swatch.custom input {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
padding: 0;
border: none;
opacity: 0;
cursor: pointer;
}
.st-seg {
display: flex;
gap: 2px;
padding: 3px;
border-radius: 100px;
background: var(--search-bg);
}
.st-seg button {
flex: 1;
height: 32px;
padding: 0 10px;
border: none;
border-radius: 100px;
background: none;
color: var(--muted);
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.st-seg button:hover {
color: var(--text);
}
.st-seg button.on {
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
}
.st-switch {
position: relative;
flex-shrink: 0;
width: 44px;
height: 26px;
}
.st-switch input {
position: absolute;
inset: 0;
z-index: 1;
width: 100%;
height: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.st-track {
position: absolute;
inset: 0;
border: 1px solid var(--border);
border-radius: 100px;
background: var(--search-bg);
transition: background 0.2s ease, border-color 0.2s ease;
}
.st-track::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--muted);
transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), background 0.2s ease;
}
.st-switch input:checked+.st-track {
border-color: transparent;
background: var(--btn-primary-bg);
}
.st-switch input:checked+.st-track::after {
transform: translateX(18px);
background: var(--btn-primary-text);
}
.st-switch input:focus-visible+.st-track {
outline: 2px solid var(--text);
outline-offset: 2px;
}
@media (min-width: 901px) {
.settings-panel {
width: min(540px, calc(100vw - 32px));
max-height: min(88vh, 780px);
}
}
@media (max-width: 900px) {
.st-top {
padding: 8px 22px 28px;
}
.st-scroll {
padding-left: 22px;
padding-right: 22px;
}
.st-bottom {
padding: 36px 22px calc(18px + env(safe-area-inset-bottom));
}
.st-bottom .sheet-btn-primary {
flex: 1;
width: auto;
}
.st-bottom .sheet-btn-never {
flex: 0 0 auto;
width: auto;
}
}
@media (orientation: landscape) and (max-height: 550px) {
.settings-panel {
max-height: 94vh;
}
.st-top {
padding: 14px 24px 22px;
}
.st-top .sheet-title {
font-size: 17px;
margin-bottom: 0;
}
.st-top .sheet-desc {
display: none;
}
}

View File

@@ -11,6 +11,10 @@ main.prose .eyebrow {
font-weight: 600;
margin: 0 0 8px;
color: var(--text);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
main.prose h1 {
@@ -18,7 +22,7 @@ main.prose h1 {
font-weight: 700;
letter-spacing: 0;
margin: 0 0 6px;
color: var(--text-heading);
color: var(--text);
}
main.prose .page-description {
@@ -48,21 +52,22 @@ main.prose h3 {
main.prose h2,
main.prose h3 {
scroll-margin-top: 130px;
color: var(--text-heading);
color: var(--text);
}
main.prose p,
main.prose li {
color: var(--text);
color: var(--muted);
line-height: 1.75;
}
main.prose strong {
color: var(--text-heading);
color: var(--text);
}
main.prose a {
color: var(--text);
;
font-weight: 600;
text-decoration: underline;
}
@@ -93,7 +98,7 @@ main.prose hr {
main.prose code {
background: var(--code-inline-bg);
color: var(--code-inline-text);
color: var(--text);
font-family: var(--mono);
font-size: 13px;
padding: 2px 6px;
@@ -162,6 +167,7 @@ td {
vertical-align: top;
border-bottom: 1px solid var(--border-subtle);
color: var(--text);
;
}
tbody tr:last-child td {
@@ -246,7 +252,7 @@ aside#toc .toc-title {
font-size: 14px;
font-weight: 600;
line-height: 20px;
color: var(--text-heading);
color: var(--text);
margin-bottom: 2px;
}
@@ -274,11 +280,11 @@ aside#toc li a {
}
aside#toc li a:hover {
color: var(--text-heading);
color: var(--text);
}
aside#toc li a.active {
color: var(--text-heading);
color: var(--text);
font-weight: 600;
}
@@ -287,3 +293,9 @@ aside#toc li a.active {
display: none;
}
}
@supports (-webkit-touch-callout: none) {
#main-wrap {
margin-top: max(180px, env(safe-area-inset-top)) !important;
}
}

View File

@@ -4,7 +4,7 @@
:root[data-theme="dark"] {
--bg: #000000;
--text: #f3f3f3;
--text-heading: #ffffff;
--text: #ffffff;
--muted: #a3a3a3;
--border: #2b2b2b98;
--bg-elevated: rgba(255, 255, 255, 0.08);
@@ -38,6 +38,9 @@
--spider-display: none;
--cobweb-display: none;
--spider-thread: rgba(255, 255, 255, 0.3);
--snow-display: none;
--leaf-display: none;
--lights-display: none;
--hl-comment: #5c6370;
--hl-punct: #abb2bf;
@@ -71,7 +74,7 @@
:root[data-theme="light"] {
--bg: #ffffff;
--text: #0b0b0c;
--text-heading: #111827;
--text: #111827;
--muted: #6b7280;
--border: #e5e7eb;
--bg-elevated: rgba(0, 0, 0, 0.05);
@@ -82,7 +85,7 @@
--code-inline-text: #0b0b0c;
--code-text: #24292e;
--bg-menu: #ffffff;
--bg-modal: rgba(240, 240, 240, 0.7);
--bg-modal: rgba(240, 240, 240);
--hover-bg: rgba(0, 0, 0, 0.05);
--search-bg: rgba(0, 0, 0, 0.05);
--search-panel-bg: rgba(255, 255, 255, 0.85);
@@ -126,7 +129,7 @@
:root[data-theme="midnight"] {
--bg: #070913;
--text: #e2e8f0;
--text-heading: #f8fafc;
--text: #f8fafc;
--muted: #7f8ea3;
--border: #1a2234;
--bg-elevated: rgba(30, 41, 59, 0.5);
@@ -181,7 +184,7 @@
:root[data-theme="nord"] {
--bg: #2e3440;
--text: #eceff4;
--text-heading: #88c0d0;
--text: #88c0d0;
--muted: #d8dee9;
--border: #3b4252;
--bg-elevated: rgba(67, 76, 94, 0.5);
@@ -236,7 +239,7 @@
:root[data-theme="dracula"] {
--bg: #282a36;
--text: #f8f8f2;
--text-heading: #bd93f9;
--text: #bd93f9;
--muted: #6272a4;
--border: #44475a;
--bg-elevated: rgba(68, 71, 90, 0.5);
@@ -291,7 +294,7 @@
:root[data-theme="forest"] {
--bg: #0d1612;
--text: #e2ede5;
--text-heading: #86efac;
--text: #86efac;
--muted: #739481;
--border: #1a2f24;
--bg-elevated: rgba(34, 76, 56, 0.35);
@@ -346,7 +349,7 @@
:root[data-theme="sunset"] {
--bg: #180d18;
--text: #fce7f3;
--text-heading: #fb7185;
--text: #fb7185;
--muted: #a77c98;
--border: #3b1d36;
--bg-elevated: rgba(80, 30, 70, 0.35);
@@ -401,7 +404,7 @@
:root[data-theme="cyberpunk"] {
--bg: #050508;
--text: #00ffcc;
--text-heading: #ffe600;
--text: #ffe600;
--muted: #5e7a75;
--border: #1a2a26;
--bg-elevated: rgba(0, 255, 204, 0.08);
@@ -453,61 +456,6 @@
--hl-inserted-bg: rgba(0, 255, 204, 0.2);
}
:root[data-theme="halloween"] {
--bg: #0a0604;
--text: #f5ebe6;
--text-heading: #ff7518;
--muted: #a37559;
--border: #3d1c0c;
--bg-elevated: rgba(255, 117, 24, 0.12);
--bg-card: rgba(28, 14, 8, 0.6);
--bg-card-hover: rgba(46, 21, 10, 0.85);
--bg-code: #050201;
--code-inline-bg: rgba(255, 117, 24, 0.15);
--code-inline-text: #ff944d;
--code-text: #fed7aa;
--bg-menu: #0e0704;
--bg-modal: rgba(10, 6, 4, 0.92);
--hover-bg: rgba(255, 117, 24, 0.12);
--search-bg: rgba(255, 117, 24, 0.08);
--search-panel-bg: rgba(14, 7, 4, 0.95);
--code-group-bg: #1c0e07;
--code-group-panel-bg: #0a0604;
--code-tab-active: #ff7518;
--footer-border: #3d1c0c;
--border-subtle: rgba(255, 117, 24, 0.15);
--btn-primary-bg: #ff7518;
--btn-primary-text: #000000;
--btn-home-bg: #ff7518;
--btn-home-text: #000000;
--callout-note-bg: rgba(255, 117, 24, 0.14);
--callout-info-bg: rgba(168, 85, 247, 0.15);
--callout-warning-bg: rgba(255, 154, 77, 0.18);
--callout-warning-text: #ffaa5e;
--body-radial-tint: rgba(10, 6, 4, 0.85);
--skeleton-pulse-1: rgba(255, 117, 24, 0.06);
--skeleton-pulse-2: rgba(255, 117, 24, 0.18);
--spider-display: block;
--cobweb-display: block;
--spider-thread: rgba(255, 154, 77, 0.4);
--hl-comment: #7c4f34;
--hl-punct: #fed7aa;
--hl-func: #ff944d;
--hl-keyword: #a855f7;
--hl-string: #84cc16;
--hl-var: #ff7518;
--hl-op: #a855f7;
--hl-num: #f59e0b;
--hl-param: #ffedd5;
--hl-class: #c084fc;
--hl-tag: #ff7518;
--hl-deleted-color: #ef4444;
--hl-deleted-bg: rgba(239, 68, 68, 0.2);
--hl-inserted-color: #84cc16;
--hl-inserted-bg: rgba(132, 204, 22, 0.2);
}
body.banner-dismissed {
--header-height: 109px;
}
@@ -526,14 +474,18 @@ html {
body {
min-height: 100%;
min-height: -webkit-fill-available;
margin: 0;
font-family: var(--font);
background: var(--bg);
color: var(--text);
;
line-height: 1.65;
overflow-x: hidden;
-ms-overflow-style: none;
scrollbar-width: none;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
html::-webkit-scrollbar,
@@ -565,100 +517,3 @@ a {
color: inherit;
text-decoration: none;
}
.cobweb {
display: var(--cobweb-display);
position: fixed;
width: 160px;
height: 160px;
pointer-events: none;
z-index: 35;
opacity: 0.8;
}
.cobweb-top-left {
top: 0;
left: 0;
transform: scaleX(-1);
}
.cobweb-top-right {
top: 0;
right: 0;
}
.cobweb-media {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.spider-container {
display: var(--spider-display);
position: fixed;
top: 0;
right: 12%;
pointer-events: none;
z-index: 36;
animation: spiderDescend 10s ease-in-out infinite;
}
.spider-thread {
width: 1.5px;
height: 100%;
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
background: var(--spider-thread);
}
.spider-body {
position: relative;
width: 48px;
height: 48px;
margin-top: 100px;
animation: spiderSway 3s ease-in-out infinite alternate;
}
.spider-media {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
scale: 2;
padding-top: 20px;
}
@keyframes spiderDescend {
0% {
transform: translateY(-80px);
}
45% {
transform: translateY(240px);
}
55% {
transform: translateY(240px);
}
90% {
transform: translateY(10px);
}
100% {
transform: translateY(-80px);
}
}
@keyframes spiderSway {
0% {
transform: rotate(-10deg);
}
100% {
transform: rotate(10deg);
}
}

574
assets/styling/themes.css Normal file
View File

@@ -0,0 +1,574 @@
:root[data-theme="halloween"] {
--bg: #0a0604;
--text: #f5ebe6;
--text: #ff7518;
--muted: #a37559;
--border: #3d1c0c;
--bg-elevated: rgba(255, 117, 24, 0.12);
--bg-card: rgba(28, 14, 8, 0.6);
--bg-card-hover: rgba(46, 21, 10, 0.85);
--bg-code: #050201;
--code-inline-bg: rgba(255, 117, 24, 0.15);
--code-inline-text: #ff944d;
--code-text: #fed7aa;
--bg-menu: #0e0704;
--bg-modal: rgba(10, 6, 4, 0.92);
--hover-bg: rgba(255, 117, 24, 0.12);
--search-bg: rgba(255, 117, 24, 0.08);
--search-panel-bg: rgba(14, 7, 4, 0.95);
--code-group-bg: #1c0e07;
--code-group-panel-bg: #0a0604;
--code-tab-active: #ff7518;
--footer-border: #3d1c0c;
--border-subtle: rgba(255, 117, 24, 0.15);
--btn-primary-bg: #ff7518;
--btn-primary-text: #000000;
--btn-home-bg: #ff7518;
--btn-home-text: #000000;
--callout-note-bg: rgba(255, 117, 24, 0.14);
--callout-info-bg: rgba(168, 85, 247, 0.15);
--callout-warning-bg: rgba(255, 154, 77, 0.18);
--callout-warning-text: #ffaa5e;
--body-radial-tint: rgba(10, 6, 4, 0.85);
--skeleton-pulse-1: rgba(255, 117, 24, 0.06);
--skeleton-pulse-2: rgba(255, 117, 24, 0.18);
--spider-display: block;
--cobweb-display: block;
--spider-thread: rgba(255, 154, 77, 0.4);
--hl-comment: #7c4f34;
--hl-punct: #fed7aa;
--hl-func: #ff944d;
--hl-keyword: #a855f7;
--hl-string: #84cc16;
--hl-var: #ff7518;
--hl-op: #a855f7;
--hl-num: #f59e0b;
--hl-param: #ffedd5;
--hl-class: #c084fc;
--hl-tag: #ff7518;
--hl-deleted-color: #ef4444;
--hl-deleted-bg: rgba(239, 68, 68, 0.2);
--hl-inserted-color: #84cc16;
--hl-inserted-bg: rgba(132, 204, 22, 0.2);
}
:root[data-theme="christmas"] {
--bg: #07110c;
--text: #f4efe6;
--text: #ff5c5c;
--muted: #8fa89a;
--border: #1f3a2b;
--bg-elevated: rgba(255, 92, 92, 0.1);
--bg-card: rgba(10, 26, 18, 0.6);
--bg-card-hover: rgba(18, 44, 30, 0.85);
--bg-code: #040a07;
--code-inline-bg: rgba(245, 196, 81, 0.14);
--code-inline-text: #f5c451;
--code-text: #e8f5e9;
--bg-menu: #09150e;
--bg-modal: rgba(7, 17, 12, 0.9);
--hover-bg: rgba(255, 92, 92, 0.12);
--search-bg: rgba(245, 196, 81, 0.08);
--search-panel-bg: rgba(9, 21, 14, 0.95);
--code-group-bg: #11261a;
--code-group-panel-bg: #07110c;
--code-tab-active: #ff5c5c;
--footer-border: #1f3a2b;
--border-subtle: rgba(245, 196, 81, 0.14);
--btn-primary-bg: #d92d3a;
--btn-primary-text: #ffffff;
--btn-home-bg: #d92d3a;
--btn-home-text: #ffffff;
--callout-note-bg: rgba(47, 191, 113, 0.14);
--callout-info-bg: rgba(245, 196, 81, 0.14);
--callout-warning-bg: rgba(255, 92, 92, 0.18);
--callout-warning-text: #ff9a9a;
--body-radial-tint: rgba(7, 17, 12, 0.85);
--skeleton-pulse-1: rgba(245, 196, 81, 0.06);
--skeleton-pulse-2: rgba(245, 196, 81, 0.16);
--snow-display: block;
--lights-display: block;
--hl-comment: #5f8a72;
--hl-punct: #e8f5e9;
--hl-func: #f5c451;
--hl-keyword: #ff6b6b;
--hl-string: #5fd68f;
--hl-var: #ff9f68;
--hl-op: #ff6b6b;
--hl-num: #f5c451;
--hl-param: #fdf2d0;
--hl-class: #7fe3b0;
--hl-tag: #ff6b6b;
--hl-deleted-color: #ff6b6b;
--hl-deleted-bg: rgba(255, 107, 107, 0.18);
--hl-inserted-color: #5fd68f;
--hl-inserted-bg: rgba(95, 214, 143, 0.18);
}
:root[data-theme="autumn"] {
--bg: #1a0f08;
--text: #f7ead7;
--text: #f4b942;
--muted: #a68a6d;
--border: #3a2412;
--bg-elevated: rgba(244, 185, 66, 0.1);
--bg-card: rgba(40, 23, 11, 0.6);
--bg-card-hover: rgba(58, 36, 18, 0.85);
--bg-code: #0e0703;
--code-inline-bg: rgba(244, 185, 66, 0.14);
--code-inline-text: #f4b942;
--code-text: #fde7c0;
--bg-menu: #1a0f08;
--bg-modal: rgba(26, 15, 8, 0.9);
--hover-bg: rgba(244, 185, 66, 0.12);
--search-bg: rgba(244, 185, 66, 0.08);
--search-panel-bg: rgba(26, 15, 8, 0.95);
--code-group-bg: #28170b;
--code-group-panel-bg: #1a0f08;
--code-tab-active: #f4b942;
--footer-border: #3a2412;
--border-subtle: rgba(244, 185, 66, 0.14);
--btn-primary-bg: #f4b942;
--btn-primary-text: #1a0f08;
--btn-home-bg: #f4b942;
--btn-home-text: #1a0f08;
--callout-note-bg: rgba(200, 69, 44, 0.16);
--callout-info-bg: rgba(132, 160, 60, 0.16);
--callout-warning-bg: rgba(244, 185, 66, 0.18);
--callout-warning-text: #f8cf7a;
--body-radial-tint: rgba(26, 15, 8, 0.85);
--skeleton-pulse-1: rgba(244, 185, 66, 0.06);
--skeleton-pulse-2: rgba(244, 185, 66, 0.17);
--leaf-display: block;
--hl-comment: #8a6a4a;
--hl-punct: #fde7c0;
--hl-func: #f4b942;
--hl-keyword: #e0643f;
--hl-string: #a3b94a;
--hl-var: #e8913a;
--hl-op: #e0643f;
--hl-num: #f4b942;
--hl-param: #fdf0d8;
--hl-class: #d9a35b;
--hl-tag: #e0643f;
--hl-deleted-color: #e0643f;
--hl-deleted-bg: rgba(224, 100, 63, 0.18);
--hl-inserted-color: #a3b94a;
--hl-inserted-bg: rgba(163, 185, 74, 0.18);
}
:root[data-theme="winter"] {
--bg: #0d1b2a;
--text: #e6f0fa;
--text: #d6ecff;
--muted: #7f98b3;
--border: #1b2c44;
--bg-elevated: rgba(138, 209, 255, 0.1);
--bg-card: rgba(16, 32, 52, 0.6);
--bg-card-hover: rgba(26, 48, 76, 0.85);
--bg-code: #08121e;
--code-inline-bg: rgba(138, 209, 255, 0.14);
--code-inline-text: #8ad1ff;
--code-text: #cfe4f7;
--bg-menu: #0d1b2a;
--bg-modal: rgba(13, 27, 42, 0.88);
--hover-bg: rgba(138, 209, 255, 0.1);
--search-bg: rgba(138, 209, 255, 0.08);
--search-panel-bg: rgba(13, 27, 42, 0.95);
--code-group-bg: #132640;
--code-group-panel-bg: #0d1b2a;
--code-tab-active: #8ad1ff;
--footer-border: #1b2c44;
--border-subtle: rgba(138, 209, 255, 0.14);
--btn-primary-bg: #8ad1ff;
--btn-primary-text: #06101c;
--btn-home-bg: #8ad1ff;
--btn-home-text: #06101c;
--callout-note-bg: rgba(138, 209, 255, 0.14);
--callout-info-bg: rgba(185, 166, 255, 0.14);
--callout-warning-bg: rgba(255, 214, 138, 0.16);
--callout-warning-text: #ffd68a;
--body-radial-tint: rgba(13, 27, 42, 0.85);
--skeleton-pulse-1: rgba(138, 209, 255, 0.06);
--skeleton-pulse-2: rgba(138, 209, 255, 0.16);
--snow-display: block;
--hl-comment: #5b7491;
--hl-punct: #cfe4f7;
--hl-func: #8ad1ff;
--hl-keyword: #b9a6ff;
--hl-string: #9be7d4;
--hl-var: #ffb4c7;
--hl-op: #8ad1ff;
--hl-num: #ffd68a;
--hl-param: #e6f0fa;
--hl-class: #a9e4ff;
--hl-tag: #b9a6ff;
--hl-deleted-color: #ff8fa3;
--hl-deleted-bg: rgba(255, 143, 163, 0.18);
--hl-inserted-color: #9be7d4;
--hl-inserted-bg: rgba(155, 231, 212, 0.18);
}
.cobweb {
display: var(--cobweb-display);
position: fixed;
width: 160px;
height: 160px;
pointer-events: none;
z-index: 35;
opacity: 0.8;
}
.cobweb-top-left {
top: 0;
left: 0;
transform: scaleX(-1);
}
.cobweb-top-right {
top: 0;
right: 0;
}
.cobweb-media {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.spider-container {
display: var(--spider-display);
position: fixed;
top: 0;
right: 12%;
pointer-events: none;
z-index: 100;
animation: spiderDescend 10s ease-in-out infinite;
}
.spider-thread {
width: 1.5px;
height: 100vh;
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--spider-thread);
}
.spider-body {
position: relative;
width: 48px;
height: 48px;
margin-top: 100px;
pointer-events: auto;
cursor: pointer;
animation: spiderSway 3s ease-in-out infinite alternate;
}
.spider-media {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
scale: 2;
padding-top: 20px;
}
@keyframes spiderDescend {
0% {
transform: translateY(-80px);
}
45% {
transform: translateY(240px);
}
55% {
transform: translateY(240px);
}
90% {
transform: translateY(10px);
}
100% {
transform: translateY(-80px);
}
}
@keyframes spiderSway {
0% {
transform: rotate(-10deg);
}
100% {
transform: rotate(10deg);
}
}
@keyframes spiderDrop {
from {
transform: translateY(240px);
}
to {
transform: translateY(130vh);
}
}
:root {
--snow-tile: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cg fill='white'%3E%3Ccircle cx='22' cy='38' r='2.4' fill-opacity='.9'/%3E%3Ccircle cx='96' cy='126' r='1.8' fill-opacity='.7'/%3E%3Ccircle cx='168' cy='28' r='3' fill-opacity='.85'/%3E%3Ccircle cx='236' cy='204' r='2' fill-opacity='.75'/%3E%3Ccircle cx='272' cy='92' r='2.8' fill-opacity='.9'/%3E%3Ccircle cx='58' cy='252' r='2.2' fill-opacity='.8'/%3E%3Ccircle cx='196' cy='282' r='1.6' fill-opacity='.7'/%3E%3Ccircle cx='140' cy='190' r='2.4' fill-opacity='.85'/%3E%3C/g%3E%3Cg stroke='white' stroke-opacity='.9' stroke-width='1.3' stroke-linecap='round' fill='none'%3E%3Cpath transform='translate(126 72)' d='M0 -8V8M-6.9 -4L6.9 4M-6.9 4L6.9 -4'/%3E%3Cpath transform='translate(246 150)' d='M0 -6V6M-5.2 -3L5.2 3M-5.2 3L5.2 -3'/%3E%3Cpath transform='translate(40 160)' d='M0 -7V7M-6 -3.5L6 3.5M-6 3.5L6 -3.5'/%3E%3Cpath transform='translate(210 250)' d='M0 -6V6M-5.2 -3L5.2 3M-5.2 3L5.2 -3'/%3E%3C/g%3E%3C/svg%3E");
--leaf-tile: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='360' height='360'%3E%3Cg transform='translate(50 60) rotate(25) scale(1.2)' fill='%23e8710a'%3E%3Cpath d='M0 -14C10 -8 12 6 0 16C-12 6 -10 -8 0 -14Z'/%3E%3Cpath d='M0 -12V14' stroke='%23000' stroke-opacity='.25' stroke-width='1'/%3E%3C/g%3E%3Cg transform='translate(190 40) rotate(-40)' fill='%23c2410c'%3E%3Cpath d='M0 -14C10 -8 12 6 0 16C-12 6 -10 -8 0 -14Z'/%3E%3Cpath d='M0 -12V14' stroke='%23000' stroke-opacity='.25' stroke-width='1'/%3E%3C/g%3E%3Cg transform='translate(300 130) rotate(70) scale(1.3)' fill='%23eab308'%3E%3Cpath d='M0 -14C10 -8 12 6 0 16C-12 6 -10 -8 0 -14Z'/%3E%3Cpath d='M0 -12V14' stroke='%23000' stroke-opacity='.25' stroke-width='1'/%3E%3C/g%3E%3Cg transform='translate(120 200) rotate(-15) scale(1.1)' fill='%23b45309'%3E%3Cpath d='M0 -14C10 -8 12 6 0 16C-12 6 -10 -8 0 -14Z'/%3E%3Cpath d='M0 -12V14' stroke='%23000' stroke-opacity='.25' stroke-width='1'/%3E%3C/g%3E%3Cg transform='translate(250 280) rotate(140) scale(1.2)' fill='%23d97706'%3E%3Cpath d='M0 -14C10 -8 12 6 0 16C-12 6 -10 -8 0 -14Z'/%3E%3Cpath d='M0 -12V14' stroke='%23000' stroke-opacity='.25' stroke-width='1'/%3E%3C/g%3E%3Cg transform='translate(40 310) rotate(-80)' fill='%23ca8a04'%3E%3Cpath d='M0 -14C10 -8 12 6 0 16C-12 6 -10 -8 0 -14Z'/%3E%3Cpath d='M0 -12V14' stroke='%23000' stroke-opacity='.25' stroke-width='1'/%3E%3C/g%3E%3C/svg%3E");
--lights-tile: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='56'%3E%3Cpath d='M0 4Q40 32 80 4Q120 32 160 4' fill='none' stroke='%23245c3a' stroke-width='1.6'/%3E%3Cg fill='%23222'%3E%3Crect x='18' y='13' width='4' height='4'/%3E%3Crect x='38' y='17' width='4' height='4'/%3E%3Crect x='58' y='13' width='4' height='4'/%3E%3Crect x='98' y='13' width='4' height='4'/%3E%3Crect x='118' y='17' width='4' height='4'/%3E%3Crect x='138' y='13' width='4' height='4'/%3E%3C/g%3E%3Cellipse cx='20' cy='22' rx='4' ry='5.5' fill='%23ff4d4d'/%3E%3Cellipse cx='40' cy='26' rx='4' ry='5.5' fill='%23ffd24d'/%3E%3Cellipse cx='60' cy='22' rx='4' ry='5.5' fill='%234dff88'/%3E%3Cellipse cx='100' cy='22' rx='4' ry='5.5' fill='%234da6ff'/%3E%3Cellipse cx='120' cy='26' rx='4' ry='5.5' fill='%23ff4d4d'/%3E%3Cellipse cx='140' cy='22' rx='4' ry='5.5' fill='%23ffd24d'/%3E%3C/svg%3E");
}
.snowfall,
.leaf-fall {
position: fixed;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 30;
}
.snowfall {
display: var(--snow-display);
}
.leaf-fall {
display: var(--leaf-display);
}
.snowfall::before,
.snowfall::after,
.leaf-fall::before,
.leaf-fall::after {
content: "";
position: absolute;
background-repeat: repeat;
will-change: transform;
}
.snowfall::before {
top: -600px;
left: 0;
right: 0;
bottom: 0;
background-image: var(--snow-tile);
background-size: 300px 300px;
opacity: 0.9;
animation: snowFallNear 18s linear infinite;
}
.snowfall::after {
top: -500px;
left: 0;
right: 0;
bottom: 0;
background-image: var(--snow-tile);
background-size: 500px 500px;
opacity: 0.6;
animation: snowFallFar 30s linear infinite;
}
.leaf-fall::before {
top: -720px;
left: 0;
right: -360px;
bottom: 0;
background-image: var(--leaf-tile);
background-size: 360px 360px;
opacity: 0.9;
animation: leafFallNear 26s linear infinite;
}
.leaf-fall::after {
top: -540px;
left: -540px;
right: 0;
bottom: 0;
background-image: var(--leaf-tile);
background-size: 540px 540px;
opacity: 0.7;
animation: leafFallFar 40s linear infinite;
}
.light-string {
display: var(--lights-display);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 56px;
pointer-events: none;
z-index: 100;
background-image: var(--lights-tile);
background-repeat: repeat-x;
background-size: 160px 56px;
animation: lightTwinkle 2.6s ease-in-out infinite alternate;
}
@keyframes snowFallNear {
from {
transform: translate3d(0, 0, 0);
}
to {
transform: translate3d(0, 600px, 0);
}
}
@keyframes snowFallFar {
from {
transform: translate3d(0, 0, 0);
}
to {
transform: translate3d(0, 500px, 0);
}
}
@keyframes leafFallNear {
from {
transform: translate3d(0, 0, 0);
}
to {
transform: translate3d(-360px, 720px, 0);
}
}
@keyframes leafFallFar {
from {
transform: translate3d(0, 0, 0);
}
to {
transform: translate3d(540px, 540px, 0);
}
}
@keyframes lightTwinkle {
from {
opacity: 0.65;
filter: drop-shadow(0 0 3px rgba(255, 214, 120, 0.5));
}
to {
opacity: 1;
filter: drop-shadow(0 0 8px rgba(255, 214, 120, 0.9));
}
}
@media (prefers-reduced-motion: reduce) {
.snowfall::before,
.snowfall::after,
.leaf-fall::before,
.leaf-fall::after,
.light-string {
animation: none;
}
}
#navbar .brand {
position: relative;
}
#navbar .brand::after {
content: "";
position: absolute;
top: -9px;
right: -9px;
font-size: 14px;
line-height: 1;
pointer-events: none;
}
:root[data-theme="halloween"] #navbar .brand::after {
content: "🎃";
}
:root[data-theme="autumn"] #navbar .brand::after {
content: "🍂";
}
:root[data-theme="winter"] #navbar .brand::after {
content: "❄️";
}
:root[data-theme="christmas"] #navbar .brand::after {
content: "🎄";
}
.st-themes {
padding: 14px 16px 16px;
}
.st-sub {
margin: 0 0 8px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.st-theme-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin-bottom: 16px;
}
.st-theme-grid:last-child {
margin-bottom: 0;
}
.st-theme {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
height: 40px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 100px;
background: var(--search-bg);
color: var(--muted);
font: inherit;
font-size: 13.5px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.st-theme .ico {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.st-theme span {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.st-theme:hover {
color: var(--text);
}
.st-theme.on {
border-color: var(--text);
background: var(--hover-bg);
color: var(--text);
font-weight: 600;
}

View File

@@ -12,14 +12,14 @@ Provider availability and performance can vary based on your network environment
## How Providers Are Used
When your app calls `getStream()`—directly or through Stream API:
When your app calls `getStream()`-directly or through Stream API:
1. All active (non-disabled) providers are queried **simultaneously** with per-source random jitter to stagger upstream requests
2. Each provider has its own **timeout**, **retry count**, and **backoff delay**
3. Every returned URL is **verified** — first via a HEAD request to the raw upstream URL, then via a proxied HLS playability check
3. Every returned URL is **verified** - first via a HEAD request to the raw upstream URL, then via a proxied HLS playability check
4. Only URLs that pass both verification steps appear in the response as `source` events
Because the endpoint is SSE, **verified sources are emitted immediately as each provider resolves** — your player receives them progressively. The stream closes once every provider has either resolved or timed out.
Because the endpoint is SSE, **verified sources are emitted immediately as each provider resolves** - your player receives them progressively. The stream closes once every provider has either resolved or timed out.
:::callout-info
Call `sdk.getSources(true)` to inspect enabled providers and `sdk.probeAllSources()` for a live availability signal. If you use Stream API, its `/health` endpoint exposes probes over HTTP.
@@ -68,7 +68,7 @@ CACHE_TTL = 5 minutes
All provider stream URLs are wrapped in the server proxy (unless `skipProxy: true`):
```
http://localhost:7860/api?url=<encoded_stream_url>&<proxyParam>=1
http://YOUR_API_URL/api?url=<encoded_stream_url>&<proxyParam>=1
```
When your player fetches this path from your self-hosted instance, the server:
@@ -83,5 +83,5 @@ When your player fetches this path from your self-hosted instance, the server:
Before a URL is emitted as a `source` event, it passes two verification steps:
1. **`verifyStream`** — a HEAD request to the raw upstream URL. Must return HTTP `< 400`.
2. **`verifyPlayable`** — fetches the proxied URL, validates `#EXTM3U`, checks for `#EXTINF` or `#EXT-X-STREAM-INF`, then fetches the first segment.
1. **`verifyStream`** - a HEAD request to the raw upstream URL. Must return HTTP `< 400`.
2. **`verifyPlayable`** - fetches the proxied URL, validates `#EXTM3U`, checks for `#EXTINF` or `#EXT-X-STREAM-INF`, then fetches the first segment.

View File

@@ -93,19 +93,14 @@
gap: 6px;
padding: 10px 22px;
border-radius: 100px;
background: #000000;
color: #ffffff !important;
background: var(--btn-primary-bg);
color: var(--btn-primary-text) !important;
font-size: 13.5px;
font-weight: 600;
text-decoration: none !important;
transition: transform 0.15s ease;
}
html.dark .btn-primary {
background: #FFFFFF;
color: #000000 !important;
}
.btn-secondary {
display: inline-flex;
align-items: center;
@@ -156,6 +151,11 @@
z-index: 1;
}
.header-desc {
font-size: 14.5px;
margin-top: 0 !important;
}
.section-head {
margin-bottom: 22px;
text-align: left;
@@ -166,13 +166,13 @@
font-size: 22px !important;
font-weight: 700 !important;
margin: 0 0 6px !important;
color: #ffffff !important;
color: var(--text) !important;
}
.section-head p {
font-size: 14.5px;
margin: 0;
color: #a1a1aa;
color: var(--muted);
}
.vyla-grid {
@@ -247,11 +247,71 @@
opacity: 0.75;
}
.hero-greeting {
margin: 0 0 14px;
padding: 6px 16px;
border-radius: 100px;
background: var(--hover-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
color: var(--text);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.01em;
}
.hero-resume {
max-width: min(90vw, 360px);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--text);
}
@media (max-width: 900px) {
#header-container {
border-radius: 100px;
}
}
@media (orientation: landscape) and (max-height: 550px) {
.home-header {
height: auto;
min-height: 280px;
padding-top: calc(var(--header-height) + 24px);
padding-bottom: 28px;
}
.header-logo-badge {
width: 44px;
height: 44px;
margin-bottom: 8px;
}
.header-title {
font-size: 26px;
margin-bottom: 6px;
}
.header-desc {
font-size: 13.5px;
margin-bottom: 14px;
max-width: 540px;
}
.content-section {
margin-top: 32px;
}
.vyla-grid {
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
}
.feature-box {
padding: 18px;
}
}
</style>
<div class="home-container">

View File

@@ -3,8 +3,11 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Vyla Entertainment — Vyla</title>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<title>Vyla Entertainment - Vyla</title>
<meta name="description"
content="Vyla Entertainment is a community-built, decentralized media platform and developer ecosystem. Featuring native client apps for Desktop, Mobile, and Android TV, a scraping SDK, and a self-hosted Stream API." />
<meta name="keywords"
@@ -15,7 +18,7 @@
<meta property="og:locale" content="en_US" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Vyla Entertainment — Free & Decentralized Media Platform" />
<meta property="og:title" content="Vyla Entertainment - Free & Decentralized Media Platform" />
<meta property="og:description"
content="A community-built media ecosystem focused on freedom, privacy, and control. Featuring native Desktop, Android, and TV apps backed by modular developer tools." />
<meta property="og:url" content="https://vyla.cc/" />
@@ -26,7 +29,7 @@
<meta property="og:image:alt" content="Vyla Entertainment Ecosystem" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Vyla Entertainment — Free & Decentralized Media Platform" />
<meta name="twitter:title" content="Vyla Entertainment - Free & Decentralized Media Platform" />
<meta name="twitter:description"
content="Explore Vyla Home, Go, TV, and the developer SDK. Built for privacy, performance, and complete streaming control." />
<meta name="twitter:image" content="/assets/images/site_preview.png" />
@@ -77,6 +80,9 @@
<link rel="stylesheet" href="/assets/styling/layout.css" />
<link rel="stylesheet" href="/assets/styling/prose.css" />
<link rel="stylesheet" href="/assets/styling/components.css" />
<link rel="stylesheet" href="/assets/styling/themes.css" />
<link rel="stylesheet" href="/assets/styling/overlay.css" />
<link rel="stylesheet" href="/assets/styling/features.css" />
<link rel="stylesheet" href="/assets/styling/highlight.css" />
<link rel="stylesheet" href="/assets/styling/404.css" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@@ -87,7 +93,12 @@
src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script>
(function () {
var saved = localStorage.getItem('vyla-theme') || 'halloween';
var saved = localStorage.getItem('vyla-theme') || 'auto';
if (saved === 'auto') {
var d = new Date();
var m = d.getMonth();
saved = m === 9 ? 'halloween' : m === 10 ? 'autumn' : m === 11 ? 'christmas' : (m === 0 || m === 1) ? 'winter' : (m === 8 && d.getDate() >= 22) ? 'autumn' : 'dark';
}
document.documentElement.setAttribute('data-theme', saved);
})();
</script>
@@ -106,10 +117,13 @@
<img class="spider-media" src="https://www.pngmart.com/files/1/Spider-PNG-File.png" alt="" />
</div>
</div>
<div class="snowfall" aria-hidden="true"></div>
<div class="leaf-fall" aria-hidden="true"></div>
<div class="light-string" aria-hidden="true"></div>
<video class="video-bg" autoplay muted loop playsinline></video>
<div id="banner">
<span>
Rights holders can request source removal — <a href="/misc/project/dmca">DMCA policy</a>.
Rights holders can request source removal - <a href="/misc/project/dmca">DMCA policy</a>.
</span>
<button id="banner-close" onclick="dismissBanner()" aria-label="Dismiss">✕</button>
</div>
@@ -120,6 +134,8 @@
height="28" decoding="async" /></a>
<button id="search-trigger" type="button" onclick="openSearch()" aria-label="Search"></button>
<div class="links">
<button id="settings-trigger" type="button" onclick="openSettings()"
aria-label="Personalize"></button>
<button id="theme-toggle" type="button" onclick="toggleThemeMenu(event)"
aria-label="Theme"></button>
</div>
@@ -139,6 +155,7 @@
<a id="footer-next" class="footer-link next" href="#"></a>
</div>
<div class="footer-bottom">
<p id="footer-note"></p>
<div class="footer-socials">
<a href="https://discord.gg/5eqRhKeywG" target="_blank" rel="noopener"
aria-label="Discord">

View File

@@ -20,7 +20,7 @@ At no point does Vyla store, transcode, or re-host any media file.
## Content Sources
The following third-party sources are scraped or proxied by Vyla API. These are upstream providers — Vyla does not control, operate, or own any of them.
The following third-party sources are scraped or proxied by Vyla API. These are upstream providers - Vyla does not control, operate, or own any of them.
You can find the full list of sources in the [stream-api sources directory](https://gitlab.com/vyla-entertainment/sdk/-/tree/main/src/sources).

View File

@@ -0,0 +1,5 @@
# Changelog
<p class="page-description">Release notes for every Vyla project, pulled live from GitLab.</p>
<div id="changelog" class="changelog"></div>

View File

@@ -53,3 +53,11 @@ Massive thanks to:
- The P-Stream contributors
- The CinePro contributors
- The Movieslay contributors
---
## Contributors
People who have committed to Vyla projects, pulled live from GitLab.
<div id="contributors" class="contributors"></div>

20
misc/project/story.md Normal file
View File

@@ -0,0 +1,20 @@
# Our story
<p class="page-description">Why Vyla exists, what we care about, and how you can help.</p>
Vyla started as a small web project with a simple goal: a cleaner, more private way to discover and watch movies and shows without relying on a single company or centralized service. It has since grown into apps for desktop, Android and Android TV, plus the tools that power them.
Vyla is software, not a streaming service. We do not host or provide any media.
## What we care about
- **Control.** You decide how much of Vyla you use and where it runs.
- **Privacy.** Vyla stays local and self-hostable, with no unnecessary centralized dependencies.
- **Community.** Vyla is built in the open by the people who use it.
## Where to go next
- Pick an app: [Vyla Home](/products/home), [Vyla Go](/products/go) or [Vyla TV](/products/tv).
- Build with Vyla: start with the [SDK quickstart](/sdk/vyla-sdk/quickstart) or the [Stream API](/stream-api/overview).
- Help fix sources: read the [contributing guide](/sdk/vyla-sdk/contributing).
- See who made this possible on the [credits page](/misc/project/credit).

View File

@@ -12,6 +12,6 @@ Vyla Go is available for Android. Get the latest version below:
| Platform | Download |
| --- | --- |
| Android | [Download .apk](https://gitlab.com/vyla-entertainment/go/-/releases) |
| Android | [Download: v0.0.0 (.apk)](https://gitlab.com/vyla-entertainment/go/-/releases) |
Inside the releases page, you can find the latest version of Vyla Go for Android, find the `.apk` file, and install it on your device.
Clicking the download button will retrieve the latest `.apk` package for your device.

View File

@@ -14,11 +14,11 @@ Vyla Home is available for Windows, macOS, and Linux. Get the latest version bel
| Platform | Download |
| --- | --- |
| Windows | [Download .exe](https://gitlab.com/vyla-entertainment/home/-/releases) |
| macOS | [Download .dmg](https://gitlab.com/vyla-entertainment/home/-/releases) |
| Windows | [Download: v0.0.0 (.exe)](https://gitlab.com/vyla-entertainment/home/-/releases) |
| macOS | [Download: v0.0.0 (.dmg)](https://gitlab.com/vyla-entertainment/home/-/releases) |
| Linux | [Build from source](#linux-setup) |
Inside the releases page, you can find the latest version of Vyla Home for each platform, find the `.exe` for windows, and `.dmg` for macOS.
Clicking the download button will retrieve the latest installer for Windows or macOS.
## Linux Setup

View File

@@ -14,6 +14,6 @@ Vyla TV is available for Android TV. Get the latest version below:
| Platform | Download |
| --- | --- |
| Android TV | [Download .apk](https://gitlab.com/vyla-entertainment/TV/-/releases) |
| Android TV | [Download: v0.0.0 (.apk)](https://gitlab.com/vyla-entertainment/TV/-/releases) |
Inside the releases page, you can find the latest version of Vyla TV for Android TV, find the `.apk` file, and install it on your device.
Clicking the download button will retrieve the latest `.apk` package for your Android TV device.

View File

@@ -0,0 +1,170 @@
# Contributing to Sources
<p class="page-description">Help maintain, fix broken scrapers, and add new video or download providers to the Vyla SDK.</p>
Third-party streaming and scraping endpoints frequently change their DOM layout, API endpoints, tokens, and CDN protections. Community contributions are vital to keeping providers operational.
---
## Getting Started
### 1. Clone the repository
Clone the SDK repository to your local machine:
```bash
git clone https://gitlab.com/vyla-entertainment/vyla-sdk.git
cd vyla-sdk
```
### 2. Install dependencies
Ensure you are running Node.js 18 or newer:
```bash
npm install
```
### 3. Environment configuration
Copy the example environment file and add your TMDB API key:
```bash
cp .env.example .env
```
Open `.env` and set your key:
```ini
TMDB_API_KEY=your_actual_tmdb_api_key_here
```
---
## Project Structure
- `src/sources/`: Scraper implementations for each provider.
- `src/config.js`: Source registry containing timeouts, retries, headers, and proxy flags.
- `src/downloads/downloads.js`: Downloader resolver extracting direct MP4 streams.
- `src/subtitles.js`: Subtitle mirror aggregation endpoints.
- `test/index.js`: Test runner for testing streams, subtitles, and downloads.
---
## Testing Sources
Testing is driven by `test/index.js`. Open the file and update `CONFIG` to target the source you are fixing or adding:
```javascript
const CONFIG = {
DEBUG: true,
MOVIE_ID: "936075",
TV_ID: "37854",
TV_SEASON: "1",
TV_EPISODE: "1",
STREAM_SOURCE: "vidrock",
TEST_SUBTITLE: false,
TEST_DOWNLOAD: false,
TEST_MOVIE: true,
TEST_TV: true,
EXTRA_SDK_PARAMS: {}
};
```
Run the test suite:
```bash
npm test
```
When `DEBUG: true` is enabled, all outbound fetch calls, headers, request payloads, and raw responses will be logged to your terminal and saved to `test/debug-output.json`.
---
## Fixing or Adding a Source
### 1. Source Resolver
All source files are located in `src/sources/`. Each module must export an asynchronous `getStream` function:
```javascript
export async function getStream({ id, s, e, clientIP, audio, sdk, config }) {
const isTv = Boolean(s && e);
const mediaId = id;
const streamUrl = "https://example-cdn.com/stream/playlist.m3u8";
return {
url: streamUrl,
quality: "auto",
isM3U8: true,
headers: {
Referer: "https://example-provider.to/",
Origin: "https://example-provider.to"
},
allUrls: [
{
url: streamUrl,
quality: "1080p",
isM3U8: true
}
]
};
}
```
### 2. Registering in `src/config.js`
Add or update the source entry inside the `SOURCES` array in `src/config.js`:
```javascript
{
key: 'mysource',
label: 'MySource',
sourceFile: 'mysource',
proxyParam: 'ms',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true,
cdnHeaders: [{
pattern: /cdn\.mysource\.to/i,
headers: {
Referer: 'https://mysource.to/',
Origin: 'https://mysource.to',
},
}],
}
```
- `skipProxy`: Set to `true` if the stream plays directly in client players without server-side proxying.
- `multiUrl`: Set to `true` if the scraper provides multiple resolutions or fallback mirrors.
- `cdnHeaders`: Include custom headers required by the upstream CDN to avoid HTTP 403 Forbidden responses.
---
## Download Providers
Downloads are handled automatically by `src/downloads/downloads.js`. The downloader queries active sources that have `skipProxy: true` enabled and inspects returned streams:
- URLs ending in `.mp4` or containing `.mp4` queries are automatically extracted.
- `.m3u8` streams are filtered out because they are HLS segment playlists rather than direct file downloads.
- If you are implementing a provider that supplies raw MP4 video files, return them in `allUrls` or `url` with `skipProxy: true` in `src/config.js`.
To verify download resolution in `test/index.js`, toggle:
```javascript
TEST_DOWNLOAD: true
```
---
## Submitting Pull Requests
1. Verify that your updated source passes tests for both movies (`TEST_MOVIE: true`) and TV shows (`TEST_TV: true`).
2. Ensure you have not committed your `.env` file or personal TMDB API keys.
3. Push your branch and open a Merge Request on GitLab.

View File

@@ -19,7 +19,7 @@ The SDK can be used independently of the self-hosted Stream API. If you prefer t
- **Automated Anime Sub/Dub Routing**: Automatically configures audio track requirements (`sub` vs `dub`) based on provider naming conventions.
- **Multi-Source Subtitle Aggregation**: Merges subtitle tracks from multiple redundant subtitle mirrors into a unified VTT/SRT format.
- **Metadata Fallbacks**: Automatically maps TMDB IDs to external IDs (such as IMDB and AniList) for smooth stream lookups.
- **Self-Contained**: No need to deploy a separate API server — all streaming functionality runs within your existing Node.js application.
- **Self-Contained**: No need to deploy a separate API server - all streaming functionality runs within your existing Node.js application.
---
@@ -82,8 +82,8 @@ const geoStream = await sdk.getStream("sourcename", "155", null, null, "203.0.11
| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `key` | `string` | **Yes** | — | Unique key identifier for the provider (e.g. `"sourcename"`). |
| `id` | `string` | **Yes** | — | TMDB media ID (e.g., `"155"`). |
| `key` | `string` | **Yes** | - | Unique key identifier for the provider (e.g. `"sourcename"`). |
| `id` | `string` | **Yes** | - | TMDB media ID (e.g., `"155"`). |
| `s` | `string \| number` | **No** | `null` | Season number (required for TV shows). |
| `e` | `string \| number` | **No** | `null` | Episode number (required for TV shows). |
| `clientIP` | `string` | **No** | `null` | End-user IP address forwarded for geo-restricted scrapers. |
@@ -127,7 +127,7 @@ const tvSubs = await sdk.getSubtitles("1399", 1, 1);
| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | **Yes** | — | TMDB media ID. |
| `id` | `string` | **Yes** | - | TMDB media ID. |
| `s` | `string \| number` | **No** | `null` | Season number (required for TV shows). |
| `e` | `string \| number` | **No** | `null` | Episode number (required for TV shows). |

View File

@@ -41,7 +41,7 @@ GA_API_SECRET=
| `GA_MEASUREMENT_ID` / `GA_API_SECRET` | No | Enables Google Analytics event reporting. Leave blank to disable. |
| `PORT` | No | Port the server listens on. Defaults to `7860`. |
| `WORKER_COUNT` | No | Number of cluster workers to fork. Defaults to `1` locally. |
| `ENABLE_DEBUG_ROUTE` | No | Set to `true` to enable `/api/debug/:id`. Keep this `false` in production — it exposes upstream request/response headers and raw source output. |
| `ENABLE_DEBUG_ROUTE` | No | Set to `true` to enable `/api/debug/:id`. Keep this `false` in production - it exposes upstream request/response headers and raw source output. |
| `PROXY_STREAMS` | No | Set to `true` to route stream playback through your own server's `/api` proxy endpoint instead of direct URLs. |
| `PROXY_URL` | No | An external proxy base URL to use instead of the built-in `/api` proxy. Only used if `PROXY_STREAMS` is enabled. |

View File

@@ -4,7 +4,7 @@
## Use this only when HTTP is the right boundary
Stream API packages Vyla SDK behind a small Node.js HTTP service. It is useful when a browser, mobile app, or another process needs progressive playback candidates over HTTP. It is not required to use Vyla, and it is not the primary integration for a Node.js application—use [Vyla SDK](/sdk) directly in that case.
Stream API packages Vyla SDK behind a small Node.js HTTP service. It is useful when a browser, mobile app, or another process needs progressive playback candidates over HTTP. It is not required to use Vyla, and it is not the primary integration for a Node.js application-use [Vyla SDK](/sdk) directly in that case.
The service accepts TMDB IDs, asks configured SDK providers for candidates, verifies results, and emits usable sources as they arrive. A player can start with the first source while retaining later sources as fallbacks.

View File

@@ -6,15 +6,15 @@
`GET /health` (or `/api/health`) probes active providers and returns a JSON summary. It includes a status, timestamp, TMDB-key presence, cache size, and a map of provider keys to `{ ok, ms }` results.
Health is a signal, not a promise of playback. Provider behavior can differ by title, client network, and the moment of the request. Use it for observability and source selection—not as a guarantee that a viewer will receive a stream.
Health is a signal, not a promise of playback. Provider behavior can differ by title, client network, and the moment of the request. Use it for observability and source selection-not as a guarantee that a viewer will receive a stream.
## Test one provider
`GET /test/:id?source=:providerKey` resolves a single provider for a movie. Add `season` and `episode` (or `s` and `e`) for TV.
```bash
curl "http://localhost:7860/test/155?source=provider-key"
curl "http://localhost:7860/test/1396?season=1&episode=1&source=provider-key"
curl "YOUR_API_URL/test/155?source=provider-key"
curl "YOUR_API_URL/test/1396?season=1&episode=1&source=provider-key"
```
Use source keys reported by the running deployment. A test result reports whether resolution and verification succeeded; clients should inspect the result body rather than relying only on a transport success.

View File

@@ -14,8 +14,8 @@ The API supports both the short paths below and `/api`-prefixed equivalents. A m
`id` is a TMDB ID. For example:
```bash
curl -N "http://localhost:7860/movie?id=155"
curl -N "http://localhost:7860/tv?id=1396&season=1&episode=1"
curl -N "YOUR_API_URL/movie?id=155"
curl -N "YOUR_API_URL/tv?id=1396&season=1&episode=1"
```
## Event contract
@@ -44,7 +44,7 @@ One event for each candidate that passes the service's verification pipeline.
"source": {
"source": "provider-key",
"label": "Provider name",
"url": "http://localhost:7860/api?url=…"
"url": "YOUR_API_URL/api?url=…"
}
}
```