refactor: more refactoring to topics as the top-level organization

This commit is contained in:
Andy Burke 2025-09-11 14:09:28 -07:00
parent 11ecd86bb9
commit 4347d20263
18 changed files with 730 additions and 317 deletions

View file

@ -33,12 +33,12 @@
"imports": { "imports": {
"@andyburke/fsdb": "jsr:@andyburke/fsdb@^1.0.2", "@andyburke/fsdb": "jsr:@andyburke/fsdb@^1.0.2",
"@andyburke/lurid": "jsr:@andyburke/lurid@^0.2.0", "@andyburke/lurid": "jsr:@andyburke/lurid@^0.2.0",
"@andyburke/serverus": "jsr:@andyburke/serverus@^0.12.5", "@andyburke/serverus": "jsr:@andyburke/serverus@^0.13.0",
"@da/bcrypt": "jsr:@da/bcrypt@^1.0.1", "@da/bcrypt": "jsr:@da/bcrypt@^1.0.1",
"@std/assert": "jsr:@std/assert@^1.0.13", "@std/assert": "jsr:@std/assert@^1.0.14",
"@std/encoding": "jsr:@std/encoding@^1.0.10", "@std/encoding": "jsr:@std/encoding@^1.0.10",
"@std/fs": "jsr:@std/fs@^1.0.19", "@std/fs": "jsr:@std/fs@^1.0.19",
"@std/http": "jsr:@std/http@^1.0.20", "@std/http": "jsr:@std/http@^1.0.20",
"@std/path": "jsr:@std/path@^1.1.1" "@std/path": "jsr:@std/path@^1.1.2"
} }
} }

33
deno.lock generated
View file

@ -3,10 +3,10 @@
"specifiers": { "specifiers": {
"jsr:@andyburke/fsdb@^1.0.2": "1.0.2", "jsr:@andyburke/fsdb@^1.0.2": "1.0.2",
"jsr:@andyburke/lurid@0.2": "0.2.0", "jsr:@andyburke/lurid@0.2": "0.2.0",
"jsr:@andyburke/serverus@~0.12.5": "0.12.5", "jsr:@andyburke/serverus@0.13": "0.13.0",
"jsr:@da/bcrypt@*": "1.0.1", "jsr:@da/bcrypt@*": "1.0.1",
"jsr:@da/bcrypt@^1.0.1": "1.0.1", "jsr:@da/bcrypt@^1.0.1": "1.0.1",
"jsr:@std/assert@^1.0.13": "1.0.13", "jsr:@std/assert@^1.0.14": "1.0.14",
"jsr:@std/cli@^1.0.19": "1.0.21", "jsr:@std/cli@^1.0.19": "1.0.21",
"jsr:@std/cli@^1.0.20": "1.0.21", "jsr:@std/cli@^1.0.20": "1.0.21",
"jsr:@std/cli@^1.0.21": "1.0.21", "jsr:@std/cli@^1.0.21": "1.0.21",
@ -17,12 +17,13 @@
"jsr:@std/fs@^1.0.19": "1.0.19", "jsr:@std/fs@^1.0.19": "1.0.19",
"jsr:@std/html@^1.0.4": "1.0.4", "jsr:@std/html@^1.0.4": "1.0.4",
"jsr:@std/http@^1.0.20": "1.0.20", "jsr:@std/http@^1.0.20": "1.0.20",
"jsr:@std/internal@^1.0.6": "1.0.10", "jsr:@std/internal@^1.0.10": "1.0.10",
"jsr:@std/internal@^1.0.9": "1.0.10", "jsr:@std/internal@^1.0.9": "1.0.10",
"jsr:@std/media-types@^1.1.0": "1.1.0", "jsr:@std/media-types@^1.1.0": "1.1.0",
"jsr:@std/net@^1.0.4": "1.0.4", "jsr:@std/net@^1.0.4": "1.0.4",
"jsr:@std/path@^1.1.0": "1.1.1", "jsr:@std/path@^1.1.0": "1.1.2",
"jsr:@std/path@^1.1.1": "1.1.1", "jsr:@std/path@^1.1.1": "1.1.2",
"jsr:@std/path@^1.1.2": "1.1.2",
"jsr:@std/streams@^1.0.10": "1.0.10", "jsr:@std/streams@^1.0.10": "1.0.10",
"npm:@types/node@*": "22.15.15" "npm:@types/node@*": "22.15.15"
}, },
@ -41,8 +42,8 @@
"jsr:@std/cli@^1.0.19" "jsr:@std/cli@^1.0.19"
] ]
}, },
"@andyburke/serverus@0.12.5": { "@andyburke/serverus@0.13.0": {
"integrity": "c6bf017e82f20625f9d29dacaa7e2b034e91c37b8171f6725fade4599db66864", "integrity": "73f451e1b68cd9be3938333b06290bfeab275361453559f40dfeab19dc4ad6d7",
"dependencies": [ "dependencies": [
"jsr:@std/cli@^1.0.21", "jsr:@std/cli@^1.0.21",
"jsr:@std/fmt@^1.0.6", "jsr:@std/fmt@^1.0.6",
@ -55,10 +56,10 @@
"@da/bcrypt@1.0.1": { "@da/bcrypt@1.0.1": {
"integrity": "d2172d3acbcff52e0465557a1a48b1ff1c92df08c90712dae5372255a8c45eb3" "integrity": "d2172d3acbcff52e0465557a1a48b1ff1c92df08c90712dae5372255a8c45eb3"
}, },
"@std/assert@1.0.13": { "@std/assert@1.0.14": {
"integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", "integrity": "68d0d4a43b365abc927f45a9b85c639ea18a9fab96ad92281e493e4ed84abaa4",
"dependencies": [ "dependencies": [
"jsr:@std/internal@^1.0.6" "jsr:@std/internal@^1.0.10"
] ]
}, },
"@std/cli@1.0.21": { "@std/cli@1.0.21": {
@ -103,10 +104,10 @@
"@std/net@1.0.4": { "@std/net@1.0.4": {
"integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852" "integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852"
}, },
"@std/path@1.1.1": { "@std/path@1.1.2": {
"integrity": "fe00026bd3a7e6a27f73709b83c607798be40e20c81dde655ce34052fd82ec76", "integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038",
"dependencies": [ "dependencies": [
"jsr:@std/internal@^1.0.9" "jsr:@std/internal@^1.0.10"
] ]
}, },
"@std/streams@1.0.10": { "@std/streams@1.0.10": {
@ -131,13 +132,13 @@
"dependencies": [ "dependencies": [
"jsr:@andyburke/fsdb@^1.0.2", "jsr:@andyburke/fsdb@^1.0.2",
"jsr:@andyburke/lurid@0.2", "jsr:@andyburke/lurid@0.2",
"jsr:@andyburke/serverus@~0.12.5", "jsr:@andyburke/serverus@0.13",
"jsr:@da/bcrypt@^1.0.1", "jsr:@da/bcrypt@^1.0.1",
"jsr:@std/assert@^1.0.13", "jsr:@std/assert@^1.0.14",
"jsr:@std/encoding@^1.0.10", "jsr:@std/encoding@^1.0.10",
"jsr:@std/fs@^1.0.19", "jsr:@std/fs@^1.0.19",
"jsr:@std/http@^1.0.20", "jsr:@std/http@^1.0.20",
"jsr:@std/path@^1.1.1" "jsr:@std/path@^1.1.2"
] ]
} }
} }

View file

@ -1,12 +1,11 @@
/* Dark mode default */ /* Dark mode default */
:root { :root {
--base-color: #fa0; --base-color: #fa0;
--bg: hsl(from var(--base-color) h 20% 7.5%); --bg: hsl(from var(--base-color) h 20% 7.5%);
--text: hsl(from var(--base-color) h 5% 100% ); --text: hsl(from var(--base-color) h 5% 100%);
--accent: hsl(from var(--base-color) h clamp( 0, calc(s + 10), 100 ) clamp( 0, calc(l + 20), 100 ) ); --accent: hsl(from var(--base-color) h clamp(0, calc(s + 10), 100) clamp(0, calc(l + 20), 100));
--border-subtle: hsl(from var(--base-color) h 50% 25%); --border-subtle: hsl(from var(--base-color) h 50% 25%);
--border-normal: hsl(from var(--base-color) h 50% 50%); --border-normal: hsl(from var(--base-color) h 50% 50%);
@ -20,8 +19,8 @@
:root { :root {
--bg: hsl(from var(--base-color) h 20% 95%); --bg: hsl(from var(--base-color) h 20% 95%);
--text: hsl(from var(--base-color) h 5% 5% ); --text: hsl(from var(--base-color) h 5% 5%);
--accent: hsl(from var(--base-color) h calc(s + 10) calc(l + 20) ); --accent: hsl(from var(--base-color) h calc(s + 10) calc(l + 20));
--border-subtle: hsl(from var(--base-color) h 50% 90%); --border-subtle: hsl(from var(--base-color) h 50% 90%);
--border-normal: hsl(from var(--base-color) h 50% 50%); --border-normal: hsl(from var(--base-color) h 50% 50%);
@ -121,7 +120,20 @@ body {
background-color: var(--bg); background-color: var(--bg);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; // fixed height? height: 100vh; /* fixed height? */
}
input[type="text"]:focus,
input[type="textarea"]:focus,
textarea:focus {
border-color: rgb(from var(--border-highlight) r g b / 60%);
outline: 0;
-webkit-box-shadow:
inset 0 1px 1px rgba(0, 0, 0, 0.075),
0 0 2px rgb(from var(--border-highlight) r g b / 60%);
box-shadow:
inset 0 1px 1px rgba(0, 0, 0, 0.075),
0 0 2px rgb(from var(--border-highlight) r g b / 60%);
} }
.clickable { .clickable {
@ -134,6 +146,26 @@ body {
border-right: 4px solid #444; border-right: 4px solid #444;
} }
.mockup {
position: relative;
}
.mockup::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
-55deg,
rgba(0, 0, 0, 0.25) 0px,
rgba(0, 0, 0, 0.25) 20px,
rgba(255, 177, 1, 0.25) 20px,
rgba(255, 177, 1, 0.25) 40px
);
}
form div { form div {
position: relative; position: relative;
display: flex; display: flex;
@ -164,7 +196,7 @@ form input:valid ~ label {
form input { form input {
width: 100%; width: 100%;
padding: 20px; padding: 20px;
border: 1px solid var(--text); border: 1px solid rgb(from var(--text) r g b / 60%);
font-size: 20px; font-size: 20px;
background-color: var(--bg); background-color: var(--bg);
color: var(--text); color: var(--text);
@ -288,7 +320,11 @@ body[data-perms*="topics.create"] [data-requires-permission="topics.create"] {
max-width: 800px; max-width: 800px;
} }
.audio-container .audio-controls-container .progress-container .slider-container input[name="progress"] { .audio-container
.audio-controls-container
.progress-container
.slider-container
input[name="progress"] {
width: 100%; width: 100%;
} }
@ -320,56 +356,58 @@ body[data-perms*="topics.create"] [data-requires-permission="topics.create"] {
} }
.audio-container .audio-controls-container input[type="range"] { .audio-container .audio-controls-container input[type="range"] {
--c: var(--accent); /* active color */ --c: var(--accent); /* active color */
--g: 4px; /* the gap */ --g: 4px; /* the gap */
--l: 2px; /* line thickness*/ --l: 2px; /* line thickness*/
--s: 15px; /* thumb size*/ --s: 15px; /* thumb size*/
width: 100%; width: 100%;
height: var(--s); /* needed for Firefox*/ height: var(--s); /* needed for Firefox*/
--_c: color-mix(in srgb, var(--c), #000 var(--p,0%)); --_c: color-mix(in srgb, var(--c), #000 var(--p, 0%));
-webkit-appearance :none; -webkit-appearance: none;
-moz-appearance :none; -moz-appearance: none;
appearance :none; appearance: none;
background: none; background: none;
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
} }
.audio-container .audio-controls-container input[type="range"]:focus-visible, .audio-container .audio-controls-container input[type="range"]:focus-visible,
.audio-container .audio-controls-container input[type="range"]:hover { .audio-container .audio-controls-container input[type="range"]:hover {
--p: 25%; --p: 25%;
} }
.audio-container .audio-controls-container input[type="range"]:active, .audio-container .audio-controls-container input[type="range"]:active,
.audio-container .audio-controls-container input[type="range"]:focus-visible{ .audio-container .audio-controls-container input[type="range"]:focus-visible {
--_b: var(--s) --_b: var(--s);
} }
/* chromium */ /* chromium */
.audio-container .audio-controls-container input[type="range"]::-webkit-slider-thumb{ .audio-container .audio-controls-container input[type="range"]::-webkit-slider-thumb {
height: var(--s); height: var(--s);
aspect-ratio: 1; aspect-ratio: 1;
border-radius: 50%; border-radius: 50%;
box-shadow: 0 0 0 var(--_b,var(--l)) inset var(--_c); box-shadow: 0 0 0 var(--_b, var(--l)) inset var(--_c);
border-image: linear-gradient(90deg,var(--_c) 50%,#ababab 0) 0 1/calc(50% - var(--l)/2) 100vw/0 calc(100vw + var(--g)); border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1 / calc(50% - var(--l) / 2)
-webkit-appearance: none; 100vw/0 calc(100vw + var(--g));
appearance: none; -webkit-appearance: none;
transition: .2s; appearance: none;
transition: 0.2s;
} }
/* Firefox */ /* Firefox */
.audio-container .audio-controls-container input[type="range"]::-moz-range-thumb { .audio-container .audio-controls-container input[type="range"]::-moz-range-thumb {
height: var(--s); height: var(--s);
width: var(--s); width: var(--s);
background: none; background: none;
border-radius: 50%; border-radius: 50%;
box-shadow: 0 0 0 var(--_b,var(--l)) inset var(--_c); box-shadow: 0 0 0 var(--_b, var(--l)) inset var(--_c);
border-image: linear-gradient(90deg,var(--_c) 50%,#ababab 0) 0 1/calc(50% - var(--l)/2) 100vw/0 calc(100vw + var(--g)); border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1 / calc(50% - var(--l) / 2)
-moz-appearance: none; 100vw/0 calc(100vw + var(--g));
appearance: none; -moz-appearance: none;
transition: .2s; appearance: none;
transition: 0.2s;
} }
@supports not (color: color-mix(in srgb,red,red)) { @supports not (color: color-mix(in srgb, red, red)) {
.audio-container .audio-controls-container input[type="range"] { .audio-container .audio-controls-container input[type="range"] {
--_c: var(--c); --_c: var(--c);
} }
} }
.audio-container .audio-controls-container .volume label[for="volume"] { .audio-container .audio-controls-container .volume label[for="volume"] {
@ -399,6 +437,14 @@ body[data-perms*="topics.create"] [data-requires-permission="topics.create"] {
display: block; display: block;
} }
.html-from-markdown {
padding: 2em;
}
.inline {
display: inline-block !important;
}
/* ICONS */ /* ICONS */
.icon { .icon {
width: 24px; width: 24px;
@ -447,7 +493,6 @@ body[data-perms*="topics.create"] [data-requires-permission="topics.create"] {
left: 8px; left: 8px;
} }
/* ICON - ATTACHMENT */ /* ICON - ATTACHMENT */
.icon.attachment { .icon.attachment {
box-sizing: border-box; box-sizing: border-box;
@ -490,6 +535,38 @@ body[data-perms*="topics.create"] [data-requires-permission="topics.create"] {
bottom: 4px; bottom: 4px;
} }
/* ICON - BLURB */
.icon.blurb {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--icon-scale, 1));
border: 2px solid;
border-radius: 3px;
width: 22px;
height: 16px;
}
.icon.blurb::after,
.icon.blurb::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
height: 2px;
border-radius: 3px;
background: currentColor;
bottom: 2px;
}
.icon.blurb::before {
width: 10px;
left: 2px;
box-shadow: 4px -4px 0;
}
.icon.blurb::after {
width: 3px;
right: 2px;
box-shadow: -11px -4px 0;
}
/* ICON - CALENDAR */ /* ICON - CALENDAR */
.icon.calendar, .icon.calendar,
@ -524,13 +601,8 @@ body[data-perms*="topics.create"] [data-requires-permission="topics.create"] {
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--icon-scale, 1)); transform: scale(var(--icon-scale, 1));
width: 20px; width: 14px;
height: 16px; height: 10px;
border: 2px solid;
border-bottom: 0;
box-shadow:
-6px 8px 0 -6px,
6px 8px 0 -6px;
} }
.icon.chat::after, .icon.chat::after,
.icon.chat::before { .icon.chat::before {
@ -538,53 +610,60 @@ body[data-perms*="topics.create"] [data-requires-permission="topics.create"] {
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
width: 8px; border-radius: 3px;
}
.icon.chat::before {
border: 2px solid;
border-top-color: transparent;
border-bottom-left-radius: 20px;
right: 4px;
bottom: -6px;
height: 6px;
}
.icon.chat::after {
height: 2px; height: 2px;
background: currentColor; background: currentColor;
box-shadow: 0 4px 0 0; }
left: 4px; .icon.chat::before {
top: 4px; width: 10px;
opacity: 0.5;
box-shadow: 0 4px 0;
}
.icon.chat::after {
width: 14px;
bottom: 0;
}
/* ICON - CIRCLE */
.icon.circle {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--icon-scale, 1));
width: 19px;
height: 19px;
border: 2px solid;
border-radius: 100px;
} }
/* ICON - CONTROLLER */ /* ICON - CONTROLLER */
.icon.controller { .icon.controller {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--icon-scale, 1)); transform: scale(var(--icon-scale, 1));
width: 8px; width: 8px;
height: 8px; height: 8px;
border: 2px solid; border: 2px solid;
border-radius: 100px; border-radius: 100px;
} }
.icon.controller::before { .icon.controller::before {
content: ""; content: "";
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
width: 14px; width: 14px;
height: 14px; height: 14px;
box-shadow: box-shadow:
-6px -6px 0 -4px, -6px -6px 0 -4px,
6px 6px 0 -4px, 6px 6px 0 -4px,
6px -6px 0 -4px, 6px -6px 0 -4px,
-6px 6px 0 -4px; -6px 6px 0 -4px;
left: -5px; left: -5px;
top: -5px; top: -5px;
transform: rotate(45deg); transform: rotate(45deg);
} }
/* ICON - DOWNLOAD */ /* ICON - DOWNLOAD */
.icon.download { .icon.download {
box-sizing: border-box; box-sizing: border-box;
@ -625,6 +704,42 @@ body[data-perms*="topics.create"] [data-requires-permission="topics.create"] {
bottom: 5px; bottom: 5px;
} }
/* ICON - ESSAY */
.icon.essay {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--icon-scale, 1));
width: 22px;
height: 18px;
border: 2px solid;
border-radius: 3px;
box-shadow: 0 -1px 0;
}
.icon.essay::after,
.icon.essay::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 6px;
top: 2px;
}
.icon.essay::before {
background: currentColor;
left: 2px;
box-shadow:
0 4px 0,
0 8px 0;
border-radius: 3px;
height: 2px;
}
.icon.essay::after {
height: 10px;
border: 2px solid;
right: 2px;
border-radius: 1px;
}
/* ICON - EXCHANGE */ /* ICON - EXCHANGE */
.icon.exchange, .icon.exchange,
@ -657,6 +772,40 @@ body[data-perms*="topics.create"] [data-requires-permission="topics.create"] {
right: -5px; right: -5px;
} }
/* ICON - FORUM */
.icon.forum {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--icon-scale, 1));
width: 16px;
height: 14px;
border-bottom: 2px solid;
}
.icon.forum::after,
.icon.forum::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
top: 2px;
}
.icon.forum::before {
border-left: 4px solid;
left: 1px;
width: 0;
height: 0;
border-top: 3px solid transparent;
border-bottom: 3px solid transparent;
}
.icon.forum::after {
width: 8px;
height: 6px;
border-top: 2px solid;
border-bottom: 2px solid;
right: 0;
}
/* ICON - FORWARD */ /* ICON - FORWARD */
.icon.forward-copy { .icon.forward-copy {
box-sizing: border-box; box-sizing: border-box;
@ -696,10 +845,8 @@ body[data-perms*="topics.create"] [data-requires-permission="topics.create"] {
/* ICON - HOME */ /* ICON - HOME */
.icon.home { .icon.home {
background: background:
linear-gradient(to left, currentColor 5px, transparent 0) no-repeat 0 bottom/4px linear-gradient(to left, currentColor 5px, transparent 0) no-repeat 0 bottom/4px 2px,
2px, linear-gradient(to left, currentColor 5px, transparent 0) no-repeat right bottom/4px 2px;
linear-gradient(to left, currentColor 5px, transparent 0) no-repeat right
bottom/4px 2px;
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
@ -768,8 +915,8 @@ body[data-perms*="topics.create"] [data-requires-permission="topics.create"] {
top: 6px; top: 6px;
left: 8px; left: 8px;
box-shadow: box-shadow:
-5px 0 0, -5px 0 0,
5px 0 0; 5px 0 0;
} }
/* ICON - MOREBORDERLESS */ /* ICON - MOREBORDERLESS */
@ -823,11 +970,10 @@ body[data-perms*="topics.create"] [data-requires-permission="topics.create"] {
top: 8px; top: 8px;
left: 8px; left: 8px;
box-shadow: box-shadow:
-5px 0 0, -5px 0 0,
5px 0 0; 5px 0 0;
} }
/* ICON - PLUS */ /* ICON - PLUS */
.icon.plus, .icon.plus,
.icon.plus::after, .icon.plus::after,
@ -996,7 +1142,10 @@ body[data-perms*="topics.create"] [data-requires-permission="topics.create"] {
width: 10px; width: 10px;
height: 12px; height: 12px;
border: 2px solid transparent; border: 2px solid transparent;
box-shadow: 0 0 0 2px, inset -2px 0 0, inset 2px 0 0; box-shadow:
0 0 0 2px,
inset -2px 0 0,
inset 2px 0 0;
border-bottom-left-radius: 1px; border-bottom-left-radius: 1px;
border-bottom-right-radius: 1px; border-bottom-right-radius: 1px;
margin-top: 4px; margin-top: 4px;
@ -1149,175 +1298,172 @@ body[data-perms*="topics.create"] [data-requires-permission="topics.create"] {
left: 11px; left: 11px;
} }
/* AUDIO PLAYER ICONS */ /* AUDIO PLAYER ICONS */
.icon.skip-back { .icon.skip-back {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--icon-scale, 1)); transform: scale(var(--icon-scale, 1));
width: 22px; width: 22px;
height: 22px; height: 22px;
border: 2px solid; border: 2px solid;
border-radius: 4px; border-radius: 4px;
} }
.icon.skip-back::after, .icon.skip-back::after,
.icon.skip-back::before { .icon.skip-back::before {
content: ""; content: "";
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
height: 8px; height: 8px;
top: 5px; top: 5px;
} }
.icon.skip-back::before { .icon.skip-back::before {
width: 2px; width: 2px;
border-radius: 2px; border-radius: 2px;
right: 11px; right: 11px;
background: currentColor; background: currentColor;
} }
.icon.skip-back::after { .icon.skip-back::after {
width: 0; width: 0;
border-top: 4px solid transparent; border-top: 4px solid transparent;
border-bottom: 4px solid transparent; border-bottom: 4px solid transparent;
border-right: 5px solid; border-right: 5px solid;
right: 5px; right: 5px;
} }
.icon.rewind { .icon.rewind {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--icon-scale, 1)); transform: scale(var(--icon-scale, 1));
border: 2px solid; border: 2px solid;
border-radius: 4px; border-radius: 4px;
width: 22px; width: 22px;
height: 22px; height: 22px;
} }
.icon.rewind::after, .icon.rewind::after,
.icon.rewind::before { .icon.rewind::before {
content: ""; content: "";
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
width: 6px; width: 6px;
height: 6px; height: 6px;
border-left: 2px solid; border-left: 2px solid;
border-bottom: 2px solid; border-bottom: 2px solid;
transform: rotate(45deg); transform: rotate(45deg);
top: 6px; top: 6px;
left: 5px; left: 5px;
} }
.icon.rewind::after { .icon.rewind::after {
left: 9px; left: 9px;
} }
.icon.play { .icon.play {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--icon-scale, 1)); transform: scale(var(--icon-scale, 1));
width: 22px; width: 22px;
height: 22px; height: 22px;
border: 2px solid; border: 2px solid;
border-radius: 4px; border-radius: 4px;
} }
.icon.play::before { .icon.play::before {
content: ""; content: "";
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
width: 0; width: 0;
height: 10px; height: 10px;
border-top: 5px solid transparent; border-top: 5px solid transparent;
border-bottom: 5px solid transparent; border-bottom: 5px solid transparent;
border-left: 6px solid; border-left: 6px solid;
top: 4px; top: 4px;
left: 7px; left: 7px;
} }
.icon.pause { .icon.pause {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--icon-scale, 1)); transform: scale(var(--icon-scale, 1));
width: 22px; width: 22px;
height: 22px; height: 22px;
border: 2px solid; border: 2px solid;
border-radius: 4px; border-radius: 4px;
} }
.icon.pause::before { .icon.pause::before {
content: ""; content: "";
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
width: 6px; width: 6px;
height: 6px; height: 6px;
left: 6px; left: 6px;
top: 6px; top: 6px;
border-left: 2px solid; border-left: 2px solid;
border-right: 2px solid; border-right: 2px solid;
} }
.icon.fastforward { .icon.fastforward {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--icon-scale, 1)); transform: scale(var(--icon-scale, 1));
border: 2px solid; border: 2px solid;
border-radius: 4px; border-radius: 4px;
width: 22px; width: 22px;
height: 22px; height: 22px;
} }
.icon.fastforward::after, .icon.fastforward::after,
.icon.fastforward::before { .icon.fastforward::before {
content: ""; content: "";
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
width: 6px; width: 6px;
height: 6px; height: 6px;
border-right: 2px solid; border-right: 2px solid;
border-top: 2px solid; border-top: 2px solid;
transform: rotate(45deg); transform: rotate(45deg);
top: 6px; top: 6px;
right: 5px; right: 5px;
} }
.icon.fastforward::after { .icon.fastforward::after {
right: 9px; right: 9px;
} }
.icon.skip-forward { .icon.skip-forward {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--icon-scale, 1)); transform: scale(var(--icon-scale, 1));
width: 22px; width: 22px;
height: 22px; height: 22px;
border: 2px solid; border: 2px solid;
border-radius: 4px; border-radius: 4px;
} }
.icon.skip-forward::after, .icon.skip-forward::after,
.icon.skip-forward::before { .icon.skip-forward::before {
content: ""; content: "";
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
height: 8px; height: 8px;
top: 5px; top: 5px;
} }
.icon.skip-forward::before { .icon.skip-forward::before {
width: 2px; width: 2px;
border-radius: 2px; border-radius: 2px;
left: 11px; left: 11px;
background: currentColor; background: currentColor;
} }
.icon.skip-forward::after { .icon.skip-forward::after {
width: 0; width: 0;
border-top: 4px solid transparent; border-top: 4px solid transparent;
border-bottom: 4px solid transparent; border-bottom: 4px solid transparent;
border-left: 5px solid; border-left: 5px solid;
left: 5px; left: 5px;
} }

View file

@ -21,6 +21,18 @@ document.addEventListener("DOMContentLoaded", () => {
const form_data = new FormData(form); const form_data = new FormData(form);
for (const [key, value] of form_data.entries()) { for (const [key, value] of form_data.entries()) {
if (key.length === 0) {
continue;
}
if (typeof value === "string" && value.length === 0) {
const input = form.querySelector(`[name="${key}"]`);
const should_submit_empty = input && input.dataset["smartformsSubmitEmpty"];
if (!should_submit_empty) {
continue;
}
}
const elements = key.split("."); const elements = key.split(".");
let current = json; let current = json;
for (const element of elements.slice(0, elements.length - 1)) { for (const element of elements.slice(0, elements.length - 1)) {

View file

@ -129,63 +129,244 @@
<label id="sidebar-toggle-icon" for="sidebar-toggle"> <label id="sidebar-toggle-icon" for="sidebar-toggle">
<div class="icon right"></div> <div class="icon right"></div>
</label> </label>
<div>
<span class="title">topics</span>
</div>
<ul id="topic-list" class="topic-list"></ul>
<div id="topic-creation-container" data-requires-permission="topics.create">
<button
id="toggle-topic-creation-form-button"
onclick="((event) => {
event.preventDefault();
const topic_create_form = document.getElementById( 'topic-create' );
topic_create_form.style[ 'height' ] = topic_create_form.style[ 'height' ] === '5rem' ? '0' : '5rem';
})(event)"
>
<div class="icon plus"></div>
</button>
<form
id="topic-create"
data-smart="true"
action="/api/topics"
method="POST"
style="
margin-top: 1rem;
width: 100%;
overflow: hidden;
height: 0;
overflow: hidden;
transition: all 0.5s;
"
>
<input
id="new-topic-name-input"
type="text"
name="name"
value=""
placeholder="new topic"
/>
<input type="submit" hidden /> <script>
const DEFAULT_AVATAR_URL = `//${window.location.host}/images/default_avatar.gif`;
new MutationObserver((mutations, observer) => {
mutations.forEach((mutation) => {
const user_json = document.body.dataset.user;
const user = user_json
? JSON.parse(user_json)
: {
username: "",
meta: {
avatar: DEFAULT_AVATAR_URL,
},
};
const ids = document.querySelectorAll("[data-bind__user_id]");
for (const id of ids) {
const bound_to =
typeof id.dataset["bind__user_id"] === "string" &&
id.dataset["bind__user_id"].length > 0
? id.dataset["bind__user_id"]
: "innerHTML";
avatar[bound_to] = user.id ?? "<unknown>";
}
const avatars = document.querySelectorAll("[data-bind__user_meta_avatar]");
for (const avatar of avatars) {
const bound_to =
typeof avatar.dataset["bind__user_meta_avatar"] === "string" &&
avatar.dataset["bind__user_meta_avatar"].length
? avatar.dataset["bind__user_meta_avatar"]
: "innerHTML";
avatar[bound_to] = user.meta?.avatar ?? DEFAULT_AVATAR_URL;
}
const usernames = document.querySelectorAll("[data-bind__user_username]");
for (const username of usernames) {
const bound_to =
typeof username.dataset["bind__user_username"] === "string" &&
username.dataset["bind__user_username"].length > 0
? username.dataset["bind__user_username"]
: "innerHTML";
username[bound_to] = user.username;
}
});
}).observe(document.body, {
attributes: true,
attributeFilter: ["data-user"],
});
</script>
<style type="text/css">
.profile-container {
margin: 1rem auto;
max-width: 1024px;
padding: 1rem;
}
.profile-container .avatar-container {
position: relative;
width: 100%;
max-width: 200px;
}
.profile-container .avatar-container #user-avatar {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
.profile-container .avatar-container input[type="file"] {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
cursor: pointer;
}
</style>
<div class="profile-container">
<div class="avatar-container">
<img
id="user-avatar"
src="/images/default_avatar.gif"
alt="User Avatar"
data-bind__user_meta_avatar="src"
/>
<input type="file" accept="image/*" name="avatar" />
<script>
const avatar_file_input = document.querySelector('input[name="avatar"]');
avatar_file_input.addEventListener("change", async (event) => {
const user_json = document.body.dataset.user;
if (!user_json) {
return alert("You must be logged in.");
}
const user = JSON.parse(user_json);
const avatar = avatar_file_input.files[0];
if (!avatar || !avatar.type || !avatar.type.includes("image")) {
return alert("You must select a valid image to upload as your avatar.");
}
// TODO: actually enforce this on the upload in serverus somehow
if (avatar.size > 512_000) {
return alert("512K is the largest allowed avatar size.");
}
const body = new FormData();
body.append("file", avatar, encodeURIComponent(avatar.name));
const avatar_path = `/files/users/${user.id}/avatars/${encodeURIComponent(avatar.name)}`;
const avatar_upload_response = await api.fetch(avatar_path, {
method: "PUT",
body,
});
if (!avatar_upload_response.ok) {
const error = await avatar_upload_response.json();
return alert(error?.error?.message ?? "Unknown error.");
}
const updated_user = { ...user };
updated_user.meta = updated_user.meta ?? {};
updated_user.meta.avatar = `//${window.location.host}${avatar_path}`;
const saved_user_response = await api.fetch(`/api/users/${user.id}`, {
method: "PUT",
json: updated_user,
});
if (!saved_user_response.ok) {
const error = await avatar_upload_response.json();
return alert(error?.error?.message ?? "Unknown error.");
}
const saved_user = await saved_user_response.json();
document.body.dataset.user = JSON.stringify(saved_user);
document.body.dataset.perms = saved_user.permissions.join(":");
});
</script>
</div>
<div class="username-container">
<span class="username" data-bind__user_username></span>
</div>
<div class="notifications-settings-container">
<button class="mockup" onclick="NOTIFICATIONS.request_permission()">
Enable Notifications
</button>
</div>
<form data-smart="true" data-method="DELETE" action="/api/auth">
<script> <script>
{ {
const form = document.currentScript.closest("form"); const form = document.currentScript.closest("form");
const topic_create_form = document.getElementById("topic-create"); form.on_reply = (response) => {
const new_topic_name_input = document.getElementById("new-topic-name-input"); if (!response.deleted) {
alert("error logging out? please reload.");
return;
}
form.on_reply = (new_topic) => { delete document.body.dataset.user;
const topic_list = document.getElementById("topic-list"); delete document.body.dataset.perms;
topic_list.insertAdjacentHTML( window.location = "/";
"beforeend",
`<li id="topic-selector-${new_topic.id}" class="topic"><a href="#/topic/${new_topic.id}">${new_topic.name}</a></li>`,
);
new_topic_name_input.value = "";
window.location.hash = `/topic/${new_topic.id}`;
topic_create_form.style["height"] = "0";
}; };
} }
</script> </script>
<button class="primary">Log Out</button>
</form> </form>
</div> </div>
<div class="topics-container">
<div>
<span class="title">topics</span>
</div>
<ul id="topic-list" class="topic-list"></ul>
<div id="topic-creation-container" data-requires-permission="topics.create">
<button
id="toggle-topic-creation-form-button"
onclick="((event) => {
event.preventDefault();
const topic_create_form = document.getElementById( 'topic-create' );
topic_create_form.style[ 'height' ] = topic_create_form.style[ 'height' ] === '5rem' ? '0' : '5rem';
})(event)"
>
<div class="icon plus"></div>
</button>
<form
id="topic-create"
data-smart="true"
action="/api/topics"
method="POST"
style="
margin-top: 1rem;
width: 100%;
overflow: hidden;
height: 0;
overflow: hidden;
transition: all 0.5s;
"
>
<input
id="new-topic-name-input"
type="text"
name="name"
value=""
placeholder="new topic"
/>
<input type="submit" hidden />
<script>
{
const form = document.currentScript.closest("form");
const topic_create_form = document.getElementById("topic-create");
const new_topic_name_input =
document.getElementById("new-topic-name-input");
form.on_reply = (new_topic) => {
const topic_list = document.getElementById("topic-list");
topic_list.insertAdjacentHTML(
"beforeend",
`<li id="topic-selector-${new_topic.id}" class="topic"><a href="#/topic/${new_topic.id}">${new_topic.name}</a></li>`,
);
new_topic_name_input.value = "";
window.location.hash = `/topic/${new_topic.id}`;
topic_create_form.style["height"] = "0";
};
}
</script>
</form>
</div>
</div>
</div> </div>

View file

@ -0,0 +1,3 @@
# Blurbs
Blurbs are going to be short (128? 256? character thoughts)

View file

@ -0,0 +1,16 @@
<div id="blurb" class="tab">
<input
type="radio"
name="top-level-tabs"
id="blurb-tab-input"
class="tab-switch"
data-view="blurb"
/>
<label for="blurb-tab-input" class="tab-label mockup"
><div class="icon blurb"></div>
<div class="label">Blurbs</div></label
>
<div class="tab-content">
<!-- #include file="./README.md" -->
</div>
</div>

View file

@ -1,3 +1,3 @@
# calendar # Calendar
The calendar should help people coordinate events. The calendar should help people coordinate events around a topic.

View file

@ -6,7 +6,7 @@
class="tab-switch" class="tab-switch"
data-view="calendar" data-view="calendar"
/> />
<label for="calendar-tab-input" class="tab-label" <label for="calendar-tab-input" class="tab-label mockup"
><div class="icon calendar"></div> ><div class="icon calendar"></div>
<div class="label">Calendar</div></label <div class="label">Calendar</div></label
> >

View file

@ -11,7 +11,12 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 5rem; bottom: 5rem;
padding: 0.5rem; padding: 1.5rem 1.5rem 0.75rem 1.5rem;
}
@media screen and (max-width: 1200px) {
#chat #topic-chat-content {
padding: 0.75rem;
}
} }
#chat #topic-chat-entry-container { #chat #topic-chat-entry-container {
@ -51,7 +56,7 @@
cursor: pointer; cursor: pointer;
align-content: center; align-content: center;
border-radius: var(--border-radius); border-radius: var(--border-radius);
border: 1px solid var(--text); border: 1px solid rgba(128, 128, 128, 0.2);
} }
#chat #topic-chat-entry-container form textarea { #chat #topic-chat-entry-container form textarea {
@ -60,6 +65,10 @@
background: inherit; background: inherit;
color: inherit; color: inherit;
border-radius: var(--border-radius); border-radius: var(--border-radius);
border: 1px solid rgba(128, 128, 128, 0.2);
resize: none;
background: rgba(0, 0, 0, 0.1);
padding: 0.4rem;
} }
#chat .message-container { #chat .message-container {
@ -67,7 +76,7 @@
transition: all 0.33s; transition: all 0.33s;
background: rgba(255, 255, 255, 0.03); background: rgba(255, 255, 255, 0.03);
margin-top: 0.75rem; margin-top: 0.75rem;
padding: 2px; padding: 0.5rem;
border-radius: var(--border-radius); border-radius: var(--border-radius);
} }
@ -137,6 +146,7 @@
padding: 0.25rem 0 0 0; padding: 0.25rem 0 0 0;
text-align: center; text-align: center;
text-wrap: nowrap; text-wrap: nowrap;
background: none;
} }
#chat .message-container .message-actions-container label { #chat .message-container .message-actions-container label {

View file

@ -26,7 +26,6 @@
id="file-upload-and-share-input" id="file-upload-and-share-input"
aria-label="Upload and share file" aria-label="Upload and share file"
type="file" type="file"
name="file-upload-and-share"
multiple multiple
/> />
<label for="file-upload-and-share-input"> <label for="file-upload-and-share-input">
@ -36,7 +35,7 @@
id="topic-chat-input" id="topic-chat-input"
class="topic-chat-input" class="topic-chat-input"
rows="1" rows="1"
name="data.message" name="data.content"
></textarea> ></textarea>
<button id="topic-chat-send" class="primary" aria-label="Send a message"> <button id="topic-chat-send" class="primary" aria-label="Send a message">
<i class="icon send"></i> <i class="icon send"></i>
@ -44,9 +43,7 @@
<script> <script>
{ {
const form = document.currentScript.closest("form"); const form = document.currentScript.closest("form");
const file_input = document.querySelector( const file_input = document.querySelector('input[type="file"]');
'input[name="file-upload-and-share"]',
);
const chat_input = document.getElementById("topic-chat-input"); const chat_input = document.getElementById("topic-chat-input");
const parent_id_input = document.getElementById("parent-id"); const parent_id_input = document.getElementById("parent-id");
const topic_chat_container = const topic_chat_container =
@ -64,7 +61,7 @@
form.on_submit = async (event) => { form.on_submit = async (event) => {
const user = JSON.parse(document.body.dataset.user); const user = JSON.parse(document.body.dataset.user);
const topic_id = topic_chat_container.dataset.topic_id; const topic_id = document.body.dataset.topic;
if (!topic_id) { if (!topic_id) {
alert("Failed to get topic_id!"); alert("Failed to get topic_id!");
return false; return false;
@ -123,10 +120,10 @@
if (form.uploaded_urls.length) { if (form.uploaded_urls.length) {
json.data = json.data ?? {}; json.data = json.data ?? {};
json.data.message = json.data.content =
(typeof json.data.message === "string" && (typeof json.data.content === "string" &&
json.data.message.trim().length json.data.content.trim().length
? json.data.message.trim() + "\n" ? json.data.content.trim() + "\n"
: "") + form.uploaded_urls.join("\n"); : "") + form.uploaded_urls.join("\n");
} }

View file

@ -282,10 +282,10 @@ function render_text_event(topic_chat_content, event, creator, existing_element)
<div class="icon more-borderless"></div> <div class="icon more-borderless"></div>
</label> </label>
<button class="message-action" data-action="react"><i class="icon more-circle"></i><span class="action-name">React</span></button> <button class="message-action mockup" data-action="react"><i class="icon circle"></i><span class="action-name">React</span></button>
<button class="message-action" data-action="reply" onclick="document.getElementById( 'parent-id' ).value = '${message_id}';"><i class="icon reply"></i><span class="action-name">Reply</span></button> <button class="message-action" data-action="reply" onclick="document.getElementById( 'parent-id' ).value = '${message_id}';"><i class="icon reply"></i><span class="action-name">Reply</span></button>
<button class="message-action" data-action="forward_copy"><i class="icon forward-copy"></i><span class="action-name">Copy Link</span></button> <button class="message-action mockup" data-action="forward_copy"><i class="icon forward-copy"></i><span class="action-name">Copy Link</span></button>
<button class="message-action" data-action="delete"><i class="icon trash"></i><span class="action-name">Delete</span></button> <button class="message-action mockup" data-action="delete"><i class="icon trash"></i><span class="action-name">Delete</span></button>
</div> </div>
<div class="info-container"> <div class="info-container">
<div class="avatar-container"> <div class="avatar-container">
@ -299,7 +299,7 @@ function render_text_event(topic_chat_content, event, creator, existing_element)
<span class="short">${event_datetime.short}</span> <span class="short">${event_datetime.short}</span>
</div> </div>
</div> </div>
<div class="message-content-container">${message_text_to_html(event.data.message)}</div> <div class="message-content-container">${message_text_to_html(event.data.content)}</div>
</div>`; </div>`;
if (existing_element) { if (existing_element) {

View file

@ -0,0 +1,3 @@
# Essays
Essays are long-form, authored posts intended to be effectively, a broadcast.

View file

@ -0,0 +1,16 @@
<div id="essays" class="tab">
<input
type="radio"
name="top-level-tabs"
id="essays-tab-input"
class="tab-switch"
data-view="essays"
/>
<label for="essays-tab-input" class="tab-label mockup"
><div class="icon essay"></div>
<div class="label">Essays</div></label
>
<div class="tab-content">
<!-- #include file="./README.md" -->
</div>
</div>

View file

@ -0,0 +1,3 @@
# Forum
Forums are for more thoughtful and long form discussion.

View file

@ -0,0 +1,23 @@
<style>
.forum-container {
padding: 2rem;
}
</style>
<div id="forum" class="tab">
<input
type="radio"
name="top-level-tabs"
id="forum-tab-input"
class="tab-switch"
data-view="forum"
/>
<label for="forum-tab-input" class="tab-label mockup"
><div class="icon forum"></div>
<div class="label">Forum</div></label
>
<div class="tab-content forum-container">
<button>
<i class="icon plus" style="display: inline-block; margin-right: 1rem"></i>New Thread
</button>
</div>
</div>

View file

@ -6,7 +6,7 @@
class="tab-switch" class="tab-switch"
data-view="resources" data-view="resources"
/> />
<label for="resources-tab-input" class="tab-label" <label for="resources-tab-input" class="tab-label mockup"
><div class="icon resources"></div> ><div class="icon resources"></div>
<div class="label">Resources</div></label <div class="label">Resources</div></label
> >

View file

@ -122,7 +122,9 @@
<div class="tabs"> <div class="tabs">
<!-- #include file="./chat/chat.html" --> <!-- #include file="./chat/chat.html" -->
<!-- #include file="./blurbs/blurbs.html" -->
<!-- #include file="./forum/forum.html" -->
<!-- #include file="./essays/essays.html" -->
<!-- #include file="./resources/resources.html" --> <!-- #include file="./resources/resources.html" -->
<!-- #include file="./calendar/calendar.html" --> <!-- #include file="./calendar/calendar.html" -->
<!-- #include file="./profile/profile.html" -->
</div> </div>