Compare commits
36 Commits
b0fd2a4f0c
...
main
Author | SHA1 | Date | |
---|---|---|---|
6cfc862d63
|
|||
70646ba381
|
|||
f04f0fb51b
|
|||
317182ae12
|
|||
751be27b52
|
|||
6dd9f5bf65
|
|||
1f80ed7ca5
|
|||
89817340c9
|
|||
fc80823713
|
|||
184472726e
|
|||
68cf5f7d57
|
|||
4ef7b0ba1e
|
|||
aaeb3a524b
|
|||
f1f62fa2c8
|
|||
8c917f6ae2
|
|||
4f88d14b45
|
|||
9238385244
|
|||
cf89070639
|
|||
4a8f87d64a
|
|||
2b1f52a99d
|
|||
d0b702e1e8
|
|||
14b96bf37e
|
|||
cf4bf3caa3
|
|||
382080ceaa
|
|||
304a862931
|
|||
348b782350
|
|||
aec4724e2f
|
|||
53d39d5a36
|
|||
05bd034b23
|
|||
033df03c49
|
|||
a0c86f33b4
|
|||
712782bc1c
|
|||
1c80777fe4
|
|||
4c2877403d
|
|||
cf2d605077
|
|||
c68ead85c0
|
@ -1,11 +1,11 @@
|
||||
FROM python:3.11-slim
|
||||
FROM python:3.13-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
nginx \
|
||||
uwsgi \
|
||||
uwsgi-plugin-python3 \
|
||||
sqlite3 \
|
||||
libargon2-0 \
|
||||
libargon2-1 \
|
||||
imagemagick \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
@ -25,7 +25,7 @@ Designers: Paul James Miller
|
||||
|
||||
## ICONCINO
|
||||
|
||||
Affected files: [`data/static/misc/error.svg`](./data/static/misc/error.svg) [`data/static/misc/image.svg`](./data/static/misc/image.svg) [`data/static/misc/info.svg`](./data/static/misc/info.svg) [`data/static/misc/lock.svg`](./data/static/misc/lock.svg) [`data/static/misc/sticky.svg`](./data/static/misc/sticky.svg) [`data/static/misc/warn.svg`](./data/static/misc/warn.svg)
|
||||
Affected files: [`data/static/misc/error.svg`](./data/static/misc/error.svg) [`data/static/misc/image.svg`](./data/static/misc/image.svg) [`data/static/misc/info.svg`](./data/static/misc/info.svg) [`data/static/misc/lock.svg`](./data/static/misc/lock.svg) [`data/static/misc/spoiler.svg`](./data/static/misc/spoiler.svg) [`data/static/misc/sticky.svg`](./data/static/misc/sticky.svg) [`data/static/misc/warn.svg`](./data/static/misc/warn.svg)
|
||||
URL: https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license
|
||||
Copyright: Gabriele Malaspina
|
||||
Designers: Gabriele Malaspina
|
||||
|
@ -1,18 +1,20 @@
|
||||
from flask import Flask, session
|
||||
from dotenv import load_dotenv
|
||||
from .models import Avatars, Users
|
||||
from .models import Avatars, Users, PostHistory, Posts
|
||||
from .auth import digest
|
||||
from .routes.users import is_logged_in, get_active_user
|
||||
from .routes.users import is_logged_in, get_active_user, get_prefers_theme
|
||||
from .routes.threads import get_post_url
|
||||
from .constants import (
|
||||
PermissionLevel, permission_level_string,
|
||||
InfoboxKind, InfoboxIcons, InfoboxHTMLClass,
|
||||
REACTION_EMOJI,
|
||||
)
|
||||
from .lib.babycode import babycode_to_html, EMOJI
|
||||
from .lib.babycode import babycode_to_html, EMOJI, BABYCODE_VERSION
|
||||
from datetime import datetime
|
||||
import os
|
||||
import time
|
||||
import secrets
|
||||
import tomllib
|
||||
|
||||
def create_default_avatar():
|
||||
if Avatars.count() == 0:
|
||||
@ -46,8 +48,26 @@ def create_deleted_user():
|
||||
"permission": PermissionLevel.SYSTEM.value,
|
||||
})
|
||||
|
||||
def reparse_posts():
|
||||
from .db import db
|
||||
post_histories = PostHistory.findall([
|
||||
('markup_language', '=', 'babycode'),
|
||||
('format_version', 'IS NOT', BABYCODE_VERSION)
|
||||
])
|
||||
if len(post_histories) == 0:
|
||||
return
|
||||
print('Re-parsing babycode, this may take a while...')
|
||||
with db.transaction():
|
||||
for ph in post_histories:
|
||||
ph.update({
|
||||
'content': babycode_to_html(ph['original_markup']),
|
||||
'format_version': BABYCODE_VERSION,
|
||||
})
|
||||
print('Re-parsing done.')
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
app.config.from_file('../config/pyrom_config.toml', load=tomllib.load, text=False)
|
||||
|
||||
if os.getenv("PYROM_PROD") is None:
|
||||
app.static_folder = os.path.join(os.path.dirname(__file__), "../data/static")
|
||||
@ -63,6 +83,18 @@ def create_app():
|
||||
app.config['MAX_CONTENT_LENGTH'] = 1000 * 1000
|
||||
|
||||
os.makedirs(os.path.dirname(app.config["DB_PATH"]), exist_ok = True)
|
||||
|
||||
css_dir = 'data/static/css/'
|
||||
allowed_themes = []
|
||||
for f in os.listdir(css_dir):
|
||||
if not os.path.isfile(os.path.join(css_dir, f)):
|
||||
continue
|
||||
theme_name = os.path.splitext(os.path.basename(f))[0]
|
||||
allowed_themes.append(theme_name)
|
||||
|
||||
allowed_themes.sort(key=(lambda x: (x != 'style', x)))
|
||||
app.config['allowed_themes'] = allowed_themes
|
||||
|
||||
with app.app_context():
|
||||
from .schema import create as create_tables
|
||||
from .migrations import run_migrations
|
||||
@ -73,6 +105,8 @@ def create_app():
|
||||
create_admin()
|
||||
create_deleted_user()
|
||||
|
||||
reparse_posts()
|
||||
|
||||
from app.routes.app import bp as app_bp
|
||||
from app.routes.topics import bp as topics_bp
|
||||
from app.routes.threads import bp as threads_bp
|
||||
@ -114,6 +148,13 @@ def create_app():
|
||||
def inject_auth():
|
||||
return {"is_logged_in": is_logged_in, "get_active_user": get_active_user, "active_user": get_active_user()}
|
||||
|
||||
@app.context_processor
|
||||
def inject_funcs():
|
||||
return {
|
||||
'get_post_url': get_post_url,
|
||||
'get_prefers_theme': get_prefers_theme,
|
||||
}
|
||||
|
||||
@app.template_filter("ts_datetime")
|
||||
def ts_datetime(ts, format):
|
||||
return datetime.utcfromtimestamp(ts or int(time.time())).strftime(format)
|
||||
@ -150,4 +191,11 @@ def create_app():
|
||||
def cachebust(subject):
|
||||
return f"{subject}?v={str(int(time.time()))}"
|
||||
|
||||
@app.template_filter('theme_name')
|
||||
def get_theme_name(subject: str):
|
||||
if subject == 'style':
|
||||
return 'Default'
|
||||
|
||||
return f'{subject.removeprefix('theme-').capitalize()} (beta)'
|
||||
|
||||
return app
|
||||
|
15
app/db.py
15
app/db.py
@ -55,7 +55,7 @@ class DB:
|
||||
|
||||
def insert(self, table, columns, *values):
|
||||
if isinstance(columns, (list, tuple)):
|
||||
columns = ", ".join(columns)
|
||||
columns = ", ".join([f'"{column}"' for column in columns])
|
||||
|
||||
placeholders = ", ".join(["?"] * len(values))
|
||||
sql = f"""
|
||||
@ -201,6 +201,19 @@ class Model:
|
||||
return instance
|
||||
|
||||
|
||||
@classmethod
|
||||
def findall(cls, condition, operator='='):
|
||||
rows = db.QueryBuilder(cls.table)\
|
||||
.where(condition, operator)\
|
||||
.all()
|
||||
res = []
|
||||
for row in rows:
|
||||
instance = cls(cls.table)
|
||||
instance._data = dict(row)
|
||||
res.append(instance)
|
||||
return res
|
||||
|
||||
|
||||
@classmethod
|
||||
def create(cls, values):
|
||||
if not values:
|
||||
|
@ -1,7 +1,9 @@
|
||||
from .babycode_parser import Parser
|
||||
from markupsafe import escape
|
||||
from markupsafe import Markup, escape
|
||||
import re
|
||||
|
||||
BABYCODE_VERSION = 3
|
||||
|
||||
NAMED_COLORS = [
|
||||
'black', 'silver', 'gray', 'white', 'maroon', 'red',
|
||||
'purple', 'fuchsia', 'green', 'lime', 'olive', 'yellow',
|
||||
@ -33,7 +35,23 @@ NAMED_COLORS = [
|
||||
'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen',
|
||||
]
|
||||
|
||||
def tag_code(children, attr):
|
||||
def is_tag(e, tag=None):
|
||||
if e is None:
|
||||
return False
|
||||
if isinstance(e, str):
|
||||
return False
|
||||
if e['type'] != 'bbcode':
|
||||
return False
|
||||
|
||||
if tag is None:
|
||||
return True
|
||||
|
||||
return e['name'] == tag
|
||||
|
||||
def is_text(e):
|
||||
return isinstance(e, str)
|
||||
|
||||
def tag_code(children, attr, surrounding):
|
||||
is_inline = children.find('\n') == -1
|
||||
if is_inline:
|
||||
return f"<code class=\"inline-code\">{children}</code>"
|
||||
@ -47,7 +65,7 @@ def tag_list(children):
|
||||
list_body = re.sub(r"\n\n+", "\1", list_body)
|
||||
return " ".join([f"<li>{x}</li>" for x in list_body.split("\1") if x])
|
||||
|
||||
def tag_color(children, attr):
|
||||
def tag_color(children, attr, surrounding):
|
||||
hex_re = r"^#?([0-9a-f]{6}|[0-9a-f]{3})$"
|
||||
potential_color = attr.lower().strip()
|
||||
|
||||
@ -61,25 +79,48 @@ def tag_color(children, attr):
|
||||
# return just the way it was if we can't parse it
|
||||
return f"[color={attr}]{children}[/color]"
|
||||
|
||||
def tag_spoiler(children, attr, surrounding):
|
||||
spoiler_name = attr if attr else "Spoiler"
|
||||
content = f"<div class='accordion-content post-accordion-content hidden'>{children}</div>"
|
||||
container = f"""<div class='accordion hidden'><div class='accordion-header'><button type='button' class='accordion-toggle'>+</button><span>{spoiler_name}</span></div>{content}</div>"""
|
||||
return container
|
||||
|
||||
def tag_image(children, attr, surrounding):
|
||||
img = f"<img class=\"post-image\" src=\"{attr}\" alt=\"{children}\">"
|
||||
if not is_tag(surrounding[0], 'img'):
|
||||
img = f"<div class=post-img-container>{img}"
|
||||
if not is_tag(surrounding[1], 'img'):
|
||||
img = f"{img}</div>"
|
||||
return img
|
||||
|
||||
TAGS = {
|
||||
"b": lambda children, attr: f"<strong>{children}</strong>",
|
||||
"i": lambda children, attr: f"<em>{children}</em>",
|
||||
"s": lambda children, attr: f"<del>{children}</del>",
|
||||
"u": lambda children, attr: f"<u>{children}</u>",
|
||||
"b": lambda children, attr, _: f"<strong>{children}</strong>",
|
||||
"i": lambda children, attr, _: f"<em>{children}</em>",
|
||||
"s": lambda children, attr, _: f"<del>{children}</del>",
|
||||
"u": lambda children, attr, _: f"<u>{children}</u>",
|
||||
|
||||
"img": lambda children, attr: f"<div class=\"post-img-container\"><img class=\"block-img\" src=\"{attr}\" alt=\"{children}\"></div>",
|
||||
"url": lambda children, attr: f"<a href={attr}>{children}</a>",
|
||||
"quote": lambda children, attr: f"<blockquote>{children}</blockquote>",
|
||||
"img": tag_image,
|
||||
"url": lambda children, attr, _: f"<a href={attr}>{children}</a>",
|
||||
"quote": lambda children, attr, _: f"<blockquote>{children}</blockquote>",
|
||||
"code": tag_code,
|
||||
"ul": lambda children, attr: f"<ul>{tag_list(children)}</ul>",
|
||||
"ol": lambda children, attr: f"<ol>{tag_list(children)}</ol>",
|
||||
"ul": lambda children, attr, _: f"<ul>{tag_list(children)}</ul>",
|
||||
"ol": lambda children, attr, _: f"<ol>{tag_list(children)}</ol>",
|
||||
|
||||
"big": lambda children, attr: f"<span style='font-size: 2rem;'>{children}</span>",
|
||||
"small": lambda children, attr: f"<span style='font-size: 0.75rem;'>{children}</span>",
|
||||
"big": lambda children, attr, _: f"<span style='font-size: 2rem;'>{children}</span>",
|
||||
"small": lambda children, attr, _: f"<span style='font-size: 0.75rem;'>{children}</span>",
|
||||
"color": tag_color,
|
||||
|
||||
"center": lambda children, attr: f"<div style='text-align: center;'>{children}</div>",
|
||||
"right": lambda children, attr: f"<div style='text-align: right;'>{children}</div>",
|
||||
"center": lambda children, attr, _: f"<div style='text-align: center;'>{children}</div>",
|
||||
"right": lambda children, attr, _: f"<div style='text-align: right;'>{children}</div>",
|
||||
|
||||
"spoiler": tag_spoiler,
|
||||
}
|
||||
|
||||
# [img] is considered block for the purposes of collapsing whitespace,
|
||||
# despite being potentially inline (since the resulting <img> tag is inline, but creates a block container around itself and sibling images).
|
||||
# [code] has a special case in is_inline().
|
||||
INLINE_TAGS = {
|
||||
'b', 'i', 's', 'u', 'color', 'big', 'small', 'url'
|
||||
}
|
||||
|
||||
def make_emoji(name, code):
|
||||
@ -138,6 +179,33 @@ def break_lines(text):
|
||||
text = re.sub(r"\n\n+", "<br><br>", text)
|
||||
return text
|
||||
|
||||
def is_inline(e):
|
||||
if e is None:
|
||||
return False # i think
|
||||
|
||||
if is_text(e):
|
||||
return True
|
||||
|
||||
if is_tag(e):
|
||||
if is_tag(e, 'code'): # special case, since [code] can be inline OR block
|
||||
return '\n' not in e['children']
|
||||
|
||||
return e['name'] in INLINE_TAGS
|
||||
|
||||
return e['type'] != 'rule'
|
||||
|
||||
def should_collapse(text, surrounding):
|
||||
if not isinstance(text, str):
|
||||
return False
|
||||
|
||||
if not text:
|
||||
return True
|
||||
|
||||
if not text.strip() and '\n' not in text:
|
||||
return not is_inline(surrounding[0]) and not is_inline(surrounding[1])
|
||||
|
||||
return False
|
||||
|
||||
def babycode_to_html(s):
|
||||
subj = escape(s.strip().replace('\r\n', '\n').replace('\r', '\n'))
|
||||
parser = Parser(subj)
|
||||
@ -145,10 +213,19 @@ def babycode_to_html(s):
|
||||
parser.bbcode_tags_only_text_children = TEXT_ONLY
|
||||
parser.valid_emotes = EMOJI.keys()
|
||||
|
||||
elements = parser.parse()
|
||||
print(elements)
|
||||
uncollapsed = parser.parse()
|
||||
elements = []
|
||||
for i in range(len(uncollapsed)):
|
||||
e = uncollapsed[i]
|
||||
surrounding = (
|
||||
uncollapsed[i - 1] if i-1 >= 0 else None,
|
||||
uncollapsed[i + 1] if i+1 < len(uncollapsed) else None
|
||||
)
|
||||
if not should_collapse(e, surrounding):
|
||||
elements.append(e)
|
||||
|
||||
out = ""
|
||||
def fold(element, nobr):
|
||||
def fold(element, nobr, surrounding):
|
||||
if isinstance(element, str):
|
||||
if nobr:
|
||||
return element
|
||||
@ -157,10 +234,15 @@ def babycode_to_html(s):
|
||||
match element['type']:
|
||||
case "bbcode":
|
||||
c = ""
|
||||
for child in element['children']:
|
||||
for i in range(len(element['children'])):
|
||||
child = element['children'][i]
|
||||
_surrounding = (
|
||||
element['children'][i - 1] if i-1 >= 0 else None,
|
||||
element['children'][i + 1] if i+1 < len(element['children']) else None
|
||||
)
|
||||
_nobr = element['name'] == "code" or element['name'] == "ul" or element['name'] == "ol"
|
||||
c = c + fold(child, _nobr)
|
||||
res = TAGS[element['name']](c, element['attr'])
|
||||
c = c + Markup(fold(child, _nobr, _surrounding))
|
||||
res = TAGS[element['name']](c, element['attr'], surrounding)
|
||||
return res
|
||||
case "link":
|
||||
return f"<a href=\"{element['url']}\">{element['url']}</a>"
|
||||
@ -168,6 +250,12 @@ def babycode_to_html(s):
|
||||
return EMOJI[element['name']]
|
||||
case "rule":
|
||||
return "<hr>"
|
||||
for e in elements:
|
||||
out = out + fold(e, False)
|
||||
|
||||
for i in range(len(elements)):
|
||||
e = elements[i]
|
||||
surrounding = (
|
||||
elements[i - 1] if i-1 >= 0 else None,
|
||||
elements[i + 1] if i+1 < len(elements) else None
|
||||
)
|
||||
out = out + fold(e, False, surrounding)
|
||||
return out
|
||||
|
@ -4,7 +4,7 @@ import re
|
||||
|
||||
PAT_EMOTE = r"[^\s:]"
|
||||
PAT_BBCODE_TAG = r"\w"
|
||||
PAT_BBCODE_ATTR = r"[^\s\]]"
|
||||
PAT_BBCODE_ATTR = r"[^\]]"
|
||||
PAT_LINK = r"https?:\/\/[\w\-_.?:\/=&~@#%]+[\w\-\/]"
|
||||
|
||||
class Parser:
|
||||
@ -193,9 +193,10 @@ class Parser:
|
||||
self.save_position()
|
||||
|
||||
# extract printable chars (extreme hack edition)
|
||||
word = self.match_pattern(r'[ -~]')
|
||||
word = self.match_pattern(r'[!-~]')
|
||||
|
||||
if not re.match(PAT_LINK, word):
|
||||
match = re.match(PAT_LINK, word)
|
||||
if not match:
|
||||
self.restore_position()
|
||||
return None
|
||||
|
||||
|
@ -9,6 +9,8 @@ def migrate_old_avatars():
|
||||
MIGRATIONS = [
|
||||
migrate_old_avatars,
|
||||
'DELETE FROM sessions', # delete old lua porom sessions
|
||||
'ALTER TABLE "users" ADD COLUMN "invited_by" INTEGER REFERENCES users(id)', # invitation system
|
||||
'ALTER TABLE "post_history" ADD COLUMN "format_version" INTEGER DEFAULT NULL',
|
||||
]
|
||||
|
||||
def run_migrations():
|
||||
|
@ -1,5 +1,6 @@
|
||||
from .db import Model, db
|
||||
from .constants import PermissionLevel
|
||||
from flask import current_app
|
||||
import time
|
||||
|
||||
class Users(Model):
|
||||
@ -17,6 +18,9 @@ class Users(Model):
|
||||
def is_mod(self):
|
||||
return self.permission >= PermissionLevel.MODERATOR.value
|
||||
|
||||
def is_mod_only(self):
|
||||
return self.permission == PermissionLevel.MODERATOR.value
|
||||
|
||||
def is_admin(self):
|
||||
return self.permission == PermissionLevel.ADMIN.value
|
||||
|
||||
@ -48,7 +52,8 @@ class Users(Model):
|
||||
COUNT(DISTINCT posts.id) AS post_count,
|
||||
COUNT(DISTINCT threads.id) AS thread_count,
|
||||
MAX(threads.title) FILTER (WHERE threads.created_at = latest.created_at) AS latest_thread_title,
|
||||
MAX(threads.slug) FILTER (WHERE threads.created_at = latest.created_at) AS latest_thread_slug
|
||||
MAX(threads.slug) FILTER (WHERE threads.created_at = latest.created_at) AS latest_thread_slug,
|
||||
inviter.username AS inviter_username
|
||||
FROM users
|
||||
LEFT JOIN posts ON posts.user_id = users.id
|
||||
LEFT JOIN threads ON threads.user_id = users.id
|
||||
@ -57,6 +62,7 @@ class Users(Model):
|
||||
FROM threads
|
||||
GROUP BY user_id
|
||||
) latest ON latest.user_id = users.id
|
||||
LEFT JOIN users AS inviter ON inviter.id = users.invited_by
|
||||
WHERE users.id = ?"""
|
||||
return db.fetch_one(q, self.id)
|
||||
|
||||
@ -83,6 +89,18 @@ class Users(Model):
|
||||
|
||||
return True
|
||||
|
||||
def can_invite(self):
|
||||
if not current_app.config['DISABLE_SIGNUP']:
|
||||
return True
|
||||
|
||||
if current_app.config['MODS_CAN_INVITE'] and self.is_mod():
|
||||
return True
|
||||
|
||||
if current_app.config['USERS_CAN_INVITE'] and not self.is_guest():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class Topics(Model):
|
||||
table = "topics"
|
||||
@ -160,7 +178,7 @@ class Topics(Model):
|
||||
|
||||
q = """
|
||||
SELECT
|
||||
threads.title, threads.slug, threads.created_at, threads.is_locked, threads.is_stickied,
|
||||
threads.id, threads.title, threads.slug, threads.created_at, threads.is_locked, threads.is_stickied,
|
||||
users.username AS started_by,
|
||||
u.username AS latest_post_username,
|
||||
ph.content AS latest_post_content,
|
||||
@ -233,6 +251,16 @@ class Avatars(Model):
|
||||
class Subscriptions(Model):
|
||||
table = "subscriptions"
|
||||
|
||||
def get_unread_count(self):
|
||||
q = """SELECT COUNT(*) AS unread_count
|
||||
FROM posts
|
||||
LEFT JOIN subscriptions ON subscriptions.thread_id = posts.thread_id
|
||||
WHERE subscriptions.user_id = ? AND posts.created_at > subscriptions.last_seen AND posts.thread_id = ?"""
|
||||
res = db.fetch_one(q, self.user_id, self.thread_id)
|
||||
if res:
|
||||
return res['unread_count']
|
||||
return None
|
||||
|
||||
class APIRateLimits(Model):
|
||||
table = 'api_rate_limits'
|
||||
|
||||
@ -281,3 +309,11 @@ class Reactions(Model):
|
||||
"""
|
||||
|
||||
return db.query(q, post_id, reaction_text)
|
||||
|
||||
|
||||
class PasswordResetLinks(Model):
|
||||
table = "password_reset_links"
|
||||
|
||||
|
||||
class InviteKeys(Model):
|
||||
table = 'invite_keys'
|
||||
|
@ -1,9 +1,12 @@
|
||||
from flask import (
|
||||
Blueprint, render_template, request, redirect, url_for
|
||||
)
|
||||
from .users import login_required, mod_only, get_active_user
|
||||
from ..models import Users
|
||||
from .users import login_required, mod_only, get_active_user, admin_only
|
||||
from ..models import Users, PasswordResetLinks
|
||||
from ..db import db, DB
|
||||
import secrets
|
||||
import time
|
||||
|
||||
bp = Blueprint("mod", __name__, url_prefix = "/mod/")
|
||||
|
||||
@bp.get("/sort-topics")
|
||||
@ -31,3 +34,18 @@ def sort_topics_post():
|
||||
def user_list():
|
||||
users = Users.select()
|
||||
return render_template("mod/user-list.html", users = users)
|
||||
|
||||
|
||||
@bp.post("/reset-pass/<user_id>")
|
||||
@login_required
|
||||
@mod_only("topics.all_topics")
|
||||
def create_reset_pass(user_id):
|
||||
now = int(time.time())
|
||||
key = secrets.token_urlsafe(20)
|
||||
reset_link = PasswordResetLinks.create({
|
||||
'user_id': int(user_id),
|
||||
'expires_at': now + 24 * 60 * 60,
|
||||
'key': key,
|
||||
})
|
||||
|
||||
return redirect(url_for('users.reset_link_login', key=key))
|
||||
|
@ -2,15 +2,16 @@ from flask import (
|
||||
Blueprint, redirect, url_for, flash, render_template, request
|
||||
)
|
||||
from .users import login_required, get_active_user
|
||||
from ..lib.babycode import babycode_to_html
|
||||
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION
|
||||
from ..constants import InfoboxKind
|
||||
from ..db import db
|
||||
from ..models import Posts, PostHistory, Threads
|
||||
from ..models import Posts, PostHistory, Threads, Topics
|
||||
|
||||
bp = Blueprint("posts", __name__, url_prefix = "/post")
|
||||
|
||||
|
||||
def create_post(thread_id, user_id, content, markup_language="babycode"):
|
||||
parsed_content = babycode_to_html(content)
|
||||
with db.transaction():
|
||||
post = Posts.create({
|
||||
"thread_id": thread_id,
|
||||
@ -20,10 +21,11 @@ def create_post(thread_id, user_id, content, markup_language="babycode"):
|
||||
|
||||
revision = PostHistory.create({
|
||||
"post_id": post.id,
|
||||
"content": babycode_to_html(content),
|
||||
"content": parsed_content,
|
||||
"is_initial_revision": True,
|
||||
"original_markup": content,
|
||||
"markup_language": markup_language,
|
||||
"format_version": BABYCODE_VERSION,
|
||||
})
|
||||
|
||||
post.update({"current_revision_id": revision.id})
|
||||
@ -31,14 +33,16 @@ def create_post(thread_id, user_id, content, markup_language="babycode"):
|
||||
|
||||
|
||||
def update_post(post_id, new_content, markup_language='babycode'):
|
||||
parsed_content = babycode_to_html(new_content)
|
||||
with db.transaction():
|
||||
post = Posts.find({'id': post_id})
|
||||
new_revision = PostHistory.create({
|
||||
'post_id': post.id,
|
||||
'content': babycode_to_html(new_content),
|
||||
'content': parsed_content,
|
||||
'is_initial_revision': False,
|
||||
'original_markup': new_content,
|
||||
'markup_language': markup_language,
|
||||
'format_version': BABYCODE_VERSION,
|
||||
})
|
||||
|
||||
post.update({'current_revision_id': new_revision.id})
|
||||
@ -48,12 +52,30 @@ def update_post(post_id, new_content, markup_language='babycode'):
|
||||
@login_required
|
||||
def delete(post_id):
|
||||
post = Posts.find({'id': post_id})
|
||||
if not post:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
thread = Threads.find({'id': post.thread_id})
|
||||
user = get_active_user()
|
||||
if not user:
|
||||
return redirect(url_for('threads.thread', slug=thread.slug))
|
||||
|
||||
if user.is_mod() or post.user_id == user.id:
|
||||
post.delete()
|
||||
flash("Post deleted.", InfoboxKind.INFO)
|
||||
|
||||
post_count = Posts.count({
|
||||
'thread_id': thread.id,
|
||||
})
|
||||
|
||||
if post_count == 0:
|
||||
topic = Topics.find({
|
||||
'id': thread.topic_id,
|
||||
})
|
||||
thread.delete()
|
||||
flash('Thread deleted.', InfoboxKind.INFO)
|
||||
return redirect(url_for('topics.topic', slug=topic.slug))
|
||||
|
||||
flash('Post deleted.', InfoboxKind.INFO)
|
||||
|
||||
return redirect(url_for('threads.thread', slug=thread.slug))
|
||||
|
||||
@ -61,6 +83,10 @@ def delete(post_id):
|
||||
@bp.get("/<post_id>/edit")
|
||||
@login_required
|
||||
def edit(post_id):
|
||||
post = Posts.find({'id': post_id})
|
||||
if not post:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
user = get_active_user()
|
||||
q = f"{Posts.FULL_POSTS_QUERY} WHERE posts.id = ?"
|
||||
editing_post = db.fetch_one(q, post_id)
|
||||
@ -91,6 +117,8 @@ def edit(post_id):
|
||||
def edit_form(post_id):
|
||||
user = get_active_user()
|
||||
post = Posts.find({'id': post_id})
|
||||
if not post:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
if post.user_id != user.id:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
|
@ -13,6 +13,20 @@ import time
|
||||
bp = Blueprint("threads", __name__, url_prefix = "/threads/")
|
||||
|
||||
|
||||
def get_post_url(post_id, _anchor=False):
|
||||
post = Posts.find({'id': post_id})
|
||||
if not post:
|
||||
return ""
|
||||
|
||||
thread = Threads.find({'id': post.thread_id})
|
||||
|
||||
res = url_for('threads.thread', slug=thread.slug, after=post_id)
|
||||
if not _anchor:
|
||||
return res
|
||||
|
||||
return f"{res}#post-{post_id}"
|
||||
|
||||
|
||||
@bp.get("/<slug>")
|
||||
def thread(slug):
|
||||
POSTS_PER_PAGE = 10
|
||||
@ -40,12 +54,14 @@ def thread(slug):
|
||||
other_topics = Topics.select()
|
||||
|
||||
is_subscribed = False
|
||||
unread_count = None
|
||||
if is_logged_in():
|
||||
subscription = Subscriptions.find({
|
||||
'thread_id': thread.id,
|
||||
'user_id': get_active_user().id,
|
||||
})
|
||||
if subscription:
|
||||
unread_count = subscription.get_unread_count()
|
||||
if int(posts[-1]['created_at']) > int(subscription.last_seen):
|
||||
subscription.update({
|
||||
'last_seen': int(posts[-1]['created_at'])
|
||||
@ -62,6 +78,7 @@ def thread(slug):
|
||||
topics = other_topics,
|
||||
is_subscribed = is_subscribed,
|
||||
Reactions = Reactions,
|
||||
unread_count = unread_count,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
from flask import (
|
||||
Blueprint, render_template, request, redirect, url_for, flash, session
|
||||
)
|
||||
from .users import login_required, mod_only
|
||||
from ..models import Users, Topics, Threads
|
||||
from .users import login_required, mod_only, get_active_user, is_logged_in
|
||||
from ..models import Users, Topics, Threads, Subscriptions
|
||||
from ..constants import InfoboxKind
|
||||
from slugify import slugify
|
||||
import time
|
||||
@ -62,9 +62,21 @@ def topic(slug):
|
||||
page_count = max(math.ceil(threads_count / THREADS_PER_PAGE), 1)
|
||||
page = max(1, min(int(request.args.get('page', default=1)), page_count))
|
||||
|
||||
threads_list = target_topic.get_threads(THREADS_PER_PAGE, page, sort_by)
|
||||
subscriptions = {}
|
||||
if is_logged_in():
|
||||
for thread in threads_list:
|
||||
subscription = Subscriptions.find({
|
||||
'user_id': get_active_user().id,
|
||||
'thread_id': thread['id'],
|
||||
})
|
||||
if subscription:
|
||||
subscriptions[thread['id']] = subscription.get_unread_count()
|
||||
|
||||
return render_template(
|
||||
"topics/topic.html",
|
||||
threads_list = target_topic.get_threads(THREADS_PER_PAGE, page, sort_by),
|
||||
threads_list = threads_list,
|
||||
subscriptions = subscriptions,
|
||||
topic = target_topic,
|
||||
current_page = page,
|
||||
page_count = page_count
|
||||
|
@ -4,7 +4,7 @@ from flask import (
|
||||
from functools import wraps
|
||||
from ..db import db
|
||||
from ..lib.babycode import babycode_to_html
|
||||
from ..models import Users, Sessions, Subscriptions, Avatars
|
||||
from ..models import Users, Sessions, Subscriptions, Avatars, PasswordResetLinks, InviteKeys
|
||||
from ..constants import InfoboxKind, PermissionLevel
|
||||
from ..auth import digest, verify
|
||||
from wand.image import Image
|
||||
@ -165,6 +165,15 @@ def admin_only(*args, **kwargs):
|
||||
return decorator
|
||||
|
||||
|
||||
def get_prefers_theme():
|
||||
if not 'theme' in session:
|
||||
return 'style'
|
||||
|
||||
if session['theme'] not in current_app.config['allowed_themes']:
|
||||
return 'style'
|
||||
|
||||
return session['theme']
|
||||
|
||||
@bp.get("/log_in")
|
||||
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
||||
def log_in():
|
||||
@ -195,32 +204,53 @@ def log_in_post():
|
||||
@bp.get("/sign_up")
|
||||
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
||||
def sign_up():
|
||||
if current_app.config['DISABLE_SIGNUP']:
|
||||
key = request.args.get('key', default=None)
|
||||
if key is None:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
invite = InviteKeys.find({'key': key})
|
||||
if not invite:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
inviter = Users.find({'id': invite.created_by})
|
||||
return render_template("users/sign_up.html", inviter=inviter, key=key)
|
||||
|
||||
return render_template("users/sign_up.html")
|
||||
|
||||
|
||||
@bp.post("/sign_up")
|
||||
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
||||
def sign_up_post():
|
||||
key = request.form.get('key', default=None)
|
||||
|
||||
if current_app.config['DISABLE_SIGNUP']:
|
||||
if not key:
|
||||
return redirect(url_for("topics.all_topics"))
|
||||
invite_key = InviteKeys.find({'key': key})
|
||||
if not invite_key:
|
||||
return redirect(url_for("topics.all_topics"))
|
||||
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
password_confirm = request.form['password-confirm']
|
||||
|
||||
if not validate_username(username):
|
||||
flash("Invalid username.", InfoboxKind.ERROR)
|
||||
return redirect(url_for("users.sign_up"))
|
||||
return redirect(url_for("users.sign_up", key=key))
|
||||
|
||||
user_exists = Users.count({"username": username}) > 0
|
||||
if user_exists:
|
||||
flash(f"Username '{username}' is already taken.", InfoboxKind.ERROR)
|
||||
return redirect(url_for("users.sign_up"))
|
||||
return redirect(url_for("users.sign_up", key=key))
|
||||
|
||||
if not validate_password(password):
|
||||
flash("Invalid password.", InfoboxKind.ERROR)
|
||||
return redirect(url_for("users.sign_up"))
|
||||
return redirect(url_for("users.sign_up", key=key))
|
||||
|
||||
if password != password_confirm:
|
||||
flash("Passwords do not match.", InfoboxKind.ERROR)
|
||||
return redirect(url_for("users.sign_up"))
|
||||
return redirect(url_for("users.sign_up", key=key))
|
||||
|
||||
hashed = digest(password)
|
||||
|
||||
@ -230,11 +260,19 @@ def sign_up_post():
|
||||
"permission": PermissionLevel.GUEST.value,
|
||||
})
|
||||
|
||||
if current_app.config['DISABLE_SIGNUP']:
|
||||
invite_key = InviteKeys.find({'key': key})
|
||||
new_user.update({
|
||||
'invited_by': invite_key.created_by,
|
||||
'permission': PermissionLevel.USER.value,
|
||||
})
|
||||
invite_key.delete()
|
||||
|
||||
session_obj = create_session(new_user.id)
|
||||
|
||||
session['pyrom_session_key'] = session_obj.key
|
||||
flash("Signed up successfully!", InfoboxKind.INFO)
|
||||
return redirect(url_for("users.sign_up"))
|
||||
return redirect(url_for("topics.all_topics"))
|
||||
|
||||
|
||||
@bp.get("/<username>")
|
||||
@ -259,6 +297,12 @@ def settings_form(username):
|
||||
# we silently ignore the passed username
|
||||
# and grab the correct user from the session
|
||||
user = get_active_user()
|
||||
theme = request.form.get('theme', default='style')
|
||||
if theme == 'style':
|
||||
if 'theme' in session:
|
||||
session.pop('theme')
|
||||
else:
|
||||
session['theme'] = theme
|
||||
topic_sort_by = request.form.get('topic_sort_by', default='activity')
|
||||
if topic_sort_by == 'activity' or topic_sort_by == 'thread':
|
||||
sort_by = session['sort_by'] = topic_sort_by
|
||||
@ -281,17 +325,17 @@ def settings_form(username):
|
||||
def set_avatar(username):
|
||||
user = get_active_user()
|
||||
if user.is_guest():
|
||||
flash('You must be logged in to perform this action.', InfoboxKind.ERROR)
|
||||
return redirect(url_for('.settings', user.username))
|
||||
flash('You are a guest. Your account must be confirmed by a moderator to perform this action.', InfoboxKind.ERROR)
|
||||
return redirect(url_for('.settings', username=user.username))
|
||||
if 'avatar' not in request.files:
|
||||
flash('Avatar missing.', InfoboxKind.ERROR)
|
||||
return redirect(url_for('.settings', user.username))
|
||||
return redirect(url_for('.settings', username=user.username))
|
||||
|
||||
file = request.files['avatar']
|
||||
|
||||
if file.filename == '':
|
||||
flash('Avatar missing.', InfoboxKind.ERROR)
|
||||
return redirect(url_for('.settings', user.username))
|
||||
return redirect(url_for('.settings', username=user.username))
|
||||
|
||||
file_bytes = file.read()
|
||||
|
||||
@ -316,7 +360,7 @@ def set_avatar(username):
|
||||
return redirect(url_for('.settings', username=user.username))
|
||||
else:
|
||||
flash('Something went wrong. Please try again later.', InfoboxKind.WARN)
|
||||
return redirect(url_for('.settings', user.username))
|
||||
return redirect(url_for('.settings', username=user.username))
|
||||
|
||||
|
||||
@bp.post('/<username>/change_password')
|
||||
@ -419,12 +463,12 @@ def demod_user(user_id):
|
||||
|
||||
@bp.post("/guest_user/<user_id>")
|
||||
@login_required
|
||||
@admin_only("topics.all_topics")
|
||||
@mod_only("topics.all_topics")
|
||||
def guest_user(user_id):
|
||||
target_user = Users.find({"id": user_id})
|
||||
if not target_user:
|
||||
return redirect(url_for('.all_topics'))
|
||||
if target_user.is_mod():
|
||||
if get_active_user().is_mod_only() and target_user.is_mod():
|
||||
return redirect(url_for('.page', username=target_user.username))
|
||||
|
||||
target_user.update({
|
||||
@ -516,3 +560,126 @@ def inbox(username):
|
||||
})
|
||||
|
||||
return render_template("users/inbox.html", new_posts = new_posts, total_unreads_count = total_unreads_count, all_subscriptions = all_subscriptions)
|
||||
|
||||
|
||||
@bp.get('/reset-link/<key>')
|
||||
def reset_link_login(key):
|
||||
reset_link = PasswordResetLinks.find({
|
||||
'key': key
|
||||
})
|
||||
if not reset_link:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
if int(time.time()) > int(reset_link.expires_at):
|
||||
reset_link.delete()
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
target_user = Users.find({
|
||||
'id': reset_link.user_id
|
||||
})
|
||||
|
||||
return render_template('users/reset_link_login.html', username = target_user.username)
|
||||
|
||||
|
||||
@bp.post('/reset-link/<key>')
|
||||
def reset_link_login_form(key):
|
||||
reset_link = PasswordResetLinks.find({
|
||||
'key': key
|
||||
})
|
||||
if not reset_link:
|
||||
return redirect('topics.all_topics')
|
||||
|
||||
if int(time.time()) > int(reset_link.expires_at):
|
||||
reset_link.delete()
|
||||
return redirect('topics.all_topics')
|
||||
|
||||
password = request.form.get('password')
|
||||
password2 = request.form.get('password2')
|
||||
|
||||
if not validate_password(password):
|
||||
flash("Invalid password.", InfoboxKind.ERROR)
|
||||
return redirect(url_for('.reset_link_login', key=key))
|
||||
|
||||
if password != password2:
|
||||
flash("Passwords do not match.", InfoboxKind.ERROR)
|
||||
return redirect(url_for('.reset_link_login', key=key))
|
||||
|
||||
target_user = Users.find({
|
||||
'id': reset_link.user_id
|
||||
})
|
||||
reset_link.delete()
|
||||
|
||||
hashed = digest(password)
|
||||
target_user.update({'password_hash': hashed})
|
||||
session_obj = create_session(target_user.id)
|
||||
|
||||
session['pyrom_session_key'] = session_obj.key
|
||||
flash("Logged in!", InfoboxKind.INFO)
|
||||
|
||||
return redirect(url_for('.page', username=target_user.username))
|
||||
|
||||
|
||||
@bp.get('/<username>/invite-links/')
|
||||
@login_required
|
||||
def invite_links(username):
|
||||
target_user = Users.find({
|
||||
'username': username
|
||||
})
|
||||
if not target_user or not target_user.can_invite():
|
||||
return redirect(url_for('.page', username=username))
|
||||
|
||||
if target_user.username != get_active_user().username:
|
||||
return redirect(url_for('.invite_links', username=target_user.username))
|
||||
|
||||
invites = InviteKeys.findall({
|
||||
'created_by': target_user.id
|
||||
})
|
||||
|
||||
return render_template('users/invite_links.html', invites=invites)
|
||||
|
||||
|
||||
@bp.post('/<username>/invite-links/create')
|
||||
@login_required
|
||||
def create_invite_link(username):
|
||||
target_user = Users.find({
|
||||
'username': username
|
||||
})
|
||||
if not target_user or not target_user.can_invite():
|
||||
return redirect(url_for('.page', username=username))
|
||||
|
||||
if target_user.username != get_active_user().username:
|
||||
return redirect(url_for('.invite_links', username=target_user.username))
|
||||
|
||||
invite = InviteKeys.create({
|
||||
'created_by': target_user.id,
|
||||
'key': secrets.token_urlsafe(20),
|
||||
})
|
||||
|
||||
return redirect(url_for('.invite_links', username=target_user.username))
|
||||
|
||||
|
||||
@bp.post('/<username>/invite-links/revoke')
|
||||
@login_required
|
||||
def revoke_invite_link(username):
|
||||
target_user = Users.find({
|
||||
'username': username
|
||||
})
|
||||
if not target_user or not target_user.can_invite():
|
||||
return redirect(url_for('.page', username=username))
|
||||
|
||||
if target_user.username != get_active_user().username:
|
||||
return redirect(url_for('.invite_links', username=target_user.username))
|
||||
|
||||
invite = InviteKeys.find({
|
||||
'key': request.form.get('key'),
|
||||
})
|
||||
|
||||
if not invite:
|
||||
return redirect(url_for('.invite_links', username=target_user.username))
|
||||
|
||||
if invite.created_by != target_user.id:
|
||||
return redirect(url_for('.invite_links', username=target_user.username))
|
||||
|
||||
invite.delete()
|
||||
|
||||
return redirect(url_for('.invite_links', username=target_user.username))
|
||||
|
@ -83,6 +83,19 @@ SCHEMA = [
|
||||
"reaction_text" TEXT NOT NULL DEFAULT ''
|
||||
)""",
|
||||
|
||||
"""CREATE TABLE IF NOT EXISTS "password_reset_links" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY,
|
||||
"user_id" REFERENCES users(id) ON DELETE CASCADE,
|
||||
"expires_at" INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP)),
|
||||
"key" TEXT NOT NULL UNIQUE
|
||||
)""",
|
||||
|
||||
"""CREATE TABLE IF NOT EXISTS "invite_keys" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY,
|
||||
"created_by" REFERENCES users(id) ON DELETE CASCADE,
|
||||
"key" TEXT NOT NULL UNIQUE
|
||||
)""",
|
||||
|
||||
# INDEXES
|
||||
"CREATE INDEX IF NOT EXISTS idx_post_history_post_id ON post_history(post_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_posts_thread ON posts(thread_id, created_at, id)",
|
||||
|
@ -126,8 +126,9 @@
|
||||
<code class="inline-code">[img=https://forum.poto.cafe/avatars/default.webp]the Python logo with a cowboy hat[/img]</code>
|
||||
{{ '[img=/static/avatars/default.webp]the Python logo with a cowboy hat[/img]' | babycode | safe }}
|
||||
</p>
|
||||
<p>Text inside the tag becomes the alt text. The attribute is the image URL.</p>
|
||||
<p>Images will always break up a paragraph and will get scaled down to a maximum of 400px. The text inside the tag will become the image's alt text.</p>
|
||||
<p>Text inside the tag becomes the alt text. The attribute is the image URL. The text inside the tag will become the image's alt text.</p>
|
||||
<p>Images will always break up a paragraph and will get scaled down to a maximum of 400px. However, consecutive image tags will try to stay in one line, wrapping if necessary. Break the paragraph if you wish to keep images on their own paragraph.</p>
|
||||
<p>Multiple images attached to a post can be clicked to open a dialog to view them.</p>
|
||||
</section>
|
||||
<section class="babycode-guide-section">
|
||||
<h2 id="adding-code-blocks">Adding code blocks</h2>
|
||||
@ -151,6 +152,15 @@
|
||||
Will produce the following list:
|
||||
{{ list | babycode | safe }}
|
||||
</section>
|
||||
<section class="babycode-guide-section">
|
||||
<h2 id="spoilers">Spoilers</h2>
|
||||
{% set spoiler = "[spoiler=Major Metal Gear Spoilers]Snake dies[/spoiler]" %}
|
||||
<p>You can make a section collapsible by using the <code class="inline-code">[spoiler]</code> tag:</p>
|
||||
{{ ("[code]\n%s[/code]" % spoiler) | babycode | safe }}
|
||||
Will produce:
|
||||
{{ spoiler | babycode | safe }}
|
||||
All other tags are supported inside spoilers.
|
||||
</section>
|
||||
{% endset %}
|
||||
{{ sections | safe }}
|
||||
</div>
|
||||
|
@ -4,11 +4,11 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
{% if self.title() %}
|
||||
<title>Porom - {% block title %}{% endblock %}</title>
|
||||
<title>{{config.SITE_NAME}} - {% block title %}{% endblock %}</title>
|
||||
{% else %}
|
||||
<title>Porom</title>
|
||||
<title>{{config.SITE_NAME}}</title>
|
||||
{% endif %}
|
||||
<link rel="stylesheet" href="{{ "/static/style.css" | cachebust }}">
|
||||
<link rel="stylesheet" href="{{ ("/static/css/%s.css" % get_prefers_theme()) | cachebust }}">
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||
</head>
|
||||
<body>
|
||||
|
@ -59,6 +59,7 @@
|
||||
<button class="babycode-button contain-svg full" type=button id="post-editor-img" title="Insert Image"><img src="/static/misc/image.svg"></button>
|
||||
<button class="babycode-button" type=button id="post-editor-ol" title="Insert Ordered list">1.</button>
|
||||
<button class="babycode-button" type=button id="post-editor-ul" title="Insert Unordered list">•</button>
|
||||
<button class="babycode-button contain-svg full" type=button id="post-editor-spoiler" title="Insert spoiler"><img src="/static/misc/spoiler.svg"></button>
|
||||
</span>
|
||||
<textarea class="babycode-editor" name="{{ ta_name }}" id="babycode-content" placeholder="{{ ta_placeholder }}" {{ "required" if not optional else "" }}>{{ prefill }}</textarea>
|
||||
<a href="{{ url_for("app.babycode_guide") }}" target="_blank">babycode guide</a>
|
||||
@ -143,7 +144,7 @@
|
||||
|
||||
{% if show_reply %}
|
||||
{% set qtext = "[url=%s]%s said:[/url]" | format(post_permalink, post['username']) %}
|
||||
{% set reply_text = "%s\n[quote]%s[/quote]\n" | format(qtext, post['original_markup']) %}
|
||||
{% set reply_text = "%s\n[quote]\n%s\n[/quote]\n" | format(qtext, post['original_markup']) %}
|
||||
<button value="{{ reply_text }}" class="reply-button">Quote</button>
|
||||
{% endif %}
|
||||
|
||||
|
@ -1,10 +1,14 @@
|
||||
<nav id="topnav">
|
||||
<span>
|
||||
<a class="site-title" href="{{url_for('topics.all_topics')}}">Porom</a>
|
||||
<a class="site-title" href="{{url_for('topics.all_topics')}}">{{config.SITE_NAME}}</a>
|
||||
</span>
|
||||
<span>
|
||||
{% if not is_logged_in() %}
|
||||
{% if not config.DISABLE_SIGNUP %}
|
||||
Welcome, guest. Please <a href="{{url_for('users.sign_up')}}">sign up</a> or <a href="{{url_for('users.log_in')}}">log in</a>
|
||||
{% else %}
|
||||
Welcome, guest. Please <a href="{{url_for('users.log_in')}}">log in</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% with user = get_active_user() %}
|
||||
Welcome, <a href="{{ url_for("users.page", username = user.username) }}">{{user.username}}</a>
|
||||
@ -12,6 +16,10 @@
|
||||
<a href="{{ url_for("users.settings", username = user.username) }}">Settings</a>
|
||||
•
|
||||
<a href="{{ url_for("users.inbox", username = user.username) }}">Inbox</a>
|
||||
{% if config.DISABLE_SIGNUP and user.can_invite() %}
|
||||
•
|
||||
<a href="{{ url_for('users.invite_links', username=user.username )}}">Invite to {{ config.SITE_NAME }}</a>
|
||||
{% endif %}
|
||||
{% if user.is_mod() %}
|
||||
•
|
||||
<a href="{{ url_for("mod.user_list") }}">User list</a>
|
||||
|
@ -37,6 +37,9 @@
|
||||
<th>Username</th>
|
||||
<th class="small">Permission</th>
|
||||
<th class="small">Signed up on</th>
|
||||
{% if active_user.is_admin() %}
|
||||
<th class="small">Create password reset link</th>
|
||||
{% endif %}
|
||||
</thead>
|
||||
{% for user in not_guests %}
|
||||
<tr>
|
||||
@ -50,6 +53,13 @@
|
||||
<td>
|
||||
{{ timestamp(user.created_at) }}
|
||||
</td>
|
||||
{% if active_user.is_admin() %}
|
||||
<td>
|
||||
<form method="post" action="{{url_for('mod.create_reset_pass', user_id=user.id)}}">
|
||||
<input type="submit" class="warn" value="Create password reset link">
|
||||
</form>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
@ -12,7 +12,7 @@
|
||||
{% endif %}
|
||||
<main>
|
||||
<nav class="darkbg">
|
||||
<h1 class="thread-title">{{ thread.title }}</h1>
|
||||
<h1 class="thread-title">{{ thread.title }}{% if unread_count is not none %} ({{ unread_count }} unread){% endif %}</h1>
|
||||
<span>Posted in <a href="{{ url_for("topics.topic", slug=topic.slug) }}">{{ topic.name }}</a>
|
||||
{% if thread.is_stickied %}
|
||||
• <i>stickied, so it's probably important</i>
|
||||
|
@ -40,6 +40,9 @@
|
||||
<div class="thread-info-container">
|
||||
<span>
|
||||
<span class="thread-title"><a href="{{ url_for("threads.thread", slug=thread['slug']) }}">{{thread['title']}}</a></span>
|
||||
{% if thread['id'] in subscriptions %}
|
||||
({{ subscriptions[thread['id']] }} unread)
|
||||
{% endif %}
|
||||
•
|
||||
Started by <a href="{{ url_for("users.page", username=thread['started_by']) }}">{{ thread['started_by'] }}</a> on {{ timestamp(thread['created_at'])}}
|
||||
</span>
|
||||
|
@ -23,7 +23,7 @@
|
||||
{% if topic['id'] in active_threads %}
|
||||
{% with thread=active_threads[topic['id']] %}
|
||||
<span>
|
||||
Latest post in: <a href="{{ url_for("threads.thread", slug=thread['thread_slug'])}}">{{ thread['thread_title'] }}</a> by <a href="{{ url_for("users.page", username=thread['username'])}}">{{ thread['username'] }}</a> at <a href="">{{ timestamp(thread['post_created_at']) }}</a>
|
||||
Latest post in: <a href="{{ url_for("threads.thread", slug=thread['thread_slug'])}}">{{ thread['thread_title'] }}</a> by <a href="{{ url_for("users.page", username=thread['username'])}}">{{ thread['username'] }}</a> at <a href="{{ get_post_url(thread.post_id, _anchor=true) }}">{{ timestamp(thread['post_created_at']) }}</a>
|
||||
</span>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
36
app/templates/users/invite_links.html
Normal file
36
app/templates/users/invite_links.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% from 'common/macros.html' import accordion %}
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}invites{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg inbox-container">
|
||||
<p>To manage growth, {{ config.SITE_NAME }} disallows direct sign ups. Instead, users already with an account may invite people they know. You can create invite links here. Once an invite link is used to sign up, it can no longer be used.</p>
|
||||
{% call(section) accordion(disabled=invites | length == 0) %}
|
||||
{% if section == 'header' %}
|
||||
Your invites
|
||||
{% else %}
|
||||
{% if invites %}
|
||||
<table class="colorful-table">
|
||||
<thead>
|
||||
<th class='small'>Link</th>
|
||||
<th class='small'>Revoke</th>
|
||||
</thead>
|
||||
{% for invite in invites %}
|
||||
<tr>
|
||||
<td><a href="{{url_for('users.sign_up', key=invite.key)}}">Link</a></td>
|
||||
<td>
|
||||
<form method="post" action="{{ url_for('users.revoke_invite_link', username=active_user.username) }}">
|
||||
<input type=hidden value="{{ invite.key }}" name="key">
|
||||
<input type=submit class=warn value="Revoke">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
<form method="post" action="{{ url_for('users.create_invite_link', username=active_user.username) }}">
|
||||
<input type=submit value="Create new invite">
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
15
app/templates/users/reset_link_login.html
Normal file
15
app/templates/users/reset_link_login.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Reset password{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg login-container">
|
||||
<h1>Reset password for {{username}}</h1>
|
||||
<p>Send this link to {{username}} to allow them to reset their password.</p>
|
||||
<form method="post">
|
||||
<label for="password">New password</label><br>
|
||||
<input type="password" id="password" name="password" autocomplete="new-password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required><br>
|
||||
<label for="password2">Confirm password</label><br>
|
||||
<input type="password" id="password2" name="password2" autocomplete="new-password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required><br>
|
||||
<input type="submit" value="Reset password">
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
@ -15,6 +15,12 @@
|
||||
</div>
|
||||
</form>
|
||||
<form method='post'>
|
||||
<label for='theme'>Theme (beta)</label>
|
||||
<select autocomplete='off' id='theme' name='theme'>
|
||||
{% for theme in config.allowed_themes %}
|
||||
<option value="{{ theme }}" {{ 'selected' if get_prefers_theme() == theme }}>{{ theme | theme_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for='topic_sort_by'>Sort threads by:</label>
|
||||
<select id='topic_sort_by' name='topic_sort_by'>
|
||||
<option value='activity' {{ 'selected' if session['sort_by'] == 'activity' else '' }}>Latest activity</option>
|
||||
|
@ -3,7 +3,13 @@
|
||||
{% block content %}
|
||||
<div class="darkbg login-container">
|
||||
<h1>Sign up</h1>
|
||||
{% if inviter %}
|
||||
<p>You have been invited by <a href="{{ url_for('users.page', username=inviter.username) }}">{{ inviter.username }}</a> to join {{ config.SITE_NAME }}. Create an identity below.</p>
|
||||
{% endif %}
|
||||
<form method="post">
|
||||
{% if key %}
|
||||
<input type="hidden" value={{key}} name="key">
|
||||
{% endif %}
|
||||
<label for="username">Username</label><br>
|
||||
<input type="text" id="username" name="username" pattern="[a-zA-Z0-9_-]{3,20}" title="3-20 characters. Only upper and lowercase letters, digits, hyphens, and underscores" required autocomplete="username"><br>
|
||||
<label for="password">Password</label>
|
||||
@ -12,6 +18,8 @@
|
||||
<input type="password" id="password-confirm" name="password-confirm" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,255}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
|
||||
<input type="submit" value="Sign up">
|
||||
</form>
|
||||
{% if not inviter %}
|
||||
<span>After you sign up, a moderator will need to confirm your account before you will be allowed to post.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -64,6 +64,9 @@
|
||||
{% if stats.latest_thread_title %}
|
||||
<li>Latest started thread: <a href="{{ url_for("threads.thread", slug = stats.latest_thread_slug) }}">{{ stats.latest_thread_title }}</a>
|
||||
{% endif %}
|
||||
{% if stats.inviter_username %}
|
||||
<li>Invited by <a href="{{ url_for('users.page', username=stats.inviter_username) }}">{{ stats.inviter_username }}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endwith %}
|
||||
Latest posts:
|
||||
|
20
build-themes.sh
Executable file
20
build-themes.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
sass_dir="sass"
|
||||
css_dir="data/static/css"
|
||||
|
||||
if [[ "$1" == "--watch" && -n "$2" ]]; then
|
||||
file="$2"
|
||||
[[ $(basename "$file") = _* ]] && exit 1
|
||||
sass --no-source-map --watch "$file" "$css_dir/theme-$(basename "$file" .scss).css"
|
||||
else
|
||||
set -u
|
||||
rm -r "$css_dir/"
|
||||
#build default first
|
||||
sass --no-source-map "$sass_dir/_default.scss" "$css_dir/style.css"
|
||||
for file in "$sass_dir"/*.scss; do
|
||||
[[ $(basename "$file") = _* ]] && continue
|
||||
sass --no-source-map "$file" "$css_dir/theme-$(basename "$file" .scss).css"
|
||||
done
|
||||
fi
|
8
config/pyrom_config.toml
Normal file
8
config/pyrom_config.toml
Normal file
@ -0,0 +1,8 @@
|
||||
SITE_NAME = "Porom"
|
||||
DISABLE_SIGNUP = false # if true, no one can sign up.
|
||||
|
||||
# if neither of the following two options is true,
|
||||
# no one can sign up. this may be useful later when/if LDAP is implemented.
|
||||
|
||||
MODS_CAN_INVITE = true # if true, allows moderators to create invite links. useless unless DISABLE_SIGNUP to be true.
|
||||
USERS_CAN_INVITE = false # if true, allows users to create invite links. useless unless DISABLE_SIGNUP to be true.
|
@ -28,12 +28,11 @@
|
||||
}
|
||||
.reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton {
|
||||
cursor: default;
|
||||
color: black;
|
||||
font-size: 0.9em;
|
||||
font-family: "Cadman";
|
||||
text-decoration: none;
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
padding: 5px 20px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
@ -42,6 +41,14 @@ body {
|
||||
font-family: "Cadman";
|
||||
margin: 20px 100px;
|
||||
background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126);
|
||||
color: black;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #c11c1c;
|
||||
}
|
||||
a:visited {
|
||||
color: #730c0c;
|
||||
}
|
||||
|
||||
.big {
|
||||
@ -50,6 +57,7 @@ body {
|
||||
|
||||
#topnav {
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
background-color: #c1ceb1;
|
||||
@ -59,6 +67,7 @@ body {
|
||||
|
||||
#bottomnav {
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
background-color: rgb(143.7039271654, 144.3879625984, 142.8620374016);
|
||||
@ -81,7 +90,7 @@ body {
|
||||
font-size: 3rem;
|
||||
margin: 0 20px;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.thread-title {
|
||||
@ -120,7 +129,7 @@ body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
gap: 0px 0px;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas: "post-info" "post-content" "post-reactions";
|
||||
grid-area: post-content-container;
|
||||
@ -136,6 +145,7 @@ body {
|
||||
align-items: center;
|
||||
border-top: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126);
|
||||
}
|
||||
|
||||
.post-content {
|
||||
@ -185,51 +195,6 @@ pre code {
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.inline-code {
|
||||
background-color: rgb(38.5714173228, 40.9237007874, 35.6762992126);
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
display: inline-block;
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#delete-dialog, .lightbox-dialog {
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 2px solid black;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.delete-dialog-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.lightbox-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
min-width: 400px;
|
||||
background-color: #c1ceb1;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.lightbox-image {
|
||||
max-width: 70vw;
|
||||
max-height: 70vh;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
|
||||
.lightbox-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.copy-code-container {
|
||||
position: sticky;
|
||||
width: calc(100% - 4px);
|
||||
@ -254,6 +219,53 @@ pre code {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.inline-code {
|
||||
background-color: rgb(38.5714173228, 40.9237007874, 35.6762992126);
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
display: inline-block;
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#delete-dialog, .lightbox-dialog {
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 2px solid black;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.delete-dialog-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.lightbox-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
min-width: 400px;
|
||||
background-color: #c1ceb1;
|
||||
color: black;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.lightbox-image {
|
||||
max-width: 70vw;
|
||||
max-height: 70vh;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
|
||||
.lightbox-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 10px 20px;
|
||||
margin: 10px;
|
||||
@ -290,9 +302,9 @@ blockquote {
|
||||
}
|
||||
|
||||
.user-page-posts {
|
||||
border-left: solid 1px black;
|
||||
border-right: solid 1px black;
|
||||
border-bottom: solid 1px black;
|
||||
border-left: 1px solid black;
|
||||
border-right: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
background-color: #c1ceb1;
|
||||
}
|
||||
|
||||
@ -319,6 +331,7 @@ blockquote {
|
||||
button, input[type=submit], .linkbutton {
|
||||
display: inline-block;
|
||||
background-color: rgb(177, 206, 204.5);
|
||||
color: black !important;
|
||||
}
|
||||
button:hover, input[type=submit]:hover, .linkbutton:hover {
|
||||
background-color: rgb(192.6, 215.8, 214.6);
|
||||
@ -334,8 +347,8 @@ button.reduced, input[type=submit].reduced, .linkbutton.reduced {
|
||||
padding: 5px;
|
||||
}
|
||||
button.critical, input[type=submit].critical, .linkbutton.critical {
|
||||
color: white;
|
||||
background-color: red;
|
||||
color: white !important;
|
||||
}
|
||||
button.critical:hover, input[type=submit].critical:hover, .linkbutton.critical:hover {
|
||||
background-color: #ff3333;
|
||||
@ -352,6 +365,7 @@ button.critical.reduced, input[type=submit].critical.reduced, .linkbutton.critic
|
||||
}
|
||||
button.warn, input[type=submit].warn, .linkbutton.warn {
|
||||
background-color: #fbfb8d;
|
||||
color: black !important;
|
||||
}
|
||||
button.warn:hover, input[type=submit].warn:hover, .linkbutton.warn:hover {
|
||||
background-color: rgb(251.8, 251.8, 163.8);
|
||||
@ -369,7 +383,8 @@ button.warn.reduced, input[type=submit].warn.reduced, .linkbutton.warn.reduced {
|
||||
|
||||
input[type=file]::file-selector-button {
|
||||
background-color: rgb(177, 206, 204.5);
|
||||
margin: 10px 10px;
|
||||
color: black !important;
|
||||
margin: 10px;
|
||||
}
|
||||
input[type=file]::file-selector-button:hover {
|
||||
background-color: rgb(192.6, 215.8, 214.6);
|
||||
@ -391,6 +406,7 @@ p {
|
||||
|
||||
.pagebutton {
|
||||
background-color: rgb(177, 206, 204.5);
|
||||
color: black !important;
|
||||
padding: 5px 5px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
@ -425,7 +441,7 @@ p {
|
||||
}
|
||||
|
||||
.login-container > * {
|
||||
width: 25%;
|
||||
width: 40%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@ -443,11 +459,12 @@ p {
|
||||
|
||||
input[type=text], input[type=password], textarea, select {
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
padding: 7px 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
color: black;
|
||||
background-color: rgb(217.8, 225.6, 208.2);
|
||||
}
|
||||
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
||||
@ -458,12 +475,15 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
border: 2px solid black;
|
||||
background-color: #81a3e6;
|
||||
padding: 20px 15px;
|
||||
color: black;
|
||||
}
|
||||
.infobox.critical {
|
||||
background-color: rgb(237, 129, 129);
|
||||
background-color: #ed8181;
|
||||
color: black;
|
||||
}
|
||||
.infobox.warn {
|
||||
background-color: #fbfb8d;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.infobox > span {
|
||||
@ -480,7 +500,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1.6fr 96px;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0px 0px;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
min-height: 96px;
|
||||
grid-template-areas: "thread-sticky-container thread-info-container thread-locked-container";
|
||||
@ -489,11 +509,13 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
.thread-sticky-container {
|
||||
grid-area: thread-sticky-container;
|
||||
border: 2px outset rgb(217.26, 220.38, 213.42);
|
||||
background-color: none;
|
||||
}
|
||||
|
||||
.thread-locked-container {
|
||||
grid-area: thread-locked-container;
|
||||
border: 2px outset rgb(217.26, 220.38, 213.42);
|
||||
background-color: none;
|
||||
}
|
||||
|
||||
.contain-svg {
|
||||
@ -511,10 +533,21 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.block-img {
|
||||
.post-img-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.post-image {
|
||||
object-fit: contain;
|
||||
max-width: 400px;
|
||||
max-height: 400px;
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
flex: 1 1 0%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.thread-info-container {
|
||||
@ -548,7 +581,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 300px;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0px 0px;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas: "guide-topics guide-toc";
|
||||
}
|
||||
@ -615,7 +648,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 96px;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0px 0px;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas: "topic-info-container topic-locked-container";
|
||||
}
|
||||
@ -632,6 +665,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
.topic-locked-container {
|
||||
grid-area: topic-locked-container;
|
||||
border: 2px outset rgb(217.26, 220.38, 213.42);
|
||||
background-color: none;
|
||||
}
|
||||
|
||||
.draggable-topic {
|
||||
@ -639,9 +673,9 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
user-select: none;
|
||||
background-color: #c1ceb1;
|
||||
padding: 20px;
|
||||
margin: 12px 0;
|
||||
border-top: 6px outset rgb(217.26, 220.38, 213.42);
|
||||
border-bottom: 6px outset rgb(135.1928346457, 145.0974015748, 123.0025984252);
|
||||
margin: 15px 0;
|
||||
border-top: 5px outset rgb(217.26, 220.38, 213.42);
|
||||
border-bottom: 5px outset rgb(135.1928346457, 145.0974015748, 123.0025984252);
|
||||
}
|
||||
.draggable-topic.dragged {
|
||||
background-color: rgb(177, 206, 204.5);
|
||||
@ -679,6 +713,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
|
||||
.tab-button {
|
||||
background-color: rgb(177, 206, 204.5);
|
||||
color: black !important;
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
@ -711,9 +746,9 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
background-color: rgb(191.3137931034, 189.7, 193.3);
|
||||
border: 1px solid black;
|
||||
padding: 10px;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
@ -729,7 +764,7 @@ ul, ol {
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
right: 80px;
|
||||
border: 2px solid black;
|
||||
border: 1px solid black;
|
||||
background-color: #81a3e6;
|
||||
padding: 20px 15px;
|
||||
border-radius: 4px;
|
||||
@ -742,8 +777,8 @@ ul, ol {
|
||||
}
|
||||
|
||||
.accordion {
|
||||
border-top-right-radius: 3px;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid black;
|
||||
margin: 10px 5px;
|
||||
@ -786,6 +821,12 @@ ul, ol {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.post-accordion-content {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126);
|
||||
}
|
||||
|
||||
.inbox-container {
|
||||
padding: 10px;
|
||||
}
|
||||
@ -820,6 +861,7 @@ footer {
|
||||
|
||||
.reaction-button.active {
|
||||
background-color: #beb1ce;
|
||||
color: black !important;
|
||||
}
|
||||
.reaction-button.active:hover {
|
||||
background-color: rgb(203, 192.6, 215.8);
|
||||
@ -840,7 +882,7 @@ footer {
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.6901960784);
|
||||
background-color: rgba(0, 0, 0, 0.5019607843);
|
||||
padding: 5px 10px;
|
||||
width: 250px;
|
||||
}
|
@ -1,99 +1,54 @@
|
||||
@use "sass:color";
|
||||
|
||||
@font-face {
|
||||
font-family: "site-title";
|
||||
src: url("/static/fonts/ChicagoFLF.woff2");
|
||||
}
|
||||
|
||||
@mixin cadman($var) {
|
||||
@font-face {
|
||||
font-family: "Cadman";
|
||||
src: url("/static/fonts/Cadman_#{$var}.woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@include cadman("Roman");
|
||||
src: url("/static/fonts/Cadman_Roman.woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@include cadman("Bold");
|
||||
font-family: "Cadman";
|
||||
src: url("/static/fonts/Cadman_Bold.woff2");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@include cadman("Italic");
|
||||
font-family: "Cadman";
|
||||
src: url("/static/fonts/Cadman_Italic.woff2");
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@include cadman("BoldItalic");
|
||||
font-family: "Cadman";
|
||||
src: url("/static/fonts/Cadman_BoldItalic.woff2");
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
$accent_color: #c1ceb1;
|
||||
|
||||
$dark_bg: color.scale($accent_color, $lightness: -25%, $saturation: -97%);
|
||||
$dark2: color.scale($accent_color, $lightness: -30%, $saturation: -60%);
|
||||
$verydark: color.scale($accent_color, $lightness: -80%, $saturation: -70%);
|
||||
|
||||
$light: color.scale($accent_color, $lightness: 40%, $saturation: -60%);
|
||||
$lighter: color.scale($accent_color, $lightness: 60%, $saturation: -60%);
|
||||
|
||||
$main_bg: color.scale($accent_color, $lightness: -10%, $saturation: -40%);
|
||||
$button_color: color.adjust($accent_color, $hue: 90);
|
||||
$button_color2: color.adjust($accent_color, $hue: 180);
|
||||
$accordion_color: color.adjust($accent_color, $hue: 140, $lightness: -10%, $saturation: -15%);
|
||||
|
||||
%button-base {
|
||||
.reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton {
|
||||
cursor: default;
|
||||
color: black;
|
||||
font-size: 0.9em;
|
||||
font-family: "Cadman";
|
||||
text-decoration: none;
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
padding: 5px 20px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
@mixin button($color) {
|
||||
@extend %button-base;
|
||||
background-color: $color;
|
||||
|
||||
&:hover {
|
||||
background-color: color.scale($color, $lightness: 20%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: color.scale($color, $lightness: -10%, $saturation: -70%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: color.scale($color, $lightness: 30%, $saturation: -90%);
|
||||
}
|
||||
|
||||
&.reduced {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin navbar($color) {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
background-color: $color;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Cadman";
|
||||
// font-size: 18px;
|
||||
margin: 20px 100px;
|
||||
background-color: $main_bg;
|
||||
background-color: #220d16;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #e87fe1;
|
||||
}
|
||||
a:visited {
|
||||
color: #ed4fb1;
|
||||
}
|
||||
|
||||
.big {
|
||||
@ -101,20 +56,28 @@ body {
|
||||
}
|
||||
|
||||
#topnav {
|
||||
@include navbar($accent_color);
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
background-color: #303030;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
#bottomnav {
|
||||
@include navbar($dark_bg);
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
background-color: #231c23;
|
||||
}
|
||||
|
||||
.darkbg {
|
||||
padding-bottom: 10px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
background-color: $dark_bg;
|
||||
background-color: #502d50;
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
@ -127,7 +90,7 @@ body {
|
||||
font-size: 3rem;
|
||||
margin: 0 20px;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.thread-title {
|
||||
@ -142,16 +105,15 @@ body {
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"usercard post-content-container";
|
||||
border: 2px outset $dark2;
|
||||
grid-template-areas: "usercard post-content-container";
|
||||
border: 2px outset rgb(96.95, 81.55, 96.95);
|
||||
}
|
||||
|
||||
.usercard {
|
||||
grid-area: usercard;
|
||||
padding: 20px 10px;
|
||||
border: 4px outset $light;
|
||||
background-color: $dark_bg;
|
||||
border: 4px outset #503250;
|
||||
background-color: #502d50;
|
||||
border-right: solid 2px;
|
||||
}
|
||||
|
||||
@ -167,14 +129,10 @@ body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
gap: 0px 0px;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"post-info"
|
||||
"post-content"
|
||||
"post-reactions";
|
||||
grid-template-areas: "post-info" "post-content" "post-reactions";
|
||||
grid-area: post-content-container;
|
||||
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@ -187,6 +145,7 @@ body {
|
||||
align-items: center;
|
||||
border-top: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
background-color: #412841;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
@ -195,9 +154,7 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background-color: $accent_color;
|
||||
|
||||
// min-height: 0;
|
||||
background-color: #231c23;
|
||||
}
|
||||
|
||||
.post-reactions {
|
||||
@ -208,17 +165,16 @@ body {
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
background-color: $main_bg;
|
||||
background-color: #503250;
|
||||
border-top: 2px dotted gray;
|
||||
}
|
||||
|
||||
.post-inner {
|
||||
height: 100%;
|
||||
padding-right: 25%;
|
||||
|
||||
&.wider {
|
||||
padding-right: 12.5%;
|
||||
}
|
||||
}
|
||||
.post-inner.wider {
|
||||
padding-right: 12.5%;
|
||||
}
|
||||
|
||||
.signature-container {
|
||||
@ -228,25 +184,50 @@ body {
|
||||
|
||||
pre code {
|
||||
display: block;
|
||||
background-color: $verydark;
|
||||
background-color: #302731;
|
||||
font-size: 1rem;
|
||||
color: white;
|
||||
border-bottom-right-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-left: 10px solid $lighter;
|
||||
border-left: 10px solid #ae6bae;
|
||||
padding: 20px;
|
||||
overflow: scroll;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.copy-code-container {
|
||||
position: sticky;
|
||||
width: calc(100% - 4px);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: last baseline;
|
||||
font-family: "Cadman";
|
||||
border-top-right-radius: 8px;
|
||||
border-top-left-radius: 8px;
|
||||
background-color: #9b649b;
|
||||
border-left: 2px solid black;
|
||||
border-right: 2px solid black;
|
||||
border-top: 2px solid black;
|
||||
}
|
||||
.copy-code-container::before {
|
||||
content: "code block";
|
||||
font-style: italic;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.copy-code {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.inline-code {
|
||||
background-color: $verydark;
|
||||
background-color: #302731;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
display: inline-block;
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#delete-dialog, .lightbox-dialog {
|
||||
@ -268,7 +249,8 @@ pre code {
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
min-width: 400px;
|
||||
background-color: $accent_color;
|
||||
background-color: #503250;
|
||||
color: #e6e6e6;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@ -284,39 +266,12 @@ pre code {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.copy-code-container {
|
||||
position: sticky;
|
||||
// width: 100%;
|
||||
width: calc(100% - 4px);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: last baseline;
|
||||
font-family: "Cadman";
|
||||
border-top-right-radius: 8px;
|
||||
border-top-left-radius: 8px;
|
||||
background-color: $accent_color;
|
||||
border-left: 2px solid black;
|
||||
border-right: 2px solid black;
|
||||
border-top: 2px solid black;
|
||||
|
||||
&::before {
|
||||
content: "code block";
|
||||
font-style: italic;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-code {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 10px 20px;
|
||||
margin: 10px;
|
||||
border-radius: 4px;
|
||||
border-left: 10px solid $lighter;
|
||||
// background-color: $dark2;
|
||||
background-color: #00000026;
|
||||
border-left: 10px solid #ae6bae;
|
||||
background-color: rgba(251, 175, 207, 0.0392156863);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
@ -324,15 +279,14 @@ blockquote {
|
||||
grid-template-columns: 300px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0;
|
||||
grid-template-areas:
|
||||
"user-page-usercard user-page-stats";
|
||||
grid-template-areas: "user-page-usercard user-page-stats";
|
||||
}
|
||||
|
||||
.user-page-usercard {
|
||||
grid-area: user-page-usercard;
|
||||
padding: 20px 10px;
|
||||
border: 4px outset $light;
|
||||
background-color: $dark_bg;
|
||||
border: 4px outset #503250;
|
||||
background-color: #502d50;
|
||||
border-right: solid 2px;
|
||||
}
|
||||
|
||||
@ -348,15 +302,15 @@ blockquote {
|
||||
}
|
||||
|
||||
.user-page-posts {
|
||||
border-left: solid 1px black;
|
||||
border-right: solid 1px black;
|
||||
border-bottom: solid 1px black;
|
||||
background-color: $accent_color;
|
||||
border-left: 1px solid black;
|
||||
border-right: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
background-color: #9b649b;
|
||||
}
|
||||
|
||||
.user-page-post-preview {
|
||||
max-height: 200px;
|
||||
mask-image: linear-gradient(180deg,#000 60%,transparent);
|
||||
mask-image: linear-gradient(180deg, #000 60%, transparent);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@ -374,24 +328,76 @@ blockquote {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button, input[type="submit"], .linkbutton {
|
||||
button, input[type=submit], .linkbutton {
|
||||
display: inline-block;
|
||||
@include button($button_color);
|
||||
|
||||
&.critical {
|
||||
color: white;
|
||||
@include button(red);
|
||||
}
|
||||
|
||||
&.warn {
|
||||
@include button(#fbfb8d);
|
||||
}
|
||||
background-color: #3c283c;
|
||||
color: #e6e6e6 !important;
|
||||
}
|
||||
button:hover, input[type=submit]:hover, .linkbutton:hover {
|
||||
background-color: rgb(109.2, 72.8, 109.2);
|
||||
}
|
||||
button:active, input[type=submit]:active, .linkbutton:active {
|
||||
background-color: rgb(47.7, 42.3, 47.7);
|
||||
}
|
||||
button:disabled, input[type=submit]:disabled, .linkbutton:disabled {
|
||||
background-color: rgb(113.73, 109.27, 113.73);
|
||||
}
|
||||
button.reduced, input[type=submit].reduced, .linkbutton.reduced {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
button.critical, input[type=submit].critical, .linkbutton.critical {
|
||||
background-color: #d53232;
|
||||
color: #e6e6e6 !important;
|
||||
}
|
||||
button.critical:hover, input[type=submit].critical:hover, .linkbutton.critical:hover {
|
||||
background-color: rgb(221.4, 91, 91);
|
||||
}
|
||||
button.critical:active, input[type=submit].critical:active, .linkbutton.critical:active {
|
||||
background-color: rgb(141.7804251012, 94.9195748988, 94.9195748988);
|
||||
}
|
||||
button.critical:disabled, input[type=submit].critical:disabled, .linkbutton.critical:disabled {
|
||||
background-color: rgb(174.255, 162.845, 162.845);
|
||||
}
|
||||
button.critical.reduced, input[type=submit].critical.reduced, .linkbutton.critical.reduced {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
button.warn, input[type=submit].warn, .linkbutton.warn {
|
||||
background-color: #eaea6a;
|
||||
color: black !important;
|
||||
}
|
||||
button.warn:hover, input[type=submit].warn:hover, .linkbutton.warn:hover {
|
||||
background-color: rgb(238.2, 238.2, 135.8);
|
||||
}
|
||||
button.warn:active, input[type=submit].warn:active, .linkbutton.warn:active {
|
||||
background-color: rgb(176.04, 176.04, 129.96);
|
||||
}
|
||||
button.warn:disabled, input[type=submit].warn:disabled, .linkbutton.warn:disabled {
|
||||
background-color: rgb(199.98, 199.98, 191.02);
|
||||
}
|
||||
button.warn.reduced, input[type=submit].warn.reduced, .linkbutton.warn.reduced {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
// not sure why this one has to be separate, but if it's included in the rule above everything breaks
|
||||
input[type="file"]::file-selector-button {
|
||||
@include button($button_color);
|
||||
margin: 10px 10px;
|
||||
input[type=file]::file-selector-button {
|
||||
background-color: #3c283c;
|
||||
color: #e6e6e6 !important;
|
||||
margin: 10px;
|
||||
}
|
||||
input[type=file]::file-selector-button:hover {
|
||||
background-color: rgb(109.2, 72.8, 109.2);
|
||||
}
|
||||
input[type=file]::file-selector-button:active {
|
||||
background-color: rgb(47.7, 42.3, 47.7);
|
||||
}
|
||||
input[type=file]::file-selector-button:disabled {
|
||||
background-color: rgb(113.73, 109.27, 113.73);
|
||||
}
|
||||
input[type=file]::file-selector-button.reduced {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
p {
|
||||
@ -399,16 +405,29 @@ p {
|
||||
}
|
||||
|
||||
.pagebutton {
|
||||
@include button($button_color);
|
||||
background-color: #3c283c;
|
||||
color: #e6e6e6 !important;
|
||||
padding: 5px 5px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.pagebutton:hover {
|
||||
background-color: rgb(109.2, 72.8, 109.2);
|
||||
}
|
||||
.pagebutton:active {
|
||||
background-color: rgb(47.7, 42.3, 47.7);
|
||||
}
|
||||
.pagebutton:disabled {
|
||||
background-color: rgb(113.73, 109.27, 113.73);
|
||||
}
|
||||
.pagebutton.reduced {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.currentpage {
|
||||
@extend %button-base;
|
||||
border: none;
|
||||
padding: 5px 5px;
|
||||
margin: 0;
|
||||
@ -422,7 +441,7 @@ p {
|
||||
}
|
||||
|
||||
.login-container > * {
|
||||
width: 25%;
|
||||
width: 40%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@ -438,32 +457,33 @@ p {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="password"], textarea, select {
|
||||
input[type=text], input[type=password], textarea, select {
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
padding: 7px 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
background-color: color.scale($accent_color, $lightness: 40%);
|
||||
|
||||
&:focus {
|
||||
background-color: color.scale($accent_color, $lightness: 60%);
|
||||
}
|
||||
color: #e6e6e6;
|
||||
background-color: #371e37;
|
||||
}
|
||||
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
||||
background-color: #514151;
|
||||
}
|
||||
|
||||
.infobox {
|
||||
border: 2px solid black;
|
||||
background-color: #81a3e6;
|
||||
background-color: #775891;
|
||||
padding: 20px 15px;
|
||||
|
||||
&.critical {
|
||||
background-color: rgb(237, 129, 129);
|
||||
}
|
||||
|
||||
&.warn {
|
||||
background-color: #fbfb8d;
|
||||
}
|
||||
color: #e6e6e6;
|
||||
}
|
||||
.infobox.critical {
|
||||
background-color: #d53232;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
.infobox.warn {
|
||||
background-color: #eaea6a;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.infobox > span {
|
||||
@ -480,21 +500,22 @@ input[type="text"], input[type="password"], textarea, select {
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1.6fr 96px;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0px 0px;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
min-height: 96px;
|
||||
grid-template-areas:
|
||||
"thread-sticky-container thread-info-container thread-locked-container";
|
||||
grid-template-areas: "thread-sticky-container thread-info-container thread-locked-container";
|
||||
}
|
||||
|
||||
.thread-sticky-container {
|
||||
grid-area: thread-sticky-container;
|
||||
border: 2px outset $light;
|
||||
border: 2px outset #231c23;
|
||||
background-color: #503250;
|
||||
}
|
||||
|
||||
.thread-locked-container {
|
||||
grid-area: thread-locked-container;
|
||||
border: 2px outset $light;
|
||||
border: 2px outset #231c23;
|
||||
background-color: #503250;
|
||||
}
|
||||
|
||||
.contain-svg {
|
||||
@ -502,25 +523,36 @@ input[type="text"], input[type="password"], textarea, select {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
&:not(.full) > svg, &:not(.full) > img {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
&.full > svg, &.full > img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.contain-svg:not(.full) > svg, .contain-svg:not(.full) > img {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
.contain-svg.full > svg, .contain-svg.full > img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.block-img {
|
||||
.post-img-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.post-image {
|
||||
object-fit: contain;
|
||||
max-width: 400px;
|
||||
max-height: 400px;
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
flex: 1 1 0%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.thread-info-container {
|
||||
grid-area: thread-info-container;
|
||||
background-color: $accent_color;
|
||||
background-color: #231c23;
|
||||
padding: 5px 20px;
|
||||
border-top: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
@ -528,7 +560,7 @@ input[type="text"], input[type="password"], textarea, select {
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
max-height: 110px;
|
||||
mask-image: linear-gradient(180deg,#000 60%,transparent);
|
||||
mask-image: linear-gradient(180deg, #000 60%, transparent);
|
||||
}
|
||||
|
||||
.thread-info-post-preview {
|
||||
@ -539,7 +571,7 @@ input[type="text"], input[type="password"], textarea, select {
|
||||
}
|
||||
|
||||
.babycode-guide-section {
|
||||
background-color: $accent_color;
|
||||
background-color: #231c23;
|
||||
padding: 5px 20px;
|
||||
border: 1px solid black;
|
||||
padding-right: 25%;
|
||||
@ -549,10 +581,9 @@ input[type="text"], input[type="password"], textarea, select {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 300px;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0px 0px;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"guide-topics guide-toc";
|
||||
grid-template-areas: "guide-topics guide-toc";
|
||||
}
|
||||
|
||||
.guide-topics {
|
||||
@ -566,9 +597,8 @@ input[type="text"], input[type="password"], textarea, select {
|
||||
top: 100px;
|
||||
align-self: start;
|
||||
padding: 10px;
|
||||
// border-top-right-radius: 16px;
|
||||
border-bottom-right-radius: 8px;
|
||||
background-color: $button_color;
|
||||
background-color: #3c233c;
|
||||
border-right: 1px solid black;
|
||||
border-top: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
@ -600,12 +630,12 @@ input[type="text"], input[type="password"], textarea, select {
|
||||
}
|
||||
|
||||
.colorful-table tr th {
|
||||
background-color: $button_color2;
|
||||
background-color: #503250;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.colorful-table tr td {
|
||||
background-color: $button_color;
|
||||
background-color: #231c23;
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
}
|
||||
@ -618,15 +648,14 @@ input[type="text"], input[type="password"], textarea, select {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 96px;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0px 0px;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"topic-info-container topic-locked-container";
|
||||
grid-template-areas: "topic-info-container topic-locked-container";
|
||||
}
|
||||
|
||||
.topic-info-container {
|
||||
grid-area: topic-info-container;
|
||||
background-color: $accent_color;
|
||||
background-color: #231c23;
|
||||
padding: 5px 20px;
|
||||
border: 1px solid black;
|
||||
display: flex;
|
||||
@ -635,26 +664,25 @@ input[type="text"], input[type="password"], textarea, select {
|
||||
|
||||
.topic-locked-container {
|
||||
grid-area: topic-locked-container;
|
||||
border: 2px outset $light;
|
||||
border: 2px outset #231c23;
|
||||
background-color: #503250;
|
||||
}
|
||||
|
||||
|
||||
.draggable-topic {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: $accent_color;
|
||||
background-color: #9b649b;
|
||||
padding: 20px;
|
||||
margin: 12px 0;
|
||||
border-top: 6px outset $light;
|
||||
border-bottom: 6px outset $dark2;
|
||||
|
||||
&.dragged {
|
||||
background-color: $button_color;
|
||||
}
|
||||
margin: 15px 0;
|
||||
border-top: 5px outset #503250;
|
||||
border-bottom: 5px outset rgb(96.95, 81.55, 96.95);
|
||||
}
|
||||
.draggable-topic.dragged {
|
||||
background-color: #3c283c;
|
||||
}
|
||||
|
||||
.editing {
|
||||
background-color: $light;
|
||||
background-color: #503250;
|
||||
}
|
||||
|
||||
.context-explain {
|
||||
@ -684,31 +712,43 @@ input[type="text"], input[type="password"], textarea, select {
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
@include button($button_color);
|
||||
background-color: #3c283c;
|
||||
color: #e6e6e6 !important;
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
&.active {
|
||||
background-color: $button_color2;
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
.tab-button:hover {
|
||||
background-color: rgb(109.2, 72.8, 109.2);
|
||||
}
|
||||
.tab-button:active {
|
||||
background-color: rgb(47.7, 42.3, 47.7);
|
||||
}
|
||||
.tab-button:disabled {
|
||||
background-color: rgb(113.73, 109.27, 113.73);
|
||||
}
|
||||
.tab-button.reduced {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
.tab-button.active {
|
||||
background-color: #8a5584;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
|
||||
&.active {
|
||||
min-height: 250px;
|
||||
display: block;
|
||||
background-color: color.adjust($button_color2, $saturation: -20%);
|
||||
border: 1px solid black;
|
||||
padding: 10px;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
}
|
||||
.tab-content.active {
|
||||
min-height: 250px;
|
||||
display: block;
|
||||
background-color: #503250;
|
||||
border: 1px solid black;
|
||||
padding: 10px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
@ -724,8 +764,8 @@ ul, ol {
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
right: 80px;
|
||||
border: 2px solid black;
|
||||
background-color: #81a3e6;
|
||||
border: 1px solid black;
|
||||
background-color: #775891;
|
||||
padding: 20px 15px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||
@ -737,8 +777,8 @@ ul, ol {
|
||||
}
|
||||
|
||||
.accordion {
|
||||
border-top-right-radius: 3px;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid black;
|
||||
margin: 10px 5px;
|
||||
@ -752,7 +792,7 @@ ul, ol {
|
||||
.accordion-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: $accordion_color;
|
||||
background-color: #7d467d;
|
||||
padding: 0 10px;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid black;
|
||||
@ -781,6 +821,12 @@ ul, ol {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.post-accordion-content {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
background-color: #2d212d;
|
||||
}
|
||||
|
||||
.inbox-container {
|
||||
padding: 10px;
|
||||
}
|
||||
@ -794,10 +840,9 @@ ul, ol {
|
||||
.babycode-button {
|
||||
padding: 5px 10px;
|
||||
min-width: 36px;
|
||||
|
||||
&> * {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
.babycode-button > * {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.quote-popover {
|
||||
@ -806,7 +851,7 @@ ul, ol {
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #00000080;
|
||||
background-color: rgba(0, 0, 0, 0.5019607843);
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
@ -815,7 +860,21 @@ footer {
|
||||
}
|
||||
|
||||
.reaction-button.active {
|
||||
@include button($button_color2);
|
||||
background-color: #8a5584;
|
||||
color: #e6e6e6 !important;
|
||||
}
|
||||
.reaction-button.active:hover {
|
||||
background-color: rgb(167.4843049327, 112.9156950673, 161.3067264574);
|
||||
}
|
||||
.reaction-button.active:active {
|
||||
background-color: rgb(107.505, 93.195, 105.885);
|
||||
}
|
||||
.reaction-button.active:disabled {
|
||||
background-color: rgb(156.9373766816, 152.1626233184, 156.396838565);
|
||||
}
|
||||
.reaction-button.active.reduced {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.reaction-popover {
|
||||
@ -823,7 +882,7 @@ footer {
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #000000b0;
|
||||
background-color: rgba(0, 0, 0, 0.5019607843);
|
||||
padding: 5px 10px;
|
||||
width: 250px;
|
||||
}
|
||||
@ -839,3 +898,12 @@ footer {
|
||||
.babycode-guide-list {
|
||||
border-bottom: 1px dashed;
|
||||
}
|
||||
|
||||
#topnav {
|
||||
margin-bottom: 10px;
|
||||
border: 10px solid rgb(40, 40, 40);
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 10px;
|
||||
}
|
929
data/static/css/theme-peachy.css
Normal file
929
data/static/css/theme-peachy.css
Normal file
@ -0,0 +1,929 @@
|
||||
@font-face {
|
||||
font-family: "site-title";
|
||||
src: url("/static/fonts/ChicagoFLF.woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Cadman";
|
||||
src: url("/static/fonts/Cadman_Roman.woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Cadman";
|
||||
src: url("/static/fonts/Cadman_Bold.woff2");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Cadman";
|
||||
src: url("/static/fonts/Cadman_Italic.woff2");
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Cadman";
|
||||
src: url("/static/fonts/Cadman_BoldItalic.woff2");
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
.reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton {
|
||||
cursor: default;
|
||||
font-size: 0.9em;
|
||||
font-family: "Cadman";
|
||||
text-decoration: none;
|
||||
border: 1px solid black;
|
||||
border-radius: 16px;
|
||||
padding: 8px 12px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Cadman";
|
||||
margin: 12px 50px;
|
||||
background-color: #c85d45;
|
||||
color: black;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: black;
|
||||
}
|
||||
a:visited {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.big {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
#topnav {
|
||||
padding: 6px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
background-color: #f27a5a;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
#bottomnav {
|
||||
padding: 6px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
background-color: #88486d;
|
||||
}
|
||||
|
||||
.darkbg {
|
||||
padding-bottom: 6px;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
background-color: #88486d;
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
column-gap: 8px;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
font-family: "site-title";
|
||||
font-size: 3rem;
|
||||
margin: 0 12px;
|
||||
text-decoration: none;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.thread-title {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.post {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas: "usercard post-content-container";
|
||||
border: 2px outset rgb(155.8907865169, 93.2211235955, 76.5092134831);
|
||||
}
|
||||
|
||||
.usercard {
|
||||
grid-area: usercard;
|
||||
padding: 12px 6px;
|
||||
border: none;
|
||||
background-color: #88486d;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.usercard-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
top: 6px;
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
.post-content-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas: "post-info" "post-content" "post-reactions";
|
||||
grid-area: post-content-container;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.post-info {
|
||||
grid-area: post-info;
|
||||
display: flex;
|
||||
min-height: 35px;
|
||||
justify-content: space-between;
|
||||
padding: 3px 12px;
|
||||
align-items: center;
|
||||
border-top: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
background-color: #c85d45;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
grid-area: post-content;
|
||||
padding: 12px 12px 0 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background-color: #f27a5a;
|
||||
}
|
||||
|
||||
.post-reactions {
|
||||
grid-area: post-reactions;
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
padding: 6px 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
background-color: #c85d45;
|
||||
border-top: 2px dotted #f7bfdf;
|
||||
}
|
||||
|
||||
.post-inner {
|
||||
height: 100%;
|
||||
padding-right: 25%;
|
||||
}
|
||||
.post-inner.wider {
|
||||
padding-right: 12.5%;
|
||||
}
|
||||
|
||||
.signature-container {
|
||||
border-top: 2px dotted #f7bfdf;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
pre code {
|
||||
display: block;
|
||||
background-color: rgb(41.7051685393, 28.2759550562, 24.6948314607);
|
||||
font-size: 1rem;
|
||||
color: white;
|
||||
border-bottom-right-radius: 16px;
|
||||
border-bottom-left-radius: 16px;
|
||||
border-left: 6px solid rgb(231.56, 212.36, 207.24);
|
||||
padding: 12px;
|
||||
overflow: scroll;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.copy-code-container {
|
||||
position: sticky;
|
||||
width: calc(100% - 4px);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: last baseline;
|
||||
font-family: "Cadman";
|
||||
border-top-right-radius: 16px;
|
||||
border-top-left-radius: 16px;
|
||||
background-color: #f27a5a;
|
||||
border-left: 2px solid black;
|
||||
border-right: 2px solid black;
|
||||
border-top: 2px solid black;
|
||||
}
|
||||
.copy-code-container::before {
|
||||
content: "code block";
|
||||
font-style: italic;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.copy-code {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.inline-code {
|
||||
background-color: rgb(41.7051685393, 28.2759550562, 24.6948314607);
|
||||
color: white;
|
||||
padding: 3px 6px;
|
||||
display: inline-block;
|
||||
margin: 4px;
|
||||
border-radius: 16px;
|
||||
font-size: 1rem;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#delete-dialog, .lightbox-dialog {
|
||||
padding: 0;
|
||||
border-radius: 16px;
|
||||
border: 2px solid black;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.delete-dialog-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.lightbox-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
min-width: 400px;
|
||||
background-color: #f27a5a;
|
||||
color: black;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.lightbox-image {
|
||||
max-width: 70vw;
|
||||
max-height: 70vh;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
|
||||
.lightbox-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 6px 12px;
|
||||
margin: 6px;
|
||||
border-radius: 16px;
|
||||
border-left: 6px solid rgb(231.56, 212.36, 207.24);
|
||||
background-color: rgba(0, 0, 0, 0.1333333333);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0;
|
||||
grid-template-areas: "user-page-usercard user-page-stats";
|
||||
}
|
||||
|
||||
.user-page-usercard {
|
||||
grid-area: user-page-usercard;
|
||||
padding: 12px 6px;
|
||||
border: none;
|
||||
background-color: #88486d;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.user-page-stats {
|
||||
grid-area: user-page-stats;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.user-stats-list {
|
||||
list-style: none;
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
.user-page-posts {
|
||||
border-left: 1px solid black;
|
||||
border-right: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
background-color: #f27a5a;
|
||||
}
|
||||
|
||||
.user-page-post-preview {
|
||||
max-height: 200px;
|
||||
mask-image: linear-gradient(180deg, #000 60%, transparent);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
object-fit: contain;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.username-link {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.user-status {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button, input[type=submit], .linkbutton {
|
||||
display: inline-block;
|
||||
background-color: #f27a5a;
|
||||
color: black !important;
|
||||
}
|
||||
button:hover, input[type=submit]:hover, .linkbutton:hover {
|
||||
background-color: rgb(244.6, 148.6, 123);
|
||||
}
|
||||
button:active, input[type=submit]:active, .linkbutton:active {
|
||||
background-color: rgb(176.4525842697, 133.7379775281, 122.3474157303);
|
||||
}
|
||||
button:disabled, input[type=submit]:disabled, .linkbutton:disabled {
|
||||
background-color: rgb(198.02, 189.62, 187.38);
|
||||
}
|
||||
button.reduced, input[type=submit].reduced, .linkbutton.reduced {
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
button.critical, input[type=submit].critical, .linkbutton.critical {
|
||||
background-color: #f73030;
|
||||
color: white !important;
|
||||
}
|
||||
button.critical:hover, input[type=submit].critical:hover, .linkbutton.critical:hover {
|
||||
background-color: rgb(248.6, 89.4, 89.4);
|
||||
}
|
||||
button.critical:active, input[type=submit].critical:active, .linkbutton.critical:active {
|
||||
background-color: rgb(166.6956976744, 98.8043023256, 98.8043023256);
|
||||
}
|
||||
button.critical:disabled, input[type=submit].critical:disabled, .linkbutton.critical:disabled {
|
||||
background-color: rgb(186.715, 172.785, 172.785);
|
||||
}
|
||||
button.critical.reduced, input[type=submit].critical.reduced, .linkbutton.critical.reduced {
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
button.warn, input[type=submit].warn, .linkbutton.warn {
|
||||
background-color: #fbfb8d;
|
||||
color: black !important;
|
||||
}
|
||||
button.warn:hover, input[type=submit].warn:hover, .linkbutton.warn:hover {
|
||||
background-color: rgb(251.8, 251.8, 163.8);
|
||||
}
|
||||
button.warn:active, input[type=submit].warn:active, .linkbutton.warn:active {
|
||||
background-color: rgb(198.3813559322, 198.3813559322, 154.4186440678);
|
||||
}
|
||||
button.warn:disabled, input[type=submit].warn:disabled, .linkbutton.warn:disabled {
|
||||
background-color: rgb(217.55, 217.55, 209.85);
|
||||
}
|
||||
button.warn.reduced, input[type=submit].warn.reduced, .linkbutton.warn.reduced {
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
input[type=file]::file-selector-button {
|
||||
background-color: #f27a5a;
|
||||
color: black !important;
|
||||
margin: 6px;
|
||||
}
|
||||
input[type=file]::file-selector-button:hover {
|
||||
background-color: rgb(244.6, 148.6, 123);
|
||||
}
|
||||
input[type=file]::file-selector-button:active {
|
||||
background-color: rgb(176.4525842697, 133.7379775281, 122.3474157303);
|
||||
}
|
||||
input[type=file]::file-selector-button:disabled {
|
||||
background-color: rgb(198.02, 189.62, 187.38);
|
||||
}
|
||||
input[type=file]::file-selector-button.reduced {
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.pagebutton {
|
||||
background-color: #f27a5a;
|
||||
color: black !important;
|
||||
padding: 3px 3px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
min-width: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
.pagebutton:hover {
|
||||
background-color: rgb(244.6, 148.6, 123);
|
||||
}
|
||||
.pagebutton:active {
|
||||
background-color: rgb(176.4525842697, 133.7379775281, 122.3474157303);
|
||||
}
|
||||
.pagebutton:disabled {
|
||||
background-color: rgb(198.02, 189.62, 187.38);
|
||||
}
|
||||
.pagebutton.reduced {
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.currentpage {
|
||||
border: none;
|
||||
padding: 3px 3px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
min-width: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modform {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.login-container > * {
|
||||
width: 60%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.settings-container > * {
|
||||
width: 60%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.avatar-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
input[type=text], input[type=password], textarea, select {
|
||||
border: 1px solid black;
|
||||
border-radius: 16px;
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
color: black;
|
||||
background-color: rgb(247.2, 175.2, 156);
|
||||
}
|
||||
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
||||
background-color: rgb(249.8, 201.8, 189);
|
||||
}
|
||||
|
||||
.infobox {
|
||||
border: 2px solid black;
|
||||
background-color: #81a3e6;
|
||||
padding: 12px 8px;
|
||||
color: black;
|
||||
}
|
||||
.infobox.critical {
|
||||
background-color: #f73030;
|
||||
color: white;
|
||||
}
|
||||
.infobox.warn {
|
||||
background-color: #fbfb8d;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.infobox > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.infobox-icon-container {
|
||||
min-width: 60px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.thread {
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1.6fr 96px;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
min-height: 96px;
|
||||
grid-template-areas: "thread-sticky-container thread-info-container thread-locked-container";
|
||||
}
|
||||
|
||||
.thread-sticky-container {
|
||||
grid-area: thread-sticky-container;
|
||||
border: 1px solid black;
|
||||
background-color: #f27a5a;
|
||||
}
|
||||
|
||||
.thread-locked-container {
|
||||
grid-area: thread-locked-container;
|
||||
border: 1px solid black;
|
||||
background-color: #f27a5a;
|
||||
}
|
||||
|
||||
.contain-svg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.contain-svg:not(.full) > svg, .contain-svg:not(.full) > img {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
.contain-svg.full > svg, .contain-svg.full > img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post-img-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.post-image {
|
||||
object-fit: contain;
|
||||
max-width: 400px;
|
||||
max-height: 400px;
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
flex: 1 1 0%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.thread-info-container {
|
||||
grid-area: thread-info-container;
|
||||
background-color: #f27a5a;
|
||||
padding: 3px 12px;
|
||||
border-top: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
max-height: 110px;
|
||||
mask-image: linear-gradient(180deg, #000 60%, transparent);
|
||||
}
|
||||
|
||||
.thread-info-post-preview {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline;
|
||||
margin-right: 25%;
|
||||
}
|
||||
|
||||
.babycode-guide-section {
|
||||
background-color: #f27a5a;
|
||||
padding: 3px 12px;
|
||||
border: 1px solid black;
|
||||
padding-right: 25%;
|
||||
}
|
||||
|
||||
.babycode-guide-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 300px;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas: "guide-topics guide-toc";
|
||||
}
|
||||
|
||||
.guide-topics {
|
||||
grid-area: guide-topics;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.guide-toc {
|
||||
grid-area: guide-toc;
|
||||
position: sticky;
|
||||
top: 100px;
|
||||
align-self: start;
|
||||
padding: 6px;
|
||||
border-bottom-right-radius: 8px;
|
||||
background-color: #f27a5a;
|
||||
border-right: 1px solid black;
|
||||
border-top: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.emoji-table tr td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emoji-table tr th {
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
.emoji-table {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.emoji-table, th, td {
|
||||
border: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.colorful-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.colorful-table tr th {
|
||||
background-color: #b54444;
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.colorful-table tr td {
|
||||
background-color: #f27a5a;
|
||||
padding: 3px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.colorful-table .small {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.topic {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 96px;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas: "topic-info-container topic-locked-container";
|
||||
}
|
||||
|
||||
.topic-info-container {
|
||||
grid-area: topic-info-container;
|
||||
background-color: #f27a5a;
|
||||
padding: 3px 12px;
|
||||
border: 1px solid black;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.topic-locked-container {
|
||||
grid-area: topic-locked-container;
|
||||
border: 1px solid black;
|
||||
background-color: #f27a5a;
|
||||
}
|
||||
|
||||
.draggable-topic {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: #f27a5a;
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
border-top: 5px outset rgb(219.84, 191.04, 183.36);
|
||||
border-bottom: 5px outset rgb(155.8907865169, 93.2211235955, 76.5092134831);
|
||||
}
|
||||
.draggable-topic.dragged {
|
||||
background-color: #f27a5a;
|
||||
}
|
||||
|
||||
.editing {
|
||||
background-color: rgb(219.84, 191.04, 183.36);
|
||||
}
|
||||
|
||||
.context-explain {
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.post-edit-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: baseline;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.babycode-editor {
|
||||
height: 150px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.babycode-editor-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.babycode-preview-errors-container {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background-color: #f27a5a;
|
||||
color: black !important;
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.tab-button:hover {
|
||||
background-color: rgb(244.6, 148.6, 123);
|
||||
}
|
||||
.tab-button:active {
|
||||
background-color: rgb(176.4525842697, 133.7379775281, 122.3474157303);
|
||||
}
|
||||
.tab-button:disabled {
|
||||
background-color: rgb(198.02, 189.62, 187.38);
|
||||
}
|
||||
.tab-button.reduced {
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
.tab-button.active {
|
||||
background-color: #b54444;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
.tab-content.active {
|
||||
min-height: 250px;
|
||||
display: block;
|
||||
background-color: rgb(156.1, 92.9, 92.9);
|
||||
border: 1px solid black;
|
||||
padding: 6px;
|
||||
border-top-right-radius: 16px;
|
||||
border-bottom-right-radius: 16px;
|
||||
border-bottom-left-radius: 16px;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 6px 0 6px 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.new-concept-notification.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.new-concept-notification {
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
right: 80px;
|
||||
border: 1px solid black;
|
||||
background-color: #81a3e6;
|
||||
padding: 12px 8px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.emoji {
|
||||
max-width: 15px;
|
||||
max-height: 15px;
|
||||
}
|
||||
|
||||
.accordion {
|
||||
border-top-right-radius: 16px;
|
||||
border-top-left-radius: 16px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid black;
|
||||
margin: 6px 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.accordion.hidden {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #c6655b;
|
||||
padding: 0 6px;
|
||||
gap: 6px;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.accordion-toggle {
|
||||
padding: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.accordion-title {
|
||||
margin-right: auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.accordion-content.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.post-accordion-content {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
background-color: #c85d45;
|
||||
}
|
||||
|
||||
.inbox-container {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.babycode-button-container {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.babycode-button {
|
||||
padding: 3px 6px;
|
||||
min-width: 36px;
|
||||
}
|
||||
.babycode-button > * {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.quote-popover {
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
background-color: rgba(0, 0, 0, 0.5019607843);
|
||||
padding: 3px 6px;
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top: 1px solid black;
|
||||
}
|
||||
|
||||
.reaction-button.active {
|
||||
background-color: #b54444;
|
||||
color: white !important;
|
||||
}
|
||||
.reaction-button.active:hover {
|
||||
background-color: rgb(197.978313253, 103.221686747, 103.221686747);
|
||||
}
|
||||
.reaction-button.active:active {
|
||||
background-color: rgb(127.305, 96.795, 96.795);
|
||||
}
|
||||
.reaction-button.active:disabled {
|
||||
background-color: rgb(167.7956024096, 159.5043975904, 159.5043975904);
|
||||
}
|
||||
.reaction-button.active.reduced {
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.reaction-popover {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
background-color: rgba(0, 0, 0, 0.5019607843);
|
||||
padding: 3px 6px;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.reaction-popover-inner {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow: scroll;
|
||||
margin: auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.babycode-guide-list {
|
||||
border-bottom: 1px dashed;
|
||||
}
|
||||
|
||||
#topnav {
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
}
|
||||
|
||||
#bottomnav {
|
||||
border-bottom-left-radius: 16px;
|
||||
border-bottom-right-radius: 16px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
textarea {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 10px;
|
||||
border-radius: 16px;
|
||||
border: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.darkbg {
|
||||
color: white;
|
||||
}
|
||||
.darkbg a {
|
||||
color: white;
|
||||
}
|
@ -48,6 +48,7 @@
|
||||
const buttonImg = document.getElementById("post-editor-img");
|
||||
const buttonOl = document.getElementById("post-editor-ol");
|
||||
const buttonUl = document.getElementById("post-editor-ul");
|
||||
const buttonSpoiler = document.getElementById("post-editor-spoiler");
|
||||
|
||||
function insertTag(tagStart, newline = false, prefill = "") {
|
||||
const hasAttr = tagStart[tagStart.length - 1] === "=";
|
||||
@ -130,6 +131,10 @@
|
||||
e.preventDefault();
|
||||
insertTag("ul", true);
|
||||
})
|
||||
buttonSpoiler.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("spoiler=", true, "hidden content");
|
||||
})
|
||||
|
||||
const previewEndpoint = "/api/babycode-preview";
|
||||
let previousMarkup = "";
|
||||
@ -173,5 +178,8 @@
|
||||
const json_resp = await req.json();
|
||||
previewContainer.innerHTML = json_resp.html;
|
||||
previewErrorsContainer.textContent = "";
|
||||
|
||||
const accordionRefreshEvt = new CustomEvent("refresh_accordions");
|
||||
document.body.dispatchEvent(accordionRefreshEvt);
|
||||
});
|
||||
}
|
||||
|
@ -110,8 +110,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
|
||||
// accordions
|
||||
const accordions = document.querySelectorAll(".accordion");
|
||||
accordions.forEach(accordion => {
|
||||
const handledAccordions = new Set();
|
||||
function attachAccordionHandlers(accordion){
|
||||
if(handledAccordions.has(accordion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handledAccordions.add(accordion)
|
||||
const header = accordion.querySelector(".accordion-header");
|
||||
const toggleButton = header.querySelector(".accordion-toggle");
|
||||
const content = accordion.querySelector(".accordion-content");
|
||||
@ -124,12 +129,47 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
|
||||
toggleButton.addEventListener("click", toggle);
|
||||
});
|
||||
}
|
||||
|
||||
function refreshAccordions(){
|
||||
const accordions = document.querySelectorAll(".accordion");
|
||||
accordions.forEach(attachAccordionHandlers);
|
||||
}
|
||||
refreshAccordions()
|
||||
|
||||
document.body.addEventListener('refresh_accordions', refreshAccordions)
|
||||
|
||||
//lightboxes
|
||||
lightboxObj = constructLightbox();
|
||||
document.body.appendChild(lightboxObj.dialog);
|
||||
const postImages = document.querySelectorAll(".post-inner img.block-img");
|
||||
|
||||
function setImageMaxSize(img) {
|
||||
const {
|
||||
maxWidth: origMaxWidth,
|
||||
maxHeight: origMaxHeight,
|
||||
minWidth: origMinWidth,
|
||||
minHeight: origMinHeight,
|
||||
} = getComputedStyle(img);
|
||||
console.log(img, img.naturalWidth, img.naturalHeight, origMinWidth, origMinHeight, origMaxWidth, origMaxHeight)
|
||||
if (img.naturalWidth < parseInt(origMinWidth)) {
|
||||
console.log(1)
|
||||
img.style.minWidth = img.naturalWidth + "px";
|
||||
}
|
||||
if (img.naturalHeight < parseInt(origMinHeight)) {
|
||||
console.log(2)
|
||||
img.style.minHeight = img.naturalHeight + "px";
|
||||
}
|
||||
if (img.naturalWidth < parseInt(origMaxWidth)) {
|
||||
console.log(3)
|
||||
img.style.maxWidth = img.naturalWidth + "px";
|
||||
}
|
||||
if (img.naturalHeight < parseInt(origMaxHeight)) {
|
||||
console.log(4)
|
||||
img.style.maxHeight = img.naturalHeight + "px";
|
||||
}
|
||||
}
|
||||
|
||||
const postImages = document.querySelectorAll(".post-inner img.post-image");
|
||||
postImages.forEach(postImage => {
|
||||
const belongingTo = postImage.closest(".post-inner");
|
||||
const images = lightboxImages.get(belongingTo) ?? [];
|
||||
@ -144,6 +184,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
openLightbox(belongingTo, idx);
|
||||
});
|
||||
});
|
||||
const postAndSigImages = document.querySelectorAll("img.post-image");
|
||||
postAndSigImages.forEach(image => {
|
||||
if (image.complete) {
|
||||
setImageMaxSize(image);
|
||||
} else {
|
||||
image.addEventListener("load", () => setImageMaxSize(image));
|
||||
}
|
||||
})
|
||||
|
||||
// copy code blocks
|
||||
for (let button of document.querySelectorAll(".copy-code")) {
|
||||
|
5
data/static/misc/spoiler.svg
Normal file
5
data/static/misc/spoiler.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 4L9.87868 9.87868M20 20L14.1213 14.1213M9.87868 9.87868C9.33579 10.4216 9 11.1716 9 12C9 13.6569 10.3431 15 12 15C12.8284 15 13.5784 14.6642 14.1213 14.1213M9.87868 9.87868L14.1213 14.1213M6.76821 6.76821C4.72843 8.09899 2.96378 10.026 2 11.9998C3.74646 15.5764 8.12201 19 11.9998 19C13.7376 19 15.5753 18.3124 17.2317 17.2317M9.76138 5.34717C10.5114 5.12316 11.2649 5 12.0005 5C15.8782 5 20.2531 8.42398 22 12.0002C21.448 13.1302 20.6336 14.2449 19.6554 15.2412" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<!-- https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license -->
|
After Width: | Height: | Size: 814 B |
1097
sass/_default.scss
Normal file
1097
sass/_default.scss
Normal file
File diff suppressed because it is too large
Load Diff
85
sass/otomotone.scss
Normal file
85
sass/otomotone.scss
Normal file
@ -0,0 +1,85 @@
|
||||
$fc: #e6e6e6;
|
||||
$fci: black;
|
||||
|
||||
$lightish_accent: #503250;
|
||||
$lightish_accent2: #502d50;
|
||||
$dark_accent: #231c23;
|
||||
|
||||
$warn: #eaea6a;
|
||||
$crit: #d53232;
|
||||
|
||||
@use 'default' with (
|
||||
$ACCENT_COLOR: #9b649b,
|
||||
|
||||
$MAIN_BG: #220d16,
|
||||
$DARK_1: $lightish_accent2,
|
||||
$DARK_3: #302731,
|
||||
|
||||
$LIGHT_2: #ae6bae,
|
||||
$LIGHT: $lightish_accent,
|
||||
|
||||
$DEFAULT_FONT_COLOR: $fc,
|
||||
$DEFAULT_FONT_COLOR_INVERSE: $fci,
|
||||
|
||||
$BUTTON_COLOR: #3c283c,
|
||||
$BUTTON_COLOR_2: #8a5584,
|
||||
$BUTTON_FONT_COLOR: $fc,
|
||||
$BUTTON_COLOR_WARN: $warn,
|
||||
$BUTTON_WARN_FONT_COLOR: $fci,
|
||||
$BUTTON_COLOR_CRITICAL: $crit,
|
||||
$BUTTON_CRITICAL_FONT_COLOR: $fc,
|
||||
$ACCORDION_COLOR: #7d467d,
|
||||
|
||||
$bottomnav_color: $dark_accent,
|
||||
|
||||
$topic_info_background: $dark_accent,
|
||||
$topic_locked_background: $lightish_accent,
|
||||
$thread_locked_background: $lightish_accent,
|
||||
$thread_locked_border: 2px outset $dark_accent,
|
||||
|
||||
$site_title_color: white,
|
||||
$topnav_color: #303030,
|
||||
|
||||
$quote_background_color: #fbafcf0a,
|
||||
|
||||
$link_color: #e87fe1,
|
||||
$link_color_visited: #ed4fb1,
|
||||
|
||||
$post_info_background: #412841,
|
||||
$post_content_background: $dark_accent,
|
||||
|
||||
$thread_info_background_color: $dark_accent,
|
||||
|
||||
$post_reactions_background: $lightish_accent,
|
||||
|
||||
$post_accordion_content_background: #2d212d,
|
||||
|
||||
$babycode_guide_toc_background: #3c233c,
|
||||
$babycode_guide_section_background: $dark_accent,
|
||||
|
||||
$text_input_background: #371e37,
|
||||
$text_input_background_focus: #514151,
|
||||
$text_input_font_color: $fc,
|
||||
|
||||
$colorful_table_th_color: $lightish_accent,
|
||||
$colorful_table_td_color: $dark_accent,
|
||||
|
||||
$lightbox_background: $lightish_accent,
|
||||
|
||||
$infobox_info_color: #775891,
|
||||
$infobox_warn_color: $warn,
|
||||
$infobox_warn_font_color: $fci,
|
||||
$infobox_critical_color: $crit,
|
||||
|
||||
$tab_content_background: $lightish_accent,
|
||||
$tab_button_active_color: #8a5584,
|
||||
);
|
||||
|
||||
#topnav {
|
||||
margin-bottom: 10px;
|
||||
border: 10px solid rgb(40, 40, 40);
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 10px;
|
||||
}
|
97
sass/peachy.scss
Normal file
97
sass/peachy.scss
Normal file
@ -0,0 +1,97 @@
|
||||
// $accent: #dd5536;
|
||||
$accent: #f27a5a;
|
||||
|
||||
$br: 16px;
|
||||
|
||||
@use 'default' with (
|
||||
$ACCENT_COLOR: $accent,
|
||||
$thread_locked_background: $accent,
|
||||
$topic_locked_background: $accent,
|
||||
|
||||
// $DARK_1: #e36286,
|
||||
$DARK_1: #88486d,
|
||||
$MAIN_BG: #c85d45,
|
||||
|
||||
$usercard_border: none,
|
||||
$usercard_border_right: none,
|
||||
$thread_locked_border: 1px solid black,
|
||||
|
||||
$SETTINGS_WIDTH: 60%,
|
||||
$PAGE_SIDE_MARGIN: 50px,
|
||||
|
||||
$link_color: black,
|
||||
$link_color_visited: black,
|
||||
$reaction_button_active_font_color: white,
|
||||
// $DEFAULT_FONT_COLOR: white,
|
||||
// $DEFAULT_FONT_COLOR_INVERSE: black,
|
||||
|
||||
$text_input_font_color: black,
|
||||
|
||||
$BUTTON_COLOR: $accent,
|
||||
$BUTTON_COLOR_2: #b54444,
|
||||
$BUTTON_COLOR_CRITICAL: #f73030,
|
||||
$ACCORDION_COLOR: #c6655b,
|
||||
$BUTTON_WARN_FONT_COLOR: black,
|
||||
$BUTTON_CRITICAL_FONT_COLOR: white,
|
||||
|
||||
$SMALL_PADDING: 3px,
|
||||
$MEDIUM_PADDING: 6px,
|
||||
$MEDIUM_BIG_PADDING: 8px,
|
||||
$BIG_PADDING: 12px,
|
||||
$BIGGER_PADDING: 16px,
|
||||
|
||||
$DEFAULT_BORDER_RADIUS: $br,
|
||||
$code_border_radius: $br,
|
||||
$button_padding: 8px 12px,
|
||||
$reduced_button_padding: 6px,
|
||||
|
||||
$post_reactions_border_top: 2px dotted #f7bfdf,
|
||||
|
||||
$post_info_min_height: 35px,
|
||||
$post_reactions_padding: 6px 12px,
|
||||
$post_reactions_gap: 6px,
|
||||
|
||||
$text_input_padding: 8px,
|
||||
|
||||
$infobox_info_color: #81a3e6,
|
||||
$infobox_critical_color: #f73030,
|
||||
$infobox_warn_color: #fbfb8d,
|
||||
|
||||
$infobox_info_font_color: black,
|
||||
$infobox_critical_font_color: white,
|
||||
$infobox_warn_font_color: black,
|
||||
|
||||
$pagebutton_min_width: 36px,
|
||||
$quote_background_color: #0002,
|
||||
);
|
||||
|
||||
#topnav {
|
||||
border-top-left-radius: $br;
|
||||
border-top-right-radius: $br;
|
||||
}
|
||||
|
||||
#bottomnav {
|
||||
border-bottom-left-radius: $br;
|
||||
border-bottom-right-radius: $br;
|
||||
|
||||
color: white;
|
||||
}
|
||||
|
||||
textarea {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 10px;
|
||||
border-radius: $br;
|
||||
border: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.darkbg {
|
||||
color: white;
|
||||
|
||||
& a {
|
||||
color: white;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user