Merge branch 'main' into misc/add_signatures_to_requests_for_masto_compat

This commit is contained in:
Mouse Reeve 2022-03-01 10:20:39 -08:00 committed by GitHub
commit d2dab0f2db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
376 changed files with 28405 additions and 10510 deletions

View file

@ -0,0 +1,3 @@
@charset "utf-8";
// Copy this file to bookwyrm/static/css/ and set your instance custom styles.

View file

@ -8,6 +8,8 @@ USE_HTTPS=true
DOMAIN=your.domain.here
EMAIL=your@email.here
# Instance defualt language (see options at bookwyrm/settings.py "LANGUAGES"
LANGUAGE_CODE="en-us"
# Used for deciding which editions to prefer
DEFAULT_LANGUAGE="English"
@ -39,7 +41,7 @@ REDIS_BROKER_PASSWORD=redispassword123
# Monitoring for celery
FLOWER_PORT=8888
FLOWER_USER=mouse
FLOWER_USER=admin
FLOWER_PASSWORD=changeme
# Email config
@ -87,3 +89,22 @@ PREVIEW_TEXT_COLOR=#363636
PREVIEW_IMG_WIDTH=1200
PREVIEW_IMG_HEIGHT=630
PREVIEW_DEFAULT_COVER_COLOR=#002549
# Below are example keys if you want to enable automatically
# sending telemetry to an OTLP-compatible service. Many of
# the main monitoring apps have OLTP collectors, including
# NewRelic, DataDog, and Honeycomb.io - consult their
# documentation for setup instructions, and what exactly to
# put below!
#
# Service name is an arbitrary tag that is attached to any
# data sent, used to distinguish different sources. Useful
# for sending prod and dev metrics to the same place and
# keeping them separate, for instance!
# API endpoint for your provider
OTEL_EXPORTER_OTLP_ENDPOINT=
# Any headers required, usually authentication info
OTEL_EXPORTER_OTLP_HEADERS=
# Service name to identify your app
OTEL_SERVICE_NAME=

View file

@ -1,5 +1,5 @@
# @url https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
name: Lint Frontend
name: Lint Frontend (run `./bw-dev stylelint` to fix css errors)
on:
push:
@ -8,7 +8,7 @@ on:
- '.github/workflows/**'
- 'static/**'
- '.eslintrc'
- '.stylelintrc'
- '.stylelintrc.js'
pull_request:
branches: [ main, ci, frontend ]
@ -22,17 +22,16 @@ jobs:
- uses: actions/checkout@v2
- name: Install modules
run: yarn
run: npm install stylelint stylelint-config-recommended stylelint-config-standard stylelint-order eslint
# See .stylelintignore for files that are not linted.
- name: Run stylelint
run: >
yarn stylelint bookwyrm/static/**/*.css \
--report-needless-disables \
--report-invalid-scope-disables
npx stylelint bookwyrm/static/css/*.scss bookwyrm/static/css/bookwyrm/**/*.scss \
--config dev-tools/.stylelintrc.js
# See .eslintignore for files that are not linted.
- name: Run ESLint
run: >
yarn eslint bookwyrm/static \
npx eslint bookwyrm/static \
--ext .js,.jsx,.ts,.tsx

View file

@ -1,21 +0,0 @@
# @url https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
name: Lint project globally
on:
push:
branches: [ main, ci ]
pull_request:
branches: [ main, ci ]
jobs:
lint:
name: Lint with EditorConfig.
runs-on: ubuntu-20.04
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: EditorConfig
uses: greut/eclint-action@v0

View file

@ -17,8 +17,7 @@ jobs:
- uses: actions/checkout@v2
- name: Install modules
run: npm install .
run: npm install prettier
# See .stylelintignore for files that are not linted.
- name: Run Prettier
run: npx prettier --check bookwyrm/static/js/*.js

6
.gitignore vendored
View file

@ -16,6 +16,8 @@
# BookWyrm
.env
/images/
bookwyrm/static/css/bookwyrm.css
bookwyrm/static/css/_instance-settings.scss
# Testing
.coverage
@ -24,7 +26,9 @@
.idea
#Node tools
/node_modules/
node_modules/
package-lock.json
yarn.lock
#nginx
nginx/default.conf

View file

@ -13,7 +13,7 @@ Social reading and reviewing, decentralized with ActivityPub
- [Set up Bookwyrm](#set-up-bookwyrm)
## Joining BookWyrm
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://docs.joinbookwyrm.com/instances.html) list.
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://joinbookwyrm.com/instances/) list.
You can request an invite by entering your email address at https://bookwyrm.social.

View file

@ -232,7 +232,7 @@ def set_related_field(
model_field = getattr(model, related_field_name)
if hasattr(model_field, "activitypub_field"):
setattr(activity, getattr(model_field, "activitypub_field"), instance.remote_id)
item = activity.to_model()
item = activity.to_model(model=model)
# if the related field isn't serialized (attachments on Status), then
# we have to set it post-creation
@ -348,6 +348,7 @@ class Link(ActivityObject):
mediaType: str = None
id: str = None
attributedTo: str = None
availability: str = None
type: str = "Link"
def serialize(self, **kwargs):

View file

@ -16,6 +16,9 @@ class BookData(ActivityObject):
librarythingKey: str = None
goodreadsKey: str = None
bnfId: str = None
viaf: str = None
wikidata: str = None
asin: str = None
lastEditedBy: str = None
links: List[str] = field(default_factory=lambda: [])
fileLinks: List[str] = field(default_factory=lambda: [])
@ -27,8 +30,8 @@ class Book(BookData):
"""serializes an edition or work, abstract"""
title: str
sortTitle: str = ""
subtitle: str = ""
sortTitle: str = None
subtitle: str = None
description: str = ""
languages: List[str] = field(default_factory=lambda: [])
series: str = ""
@ -53,7 +56,6 @@ class Edition(Book):
isbn10: str = ""
isbn13: str = ""
oclcNumber: str = ""
asin: str = ""
pages: int = None
physicalFormat: str = ""
physicalFormatDetail: str = ""

View file

@ -39,4 +39,5 @@ class Person(ActivityObject):
bookwyrmUser: bool = False
manuallyApprovesFollowers: str = False
discoverable: str = False
hideFollows: str = False
type: str = "Person"

View file

@ -38,7 +38,7 @@ class Create(Verb):
class Delete(Verb):
"""Create activity"""
to: List[str]
to: List[str] = field(default_factory=lambda: [])
cc: List[str] = field(default_factory=lambda: [])
type: str = "Delete"
@ -137,8 +137,8 @@ class Accept(Verb):
type: str = "Accept"
def action(self):
"""find and remove the activity object"""
obj = self.object.to_model(save=False, allow_create=False)
"""accept a request"""
obj = self.object.to_model(save=False, allow_create=True)
obj.accept()
@ -150,7 +150,7 @@ class Reject(Verb):
type: str = "Reject"
def action(self):
"""find and remove the activity object"""
"""reject a follow request"""
obj = self.object.to_model(save=False, allow_create=False)
obj.reject()

54
bookwyrm/apps.py Normal file
View file

@ -0,0 +1,54 @@
"""Do further startup configuration and initialization"""
import os
import urllib
import logging
from django.apps import AppConfig
from bookwyrm import settings
logger = logging.getLogger(__name__)
def download_file(url, destination):
"""Downloads a file to the given path"""
try:
# Ensure our destination directory exists
os.makedirs(os.path.dirname(destination))
with urllib.request.urlopen(url) as stream:
with open(destination, "b+w") as outfile:
outfile.write(stream.read())
except (urllib.error.HTTPError, urllib.error.URLError):
logger.error("Failed to download file %s", url)
except OSError:
logger.error("Couldn't open font file %s for writing", destination)
except: # pylint: disable=bare-except
logger.exception("Unknown error in file download")
class BookwyrmConfig(AppConfig):
"""Handles additional configuration"""
name = "bookwyrm"
verbose_name = "BookWyrm"
# pylint: disable=no-self-use
def ready(self):
"""set up OTLP and preview image files, if desired"""
if settings.OTEL_EXPORTER_OTLP_ENDPOINT:
# pylint: disable=import-outside-toplevel
from bookwyrm.telemetry import open_telemetry
open_telemetry.instrumentDjango()
if settings.ENABLE_PREVIEW_IMAGES and settings.FONTS:
# Download any fonts that we don't have yet
logger.debug("Downloading fonts..")
for name, config in settings.FONTS.items():
font_path = os.path.join(
settings.FONT_DIR, config["directory"], config["filename"]
)
if "url" in config and not os.path.exists(font_path):
logger.info("Just a sec, downloading %s", name)
download_file(config["url"], font_path)

View file

@ -1,7 +1,11 @@
""" functionality outline for a book data connector """
from abc import ABC, abstractmethod
import imghdr
import ipaddress
import logging
from urllib.parse import urlparse
from django.core.files.base import ContentFile
from django.db import transaction
import requests
from requests.exceptions import RequestException
@ -248,6 +252,8 @@ def dict_from_mappings(data, mappings):
def get_data(url, params=None, timeout=10):
"""wrapper for request.get"""
# check if the url is blocked
raise_not_valid_url(url)
if models.FederatedServer.is_blocked(url):
raise ConnectorException(f"Attempting to load data from blocked url: {url}")
@ -280,6 +286,7 @@ def get_data(url, params=None, timeout=10):
def get_image(url, timeout=10):
"""wrapper for requesting an image"""
raise_not_valid_url(url)
try:
resp = requests.get(
url,
@ -290,10 +297,32 @@ def get_image(url, timeout=10):
)
except RequestException as err:
logger.exception(err)
return None
return None, None
if not resp.ok:
return None
return resp
return None, None
image_content = ContentFile(resp.content)
extension = imghdr.what(None, image_content.read())
if not extension:
logger.exception("File requested was not an image: %s", url)
return None, None
return image_content, extension
def raise_not_valid_url(url):
"""do some basic reality checks on the url"""
parsed = urlparse(url)
if not parsed.scheme in ["http", "https"]:
raise ConnectorException("Invalid scheme: ", url)
try:
ipaddress.ip_address(parsed.netloc)
raise ConnectorException("Provided url is an IP address: ", url)
except ValueError:
# it's not an IP address, which is good
pass
class Mapping:

View file

@ -68,7 +68,30 @@ class Connector(AbstractConnector):
Mapping("born", remote_field="birth_date"),
Mapping("died", remote_field="death_date"),
Mapping("bio", formatter=get_description),
Mapping("isni", remote_field="remote_ids", formatter=get_isni),
Mapping(
"isni",
remote_field="remote_ids",
formatter=lambda b: get_dict_field(b, "isni"),
),
Mapping(
"asin",
remote_field="remote_ids",
formatter=lambda b: get_dict_field(b, "amazon"),
),
Mapping(
"viaf",
remote_field="remote_ids",
formatter=lambda b: get_dict_field(b, "viaf"),
),
Mapping(
"wikidata",
remote_field="remote_ids",
formatter=lambda b: get_dict_field(b, "wikidata"),
),
Mapping(
"wikipedia_link", remote_field="links", formatter=get_wikipedia_link
),
Mapping("inventaire_id", remote_field="links", formatter=get_inventaire_id),
]
def get_book_data(self, remote_id):
@ -227,11 +250,38 @@ def get_languages(language_blob):
return langs
def get_isni(remote_ids_blob):
def get_dict_field(blob, field_name):
"""extract the isni from the remote id data for the author"""
if not remote_ids_blob or not isinstance(remote_ids_blob, dict):
if not blob or not isinstance(blob, dict):
return None
return remote_ids_blob.get("isni")
return blob.get(field_name)
def get_wikipedia_link(links):
"""extract wikipedia links"""
if not isinstance(links, list):
return None
for link in links:
if not isinstance(link, dict):
continue
if link.get("title") == "wikipedia":
return link.get("url")
return None
def get_inventaire_id(links):
"""extract and format inventaire ids"""
if not isinstance(links, list):
return None
for link in links:
if not isinstance(link, dict):
continue
if link.get("title") == "inventaire.io":
iv_link = link.get("url")
return iv_link.split("/")[-1]
return None
def pick_default_edition(options):

View file

@ -48,7 +48,9 @@ def moderation_report_email(report):
data["reportee"] = report.user.localname or report.user.username
data["report_link"] = report.remote_id
for admin in models.User.objects.filter(groups__name__in=["admin", "moderator"]):
for admin in models.User.objects.filter(
groups__name__in=["admin", "moderator"]
).distinct():
data["user"] = admin.display_name
send_email.delay(admin.email, *format_email("moderation_report", data))

View file

@ -1,6 +1,7 @@
""" using django model forms """
import datetime
from collections import defaultdict
from urllib.parse import urlparse
from django import forms
from django.forms import ModelForm, PasswordInput, widgets, ChoiceField
@ -53,6 +54,13 @@ class RegisterForm(CustomForm):
help_texts = {f: None for f in fields}
widgets = {"password": PasswordInput()}
def clean(self):
"""Check if the username is taken"""
cleaned_data = super().clean()
localname = cleaned_data.get("localname").strip()
if models.User.objects.filter(localname=localname).first():
self.add_error("localname", _("User with this username already exists"))
class RatingForm(CustomForm):
class Meta:
@ -145,6 +153,7 @@ class EditUserForm(CustomForm):
"manually_approves_followers",
"default_post_privacy",
"discoverable",
"hide_follows",
"preferred_timezone",
"preferred_language",
]
@ -225,7 +234,35 @@ class LinkDomainForm(CustomForm):
class FileLinkForm(CustomForm):
class Meta:
model = models.FileLink
fields = ["url", "filetype", "book", "added_by"]
fields = ["url", "filetype", "availability", "book", "added_by"]
def clean(self):
"""make sure the domain isn't blocked or pending"""
cleaned_data = super().clean()
url = cleaned_data.get("url")
filetype = cleaned_data.get("filetype")
book = cleaned_data.get("book")
domain = urlparse(url).netloc
if models.LinkDomain.objects.filter(domain=domain).exists():
status = models.LinkDomain.objects.get(domain=domain).status
if status == "blocked":
# pylint: disable=line-too-long
self.add_error(
"url",
_(
"This domain is blocked. Please contact your administrator if you think this is an error."
),
)
elif models.FileLink.objects.filter(
url=url, book=book, filetype=filetype
).exists():
# pylint: disable=line-too-long
self.add_error(
"url",
_(
"This link with file type has already been added for this book. If it is not visible, the domain is still pending."
),
)
class EditionForm(CustomForm):
@ -404,7 +441,7 @@ class GoalForm(CustomForm):
class SiteForm(CustomForm):
class Meta:
model = models.SiteSettings
exclude = []
exclude = ["admin_code", "install_mode"]
widgets = {
"instance_short_description": forms.TextInput(
attrs={"aria-describedby": "desc_instance_short_description"}
@ -444,6 +481,12 @@ class ListForm(CustomForm):
fields = ["user", "name", "description", "curation", "privacy", "group"]
class ListItemForm(CustomForm):
class Meta:
model = models.ListItem
fields = ["user", "book", "book_list", "notes"]
class GroupForm(CustomForm):
class Meta:
model = models.Group
@ -453,7 +496,7 @@ class GroupForm(CustomForm):
class ReportForm(CustomForm):
class Meta:
model = models.Report
fields = ["user", "reporter", "statuses", "links", "note"]
fields = ["user", "reporter", "status", "links", "note"]
class EmailBlocklistForm(CustomForm):
@ -500,7 +543,7 @@ class ReadThroughForm(CustomForm):
cleaned_data = super().clean()
start_date = cleaned_data.get("start_date")
finish_date = cleaned_data.get("finish_date")
if start_date > finish_date:
if start_date and finish_date and start_date > finish_date:
self.add_error(
"finish_date", _("Reading finish date cannot be before start date.")
)
@ -508,3 +551,9 @@ class ReadThroughForm(CustomForm):
class Meta:
model = models.ReadThrough
fields = ["user", "book", "start_date", "finish_date"]
class AutoModRuleForm(CustomForm):
class Meta:
model = models.AutoMod
fields = ["string_match", "flag_users", "flag_statuses", "created_by"]

View file

@ -0,0 +1,23 @@
""" Get your admin code to allow install """
from django.core.management.base import BaseCommand
from bookwyrm import models
def get_admin_code():
"""get that code"""
return models.SiteSettings.objects.get().admin_code
class Command(BaseCommand):
"""command-line options"""
help = "Gets admin code for configuring BookWyrm"
# pylint: disable=unused-argument
def handle(self, *args, **options):
"""execute init"""
self.stdout.write("*******************************************")
self.stdout.write("Use this code to create your admin account:")
self.stdout.write(get_admin_code())
self.stdout.write("*******************************************")

View file

@ -7,6 +7,7 @@ from bookwyrm import settings
r = redis.Redis(
host=settings.REDIS_ACTIVITY_HOST,
port=settings.REDIS_ACTIVITY_PORT,
password=settings.REDIS_ACTIVITY_PASSWORD,
db=settings.REDIS_ACTIVITY_DB_INDEX,
)

View file

@ -10,7 +10,9 @@ class Command(BaseCommand):
help = "Generate preview images"
# pylint: disable=no-self-use
def add_arguments(self, parser):
"""options for how the command is run"""
parser.add_argument(
"--all",
"-a",
@ -38,6 +40,7 @@ class Command(BaseCommand):
preview_images.generate_site_preview_image_task.delay()
self.stdout.write(" OK 🖼")
# pylint: disable=consider-using-f-string
if options["all"]:
# Users
users = models.User.objects.filter(

View file

@ -19,9 +19,7 @@ def init_permissions():
{
"codename": "edit_instance_settings",
"name": "change the instance info",
"groups": [
"admin",
],
"groups": ["admin"],
},
{
"codename": "set_user_group",
@ -55,7 +53,7 @@ def init_permissions():
},
]
content_type = models.ContentType.objects.get_for_model(User)
content_type = ContentType.objects.get_for_model(models.User)
for permission in permissions:
permission_obj = Permission.objects.create(
codename=permission["codename"],
@ -66,15 +64,12 @@ def init_permissions():
for group_name in permission["groups"]:
Group.objects.get(name=group_name).permissions.add(permission_obj)
# while the groups and permissions shouldn't be changed because the code
# depends on them, what permissions go with what groups should be editable
def init_connectors():
"""access book data sources"""
models.Connector.objects.create(
identifier="bookwyrm.social",
name="BookWyrm dot Social",
name="Bookwyrm.social",
connector_file="bookwyrm_connector",
base_url="https://bookwyrm.social",
books_url="https://bookwyrm.social/book",
@ -84,6 +79,7 @@ def init_connectors():
priority=2,
)
# pylint: disable=line-too-long
models.Connector.objects.create(
identifier="inventaire.io",
name="Inventaire",
@ -124,10 +120,11 @@ def init_settings():
models.SiteSettings.objects.create(
support_link="https://www.patreon.com/bookwyrm",
support_title="Patreon",
install_mode=True,
)
def init_link_domains(*_):
def init_link_domains():
"""safe book links"""
domains = [
("standardebooks.org", "Standard EBooks"),
@ -144,10 +141,15 @@ def init_link_domains(*_):
)
# pylint: disable=no-self-use
# pylint: disable=unused-argument
class Command(BaseCommand):
"""command-line options"""
help = "Initializes the database with starter data"
def add_arguments(self, parser):
"""specify which function to run"""
parser.add_argument(
"--limit",
default=None,
@ -155,6 +157,7 @@ class Command(BaseCommand):
)
def handle(self, *args, **options):
"""execute init"""
limit = options.get("limit")
tables = [
"group",
@ -164,7 +167,7 @@ class Command(BaseCommand):
"settings",
"linkdomain",
]
if limit not in tables:
if limit and limit not in tables:
raise Exception("Invalid table limit:", limit)
if not limit or limit == "group":

View file

@ -0,0 +1,32 @@
# Generated by Django 3.2.10 on 2022-01-17 17:16
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0128_merge_0126_auto_20220112_2315_0127_auto_20220110_2211"),
]
operations = [
migrations.AddField(
model_name="filelink",
name="availability",
field=bookwyrm.models.fields.CharField(
choices=[
("free", "Free"),
("purchase", "Purchasable"),
("loan", "Available for loan"),
],
default="free",
max_length=100,
),
),
migrations.AlterField(
model_name="filelink",
name="filetype",
field=bookwyrm.models.fields.CharField(max_length=50),
),
]

View file

@ -0,0 +1,21 @@
# Generated by Django 3.2.10 on 2022-01-24 20:01
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0129_auto_20220117_1716"),
]
operations = [
migrations.AlterField(
model_name="listitem",
name="notes",
field=bookwyrm.models.fields.TextField(
blank=True, max_length=300, null=True
),
),
]

View file

@ -0,0 +1,37 @@
# Generated by Django 3.2.10 on 2022-01-24 17:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0129_auto_20220117_1716"),
]
operations = [
migrations.AlterField(
model_name="user",
name="preferred_language",
field=models.CharField(
blank=True,
choices=[
("en-us", "English"),
("de-de", "Deutsch (German)"),
("es-es", "Español (Spanish)"),
("gl-es", "Galego (Galician)"),
("it-it", "Italiano (Italian)"),
("fr-fr", "Français (French)"),
("lt-lt", "Lietuvių (Lithuanian)"),
("no-no", "Norsk (Norwegian)"),
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
("pt-pt", "Português Europeu (European Portuguese)"),
("sv-se", "Swedish (Svenska)"),
("zh-hans", "简体中文 (Simplified Chinese)"),
("zh-hant", "繁體中文 (Traditional Chinese)"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.10 on 2022-01-25 16:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0130_alter_listitem_notes"),
("bookwyrm", "0130_alter_user_preferred_language"),
]
operations = []

View file

@ -0,0 +1,37 @@
# Generated by Django 3.2.10 on 2022-02-02 20:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0131_merge_20220125_1644"),
]
operations = [
migrations.AlterField(
model_name="user",
name="preferred_language",
field=models.CharField(
blank=True,
choices=[
("en-us", "English"),
("de-de", "Deutsch (German)"),
("es-es", "Español (Spanish)"),
("gl-es", "Galego (Galician)"),
("it-it", "Italiano (Italian)"),
("fr-fr", "Français (French)"),
("lt-lt", "Lietuvių (Lithuanian)"),
("no-no", "Norsk (Norwegian)"),
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
("pt-pt", "Português Europeu (European Portuguese)"),
("sv-se", "Svenska (Swedish)"),
("zh-hans", "简体中文 (Simplified Chinese)"),
("zh-hant", "繁體中文 (Traditional Chinese)"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,21 @@
# Generated by Django 3.2.11 on 2022-02-04 20:06
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0132_alter_user_preferred_language"),
]
operations = [
migrations.AlterField(
model_name="listitem",
name="notes",
field=bookwyrm.models.fields.HtmlField(
blank=True, max_length=300, null=True
),
),
]

View file

@ -0,0 +1,29 @@
# Generated by Django 3.2.11 on 2022-02-11 18:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0133_alter_listitem_notes"),
]
operations = [
migrations.AddField(
model_name="announcement",
name="display_type",
field=models.CharField(
choices=[
("white-ter", "None"),
("primary-light", "Primary"),
("success-light", "Success"),
("link-light", "Link"),
("warning-light", "Warning"),
("danger-light", "Danger"),
],
default="white-ter",
max_length=20,
),
),
]

View file

@ -0,0 +1,58 @@
# Generated by Django 3.2.12 on 2022-02-17 16:24
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0134_announcement_display_type"),
]
operations = [
migrations.RenameField(
model_name="author",
old_name="viaf_id",
new_name="viaf",
),
migrations.RemoveField(
model_name="edition",
name="asin",
),
migrations.AddField(
model_name="author",
name="asin",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
migrations.AddField(
model_name="author",
name="wikidata",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
migrations.AddField(
model_name="book",
name="asin",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
migrations.AddField(
model_name="book",
name="viaf",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
migrations.AddField(
model_name="book",
name="wikidata",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 3.2.12 on 2022-02-17 17:08
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0135_auto_20220217_1624"),
]
operations = [
migrations.AddField(
model_name="sitesettings",
name="admin_code",
field=models.CharField(default=uuid.uuid4, max_length=50),
),
migrations.AddField(
model_name="sitesettings",
name="install_mode",
field=models.BooleanField(default=False),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-02-17 19:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0136_auto_20220217_1708"),
]
operations = [
migrations.AlterField(
model_name="sitesettings",
name="allow_registration",
field=models.BooleanField(default=False),
),
]

View file

@ -0,0 +1,39 @@
# Generated by Django 3.2.12 on 2022-02-24 18:59
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0137_alter_sitesettings_allow_registration"),
]
operations = [
migrations.CreateModel(
name="AutoMod",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("string_match", models.CharField(max_length=200, unique=True)),
("flag_users", models.BooleanField(default=True)),
("flag_statuses", models.BooleanField(default=True)),
(
"created_by",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View file

@ -0,0 +1,45 @@
# Generated by Django 3.2.12 on 2022-02-24 20:41
from django.db import migrations, models
import django.db.models.deletion
def set_report_statuses(apps, schema_editor):
"""copy over status fields"""
db_alias = schema_editor.connection.alias
report_model = apps.get_model("bookwyrm", "Report")
reports = report_model.objects.using(db_alias).filter(statuses__isnull=False)
for report in reports:
report.status = report.statuses.first()
report.save()
def set_reverse(apps, schema_editor):
"""copy over status fields"""
db_alias = schema_editor.connection.alias
report_model = apps.get_model("bookwyrm", "Report")
reports = report_model.objects.using(db_alias).filter(status__isnull=False)
for report in reports:
report.statuses.set(report.status)
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0138_automod"),
]
operations = [
migrations.AddField(
model_name="report",
name="status",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="reports",
to="bookwyrm.status",
),
),
migrations.RunPython(set_report_statuses, reverse_code=set_reverse),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 3.2.12 on 2022-02-24 20:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0139_report_status"),
]
operations = [
migrations.RemoveField(
model_name="report",
name="statuses",
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 3.2.12 on 2022-02-24 20:50
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0140_remove_report_statuses"),
]
operations = [
migrations.AlterField(
model_name="report",
name="status",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="bookwyrm.status",
),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 3.2.12 on 2022-02-28 19:44
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0141_alter_report_status"),
]
operations = [
migrations.AddField(
model_name="user",
name="hide_follows",
field=bookwyrm.models.fields.BooleanField(default=False),
),
]

View file

@ -29,7 +29,7 @@ from .import_job import ImportJob, ImportItem
from .site import SiteSettings, SiteInvite
from .site import PasswordReset, InviteRequest
from .announcement import Announcement
from .antispam import EmailBlocklist, IPBlocklist
from .antispam import EmailBlocklist, IPBlocklist, AutoMod, automod_task
from .notification import Notification

View file

@ -2,10 +2,21 @@
from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from .base_model import BookWyrmModel
DisplayTypes = [
("white-ter", _("None")),
("primary-light", _("Primary")),
("success-light", _("Success")),
("link-light", _("Link")),
("warning-light", _("Warning")),
("danger-light", _("Danger")),
]
class Announcement(BookWyrmModel):
"""The admin has something to say"""
@ -16,6 +27,13 @@ class Announcement(BookWyrmModel):
start_date = models.DateTimeField(blank=True, null=True)
end_date = models.DateTimeField(blank=True, null=True)
active = models.BooleanField(default=True)
display_type = models.CharField(
max_length=20,
blank=False,
null=False,
choices=DisplayTypes,
default="white-ter",
)
@classmethod
def active_announcements(cls):

View file

@ -1,6 +1,13 @@
""" Lets try NOT to sell viagra """
from django.db import models
from functools import reduce
import operator
from django.apps import apps
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from bookwyrm.tasks import app
from .user import User
@ -33,3 +40,107 @@ class IPBlocklist(models.Model):
"""default sorting"""
ordering = ("-created_date",)
class AutoMod(models.Model):
"""rules to automatically flag suspicious activity"""
string_match = models.CharField(max_length=200, unique=True)
flag_users = models.BooleanField(default=True)
flag_statuses = models.BooleanField(default=True)
created_by = models.ForeignKey("User", on_delete=models.PROTECT)
@app.task(queue="low_priority")
def automod_task():
"""Create reports"""
if not AutoMod.objects.exists():
return
reporter = AutoMod.objects.first().created_by
reports = automod_users(reporter) + automod_statuses(reporter)
if reports:
admins = User.objects.filter(
models.Q(user_permissions__name__in=["moderate_user", "moderate_post"])
| models.Q(is_superuser=True)
).all()
notification_model = apps.get_model(
"bookwyrm", "Notification", require_ready=True
)
for admin in admins:
notification_model.objects.bulk_create(
[
notification_model(
user=admin,
related_report=r,
notification_type="REPORT",
)
for r in reports
]
)
def automod_users(reporter):
"""check users for moderation flags"""
user_rules = AutoMod.objects.filter(flag_users=True).values_list(
"string_match", flat=True
)
if not user_rules:
return []
filters = []
for field in ["username", "summary", "name"]:
filters += [{f"{field}__icontains": r} for r in user_rules]
users = User.objects.filter(
reduce(operator.or_, (Q(**f) for f in filters)),
is_active=True,
local=True,
report__isnull=True, # don't flag users that already have reports
).distinct()
report_model = apps.get_model("bookwyrm", "Report", require_ready=True)
return report_model.objects.bulk_create(
[
report_model(
reporter=reporter,
note=_("Automatically generated report"),
user=u,
)
for u in users
]
)
def automod_statuses(reporter):
"""check statues for moderation flags"""
status_rules = AutoMod.objects.filter(flag_statuses=True).values_list(
"string_match", flat=True
)
if not status_rules:
return []
filters = []
for field in ["content", "content_warning", "quotation__quote", "review__name"]:
filters += [{f"{field}__icontains": r} for r in status_rules]
status_model = apps.get_model("bookwyrm", "Status", require_ready=True)
statuses = status_model.objects.filter(
reduce(operator.or_, (Q(**f) for f in filters)),
deleted=False,
local=True,
report__isnull=True, # don't flag statuses that already have reports
).distinct()
report_model = apps.get_model("bookwyrm", "Report", require_ready=True)
return report_model.objects.bulk_create(
[
report_model(
reporter=reporter,
note=_("Automatically generated report"),
user=s.user,
status=s,
)
for s in statuses
]
)

View file

@ -21,9 +21,6 @@ class Author(BookDataModel):
isni = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
viaf_id = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
gutenberg_id = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)

View file

@ -46,6 +46,15 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
bnf_id = fields.CharField( # Bibliothèque nationale de France
max_length=255, blank=True, null=True, deduplication_field=True
)
viaf = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
wikidata = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
asin = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
search_vector = SearchVectorField(null=True)
last_edited_by = fields.ForeignKey(
@ -271,9 +280,6 @@ class Edition(Book):
oclc_number = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
asin = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
pages = fields.IntegerField(blank=True, null=True)
physical_format = fields.CharField(
max_length=255, choices=FormatChoices, null=True, blank=True
@ -342,6 +348,11 @@ class Edition(Book):
# set rank
self.edition_rank = self.get_rank()
# clear author cache
if self.id:
for author_id in self.authors.values_list("id", flat=True):
cache.delete(f"author-books-{author_id}")
return super().save(*args, **kwargs)
@classmethod

View file

@ -1,6 +1,5 @@
""" activitypub-aware django model fields """
from dataclasses import MISSING
import imghdr
import re
from uuid import uuid4
from urllib.parse import urljoin
@ -9,7 +8,6 @@ import dateutil.parser
from dateutil.parser import ParserError
from django.contrib.postgres.fields import ArrayField as DjangoArrayField
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.db import models
from django.forms import ClearableFileInput, ImageField as DjangoImageField
from django.utils import timezone
@ -443,12 +441,10 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
except ValidationError:
return None
response = get_image(url)
if not response:
image_content, extension = get_image(url)
if not image_content:
return None
image_content = ContentFile(response.content)
extension = imghdr.what(None, image_content.read()) or ""
image_name = f"{uuid4()}.{extension}"
return [image_name, image_content]

View file

@ -47,13 +47,23 @@ class Link(ActivitypubMixin, BookWyrmModel):
return super().save(*args, **kwargs)
AvailabilityChoices = [
("free", _("Free")),
("purchase", _("Purchasable")),
("loan", _("Available for loan")),
]
class FileLink(Link):
"""a link to a file"""
book = models.ForeignKey(
"Book", on_delete=models.CASCADE, related_name="file_links", null=True
)
filetype = fields.CharField(max_length=5, activitypub_field="mediaType")
filetype = fields.CharField(max_length=50, activitypub_field="mediaType")
availability = fields.CharField(
max_length=100, choices=AvailabilityChoices, default="free"
)
StatusChoices = [

View file

@ -2,6 +2,7 @@
import uuid
from django.apps import apps
from django.core.exceptions import PermissionDenied
from django.db import models
from django.db.models import Q
from django.utils import timezone
@ -74,6 +75,22 @@ class List(OrderedCollectionMixin, BookWyrmModel):
return
super().raise_not_editable(viewer)
def raise_not_submittable(self, viewer):
"""can the user submit a book to the list?"""
# if you can't view the list you can't submit to it
self.raise_visible_to_user(viewer)
# all good if you're the owner or the list is open
if self.user == viewer or self.curation in ["open", "curated"]:
return
if self.curation == "group":
is_group_member = GroupMember.objects.filter(
group=self.group, user=viewer
).exists()
if is_group_member:
return
raise PermissionDenied()
@classmethod
def followers_filter(cls, queryset, viewer):
"""Override filter for "followers" privacy level to allow non-following
@ -125,7 +142,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="actor"
)
notes = fields.TextField(blank=True, null=True)
notes = fields.HtmlField(blank=True, null=True, max_length=300)
approved = models.BooleanField(default=True)
order = fields.IntegerField()
endorsement = models.ManyToManyField("User", related_name="endorsers")

View file

@ -1,5 +1,6 @@
""" flagged for moderation """
from django.db import models
from bookwyrm.settings import DOMAIN
from .base_model import BookWyrmModel
@ -11,10 +12,18 @@ class Report(BookWyrmModel):
)
note = models.TextField(null=True, blank=True)
user = models.ForeignKey("User", on_delete=models.PROTECT)
statuses = models.ManyToManyField("Status", blank=True)
status = models.ForeignKey(
"Status",
null=True,
blank=True,
on_delete=models.PROTECT,
)
links = models.ManyToManyField("Link", blank=True)
resolved = models.BooleanField(default=False)
def get_remote_id(self):
return f"https://{DOMAIN}/settings/reports/{self.id}"
class Meta:
"""set order by default"""

View file

@ -1,5 +1,6 @@
""" puttin' books on shelves """
import re
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.db import models
from django.utils import timezone
@ -94,8 +95,15 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel):
def save(self, *args, **kwargs):
if not self.user:
self.user = self.shelf.user
if self.id and self.user.local:
cache.delete(f"book-on-shelf-{self.book.id}-{self.shelf.id}")
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
if self.id and self.user.local:
cache.delete(f"book-on-shelf-{self.book.id}-{self.shelf.id}")
super().delete(*args, **kwargs)
class Meta:
"""an opinionated constraint!
you can't put a book on shelf twice"""

View file

@ -1,6 +1,7 @@
""" the particulars for this instance of BookWyrm """
import datetime
from urllib.parse import urljoin
import uuid
from django.db import models, IntegrityError
from django.dispatch import receiver
@ -24,6 +25,10 @@ class SiteSettings(models.Model):
instance_description = models.TextField(default="This instance has no description.")
instance_short_description = models.CharField(max_length=255, blank=True, null=True)
# admin setup options
install_mode = models.BooleanField(default=False)
admin_code = models.CharField(max_length=50, default=uuid.uuid4)
# about page
registration_closed_text = models.TextField(
default="We aren't taking new users at this time. You can find an open "
@ -38,7 +43,7 @@ class SiteSettings(models.Model):
privacy_policy = models.TextField(default="Add a privacy policy here.")
# registration
allow_registration = models.BooleanField(default=True)
allow_registration = models.BooleanField(default=False)
allow_invite_requests = models.BooleanField(default=True)
require_confirm_email = models.BooleanField(default=True)
@ -90,6 +95,14 @@ class SiteSettings(models.Model):
return get_absolute_url(uploaded)
return urljoin(STATIC_FULL_URL, default_path)
def save(self, *args, **kwargs):
"""if require_confirm_email is disabled, make sure no users are pending"""
if not self.require_confirm_email:
User.objects.filter(is_active=False, deactivation_reason="pending").update(
is_active=True, deactivation_reason=None
)
super().save(*args, **kwargs)
class SiteInvite(models.Model):
"""gives someone access to create an account on the instance"""

View file

@ -3,6 +3,7 @@ from dataclasses import MISSING
import re
from django.apps import apps
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
@ -226,7 +227,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
@classmethod
def privacy_filter(cls, viewer, privacy_levels=None):
queryset = super().privacy_filter(viewer, privacy_levels=privacy_levels)
return queryset.filter(deleted=False)
return queryset.filter(deleted=False, user__is_active=True)
@classmethod
def direct_filter(cls, queryset, viewer):
@ -373,6 +374,12 @@ class Review(BookStatus):
activity_serializer = activitypub.Review
pure_type = "Article"
def save(self, *args, **kwargs):
"""clear rating caches"""
if self.book.parent_work:
cache.delete(f"book-rating-{self.book.parent_work.id}-*")
super().save(*args, **kwargs)
class ReviewRating(Review):
"""a subtype of review that only contains a rating"""

View file

@ -136,6 +136,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
updated_date = models.DateTimeField(auto_now=True)
last_active_date = models.DateTimeField(default=timezone.now)
manually_approves_followers = fields.BooleanField(default=False)
hide_follows = fields.BooleanField(default=False)
# options to turn features on and off
show_goal = models.BooleanField(default=True)
@ -478,10 +479,13 @@ def set_remote_server(user_id):
get_remote_reviews.delay(user.outbox)
def get_or_create_remote_server(domain):
def get_or_create_remote_server(domain, refresh=False):
"""get info on a remote server"""
server = FederatedServer()
try:
return FederatedServer.objects.get(server_name=domain)
server = FederatedServer.objects.get(server_name=domain)
if not refresh:
return server
except FederatedServer.DoesNotExist:
pass
@ -496,13 +500,15 @@ def get_or_create_remote_server(domain):
application_type = data.get("software", {}).get("name")
application_version = data.get("software", {}).get("version")
except ConnectorException:
if server.id:
return server
application_type = application_version = None
server = FederatedServer.objects.create(
server_name=domain,
application_type=application_type,
application_version=application_version,
)
server.server_name = domain
server.application_type = application_type
server.application_version = application_version
server.save()
return server

View file

@ -4,6 +4,7 @@ import os
import textwrap
from io import BytesIO
from uuid import uuid4
import logging
import colorsys
from colorthief import ColorThief
@ -17,34 +18,49 @@ from django.db.models import Avg
from bookwyrm import models, settings
from bookwyrm.tasks import app
logger = logging.getLogger(__name__)
IMG_WIDTH = settings.PREVIEW_IMG_WIDTH
IMG_HEIGHT = settings.PREVIEW_IMG_HEIGHT
BG_COLOR = settings.PREVIEW_BG_COLOR
TEXT_COLOR = settings.PREVIEW_TEXT_COLOR
DEFAULT_COVER_COLOR = settings.PREVIEW_DEFAULT_COVER_COLOR
DEFAULT_FONT = settings.PREVIEW_DEFAULT_FONT
TRANSPARENT_COLOR = (0, 0, 0, 0)
margin = math.floor(IMG_HEIGHT / 10)
gutter = math.floor(margin / 2)
inner_img_height = math.floor(IMG_HEIGHT * 0.8)
inner_img_width = math.floor(inner_img_height * 0.7)
font_dir = os.path.join(settings.STATIC_ROOT, "fonts/public_sans")
def get_font(font_name, size=28):
"""Loads custom font"""
if font_name == "light":
font_path = os.path.join(font_dir, "PublicSans-Light.ttf")
if font_name == "regular":
font_path = os.path.join(font_dir, "PublicSans-Regular.ttf")
elif font_name == "bold":
font_path = os.path.join(font_dir, "PublicSans-Bold.ttf")
def get_imagefont(name, size):
"""Loads an ImageFont based on config"""
try:
config = settings.FONTS[name]
path = os.path.join(settings.FONT_DIR, config["directory"], config["filename"])
return ImageFont.truetype(path, size)
except KeyError:
logger.error("Font %s not found in config", name)
except OSError:
logger.error("Could not load font %s from file", name)
return ImageFont.load_default()
def get_font(weight, size=28):
"""Gets a custom font with the given weight and size"""
font = get_imagefont(DEFAULT_FONT, size)
try:
font = ImageFont.truetype(font_path, size)
except OSError:
font = ImageFont.load_default()
if weight == "light":
font.set_variation_by_name("Light")
if weight == "bold":
font.set_variation_by_name("Bold")
if weight == "regular":
font.set_variation_by_name("Regular")
except AttributeError:
pass
return font

View file

@ -22,6 +22,7 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
"ol",
"li",
]
self.allowed_attrs = ["href", "rel", "src", "alt"]
self.tag_stack = []
self.output = []
# if the html appears invalid, we just won't allow any at all
@ -30,7 +31,14 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
def handle_starttag(self, tag, attrs):
"""check if the tag is valid"""
if self.allow_html and tag in self.allowed_tags:
self.output.append(("tag", self.get_starttag_text()))
allowed_attrs = " ".join(
f'{a}="{v}"' for a, v in attrs if a in self.allowed_attrs
)
reconstructed = f"<{tag}"
if allowed_attrs:
reconstructed += " " + allowed_attrs
reconstructed += ">"
self.output.append(("tag", reconstructed))
self.tag_stack.append(tag)
else:
self.output.append(("data", ""))

View file

@ -6,15 +6,22 @@ import requests
from django.utils.translation import gettext_lazy as _
# pylint: disable=line-too-long
env = Env()
env.read_env()
DOMAIN = env("DOMAIN")
VERSION = "0.2.0"
VERSION = "0.3.1"
RELEASE_API = env(
"RELEASE_API",
"https://api.github.com/repos/bookwyrm-social/bookwyrm/releases/latest",
)
PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "a47cc2ca"
JS_CACHE = "c7144efb"
# email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
@ -35,6 +42,9 @@ LOCALE_PATHS = [
]
LANGUAGE_COOKIE_NAME = env.str("LANGUAGE_COOKIE_NAME", "django_language")
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Preview image
@ -44,6 +54,16 @@ PREVIEW_TEXT_COLOR = env.str("PREVIEW_TEXT_COLOR", "#363636")
PREVIEW_IMG_WIDTH = env.int("PREVIEW_IMG_WIDTH", 1200)
PREVIEW_IMG_HEIGHT = env.int("PREVIEW_IMG_HEIGHT", 630)
PREVIEW_DEFAULT_COVER_COLOR = env.str("PREVIEW_DEFAULT_COVER_COLOR", "#002549")
PREVIEW_DEFAULT_FONT = env.str("PREVIEW_DEFAULT_FONT", "Source Han Sans")
FONTS = {
"Source Han Sans": {
"directory": "source_han_sans",
"filename": "SourceHanSans-VF.ttf.ttc",
"url": "https://github.com/adobe-fonts/source-han-sans/raw/release/Variable/OTC/SourceHanSans-VF.ttf.ttc",
}
}
FONT_DIR = os.path.join(STATIC_ROOT, "fonts")
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
@ -53,7 +73,7 @@ SECRET_KEY = env("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env.bool("DEBUG", True)
USE_HTTPS = env.bool("USE_HTTPS", False)
USE_HTTPS = env.bool("USE_HTTPS", not DEBUG)
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"])
@ -67,7 +87,7 @@ INSTALLED_APPS = [
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.humanize",
"django_rename_app",
"sass_processor",
"bookwyrm",
"celery",
"imagekit",
@ -106,6 +126,76 @@ TEMPLATES = [
},
]
LOG_LEVEL = env("LOG_LEVEL", "INFO").upper()
# Override aspects of the default handler to our taste
# See https://docs.djangoproject.com/en/3.2/topics/logging/#default-logging-configuration
# for a reference to the defaults we're overriding
#
# It seems that in order to override anything you have to include its
# entire dependency tree (handlers and filters) which makes this a
# bit verbose
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {
# These are copied from the default configuration, required for
# implementing mail_admins below
"require_debug_false": {
"()": "django.utils.log.RequireDebugFalse",
},
"require_debug_true": {
"()": "django.utils.log.RequireDebugTrue",
},
},
"handlers": {
# Overrides the default handler to make it log to console
# regardless of the DEBUG setting (default is to not log to
# console if DEBUG=False)
"console": {
"level": LOG_LEVEL,
"class": "logging.StreamHandler",
},
# This is copied as-is from the default logger, and is
# required for the django section below
"mail_admins": {
"level": "ERROR",
"filters": ["require_debug_false"],
"class": "django.utils.log.AdminEmailHandler",
},
},
"loggers": {
# Install our new console handler for Django's logger, and
# override the log level while we're at it
"django": {
"handlers": ["console", "mail_admins"],
"level": LOG_LEVEL,
},
"django.utils.autoreload": {
"level": "INFO",
},
# Add a bookwyrm-specific logger
"bookwyrm": {
"handlers": ["console"],
"level": LOG_LEVEL,
},
},
}
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
"sass_processor.finders.CssFinder",
]
SASS_PROCESSOR_INCLUDE_FILE_PATTERN = r"^.+\.[s]{0,1}(?:a|c)ss$"
SASS_PROCESSOR_INCLUDE_DIRS = [
os.path.join(BASE_DIR, ".css-config-sample"),
]
# minify css is production but not dev
if not DEBUG:
SASS_OUTPUT_STYLE = "compressed"
WSGI_APPLICATION = "bookwyrm.wsgi.application"
@ -136,7 +226,6 @@ if env("USE_DUMMY_CACHE", False):
}
}
else:
# pylint: disable=line-too-long
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
@ -171,7 +260,6 @@ AUTH_USER_MODEL = "bookwyrm.User"
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
# pylint: disable=line-too-long
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
@ -191,7 +279,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = "en-us"
LANGUAGE_CODE = env("LANGUAGE_CODE", "en-us")
LANGUAGES = [
("en-us", _("English")),
("de-de", _("Deutsch (German)")),
@ -203,6 +291,7 @@ LANGUAGES = [
("no-no", _("Norsk (Norwegian)")),
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
("pt-pt", _("Português Europeu (European Portuguese)")),
("sv-se", _("Svenska (Swedish)")),
("zh-hans", _("简体中文 (Simplified Chinese)")),
("zh-hant", _("繁體中文 (Traditional Chinese)")),
]
@ -258,13 +347,12 @@ if USE_S3:
MEDIA_FULL_URL = MEDIA_URL
STATIC_FULL_URL = STATIC_URL
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
# I don't know if it's used, but the site crashes without it
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
else:
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
MEDIA_URL = "/images/"
MEDIA_FULL_URL = f"{PROTOCOL}://{DOMAIN}{MEDIA_URL}"
STATIC_FULL_URL = f"{PROTOCOL}://{DOMAIN}{STATIC_URL}"
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
OTEL_EXPORTER_OTLP_ENDPOINT = env("OTEL_EXPORTER_OTLP_ENDPOINT", None)
OTEL_EXPORTER_OTLP_HEADERS = env("OTEL_EXPORTER_OTLP_HEADERS", None)
OTEL_SERVICE_NAME = env("OTEL_SERVICE_NAME", None)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
@charset "utf-8";
@import "instance-settings";
@import "themes/light.scss";
@import "vendor/bulma/bulma.sass";
@import "vendor/icons.css";
@import "bookwyrm/all.scss";

View file

@ -0,0 +1,159 @@
/** Imports
******************************************************************************/
@import "components/avatar";
@import "components/book_cover";
@import "components/book_grid";
@import "components/book_list";
@import "components/book_preview_table";
@import "components/breadcrumbs";
@import "components/copy";
@import "components/details";
@import "components/file_input";
@import "components/live_message";
@import "components/shelving";
@import "components/stars";
@import "components/status";
@import "components/tabs";
@import "components/toggle";
@import "overrides/bulma_overrides";
@import "utilities/a11y";
@import "utilities/alignments";
@import "utilities/colors";
@import "utilities/size";
@import "utilities/spacings";
@import "utilities/transitions";
html {
scroll-behavior: smooth;
}
body {
min-height: 100vh;
display: flex;
flex-direction: column;
}
button {
border: none;
margin: 0;
padding: 0;
width: auto;
overflow: visible;
background: transparent;
/* inherit font, color & alignment from ancestor */
color: inherit;
font: inherit;
text-align: inherit;
/* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
line-height: normal;
/* Corrects font smoothing for webkit */
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
/* Corrects inability to style clickable `input` types in iOS */
-webkit-appearance: none;
/* Generalizes pointer cursor */
cursor: pointer;
}
button::-moz-focus-inner {
/* Remove excess padding and border in Firefox 4+ */
border: 0;
padding: 0;
}
/* Better accessibility for keyboard users */
*:focus-visible {
outline-style: auto !important;
}
/** Utilities not covered by Bulma
******************************************************************************/
.tag.is-small {
height: auto;
}
.button.is-transparent {
background-color: transparent;
}
.card.is-stretchable {
display: flex;
flex-direction: column;
height: 100%;
}
.card.is-stretchable .card-content {
flex-grow: 1;
}
.preserve-whitespace p {
white-space: pre-wrap !important;
}
.display-inline p {
display: inline !important;
}
button .button-invisible-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 1rem;
box-sizing: border-box;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
background: rgba($scheme-invert, 0.66);
color: white;
opacity: 0;
transition: opacity 0.2s ease;
}
button:hover .button-invisible-overlay,
button:active .button-invisible-overlay,
button:focus-visible .button-invisible-overlay {
opacity: 1;
}
/** Tooltips
******************************************************************************/
.tooltip {
width: 100%;
}
/** States
******************************************************************************/
/* "disabled" for non-buttons */
.is-disabled {
background-color: $pagination-disabled-background-color;
border-color: $pagination-disabled-border-color;
box-shadow: none;
color: $pagination-disabled-color;
opacity: 0.5;
cursor: not-allowed;
}
/* Notifications page
******************************************************************************/
.notification a.icon {
text-decoration: none !important;
}

View file

@ -0,0 +1,7 @@
/** Avatars
******************************************************************************/
.avatar {
vertical-align: middle;
display: inline;
}

View file

@ -0,0 +1,70 @@
/** Book covers
*
* - .is-cover gives the behaviour of the cover and its surrounding. (optional)
* - .cover-container gives the dimensions and position (for borders, image and other elements).
* - .book-cover is positioned and sized based on its container.
*
* To have the cover within specific dimensions, specify a width or height for
* standard bulmas named breapoints:
*
* `is-(w|h)-(auto|xs|s|m|l|xl|xxl)[-(mobile|tablet|desktop)]`
*
* The cover will be centered horizontally and vertically within those dimensions.
*
* When using `.column.is-N`, add `.is-w-auto` to the container so that the flex
* calculations are not biased by the default `max-content`.
******************************************************************************/
.column.is-cover {
flex-grow: 0 !important;
}
.column.is-cover,
.column.is-cover + .column {
flex-basis: auto !important;
}
.cover-container {
display: flex;
justify-content: center;
align-items: center;
position: relative;
width: max-content;
max-width: 100%;
overflow: hidden;
}
/* Book cover
* -------------------------------------------------------------------------- */
.book-cover {
display: block;
max-width: 100%;
max-height: 100%;
/* Useful when stretching under-sized images. */
image-rendering: optimizequality;
image-rendering: smooth;
}
/* Cover caption
* -------------------------------------------------------------------------- */
.no-cover .cover-caption {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: 0.5em;
font-size: 0.75em;
color: white;
background-color: $no-cover-color;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 1em;
white-space: initial;
text-align: center;
}

View file

@ -0,0 +1,36 @@
/* Books grid
******************************************************************************/
.books-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
align-items: end;
justify-items: stretch;
}
.books-grid > .is-big {
grid-column: span 2;
grid-row: span 2;
justify-self: stretch;
}
.books-grid .book-cover {
width: 100%;
}
.books-grid .book-title {
--height-basis: 1.35rem;
display: block;
margin-top: 0.5rem;
line-height: var(--height-basis);
min-height: calc(2 * var(--height-basis));
}
@media only screen and (min-width: 769px) {
.books-grid {
gap: 1.5rem;
grid-template-columns: repeat(auto-fill, minmax(8em, 1fr));
}
}

View file

@ -0,0 +1,47 @@
/* Book list
******************************************************************************/
ol.ordered-list {
list-style: none;
counter-reset: list-counter;
}
ol.ordered-list li {
counter-increment: list-counter;
}
ol.ordered-list li::before {
content: counter(list-counter);
position: absolute;
left: -20px;
width: 20px;
height: 24px;
background-color: $scheme-main;
border: 1px solid $border;
border-right: 0;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
display: flex;
justify-content: center;
align-items: center;
color: $text-light;
font-size: 0.8em;
font-weight: bold;
}
@media only screen and (max-width: 768px) {
ol.ordered-list li::before {
left: 0;
z-index: 1;
border: 0;
border-right: 1px solid $border;
border-bottom: 1px solid $border;
border-radius: 0;
border-bottom-right-radius: 2px;
}
}
.overflow-wrap-anywhere {
overflow-wrap: anywhere;
min-width: 10em;
}

View file

@ -0,0 +1,49 @@
/* Book preview table
******************************************************************************/
.book-preview td {
vertical-align: middle;
}
@media only screen and (max-width: 768px) {
table.is-mobile,
table.is-mobile tbody {
display: block;
}
table.is-mobile tr {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
border-top: 1px solid $border;
}
table.is-mobile td {
display: block;
box-sizing: border-box;
flex: 1 0 100%;
order: 2;
border-bottom: 0;
}
table.is-mobile td.book-preview-top-row {
order: 1;
flex-basis: auto;
}
table.is-mobile td[data-title]:not(:empty)::before {
content: attr(data-title);
display: block;
font-size: 0.75em;
font-weight: bold;
}
table.is-mobile td:empty {
padding: 0;
}
table.is-mobile th,
table.is-mobile thead {
display: none;
}
}

View file

@ -0,0 +1,13 @@
/* Breadcrumbs
******************************************************************************/
.breadcrumb li:first-child * {
padding-left: 0;
}
.breadcrumb li > * {
align-items: center;
display: flex;
justify-content: center;
padding: 0 0.75em;
}

View file

@ -0,0 +1,30 @@
/* Copy
******************************************************************************/
.horizontal-copy {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.75rem;
}
.horizontal-copy textarea {
min-width: initial;
white-space: nowrap;
}
.horizontal-copy button {
align-self: stretch;
height: unset;
}
.vertical-copy {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
.vertical-copy button {
width: 100%;
}

View file

@ -0,0 +1,116 @@
/** General `details` element styles
******************************************************************************/
details summary {
cursor: pointer;
}
summary::-webkit-details-marker {
display: none;
}
details summary::marker {
content: none;
}
details.detail-pinned-button summary {
position: absolute;
right: 0;
}
details.detail-pinned-button form {
float: left;
width: 100%;
margin-top: 1em;
}
/** Dropdown w/ Details element
******************************************************************************/
details.dropdown[open] summary.dropdown-trigger::before {
content: "";
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
details.dropdown .dropdown-menu {
display: block !important;
}
details.dropdown .dropdown-menu button {
/* Fix weird Safari defaults */
box-sizing: border-box;
}
details.dropdown .dropdown-menu button:focus-visible,
details.dropdown .dropdown-menu a:focus-visible {
outline-style: auto;
outline-offset: -2px;
}
@media only screen and (max-width: 768px) {
details.dropdown[open] summary.dropdown-trigger::before {
background-color: rgba($scheme-invert, 0.5);
z-index: 30;
}
details .dropdown-menu {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex !important;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 100;
}
details .dropdown-menu > * {
pointer-events: all;
}
}
/** Details panel
******************************************************************************/
details.details-panel {
box-shadow: 0 0 0 1px $border;
transition: box-shadow 0.2s ease;
padding: 0.75rem;
}
details[open].details-panel,
details.details-panel:hover {
box-shadow: 0 0 0 1px $border;
}
details.details-panel summary {
position: relative;
}
details summary .details-close {
position: absolute;
right: 0;
top: 0;
transform: rotate(45deg);
transition: transform 0.2s ease;
}
details[open] summary .details-close {
transform: rotate(0deg);
}
@media only screen and (min-width: 769px) {
.details-panel .filters-field:not(:last-child) {
border-right: 1px solid $border;
margin-top: 0.75rem;
margin-bottom: 0.75rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
}

View file

@ -0,0 +1,28 @@
/** File input styles
******************************************************************************/
input[type="file"]::file-selector-button {
-moz-appearance: none;
-webkit-appearance: none;
background-color: $scheme-main;
border-radius: 4px;
border: 1px solid $border;
box-shadow: none;
color: $text;
cursor: pointer;
font-size: 1rem;
height: 2.5em;
justify-content: center;
line-height: 1.5;
padding-bottom: calc(0.5em - 1px);
padding-left: 1em;
padding-right: 1em;
padding-top: calc(0.5em - 1px);
text-align: center;
white-space: nowrap;
}
input[type="file"]::file-selector-button:hover {
border-color: $border-hover;
color: text;
}

View file

@ -0,0 +1,8 @@
/** Transient notification
******************************************************************************/
#live-messages {
position: fixed;
bottom: 1em;
right: 1em;
}

View file

@ -0,0 +1,10 @@
/** Shelving
******************************************************************************/
/** @todo Replace icons with SVG symbols.
@see https://www.youtube.com/watch?v=9xXBYcWgCHA */
.shelf-option:disabled > *::after {
font-family: icomoon; /* stylelint-disable font-family-no-missing-generic-family-keyword */
content: "\e919"; /* icon-check */
margin-left: 0.5em;
}

View file

@ -0,0 +1,52 @@
/** Stars
******************************************************************************/
.stars {
white-space: nowrap;
}
/** Stars in a review form
*
* Specificity makes hovering taking over checked inputs.
*
* \e9d9: filled star
* \e9d7: empty star;
* -------------------------------------------------------------------------- */
.form-rate-stars {
width: max-content;
}
/* All stars are visually filled by default. */
.form-rate-stars .icon::before {
content: "\e9d9"; /* icon-star-full */
}
/* Icons directly following half star inputs are marked as half */
.form-rate-stars input.half:checked ~ .icon::before {
content: "\e9d8"; /* icon-star-half */
}
/* stylelint-disable no-descending-specificity */
.form-rate-stars input.half:checked + input + .icon:hover::before {
content: "\e9d8" !important; /* icon-star-half */
}
/* Icons directly following half check inputs that follow the checked input are emptied. */
.form-rate-stars input.half:checked + input + .icon ~ .icon::before {
content: "\e9d7"; /* icon-star-empty */
}
/* Icons directly following inputs that follow the checked input are emptied. */
.form-rate-stars input:checked ~ input + .icon::before {
content: "\e9d7"; /* icon-star-empty */
}
/* When a label is hovered, repeat the fill-all-then-empty-following pattern. */
.form-rate-stars:hover .icon.icon::before {
content: "\e9d9" !important; /* icon-star-full */
}
.form-rate-stars .icon:hover ~ .icon::before {
content: "\e9d7" !important; /* icon-star-empty */
}

View file

@ -0,0 +1,57 @@
/** Statuses: Quotes
*
* \e906: icon-quote-open
* \e905: icon-quote-close
*
* The `content` class on the blockquote allows to apply styles to markdown
* generated HTML in the quote: https://bulma.io/documentation/elements/content/
*
* ```html
* <div class="quote block">
* <blockquote dir="auto" class="content mb-2">
* User generated quote in markdown
* </blockquote>
*
* <p> <a>Book Title</a> by <aclass="author">Author</a></p>
* </div>
* ```
******************************************************************************/
.quote > blockquote {
position: relative;
padding-left: 2em;
}
.quote > blockquote::before,
.quote > blockquote::after {
font-family: icomoon; /* stylelint-disable font-family-no-missing-generic-family-keyword */
position: absolute;
}
.quote > blockquote::before {
content: "\e907"; /* icon-quote-open */
top: 0;
left: 0;
}
.quote > blockquote::after {
content: "\e906"; /* icon-quote-close */
right: 0;
}
/* Threads
******************************************************************************/
.thread .is-main .card {
box-shadow: 0 0.5em 1em -0.125em rgba($link, 0.35), 0 0 0 1px rgba($link, 0.02);
}
.thread::after {
content: "";
position: absolute;
z-index: -1;
top: 0;
bottom: 0;
left: 2.5em;
border-left: 2px solid $border;
}

View file

@ -0,0 +1,176 @@
/** Bookwyrm Tabs
******************************************************************************/
.bw-tabs {
-webkit-overflow-scrolling: touch;
-webkit-touch-callout: none;
position: relative;
align-items: center;
display: flex;
font-size: 1rem;
justify-content: flex-start;
overflow-x: auto;
overflow-y: hidden;
user-select: none;
white-space: nowrap;
}
.bw-tabs::before {
border-bottom-color: $border;
border-bottom-style: solid;
border-bottom-width: 1px;
bottom: 0;
content: "";
position: absolute;
width: 100%;
}
.bw-tabs:not(:last-child) {
margin-bottom: 1.5rem;
}
.bw-tabs a {
align-items: center;
border-bottom-color: $border;
border-bottom-style: solid;
border-bottom-width: 1px;
color: $text;
display: flex;
justify-content: center;
margin-bottom: -1px;
padding: 0.5em 1em;
position: relative;
}
.bw-tabs a:hover {
border-bottom-color: transparent;
color: $text;
}
.bw-tabs a.is-active {
border-bottom-color: transparent;
color: $link;
}
.bw-tabs.is-left {
padding-right: 0.75em;
}
.bw-tabs.is-center {
flex: none;
justify-content: center;
padding-left: 0.75em;
padding-right: 0.75em;
}
.bw-tabs.is-right {
justify-content: flex-end;
padding-left: 0.75em;
}
.bw-tabs .icon:first-child {
margin-right: 0.5em;
}
.bw-tabs .icon:last-child {
margin-left: 0.5em;
}
.bw-tabs.is-centered {
justify-content: center;
}
.bw-tabs.is-boxed a {
border: 1px solid transparent;
border-radius: 4px 4px 0 0;
}
.bw-tabs.is-boxed a:hover {
background-color: $background-secondary;
border-bottom-color: $border-hover;
}
.bw-tabs.is-boxed a.is-active {
background-color: $background-body;
border-color: $border;
border-bottom-color: $border !important;
}
.bw-tabs.is-fullwidth a {
flex-grow: 1;
flex-shrink: 0;
}
.bw-tabs.is-toggle a {
border-color: $border;
border-style: solid;
border-width: 1px;
margin-bottom: 0;
position: relative;
}
.bw-tabs.is-toggle a:hover {
background-color: $background-secondary;
border-color: $border;
z-index: 2;
}
.bw-tabs.is-toggle a + a {
margin-left: -1px;
}
.bw-tabs.is-toggle a:first-child {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.bw-tabs.is-toggle a:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.bw-tabs.is-toggle a.is-active {
background-color: $link-background;
border-color: $link;
color: $text;
z-index: 1;
}
.bw-tabs.is-toggle {
border-bottom: none;
}
.bw-tabs.is-toggle.is-toggle-rounded a:first-child {
border-bottom-left-radius: 290486px;
border-top-left-radius: 290486px;
padding-left: 1.25em;
}
.bw-tabs.is-toggle.is-toggle-rounded a:last-child {
border-bottom-right-radius: 290486px;
border-top-right-radius: 290486px;
padding-right: 1.25em;
}
.bw-tabs.is-small {
font-size: 0.75rem;
}
.bw-tabs.is-medium {
font-size: 1.25rem;
}
.bw-tabs.is-large {
font-size: 1.5rem;
}
.bw-tabs.has-aside-text a {
margin-top: 1.5rem;
}
.bw-tabs a .aside-text {
position: absolute;
top: calc(-0.75rem - 0.75rem);
left: 0;
color: $text;
}

View file

@ -0,0 +1,45 @@
/** Toggles
******************************************************************************/
.toggle-button[aria-pressed="true"],
.toggle-button[aria-pressed="true"]:hover {
background-color: hsl(171deg, 100%, 41%);
color: white;
}
.hide-active[aria-pressed="true"],
.hide-inactive[aria-pressed="false"] {
display: none;
}
.transition-x.is-hidden,
.transition-y.is-hidden {
display: block !important;
visibility: hidden !important;
height: 0 !important;
width: 0 !important;
margin: 0 !important;
padding: 0 !important;
overflow: auto;
}
.transition-x,
.transition-y {
transition-duration: 0.5s;
transition-timing-function: ease;
}
.transition-x {
transition-property: width, margin-left, margin-right, padding-left, padding-right;
}
.transition-y {
transition-property: height, margin-top, margin-bottom, padding-top, padding-bottom;
}
@media (prefers-reduced-motion: reduce) {
.transition-x,
.transition-y {
transition-duration: 0.001ms !important;
}
}

View file

@ -0,0 +1,61 @@
.image {
overflow: hidden;
}
.navbar .logo {
max-height: 50px;
}
.card {
overflow: visible;
}
.card.has-border {
border: 1px solid $border;
}
.scroll-x {
overflow: hidden;
overflow-x: auto;
}
.modal-card {
pointer-events: none;
}
.modal-card > * {
pointer-events: all;
}
/* stylelint-disable no-descending-specificity */
.modal-card:focus {
outline-style: auto;
}
.modal-card:focus:not(:focus-visible) {
outline-style: initial;
}
.modal-card:focus-visible {
outline-style: auto;
}
/* stylelint-enable no-descending-specificity */
.modal-card.is-fullwidth {
min-width: 75% !important;
}
@media only screen and (min-width: 769px) {
.modal-card.is-thin {
width: 350px !important;
}
}
.modal-card-body {
max-height: 70vh;
}
.clip-text {
max-height: 35em;
overflow: hidden;
}

View file

@ -0,0 +1,33 @@
@media only screen and (max-width: 768px) {
.is-sr-only-mobile {
border: none !important;
clip: rect(0, 0, 0, 0) !important;
height: 0.01em !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
white-space: nowrap !important;
width: 0.01em !important;
}
.m-0-mobile {
margin: 0 !important;
}
.card-footer.is-stacked-mobile {
flex-direction: column;
}
.card-footer.is-stacked-mobile .card-footer-item:not(:last-child) {
border-bottom: 1px solid $background-tertiary;
border-right: 0;
}
.is-flex-direction-row-mobile {
flex-direction: row !important;
}
.is-flex-direction-column-mobile {
flex-direction: column !important;
}
}

View file

@ -0,0 +1,76 @@
/* Alignments
*
* Use them with `.align.to-(c|t|r|b|l)[-(mobile|tablet)]`
******************************************************************************/
/* Flex item position
* -------------------------------------------------------------------------- */
.align {
display: flex !important;
flex-direction: row !important;
}
.align.to-c {
justify-content: center !important;
}
.align.to-t {
align-items: flex-start !important;
}
.align.to-r {
justify-content: flex-end !important;
}
.align.to-b {
align-items: flex-end !important;
}
.align.to-l {
justify-content: flex-start !important;
}
@media screen and (max-width: 768px) {
.align.to-c-mobile {
justify-content: center !important;
}
.align.to-t-mobile {
align-items: flex-start !important;
}
.align.to-r-mobile {
justify-content: flex-end !important;
}
.align.to-b-mobile {
align-items: flex-end !important;
}
.align.to-l-mobile {
justify-content: flex-start !important;
}
}
@media screen and (min-width: 769px) {
.align.to-c-tablet {
justify-content: center !important;
}
.align.to-t-tablet {
align-items: flex-start !important;
}
.align.to-r-tablet {
justify-content: flex-end !important;
}
.align.to-b-tablet {
align-items: flex-end !important;
}
.align.to-l-tablet {
justify-content: flex-start !important;
}
}

View file

@ -0,0 +1,25 @@
/* Semantic color classes */
.has-background-primary-highlight {
background-color: $primary-highlight;
}
.has-background-info-highlight {
background-color: $info-highlight;
}
.has-background-success-highlight {
background-color: $success-highlight;
}
.has-background-body {
background-color: $background-body;
}
.has-background-secondary {
background-color: $background-secondary !important;
}
.has-background-tertiary {
background-color: $background-tertiary !important;
}

View file

@ -0,0 +1,227 @@
/* Dimensions
* @todo These could be in rem.
******************************************************************************/
.is-32x32 {
min-width: 32px !important;
min-height: 32px !important;
}
.is-96x96 {
min-width: 96px !important;
min-height: 96px !important;
}
.is-w-auto {
width: auto !important;
}
.is-w-xs {
width: 80px !important;
}
.is-w-s {
width: 100px !important;
}
.is-w-m {
width: 150px !important;
}
.is-w-l {
width: 200px !important;
}
.is-w-xl {
width: 250px !important;
}
.is-w-xxl {
width: 500px !important;
}
.is-h-xs {
height: 80px !important;
}
.is-h-s {
height: 100px !important;
}
.is-h-m {
height: 150px !important;
}
.is-h-l {
height: 200px !important;
}
.is-h-xl {
height: 250px !important;
}
.is-h-xxl {
height: 500px !important;
}
@media only screen and (max-width: 768px) {
.is-w-auto-mobile {
width: auto !important;
}
.is-w-xs-mobile {
width: 80px !important;
}
.is-w-s-mobile {
width: 100px !important;
}
.is-w-m-mobile {
width: 150px !important;
}
.is-w-l-mobile {
width: 200px !important;
}
.is-w-xl-mobile {
width: 250px !important;
}
.is-w-xxl-mobile {
width: 500px !important;
}
.is-h-xs-mobile {
height: 80px !important;
}
.is-h-s-mobile {
height: 100px !important;
}
.is-h-m-mobile {
height: 150px !important;
}
.is-h-l-mobile {
height: 200px !important;
}
.is-h-xl-mobile {
height: 250px !important;
}
.is-h-xxl-mobile {
height: 500px !important;
}
}
@media only screen and (min-width: 769px) {
.is-w-auto-tablet {
width: auto !important;
}
.is-w-xs-tablet {
width: 80px !important;
}
.is-w-s-tablet {
width: 100px !important;
}
.is-w-m-tablet {
width: 150px !important;
}
.is-w-l-tablet {
width: 200px !important;
}
.is-w-xl-tablet {
width: 250px !important;
}
.is-w-xxl-tablet {
width: 500px !important;
}
.is-h-xs-tablet {
height: 80px !important;
}
.is-h-s-tablet {
height: 100px !important;
}
.is-h-m-tablet {
height: 150px !important;
}
.is-h-l-tablet {
height: 200px !important;
}
.is-h-xl-tablet {
height: 250px !important;
}
.is-h-xxl-tablet {
height: 500px !important;
}
}
@media only screen and (min-width: 1024px) {
.is-w-auto-desktop {
width: auto !important;
}
.is-w-xs-desktop {
width: 80px !important;
}
.is-w-s-desktop {
width: 100px !important;
}
.is-w-m-desktop {
width: 150px !important;
}
.is-w-l-desktop {
width: 200px !important;
}
.is-w-xl-desktop {
width: 250px !important;
}
.is-w-xxl-desktop {
width: 500px !important;
}
.is-h-xs-desktop {
height: 80px !important;
}
.is-h-s-desktop {
height: 100px !important;
}
.is-h-m-desktop {
height: 150px !important;
}
.is-h-l-desktop {
height: 200px !important;
}
.is-h-xl-desktop {
height: 250px !important;
}
.is-h-xxl-desktop {
height: 500px !important;
}
}

View file

@ -0,0 +1,167 @@
/* Spacings
*
* Those are supplementary rules to Bulmas. They follow the same conventions.
* Add those youll need.
******************************************************************************/
.mr-auto {
margin-right: auto !important;
}
.ml-auto {
margin-left: auto !important;
}
@media screen and (max-width: 768px) {
.m-0-mobile {
margin: 0 !important;
}
.mr-auto-mobile {
margin-right: auto !important;
}
.ml-auto-mobile {
margin-left: auto !important;
}
.mt-3-mobile {
margin-top: 0.75rem !important;
}
.ml-3-mobile {
margin-left: 0.75rem !important;
}
.mx-3-mobile {
margin-right: 0.75rem !important;
margin-left: 0.75rem !important;
}
.my-3-mobile {
margin-top: 0.75rem !important;
margin-bottom: 0.75rem !important;
}
}
@media screen and (min-width: 769px) {
.m-0-tablet {
margin: 0 !important;
}
.mr-auto-tablet {
margin-right: auto !important;
}
.ml-auto-tablet {
margin-left: auto !important;
}
.mt-3-tablet {
margin-top: 0.75rem !important;
}
.ml-3-tablet {
margin-left: 0.75rem !important;
}
.mx-3-tablet {
margin-right: 0.75rem !important;
margin-left: 0.75rem !important;
}
.my-3-tablet {
margin-top: 0.75rem !important;
margin-bottom: 0.75rem !important;
}
}
/* Gaps (for Flexbox and Grid)
*
* Those are supplementary rules to Bulmas. They follow the same conventions.
* Add those youll need.
******************************************************************************/
.is-gap-0 {
gap: 0;
}
.is-gap-1 {
gap: 0.25rem;
}
.is-gap-2 {
gap: 0.5rem;
}
.is-gap-3 {
gap: 0.75rem;
}
.is-gap-4 {
gap: 1rem;
}
.is-gap-5 {
gap: 1.5rem;
}
.is-gap-6 {
gap: 3rem;
}
.is-row-gap-0 {
row-gap: 0;
}
.is-row-gap-1 {
row-gap: 0.25rem;
}
.is-row-gap-2 {
row-gap: 0.5rem;
}
.is-row-gap-3 {
row-gap: 0.75rem;
}
.is-row-gap-4 {
row-gap: 1rem;
}
.is-row-gap-5 {
row-gap: 1.5rem;
}
.is-row-gap-6 {
row-gap: 3rem;
}
.is-column-gap-0 {
column-gap: 0;
}
.is-column-gap-1 {
column-gap: 0.25rem;
}
.is-column-gap-2 {
column-gap: 0.5rem;
}
.is-column-gap-3 {
column-gap: 0.75rem;
}
.is-column-gap-4 {
column-gap: 1rem;
}
.is-column-gap-5 {
column-gap: 1.5rem;
}
.is-column-gap-6 {
column-gap: 3rem;
}

View file

@ -0,0 +1,25 @@
/** Animations and transitions
******************************************************************************/
@keyframes turning {
from { transform: rotateZ(0deg); }
to { transform: rotateZ(360deg); }
}
.is-processing .icon-spinner::before {
animation: turning 1.5s infinite linear;
}
.icon-spinner {
display: none;
}
.is-processing .icon-spinner {
display: flex;
}
@media (prefers-reduced-motion: reduce) {
.is-processing .icon::before {
transition-duration: 0.001ms !important;
}
}

View file

@ -0,0 +1,55 @@
@import "../vendor/bulma/sass/utilities/derived-variables.sass";
/* Colors
******************************************************************************/
/* states */
$primary: #016a5b;
$info: #1f4666;
$success: #246447;
$warning: #8b6c15;
$danger: #872538;
/* book cover standins */
$no-cover-color: #002549;
/* background colors */
$scheme-main: $grey-darker;
$scheme-main-bis: $black-ter;
$background-body: $grey-darker;
$background-secondary: $grey-dark;
$background-tertiary: #555;
/* highlight colors */
$primary-highlight: $primary;
$info-highlight: $info;
$success-highlight: $success;
/* borders */
$border: $grey;
$border-hover: $grey-light;
$border-light: $grey;
$border-light-hover: $grey-light;
/* text */
$text: $grey-lightest;
$text-light: $grey-lighter;
$text-strong: $white-ter;
/* links */
$link: $white;
$link-background: $background-tertiary;
$link-hover: $white-bis;
$link-focus: $white-bis;
$link-active: $white-bis;
/* misc */
/* bulma overrides */
$background: $background-secondary;
$menu-item-active-background-color: $link-background;
/* Fonts
******************************************************************************/
$family-primary: $family-sans-serif;
$family-secondary: $family-sans-serif;

View file

@ -0,0 +1,53 @@
@import "../vendor/bulma/sass/utilities/derived-variables.sass";
/* Colors
******************************************************************************/
/* states */
$primary: $turquoise;
$info: $cyan;
$success: $green;
$warning: $yellow;
$danger: $red;
/* book cover standins */
$no-cover-color: #002549;
/* background colors */
$scheme-main: $white;
$scheme-main: $white-bis;
$background-body: $white;
$background-secondary: $white-ter;
$background-tertiary: $white-bis;
/* highlight colors */
$primary-highlight: $primary-light;
$info-highlight: $info-light;
$success-highlight: $success-light;
/* borders */
$border: $grey-lighter;
$border-hover: $grey-light;
$border-light: $grey-lightest;
$border-light-hover: $grey-light;
/* text */
$text: $grey-dark;
$text-light: $grey;
$text-strong: $grey-darker;
/* links */
$link: #3273dc;
$link-background: $link;
$link-hover: $grey-darker;
$link-focus: $grey-darker;
$link-active: $grey-darker;
/* bulma overrides */
$background: $background-secondary;
$menu-item-active-background-color: $link-background;
/* Fonts
******************************************************************************/
$family-primary: $family-sans-serif;
$family-secondary: $family-sans-serif;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020 Jeremy Thomas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,10 @@
@charset "utf-8"
/*! bulma.io v0.9.3 | MIT License | github.com/jgthms/bulma */
@import "sass/utilities/_all"
@import "sass/base/_all"
@import "sass/elements/_all"
@import "sass/form/_all"
@import "sass/components/_all"
@import "sass/grid/_all"
@import "sass/helpers/_all"
@import "sass/layout/_all"

View file

@ -0,0 +1,56 @@
{
"name": "bulma",
"version": "0.9.3",
"homepage": "https://bulma.io",
"author": {
"name": "Jeremy Thomas",
"email": "bbxdesign@gmail.com",
"url": "https://jgthms.com"
},
"description": "Modern CSS framework based on Flexbox",
"main": "bulma.sass",
"unpkg": "css/bulma.css",
"style": "bulma/css/bulma.min.css",
"repository": {
"type": "git",
"url": "git+https://github.com/jgthms/bulma.git"
},
"license": "MIT",
"keywords": [
"css",
"sass",
"flexbox",
"responsive",
"framework"
],
"bugs": {
"url": "https://github.com/jgthms/bulma/issues"
},
"devDependencies": {
"autoprefixer": "^9.8.6",
"clean-css-cli": "^4.3.0",
"node-sass": "^4.14.1",
"postcss-cli": "^7.1.2",
"rimraf": "^3.0.2"
},
"scripts": {
"build": "npm run build-sass && npm run build-autoprefix && npm run build-cleancss",
"build-autoprefix": "postcss --use autoprefixer --map false --output css/bulma.css css/bulma.css",
"build-cleancss": "cleancss -o css/bulma.min.css css/bulma.css",
"build-sass": "node-sass --output-style expanded --source-map true bulma.sass css/bulma.css",
"clean": "rimraf css",
"rtl": "npm run rtl-sass && npm run rtl-autoprefix && npm run rtl-cleancss",
"rtl-sass": "node-sass --output-style expanded --source-map true bulma-rtl.sass css/bulma-rtl.css",
"rtl-autoprefix": "postcss --use autoprefixer --map false --output css/bulma-rtl.css css/bulma-rtl.css",
"rtl-cleancss": "cleancss -o css/bulma-rtl.min.css css/bulma-rtl.css",
"deploy": "npm run clean && npm run build && npm run rtl",
"start": "npm run build-sass -- --watch"
},
"files": [
"css",
"sass",
"bulma.sass",
"LICENSE",
"README.md"
]
}

View file

@ -0,0 +1,6 @@
/* Bulma Base */
@charset "utf-8"
@import "minireset"
@import "generic"
@import "animations"

View file

@ -0,0 +1,5 @@
@keyframes spinAround
from
transform: rotate(0deg)
to
transform: rotate(359deg)

View file

@ -0,0 +1,145 @@
@import "../utilities/mixins"
$body-background-color: $scheme-main !default
$body-size: 16px !default
$body-min-width: 300px !default
$body-rendering: optimizeLegibility !default
$body-family: $family-primary !default
$body-overflow-x: hidden !default
$body-overflow-y: scroll !default
$body-color: $text !default
$body-font-size: 1em !default
$body-weight: $weight-normal !default
$body-line-height: 1.5 !default
$code-family: $family-code !default
$code-padding: 0.25em 0.5em 0.25em !default
$code-weight: normal !default
$code-size: 0.875em !default
$small-font-size: 0.875em !default
$hr-background-color: $background !default
$hr-height: 2px !default
$hr-margin: 1.5rem 0 !default
$strong-color: $text-strong !default
$strong-weight: $weight-bold !default
$pre-font-size: 0.875em !default
$pre-padding: 1.25rem 1.5rem !default
$pre-code-font-size: 1em !default
html
background-color: $body-background-color
font-size: $body-size
-moz-osx-font-smoothing: grayscale
-webkit-font-smoothing: antialiased
min-width: $body-min-width
overflow-x: $body-overflow-x
overflow-y: $body-overflow-y
text-rendering: $body-rendering
text-size-adjust: 100%
article,
aside,
figure,
footer,
header,
hgroup,
section
display: block
body,
button,
input,
optgroup,
select,
textarea
font-family: $body-family
code,
pre
-moz-osx-font-smoothing: auto
-webkit-font-smoothing: auto
font-family: $code-family
body
color: $body-color
font-size: $body-font-size
font-weight: $body-weight
line-height: $body-line-height
// Inline
a
color: $link
cursor: pointer
text-decoration: none
strong
color: currentColor
&:hover
color: $link-hover
code
background-color: $code-background
color: $code
font-size: $code-size
font-weight: $code-weight
padding: $code-padding
hr
background-color: $hr-background-color
border: none
display: block
height: $hr-height
margin: $hr-margin
img
height: auto
max-width: 100%
input[type="checkbox"],
input[type="radio"]
vertical-align: baseline
small
font-size: $small-font-size
span
font-style: inherit
font-weight: inherit
strong
color: $strong-color
font-weight: $strong-weight
// Block
fieldset
border: none
pre
+overflow-touch
background-color: $pre-background
color: $pre
font-size: $pre-font-size
overflow-x: auto
padding: $pre-padding
white-space: pre
word-wrap: normal
code
background-color: transparent
color: currentColor
font-size: $pre-code-font-size
padding: 0
table
td,
th
vertical-align: top
&:not([align])
text-align: inherit
th
color: $text-strong

View file

@ -0,0 +1 @@
@warn "The helpers.sass file is DEPRECATED. It has moved into its own /helpers folder. Please import sass/helpers/_all instead."

View file

@ -0,0 +1,79 @@
/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */
// Blocks
html,
body,
p,
ol,
ul,
li,
dl,
dt,
dd,
blockquote,
figure,
fieldset,
legend,
textarea,
pre,
iframe,
hr,
h1,
h2,
h3,
h4,
h5,
h6
margin: 0
padding: 0
// Headings
h1,
h2,
h3,
h4,
h5,
h6
font-size: 100%
font-weight: normal
// List
ul
list-style: none
// Form
button,
input,
select,
textarea
margin: 0
// Box sizing
html
box-sizing: border-box
*
&,
&::before,
&::after
box-sizing: inherit
// Media
img,
video
height: auto
max-width: 100%
// Iframe
iframe
border: 0
// Table
table
border-collapse: collapse
border-spacing: 0
td,
th
padding: 0
&:not([align])
text-align: inherit

View file

@ -0,0 +1,15 @@
/* Bulma Components */
@charset "utf-8"
@import "breadcrumb"
@import "card"
@import "dropdown"
@import "level"
@import "media"
@import "menu"
@import "message"
@import "modal"
@import "navbar"
@import "pagination"
@import "panel"
@import "tabs"

View file

@ -0,0 +1,77 @@
@import "../utilities/mixins"
$breadcrumb-item-color: $link !default
$breadcrumb-item-hover-color: $link-hover !default
$breadcrumb-item-active-color: $text-strong !default
$breadcrumb-item-padding-vertical: 0 !default
$breadcrumb-item-padding-horizontal: 0.75em !default
$breadcrumb-item-separator-color: $border-hover !default
.breadcrumb
@extend %block
@extend %unselectable
font-size: $size-normal
white-space: nowrap
a
align-items: center
color: $breadcrumb-item-color
display: flex
justify-content: center
padding: $breadcrumb-item-padding-vertical $breadcrumb-item-padding-horizontal
&:hover
color: $breadcrumb-item-hover-color
li
align-items: center
display: flex
&:first-child a
+ltr-property("padding", 0, false)
&.is-active
a
color: $breadcrumb-item-active-color
cursor: default
pointer-events: none
& + li::before
color: $breadcrumb-item-separator-color
content: "\0002f"
ul,
ol
align-items: flex-start
display: flex
flex-wrap: wrap
justify-content: flex-start
.icon
&:first-child
+ltr-property("margin", 0.5em)
&:last-child
+ltr-property("margin", 0.5em, false)
// Alignment
&.is-centered
ol,
ul
justify-content: center
&.is-right
ol,
ul
justify-content: flex-end
// Sizes
&.is-small
font-size: $size-small
&.is-medium
font-size: $size-medium
&.is-large
font-size: $size-large
// Styles
&.has-arrow-separator
li + li::before
content: "\02192"
&.has-bullet-separator
li + li::before
content: "\02022"
&.has-dot-separator
li + li::before
content: "\000b7"
&.has-succeeds-separator
li + li::before
content: "\0227B"

View file

@ -0,0 +1,103 @@
@import "../utilities/mixins"
$card-color: $text !default
$card-background-color: $scheme-main !default
$card-shadow: $shadow !default
$card-radius: 0.25rem !default
$card-header-background-color: transparent !default
$card-header-color: $text-strong !default
$card-header-padding: 0.75rem 1rem !default
$card-header-shadow: 0 0.125em 0.25em rgba($scheme-invert, 0.1) !default
$card-header-weight: $weight-bold !default
$card-content-background-color: transparent !default
$card-content-padding: 1.5rem !default
$card-footer-background-color: transparent !default
$card-footer-border-top: 1px solid $border-light !default
$card-footer-padding: 0.75rem !default
$card-media-margin: $block-spacing !default
.card
background-color: $card-background-color
border-radius: $card-radius
box-shadow: $card-shadow
color: $card-color
max-width: 100%
position: relative
%card-item
&:first-child
border-top-left-radius: $card-radius
border-top-right-radius: $card-radius
&:last-child
border-bottom-left-radius: $card-radius
border-bottom-right-radius: $card-radius
.card-header
@extend %card-item
background-color: $card-header-background-color
align-items: stretch
box-shadow: $card-header-shadow
display: flex
.card-header-title
align-items: center
color: $card-header-color
display: flex
flex-grow: 1
font-weight: $card-header-weight
padding: $card-header-padding
&.is-centered
justify-content: center
.card-header-icon
+reset
align-items: center
cursor: pointer
display: flex
justify-content: center
padding: $card-header-padding
.card-image
display: block
position: relative
&:first-child
img
border-top-left-radius: $card-radius
border-top-right-radius: $card-radius
&:last-child
img
border-bottom-left-radius: $card-radius
border-bottom-right-radius: $card-radius
.card-content
@extend %card-item
background-color: $card-content-background-color
padding: $card-content-padding
.card-footer
@extend %card-item
background-color: $card-footer-background-color
border-top: $card-footer-border-top
align-items: stretch
display: flex
.card-footer-item
align-items: center
display: flex
flex-basis: 0
flex-grow: 1
flex-shrink: 0
justify-content: center
padding: $card-footer-padding
&:not(:last-child)
+ltr-property("border", $card-footer-border-top)
// Combinations
.card
.media:not(:last-child)
margin-bottom: $card-media-margin

View file

@ -0,0 +1,83 @@
@import "../utilities/mixins"
$dropdown-menu-min-width: 12rem !default
$dropdown-content-background-color: $scheme-main !default
$dropdown-content-arrow: $link !default
$dropdown-content-offset: 4px !default
$dropdown-content-padding-bottom: 0.5rem !default
$dropdown-content-padding-top: 0.5rem !default
$dropdown-content-radius: $radius !default
$dropdown-content-shadow: $shadow !default
$dropdown-content-z: 20 !default
$dropdown-item-color: $text !default
$dropdown-item-hover-color: $scheme-invert !default
$dropdown-item-hover-background-color: $background !default
$dropdown-item-active-color: $link-invert !default
$dropdown-item-active-background-color: $link !default
$dropdown-divider-background-color: $border-light !default
.dropdown
display: inline-flex
position: relative
vertical-align: top
&.is-active,
&.is-hoverable:hover
.dropdown-menu
display: block
&.is-right
.dropdown-menu
left: auto
right: 0
&.is-up
.dropdown-menu
bottom: 100%
padding-bottom: $dropdown-content-offset
padding-top: initial
top: auto
.dropdown-menu
display: none
+ltr-position(0, false)
min-width: $dropdown-menu-min-width
padding-top: $dropdown-content-offset
position: absolute
top: 100%
z-index: $dropdown-content-z
.dropdown-content
background-color: $dropdown-content-background-color
border-radius: $dropdown-content-radius
box-shadow: $dropdown-content-shadow
padding-bottom: $dropdown-content-padding-bottom
padding-top: $dropdown-content-padding-top
.dropdown-item
color: $dropdown-item-color
display: block
font-size: 0.875rem
line-height: 1.5
padding: 0.375rem 1rem
position: relative
a.dropdown-item,
button.dropdown-item
+ltr-property("padding", 3rem)
text-align: inherit
white-space: nowrap
width: 100%
&:hover
background-color: $dropdown-item-hover-background-color
color: $dropdown-item-hover-color
&.is-active
background-color: $dropdown-item-active-background-color
color: $dropdown-item-active-color
.dropdown-divider
background-color: $dropdown-divider-background-color
border: none
display: block
height: 1px
margin: 0.5rem 0

View file

@ -0,0 +1,79 @@
@import "../utilities/mixins"
$level-item-spacing: ($block-spacing * 0.5) !default
.level
@extend %block
align-items: center
justify-content: space-between
code
border-radius: $radius
img
display: inline-block
vertical-align: top
// Modifiers
&.is-mobile
display: flex
.level-left,
.level-right
display: flex
.level-left + .level-right
margin-top: 0
.level-item
&:not(:last-child)
margin-bottom: 0
+ltr-property("margin", $level-item-spacing)
&:not(.is-narrow)
flex-grow: 1
// Responsiveness
+tablet
display: flex
& > .level-item
&:not(.is-narrow)
flex-grow: 1
.level-item
align-items: center
display: flex
flex-basis: auto
flex-grow: 0
flex-shrink: 0
justify-content: center
.title,
.subtitle
margin-bottom: 0
// Responsiveness
+mobile
&:not(:last-child)
margin-bottom: $level-item-spacing
.level-left,
.level-right
flex-basis: auto
flex-grow: 0
flex-shrink: 0
.level-item
// Modifiers
&.is-flexible
flex-grow: 1
// Responsiveness
+tablet
&:not(:last-child)
+ltr-property("margin", $level-item-spacing)
.level-left
align-items: center
justify-content: flex-start
// Responsiveness
+mobile
& + .level-right
margin-top: 1.5rem
+tablet
display: flex
.level-right
align-items: center
justify-content: flex-end
// Responsiveness
+tablet
display: flex

View file

@ -0,0 +1,59 @@
@import "../utilities/mixins"
$media-border-color: bulmaRgba($border, 0.5) !default
$media-border-size: 1px !default
$media-spacing: 1rem !default
$media-spacing-large: 1.5rem !default
$media-content-spacing: 0.75rem !default
$media-level-1-spacing: 0.75rem !default
$media-level-1-content-spacing: 0.5rem !default
$media-level-2-spacing: 0.5rem !default
.media
align-items: flex-start
display: flex
text-align: inherit
.content:not(:last-child)
margin-bottom: $media-content-spacing
.media
border-top: $media-border-size solid $media-border-color
display: flex
padding-top: $media-level-1-spacing
.content:not(:last-child),
.control:not(:last-child)
margin-bottom: $media-level-1-content-spacing
.media
padding-top: $media-level-2-spacing
& + .media
margin-top: $media-level-2-spacing
& + .media
border-top: $media-border-size solid $media-border-color
margin-top: $media-spacing
padding-top: $media-spacing
// Sizes
&.is-large
& + .media
margin-top: $media-spacing-large
padding-top: $media-spacing-large
.media-left,
.media-right
flex-basis: auto
flex-grow: 0
flex-shrink: 0
.media-left
+ltr-property("margin", $media-spacing)
.media-right
+ltr-property("margin", $media-spacing, false)
.media-content
flex-basis: auto
flex-grow: 1
flex-shrink: 1
text-align: inherit
+mobile
.media-content
overflow-x: auto

View file

@ -0,0 +1,59 @@
@import "../utilities/mixins"
$menu-item-color: $text !default
$menu-item-radius: $radius-small !default
$menu-item-hover-color: $text-strong !default
$menu-item-hover-background-color: $background !default
$menu-item-active-color: $link-invert !default
$menu-item-active-background-color: $link !default
$menu-list-border-left: 1px solid $border !default
$menu-list-line-height: 1.25 !default
$menu-list-link-padding: 0.5em 0.75em !default
$menu-nested-list-margin: 0.75em !default
$menu-nested-list-padding-left: 0.75em !default
$menu-label-color: $text-light !default
$menu-label-font-size: 0.75em !default
$menu-label-letter-spacing: 0.1em !default
$menu-label-spacing: 1em !default
.menu
font-size: $size-normal
// Sizes
&.is-small
font-size: $size-small
&.is-medium
font-size: $size-medium
&.is-large
font-size: $size-large
.menu-list
line-height: $menu-list-line-height
a
border-radius: $menu-item-radius
color: $menu-item-color
display: block
padding: $menu-list-link-padding
&:hover
background-color: $menu-item-hover-background-color
color: $menu-item-hover-color
// Modifiers
&.is-active
background-color: $menu-item-active-background-color
color: $menu-item-active-color
li
ul
+ltr-property("border", $menu-list-border-left, false)
margin: $menu-nested-list-margin
+ltr-property("padding", $menu-nested-list-padding-left, false)
.menu-label
color: $menu-label-color
font-size: $menu-label-font-size
letter-spacing: $menu-label-letter-spacing
text-transform: uppercase
&:not(:first-child)
margin-top: $menu-label-spacing
&:not(:last-child)
margin-bottom: $menu-label-spacing

View file

@ -0,0 +1,101 @@
@import "../utilities/mixins"
$message-background-color: $background !default
$message-radius: $radius !default
$message-header-background-color: $text !default
$message-header-color: $text-invert !default
$message-header-weight: $weight-bold !default
$message-header-padding: 0.75em 1em !default
$message-header-radius: $radius !default
$message-body-border-color: $border !default
$message-body-border-width: 0 0 0 4px !default
$message-body-color: $text !default
$message-body-padding: 1.25em 1.5em !default
$message-body-radius: $radius !default
$message-body-pre-background-color: $scheme-main !default
$message-body-pre-code-background-color: transparent !default
$message-header-body-border-width: 0 !default
$message-colors: $colors !default
.message
@extend %block
background-color: $message-background-color
border-radius: $message-radius
font-size: $size-normal
strong
color: currentColor
a:not(.button):not(.tag):not(.dropdown-item)
color: currentColor
text-decoration: underline
// Sizes
&.is-small
font-size: $size-small
&.is-medium
font-size: $size-medium
&.is-large
font-size: $size-large
// Colors
@each $name, $components in $message-colors
$color: nth($components, 1)
$color-invert: nth($components, 2)
$color-light: null
$color-dark: null
@if length($components) >= 3
$color-light: nth($components, 3)
@if length($components) >= 4
$color-dark: nth($components, 4)
@else
$color-luminance: colorLuminance($color)
$darken-percentage: $color-luminance * 70%
$desaturate-percentage: $color-luminance * 30%
$color-dark: desaturate(darken($color, $darken-percentage), $desaturate-percentage)
@else
$color-lightning: max((100% - lightness($color)) - 2%, 0%)
$color-light: lighten($color, $color-lightning)
&.is-#{$name}
background-color: $color-light
.message-header
background-color: $color
color: $color-invert
.message-body
border-color: $color
color: $color-dark
.message-header
align-items: center
background-color: $message-header-background-color
border-radius: $message-header-radius $message-header-radius 0 0
color: $message-header-color
display: flex
font-weight: $message-header-weight
justify-content: space-between
line-height: 1.25
padding: $message-header-padding
position: relative
.delete
flex-grow: 0
flex-shrink: 0
+ltr-property("margin", 0.75em, false)
& + .message-body
border-width: $message-header-body-border-width
border-top-left-radius: 0
border-top-right-radius: 0
.message-body
border-color: $message-body-border-color
border-radius: $message-body-radius
border-style: solid
border-width: $message-body-border-width
color: $message-body-color
padding: $message-body-padding
code,
pre
background-color: $message-body-pre-background-color
pre code
background-color: $message-body-pre-code-background-color

View file

@ -0,0 +1,117 @@
@import "../utilities/mixins"
$modal-z: 40 !default
$modal-background-background-color: bulmaRgba($scheme-invert, 0.86) !default
$modal-content-width: 640px !default
$modal-content-margin-mobile: 20px !default
$modal-content-spacing-mobile: 160px !default
$modal-content-spacing-tablet: 40px !default
$modal-close-dimensions: 40px !default
$modal-close-right: 20px !default
$modal-close-top: 20px !default
$modal-card-spacing: 40px !default
$modal-card-head-background-color: $background !default
$modal-card-head-border-bottom: 1px solid $border !default
$modal-card-head-padding: 20px !default
$modal-card-head-radius: $radius-large !default
$modal-card-title-color: $text-strong !default
$modal-card-title-line-height: 1 !default
$modal-card-title-size: $size-4 !default
$modal-card-foot-radius: $radius-large !default
$modal-card-foot-border-top: 1px solid $border !default
$modal-card-body-background-color: $scheme-main !default
$modal-card-body-padding: 20px !default
$modal-breakpoint: $tablet !default
.modal
@extend %overlay
align-items: center
display: none
flex-direction: column
justify-content: center
overflow: hidden
position: fixed
z-index: $modal-z
// Modifiers
&.is-active
display: flex
.modal-background
@extend %overlay
background-color: $modal-background-background-color
.modal-content,
.modal-card
margin: 0 $modal-content-margin-mobile
max-height: calc(100vh - #{$modal-content-spacing-mobile})
overflow: auto
position: relative
width: 100%
// Responsiveness
+from($modal-breakpoint)
margin: 0 auto
max-height: calc(100vh - #{$modal-content-spacing-tablet})
width: $modal-content-width
.modal-close
@extend %delete
background: none
height: $modal-close-dimensions
position: fixed
+ltr-position($modal-close-right)
top: $modal-close-top
width: $modal-close-dimensions
.modal-card
display: flex
flex-direction: column
max-height: calc(100vh - #{$modal-card-spacing})
overflow: hidden
-ms-overflow-y: visible
.modal-card-head,
.modal-card-foot
align-items: center
background-color: $modal-card-head-background-color
display: flex
flex-shrink: 0
justify-content: flex-start
padding: $modal-card-head-padding
position: relative
.modal-card-head
border-bottom: $modal-card-head-border-bottom
border-top-left-radius: $modal-card-head-radius
border-top-right-radius: $modal-card-head-radius
.modal-card-title
color: $modal-card-title-color
flex-grow: 1
flex-shrink: 0
font-size: $modal-card-title-size
line-height: $modal-card-title-line-height
.modal-card-foot
border-bottom-left-radius: $modal-card-foot-radius
border-bottom-right-radius: $modal-card-foot-radius
border-top: $modal-card-foot-border-top
.button
&:not(:last-child)
+ltr-property("margin", 0.5em)
.modal-card-body
+overflow-touch
background-color: $modal-card-body-background-color
flex-grow: 1
flex-shrink: 1
overflow: auto
padding: $modal-card-body-padding

View file

@ -0,0 +1,446 @@
@import "../utilities/mixins"
$navbar-background-color: $scheme-main !default
$navbar-box-shadow-size: 0 2px 0 0 !default
$navbar-box-shadow-color: $background !default
$navbar-height: 3.25rem !default
$navbar-padding-vertical: 1rem !default
$navbar-padding-horizontal: 2rem !default
$navbar-z: 30 !default
$navbar-fixed-z: 30 !default
$navbar-item-color: $text !default
$navbar-item-hover-color: $link !default
$navbar-item-hover-background-color: $scheme-main-bis !default
$navbar-item-active-color: $scheme-invert !default
$navbar-item-active-background-color: transparent !default
$navbar-item-img-max-height: 1.75rem !default
$navbar-burger-color: $navbar-item-color !default
$navbar-tab-hover-background-color: transparent !default
$navbar-tab-hover-border-bottom-color: $link !default
$navbar-tab-active-color: $link !default
$navbar-tab-active-background-color: transparent !default
$navbar-tab-active-border-bottom-color: $link !default
$navbar-tab-active-border-bottom-style: solid !default
$navbar-tab-active-border-bottom-width: 3px !default
$navbar-dropdown-background-color: $scheme-main !default
$navbar-dropdown-border-top: 2px solid $border !default
$navbar-dropdown-offset: -4px !default
$navbar-dropdown-arrow: $link !default
$navbar-dropdown-radius: $radius-large !default
$navbar-dropdown-z: 20 !default
$navbar-dropdown-boxed-radius: $radius-large !default
$navbar-dropdown-boxed-shadow: 0 8px 8px bulmaRgba($scheme-invert, 0.1), 0 0 0 1px bulmaRgba($scheme-invert, 0.1) !default
$navbar-dropdown-item-hover-color: $scheme-invert !default
$navbar-dropdown-item-hover-background-color: $background !default
$navbar-dropdown-item-active-color: $link !default
$navbar-dropdown-item-active-background-color: $background !default
$navbar-divider-background-color: $background !default
$navbar-divider-height: 2px !default
$navbar-bottom-box-shadow-size: 0 -2px 0 0 !default
$navbar-breakpoint: $desktop !default
$navbar-colors: $colors !default
=navbar-fixed
left: 0
position: fixed
right: 0
z-index: $navbar-fixed-z
.navbar
background-color: $navbar-background-color
min-height: $navbar-height
position: relative
z-index: $navbar-z
@each $name, $pair in $navbar-colors
$color: nth($pair, 1)
$color-invert: nth($pair, 2)
&.is-#{$name}
background-color: $color
color: $color-invert
.navbar-brand
& > .navbar-item,
.navbar-link
color: $color-invert
& > a.navbar-item,
.navbar-link
&:focus,
&:hover,
&.is-active
background-color: bulmaDarken($color, 5%)
color: $color-invert
.navbar-link
&::after
border-color: $color-invert
.navbar-burger
color: $color-invert
+from($navbar-breakpoint)
.navbar-start,
.navbar-end
& > .navbar-item,
.navbar-link
color: $color-invert
& > a.navbar-item,
.navbar-link
&:focus,
&:hover,
&.is-active
background-color: bulmaDarken($color, 5%)
color: $color-invert
.navbar-link
&::after
border-color: $color-invert
.navbar-item.has-dropdown:focus .navbar-link,
.navbar-item.has-dropdown:hover .navbar-link,
.navbar-item.has-dropdown.is-active .navbar-link
background-color: bulmaDarken($color, 5%)
color: $color-invert
.navbar-dropdown
a.navbar-item
&.is-active
background-color: $color
color: $color-invert
& > .container
align-items: stretch
display: flex
min-height: $navbar-height
width: 100%
&.has-shadow
box-shadow: $navbar-box-shadow-size $navbar-box-shadow-color
&.is-fixed-bottom,
&.is-fixed-top
+navbar-fixed
&.is-fixed-bottom
bottom: 0
&.has-shadow
box-shadow: $navbar-bottom-box-shadow-size $navbar-box-shadow-color
&.is-fixed-top
top: 0
html,
body
&.has-navbar-fixed-top
padding-top: $navbar-height
&.has-navbar-fixed-bottom
padding-bottom: $navbar-height
.navbar-brand,
.navbar-tabs
align-items: stretch
display: flex
flex-shrink: 0
min-height: $navbar-height
.navbar-brand
a.navbar-item
&:focus,
&:hover
background-color: transparent
.navbar-tabs
+overflow-touch
max-width: 100vw
overflow-x: auto
overflow-y: hidden
.navbar-burger
@extend %reset
color: $navbar-burger-color
+hamburger($navbar-height)
+ltr-property("margin", auto, false)
.navbar-menu
display: none
.navbar-item,
.navbar-link
color: $navbar-item-color
display: block
line-height: 1.5
padding: 0.5rem 0.75rem
position: relative
.icon
&:only-child
margin-left: -0.25rem
margin-right: -0.25rem
a.navbar-item,
.navbar-link
cursor: pointer
&:focus,
&:focus-within,
&:hover,
&.is-active
background-color: $navbar-item-hover-background-color
color: $navbar-item-hover-color
.navbar-item
flex-grow: 0
flex-shrink: 0
img
max-height: $navbar-item-img-max-height
&.has-dropdown
padding: 0
&.is-expanded
flex-grow: 1
flex-shrink: 1
&.is-tab
border-bottom: 1px solid transparent
min-height: $navbar-height
padding-bottom: calc(0.5rem - 1px)
&:focus,
&:hover
background-color: $navbar-tab-hover-background-color
border-bottom-color: $navbar-tab-hover-border-bottom-color
&.is-active
background-color: $navbar-tab-active-background-color
border-bottom-color: $navbar-tab-active-border-bottom-color
border-bottom-style: $navbar-tab-active-border-bottom-style
border-bottom-width: $navbar-tab-active-border-bottom-width
color: $navbar-tab-active-color
padding-bottom: calc(0.5rem - #{$navbar-tab-active-border-bottom-width})
.navbar-content
flex-grow: 1
flex-shrink: 1
.navbar-link:not(.is-arrowless)
+ltr-property("padding", 2.5em)
&::after
@extend %arrow
border-color: $navbar-dropdown-arrow
margin-top: -0.375em
+ltr-position(1.125em)
.navbar-dropdown
font-size: 0.875rem
padding-bottom: 0.5rem
padding-top: 0.5rem
.navbar-item
padding-left: 1.5rem
padding-right: 1.5rem
.navbar-divider
background-color: $navbar-divider-background-color
border: none
display: none
height: $navbar-divider-height
margin: 0.5rem 0
+until($navbar-breakpoint)
.navbar > .container
display: block
.navbar-brand,
.navbar-tabs
.navbar-item
align-items: center
display: flex
.navbar-link
&::after
display: none
.navbar-menu
background-color: $navbar-background-color
box-shadow: 0 8px 16px bulmaRgba($scheme-invert, 0.1)
padding: 0.5rem 0
&.is-active
display: block
// Fixed navbar
.navbar
&.is-fixed-bottom-touch,
&.is-fixed-top-touch
+navbar-fixed
&.is-fixed-bottom-touch
bottom: 0
&.has-shadow
box-shadow: 0 -2px 3px bulmaRgba($scheme-invert, 0.1)
&.is-fixed-top-touch
top: 0
&.is-fixed-top,
&.is-fixed-top-touch
.navbar-menu
+overflow-touch
max-height: calc(100vh - #{$navbar-height})
overflow: auto
html,
body
&.has-navbar-fixed-top-touch
padding-top: $navbar-height
&.has-navbar-fixed-bottom-touch
padding-bottom: $navbar-height
+from($navbar-breakpoint)
.navbar,
.navbar-menu,
.navbar-start,
.navbar-end
align-items: stretch
display: flex
.navbar
min-height: $navbar-height
&.is-spaced
padding: $navbar-padding-vertical $navbar-padding-horizontal
.navbar-start,
.navbar-end
align-items: center
a.navbar-item,
.navbar-link
border-radius: $radius
&.is-transparent
a.navbar-item,
.navbar-link
&:focus,
&:hover,
&.is-active
background-color: transparent !important
.navbar-item.has-dropdown
&.is-active,
&.is-hoverable:focus,
&.is-hoverable:focus-within,
&.is-hoverable:hover
.navbar-link
background-color: transparent !important
.navbar-dropdown
a.navbar-item
&:focus,
&:hover
background-color: $navbar-dropdown-item-hover-background-color
color: $navbar-dropdown-item-hover-color
&.is-active
background-color: $navbar-dropdown-item-active-background-color
color: $navbar-dropdown-item-active-color
.navbar-burger
display: none
.navbar-item,
.navbar-link
align-items: center
display: flex
.navbar-item
&.has-dropdown
align-items: stretch
&.has-dropdown-up
.navbar-link::after
transform: rotate(135deg) translate(0.25em, -0.25em)
.navbar-dropdown
border-bottom: $navbar-dropdown-border-top
border-radius: $navbar-dropdown-radius $navbar-dropdown-radius 0 0
border-top: none
bottom: 100%
box-shadow: 0 -8px 8px bulmaRgba($scheme-invert, 0.1)
top: auto
&.is-active,
&.is-hoverable:focus,
&.is-hoverable:focus-within,
&.is-hoverable:hover
.navbar-dropdown
display: block
.navbar.is-spaced &,
&.is-boxed
opacity: 1
pointer-events: auto
transform: translateY(0)
.navbar-menu
flex-grow: 1
flex-shrink: 0
.navbar-start
justify-content: flex-start
+ltr-property("margin", auto)
.navbar-end
justify-content: flex-end
+ltr-property("margin", auto, false)
.navbar-dropdown
background-color: $navbar-dropdown-background-color
border-bottom-left-radius: $navbar-dropdown-radius
border-bottom-right-radius: $navbar-dropdown-radius
border-top: $navbar-dropdown-border-top
box-shadow: 0 8px 8px bulmaRgba($scheme-invert, 0.1)
display: none
font-size: 0.875rem
+ltr-position(0, false)
min-width: 100%
position: absolute
top: 100%
z-index: $navbar-dropdown-z
.navbar-item
padding: 0.375rem 1rem
white-space: nowrap
a.navbar-item
+ltr-property("padding", 3rem)
&:focus,
&:hover
background-color: $navbar-dropdown-item-hover-background-color
color: $navbar-dropdown-item-hover-color
&.is-active
background-color: $navbar-dropdown-item-active-background-color
color: $navbar-dropdown-item-active-color
.navbar.is-spaced &,
&.is-boxed
border-radius: $navbar-dropdown-boxed-radius
border-top: none
box-shadow: $navbar-dropdown-boxed-shadow
display: block
opacity: 0
pointer-events: none
top: calc(100% + (#{$navbar-dropdown-offset}))
transform: translateY(-5px)
transition-duration: $speed
transition-property: opacity, transform
&.is-right
left: auto
right: 0
.navbar-divider
display: block
.navbar > .container,
.container > .navbar
.navbar-brand
+ltr-property("margin", -.75rem, false)
.navbar-menu
+ltr-property("margin", -.75rem)
// Fixed navbar
.navbar
&.is-fixed-bottom-desktop,
&.is-fixed-top-desktop
+navbar-fixed
&.is-fixed-bottom-desktop
bottom: 0
&.has-shadow
box-shadow: 0 -2px 3px bulmaRgba($scheme-invert, 0.1)
&.is-fixed-top-desktop
top: 0
html,
body
&.has-navbar-fixed-top-desktop
padding-top: $navbar-height
&.has-navbar-fixed-bottom-desktop
padding-bottom: $navbar-height
&.has-spaced-navbar-fixed-top
padding-top: $navbar-height + ($navbar-padding-vertical * 2)
&.has-spaced-navbar-fixed-bottom
padding-bottom: $navbar-height + ($navbar-padding-vertical * 2)
// Hover/Active states
a.navbar-item,
.navbar-link
&.is-active
color: $navbar-item-active-color
&.is-active:not(:focus):not(:hover)
background-color: $navbar-item-active-background-color
.navbar-item.has-dropdown
&:focus,
&:hover,
&.is-active
.navbar-link
background-color: $navbar-item-hover-background-color
// Combination
.hero
&.is-fullheight-with-navbar
min-height: calc(100vh - #{$navbar-height})

View file

@ -0,0 +1,166 @@
@import "../utilities/controls"
@import "../utilities/mixins"
$pagination-color: $text-strong !default
$pagination-border-color: $border !default
$pagination-margin: -0.25rem !default
$pagination-min-width: $control-height !default
$pagination-item-font-size: 1em !default
$pagination-item-margin: 0.25rem !default
$pagination-item-padding-left: 0.5em !default
$pagination-item-padding-right: 0.5em !default
$pagination-nav-padding-left: 0.75em !default
$pagination-nav-padding-right: 0.75em !default
$pagination-hover-color: $link-hover !default
$pagination-hover-border-color: $link-hover-border !default
$pagination-focus-color: $link-focus !default
$pagination-focus-border-color: $link-focus-border !default
$pagination-active-color: $link-active !default
$pagination-active-border-color: $link-active-border !default
$pagination-disabled-color: $text-light !default
$pagination-disabled-background-color: $border !default
$pagination-disabled-border-color: $border !default
$pagination-current-color: $link-invert !default
$pagination-current-background-color: $link !default
$pagination-current-border-color: $link !default
$pagination-ellipsis-color: $grey-light !default
$pagination-shadow-inset: inset 0 1px 2px rgba($scheme-invert, 0.2) !default
.pagination
@extend %block
font-size: $size-normal
margin: $pagination-margin
// Sizes
&.is-small
font-size: $size-small
&.is-medium
font-size: $size-medium
&.is-large
font-size: $size-large
&.is-rounded
.pagination-previous,
.pagination-next
padding-left: 1em
padding-right: 1em
border-radius: $radius-rounded
.pagination-link
border-radius: $radius-rounded
.pagination,
.pagination-list
align-items: center
display: flex
justify-content: center
text-align: center
.pagination-previous,
.pagination-next,
.pagination-link,
.pagination-ellipsis
@extend %control
@extend %unselectable
font-size: $pagination-item-font-size
justify-content: center
margin: $pagination-item-margin
padding-left: $pagination-item-padding-left
padding-right: $pagination-item-padding-right
text-align: center
.pagination-previous,
.pagination-next,
.pagination-link
border-color: $pagination-border-color
color: $pagination-color
min-width: $pagination-min-width
&:hover
border-color: $pagination-hover-border-color
color: $pagination-hover-color
&:focus
border-color: $pagination-focus-border-color
&:active
box-shadow: $pagination-shadow-inset
&[disabled]
background-color: $pagination-disabled-background-color
border-color: $pagination-disabled-border-color
box-shadow: none
color: $pagination-disabled-color
opacity: 0.5
.pagination-previous,
.pagination-next
padding-left: $pagination-nav-padding-left
padding-right: $pagination-nav-padding-right
white-space: nowrap
.pagination-link
&.is-current
background-color: $pagination-current-background-color
border-color: $pagination-current-border-color
color: $pagination-current-color
.pagination-ellipsis
color: $pagination-ellipsis-color
pointer-events: none
.pagination-list
flex-wrap: wrap
li
list-style: none
+mobile
.pagination
flex-wrap: wrap
.pagination-previous,
.pagination-next
flex-grow: 1
flex-shrink: 1
.pagination-list
li
flex-grow: 1
flex-shrink: 1
+tablet
.pagination-list
flex-grow: 1
flex-shrink: 1
justify-content: flex-start
order: 1
.pagination-previous,
.pagination-next,
.pagination-link,
.pagination-ellipsis
margin-bottom: 0
margin-top: 0
.pagination-previous
order: 2
.pagination-next
order: 3
.pagination
justify-content: space-between
margin-bottom: 0
margin-top: 0
&.is-centered
.pagination-previous
order: 1
.pagination-list
justify-content: center
order: 2
.pagination-next
order: 3
&.is-right
.pagination-previous
order: 1
.pagination-next
order: 2
.pagination-list
justify-content: flex-end
order: 3

View file

@ -0,0 +1,121 @@
@import "../utilities/mixins"
$panel-margin: $block-spacing !default
$panel-item-border: 1px solid $border-light !default
$panel-radius: $radius-large !default
$panel-shadow: $shadow !default
$panel-heading-background-color: $border-light !default
$panel-heading-color: $text-strong !default
$panel-heading-line-height: 1.25 !default
$panel-heading-padding: 0.75em 1em !default
$panel-heading-radius: $radius !default
$panel-heading-size: 1.25em !default
$panel-heading-weight: $weight-bold !default
$panel-tabs-font-size: 0.875em !default
$panel-tab-border-bottom: 1px solid $border !default
$panel-tab-active-border-bottom-color: $link-active-border !default
$panel-tab-active-color: $link-active !default
$panel-list-item-color: $text !default
$panel-list-item-hover-color: $link !default
$panel-block-color: $text-strong !default
$panel-block-hover-background-color: $background !default
$panel-block-active-border-left-color: $link !default
$panel-block-active-color: $link-active !default
$panel-block-active-icon-color: $link !default
$panel-icon-color: $text-light !default
$panel-colors: $colors !default
.panel
border-radius: $panel-radius
box-shadow: $panel-shadow
font-size: $size-normal
&:not(:last-child)
margin-bottom: $panel-margin
// Colors
@each $name, $components in $panel-colors
$color: nth($components, 1)
$color-invert: nth($components, 2)
&.is-#{$name}
.panel-heading
background-color: $color
color: $color-invert
.panel-tabs a.is-active
border-bottom-color: $color
.panel-block.is-active .panel-icon
color: $color
.panel-tabs,
.panel-block
&:not(:last-child)
border-bottom: $panel-item-border
.panel-heading
background-color: $panel-heading-background-color
border-radius: $panel-radius $panel-radius 0 0
color: $panel-heading-color
font-size: $panel-heading-size
font-weight: $panel-heading-weight
line-height: $panel-heading-line-height
padding: $panel-heading-padding
.panel-tabs
align-items: flex-end
display: flex
font-size: $panel-tabs-font-size
justify-content: center
a
border-bottom: $panel-tab-border-bottom
margin-bottom: -1px
padding: 0.5em
// Modifiers
&.is-active
border-bottom-color: $panel-tab-active-border-bottom-color
color: $panel-tab-active-color
.panel-list
a
color: $panel-list-item-color
&:hover
color: $panel-list-item-hover-color
.panel-block
align-items: center
color: $panel-block-color
display: flex
justify-content: flex-start
padding: 0.5em 0.75em
input[type="checkbox"]
+ltr-property("margin", 0.75em)
& > .control
flex-grow: 1
flex-shrink: 1
width: 100%
&.is-wrapped
flex-wrap: wrap
&.is-active
border-left-color: $panel-block-active-border-left-color
color: $panel-block-active-color
.panel-icon
color: $panel-block-active-icon-color
&:last-child
border-bottom-left-radius: $panel-radius
border-bottom-right-radius: $panel-radius
a.panel-block,
label.panel-block
cursor: pointer
&:hover
background-color: $panel-block-hover-background-color
.panel-icon
+fa(14px, 1em)
color: $panel-icon-color
+ltr-property("margin", 0.75em)
.fa
font-size: inherit
line-height: inherit

Some files were not shown because too many files have changed in this diff Show more