Theme improvements (#822)

* start converting

* small fixes

* reorganize theme files

* cleanup search bar

* increase spacing

* small tweaks

* fix select styles in Chrome

* cleanup menus

* improve button icons

* restore badges

* remove unused classes

* restore some overrides

* restore bookmark form

* add summary outline

* avoid layout shifts

* restore bookmark details

* increase border radius for modals

* improve details modal

* restore reader mode

* restore settings

* cleanup variables

* start with dark theme

* more dark theme...

* more light theme...

* more dark theme...

* add postcss build

* remove sass processor

* update docker build

* fix alt color

* remove endless symbol

* fix tests

* update assets

* remove sass files

* fix docker build

* cleanup spacing

* improve theme

* update test scripts

* update CI workflow

* fix test
This commit is contained in:
Sascha Ißbrücker 2024-09-13 23:19:47 +02:00 committed by GitHub
parent b736464f3f
commit 3ae9cf0420
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 5366 additions and 1344 deletions

View file

@ -10,6 +10,7 @@
!/manage.py !/manage.py
!/package.json !/package.json
!/package-lock.json !/package-lock.json
!/postcss.config.js
!/requirements.dev.txt !/requirements.dev.txt
!/requirements.txt !/requirements.txt
!/rollup.config.mjs !/rollup.config.mjs

View file

@ -53,7 +53,6 @@ jobs:
- name: Run build - name: Run build
run: | run: |
npm run build npm run build
python manage.py compilescss python manage.py collectstatic
python manage.py collectstatic --ignore=*.scss
- name: Run tests - name: Run tests
run: python manage.py test bookmarks.e2e --pattern="e2e_test_*.py" run: python manage.py test bookmarks.e2e --pattern="e2e_test_*.py"

2
.gitignore vendored
View file

@ -183,7 +183,7 @@ typings/
### Custom ### Custom
# Rollup compilation output # Rollup compilation output
/bookmarks/static/bundle.js* /bookmarks/static/bundle.js*
# SASS compilation output # CSS compilation output
/bookmarks/static/theme-*.css* /bookmarks/static/theme-*.css*
# Collected static files for deployment # Collected static files for deployment
/static /static

BIN
assets/logo-inset.afdesign Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -1,136 +0,0 @@
/* Main layout */
body {
margin: 20px 10px;
@media (min-width: $size-sm) {
// Horizontal padding accounts for checkboxes that show up in bulk edit mode
margin: 20px 32px;
}
}
header {
margin-bottom: $unit-9;
.logo {
width: 28px;
height: 28px;
}
a:hover {
text-decoration: none;
}
h1 {
margin: 0 0 0 $unit-3;
font-size: $font-size-lg;
}
}
header .toasts {
margin-bottom: 20px;
.toast {
margin-bottom: 0.4rem;
}
.toast a.btn-clear:visited {
color: currentColor;
}
}
/* Shared components */
// Content area component
section.content-area {
h2 {
font-size: $font-size-lg;
}
.content-area-header {
border-bottom: solid 1px $border-color;
display: flex;
flex-wrap: wrap;
column-gap: $unit-5;
padding-bottom: $unit-1;
margin-bottom: $unit-3;
h2 {
flex: 0 0 auto;
line-height: $unit-9;
margin: 0;
}
.header-controls {
flex: 1 1 0;
display: flex;
}
}
}
// Confirm button component
span.confirmation {
display: flex;
align-items: baseline;
gap: $unit-1;
color: $error-color !important;
svg {
align-self: center;
}
.btn.btn-link {
color: $error-color !important;
&:hover {
text-decoration: underline;
}
}
}
/* Additional utilities */
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.text-gray-dark {
color: $gray-color-dark;
}
.align-baseline {
align-items: baseline;
}
.align-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.mb-4 {
margin-bottom: $unit-4;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.btn.btn-wide {
padding-left: $unit-6;
padding-right: $unit-6;
}
.btn.btn-sm.btn-icon {
display: inline-flex;
align-items: baseline;
gap: $unit-h;
svg {
align-self: center;
}
}

View file

@ -0,0 +1,150 @@
/* Common styles */
.bookmark-details {
& h2 {
flex: 1 1 0;
align-items: flex-start;
font-size: 1rem;
margin: 0;
}
& .weblinks {
display: flex;
flex-direction: column;
gap: var(--unit-2);
}
& a.weblink {
display: flex;
align-items: center;
gap: var(--unit-2);
}
& a.weblink img, & a.weblink svg {
flex: 0 0 auto;
width: 16px;
height: 16px;
color: var(--text-color);
}
& a.weblink span {
flex: 1 1 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
& .preview-image {
margin: var(--unit-4 0);
img {
max-width: 100%;
max-height: 200px;
}
}
& dl {
margin-bottom: 0;
}
& .assets {
margin-top: var(--unit-2);
& .asset {
display: flex;
align-items: center;
gap: var(--unit-2);
padding: var(--unit-2) 0;
border-top: var(--unit-o) solid var(--secondary-border-color);
}
& .asset:last-child {
border-bottom: var(--unit-o) solid var(--secondary-border-color);
}
& .asset-icon {
display: flex;
align-items: center;
justify-content: center;
}
& .asset-text {
flex: 1 1 0;
gap: var(--unit-2);
min-width: 0;
display: flex;
}
& .asset-text .truncate {
flex-shrink: 1;
}
& .asset-text .filesize {
color: var(--tertiary-text-color);
}
& .asset-actions {
display: flex;
gap: var(--unit-4);
align-items: center;
& .btn.btn-link {
height: unset;
padding: 0;
border: none;
}
}
}
& .assets-actions {
display: flex;
gap: var(--unit-4);
align-items: center;
margin-top: var(--unit-2);
& .btn.btn-link {
height: unset;
padding: 0;
border: none;
}
}
& .tags a {
color: var(--alternative-color);
}
& .status form {
display: flex;
gap: var(--unit-2);
}
& .status .form-group, .status .form-switch {
margin: 0;
}
& .actions {
display: flex;
justify-content: space-between;
align-items: center;
}
}
/* Bookmark details view specific */
.bookmark-details.page {
display: flex;
flex-direction: column;
gap: var(--unit-6);
}
/* Bookmark details modal specific */
.bookmark-details.modal {
& .modal-header {
display: flex;
align-items: flex-start;
gap: var(--unit-2);
}
& .modal-body {
padding-top: 0;
padding-bottom: 0;
}
}

View file

@ -1,141 +0,0 @@
/* Common styles */
.bookmark-details {
h2 {
flex: 1 1 0;
align-items: flex-start;
font-size: 1rem;
margin: 0;
}
.weblinks {
display: flex;
flex-direction: column;
gap: $unit-2;
}
a.weblink {
display: flex;
align-items: center;
gap: $unit-2;
}
a.weblink img, a.weblink svg {
flex: 0 0 auto;
width: 16px;
height: 16px;
color: $body-font-color;
}
a.weblink span {
flex: 1 1 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.preview-image {
margin: $unit-4 0;
img {
max-width: 100%;
max-height: 200px;
}
}
dl {
margin-bottom: 0;
}
.assets {
margin-top: $unit-2;
}
.assets .asset {
display: flex;
align-items: center;
gap: $unit-3;
padding: $unit-2 0;
border-top: $unit-o solid $border-color-light;
}
.assets .asset:last-child {
border-bottom: $unit-o solid $border-color-light;
}
.assets .asset-icon {
display: flex;
align-items: center;
justify-content: center;
}
.assets .asset-text {
flex: 1 1 0;
gap: $unit-2;
min-width: 0;
display: flex;
}
.assets .asset-text .truncate {
flex-shrink: 1;
}
.assets .asset-text .filesize {
color: $gray-color;
}
.assets .asset-actions, .assets-actions {
display: flex;
gap: $unit-4;
align-items: center;
}
.assets .asset-actions .btn, .assets-actions .btn {
height: unset;
padding: 0;
border: none;
}
.assets-actions {
margin-top: $unit-2;
}
.tags a {
color: $alternative-color;
}
.status form {
display: flex;
gap: $unit-2;
}
.status .form-group, .status .form-switch {
margin: 0;
}
.actions {
display: flex;
justify-content: space-between;
align-items: center;
}
}
/* Bookmark details view specific */
.bookmark-details.page {
display: flex;
flex-direction: column;
gap: $unit-6;
}
/* Bookmark details modal specific */
.bookmark-details.modal {
.modal-header {
display: flex;
align-items: flex-start;
gap: $unit-2;
}
.modal-body {
padding-top: 0;
padding-bottom: 0;
}
}

View file

@ -0,0 +1,48 @@
.bookmarks-form-page {
section {
max-width: 550px;
margin: 0 auto;
}
}
.bookmarks-form {
& .btn.btn-link.form-icon {
padding: 0;
width: 20px;
height: 20px;
visibility: hidden;
--btn-icon-color: var(--tertiary-text-color);
& > svg {
width: 20px;
height: 20px;
}
}
& .has-icon-right > input, & .has-icon-right > textarea {
padding-right: 30px;
}
& .has-icon-right > input:placeholder-shown ~ .btn.form-icon,
& .has-icon-right > textarea:placeholder-shown ~ .btn.form-icon {
visibility: visible;
}
& .form-icon.loading {
visibility: hidden;
}
& .form-input-hint.bookmark-exists {
display: none;
color: var(--warning-color);
}
& .form-input-hint.auto-tags {
display: none;
color: var(--success-color);
}
& details.notes textarea {
box-sizing: border-box;
}
}

View file

@ -1,49 +0,0 @@
.bookmarks-form {
.btn.form-icon {
padding: 0;
width: 20px;
height: 20px;
visibility: hidden;
color: $gray-color;
&:focus,
&:hover,
&:active,
&.active {
color: $gray-color-dark;
}
> svg {
width: 20px;
height: 20px;
}
}
.has-icon-right > input, .has-icon-right > textarea {
padding-right: 30px;
}
.has-icon-right > input:placeholder-shown ~ .btn.form-icon,
.has-icon-right > textarea:placeholder-shown ~ .btn.form-icon {
visibility: visible;
}
.form-icon.loading {
visibility: hidden;
}
.form-input-hint.bookmark-exists {
display: none;
color: $warning-color;
}
.form-input-hint.auto-tags {
display: none;
color: $success-color;
}
details.notes textarea {
box-sizing: border-box;
}
}

View file

@ -0,0 +1,457 @@
:root {
--bookmark-title-color: var(--primary-text-color);
--bookmark-title-weight: 500;
--bookmark-description-color: var(--text-color);
--bookmark-description-weight: 400;
--bookmark-actions-color: var(--secondary-text-color);
--bookmark-actions-hover-color: var(--text-color);
--bookmark-actions-weight: 400;
--bulk-actions-bg-color: var(--gray-50);
}
/* Bookmark page grid */
.bookmarks-page.grid {
grid-gap: var(--unit-9);
}
/* Bookmark area header controls */
.bookmarks-page .search-container {
flex: 1 1 0;
display: flex;
max-width: 300px;
margin-left: auto;
& form {
width: 100%;
}
@media (max-width: 600px) {
max-width: initial;
margin-left: 0;
}
/* Regular input */
& input[type='search'] {
height: var(--control-size);
-webkit-appearance: none;
}
/* Enhanced auto-complete input */
/* This needs a bit more wrangling to make the CSS component align with the attached button */
& .form-autocomplete {
height: var(--control-size);
& .form-autocomplete-input {
width: 100%;
height: var(--control-size);
& input[type='search'] {
width: 100%;
height: 100%;
margin: 0;
border: none;
}
}
}
/* Group search options button with search button */
height: var(--control-size);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow-xs);
& input, & .form-autocomplete-input {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
box-shadow: none;
}
& .dropdown-toggle {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
box-shadow: none;
outline-offset: calc(var(--focus-outline-offset) * -1);
}
/* Search option menu styles */
& .dropdown {
& .menu {
padding: var(--unit-4);
min-width: 250px;
font-size: var(--font-size-sm);
}
& .menu .actions {
margin-top: var(--unit-4);
display: flex;
justify-content: space-between;
}
& .form-group:first-of-type {
margin-top: 0;
}
& .form-group {
margin-bottom: var(--unit-3);
}
& .radio-group {
& .form-label {
margin-bottom: var(--unit-1);
}
& .form-radio.form-inline {
margin: 0 var(--unit-2) 0 0;
padding: 0;
display: inline-flex;
align-items: center;
column-gap: var(--unit-1);
}
& .form-icon {
top: 0;
position: relative;
}
}
}
}
/* Bookmark list */
ul.bookmark-list {
list-style: none;
margin: 0;
padding: 0;
/* Increase line-height for better separation within / between items */
line-height: 1.1rem;
}
@keyframes appear {
0% {
opacity: 0;
}
90% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/* Bookmarks */
li[ld-bookmark-item] {
position: relative;
display: flex;
gap: var(--unit-2);
margin-top: 0;
margin-bottom: var(--unit-3);
& .content {
flex: 1 1 0;
min-width: 0;
}
& img.preview-image {
flex: 0 0 auto;
width: 100px;
height: 60px;
margin-top: var(--unit-h);
object-fit: cover;
border-radius: var(--border-radius);
border: solid 1px var(--border-color);
}
& .form-checkbox.bulk-edit-checkbox {
display: none;
}
& .title {
position: relative;
}
& .title img {
position: absolute;
width: 16px;
height: 16px;
left: 0;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
& .title img + a {
padding-left: 22px;
}
& .title a {
color: var(--bookmark-title-color);
font-weight: var(--bookmark-title-weight);
display: block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
& .title a[data-tooltip]:hover::after, & .title a[data-tooltip]:focus::after {
content: attr(data-tooltip);
position: absolute;
z-index: 10;
top: 100%;
left: 50%;
transform: translateX(-50%);
width: max-content;
max-width: 90%;
height: fit-content;
background-color: #292f62;
color: #fff;
padding: var(--unit-1);
border-radius: var(--border-radius);
border: 1px solid #424a8c;
font-size: var(--font-size-sm);
font-style: normal;
white-space: normal;
pointer-events: none;
animation: 0.3s ease 0s appear;
}
@media (pointer: coarse) {
& .title a[data-tooltip]::after {
display: none;
}
}
&.unread .title a {
font-style: italic;
}
& .url-path, & .url-display {
font-size: var(--font-size-sm);
color: var(--secondary-link-color);
}
& .description {
color: var(--bookmark-description-color);
font-weight: var(--bookmark-description-weight);
}
& .description.separate {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--ld-bookmark-description-max-lines, 1);
overflow: hidden;
}
& .tags {
& a, & a:visited:hover {
color: var(--alternative-color);
}
}
& .actions, & .extra-actions {
display: flex;
align-items: baseline;
flex-wrap: wrap;
column-gap: var(--unit-2);
}
@media (max-width: 600px) {
& .extra-actions {
width: 100%;
margin-top: var(--unit-1);
}
}
& .actions {
color: var(--bookmark-actions-color);
font-size: var(--font-size-sm);
& a, & button.btn-link {
color: var(--bookmark-actions-color);
--btn-icon-color: var(--bookmark-actions-color);
font-weight: var(--bookmark-actions-weight);
padding: 0;
height: auto;
vertical-align: unset;
border: none;
box-sizing: border-box;
transition: none;
text-decoration: none;
&:focus,
&:hover,
&:active,
&.active {
color: var(--bookmark-actions-hover-color);
--btn-icon-color: var(--bookmark-actions-hover-color);
}
}
}
}
.bookmark-pagination {
margin-top: var(--unit-4);
/* Remove left padding from first pagination link */
& .page-item:first-child a {
padding-left: 0;
}
}
.tag-cloud {
/* Increase line-height for better separation within / between items */
line-height: 1.1rem;
& .selected-tags {
margin-bottom: var(--unit-4);
& a,
& a:visited:hover {
color: var(--error-color);
}
}
& .unselected-tags {
& a,
& a:visited:hover {
color: var(--alternative-color);
}
}
& .group {
margin-bottom: var(--unit-3);
}
& .highlight-char {
font-weight: bold;
text-transform: uppercase;
color: var(--alternative-color-dark);
}
}
/* Bookmark notes */
ul.bookmark-list {
& .notes {
display: none;
max-height: 300px;
margin: var(--unit-1) 0;
overflow-y: auto;
background: var(--body-color-contrast);
border-radius: var(--border-radius);
}
& .notes .markdown {
padding: var(--unit-2) var(--unit-3);
}
&.show-notes .notes,
& li.show-notes .notes {
display: block;
}
}
/* Bookmark bulk edit */
:root {
--bulk-edit-toggle-width: 16px;
--bulk-edit-toggle-offset: 8px;
--bulk-edit-bar-offset: calc(var(--bulk-edit-toggle-width) + (2 * var(--bulk-edit-toggle-offset)));
--bulk-edit-transition-duration: 400ms;
}
[ld-bulk-edit] {
& .bulk-edit-bar {
margin-top: -1px;
margin-left: calc(-1 * var(--bulk-edit-bar-offset));
margin-bottom: var(--unit-4);
max-height: 0;
overflow: hidden;
transition: max-height var(--bulk-edit-transition-duration);
background: var(--bulk-actions-bg-color);
}
&.active .bulk-edit-bar {
max-height: 37px;
border-bottom: solid 1px var(--secondary-border-color);
}
/* Hide section border when bulk edit bar is opened, otherwise borders overlap in dark mode due to using contrast colors */
&.active section:first-of-type .content-area-header {
border-bottom-color: transparent;
}
/* remove overflow after opening animation, otherwise tag autocomplete overlay gets cut off */
&.active:not(.activating) .bulk-edit-bar {
overflow: visible;
}
/* All checkbox */
& .form-checkbox.bulk-edit-checkbox.all {
display: block;
width: var(--bulk-edit-toggle-width);
margin: 0 0 0 var(--bulk-edit-toggle-offset);
padding: 0;
}
/* Bookmark checkboxes */
& li[ld-bookmark-item] .form-checkbox.bulk-edit-checkbox {
display: block;
position: absolute;
width: var(--bulk-edit-toggle-width);
min-height: var(--bulk-edit-toggle-width);
left: calc(-1 * var(--bulk-edit-toggle-width) - var(--bulk-edit-toggle-offset));
top: 50%;
transform: translateY(-50%);
padding: 0;
margin: 0;
visibility: hidden;
opacity: 0;
transition: all var(--bulk-edit-transition-duration);
.form-icon {
top: 0;
}
}
&.active li[ld-bookmark-item] .form-checkbox.bulk-edit-checkbox {
visibility: visible;
opacity: 1;
}
/* Actions */
& .bulk-edit-actions {
display: flex;
align-items: center;
padding: var(--unit-1) 0;
border-top: solid 1px var(--secondary-border-color);
gap: var(--unit-2);
& button {
--control-padding-x-sm: 0;
}
& button:hover {
text-decoration: underline;
}
& > input,
& .form-autocomplete,
& select {
width: auto;
max-width: 140px;
-webkit-appearance: none;
}
& .select-across {
margin: 0 0 0 auto;
font-size: var(--font-size-sm);
}
}
}

View file

@ -1,408 +0,0 @@
.bookmarks-page.grid {
grid-gap: $unit-9;
}
/* Bookmark area header controls */
.bookmarks-page .content-area-header {
--searchbox-max-width: 350px;
@media (max-width: $size-sm) {
--searchbox-max-width: initial;
flex-direction: column;
}
}
.bookmarks-page .search-container {
flex: 1 1 0;
display: flex;
justify-content: flex-end;
// Regular input
input[type='search'] {
height: $control-size;
-webkit-appearance: none;
}
// Enhanced auto-complete input
// This needs a bit more wrangling to make the CSS component align with the attached button
.form-autocomplete {
height: $control-size;
.form-autocomplete-input {
width: 100%;
height: $control-size;
input[type='search'] {
width: 100%;
height: 100%;
margin: 0;
border: none;
}
}
}
.input-group {
flex: 1 1 0;
min-width: var(--searchbox-min-width);
max-width: var(--searchbox-max-width);
}
.input-group > :first-child {
flex: 1 1 0;
}
// Group search options button with search button
.input-group input[type='submit'] {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.dropdown-toggle {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.dropdown {
margin-left: -1px;
}
// Search option menu styles
.dropdown {
.menu {
padding: $unit-4;
min-width: 250px;
font-size: $font-size-sm;
}
.menu .actions {
margin-top: $unit-4;
display: flex;
justify-content: space-between;
}
.radio-group {
margin-bottom: $unit-1;
.form-label {
padding-bottom: 0;
}
.form-radio.form-inline {
margin: 0 $unit-2 0 0;
padding: 0;
display: inline-flex;
align-items: center;
column-gap: $unit-1;
}
.form-icon {
top: 0;
position: relative;
}
}
}
}
/* Bookmark list */
ul.bookmark-list {
list-style: none;
margin: 0;
padding: 0;
/* Increase line-height for better separation within / between items */
line-height: 1.1rem;
}
@keyframes appear {
0% {
opacity: 0;
}
90% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/* Bookmarks */
li[ld-bookmark-item] {
position: relative;
display: flex;
gap: $unit-2;
margin-top: $unit-2;
.content {
flex: 1 1 0;
min-width: 0;
}
img.preview-image {
flex: 0 0 auto;
width: 100px;
height: 60px;
margin-top: $unit-h;
object-fit: cover;
border-radius: $border-radius;
border: solid 1px $border-color-dark;
}
.form-checkbox.bulk-edit-checkbox {
display: none;
}
.title {
position: relative;
}
.title img {
position: absolute;
width: 16px;
height: 16px;
left: 0;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
.title img + a {
padding-left: 22px;
}
.title a {
display: block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.title a[data-tooltip]:hover::after, .title a[data-tooltip]:focus::after {
content: attr(data-tooltip);
position: absolute;
z-index: 10;
top: 100%;
left: 50%;
transform: translateX(-50%);
width: max-content;
max-width: 90%;
height: fit-content;
background-color: #292f62;
color: #fff;
padding: $unit-1;
border-radius: $border-radius;
border: 1px solid #424a8c;
font-size: $font-size-sm;
font-style: normal;
white-space: normal;
pointer-events: none;
animation: 0.3s ease 0s appear;
}
@media (pointer:coarse) {
.title a[data-tooltip]::after {
display: none;
}
}
&.unread .title a {
font-style: italic;
}
.url-path, .url-display {
font-size: $font-size-sm;
color: $secondary-link-color;
}
.description {
color: $gray-color-dark;
}
.description.separate {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--ld-bookmark-description-max-lines, 1);
overflow: hidden;
}
.tags {
a, a:visited:hover {
color: $alternative-color;
}
}
.actions, .extra-actions {
display: flex;
align-items: baseline;
flex-wrap: wrap;
column-gap: $unit-2;
}
@media (max-width: $size-sm) {
.extra-actions {
width: 100%;
margin-top: $unit-1;
}
}
.actions {
font-size: $font-size-sm;
a, button.btn-link {
color: $gray-color;
padding: 0;
height: auto;
vertical-align: unset;
border: none;
transition: none;
text-decoration: none;
&:focus,
&:hover,
&:active,
&.active {
color: $gray-color-dark;
}
}
}
}
.bookmark-pagination {
margin-top: $unit-4;
}
.tag-cloud {
/* Increase line-height for better separation within / between items */
line-height: 1.1rem;
.selected-tags {
margin-bottom: $unit-4;
a, a:visited:hover {
color: $error-color;
}
}
.unselected-tags {
a, a:visited:hover {
color: $alternative-color;
}
}
.group {
margin-bottom: $unit-2;
}
.highlight-char {
font-weight: bold;
text-transform: uppercase;
color: $alternative-color-dark;
}
}
/* Bookmark notes */
ul.bookmark-list {
.notes {
display: none;
max-height: 300px;
margin: $unit-1 0;
overflow-y: auto;
}
.notes .markdown {
padding: $unit-2 $unit-3;
}
&.show-notes .notes,
li.show-notes .notes {
display: block;
}
}
/* Bookmark bulk edit */
$bulk-edit-toggle-width: 16px;
$bulk-edit-toggle-offset: 8px;
$bulk-edit-bar-offset: $bulk-edit-toggle-width + (2 * $bulk-edit-toggle-offset);
$bulk-edit-transition-duration: 400ms;
[ld-bulk-edit] {
.bulk-edit-bar {
margin-top: -1px;
margin-left: -$bulk-edit-bar-offset;
margin-bottom: $unit-3;
max-height: 0;
overflow: hidden;
transition: max-height $bulk-edit-transition-duration;
}
&.active .bulk-edit-bar {
max-height: 37px;
border-bottom: solid 1px $border-color;
}
/* remove overflow after opening animation, otherwise tag autocomplete overlay gets cut off */
&.active:not(.activating) .bulk-edit-bar {
overflow: visible;
}
/* All checkbox */
.form-checkbox.bulk-edit-checkbox.all {
display: block;
width: $bulk-edit-toggle-width;
margin: 0 0 0 $bulk-edit-toggle-offset;
padding: 0;
}
/* Bookmark checkboxes */
li[ld-bookmark-item] .form-checkbox.bulk-edit-checkbox {
display: block;
position: absolute;
width: $bulk-edit-toggle-width;
min-height: $bulk-edit-toggle-width;
left: -$bulk-edit-toggle-width - $bulk-edit-toggle-offset;
top: 50%;
transform: translateY(-50%);
padding: 0;
margin: 0;
visibility: hidden;
opacity: 0;
transition: all $bulk-edit-transition-duration;
.form-icon {
top: 0;
}
}
&.active li[ld-bookmark-item] .form-checkbox.bulk-edit-checkbox {
visibility: visible;
opacity: 1;
}
/* Actions */
.bulk-edit-actions {
display: flex;
align-items: center;
padding: $unit-1 0;
border-top: solid 1px $border-color;
gap: $unit-2;
button {
padding: 0 !important;
}
button:hover {
text-decoration: underline;
}
> input, .form-autocomplete, select {
width: auto;
max-width: 140px;
-webkit-appearance: none;
}
.select-across {
margin: 0 0 0 auto;
font-size: $font-size-sm;
}
}
}

View file

@ -0,0 +1,60 @@
/* Shared components */
/* Content area component */
section.content-area {
h2 {
font-size: var(--font-size-lg);
}
.content-area-header {
border-bottom: solid 1px var(--secondary-border-color);
display: flex;
flex-wrap: wrap;
column-gap: var(--unit-5);
padding-bottom: var(--unit-2);
margin-bottom: var(--unit-4);
h2 {
flex: 0 0 auto;
line-height: var(--unit-9);
margin: 0;
}
.header-controls {
flex: 1 1 0;
display: flex;
}
}
}
@media (max-width: 600px) {
section.content-area .content-area-header {
flex-direction: column;
}
}
/* Confirm button component */
span.confirmation {
display: flex;
align-items: baseline;
gap: var(--unit-1);
color: var(--error-color) !important;
svg {
align-self: center;
}
.btn.btn-link {
color: var(--error-color) !important;
&:hover {
text-decoration: underline;
}
}
}
/* Divider */
.divider {
border-bottom: solid 1px var(--secondary-border-color);
margin: var(--unit-5) 0;
}

View file

@ -0,0 +1,39 @@
/* Main layout */
body {
margin: 20px 10px;
@media (min-width: 600px) {
/* Horizontal offset accounts for checkboxes that show up in bulk edit mode */
margin: 20px 32px;
}
}
header {
margin-bottom: var(--unit-9);
.logo {
width: 28px;
height: 28px;
}
a:hover {
text-decoration: none;
}
h1 {
margin: 0 0 0 var(--unit-3);
font-size: var(--font-size-lg);
}
}
header .toasts {
margin-bottom: 20px;
.toast {
margin-bottom: 0.4rem;
}
.toast a.btn-clear:visited {
color: currentColor;
}
}

View file

@ -0,0 +1,40 @@
.markdown {
& p, & ul, & ol, & pre, & blockquote {
margin: 0 0 var(--unit-2) 0;
}
& > *:first-child {
margin-top: 0;
}
& > *:last-child {
margin-bottom: 0;
}
& ul, & ol {
margin-left: var(--unit-4);
}
& ul li, & ol li {
margin-top: var(--unit-1);
}
& pre {
padding: var(--unit-1) var(--unit-2);
background-color: var(--code-bg-color);
border-radius: var(--unit-1);
overflow-x: auto;
}
& pre code {
background: none;
box-shadow: none;
padding: 0;
}
& > pre:first-child:last-child {
padding: 0;
background: none;
border-radius: 0;
}
}

View file

@ -1,40 +0,0 @@
.markdown {
p, ul, ol, pre, blockquote {
margin: 0 0 $unit-2 0;
}
> *:first-child {
margin-top: 0;
}
> *:last-child {
margin-bottom: 0;
}
ul, ol {
margin-left: $unit-4;
}
ul li, ol li {
margin-top: $unit-1;
}
pre {
padding: $unit-1 $unit-2;
background-color: $code-bg-color;
border-radius: $unit-1;
overflow-x: auto;
}
pre code {
background: none;
box-shadow: none;
padding: 0;
}
> pre:first-child:last-child {
padding: 0;
background: none;
border-radius: 0;
}
}

View file

@ -1,10 +1,3 @@
.container {
margin-left: auto;
margin-right: auto;
width: 100%;
max-width: $size-lg;
}
.show-sm, .show-sm,
.show-md { .show-md {
display: none !important; display: none !important;
@ -26,11 +19,18 @@
width: 100%; width: 100%;
} }
.container {
margin-left: auto;
margin-right: auto;
width: 100%;
max-width: var(--size-lg);
}
.grid { .grid {
--grid-columns: 3; --grid-columns: 3;
display: grid; display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr); grid-template-columns: repeat(var(--grid-columns), 1fr);
grid-gap: $unit-4; grid-gap: var(--unit-4);
} }
.grid > * { .grid > * {
@ -46,18 +46,18 @@
} }
.col-1 { .col-1 {
grid-column: unquote("span min(1, var(--grid-columns))"); grid-column: span min(1, var(--grid-columns));
} }
.col-2 { .col-2 {
grid-column: unquote("span min(2, var(--grid-columns))"); grid-column: span min(2, var(--grid-columns));
} }
.col-3 { .col-3 {
grid-column: unquote("span min(3, var(--grid-columns))"); grid-column: span min(3, var(--grid-columns));
} }
@media (max-width: $size-md) { @media (max-width: 840px) {
.hide-md { .hide-md {
display: none !important; display: none !important;
} }
@ -86,7 +86,7 @@
} }
} }
@media (max-width: $size-sm) { @media (max-width: 600px) {
.hide-sm { .hide-sm {
display: none !important; display: none !important;
} }

View file

@ -1,9 +1,9 @@
.settings-page { .settings-page {
section.content-area { section.content-area {
margin-bottom: $unit-10; margin-bottom: var(--unit-10);
h2 { h2 {
margin-bottom: $unit-3; margin-bottom: var(--unit-3);
} }
} }
@ -17,6 +17,10 @@
} }
section.about table { section.about table {
max-width: 500px; max-width: 400px;
}
& .form-group {
margin-bottom: var(--unit-4);
} }
} }

View file

@ -1,204 +0,0 @@
// Customized Spectre CSS imports, removing modules that are not used
// See node_modules/spectre.css/src/spectre.scss for the original version
// Variables and mixins
@import "../../node_modules/spectre.css/src/variables";
// Customize variables to reduce font and control sizes
// Can use CSS variables for font sizes, as they are not used in SCSS calculations
$font-size: var(--font-size);
$font-size-sm: var(--font-size-sm);
$font-size-lg: var(--font-size-lg);
// Can't use CSS variables for these, used in SCSS calculations
$line-height: 1rem;
$control-size: $unit-8;
$control-size-sm: $unit-6;
$control-size-lg: $unit-9;
// Declare defaults for CSS variables, expose SCSS variables as CSS variables
html {
--font-size: 0.7rem;
--font-size-sm: 0.65rem;
--font-size-lg: 0.8rem;
--control-size: #{$control-size};
--control-size-sm: #{$control-size-sm};
--control-size-lg: #{$control-size-lg};
}
// Mixins
@import "../../node_modules/spectre.css/src/mixins";
/*! Spectre.css v#{$version} | MIT License | github.com/picturepan2/spectre */
// Reset and dependencies
@import "../../node_modules/spectre.css/src/normalize";
@import "../../node_modules/spectre.css/src/base";
// Elements
@import "../../node_modules/spectre.css/src/typography";
@import "../../node_modules/spectre.css/src/asian";
@import "../../node_modules/spectre.css/src/tables";
@import "../../node_modules/spectre.css/src/buttons";
@import "../../node_modules/spectre.css/src/forms";
@import "../../node_modules/spectre.css/src/labels";
@import "../../node_modules/spectre.css/src/codes";
@import "../../node_modules/spectre.css/src/media";
// Components
@import "../../node_modules/spectre.css/src/badges";
@import "../../node_modules/spectre.css/src/dropdowns";
@import "../../node_modules/spectre.css/src/empty";
@import "../../node_modules/spectre.css/src/menus";
@import "../../node_modules/spectre.css/src/modals";
@import "../../node_modules/spectre.css/src/pagination";
@import "../../node_modules/spectre.css/src/tabs";
@import "../../node_modules/spectre.css/src/toasts";
@import "../../node_modules/spectre.css/src/tooltips";
// Utility classes
@import "../../node_modules/spectre.css/src/animations";
@import "../../node_modules/spectre.css/src/utilities";
// Auto-complete component
@import "../../node_modules/spectre.css/src/autocomplete";
/* Spectre overrides / fixes */
// Fix up visited styles
a:visited {
color: $link-color;
}
a:visited:hover {
color: $link-color-dark;
}
.btn-link:visited:not(.btn-primary) {
color: $link-color;
}
.btn-link:visited:not(.btn-primary):hover {
color: $link-color-dark;
}
// Disable transitions on buttons, which can otherwise flicker while loading CSS file
// something to do with .btn applying a transition for background, and then .btn-link setting a different background
.btn {
transition: none !important;
}
// Make code work with light and dark theme
code {
color: $gray-color-dark;
background-color: $code-bg-color;
box-shadow: 1px 1px 0 $code-shadow-color;
}
// Remove left padding from first pagination link
.pagination .page-item:first-child a {
padding-left: 0;
}
// Override border color for tab block
.tab-block {
border-bottom: solid 1px $border-color;
}
// Fix padding for first menu item
ul.menu li:first-child {
margin-top: 0;
}
// Form auto-complete menu
.form-autocomplete .menu {
.menu-item.selected > a, .menu-item > a:hover {
background: $secondary-color;
color: $primary-color;
}
.group-item, .group-item:hover {
color: $gray-color;
text-transform: uppercase;
background: none;
font-size: 0.6rem;
font-weight: bold;
}
}
.modal {
// Add border to separate from background in dark mode
.modal-container {
border: solid 1px $border-color;
}
// Fix modal header to use default color
.modal-header {
color: inherit;
}
}
// Customize modal animation
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.modal.active .modal-container, .modal.active .modal-overlay {
animation: fade-in .15s ease 1;
}
.modal.active.closing .modal-container, .modal.active.closing .modal-overlay {
animation: fade-out .15s ease 1;
}
// Customize menu animation
.dropdown .menu {
animation: fade-in .15s ease 1;
}
// Modal close button
.modal .modal-header button.close {
background: none;
border: none;
padding: 0;
line-height: 0;
cursor: pointer;
opacity: .85;
color: $gray-color-dark;
&:hover {
opacity: 1;
}
}
// Increase input font size on small viewports to prevent zooming on focus the input
// on mobile devices. 430px relates to the "normalized" iPhone 14 Pro Max
// viewport size
@media screen and (max-width: 430px) {
.form-input {
font-size: 16px;
}
}
// Hide tooltips on mobile
@media (pointer:coarse) {
.tooltip::after {
display: none;
}
}

View file

@ -0,0 +1,143 @@
@import "theme-light.css";
:root {
/* Color palette */
--contrast-5: hsla(241, 65%, 85%, 0.06);
--contrast-10: hsla(241, 60%, 80%, 0.14);
--contrast-20: hsla(241, 64%, 82%, 0.23);
--contrast-30: hsla(241, 69%, 84%, 0.32);
--contrast-40: hsla(241, 73%, 86%, 0.41);
--contrast-50: hsla(241, 78%, 88%, 0.5);
--contrast-60: hsla(241, 82%, 90%, 0.58);
--contrast-70: hsla(241, 87%, 92%, 0.69);
--contrast-80: hsla(241, 91%, 94%, 0.8);
--contrast-90: hsla(241, 96%, 96%, 0.9);
--primary-color: hsl(241, 75%, 64%);
--primary-color-highlight: hsl(241, 75%, 68%);
--primary-color-shade: hsl(241, 75%, 64%, 0.42);
--alternative-color: hsl(179, 50%, 58%);
--alternative-color-dark: hsl(179, 80%, 75%);
--success-color: hsl(142, 76%, 36%);
--success-color-highlight: hsl(142, 76%, 40%);
--success-color-shade: hsla(142, 76%, 36%, 0.1);
--warning-color: hsl(38, 92%, 50%);
--warning-color-highlight: hsl(38, 92%, 55%);
--warning-color-shade: hsla(38, 92%, 50%, 0.1);
--error-color: hsl(0, 80%, 60%);
--error-color-highlight: hsl(0, 72%, 60%);
--error-color-shade: hsla(0, 72%, 51%, 0.1);
/* Core colors */
--text-color: var(--gray-300);
--secondary-text-color: var(--gray-400);
--tertiary-text-color: var(--gray-500);
--contrast-text-color: #fff;
--primary-text-color: hsl(241, 82%, 82%);
--link-color: var(--primary-text-color);
--secondary-link-color: hsla(241, 82%, 82%, 0.8);
--icon-color: var(--text-color);
--border-color: var(--contrast-30);
--secondary-border-color: var(--contrast-20);
--body-color: hsl(241, 15%, 14%);
--body-color-contrast: var(--contrast-10);
/* Focus */
--focus-outline: 2px solid hsl(241, 100%, 78%);
--focus-outline-offset: 2px;
/* Shadows */
--box-shadow-xs: none;
--box-shadow: none;
--box-shadow-lg: none;
}
:root {
--input-bg-color: var(--contrast-5);
--input-disabled-bg-color: var(--contrast-30);
--input-text-color: var(--text-color);
--input-hint-color: var(--secondary-text-color);
--input-border-color: var(--border-color);
--input-placeholder-color: var(--tertiary-text-color);
--input-box-shadow: var(--box-shadow-xs);
--checkbox-bg-color: var(--contrast-10);
--checkbox-checked-bg-color: var(--primary-color);
--checkbox-disabled-bg-color: var(--contrast-30);
--checkbox-border-color: var(--border-color);
--checkbox-icon-color: #fff;
--switch-bg-color: var(--contrast-10);
--switch-border-color: var(--border-color);
--switch-toggle-color: var(--text-color);
}
:root {
--btn-bg-color: var(--contrast-5);
--btn-hover-bg-color: var(--contrast-20);
--btn-border-color: var(--border-color);
--btn-text-color: var(--text-color);
--btn-icon-color: var(--icon-color);
--btn-font-weight: 400;
--btn-box-shadow: var(--box-shadow-xs);
--btn-primary-bg-color: var(--primary-color);
--btn-primary-hover-bg-color: var(--primary-color-highlight);
--btn-primary-text-color: var(--contrast-text-color);
--btn-success-bg-color: var(--success-color);
--btn-success-hover-bg-color: var(--success-color-highlight);
--btn-success-text-color: var(--contrast-text-color);
--btn-error-bg-color: var(--error-color);
--btn-error-hover-bg-color: var(--error-color-highlight);
--btn-error-text-color: var(--contrast-text-color);
--btn-link-text-color: var(--link-color);
--btn-link-hover-text-color: var(--link-color);
}
:root {
--modal-overlay-bg-color: hsla(229, 21%, 16%, 0.55);
--modal-container-bg-color: hsl(241, 20%, 20%);
--modal-container-border-color: var(--contrast-30);
--modal-border-radius: var(--border-radius-lg);
--modal-box-shadow: none;
}
:root {
--menu-bg-color: hsl(241, 20%, 20%);
--menu-border-color: var(--contrast-30);
--menu-border-radius: var(--border-radius);
--menu-box-shadow: none;
--menu-item-color: var(--text-color);
--menu-item-hover-color: var(--text-color);
--menu-item-bg-color: transparent;
--menu-item-hover-bg-color: var(--contrast-20);
}
:root {
--tab-color: var(--text-color);
--tab-hover-color: var(--primary-text-color);
--tab-active-color: var(--primary-text-color);
--tab-highlight-color: var(--primary-text-color);
}
:root {
--bookmark-title-color: var(--primary-text-color);
--bookmark-title-weight: 500;
--bookmark-description-color: var(--text-color);
--bookmark-description-weight: 400;
--bookmark-actions-color: var(--secondary-text-color);
--bookmark-actions-hover-color: var(--text-color);
--bookmark-actions-weight: 400;
--bulk-actions-bg-color: var(--contrast-5);
}

View file

@ -1,66 +0,0 @@
// Import custom variables
@import "variables-dark";
// Import Spectre CSS lib
@import "spectre";
// Import style modules
@import "base";
@import "responsive";
@import "bookmark-details";
@import "bookmark-page";
@import "bookmark-form";
@import "settings";
@import "markdown";
@import "reader-mode";
/* Dark theme overrides */
// Buttons
.btn.btn-primary {
background: $dt-primary-button-color;
border-color: darken($dt-primary-button-color, 5%);
&:hover, &:active, &:focus {
background: darken($dt-primary-button-color, 5%);
border-color: darken($dt-primary-button-color, 10%);
}
}
// Focus ring
a:focus, .btn:focus {
box-shadow: 0 0 0 .1rem rgba($primary-color, .5);
}
// Forms
.form-input:not(:placeholder-shown):invalid,
.form-input:not(:placeholder-shown):invalid:focus,
.has-error .form-input,
.form-input.is-error,
.has-error .form-select,
.form-select.is-error {
background: darken($error-color, 40%);
}
.form-checkbox input:checked + .form-icon, .form-radio input:checked + .form-icon, .form-switch input:checked + .form-icon {
background: $dt-primary-input-color;
border-color: $dt-primary-input-color;
}
.form-switch .form-icon::before, .form-switch input:active + .form-icon::before {
background: $light-color;
}
.form-switch input:checked + .form-icon {
background: $dt-primary-input-color;
border-color: $dt-primary-input-color;
}
.form-radio input:checked + .form-icon::before {
background: $light-color;
}
// Pagination
.pagination .page-item.active a {
background: $dt-primary-button-color;
}

View file

@ -0,0 +1,30 @@
@import "theme/variables.css";
@import "theme/_normalize.css";
@import "theme/base.css";
@import "theme/typography.css";
@import "theme/asian.css";
@import "theme/tables.css";
@import "theme/buttons.css";
@import "theme/forms.css";
@import "theme/code.css";
@import "theme/dropdowns.css";
@import "theme/menus.css";
@import "theme/badges.css";
@import "theme/empty.css";
@import "theme/modals.css";
@import "theme/pagination.css";
@import "theme/tabs.css";
@import "theme/toasts.css";
@import "theme/autocomplete.css";
@import "theme/animations.css";
@import "theme/utilities.css";
@import "responsive.css";
@import "layout.css";
@import "components.css";
@import "bookmark-details.css";
@import "bookmark-form.css";
@import "bookmark-page.css";
@import "markdown.css";
@import "reader-mode.css";
@import "settings.css";

View file

@ -1,15 +0,0 @@
// Import custom variables
@import "variables-light";
// Import Spectre CSS lib
@import "spectre";
// Import style modules
@import "base";
@import "responsive";
@import "bookmark-details";
@import "bookmark-page";
@import "bookmark-form";
@import "settings";
@import "markdown";
@import "reader-mode";

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 - 2020 Yan Zhu
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,446 @@
/* Manually forked from Normalize.css */
/* normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
/**
* 1. Change the default font family in all browsers (opinionated).
* 2. Correct the line height in all browsers.
* 3. Prevent adjustments of font size after orientation changes in
* IE on Windows Phone and in iOS.
*/
/* Document
========================================================================== */
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 3 */
-webkit-text-size-adjust: 100%; /* 3 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
}
/**
* Add the correct display in IE 9-.
*/
article,
aside,
footer,
header,
nav,
section {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
*/
figcaption,
figure,
main { /* 1 */
display: block;
}
/**
* Add the correct margin in IE 8 (removed).
*/
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers. (removed)
* 2. Correct the odd `em` font sizing in all browsers.
*/
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* Remove the outline on focused links when they are also active or hovered
* in all browsers (opinionated).
*/
a:active,
a:hover {
outline-width: 0;
}
/**
* Modify default styling of address.
*/
address {
font-style: normal;
}
/**
* 1. Remove the bottom border in Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. (removed)
*/
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: var(--mono-font-family); /* 1 (changed) */
font-size: 1em; /* 2 */
}
/**
* Add the correct font style in Android 4.3-.
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-. (Removed)
*/
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
font-weight: 400; /* (added) */
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 (changed) */
font-size: inherit; /* 1 (changed) */
line-height: inherit; /* 1 (changed) */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule (removed).
*/
/**
* Change the border, margin, and padding in all browsers (opinionated) (changed).
*/
fieldset {
border: 0;
margin: 0;
padding: 0;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Remove the default vertical scrollbar in IE.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
*/
details, /* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
outline: none;
}
/* Scripting
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
*/
template {
display: none;
}
/* Hidden
========================================================================== */
/**
* Add the correct display in IE 10-.
*/
[hidden] {
display: none;
}

View file

@ -0,0 +1,38 @@
/* Animations */
@keyframes loading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes slide-down {
0% {
opacity: 0;
transform: translateY(calc(-1 * var(--unit-8)));
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

View file

@ -0,0 +1,43 @@
/* Optimized for East Asian CJK */
html:lang(zh),
html:lang(zh-Hans),
.lang-zh,
.lang-zh-hans {
font-family: var(--cjk-zh-hans-font-family);
}
html:lang(zh-Hant),
.lang-zh-hant {
font-family: var(--cjk-zh-hant-font-family);
}
html:lang(ja),
.lang-ja {
font-family: var(--cjk-jp-font-family);
}
html:lang(ko),
.lang-ko {
font-family: var(--cjk-ko-font-family);
}
:lang(zh),
:lang(ja),
.lang-cjk {
& ins,
& u {
border-bottom: var(--border-width) solid;
text-decoration: none;
}
& del + del,
& del + s,
& ins + ins,
& ins + u,
& s + del,
& s + s,
& u + ins,
& u + u {
margin-left: .125em;
}
}

View file

@ -0,0 +1,55 @@
/* Autocomplete */
.form-autocomplete {
position: relative;
& .form-autocomplete-input {
align-content: flex-start;
display: flex;
flex-wrap: wrap;
height: auto;
min-height: var(--unit-8);
padding: var(--unit-h);
background: var(--input-bg-color);
&.is-focused {
outline: var(--focus-outline);
outline-offset: calc(var(--focus-outline-offset) * -1);
}
& .form-input {
background: transparent;
border-color: transparent;
box-shadow: none;
display: inline-block;
flex: 1 0 auto;
height: var(--unit-6);
line-height: var(--unit-4);
margin: var(--unit-h);
width: auto;
&:focus {
outline: none;
}
}
}
& .menu {
left: 0;
position: absolute;
top: 100%;
width: 100%;
& .menu-item.selected > a, & .menu-item > a:hover {
background: var(--menu-item-hover-bg-color);
color: var(--menu-item-hover-color);
}
& .group-item, & .group-item:hover {
color: var(--tertiary-text-color);
text-transform: uppercase;
background: none;
font-size: 0.6rem;
font-weight: bold;
}
}
}

View file

@ -0,0 +1,64 @@
/* Badges */
.badge {
position: relative;
white-space: nowrap;
&[data-badge],
&:not([data-badge]) {
&::after {
background: var(--primary-color);
background-clip: padding-box;
border-radius: .5rem;
box-shadow: 0 0 0 1px var(--body-color);
color: var(--contrast-text-color);
content: attr(data-badge);
display: inline-block;
transform: translate(-.05rem, -.5rem);
}
}
&[data-badge] {
&::after {
font-size: var(--font-size-sm);
height: .9rem;
line-height: 1;
min-width: .9rem;
padding: .1rem .2rem;
text-align: center;
white-space: nowrap;
}
}
&:not([data-badge]),
&[data-badge=""] {
&::after {
height: 6px;
min-width: 6px;
padding: 0;
width: 6px;
}
}
/* Badges for Buttons */
&.btn {
&::after {
position: absolute;
top: 0;
right: 0;
transform: translate(50%, -50%);
}
}
/* Badges for Avatars */
&.avatar {
&::after {
position: absolute;
top: 14.64%;
right: 14.64%;
transform: translate(50%, -50%);
z-index: var(--zindex-1);
}
}
}

View file

@ -0,0 +1,61 @@
/* Base */
*,
*::before,
*::after {
box-sizing: inherit;
}
html {
box-sizing: border-box;
font-size: var(--html-font-size);
line-height: var(--html-line-height);
-webkit-tap-highlight-color: transparent;
scrollbar-gutter: stable;
}
/* Reserve space for vert. scrollbar to avoid layout shifting when scrollbars are added */
html {
scrollbar-gutter: stable;
}
@media (pointer: coarse) {
html {
scrollbar-gutter: initial;
}
}
body {
background: var(--body-color);
color: var(--text-color);
font-family: var(--body-font-family);
font-size: var(--font-size);
overflow-x: hidden;
text-rendering: optimizeLegibility;
}
a {
color: var(--link-color);
outline: none;
text-decoration: none;
}
a:focus-visible {
outline: var(--focus-outline);
outline-offset: var(--focus-outline-offset);
}
a:focus,
a:hover,
a:active,
a.active {
text-decoration: underline;
}
summary {
cursor: pointer;
}
summary:focus-visible {
outline: var(--focus-outline);
outline-offset: var(--focus-outline-offset);
}

View file

@ -0,0 +1,257 @@
/* Buttons */
:root {
--btn-bg-color: var(--body-color);
--btn-hover-bg-color: var(--gray-50);
--btn-border-color: var(--border-color);
--btn-text-color: var(--text-color);
--btn-icon-color: var(--icon-color);
--btn-font-weight: 400;
--btn-box-shadow: var(--box-shadow-xs);
--btn-primary-bg-color: var(--primary-color);
--btn-primary-hover-bg-color: var(--primary-color-highlight);
--btn-primary-text-color: var(--contrast-text-color);
--btn-success-bg-color: var(--success-color);
--btn-success-hover-bg-color: var(--success-color-highlight);
--btn-success-text-color: var(--contrast-text-color);
--btn-error-bg-color: var(--error-color);
--btn-error-hover-bg-color: var(--error-color-highlight);
--btn-error-text-color: var(--contrast-text-color);
--btn-link-text-color: var(--link-color);
--btn-link-hover-text-color: var(--link-color);
}
.btn {
appearance: none;
background: var(--btn-bg-color);
border: var(--border-width) solid var(--btn-border-color);
border-radius: var(--border-radius);
color: var(--btn-text-color);
font-weight: var(--btn-font-weight);
cursor: pointer;
display: inline-flex;
align-items: baseline;
justify-content: center;
font-size: var(--font-size);
height: var(--control-size);
line-height: var(--line-height);
outline: none;
padding: var(--control-padding-y) var(--control-padding-x);
box-shadow: var(--btn-box-shadow);
text-align: center;
text-decoration: none;
transition: background 0.2s, border 0.2s, box-shadow 0.2s, color 0.2s;
user-select: none;
vertical-align: middle;
white-space: nowrap;
&:focus-visible {
outline: var(--focus-outline);
outline-offset: var(--focus-outline-offset);
}
&:hover {
background: var(--btn-hover-bg-color);
text-decoration: none;
}
&[disabled],
&:disabled,
&.disabled {
cursor: default;
opacity: 0.5;
pointer-events: none;
}
&:focus,
&:hover,
&:active,
&.active {
text-decoration: none;
}
/* Button Primary */
&.btn-primary {
background: var(--btn-primary-bg-color);
border-color: transparent;
color: var(--btn-primary-text-color);
--btn-icon-color: var(--btn-primary-text-color);
&:hover {
background: var(--btn-primary-hover-bg-color);
}
}
/* Button Colors */
&.btn-success {
background: var(--btn-success-bg-color);
border-color: transparent;
color: var(--btn-success-text-color);
--btn-icon-color: var(--btn-success-text-color);
&:hover {
background: var(--btn-success-hover-bg-color);
}
}
&.btn-error {
--btn-border-color: var(--error-color);
--btn-text-color: var(--error-color);
&:hover {
--btn-hover-bg-color: var(--error-color-shade);
}
}
/* Button Link */
&.btn-link {
background: transparent;
border-color: transparent;
box-shadow: none;
color: var(--btn-link-text-color);
--btn-icon-color: var(--btn-link-text-color);
&:hover {
color: var(--btn-link-hover-text-color);
--btn-icon-color: var(--btn-link-hover-text-color);
}
&:focus,
&:hover,
&:active,
&.active {
text-decoration: none;
}
}
/* Button Sizes */
&.btn-sm {
font-size: var(--font-size-sm);
height: var(--control-size-sm);
padding: var(--control-padding-y-sm) var(--control-padding-x-sm);
}
&.btn-lg {
font-size: var(--font-size-lg);
height: var(--control-size-lg);
padding: var(--control-padding-y-lg) var(--control-padding-x-lg);
}
/* Button Block */
&.btn-block {
display: block;
width: 100%;
}
/* Button Action */
&.btn-action {
width: var(--control-size);
padding-left: 0;
padding-right: 0;
&.btn-sm {
width: var(--control-size-sm);
}
&.btn-lg {
width: var(--control-size-lg);
}
}
/* Button Clear */
&.btn-clear {
background: transparent;
border: 0;
color: currentColor;
box-shadow: none;
height: var(--unit-5);
line-height: var(--unit-4);
margin-left: var(--unit-1);
margin-right: -2px;
opacity: 1;
padding: var(--unit-h);
text-decoration: none;
width: var(--unit-5);
&::before {
content: "\2715";
}
}
/* Wider button */
&.btn-wide {
padding-left: var(--unit-6);
padding-right: var(--unit-6);
}
/* Small icon button */
&.btn-sm.btn-icon {
display: inline-flex;
align-items: baseline;
gap: var(--unit-h);
svg {
align-self: center;
}
}
/* Button icons */
& svg {
color: var(--btn-icon-color);
align-self: center;
}
}
/* Button groups */
.btn-group {
display: inline-flex;
flex-wrap: wrap;
.btn {
flex: 1 0 auto;
&:first-child:not(:last-child) {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
&:not(:first-child):not(:last-child) {
border-radius: 0;
margin-left: calc(-1 * var(--border-width));
}
&:last-child:not(:first-child) {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
margin-left: calc(-1 * var(--border-width));
}
&:focus,
&:hover,
&:active,
&.active {
z-index: var(--zindex-0);
}
}
&.btn-group-block {
display: flex;
.btn {
flex: 1 0 0;
}
}
}

View file

@ -0,0 +1,30 @@
/* Code */
:root {
--code-bg-color: var(--body-color-contrast);
--code-color: var(--text-color);
}
code {
border-radius: var(--border-radius);
line-height: 1.25;
padding: .1rem .2rem;
background: var(--code-bg-color);
color: var(--code-color);
font-size: 85%;
}
.code {
border-radius: var(--border-radius);
background: var(--code-bg-color);
color: var(--text-color);
position: relative;
& code {
color: inherit;
display: block;
line-height: 1.5;
overflow-x: auto;
padding: var(--unit-2);
width: 100%;
}
}

View file

@ -0,0 +1,36 @@
/* Dropdown */
.dropdown {
display: inline-block;
position: relative;
.menu {
animation: fade-in .15s ease 1;
display: none;
left: 0;
max-height: 50vh;
overflow-y: auto;
position: absolute;
top: 100%;
}
&.dropdown-right {
.menu {
left: auto;
right: 0;
}
}
&.active .menu,
.dropdown-toggle:focus + .menu,
.menu:hover {
display: block;
}
/* Fix dropdown-toggle border radius in button groups */
.btn-group {
.dropdown-toggle:nth-last-child(2) {
border-bottom-right-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
}
}

View file

@ -0,0 +1,21 @@
/* Empty states (or Blank slates) */
.empty {
background: var(--body-color-contrast);
border-radius: var(--border-radius);
color: var(--secondary-text-color);
text-align: center;
padding: var(--unit-16) var(--unit-8);
.empty-icon {
margin-bottom: var(--layout-spacing-lg);
}
.empty-title,
.empty-subtitle {
margin: var(--layout-spacing) auto;
}
.empty-action {
margin-top: var(--layout-spacing-lg);
}
}

View file

@ -0,0 +1,515 @@
/* Forms */
:root {
--input-bg-color: var(--body-color);
--input-disabled-bg-color: var(--gray-100);
--input-text-color: var(--text-color);
--input-hint-color: var(--secondary-text-color);
--input-border-color: var(--border-color);
--input-placeholder-color: var(--tertiary-text-color);
--input-box-shadow: var(--box-shadow-xs);
--checkbox-bg-color: var(--body-color);
--checkbox-checked-bg-color: var(--primary-color);
--checkbox-disabled-bg-color: var(--gray-100);
--checkbox-border-color: var(--border-color);
--checkbox-icon-color: #fff;
--switch-bg-color: var(--gray-300);
--switch-border-color: var(--gray-400);
--switch-toggle-color: #fff;
}
.form-group {
&:first-of-type {
margin-top: var(--unit-4);
}
&:not(:last-child) {
margin-bottom: var(--unit-4);
}
}
fieldset {
margin-bottom: var(--layout-spacing-lg);
}
legend {
font-size: var(--font-size-lg);
font-weight: 500;
margin-bottom: var(--layout-spacing-lg);
}
/* Form element: Label */
.form-label {
display: block;
line-height: var(--line-height);
margin-bottom: var(--unit-2);
font-weight: 500;
}
details summary .form-label {
margin-bottom: 0;
}
details[open] summary .form-label {
margin-bottom: var(--unit-2);
}
/* Form element: Input */
.form-input {
appearance: none;
background: var(--input-bg-color);
background-image: none;
border: var(--border-width) solid var(--input-border-color);
border-radius: var(--border-radius);
box-shadow: var(--input-box-shadow);
color: var(--input-text-color);
display: block;
font-size: var(--font-size);
height: var(--control-size);
line-height: var(--line-height);
max-width: 100%;
outline: none;
padding: var(--control-padding-y) var(--control-padding-x);
position: relative;
transition: background 0.2s, border 0.2s, color 0.2s;
width: 100%;
&:focus {
outline: var(--focus-outline);
outline-offset: calc(var(--focus-outline-offset) * -1);
}
&::placeholder {
color: var(--input-placeholder-color);
opacity: 1;
}
/* Input sizes */
&.input-sm {
font-size: var(--font-size-sm);
height: var(--control-size-sm);
padding: var(--control-padding-y-sm) var(--control-padding-x-sm);
}
&.input-lg {
font-size: var(--font-size-lg);
height: var(--control-size-lg);
padding: var(--control-padding-y-lg) var(--control-padding-x-lg);
}
&.input-inline {
display: inline-block;
vertical-align: middle;
width: auto;
}
/* Input types */
&[type="file"] {
height: auto;
}
}
/* Form element: Textarea */
textarea.form-input {
&,
&.input-lg,
&.input-sm {
height: auto;
}
}
/* Form element: Input hint */
.form-input-hint {
color: var(--input-hint-color);
font-size: var(--font-size-sm);
margin-top: var(--unit-1);
.has-success &,
.is-success + & {
color: var(--success-color);
}
.has-error &,
.is-error + & {
color: var(--error-color);
}
}
/* Form element: Select */
.form-select {
appearance: none;
background: var(--input-bg-color);
border: var(--border-width) solid var(--input-border-color);
border-radius: var(--border-radius);
box-shadow: var(--input-box-shadow);
color: var(--input-text-color);
font-size: var(--font-size);
height: var(--control-size);
line-height: var(--line-height);
outline: none;
padding: var(--control-padding-y) var(--control-padding-x);
vertical-align: middle;
width: 100%;
&:focus {
outline: var(--focus-outline);
outline-offset: calc(var(--focus-outline-offset) * -1);
}
/* Select sizes */
&.select-sm {
font-size: var(--font-size-sm);
height: var(--control-size-sm);
padding: var(--control-padding-y-sm) calc(var(--control-icon-size) + var(--control-padding-x-sm)) var(--control-padding-y-sm) var(--control-padding-x-sm);
}
&.select-lg {
font-size: var(--font-size-lg);
height: var(--control-size-lg);
padding: var(--control-padding-y-lg) calc(var(--control-icon-size) + var(--control-padding-x-lg)) var(--control-padding-y-lg) var(--control-padding-x-lg);
}
/* Multiple select */
&[size],
&[multiple] {
height: auto;
padding: var(--control-padding-y) var(--control-padding-x);
& option {
padding: var(--unit-h) var(--unit-1);
}
}
&:not([multiple]):not([size]) {
background: var(--input-bg-color) url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%204%205'%3E%3Cpath%20fill='%23667189'%20d='M2%200L0%202h4zm0%205L0%203h4z'/%3E%3C/svg%3E") no-repeat right .35rem center / .4rem .5rem;
padding-right: calc(var(--control-icon-size) + var(--control-padding-x));
}
}
/* Form element: Checkbox and Radio */
.form-checkbox,
.form-radio,
.form-switch {
display: block;
line-height: var(--line-height);
margin: calc((var(--control-size) - var(--control-size-sm)) / 2) 0;
min-height: var(--control-size-sm);
padding: calc((var(--control-size-sm) - var(--line-height)) / 2) var(--control-padding-x) calc((var(--control-size-sm) - var(--line-height)) / 2) calc(var(--control-icon-size) + var(--control-padding-x));
position: relative;
input {
clip: rect(0, 0, 0, 0);
height: 1px;
margin: -1px;
overflow: hidden;
position: absolute;
width: 1px;
&:focus-visible + .form-icon {
outline: var(--focus-outline);
outline-offset: var(--focus-outline-offset);
}
&:checked + .form-icon {
background: var(--checkbox-checked-bg-color);
border-color: var(--checkbox-checked-bg-color);
}
}
.form-icon {
border: var(--border-width) solid var(--checkbox-border-color);
box-shadow: var(--input-box-shadow);
cursor: pointer;
display: inline-block;
position: absolute;
transition: background .2s, border .2s, color .2s;
}
/* Input checkbox, radio, and switch sizes */
&.input-sm {
font-size: var(--font-size-sm);
margin: 0;
}
&.input-lg {
font-size: var(--font-size-lg);
margin: calc((var(--control-size-lg) - var(--control-size-sm)) / 2) 0;
}
}
.form-checkbox,
.form-radio {
.form-icon {
background: var(--checkbox-bg-color);
height: var(--control-icon-size);
left: 0;
top: calc((var(--control-size-sm) - var(--control-icon-size)) / 2);
width: var(--control-icon-size);
}
}
.form-checkbox {
font-weight: 500;
.form-icon {
border-radius: var(--border-radius);
}
input {
&:checked + .form-icon {
&::before {
background-clip: padding-box;
border: var(--border-width-lg) solid var(--checkbox-icon-color);
border-left-width: 0;
border-top-width: 0;
content: "";
height: 9px;
left: 50%;
margin-left: -3px;
margin-top: -6px;
position: absolute;
top: 50%;
transform: rotate(45deg);
width: 6px;
}
}
&:indeterminate + .form-icon {
background: var(--checkbox-checked-bg-color);
border-color: var(--checkbox-checked-bg-color);
&::before {
background: var(--checkbox-icon-color);
content: "";
height: 2px;
left: 50%;
margin-left: -5px;
margin-top: -1px;
position: absolute;
top: 50%;
width: 10px;
}
}
}
}
.form-radio {
.form-icon {
border-radius: 50%;
}
input {
&:checked + .form-icon {
&::before {
background: var(--checkbox-icon-color);
border-radius: 50%;
content: "";
height: 6px;
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 6px;
}
}
}
}
/* Form element: Switch */
.form-switch {
padding-left: calc(var(--unit-8) + var(--control-padding-x));
.form-icon {
background: var(--switch-bg-color);
background-clip: padding-box;
border-color: var(--switch-border-color);
border-radius: calc(var(--unit-2) + var(--border-width));
height: calc(var(--unit-4) + var(--border-width) * 2);
left: 0;
top: calc((var(--control-size-sm) - var(--unit-4)) / 2 - var(--border-width));
width: var(--unit-8);
&::before {
background: var(--switch-toggle-color);
border-radius: 50%;
content: "";
display: block;
height: var(--unit-4);
left: 0;
position: absolute;
top: 0;
transition: background .2s, border .2s, color .2s, left .2s;
width: var(--unit-4);
}
}
input {
&:checked + .form-icon {
&::before {
left: 14px;
}
}
}
}
/* Form Icons */
.has-icon-left,
.has-icon-right {
position: relative;
.form-icon {
height: var(--control-icon-size);
margin: 0 var(--control-padding-y);
position: absolute;
top: 50%;
transform: translateY(-50%);
width: var(--control-icon-size);
z-index: calc(var(--zindex-0) + 1);
}
}
.has-icon-left {
& .form-icon {
left: var(--border-width);
}
& .form-input {
padding-left: calc(var(--control-icon-size) + var(--control-padding-y) * 2);
}
}
.has-icon-right {
& .form-icon {
right: var(--border-width);
}
& .form-input {
padding-right: calc(var(--control-icon-size) + var(--control-padding-y) * 2);
}
}
/* Form element: Input groups */
.input-group {
display: flex;
.input-group-addon {
background: var(--body-color);
border: var(--border-width) solid var(--input-border-color);
border-radius: var(--border-radius);
line-height: var(--line-height);
padding: var(--control-padding-y) var(--control-padding-x);
white-space: nowrap;
&.addon-sm {
font-size: var(--font-size-sm);
padding: var(--control-padding-y-sm) var(--control-padding-x-sm);
}
&.addon-lg {
font-size: var(--font-size-lg);
padding: var(--control-padding-y-lg) var(--control-padding-x-lg);
}
}
.form-input,
.form-select {
flex: 1 1 auto;
width: 1%;
}
.input-group-btn {
z-index: var(--zindex-0);
}
.form-input,
.form-select,
.input-group-addon,
.input-group-btn {
&:first-child:not(:last-child) {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
&:not(:first-child):not(:last-child) {
border-radius: 0;
margin-left: calc(-1 * var(--border-width));
}
&:last-child:not(:first-child) {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
margin-left: calc(-1 * var(--border-width));
}
&:focus {
z-index: calc(var(--zindex-0) + 1);
}
}
.form-select {
width: auto;
}
&.input-inline {
display: inline-flex;
}
}
/* Form validation states */
.form-input,
.form-select {
.has-success &,
&.is-success {
background: var(--success-color-shade);
border-color: var(--success-color);
&:focus {
outline-color: var(--success-color);
}
}
.has-error &,
&.is-error {
background: var(--error-color-shade);
border-color: var(--error-color);
&:focus {
outline-color: var(--error-color);
}
}
}
/* Form disabled and readonly */
.form-input,
.form-select {
&:disabled,
&.disabled {
background-color: var(--input-disabled-bg-color);
cursor: not-allowed;
}
}
input {
&:disabled,
&.disabled {
& + .form-icon {
background: var(--checkbox-disabled-bg-color);
cursor: not-allowed;
}
}
}
/* Increase input font size on small viewports to prevent zooming on focus the input */
/* on mobile devices. 430px relates to the "normalized" iPhone 14 Pro Max */
/* viewport size */
@media screen and (max-width: 430px) {
.form-input {
font-size: 16px;
}
}

View file

@ -0,0 +1,89 @@
:root {
--menu-bg-color: var(--body-color);
--menu-border-color: var(--gray-200);
--menu-border-radius: var(--border-radius);
--menu-box-shadow: var(--box-shadow);
--menu-item-color: var(--text-color);
--menu-item-hover-color: var(--primary-text-color);
--menu-item-bg-color: transparent;
--menu-item-hover-bg-color: var(--primary-color-shade);
}
/* Menus */
.menu {
background: var(--menu-bg-color);
border: solid 1px var(--menu-border-color);
border-radius: var(--menu-border-radius);
box-shadow: var(--menu-box-shadow);
list-style: none;
margin: 0;
min-width: var(--control-width-xs);
transform: translateY(var(--layout-spacing-sm));
z-index: var(--zindex-3);
&.menu-nav {
background: transparent;
box-shadow: none;
}
.menu-item {
margin-top: 0;
padding: 0 var(--unit-4);
position: relative;
text-decoration: none;
&:first-of-type {
padding-top: var(--unit-2);
}
&:last-of-type {
padding-bottom: var(--unit-2);
}
& > a, .btn.btn-link {
border-radius: var(--menu-border-radius);
color: var(--menu-item-color);
background: var(--menu-item-bg-color);
display: block;
margin: 0 calc(-1 * var(--unit-2));
padding: var(--unit-1) var(--unit-2);
text-decoration: none;
&:focus,
&:hover,
&:active,
&.active {
background: var(--menu-item-hover-bg-color);
color: var(--menu-item-hover-color);
}
}
.form-checkbox,
.form-radio,
.form-switch {
margin: var(--unit-h) 0;
}
& + .menu-item {
margin-top: var(--unit-1);
}
}
& .menu-badge {
align-items: center;
display: flex;
height: 100%;
position: absolute;
right: 0;
top: 0;
.label {
margin-right: var(--unit-2);
}
}
& .divider {
border-bottom: solid 1px var(--secondary-border-color);
margin: var(--unit-2) 0;
}
}

View file

@ -0,0 +1,93 @@
/* Modals */
:root {
--modal-overlay-bg-color: rgba(243, 244, 246, 0.6);
--modal-container-bg-color: var(--body-color);
--modal-container-border-color: var(--gray-200);
--modal-border-radius: var(--border-radius-lg);
--modal-box-shadow: var(--box-shadow-lg);
}
.modal {
align-items: center;
bottom: 0;
display: none;
justify-content: center;
left: 0;
opacity: 0;
overflow: hidden;
padding: var(--layout-spacing);
position: fixed;
right: 0;
top: 0;
&:target,
&.active {
display: flex;
opacity: 1;
z-index: var(--zindex-4);
& .modal-overlay {
animation: fade-in .15s ease 1;
background: var(--modal-overlay-bg-color);
bottom: 0;
cursor: default;
display: block;
left: 0;
position: absolute;
right: 0;
top: 0;
}
& .modal-container {
animation: fade-in .15s ease 1;
z-index: var(--zindex-0);
}
}
&.active.closing {
& .modal-overlay, & .modal-container {
animation: fade-out .15s ease 1;
}
}
}
.modal-container {
background: var(--modal-container-bg-color);
border: solid 1px var(--modal-container-border-color);
border-radius: var(--modal-border-radius);
box-shadow: var(--modal-box-shadow);
display: flex;
flex-direction: column;
gap: var(--unit-4);
max-height: 75vh;
max-width: var(--control-width-md);
padding: var(--unit-6);
width: 100%;
& .modal-header {
color: var(--text-color);
& button.close {
background: none;
border: none;
padding: 0;
line-height: 0;
cursor: pointer;
opacity: .85;
color: var(--secondary-text-color);
&:hover {
opacity: 1;
}
}
}
& .modal-body {
overflow-y: auto;
position: relative;
}
& .modal-footer {
text-align: right;
}
}

View file

@ -0,0 +1,61 @@
/* Pagination */
.pagination {
display: flex;
list-style: none;
margin: var(--unit-1) 0;
padding: var(--unit-1) 0;
& .page-item {
margin: var(--unit-1) var(--unit-o);
& span {
display: inline-block;
padding: var(--unit-1) var(--unit-1);
}
& a {
border-radius: var(--border-radius);
display: inline-block;
padding: var(--unit-1) var(--unit-2);
text-decoration: none;
&:focus,
&:hover {
color: var(--primary-text-color);
}
}
&.disabled {
& a {
cursor: default;
opacity: .5;
pointer-events: none;
}
}
&.active {
& a {
background: var(--primary-color);
color: var(--contrast-text-color);
}
}
&.page-prev,
&.page-next {
flex: 1 0 50%;
}
&.page-next {
text-align: right;
}
& .page-item-title {
margin: 0;
}
& .page-item-subtitle {
margin: 0;
opacity: .5;
}
}
}

View file

@ -0,0 +1,26 @@
/* Tables */
.table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
text-align: left;
/* Scrollable tables */
&.table-scroll {
display: block;
overflow-x: auto;
padding-bottom: 0.75rem;
white-space: nowrap;
}
& td,
& th {
border-bottom: var(--border-width) solid var(--border-color);
padding: var(--unit-3) var(--unit-2);
}
& th {
border-bottom-width: var(--border-width-lg);
}
}

View file

@ -0,0 +1,75 @@
/* Tabs */
:root {
--tab-color: var(--text-color);
--tab-hover-color: var(--primary-text-color);
--tab-active-color: var(--primary-text-color);
--tab-highlight-color: var(--primary-color);
}
.tab {
align-items: center;
border-bottom: var(--border-width) solid var(--border-color);
display: flex;
flex-wrap: wrap;
list-style: none;
margin: var(--unit-1) 0 calc(var(--unit-1) - var(--border-width)) 0;
& .tab-item {
margin-top: 0;
& a {
border-bottom: var(--border-width-lg) solid transparent;
color: var(--tab-color);
display: block;
margin: 0 var(--unit-2) 0 0;
padding: var(--unit-2) var(--unit-1) calc(var(--unit-2) - var(--border-width-lg)) var(--unit-1);
text-decoration: none;
&:focus,
&:hover {
color: var(--tab-hover-color);
}
}
&.active a,
& a.active {
border-bottom-color: var(--tab-highlight-color);
color: var(--tab-active-color);
}
&.tab-action {
flex: 1 0 auto;
text-align: right;
}
& .btn-clear {
margin-top: calc(-1 * var(--unit-1));
}
}
&.tab-block {
& .tab-item {
flex: 1 0 0;
text-align: center;
& a {
margin: 0;
}
& .badge {
&[data-badge]::after {
position: absolute;
right: var(--unit-h);
top: var(--unit-h);
transform: translate(0, 0);
}
}
}
}
&:not(.tab-block) {
& .badge {
padding-right: 0;
}
}
}

View file

@ -0,0 +1,35 @@
/* Toasts */
.toast {
background: var(--gray-600);
border-radius: var(--border-radius);
color: var(--contrast-text-color);
display: block;
padding: var(--layout-spacing);
width: 100%;
&.toast-primary {
background: var(--primary-color);
}
&.toast-success {
background: var(--success-color);
}
&.toast-warning {
background: var(--warning-color);
}
&.toast-error {
background: var(--error-color);
}
.btn-clear {
margin: var(--unit-h);
}
p {
&:last-child {
margin-bottom: 0;
}
}
}

View file

@ -0,0 +1,117 @@
/* Typography */
/* Headings */
h1,
h2,
h3,
h4,
h5,
h6 {
color: inherit;
font-weight: 500;
line-height: 1.2;
margin-bottom: 0.5em;
margin-top: 0;
}
.h1,
.h2,
.h3,
.h4,
.h5,
.h6 {
font-weight: 500;
}
h1,
.h1 {
font-size: 2rem;
}
h2,
.h2 {
font-size: 1.6rem;
}
h3,
.h3 {
font-size: 1.4rem;
}
h4,
.h4 {
font-size: 1.2rem;
}
h5,
.h5 {
font-size: 1rem;
}
h6,
.h6 {
font-size: 0.8rem;
}
/* Paragraphs */
p {
margin: 0 0 var(--line-height);
}
/* Semantic text elements */
a,
ins,
u {
text-decoration-skip-ink: auto;
}
abbr[title] {
border-bottom: var(--border-width) dotted;
cursor: help;
text-decoration: none;
}
/* Blockquote */
blockquote {
border-left: var(--border-width-lg) solid var(--border-color);
margin-left: 0;
padding: var(--unit-2) var(--unit-4);
& p:last-child {
margin-bottom: 0;
}
}
/* Lists */
ul,
ol {
margin: var(--unit-4) 0 var(--unit-4) var(--unit-4);
padding: 0;
& ul,
& ol {
margin: var(--unit-4) 0 var(--unit-4) var(--unit-4);
}
& li {
margin-top: var(--unit-2);
}
}
ul {
list-style: disc inside;
& ul {
list-style-type: circle;
}
}
ol {
list-style: decimal inside;
& ol {
list-style-type: lower-alpha;
}
}
dl {
& dt {
font-weight: bold;
}
& dd {
margin: var(--unit-1) 0 var(--unit-4) 0;
}
}

View file

@ -0,0 +1,296 @@
/* Colors */
.text-primary {
color: var(--primary-text-color);
}
.text-secondary {
color: var(--secondary-text-color);
}
.text-tertiary {
color: var(--tertiary-text-color);
}
.text-success {
color: var(--success-color);
}
.text-warning {
color: var(--warning-color);
}
.text-error {
color: var(--error-color);
}
.icon-color {
color: var(--icon-color);
}
/* Display */
.d-block {
display: block;
}
.d-inline {
display: inline;
}
.d-inline-block {
display: inline-block;
}
.d-flex {
display: flex;
}
.d-inline-flex {
display: inline-flex;
}
.d-none,
.d-hide {
display: none !important;
}
.d-visible {
visibility: visible;
}
.d-invisible {
visibility: hidden;
}
.text-hide {
background: transparent;
border: 0;
color: transparent;
font-size: 0;
line-height: 0;
text-shadow: none;
}
.text-assistive {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
/* Loading */
.loading {
color: transparent !important;
min-height: var(--unit-4);
pointer-events: none;
position: relative;
&::after {
animation: loading 500ms infinite linear;
background: transparent;
border: var(--border-width-lg) solid var(--primary-color);
border-radius: 50%;
border-right-color: transparent;
border-top-color: transparent;
content: "";
display: block;
height: var(--unit-4);
left: 50%;
margin-left: calc(-1 * var(--unit-2));
margin-top: calc(-1 * var(--unit-2));
opacity: 1;
padding: 0;
position: absolute;
top: 50%;
width: var(--unit-4);
z-index: var(--zindex-0);
}
&.loading-lg {
min-height: var(--unit-10);
&::after {
height: var(--unit-8);
margin-left: calc(-1 * var(--unit-4));
margin-top: calc(-1 * var(--unit-4));
width: var(--unit-8);
}
}
}
/* Position */
.m-0 {
margin: 0 !important;
}
.mb-0 {
margin-bottom: 0 !important;
}
.ml-0 {
margin-left: 0 !important;
}
.mr-0 {
margin-right: 0 !important;
}
.mt-0 {
margin-top: 0 !important;
}
.mx-0 {
margin-left: 0 !important;
margin-right: 0 !important;
}
.my-0 {
margin-bottom: 0 !important;
margin-top: 0 !important;
}
.m-1 {
margin: var(--unit-1) !important;
}
.mb-1 {
margin-bottom: var(--unit-1) !important;
}
.ml-1 {
margin-left: var(--unit-1) !important;
}
.mr-1 {
margin-right: var(--unit-1) !important;
}
.mt-1 {
margin-top: var(--unit-1) !important;
}
.mx-1 {
margin-left: var(--unit-1) !important;
margin-right: var(--unit-1) !important;
}
.my-1 {
margin-bottom: var(--unit-1) !important;
margin-top: var(--unit-1) !important;
}
.m-2 {
margin: var(--unit-2) !important;
}
.mb-2 {
margin-bottom: var(--unit-2) !important;
}
.ml-2 {
margin-left: var(--unit-2) !important;
}
.mr-2 {
margin-right: var(--unit-2) !important;
}
.mt-2 {
margin-top: var(--unit-2) !important;
}
.mx-2 {
margin-left: var(--unit-2) !important;
margin-right: var(--unit-2) !important;
}
.my-2 {
margin-bottom: var(--unit-2) !important;
margin-top: var(--unit-2) !important;
}
.m-4 {
margin: var(--unit-4) !important;
}
.mb-4 {
margin-bottom: var(--unit-4) !important;
}
.ml-4 {
margin-left: var(--unit-4) !important;
}
.mr-4 {
margin-right: var(--unit-4) !important;
}
.mt-4 {
margin-top: var(--unit-4) !important;
}
.mx-4 {
margin-left: var(--unit-4) !important;
margin-right: var(--unit-4) !important;
}
.my-4 {
margin-bottom: var(--unit-4) !important;
margin-top: var(--unit-4) !important;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
/* Text */
.text-normal {
font-weight: normal;
}
.text-bold {
font-weight: bold;
}
.text-italic {
font-style: italic;
}
.text-large {
font-size: 1.2em;
}
.text-small {
font-size: .9em;
}
.text-tiny {
font-size: .8em;
}
.text-muted {
opacity: .8;
}
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Flex */
.align-baseline {
align-items: baseline;
}
.align-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}

View file

@ -0,0 +1,135 @@
:root {
/* Color palette */
--gray-50: rgb(249, 250, 251);
--gray-100: rgb(243, 244, 246);
--gray-200: rgb(229, 231, 235);
--gray-300: rgb(209, 213, 219);
--gray-400: rgb(156, 163, 175);
--gray-500: rgb(107, 114, 128);
--gray-600: rgb(75, 85, 99);
--gray-700: rgb(55, 65, 81);
--gray-800: rgb(31, 41, 55);
--gray-900: rgb(17, 24, 39);
--primary-color: hsl(241, 63%, 59%);
--primary-color-highlight: hsl(241, 63%, 64%);
--primary-color-shade: hsl(241, 63%, 59%, 0.075);
--alternative-color: hsl(179, 94%, 29%);
--alternative-color-dark: hsl(179, 94%, 22%);
--success-color: hsl(142, 76%, 36%);
--success-color-highlight: hsl(142, 76%, 40%);
--success-color-shade: hsla(142, 76%, 36%, 0.1);
--warning-color: hsl(38, 92%, 50%);
--warning-color-highlight: hsl(38, 92%, 55%);
--warning-color-shade: hsla(38, 92%, 50%, 0.1);
--error-color: hsl(0, 72%, 51%);
--error-color-highlight: hsl(0, 72%, 60%);
--error-color-shade: hsla(0, 72%, 51%, 0.1);
/* Core colors */
--text-color: var(--gray-700);
--secondary-text-color: var(--gray-500);
--tertiary-text-color: var(--gray-500);
--contrast-text-color: #fff;
--primary-text-color: hsl(241, 63%, 55%);
--link-color: var(--primary-text-color);
--secondary-link-color: hsla(241, 63%, 54%, 0.8);
--icon-color: var(--gray-500);
--border-color: var(--gray-300);
--secondary-border-color: var(--gray-200);
--body-color: #fff;
--body-color-contrast: var(--gray-100);
/* Fonts */
--base-font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto;
--mono-font-family: "SF Mono", "Segoe UI Mono", "Roboto Mono", Menlo, Courier, monospace;
--fallback-font-family: "Helvetica Neue", sans-serif;
--cjk-zh-hans-font-family: var(--base-font-family), "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", var(--fallback-font-family);
--cjk-zh-hant-font-family: var(--base-font-family), "PingFang TC", "Hiragino Sans CNS", "Microsoft JhengHei", var(--fallback-font-family);
--cjk-jp-font-family: var(--base-font-family), "Hiragino Sans", "Hiragino Kaku Gothic Pro", "Yu Gothic", YuGothic, Meiryo, var(--fallback-font-family);
--cjk-ko-font-family: var(--base-font-family), "Malgun Gothic", var(--fallback-font-family);
--body-font-family: var(--base-font-family), var(--fallback-font-family);
/* Unit sizes */
--unit-o: 0.05rem;
--unit-h: 0.1rem;
--unit-1: 0.2rem;
--unit-2: 0.4rem;
--unit-3: 0.6rem;
--unit-4: 0.8rem;
--unit-5: 1rem;
--unit-6: 1.2rem;
--unit-7: 1.4rem;
--unit-8: 1.6rem;
--unit-9: 1.8rem;
--unit-10: 2rem;
--unit-12: 2.4rem;
--unit-16: 3.2rem;
/* Font sizes */
--html-font-size: 20px;
--html-line-height: 1.5;
--font-size: 0.7rem;
--font-size-sm: 0.65rem;
--font-size-lg: 0.8rem;
--line-height: 1rem;
/* Sizes */
--layout-spacing: var(--unit-2);
--layout-spacing-sm: var(--unit-1);
--layout-spacing-lg: var(--unit-4);
--border-radius: var(--unit-1);
--border-radius-lg: var(--unit-2);
--border-width: var(--unit-o);
--border-width-lg: var(--unit-h);
--control-size: var(--unit-8);
--control-size-sm: var(--unit-6);
--control-size-lg: var(--unit-9);
--control-padding-x: var(--unit-2);
--control-padding-x-sm: calc(var(--unit-2) * 0.75);
--control-padding-x-lg: calc(var(--unit-2) * 1.5);
--control-padding-y: calc((var(--control-size) - var(--line-height)) / 2 - var(--border-width));
--control-padding-y-sm: calc((var(--control-size-sm) - var(--line-height)) / 2 - var(--border-width));
--control-padding-y-lg: calc((var(--control-size-lg) - var(--line-height)) / 2 - var(--border-width));
--control-icon-size: 0.8rem;
--control-width-xs: 180px;
--control-width-sm: 320px;
--control-width-md: 640px;
--control-width-lg: 960px;
--control-width-xl: 1280px;
/* Responsive breakpoints */
--size-xs: 480px;
--size-sm: 600px;
--size-md: 840px;
--size-lg: 960px;
--size-xl: 1280px;
--size-2x: 1440px;
--responsive-breakpoint: var(--size-xs);
/* Z-index */
--zindex-0: 1;
--zindex-1: 100;
--zindex-2: 200;
--zindex-3: 300;
--zindex-4: 400;
/* Focus */
--focus-outline: 2px solid var(--primary-color);
--focus-outline-offset: 2px;
/* Shadows */
--box-shadow-xs: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
--box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--box-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}

View file

@ -1,32 +0,0 @@
$body-bg: #161822 !default;
$bg-color: lighten($body-bg, 5%) !default;
$bg-color-light: lighten($body-bg, 5%) !default;
$border-color: #4C4E53 !default;
$border-color-dark: $border-color !default;
$body-font-color: #b5bec8 !default;
$light-color: #fafafa !default;
$gray-color: #7f879b !default;
$gray-color-dark: lighten($gray-color, 20%) !default;
$primary-color: #a8b1ff !default;
$primary-color-dark: saturate($primary-color, 5%) !default;
$secondary-color: lighten($body-bg, 10%) !default;
$link-color: $primary-color !default;
$link-color-dark: darken($link-color, 5%) !default;
$link-color-light: $link-color !default;
$secondary-link-color: rgba(168, 177, 255, 0.73);
$alternative-color: #59bdb9;
$alternative-color-dark: #73f1eb;
$code-bg-color: rgba(255, 255, 255, 0.1);
$code-shadow-color: rgba(255, 255, 255, 0.2);
/* Dark theme specific */
$dt-primary-input-color: #5C68E7 !default;
$dt-primary-button-color: #5761cb !default;

View file

@ -1,7 +0,0 @@
$alternative-color: #05a6a3;
$alternative-color-dark: darken($alternative-color, 5%);
$secondary-link-color: rgba(87, 85, 217, 0.64);
$code-bg-color: rgba(0, 0, 0, 0.05);
$code-shadow-color: rgba(0, 0, 0, 0.15);

View file

@ -58,18 +58,18 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if bookmark_item.notes %} {% if bookmark_item.notes %}
<div class="notes bg-gray text-gray-dark"> <div class="notes">
<div class="markdown">{% markdown bookmark_item.notes %}</div> <div class="markdown">{% markdown bookmark_item.notes %}</div>
</div> </div>
{% endif %} {% endif %}
<div class="actions text-gray"> <div class="actions">
{% if bookmark_item.display_date %} {% if bookmark_item.display_date %}
{% if bookmark_item.web_archive_snapshot_url %} {% if bookmark_item.web_archive_snapshot_url %}
<a href="{{ bookmark_item.web_archive_snapshot_url }}" <a href="{{ bookmark_item.web_archive_snapshot_url }}"
title="Show snapshot on the Internet Archive Wayback Machine" title="Show snapshot on the Internet Archive Wayback Machine"
target="{{ bookmark_list.link_target }}" target="{{ bookmark_list.link_target }}"
rel="noopener"> rel="noopener">
{{ bookmark_item.display_date }} {{ bookmark_item.display_date }}
</a> </a>
{% else %} {% else %}
<span>{{ bookmark_item.display_date }}</span> <span>{{ bookmark_item.display_date }}</span>

View file

@ -1,7 +1,7 @@
{% load shared %} {% load shared %}
{% htmlmin %} {% htmlmin %}
<div class="bulk-edit-bar"> <div class="bulk-edit-bar">
<div class="bulk-edit-actions bg-gray"> <div class="bulk-edit-actions">
<label class="form-checkbox bulk-edit-checkbox all"> <label class="form-checkbox bulk-edit-checkbox all">
<input type="checkbox"> <input type="checkbox">
<i class="form-icon"></i> <i class="form-icon"></i>
@ -27,7 +27,9 @@
<input ld-tag-autocomplete variant="small" <input ld-tag-autocomplete variant="small"
name="bulk_tag_string" class="form-input input-sm" placeholder="Tag names..."> name="bulk_tag_string" class="form-input input-sm" placeholder="Tag names...">
</div> </div>
<button ld-confirm-button type="submit" name="bulk_execute" class="btn btn-link btn-sm">Execute</button> <button ld-confirm-button type="submit" name="bulk_execute" class="btn btn-link btn-sm">
<span>Execute</span>
</button>
<label class="form-checkbox select-across d-none"> <label class="form-checkbox select-across d-none">
<input type="checkbox" name="bulk_select_across"> <input type="checkbox" name="bulk_select_across">

View file

@ -1,7 +1,9 @@
<button class="btn hide-sm ml-2 bulk-edit-active-toggle" title="Bulk edit"> <button class="btn hide-sm ml-2 bulk-edit-active-toggle" title="Bulk edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="20px" <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 24 24" fill="none"
height="20px"> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/> <path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"/>
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z"/>
<path d="M16 5l3 3"/>
</svg> </svg>
</button> </button>

View file

@ -1,6 +1,6 @@
<div class="actions"> <div class="actions">
<div class="left-actions"> <div class="left-actions">
<a class="btn" <a class="btn btn-wide"
href="{% url 'bookmarks:edit' details.bookmark.id %}?return_url={{ details.edit_return_url|urlencode }}">Edit</a> href="{% url 'bookmarks:edit' details.bookmark.id %}?return_url={{ details.edit_return_url|urlencode }}">Edit</a>
</div> </div>
<div class="right-actions"> <div class="right-actions">
@ -8,7 +8,7 @@
method="post"> method="post">
{% csrf_token %} {% csrf_token %}
<button ld-confirm-button type="submit" name="remove" value="{{ details.bookmark.id }}" <button ld-confirm-button type="submit" name="remove" value="{{ details.bookmark.id }}"
class="btn btn-link text-error"> class="btn btn-error btn-wide">
Delete... Delete...
</button> </button>
</form> </form>

View file

@ -36,11 +36,11 @@
{% if details.is_editable %} {% if details.is_editable %}
<div class="assets-actions"> <div class="assets-actions">
<button type="submit" name="create_snapshot" class="btn btn-link" <button type="submit" name="create_snapshot" class="btn btn-sm"
{% if details.has_pending_assets %}disabled{% endif %}>Create HTML snapshot {% if details.has_pending_assets %}disabled{% endif %}>Create HTML snapshot
</button> </button>
<button ld-upload-button id="upload-asset" name="upload_asset" value="{{ details.bookmark.id }}" type="button" <button ld-upload-button id="upload-asset" name="upload_asset" value="{{ details.bookmark.id }}" type="button"
class="btn btn-link">Upload file class="btn btn-sm">Upload file
</button> </button>
<input id="upload-asset-file" name="upload_asset_file" type="file" class="d-hide"> <input id="upload-asset-file" name="upload_asset_file" type="file" class="d-hide">
</div> </div>

View file

@ -2,13 +2,15 @@
{% load bookmarks %} {% load bookmarks %}
{% block content %} {% block content %}
<section class="content-area"> <div class="bookmarks-form-page">
<div class="content-area-header"> <section class="content-area">
<h2>Edit bookmark</h2> <div class="content-area-header">
</div> <h2>Edit bookmark</h2>
<form action="{% url 'bookmarks:edit' bookmark_id %}?return_url={{ return_url|urlencode }}" method="post" </div>
class="width-50 width-md-100" novalidate> <form action="{% url 'bookmarks:edit' bookmark_id %}?return_url={{ return_url|urlencode }}" method="post"
{% bookmark_form form return_url bookmark_id %} novalidate>
</form> {% bookmark_form form return_url bookmark_id %}
</section> </form>
</section>
</div>
{% endblock %} {% endblock %}

View file

@ -34,14 +34,15 @@
<div class="has-icon-right"> <div class="has-icon-right">
{{ form.title|add_class:"form-input"|attr:"autocomplete:off" }} {{ form.title|add_class:"form-input"|attr:"autocomplete:off" }}
<i class="form-icon loading"></i> <i class="form-icon loading"></i>
<a class="btn btn-link form-icon" title="Edit title from website"> <button type="button" class="btn btn-link form-icon" title="Edit title from website">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="none"
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"/> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path fill-rule="evenodd" <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" <path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"/>
clip-rule="evenodd"/> <path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z"/>
<path d="M16 5l3 3"/>
</svg> </svg>
</a> </button>
</div> </div>
<div class="form-input-hint"> <div class="form-input-hint">
Optional, leave empty to use title from website. Optional, leave empty to use title from website.
@ -53,14 +54,15 @@
<div class="has-icon-right"> <div class="has-icon-right">
{{ form.description|add_class:"form-input"|attr:"rows:2" }} {{ form.description|add_class:"form-input"|attr:"rows:2" }}
<i class="form-icon loading"></i> <i class="form-icon loading"></i>
<a class="btn btn-link form-icon" title="Edit description from website"> <button type="button" class="btn btn-link form-icon" title="Edit description from website">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="none"
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"/> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path fill-rule="evenodd" <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" <path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"/>
clip-rule="evenodd"/> <path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z"/>
<path d="M16 5l3 3"/>
</svg> </svg>
</a> </button>
</div> </div>
<div class="form-input-hint"> <div class="form-input-hint">
Optional, leave empty to use description from website. Optional, leave empty to use description from website.
@ -74,11 +76,11 @@
</summary> </summary>
<label for="{{ form.notes.id_for_label }}" class="text-assistive">Notes</label> <label for="{{ form.notes.id_for_label }}" class="text-assistive">Notes</label>
{{ form.notes|add_class:"form-input"|attr:"rows:8" }} {{ form.notes|add_class:"form-input"|attr:"rows:8" }}
<div class="form-input-hint">
Additional notes, supports Markdown.
</div>
{{ form.notes.errors }}
</details> </details>
<div class="form-input-hint">
Additional notes, supports Markdown.
</div>
{{ form.notes.errors }}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.unread.id_for_label }}" class="form-checkbox"> <label for="{{ form.unread.id_for_label }}" class="form-checkbox">
@ -106,12 +108,12 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
<br/> <div class="divider"></div>
<div class="form-group"> <div class="form-group d-flex justify-between">
{% if auto_close %} {% if auto_close %}
<input type="submit" value="Save and close" class="btn btn-primary mr-2"> <input type="submit" value="Save and close" class="btn btn-primary btn-wide">
{% else %} {% else %}
<input type="submit" value="Save" class="btn btn-primary mr-2"> <input type="submit" value="Save" class="btn btn-primary btn btn-primary btn-wide">
{% endif %} {% endif %}
<a href="{{ cancel_url }}" class="btn">Nevermind</a> <a href="{{ cancel_url }}" class="btn">Nevermind</a>
</div> </div>

View file

@ -1,5 +1,4 @@
{% load static %} {% load static %}
{% load sass_tags %}
<!DOCTYPE html> <!DOCTYPE html>
{# Use data attributes as storage for access in static scripts #} {# Use data attributes as storage for access in static scripts #}
@ -17,19 +16,18 @@
<meta name="robots" content="index,follow"> <meta name="robots" content="index,follow">
<meta name="author" content="Sascha Ißbrücker"> <meta name="author" content="Sascha Ißbrücker">
<title>linkding</title> <title>linkding</title>
{# Include SASS styles, files are resolved from bookmarks/styles #}
{# Include specific theme variant based on user profile setting #} {# Include specific theme variant based on user profile setting #}
{% if request.user_profile.theme == 'light' %} {% if request.user_profile.theme == 'light' %}
<link href="{% sass_src 'theme-light.scss' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/> <link href="{% static 'theme-light.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/>
<meta name="theme-color" content="#5856e0"> <meta name="theme-color" content="#5856e0">
{% elif request.user_profile.theme == 'dark' %} {% elif request.user_profile.theme == 'dark' %}
<link href="{% sass_src 'theme-dark.scss' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/> <link href="{% static 'theme-dark.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/>
<meta name="theme-color" content="#161822"> <meta name="theme-color" content="#161822">
{% else %} {% else %}
{# Use auto theme as fallback #} {# Use auto theme as fallback #}
<link href="{% sass_src 'theme-dark.scss' %}?v={{ app_version }}" rel="stylesheet" type="text/css" <link href="{% static 'theme-dark.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"
media="(prefers-color-scheme: dark)"/> media="(prefers-color-scheme: dark)"/>
<link href="{% sass_src 'theme-light.scss' %}?v={{ app_version }}" rel="stylesheet" type="text/css" <link href="{% static 'theme-light.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"
media="(prefers-color-scheme: light)"/> media="(prefers-color-scheme: light)"/>
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#161822"> <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#161822">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#5856e0"> <meta name="theme-color" media="(prefers-color-scheme: light)" content="#5856e0">

View file

@ -1,88 +1,83 @@
{% load shared %} {% load shared %}
{% htmlmin %} {% htmlmin %}
{# Basic menu list #} {# Basic menu list #}
<div class="hide-md"> <div class="hide-md">
<a href="{% url 'bookmarks:new' %}" class="btn btn-primary mr-2">Add bookmark</a> <a href="{% url 'bookmarks:new' %}" class="btn btn-primary mr-2">Add bookmark</a>
<div class="dropdown"> <div class="dropdown">
<a href="#" class="btn btn-link dropdown-toggle" tabindex="0" style="padding-right: 0.2rem"> <button class="btn btn-link dropdown-toggle" tabindex="0">
Bookmarks Bookmarks
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" </button>
style="height:1rem;width:1rem;vertical-align: middle;"> <ul class="menu">
<path fill-rule="evenodd" <li class="menu-item">
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" <a href="{% url 'bookmarks:index' %}" class="menu-link">Active</a>
clip-rule="evenodd"/>
</svg>
</a>
<ul class="menu">
<li>
<a href="{% url 'bookmarks:index' %}" class="btn btn-link">Active</a>
</li>
<li>
<a href="{% url 'bookmarks:archived' %}" class="btn btn-link">Archived</a>
</li>
{% if request.user_profile.enable_sharing %}
<li>
<a href="{% url 'bookmarks:shared' %}" class="btn btn-link">Shared</a>
</li> </li>
{% endif %} <li class="menu-item">
<li> <a href="{% url 'bookmarks:archived' %}" class="menu-link">Archived</a>
<a href="{% url 'bookmarks:index' %}?unread=yes" class="btn btn-link">Unread</a> </li>
</li> {% if request.user_profile.enable_sharing %}
<li> <li class="menu-item">
<a href="{% url 'bookmarks:index' %}?q=!untagged" class="btn btn-link">Untagged</a> <a href="{% url 'bookmarks:shared' %}" class="menu-link">Shared</a>
</li> </li>
</ul> {% endif %}
<li class="menu-item">
<a href="{% url 'bookmarks:index' %}?unread=yes" class="menu-link">Unread</a>
</li>
<li class="menu-item">
<a href="{% url 'bookmarks:index' %}?q=!untagged" class="menu-link">Untagged</a>
</li>
</ul>
</div>
<a href="{% url 'bookmarks:settings.index' %}" class="btn btn-link">Settings</a>
<form class="d-inline" action="{% url 'logout' %}" method="post">
{% csrf_token %}
<button type="submit" class="btn btn-link">Logout</button>
</form>
</div> </div>
<a href="{% url 'bookmarks:settings.index' %}" class="btn btn-link">Settings</a> {# Menu drop-down for smaller devices #}
<form class="d-inline" action="{% url 'logout' %}" method="post"> <div class="show-md">
{% csrf_token %} <a href="{% url 'bookmarks:new' %}" class="btn btn-primary">
<button type="submit" class="btn btn-link">Logout</button>
</form>
</div>
{# Menu drop-down for smaller devices #}
<div class="show-md">
<a href="{% url 'bookmarks:new' %}" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
style="width: 24px; height: 24px">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
</a>
<div ld-dropdown class="dropdown dropdown-right">
<a href="#" class="btn btn-link dropdown-toggle" tabindex="0">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
style="width: 24px; height: 24px"> style="width: 24px; height: 24px">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg> </svg>
</a> </a>
<!-- menu component --> <div ld-dropdown class="dropdown dropdown-right">
<ul class="menu"> <button class="btn btn-link dropdown-toggle" tabindex="0">
<li> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
<a href="{% url 'bookmarks:index' %}" class="btn btn-link">Bookmarks</a> style="width: 24px; height: 24px">
</li> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
<li style="padding-left: 1rem"> </svg>
<a href="{% url 'bookmarks:archived' %}" class="btn btn-link">Archived</a> </button>
</li> <!-- menu component -->
{% if request.user_profile.enable_sharing %} <ul class="menu">
<li style="padding-left: 1rem"> <li class="menu-item">
<a href="{% url 'bookmarks:shared' %}" class="btn btn-link">Shared</a> <a href="{% url 'bookmarks:index' %}" class="menu-link">Bookmarks</a>
</li> </li>
{% endif %} <li class="menu-item">
<li style="padding-left: 1rem"> <a href="{% url 'bookmarks:archived' %}" class="menu-link">Archived</a>
<a href="{% url 'bookmarks:index' %}?unread=yes" class="btn btn-link">Unread</a> </li>
</li> {% if request.user_profile.enable_sharing %}
<li style="padding-left: 1rem"> <li class="menu-item">
<a href="{% url 'bookmarks:index' %}?q=!untagged" class="btn btn-link">Untagged</a> <a href="{% url 'bookmarks:shared' %}" class="menu-link">Shared</a>
</li> </li>
<li> {% endif %}
<a href="{% url 'bookmarks:settings.index' %}" class="btn btn-link">Settings</a> <li class="menu-item">
</li> <a href="{% url 'bookmarks:index' %}?unread=yes" class="menu-link">Unread</a>
<li> </li>
<form class="d-inline" action="{% url 'logout' %}" method="post"> <li class="menu-item">
{% csrf_token %} <a href="{% url 'bookmarks:index' %}?q=!untagged" class="menu-link">Untagged</a>
<button type="submit" class="btn btn-link">Logout</button> </li>
</form> <div class="divider"></div>
</li> <li class="menu-item">
</ul> <a href="{% url 'bookmarks:settings.index' %}" class="menu-link">Settings</a>
</li>
<li class="menu-item">
<form class="d-inline" action="{% url 'logout' %}" method="post">
{% csrf_token %}
<button type="submit" class="btn btn-link menu-link">Logout</button>
</form>
</li>
</ul>
</div>
</div> </div>
</div>
{% endhtmlmin %} {% endhtmlmin %}

View file

@ -2,12 +2,14 @@
{% load bookmarks %} {% load bookmarks %}
{% block content %} {% block content %}
<section class="content-area"> <div class="bookmarks-form-page">
<div class="content-area-header"> <section class="content-area">
<h2>New bookmark</h2> <div class="content-area-header">
</div> <h2>New bookmark</h2>
<form action="{% url 'bookmarks:new' %}" method="post" class="width-50 width-md-100" novalidate> </div>
{% bookmark_form form return_url auto_close=auto_close %} <form action="{% url 'bookmarks:new' %}" method="post" novalidate>
</form> {% bookmark_form form return_url auto_close=auto_close %}
</section> </form>
</section>
</div>
{% endblock %} {% endblock %}

View file

@ -1,4 +1,3 @@
{% load sass_tags %}
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="reader-mode"> <html lang="en" class="reader-mode">
@ -6,16 +5,21 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Reader view</title> <title>Reader view</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui"> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
{# Include specific theme variant based on user profile setting #}
{% if request.user_profile.theme == 'light' %} {% if request.user_profile.theme == 'light' %}
<link href="{% sass_src 'theme-light.scss' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/> <link href="{% static 'theme-light.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/>
<meta name="theme-color" content="#5856e0">
{% elif request.user_profile.theme == 'dark' %} {% elif request.user_profile.theme == 'dark' %}
<link href="{% sass_src 'theme-dark.scss' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/> <link href="{% static 'theme-dark.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/>
<meta name="theme-color" content="#161822">
{% else %} {% else %}
{# Use auto theme as fallback #} {# Use auto theme as fallback #}
<link href="{% sass_src 'theme-dark.scss' %}?v={{ app_version }}" rel="stylesheet" type="text/css" <link href="{% static 'theme-dark.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"
media="(prefers-color-scheme: dark)"/> media="(prefers-color-scheme: dark)"/>
<link href="{% sass_src 'theme-light.scss' %}?v={{ app_version }}" rel="stylesheet" type="text/css" <link href="{% static 'theme-light.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"
media="(prefers-color-scheme: light)"/> media="(prefers-color-scheme: light)"/>
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#161822">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#5856e0">
{% endif %} {% endif %}
</head> </head>
<body> <body>

View file

@ -1,10 +1,10 @@
{% load widget_tweaks %} {% load widget_tweaks %}
<div class="search-container"> <div class="search-container">
<form id="search" class="input-group" action="" method="get" role="search"> <form id="search" action="" method="get" role="search">
<input type="search" class="form-input" name="q" placeholder="Search for words or #tags" <input type="search" class="form-input" name="q" placeholder="Search for words or #tags"
value="{{ search.q }}"> value="{{ search.q }}">
<input type="submit" value="Search" class="btn input-group-btn"> <input type="submit" value="Search" class="d-none">
{% for hidden_field in search_form.hidden_fields %} {% for hidden_field in search_form.hidden_fields %}
{{ hidden_field }} {{ hidden_field }}
{% endfor %} {% endfor %}

View file

@ -73,7 +73,7 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Bookmark actions</label> <label class="form-label">Bookmark actions</label>
<label for="{{ form.display_view_bookmark_action.id_for_label }}" class="form-checkbox"> <label for="{{ form.display_view_bookmark_action.id_for_label }}" class="form-checkbox">
{{ form.display_view_bookmark_action }} {{ form.display_view_bookmark_action }}
<i class="form-icon"></i> View <i class="form-icon"></i> View
@ -121,9 +121,11 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<details {% if form.auto_tagging_rules.value %}open{% endif %}> <details {% if form.auto_tagging_rules.value %}open{% endif %}>
<summary>Auto Tagging</summary> <summary>
<span class="form-label d-inline-block">Auto Tagging</span>
</summary>
<label for="{{ form.auto_tagging_rules.id_for_label }}" class="text-assistive">Auto Tagging</label> <label for="{{ form.auto_tagging_rules.id_for_label }}" class="text-assistive">Auto Tagging</label>
<div class="mt-2"> <div>
{{ form.auto_tagging_rules|add_class:"form-input monospace"|attr:"rows:6" }} {{ form.auto_tagging_rules|add_class:"form-input monospace"|attr:"rows:6" }}
</div> </div>
</details> </details>
@ -223,9 +225,11 @@ reddit.com/r/Music music reddit</pre>
</div> </div>
<div class="form-group"> <div class="form-group">
<details {% if form.custom_css.value %}open{% endif %}> <details {% if form.custom_css.value %}open{% endif %}>
<summary>Custom CSS</summary> <summary>
<span class="form-label d-inline-block">Custom CSS</span>
</summary>
<label for="{{ form.custom_css.id_for_label }}" class="text-assistive">Custom CSS</label> <label for="{{ form.custom_css.id_for_label }}" class="text-assistive">Custom CSS</label>
<div class="mt-2"> <div>
{{ form.custom_css|add_class:"form-input monospace"|attr:"rows:6" }} {{ form.custom_css|add_class:"form-input monospace"|attr:"rows:6" }}
</div> </div>
</details> </details>
@ -234,7 +238,7 @@ reddit.com/r/Music music reddit</pre>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="submit" name="update_profile" value="Save" class="btn btn-primary mt-2"> <input type="submit" name="update_profile" value="Save" class="btn btn-primary btn-wide mt-2">
</div> </div>
</form> </form>
</section> </section>
@ -264,7 +268,7 @@ reddit.com/r/Music music reddit</pre>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="submit" name="update_global_settings" value="Save" class="btn btn-primary mt-2"> <input type="submit" name="update_global_settings" value="Save" class="btn btn-primary btn-wide mt-2">
</div> </div>
</form> </form>
</section> </section>

View file

@ -35,10 +35,8 @@
<h2>REST API</h2> <h2>REST API</h2>
<p>The following token can be used to authenticate 3rd-party applications against the REST API:</p> <p>The following token can be used to authenticate 3rd-party applications against the REST API:</p>
<div class="form-group"> <div class="form-group">
<div class="columns"> <div class="width-50 width-md-100">
<div class="column width-50 width-md-100"> <input class="form-input" value="{{ api_token }}" readonly>
<input class="form-input" value="{{ api_token }}" readonly>
</div>
</div> </div>
</div> </div>
<p> <p>
@ -57,7 +55,7 @@
<li><a href="{{ all_feed_url }}">All bookmarks</a></li> <li><a href="{{ all_feed_url }}">All bookmarks</a></li>
<li><a href="{{ unread_feed_url }}">Unread bookmarks</a></li> <li><a href="{{ unread_feed_url }}">Unread bookmarks</a></li>
<li><a href="{{ shared_feed_url }}">Shared bookmarks</a></li> <li><a href="{{ shared_feed_url }}">Shared bookmarks</a></li>
<li><a href="{{ public_shared_feed_url }}">Public shared bookmarks</a><br><span class="text-small text-gray">The public shared feed does not contain an authentication token and can be shared with other people. Only shows shared bookmarks from users who have explicitly enabled public sharing.</span> <li><a href="{{ public_shared_feed_url }}">Public shared bookmarks</a><br><span class="text-small text-secondary">The public shared feed does not contain an authentication token and can be shared with other people. Only shows shared bookmarks from users who have explicitly enabled public sharing.</span>
</li> </li>
</ul> </ul>
<p> <p>

View file

@ -10,7 +10,7 @@
<a href="{% url 'bookmarks:settings.integrations' %}">Integrations</a> <a href="{% url 'bookmarks:settings.integrations' %}">Integrations</a>
</li> </li>
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
<li class="tab-item tooltip tooltip-bottom" data-tooltip="The admin panel provides additional features &#010; such as user management and bulk operations."> <li class="tab-item">
<a href="{% url 'admin:index' %}" target="_blank"> <a href="{% url 'admin:index' %}" target="_blank">
<span>Admin</span> <span>Admin</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="ml-1" style="width: 1.2em; height: 1.2em; vertical-align: -0.2em;"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="ml-1" style="width: 1.2em; height: 1.2em; vertical-align: -0.2em;">

View file

@ -115,9 +115,6 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
</summary> </summary>
<label for="id_notes" class="text-assistive">Notes</label> <label for="id_notes" class="text-assistive">Notes</label>
<textarea name="notes" cols="40" rows="8" class="form-input" id="id_notes">**Find** more info [here](http://example.com)</textarea> <textarea name="notes" cols="40" rows="8" class="form-input" id="id_notes">**Find** more info [here](http://example.com)</textarea>
<div class="form-input-hint">
Additional notes, supports Markdown.
</div>
</details> </details>
""", """,
html, html,

View file

@ -44,7 +44,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
f""" f"""
<a href="{url}" <a href="{url}"
title="Show snapshot on the Internet Archive Wayback Machine" target="{link_target}" rel="noopener"> title="Show snapshot on the Internet Archive Wayback Machine" target="{link_target}" rel="noopener">
{label_content} {label_content}
</a> </a>
<span>|</span> <span>|</span>
""", """,
@ -203,7 +203,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
def assertNotes(self, html: str, notes_html: str, count=1): def assertNotes(self, html: str, notes_html: str, count=1):
self.assertInHTML( self.assertInHTML(
f""" f"""
<div class="notes bg-gray text-gray-dark"> <div class="notes">
<div class="markdown"> <div class="markdown">
{notes_html} {notes_html}
</div> </div>

View file

@ -18,7 +18,7 @@ class NavMenuTestCase(TestCase, BookmarkFactoryMixin):
self.assertInHTML( self.assertInHTML(
f""" f"""
<a href="{reverse('bookmarks:shared')}" class="btn btn-link">Shared</a> <a href="{reverse('bookmarks:shared')}" class="menu-link">Shared</a>
""", """,
html, html,
count=0, count=0,
@ -31,7 +31,7 @@ class NavMenuTestCase(TestCase, BookmarkFactoryMixin):
self.assertInHTML( self.assertInHTML(
f""" f"""
<a href="{reverse('bookmarks:shared')}" class="btn btn-link">Shared</a> <a href="{reverse('bookmarks:shared')}" class="menu-link">Shared</a>
""", """,
html, html,
count=2, count=2,

View file

@ -383,13 +383,13 @@ class BookmarkAssetItem:
icon_classes = [] icon_classes = []
text_classes = [] text_classes = []
if asset.status == BookmarkAsset.STATUS_PENDING: if asset.status == BookmarkAsset.STATUS_PENDING:
icon_classes.append("text-gray") icon_classes.append("text-tertiary")
text_classes.append("text-gray") text_classes.append("text-tertiary")
elif asset.status == BookmarkAsset.STATUS_FAILURE: elif asset.status == BookmarkAsset.STATUS_FAILURE:
icon_classes.append("text-error") icon_classes.append("text-error")
text_classes.append("text-error") text_classes.append("text-error")
else: else:
icon_classes.append("text-primary") icon_classes.append("icon-color")
self.icon_classes = " ".join(icon_classes) self.icon_classes = " ".join(icon_classes)
self.text_classes = " ".join(text_classes) self.text_classes = " ".join(text_classes)

View file

@ -1,10 +1,11 @@
FROM node:18-alpine AS node-build FROM node:18-alpine AS node-build
WORKDIR /etc/linkding WORKDIR /etc/linkding
# install build dependencies # install build dependencies
COPY rollup.config.mjs package.json package-lock.json ./ COPY rollup.config.mjs postcss.config.js package.json package-lock.json ./
RUN npm ci RUN npm ci
# copy files needed for JS build # copy files needed for JS build
COPY bookmarks/frontend ./bookmarks/frontend COPY bookmarks/frontend ./bookmarks/frontend
COPY bookmarks/styles ./bookmarks/styles
# run build # run build
RUN npm run build RUN npm run build
@ -23,18 +24,15 @@ WORKDIR /etc/linkding
FROM python-base AS python-build FROM python-base AS python-build
# install build dependencies # install build dependencies
COPY requirements.txt requirements.txt COPY requirements.txt requirements.txt
COPY requirements.dev.txt requirements.dev.txt RUN pip install -U pip && pip install -r requirements.txt
# remove playwright from requirements as there is not always a distro and it's not needed for the build
RUN sed -i '/playwright/d' requirements.dev.txt
RUN pip install -U pip && pip install -r requirements.txt -r requirements.dev.txt
# copy files needed for Django build # copy files needed for Django build
COPY . . COPY . .
COPY --from=node-build /etc/linkding . COPY --from=node-build /etc/linkding .
# remove style sources
RUN rm -rf bookmarks/styles
# run Django part of the build # run Django part of the build
RUN mkdir data && \ RUN mkdir data && \
python manage.py compilescss && \ python manage.py collectstatic
python manage.py collectstatic --ignore=*.scss && \
python manage.py compilescss --delete-files
FROM python-base AS prod-deps FROM python-base AS prod-deps

View file

@ -1,10 +1,11 @@
FROM node:18-alpine AS node-build FROM node:18-alpine AS node-build
WORKDIR /etc/linkding WORKDIR /etc/linkding
# install build dependencies # install build dependencies
COPY rollup.config.mjs package.json package-lock.json ./ COPY rollup.config.mjs postcss.config.js package.json package-lock.json ./
RUN npm ci RUN npm ci
# copy files needed for JS build # copy files needed for JS build
COPY bookmarks/frontend ./bookmarks/frontend COPY bookmarks/frontend ./bookmarks/frontend
COPY bookmarks/styles ./bookmarks/styles
# run build # run build
RUN npm run build RUN npm run build
@ -25,18 +26,15 @@ WORKDIR /etc/linkding
FROM python-base AS python-build FROM python-base AS python-build
# install build dependencies # install build dependencies
COPY requirements.txt requirements.txt COPY requirements.txt requirements.txt
COPY requirements.dev.txt requirements.dev.txt RUN pip install -U pip && pip install -r requirements.txt
# remove playwright from requirements as there is not always a distro and it's not needed for the build
RUN sed -i '/playwright/d' requirements.dev.txt
RUN pip install -U pip && pip install -r requirements.txt -r requirements.dev.txt
# copy files needed for Django build # copy files needed for Django build
COPY . . COPY . .
COPY --from=node-build /etc/linkding . COPY --from=node-build /etc/linkding .
# remove style sources
RUN rm -rf bookmarks/styles
# run Django part of the build # run Django part of the build
RUN mkdir data && \ RUN mkdir data && \
python manage.py compilescss && \ python manage.py collectstatic
python manage.py collectstatic --ignore=*.scss && \
python manage.py compilescss --delete-files
FROM python-base AS prod-deps FROM python-base AS prod-deps

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View file

@ -4,12 +4,12 @@
<g transform="matrix(1.18075,0,0,1.18075,-1265.31,-1395.82)"> <g transform="matrix(1.18075,0,0,1.18075,-1265.31,-1395.82)">
<circle cx="1314.98" cy="1424.52" r="190.496" style="fill:rgb(88,86,224);"/> <circle cx="1314.98" cy="1424.52" r="190.496" style="fill:rgb(88,86,224);"/>
</g> </g>
<g transform="matrix(1,0,0,1,-1017.49,-1140.55)"> <g transform="matrix(0.823127,0,0,0.823127,-786.171,-888.198)">
<g transform="matrix(0.707351,0.706862,-0.706862,0.707351,1331.93,-512.804)"> <g transform="matrix(0.707351,0.706862,-0.706862,0.707351,1331.93,-512.804)">
<path d="M1244.39,1293.95L1244.39,1493.59C1244.39,1493.59 1243.58,1561.48 1319.29,1562.47C1395.27,1563.46 1394.17,1493.59 1394.17,1493.59L1394.17,1293.95" style="fill:none;stroke:white;stroke-width:31.25px;"/> <path d="M1244.39,1293.95L1244.39,1493.59C1244.39,1493.59 1243.58,1561.48 1319.29,1562.47C1395.27,1563.46 1394.17,1493.59 1394.17,1493.59L1394.17,1293.95" style="fill:none;stroke:white;stroke-width:35.43px;"/>
</g> </g>
<g transform="matrix(-0.710067,-0.704134,0.704134,-0.710067,1284.12,3366.41)"> <g transform="matrix(-0.710067,-0.704134,0.704134,-0.710067,1284.12,3366.41)">
<path d="M1244.39,1293.95L1244.39,1493.59C1244.39,1493.59 1243.58,1561.48 1319.29,1562.47C1395.27,1563.46 1394.17,1493.59 1394.17,1493.59L1394.17,1293.95" style="fill:none;stroke:white;stroke-width:31.25px;"/> <path d="M1244.39,1293.95L1244.39,1493.59C1244.39,1493.59 1243.58,1561.48 1319.29,1562.47C1395.27,1563.46 1394.17,1493.59 1394.17,1493.59L1394.17,1293.95" style="fill:none;stroke:white;stroke-width:35.43px;"/>
</g> </g>
</g> </g>
<g transform="matrix(8.26174,0,0,8.26174,-5762.21,-2037.46)"> <g transform="matrix(8.26174,0,0,8.26174,-5762.21,-2037.46)">

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 KiB

After

Width:  |  Height:  |  Size: 447 KiB

1655
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,10 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "rollup -c", "build": "npm run build-js && npm run build-theme-light && npm run build-theme-dark",
"build-js": "rollup -c",
"build-theme-light": "postcss -o bookmarks/static/theme-light.css bookmarks/styles/theme-light.css",
"build-theme-dark": "postcss -o bookmarks/static/theme-dark.css bookmarks/styles/theme-dark.css",
"dev": "rollup -c -w" "dev": "rollup -c -w"
}, },
"repository": { "repository": {
@ -22,6 +25,11 @@
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@rollup/wasm-node": "^4.13.0", "@rollup/wasm-node": "^4.13.0",
"cssnano": "^7.0.6",
"postcss": "^8.4.45",
"postcss-cli": "^11.0.0",
"postcss-import": "^16.1.0",
"postcss-nesting": "^13.0.0",
"rollup-plugin-svelte": "^7.2.0", "rollup-plugin-svelte": "^7.2.0",
"spectre.css": "^0.5.8", "spectre.css": "^0.5.8",
"svelte": "^4.0.0" "svelte": "^4.0.0"

13
postcss.config.js Normal file
View file

@ -0,0 +1,13 @@
const cssnano = require("cssnano");
const postcssImport = require("postcss-import");
const postcssNesting = require("postcss-nesting");
module.exports = {
plugins: [
postcssImport,
postcssNesting,
cssnano({
preset: "default",
}),
],
};

View file

@ -1,8 +1,6 @@
black black
coverage coverage
django-compressor
django-debug-toolbar django-debug-toolbar
libsass
playwright playwright
pytest pytest
pytest-django pytest-django

View file

@ -13,13 +13,7 @@ click==8.1.7
coverage==7.4.1 coverage==7.4.1
# via -r requirements.dev.in # via -r requirements.dev.in
django==5.0.8 django==5.0.8
# via # via django-debug-toolbar
# django-appconf
# django-debug-toolbar
django-appconf==1.0.6
# via django-compressor
django-compressor==4.4
# via -r requirements.dev.in
django-debug-toolbar==4.2.0 django-debug-toolbar==4.2.0
# via -r requirements.dev.in # via -r requirements.dev.in
execnet==2.1.1 execnet==2.1.1
@ -28,8 +22,6 @@ greenlet==3.0.3
# via playwright # via playwright
iniconfig==2.0.0 iniconfig==2.0.0
# via pytest # via pytest
libsass==0.23.0
# via -r requirements.dev.in
mypy-extensions==1.0.0 mypy-extensions==1.0.0
# via black # via black
packaging==23.2 packaging==23.2
@ -55,10 +47,6 @@ pytest-django==4.7.0
# via -r requirements.dev.in # via -r requirements.dev.in
pytest-xdist==3.6.1 pytest-xdist==3.6.1
# via -r requirements.dev.in # via -r requirements.dev.in
rcssmin==1.1.1
# via django-compressor
rjsmin==1.2.1
# via django-compressor
sqlparse==0.5.0 sqlparse==0.5.0
# via # via
# django # django

View file

@ -3,7 +3,6 @@ bleach
bleach-allowlist bleach-allowlist
Django Django
django-registration django-registration
django-sass-processor
django-widget-tweaks django-widget-tweaks
djangorestframework djangorestframework
huey huey

View file

@ -35,8 +35,6 @@ django==5.0.8
# mozilla-django-oidc # mozilla-django-oidc
django-registration==3.4 django-registration==3.4
# via -r requirements.in # via -r requirements.in
django-sass-processor==1.4
# via -r requirements.in
django-widget-tweaks==1.5.0 django-widget-tweaks==1.5.0
# via -r requirements.in # via -r requirements.in
djangorestframework==3.15.2 djangorestframework==3.15.2

View file

@ -6,9 +6,7 @@ playwright install chromium
# Test server loads assets from static folder, so make sure files there are up-to-date # Test server loads assets from static folder, so make sure files there are up-to-date
rm -rf static rm -rf static
npm run build npm run build
python manage.py compilescss python manage.py collectstatic
python manage.py collectstatic --ignore=*.scss
python manage.py compilescss --delete-files
# Run E2E tests # Run E2E tests
python manage.py test bookmarks.e2e --pattern="e2e_test_*.py" python manage.py test bookmarks.e2e --pattern="e2e_test_*.py"

View file

@ -39,7 +39,6 @@ INSTALLED_APPS = [
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"sass_processor",
"widget_tweaks", "widget_tweaks",
"rest_framework", "rest_framework",
"rest_framework.authtoken", "rest_framework.authtoken",
@ -129,19 +128,10 @@ STATIC_URL = "/" + LD_CONTEXT_PATH + "static/"
# Collect static files in static folder # Collect static files in static folder
STATIC_ROOT = os.path.join(BASE_DIR, "static") STATIC_ROOT = os.path.join(BASE_DIR, "static")
# Turn off SASS compilation by default
SASS_PROCESSOR_ENABLED = False
# Add SASS preprocessor finder to resolve generated CSS
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
"sass_processor.finders.CssFinder",
]
# Enable SASS processor to find custom folder for SCSS sources through static file finders
STATICFILES_DIRS = [ STATICFILES_DIRS = [
# Resolve theme files from style source folder
os.path.join(BASE_DIR, "bookmarks", "styles"), os.path.join(BASE_DIR, "bookmarks", "styles"),
# Resolve downloaded files in dev environment
os.path.join(BASE_DIR, "data", "favicons"), os.path.join(BASE_DIR, "data", "favicons"),
os.path.join(BASE_DIR, "data", "previews"), os.path.join(BASE_DIR, "data", "previews"),
] ]

View file

@ -8,8 +8,6 @@ from .base import *
# Turn on debug mode # Turn on debug mode
DEBUG = True DEBUG = True
# Turn on SASS compilation
SASS_PROCESSOR_ENABLED = True
# Enable debug toolbar # Enable debug toolbar
INSTALLED_APPS.append("debug_toolbar") INSTALLED_APPS.append("debug_toolbar")

View file

@ -11,8 +11,6 @@ from .base import *
# Turn of debug mode # Turn of debug mode
DEBUG = False DEBUG = False
# Turn off SASS compilation
SASS_PROCESSOR_ENABLED = False
# Try read secret key from file # Try read secret key from file
try: try: