Add comments to Sass variables and tweak main window (#2137)

* Prevent multiple inclusion of variables in CSS files

* Use dict instead of tuple for variables

* Add comments to variables

* Improve appearance of main window

* Tweak main window styles

* Use json.dumps over pprint.format

* Make study button primary

* Improve header margin

* Make bottom toolbar slimmer

* Make congrats page more balanced

* Fix type issue

* Replace day/night with light/dark

* Exclude top-level-drag-row from hover effect

* Create dataclass for variables

* Run formatter

* Apply CSS variables from Python side

Why go full-circle with the Sass variables? This way we only need one interface for add-on authors to interact with. It also makes it easier for us to apply additional themes in the future.

* Fix typing

* Fix rgba values in Qt

* Darken button background

* Fix palette not being applied in light theme

For some odd reason this problem arose much later than #2016.

* Tweak default button look

* Reformat

* Apply CSS vars to ts pages

* Include elevation in button_mixins_lib

* Cast opacity to int

* Add some margin to studiedToday info

* Tweak light theme button gradient

* Tweak highlight-bg for light theme

* Add back default button color

as it made the browser sidebar tool icons dark in light theme.

* Reformat

* Tweak light theme buttons once more

Sorry for the back-and-forth. Sass only compiles when there are changes in user files, not when I only change the vars.

* Fix bottom toolbar button indicators

* Make buttons more clicky

* Fix button padding

* Handle macOS separately again

* Decrease elevation effect for main window buttons to 1

* Imitate box-shadow for Qt elements

* Adjust shadow vars

* Adjust primary border color

because the save button in the deck options had a lighter color than its background gradient.

* Boost box-shadow color of primary buttons

* Format

* Adjust Qt box-shadow imitation and shadow colors

* Use more subtle default shadow color

* Add some more padding to top toolbar

* Revert "Apply CSS vars to ts pages"

This reverts commit 5d8e7f6b7f.

* Revert "Apply CSS variables from Python side"

This reverts commit 87db774412.

* Better match the standard macOS buttons

In the dark theme the standard color is a lighter grey, but at least
the size/shape is similar again.

This doesn't work for the editor buttons.

* Reduce the top margin of the congrats screen

* Fix illegible buttons when changing theme on macOS; match dark button style
This commit is contained in:
Matthias Metelka 2022-10-29 02:48:53 +02:00 committed by GitHub
parent 95d0c78b78
commit 0c340c4f74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 691 additions and 405 deletions

View file

@ -14,7 +14,7 @@ genrule(
genrule( genrule(
name = "extract_sass_vars", name = "extract_sass_vars",
srcs = [ srcs = [
"//sass:_vars.css", "//sass:_root-vars.css",
], ],
outs = [ outs = [
"colors.py", "colors.py",

View file

@ -49,7 +49,7 @@ class CellRow:
) -> None: ) -> None:
self.refreshed_at: float = time.time() self.refreshed_at: float = time.time()
self.cells: tuple[Cell, ...] = tuple(Cell(*cell) for cell in cells) self.cells: tuple[Cell, ...] = tuple(Cell(*cell) for cell in cells)
self.color: tuple[str, str] | None = backend_color_to_aqt_color(color) self.color: dict[str, str] | None = backend_color_to_aqt_color(color)
self.font_name: str = font_name or "arial" self.font_name: str = font_name or "arial"
self.font_size: int = font_size if font_size > 0 else 12 self.font_size: int = font_size if font_size > 0 else 12
@ -76,7 +76,7 @@ class CellRow:
return row return row
def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> tuple[str, str] | None: def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> dict[str, str] | None:
temp_color = None temp_color = None
if color == BrowserRow.COLOR_MARKED: if color == BrowserRow.COLOR_MARKED:
@ -101,12 +101,11 @@ def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> tuple[str, str] | N
return adjusted_bg_color(temp_color) return adjusted_bg_color(temp_color)
def adjusted_bg_color(color: tuple[str, str]) -> tuple[str, str]: def adjusted_bg_color(color: dict[str, str]) -> dict[str, str]:
if color: if color:
return ( color["light"] = QColor(color["light"]).lighter(150).name()
QColor(color[0]).lighter(150).name(), color["dark"] = QColor(color["dark"]).darker(150).name()
QColor(color[1]).darker(150).name(), return color
)
else: else:
return None return None

View file

@ -37,11 +37,13 @@ with open(input_svg, "r") as f:
elif f"{prefix}-dark.svg" in path: elif f"{prefix}-dark.svg" in path:
dark_svg = path dark_svg = path
for (idx, filename) in enumerate((light_svg, dark_svg)): def substitute(data: str, filename: str, mode: str) -> None:
data = svg_data
if "fill" in data: if "fill" in data:
data = re.sub(r"fill=\"#.+?\"", f'fill="{color[idx]}"', data) data = re.sub(r"fill=\"#.+?\"", f'fill="{color[mode]}"', data)
else: else:
data = re.sub(r"<svg", f'<svg fill="{color[idx]}"', data, 1) data = re.sub(r"<svg", f'<svg fill="{color[mode]}"', data, 1)
with open(filename, "w") as f: with open(filename, "w") as f:
f.write(data) f.write(data)
substitute(svg_data, light_svg, "light")
substitute(svg_data, dark_svg, "dark")

View file

@ -9,7 +9,7 @@ compile_sass(
group = "css_local", group = "css_local",
visibility = ["//visibility:private"], visibility = ["//visibility:private"],
deps = [ deps = [
"//sass:vars_lib", "//sass:base_lib",
"//sass:buttons_lib", "//sass:buttons_lib",
"//sass:card_counts_lib", "//sass:card_counts_lib",
"//sass:scrollbar_lib", "//sass:scrollbar_lib",

View file

@ -1,11 +1,11 @@
/* Copyright: Ankitects Pty Ltd and contributors /* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
@use "root-vars";
@use "sass/vars"; @use "sass/vars" as *;
@use "sass/card-counts"; @use "sass/card-counts";
a.deck { a.deck {
color: var(--fg); color: color(fg);
text-decoration: none; text-decoration: none;
min-width: 5em; min-width: 5em;
display: inline-block; display: inline-block;
@ -15,8 +15,13 @@ a.deck:hover {
text-decoration: underline; text-decoration: underline;
} }
th {
border-bottom: 1px solid color(border-subtle);
padding-bottom: 5px;
}
tr.deck td { tr.deck td {
border-bottom: 1px solid var(--border-faint); padding: 5px 12px;
} }
tr.top-level-drag-row td { tr.top-level-drag-row td {
@ -28,16 +33,30 @@ td {
} }
tr.drag-hover td { tr.drag-hover td {
border-bottom: 1px solid var(--border); border-bottom: 1px solid color(border);
} }
body { body {
margin: 1em; margin: 2em 1em 1em 1em;
-webkit-user-select: none; -webkit-user-select: none;
} }
.current { .current,
background-color: var(--shadow-subtle); tr:hover:not(.top-level-drag-row) {
td {
background: color(canvas-inset);
&:first-child {
border-top-left-radius: prop(border-radius-large);
border-bottom-left-radius: prop(border-radius-large);
}
&:last-child {
border-top-right-radius: prop(border-radius-large);
border-bottom-right-radius: prop(border-radius-large);
}
.gears {
visibility: visible;
}
}
} }
.decktd { .decktd {
@ -56,14 +75,14 @@ body {
} }
.collapse { .collapse {
color: var(--fg); color: color(fg);
text-decoration: none; text-decoration: none;
display: inline-block; display: inline-block;
width: 1em; width: 1em;
} }
.filtered { .filtered {
color: var(--accent-link) !important; color: color(fg-link) !important;
} }
.gears { .gears {
@ -72,6 +91,7 @@ body {
opacity: 0.5; opacity: 0.5;
padding-top: 0.2em; padding-top: 0.2em;
cursor: pointer; cursor: pointer;
visibility: hidden;
} }
.nightMode { .nightMode {
@ -81,7 +101,7 @@ body {
} }
.callout { .callout {
background: var(--border); background: color(border);
padding: 1em; padding: 1em;
margin: 1em; margin: 1em;
@ -89,3 +109,7 @@ body {
margin: 1em; margin: 1em;
} }
} }
#studiedToday {
margin: 2em 0;
}

View file

@ -1,7 +1,10 @@
/* Copyright: Ankitects Pty Ltd and contributors /* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
@use "root-vars";
@use "sass/vars" as *;
@use "sass/card-counts"; @use "sass/card-counts";
@use "sass/button-mixins" as button;
.smallLink { .smallLink {
font-size: 10px; font-size: 10px;
@ -13,7 +16,7 @@ h3 {
.descfont { .descfont {
padding: 1em; padding: 1em;
color: var(--fg-subtle); color: color(fg-subtle);
} }
.description { .description {
@ -33,3 +36,7 @@ h3 {
.dyn { .dyn {
text-align: center; text-align: center;
} }
#study {
@include button.base($primary: true);
}

View file

@ -1,11 +1,12 @@
/* Copyright: Ankitects Pty Ltd and contributors /* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
@use "vars"; @use "root-vars";
@use "sass/vars" as *;
@use "sass/card-counts"; @use "sass/card-counts";
:root { :root {
--focus-color: #{vars.palette-of(button-focus)}; --focus-color: #{palette-of(border-focus)};
.isMac { .isMac {
--focus-color: rgba(0 103 244 / 0.247); --focus-color: rgba(0 103 244 / 0.247);
@ -17,6 +18,15 @@ body {
padding: 0; padding: 0;
} }
#innertable {
padding-top: 10px;
}
#middle td[align="center"] {
padding-top: 10px;
position: relative;
}
button { button {
min-width: 60px; min-width: 60px;
white-space: nowrap; white-space: nowrap;
@ -28,7 +38,7 @@ button {
} }
.stat { .stat {
padding-top: 5px; padding-top: 10px;
@media (max-width: 583px) { @media (max-width: 583px) {
display: none; display: none;
@ -36,13 +46,11 @@ button {
} }
.stat2 { .stat2 {
padding-top: 3px; padding-top: 10px;
font-weight: normal; font-weight: normal;
} }
.stattxt { .stattxt {
padding-left: 5px;
padding-right: 5px;
white-space: nowrap; white-space: nowrap;
} }
@ -51,18 +59,17 @@ button {
} }
:focus { :focus {
outline: 1px auto var(--focus-color); border-color: color(border-focus);
.nightMode & {
outline: none;
box-shadow: 0 0 0 2px var(--focus-color);
}
} }
.nobold { .nobold,
.stattxt {
position: absolute;
top: -5px;
left: 50%;
transform: translateX(-50%);
font-weight: normal; font-weight: normal;
display: inline-block; display: inline-block;
padding-top: 4px;
} }
.spacer { .spacer {
@ -74,13 +81,13 @@ button {
} }
#outer { #outer {
border-top: 1px solid var(--border); border-top: 1px solid color(border);
/* Better compatibility with graphics pad/touchscreen */ /* Better compatibility with graphics pad/touchscreen */
-webkit-user-select: none; -webkit-user-select: none;
} }
.nightMode { .nightMode {
#outer { #outer {
border-top-color: var(--border-subtle); border-top-color: color(border-subtle);
} }
} }

View file

@ -1,14 +1,23 @@
/* Copyright: Ankitects Pty Ltd and contributors /* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
@use "root-vars";
@use "sass/vars" as *;
@use "sass/elevation";
@use "sass/button-mixins" as button;
#header { #header {
padding: 3px; padding-bottom: 4px;
font-weight: bold;
border-bottom: 1px solid var(--border);
} }
.tdcenter { .tdcenter {
white-space: nowrap; white-space: nowrap;
border-radius: prop(border-radius);
border-bottom-left-radius: prop(border-radius-large);
border-bottom-right-radius: prop(border-radius-large);
@include button.base($with-hover: false);
@include elevation.elevation(2);
overflow: hidden;
} }
body { body {
@ -23,28 +32,25 @@ body {
} }
.hitem { .hitem {
padding-right: 12px; font-weight: bold;
padding-left: 12px; padding: 8px 14px;
text-decoration: none; text-decoration: none;
color: var(--fg); color: color(fg);
display: inline-block;
@include button.base($elevation: 0);
border: none;
&:first-child {
padding-left: 18px;
}
&:last-child {
padding-right: 18px;
} }
.hitem:hover {
text-decoration: underline;
} }
.hitem:focus { .hitem:focus {
outline: 0; outline: 0;
} }
.nightMode #header {
border-bottom-color: var(--border-subtle);
}
.isMac.nightMode #header {
border-bottom-color: var(--canvas-elevated);
}
@keyframes spin { @keyframes spin {
0% { 0% {
-webkit-transform: rotate(0deg); -webkit-transform: rotate(0deg);
@ -55,25 +61,27 @@ body {
} }
.spin { .spin {
width: 16px;
animation: spin; animation: spin;
animation-duration: 2s; animation-duration: 2s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
display: inline-block; display: inline-block;
visibility: visible !important; visibility: visible !important;
animation-timing-function: linear; animation-timing-function: linear;
transition: all 0.2s ease-in;
} }
#sync-spinner { #sync-spinner {
width: 16px;
height: 16px; height: 16px;
margin-bottom: -3px; margin-bottom: -3px;
visibility: hidden; visibility: hidden;
width: 0;
} }
.normal-sync { .normal-sync {
color: var(--state-new); color: color(state-new);
} }
.full-sync { .full-sync {
color: var(--state-learn); color: color(state-learn);
} }

View file

@ -24,7 +24,7 @@ body {
} }
a { a {
color: var(--accent-link); color: var(--fg-link);
text-decoration: none; text-decoration: none;
} }

View file

@ -84,7 +84,7 @@ class FilteredDeckConfigDialog(QDialog):
qconnect(self.form.search_button.clicked, self.on_search_button) qconnect(self.form.search_button.clicked, self.on_search_button)
qconnect(self.form.search_button_2.clicked, self.on_search_button_2) qconnect(self.form.search_button_2.clicked, self.on_search_button_2)
qconnect(self.form.hint_button.clicked, self.on_hint_button) qconnect(self.form.hint_button.clicked, self.on_hint_button)
blue = theme_manager.var(colors.ACCENT_LINK) blue = theme_manager.var(colors.FG_LINK)
grey = theme_manager.var(colors.FG_DISABLED) grey = theme_manager.var(colors.FG_DISABLED)
self.setStyleSheet( self.setStyleSheet(
f"""QPushButton[label] {{ padding: 0; border: 0 }} f"""QPushButton[label] {{ padding: 0; border: 0 }}

View file

@ -663,12 +663,11 @@ class Reviewer:
<table id=innertable width=100%% cellspacing=0 cellpadding=0> <table id=innertable width=100%% cellspacing=0 cellpadding=0>
<tr> <tr>
<td align=left width=50 valign=top class=stat> <td align=left width=50 valign=top class=stat>
<br>
<button title="%(editkey)s" onclick="pycmd('edit');">%(edit)s</button></td> <button title="%(editkey)s" onclick="pycmd('edit');">%(edit)s</button></td>
<td align=center valign=top id=middle> <td align=center valign=top id=middle>
</td> </td>
<td width=50 align=right valign=top class=stat><span id=time class=stattxt> <td width=50 align=right valign=top class=stat><span id=time class=stattxt>
</span><br> </span>
<button onclick="pycmd('more');">%(more)s %(downArrow)s</button> <button onclick="pycmd('more');">%(more)s %(downArrow)s</button>
</td> </td>
</tr> </tr>
@ -688,7 +687,7 @@ time = %(time)d;
def _showAnswerButton(self) -> None: def _showAnswerButton(self) -> None:
middle = """ middle = """
<span class=stattxt>{}</span><br> <span class=stattxt>{}</span>
<button title="{}" id="ansbut" onclick='pycmd("ans");'>{}</button>""".format( <button title="{}" id="ansbut" onclick='pycmd("ans");'>{}</button>""".format(
self._remaining(), self._remaining(),
tr.actions_shortcut_key(val=tr.studying_space()), tr.actions_shortcut_key(val=tr.studying_space()),
@ -805,7 +804,7 @@ time = %(time)d;
txt = v3_labels[i - 1] txt = v3_labels[i - 1]
else: else:
txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or "&nbsp;" txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or "&nbsp;"
return f"<span class=nobold>{txt}</span><br>" return f"<span class=nobold>{txt}</span>"
# Leeches # Leeches
########################################################################## ##########################################################################

View file

@ -4,12 +4,13 @@ from aqt import colors, props
from aqt.theme import ThemeManager from aqt.theme import ThemeManager
def button_gradient(start: str, end: str) -> str: def button_gradient(start: str, end: str, shadow: str) -> str:
return f""" return f"""
qlineargradient( qlineargradient(
spread:pad, x1:0.5, y1:0, x2:0.5, y2:1.25, spread:pad, x1:0.5, y1:0, x2:0.5, y2:1,
stop:0 {start}, stop:0 {start},
stop:1 {end} stop:0.94 {end}
stop:1 {shadow}
); );
""" """
@ -62,7 +63,7 @@ QSpinBox {{
def menu_styles(tm: ThemeManager) -> str: def menu_styles(tm: ThemeManager) -> str:
return f""" return f"""
QMenuBar {{ QMenuBar {{
border-bottom: 1px solid {tm.var(colors.BORDER_FAINT)}; border-bottom: 1px solid {tm.var(colors.BORDER_SUBTLE)};
}} }}
QMenuBar::item {{ QMenuBar::item {{
background-color: transparent; background-color: transparent;
@ -111,9 +112,11 @@ QComboBox:!editable {{
background: { background: {
button_gradient( button_gradient(
tm.var(colors.BUTTON_GRADIENT_START), tm.var(colors.BUTTON_GRADIENT_START),
tm.var(colors.BUTTON_GRADIENT_END) tm.var(colors.BUTTON_GRADIENT_END),
tm.var(colors.SHADOW)
) )
}; };
border-bottom: 1px solid {tm.var(colors.SHADOW)};
}} }}
QPushButton:hover, QPushButton:hover,
QTabBar::tab:hover, QTabBar::tab:hover,
@ -121,18 +124,19 @@ QComboBox:!editable:hover {{
background: { background: {
button_gradient( button_gradient(
tm.var(colors.BUTTON_HOVER_GRADIENT_START), tm.var(colors.BUTTON_HOVER_GRADIENT_START),
tm.var(colors.BUTTON_HOVER_GRADIENT_END) tm.var(colors.BUTTON_HOVER_GRADIENT_END),
tm.var(colors.SHADOW)
) )
}; };
}} }}
QPushButton:pressed, QPushButton:pressed,
QComboBox:!editable:pressed {{ QComboBox:!editable:pressed {{
border: 1px solid {tm.var(colors.BUTTON_PRESSED_BORDER)}; border: 1px solid {tm.var(colors.BORDER_STRONG)};
background: { background: {
button_pressed_gradient( button_pressed_gradient(
tm.var(colors.BUTTON_GRADIENT_START), tm.var(colors.BUTTON_GRADIENT_START),
tm.var(colors.BUTTON_GRADIENT_END), tm.var(colors.BUTTON_GRADIENT_END),
tm.var(colors.BUTTON_PRESSED_SHADOW) tm.var(colors.SHADOW)
) )
}; };
}} }}
@ -190,7 +194,7 @@ QComboBox::drop-down {{
padding: 2px; padding: 2px;
width: 16px; width: 16px;
subcontrol-position: top right; subcontrol-position: top right;
border: 1px solid {tm.var(colors.BUTTON_BORDER)}; border: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-top-{tm.right()}-radius: {tm.var(props.BORDER_RADIUS)}; border-top-{tm.right()}-radius: {tm.var(props.BORDER_RADIUS)};
border-bottom-{tm.right()}-radius: {tm.var(props.BORDER_RADIUS)}; border-bottom-{tm.right()}-radius: {tm.var(props.BORDER_RADIUS)};
}} }}
@ -201,17 +205,21 @@ QComboBox::drop-down {{
background: { background: {
button_gradient( button_gradient(
tm.var(colors.BUTTON_GRADIENT_START), tm.var(colors.BUTTON_GRADIENT_START),
tm.var(colors.BUTTON_GRADIENT_END) tm.var(colors.BUTTON_GRADIENT_END),
tm.var(colors.SHADOW)
) )
}; };
border-bottom: 1px solid {tm.var(colors.SHADOW)};
}} }}
QComboBox::drop-down:hover {{ QComboBox::drop-down:hover {{
background: { background: {
button_gradient( button_gradient(
tm.var(colors.BUTTON_HOVER_GRADIENT_START), tm.var(colors.BUTTON_HOVER_GRADIENT_START),
tm.var(colors.BUTTON_HOVER_GRADIENT_END) tm.var(colors.BUTTON_HOVER_GRADIENT_END),
tm.var(colors.SHADOW)
) )
}; };
border-bottom: 1px solid {tm.var(colors.SHADOW)};
}} }}
""" """
@ -239,6 +247,7 @@ QTabBar::tab {{
}} }}
QTabBar::tab {{ QTabBar::tab {{
border: 1px solid {tm.var(colors.BORDER_SUBTLE)}; border: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-bottom-color: {tm.var(colors.SHADOW)};
}} }}
QTabBar::tab:first {{ QTabBar::tab:first {{
border-top-{tm.left()}-radius: {tm.var(props.BORDER_RADIUS)}; border-top-{tm.left()}-radius: {tm.var(props.BORDER_RADIUS)};
@ -256,7 +265,8 @@ QTabBar::tab:selected {{
background: { background: {
button_gradient( button_gradient(
tm.var(colors.BUTTON_PRIMARY_GRADIENT_START), tm.var(colors.BUTTON_PRIMARY_GRADIENT_START),
tm.var(colors.BUTTON_PRIMARY_GRADIENT_END) tm.var(colors.BUTTON_PRIMARY_GRADIENT_END),
tm.var(colors.SHADOW)
) )
}; };
}} }}
@ -268,8 +278,8 @@ def table_styles(tm: ThemeManager) -> str:
QTableView {{ QTableView {{
border-radius: {tm.var(props.BORDER_RADIUS)}; border-radius: {tm.var(props.BORDER_RADIUS)};
gridline-color: {tm.var(colors.BORDER_SUBTLE)}; gridline-color: {tm.var(colors.BORDER_SUBTLE)};
selection-background-color: {tm.var(colors.SELECTION_BG)}; selection-background-color: {tm.var(colors.SELECTED_BG)};
selection-color: {tm.var(colors.SELECTION_FG)}; selection-color: {tm.var(colors.SELECTED_FG)};
}} }}
QHeaderView {{ QHeaderView {{
background: {tm.var(colors.CANVAS)}; background: {tm.var(colors.CANVAS)};
@ -279,18 +289,19 @@ QHeaderView::section {{
background: { background: {
button_gradient( button_gradient(
tm.var(colors.BUTTON_GRADIENT_START), tm.var(colors.BUTTON_GRADIENT_START),
tm.var(colors.BUTTON_GRADIENT_END) tm.var(colors.BUTTON_GRADIENT_END),
tm.var(colors.SHADOW)
) )
}; };
}} }}
QHeaderView::section:pressed, QHeaderView::section:pressed,
QHeaderView::section:pressed:!first {{ QHeaderView::section:pressed:!first {{
border: 1px solid {tm.var(colors.BUTTON_PRESSED_BORDER)}; border: 1px solid {tm.var(colors.BORDER_STRONG)};
background: { background: {
button_pressed_gradient( button_pressed_gradient(
tm.var(colors.BUTTON_GRADIENT_START), tm.var(colors.BUTTON_GRADIENT_START),
tm.var(colors.BUTTON_GRADIENT_END), tm.var(colors.BUTTON_GRADIENT_END),
tm.var(colors.BUTTON_PRESSED_SHADOW) tm.var(colors.SHADOW)
) )
} }
}} }}
@ -298,7 +309,8 @@ QHeaderView::section:hover {{
background: { background: {
button_gradient( button_gradient(
tm.var(colors.BUTTON_HOVER_GRADIENT_START), tm.var(colors.BUTTON_HOVER_GRADIENT_START),
tm.var(colors.BUTTON_HOVER_GRADIENT_END) tm.var(colors.BUTTON_HOVER_GRADIENT_END),
tm.var(colors.SHADOW)
) )
}; };
}} }}
@ -339,22 +351,23 @@ QSpinBox::up-button,
QSpinBox::down-button {{ QSpinBox::down-button {{
subcontrol-origin: border; subcontrol-origin: border;
width: 16px; width: 16px;
border: 1px solid {tm.var(colors.BUTTON_BORDER)}; border: 1px solid {tm.var(colors.BORDER_SUBTLE)};
background: { background: {
button_gradient( button_gradient(
tm.var(colors.BUTTON_GRADIENT_START), tm.var(colors.BUTTON_GRADIENT_START),
tm.var(colors.BUTTON_GRADIENT_END) tm.var(colors.BUTTON_GRADIENT_END),
tm.var(colors.SHADOW)
) )
}; };
}} }}
QSpinBox::up-button:pressed, QSpinBox::up-button:pressed,
QSpinBox::down-button:pressed {{ QSpinBox::down-button:pressed {{
border: 1px solid {tm.var(colors.BUTTON_PRESSED_BORDER)}; border: 1px solid {tm.var(colors.BORDER_STRONG)};
background: { background: {
button_pressed_gradient( button_pressed_gradient(
tm.var(colors.BUTTON_GRADIENT_START), tm.var(colors.BUTTON_GRADIENT_START),
tm.var(colors.BUTTON_GRADIENT_END), tm.var(colors.BUTTON_GRADIENT_END),
tm.var(colors.BUTTON_PRESSED_SHADOW) tm.var(colors.SHADOW)
) )
} }
}} }}
@ -363,7 +376,8 @@ QSpinBox::down-button:hover {{
background: { background: {
button_gradient( button_gradient(
tm.var(colors.BUTTON_HOVER_GRADIENT_START), tm.var(colors.BUTTON_HOVER_GRADIENT_START),
tm.var(colors.BUTTON_HOVER_GRADIENT_END) tm.var(colors.BUTTON_HOVER_GRADIENT_END),
tm.var(colors.SHADOW)
) )
}; };
}} }}
@ -418,7 +432,7 @@ QRadioButton {{
QCheckBox::indicator, QCheckBox::indicator,
QRadioButton::indicator, QRadioButton::indicator,
QMenu::indicator {{ QMenu::indicator {{
border: 1px solid {tm.var(colors.BUTTON_BORDER)}; border: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-radius: {tm.var(props.BORDER_RADIUS)}; border-radius: {tm.var(props.BORDER_RADIUS)};
background: {tm.var(colors.CANVAS_INSET)}; background: {tm.var(colors.CANVAS_INSET)};
width: 16px; width: 16px;

View file

@ -19,8 +19,8 @@ class Switch(QAbstractButton):
radius: int = 10, radius: int = 10,
left_label: str = "", left_label: str = "",
right_label: str = "", right_label: str = "",
left_color: tuple[str, str] = colors.ACCENT_CARD, left_color: dict[str, str] = colors.ACCENT_CARD | {},
right_color: tuple[str, str] = colors.ACCENT_NOTE, right_color: dict[str, str] = colors.ACCENT_NOTE | {},
parent: QWidget = None, parent: QWidget = None,
) -> None: ) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)

View file

@ -30,16 +30,15 @@ from aqt.qt import (
@dataclass @dataclass
class ColoredIcon: class ColoredIcon:
path: str path: str
# (day, night) color: dict[str, str]
color: tuple[str, str]
def current_color(self, night_mode: bool) -> str: def current_color(self, night_mode: bool) -> str:
if night_mode: if night_mode:
return self.color[1] return self.color.get("dark", "")
else: else:
return self.color[0] return self.color.get("light", "")
def with_color(self, color: tuple[str, str]) -> ColoredIcon: def with_color(self, color: dict[str, str]) -> ColoredIcon:
return ColoredIcon(path=self.path, color=color) return ColoredIcon(path=self.path, color=color)
@ -177,12 +176,22 @@ class ThemeManager:
"Returns body classes used when showing a card." "Returns body classes used when showing a card."
return f"card card{card_ord+1} {self.body_class(night_mode)}" return f"card card{card_ord+1} {self.body_class(night_mode)}"
def var(self, vars: tuple[str, str]) -> str: def var(self, vars: dict[str, str]) -> str:
"""Given day/night colors/props, return the correct one for the current theme.""" """Given day/night colors/props, return the correct one for the current theme."""
idx = 1 if self.night_mode else 0 return vars["dark" if self.night_mode else "light"]
return vars[idx]
def qcolor(self, colors: tuple[str, str]) -> QColor: def qcolor(self, colors: dict[str, str]) -> QColor:
"""Create QColor instance from CSS string for the current theme."""
if m := re.match(
r"rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+\.*\d+?)\)", self.var(colors)
):
return QColor(
int(m.group(1)),
int(m.group(2)),
int(m.group(3)),
int(255 * float(m.group(4))),
)
return QColor(self.var(colors)) return QColor(self.var(colors))
def _determine_night_mode(self) -> bool: def _determine_night_mode(self) -> bool:
@ -257,7 +266,7 @@ class ThemeManager:
def _apply_palette(self, app: QApplication) -> None: def _apply_palette(self, app: QApplication) -> None:
set_macos_dark_mode(self.night_mode) set_macos_dark_mode(self.night_mode)
if not self.night_mode: if is_mac:
app.setStyle(QStyleFactory.create(self._default_style)) # type: ignore app.setStyle(QStyleFactory.create(self._default_style)) # type: ignore
self.default_palette.setColor( self.default_palette.setColor(
QPalette.ColorRole.Window, self.qcolor(colors.CANVAS) QPalette.ColorRole.Window, self.qcolor(colors.CANVAS)
@ -265,11 +274,9 @@ class ThemeManager:
app.setPalette(self.default_palette) app.setPalette(self.default_palette)
return return
if not self.macos_dark_mode():
app.setStyle(QStyleFactory.create("fusion")) # type: ignore app.setStyle(QStyleFactory.create("fusion")) # type: ignore
palette = QPalette() palette = QPalette()
text = self.qcolor(colors.FG) text = self.qcolor(colors.FG)
palette.setColor(QPalette.ColorRole.WindowText, text) palette.setColor(QPalette.ColorRole.WindowText, text)
palette.setColor(QPalette.ColorRole.ToolTipText, text) palette.setColor(QPalette.ColorRole.ToolTipText, text)
@ -277,7 +284,6 @@ class ThemeManager:
palette.setColor(QPalette.ColorRole.ButtonText, text) palette.setColor(QPalette.ColorRole.ButtonText, text)
hlbg = self.qcolor(colors.HIGHLIGHT_BG) hlbg = self.qcolor(colors.HIGHLIGHT_BG)
hlbg.setAlpha(64)
palette.setColor( palette.setColor(
QPalette.ColorRole.HighlightedText, self.qcolor(colors.HIGHLIGHT_FG) QPalette.ColorRole.HighlightedText, self.qcolor(colors.HIGHLIGHT_FG)
) )
@ -287,11 +293,13 @@ class ThemeManager:
palette.setColor(QPalette.ColorRole.Window, canvas) palette.setColor(QPalette.ColorRole.Window, canvas)
palette.setColor(QPalette.ColorRole.AlternateBase, canvas) palette.setColor(QPalette.ColorRole.AlternateBase, canvas)
palette.setColor(QPalette.ColorRole.Button, QColor("#454545")) palette.setColor(
QPalette.ColorRole.Button, self.qcolor(colors.BUTTON_GRADIENT_START)
)
canvas_inset = self.qcolor(colors.CANVAS_INSET) input_base = self.qcolor(colors.CANVAS_CODE)
palette.setColor(QPalette.ColorRole.Base, canvas_inset) palette.setColor(QPalette.ColorRole.Base, input_base)
palette.setColor(QPalette.ColorRole.ToolTipBase, canvas_inset) palette.setColor(QPalette.ColorRole.ToolTipBase, input_base)
palette.setColor( palette.setColor(
QPalette.ColorRole.PlaceholderText, self.qcolor(colors.FG_SUBTLE) QPalette.ColorRole.PlaceholderText, self.qcolor(colors.FG_SUBTLE)
@ -310,7 +318,7 @@ class ThemeManager:
disabled_color, disabled_color,
) )
palette.setColor(QPalette.ColorRole.Link, self.qcolor(colors.ACCENT_LINK)) palette.setColor(QPalette.ColorRole.Link, self.qcolor(colors.FG_LINK))
palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red) palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red)

View file

@ -196,7 +196,7 @@ class Toolbar:
_body = """ _body = """
<center id=outer> <center id=outer>
<table id=header width=100%%> <table id=header>
<tr> <tr>
<td class=tdcenter align=center>%s</td> <td class=tdcenter align=center>%s</td>
</tr></table> </tr></table>

View file

@ -412,30 +412,32 @@ class AnkiWebView(QWebEngineView):
family = tr.qt_misc_segoe_ui() family = tr.qt_misc_segoe_ui()
button_style = f""" button_style = f"""
button {{ font-family: {family}; }} button {{ font-family: {family}; }}
button:focus {{ outline: 5px auto {color_hl}; }}""" """
font = f"font-size:12px;font-family:{family};" font = f"font-size:12px;font-family:{family};"
elif is_mac: elif is_mac:
family = "Helvetica" family = "Helvetica"
font = f'font-size:15px;font-family:"{family}";' font = f'font-size:14px;font-family:"{family}";'
color = "" button_style = """
if not theme_manager.night_mode: button {
color = "background: #fff; border: 1px solid #ccc;" --canvas: #fff;
button_style = ( -webkit-appearance: none;
background: var(--canvas);
border-radius: var(--border-radius);
padding: 3px 12px;
border: 0.5px solid var(--border);
box-shadow: 0px 1px 3px var(--border-subtle);
font-family: Helvetica
}
.night-mode button { --canvas: #606060; --fg: #eee; }
""" """
button { -webkit-appearance: none; %s
border-radius:5px; font-family: Helvetica }"""
% color
)
else: else:
family = self.font().family() family = self.font().family()
color_hl_txt = palette.color(QPalette.ColorRole.HighlightedText).name()
font = f'font-size:14px;font-family:"{family}", sans-serif;' font = f'font-size:14px;font-family:"{family}", sans-serif;'
button_style = """ button_style = """
/* Buttons */ /* Buttons */
button{{ button{{
font-family:"{family}", sans-serif; }} font-family: "{family}", sans-serif;
button:focus{{ border-color: {color_hl} }} }}
button:active, button:active:hover {{ background-color: {color_hl}; color: {color_hl_txt};}}
/* Input field focus outline */ /* Input field focus outline */
textarea:focus, input:focus, input[type]:focus, .uneditable-input:focus, textarea:focus, input:focus, input[type]:focus, .uneditable-input:focus,
div[contenteditable="true"]:focus {{ div[contenteditable="true"]:focus {{
@ -444,7 +446,6 @@ div[contenteditable="true"]:focus {{
}}""".format( }}""".format(
family=family, family=family,
color_hl=color_hl, color_hl=color_hl,
color_hl_txt=color_hl_txt,
) )
zoom = self.app_zoom_factor() zoom = self.app_zoom_factor()
@ -453,8 +454,8 @@ div[contenteditable="true"]:focus {{
body {{ zoom: {zoom}; background-color: var(--canvas); }} body {{ zoom: {zoom}; background-color: var(--canvas); }}
html {{ {font} }} html {{ {font} }}
{button_style} {button_style}
:root {{ --canvas: {colors.CANVAS[0]} }} :root {{ --canvas: {colors.CANVAS["light"]} }}
:root[class*=night-mode] {{ --canvas: {colors.CANVAS[1]} }} :root[class*=night-mode] {{ --canvas: {colors.CANVAS["dark"]} }}
""" """
def stdHtml( def stdHtml(

View file

@ -1,13 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import json import json
import re import re
import sys import sys
# bazel genrule "srcs" # bazel genrule "srcs"
vars_css = sys.argv[1] root_vars_css = sys.argv[1]
# bazel genrule "outs" # bazel genrule "outs"
colors_py = sys.argv[2] colors_py = sys.argv[2]
@ -16,14 +15,19 @@ props_py = sys.argv[3]
colors = {} colors = {}
props = {} props = {}
reached_props = False reached_props = False
comment = ""
for line in re.split(r"[;\{\}]", open(vars_css).read()): for line in re.split(r"[;\{\}]|\*\/", open(root_vars_css).read()):
line = line.strip() line = line.strip()
if not line: if not line:
continue continue
if line.startswith("/*!"):
if "props" in line: if "props" in line:
reached_props = True reached_props = True
else:
comment = re.match(r"\/\*!\s*(.*)$", line)[1]
continue
m = re.match(r"--(.+):(.+)$", line) m = re.match(r"--(.+):(.+)$", line)
@ -38,39 +42,47 @@ for line in re.split(r"[;\{\}]", open(vars_css).read()):
print("failed to match", line) print("failed to match", line)
continue continue
var = m.group(1) var = m.group(1).replace("-", "_").upper()
val = m.group(2) val = m.group(2)
if reached_props: if reached_props:
props.setdefault(var, []).append(val) if not var in props:
props.setdefault(var, {})["comment"] = comment
props[var]["light"] = val
else: else:
colors.setdefault(var, []).append(val) props[var]["dark"] = val
else:
if not var in colors:
colors.setdefault(var, {})["comment"] = comment
colors[var]["light"] = val
else:
colors[var]["dark"] = val
comment = ""
copyright_notice = """\ copyright_notice = """\
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html\n
""" """
with open(colors_py, "w") as buf: with open(colors_py, "w") as buf:
buf.write(copyright_notice) buf.write(copyright_notice)
buf.write("# this file is auto-generated from _vars.scss and _colors.scss\n") buf.write("# This file was automatically generated from _root-vars.scss\n")
for color, val in colors.items(): for color, val in colors.items():
day = val[0] if not "dark" in val:
night = val[1] if len(val) > 1 else day val["dark"] = val.light
color = color.replace("-", "_").upper() buf.write(re.sub(r"\"\n", '",\n', f"{color} = {json.dumps(val, indent=4)}\n"))
buf.write(f'{color} = ("{day}", "{night}")\n')
with open(props_py, "w") as buf: with open(props_py, "w") as buf:
buf.write(copyright_notice) buf.write(copyright_notice)
buf.write("# this file is auto-generated from _vars.scss\n") buf.write("# This file was automatically generated from _root-vars.scss\n")
for prop, val in props.items(): for prop, val in props.items():
day = val[0] if not "dark" in val:
night = val[1] if len(val) > 1 else day val["dark"] = val.light
prop = prop.replace("-", "_").upper() buf.write(re.sub(r"\"\n", '",\n', f"{prop} = {json.dumps(val, indent=4)}\n"))
buf.write(f'{prop} = ("{day}", "{night}")\n')

View file

@ -17,7 +17,7 @@ sass_library(
sass_library( sass_library(
name = "vars_lib", name = "vars_lib",
srcs = [ srcs = [
"_colors.scss", "_color-palette.scss",
"_functions.scss", "_functions.scss",
"_vars.scss", "_vars.scss",
], ],
@ -104,7 +104,7 @@ sass_library(
) )
compile_sass( compile_sass(
srcs = ["_vars.scss"], srcs = ["_root-vars.scss"],
group = "vars_css", group = "vars_css",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
) )

View file

@ -2,13 +2,14 @@
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
@use "vars"; @use "vars";
@use "sass:color"; @use "sass:color";
@use "sass/elevation" as *;
@import "bootstrap/scss/functions"; @import "bootstrap/scss/functions";
@import "bootstrap/scss/variables"; @import "bootstrap/scss/variables";
@mixin impressed-shadow($intensity) { @mixin impressed-shadow($intensity) {
box-shadow: inset 0 calc(var(--buttons-size) / 15) calc(var(--buttons-size) / 5) box-shadow: inset 0 calc(var(--buttons-size, 10px) / 15)
rgba(black, $intensity); calc(var(--buttons-size, 10px) / 5) rgba(black, $intensity);
} }
@mixin border-radius { @mixin border-radius {
@ -19,13 +20,14 @@
border-bottom-right-radius: var(--border-right-radius); border-bottom-right-radius: var(--border-right-radius);
} }
@mixin background($primary: false) { @mixin background($primary: false, $elevation: 1, $hover: true) {
@if $primary { @if $primary {
background: linear-gradient( background: linear-gradient(
180deg, 180deg,
var(--button-primary-gradient-start) 0%, var(--button-primary-gradient-start) 0%,
var(--button-primary-gradient-end) 100% var(--button-primary-gradient-end) 100%
); );
@if $hover {
&:hover { &:hover {
background: linear-gradient( background: linear-gradient(
180deg, 180deg,
@ -34,12 +36,17 @@
); );
border-color: var(--button-hover-border); border-color: var(--button-hover-border);
} }
}
@if $elevation != 0 {
@include elevation($elevation, 0.2);
}
} @else { } @else {
background: linear-gradient( background: linear-gradient(
180deg, 180deg,
var(--button-gradient-start) 0%, var(--button-gradient-start) 0%,
var(--button-gradient-end) 100% var(--button-gradient-end) 100%
); );
@if $hover {
&:hover { &:hover {
background: linear-gradient( background: linear-gradient(
180deg, 180deg,
@ -49,6 +56,10 @@
border-color: var(--button-hover-border); border-color: var(--button-hover-border);
} }
} }
@if $elevation != 0 {
@include elevation($elevation);
}
}
} }
@mixin base( @mixin base(
@ -57,16 +68,21 @@
$with-hover: true, $with-hover: true,
$with-active: true, $with-active: true,
$active-class: "", $active-class: "",
$with-disabled: true $with-disabled: true,
$elevation: 1
) { ) {
-webkit-appearance: none; -webkit-appearance: none;
cursor: pointer; cursor: pointer;
@if $border { @if $border {
border: 1px solid var(--button-border); @if $primary {
border: none;
} @else {
border: 1px solid var(--border-subtle);
}
} @else { } @else {
border: none; border: none;
} }
@include background($primary); @include background($primary, $elevation, $hover: $with-hover);
@if ($primary) { @if ($primary) {
color: white; color: white;
@ -77,12 +93,12 @@
@if ($with-active) { @if ($with-active) {
&:active { &:active {
@include impressed-shadow(0.35); @include impressed-shadow(0.35);
border-color: var(--button-border); border-color: var(--border-subtle);
} }
@if ($active-class != "") { @if ($active-class != "") {
&.#{$active-class} { &.#{$active-class} {
@include impressed-shadow(0.35); @include impressed-shadow(0.35);
border-color: var(--button-border); border-color: var(--border);
} }
} }
} }

View file

@ -5,7 +5,7 @@
* custom gray, rest from Tailwind CSS v3 palette * custom gray, rest from Tailwind CSS v3 palette
* */ * */
$colors: ( $color-palette: (
lightgray: ( lightgray: (
0: #fcfcfc, 0: #fcfcfc,
1: #fafafa, 1: #fafafa,
@ -26,7 +26,7 @@ $colors: (
4: #363636, 4: #363636,
5: #2c2c2c, 5: #2c2c2c,
6: #252525, 6: #252525,
7: #1f1f1f, 7: #202020,
8: #141414, 8: #141414,
9: #020202, 9: #020202,
), ),

View file

@ -6,7 +6,17 @@
@function create-vars-from-map($map, $theme, $name: "-", $output: ()) { @function create-vars-from-map($map, $theme, $name: "-", $output: ()) {
@each $key, $value in $map { @each $key, $value in $map {
@if $key == $theme { @if $key ==
$theme or
(
$key ==
"default" and
type-of($value) !=
"map" and
type-of($value) !=
"list"
)
{
@return map.set($output, $name, map.get($map, $key)); @return map.set($output, $name, map.get($map, $key));
} }
@if type-of($value) == "map" { @if type-of($value) == "map" {
@ -21,8 +31,24 @@
create-vars-from-map($value, $theme, #{$name}-#{$key}, $output) create-vars-from-map($value, $theme, #{$name}-#{$key}, $output)
); );
} }
} @else if $key == "default" { } @else if type-of($value) == "list" and list.length($value) > 1 {
@return map.set($output, $name, map.get($map, $key)); $next-name: #{$name}-#{$key};
@if $key == "default" {
$next-name: $name;
}
$output: map-merge(
$output,
(#{"comment"}#{$next-name}: list.nth($value, 1))
);
$output: map-merge(
$output,
create-vars-from-map(
list.nth($value, 2),
$theme,
#{$next-name},
$output
)
);
} }
} }
@return $output; @return $output;
@ -30,8 +56,12 @@
@function map-deep-get($map, $keys) { @function map-deep-get($map, $keys) {
@each $key in $keys { @each $key in $keys {
@if type-of($map) == "list" and list.length($map) > 1 {
$map: map-get(list.nth($map, 2), $key);
} @else {
$map: map-get($map, $key); $map: map-get($map, $key);
} }
}
@return $map; @return $map;
} }

50
sass/_root-vars.scss Normal file
View file

@ -0,0 +1,50 @@
/* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later, http://www.gnu.org/licenses/agpl.html */
@use "sass:map";
@use "vars" as *;
@use "functions" as *;
/*! colors */
:root {
$colors: map.get($vars, colors);
@each $name, $val in create-vars-from-map($colors, light) {
@if str-index($name, "comment") == 1 {
/*! #{$val} */
} @else {
#{$name}: #{$val};
}
}
color-scheme: light;
&.night-mode {
@each $name, $val in create-vars-from-map($colors, dark) {
@if str-index($name, "comment") == 1 {
/*! #{$val} */
} @else {
#{$name}: #{$val};
}
}
color-scheme: dark;
}
}
/*! props */
:root {
$props: map.get($vars, props);
@each $name, $val in create-vars-from-map($props, light) {
@if str-index($name, "comment") == 1 {
/*! #{$val} */
} @else {
#{$name}: #{$val};
}
}
&.night-mode {
@each $name, $val in create-vars-from-map($props, dark) {
@if str-index($name, "comment") == 1 {
/*! #{$val} */
} @else {
#{$name}: #{$val};
}
}
}
}

View file

@ -4,296 +4,439 @@
@use "sass:map"; @use "sass:map";
@use "sass:color"; @use "sass:color";
@use "functions" as *; @use "functions" as *;
@use "colors" as *; @use "color-palette" as *;
@function palette($key, $shade) { @function palette($key, $shade) {
$color: map.get($colors, $key); $color: map.get($color-palette, $key);
@return map.get($color, $shade); @return map.get($color, $shade);
} }
$vars: ( $vars: (
props: ( props: (
border-radius: ( border-radius: (
default: (
"Used to round corners of various UI elements",
(
default: 5px, default: 5px,
), ),
), ),
large: (
"Used for big centered buttons",
(
default: 15px,
),
),
),
),
colors: ( colors: (
white: (
default: white,
),
black: (
default: black,
),
fg: ( fg: (
default: ( default: (
"Default text/icon color",
(
light: palette(darkgray, 9), light: palette(darkgray, 9),
dark: palette(lightgray, 0), dark: palette(lightgray, 0),
), ),
),
subtle: ( subtle: (
"Placeholder text, icons in idle state",
(
light: palette(darkgray, 6), light: palette(darkgray, 6),
dark: palette(lightgray, 3), dark: palette(lightgray, 3),
), ),
),
disabled: ( disabled: (
"Foreground color of disabled UI elements",
(
light: palette(lightgray, 9), light: palette(lightgray, 9),
dark: palette(darkgray, 0), dark: palette(darkgray, 0),
), ),
),
faint: ( faint: (
"Foreground color that barely stands out against canvas",
(
light: palette(lightgray, 7), light: palette(lightgray, 7),
dark: palette(darkgray, 2), dark: palette(darkgray, 2),
), ),
), ),
link: (
"Hyperlink foreground color",
(
light: palette(blue, 7),
dark: palette(blue, 2),
),
),
),
canvas: ( canvas: (
default: ( default: (
light: palette(lightgray, 3), "Window background",
(
light: palette(lightgray, 2),
dark: palette(darkgray, 5), dark: palette(darkgray, 5),
), ),
),
elevated: ( elevated: (
"Slightly brighter than window background",
(
light: white, light: white,
dark: palette(darkgray, 4), dark: palette(darkgray, 4),
), ),
),
inset: ( inset: (
light: palette(lightgray, 4), "Slightly darker than window background",
(
light: palette(lightgray, 3),
dark: palette(darkgray, 6), dark: palette(darkgray, 6),
), ),
),
overlay: ( overlay: (
"Background of floating elements (menus, tooltips)",
(
light: palette(lightgray, 0), light: palette(lightgray, 0),
dark: palette(darkgray, 5), dark: palette(darkgray, 5),
), ),
),
code: ( code: (
"Background of code editors",
(
light: white, light: white,
dark: palette(darkgray, 6), dark: palette(darkgray, 6),
), ),
), ),
),
border: ( border: (
default: ( default: (
"Border color with medium contrast against window background",
(
light: palette(lightgray, 6), light: palette(lightgray, 6),
dark: palette(darkgray, 4),
),
strong: (
light: palette(lightgray, 9),
dark: palette(darkgray, 1),
),
subtle: (
light: palette(lightgray, 5),
dark: palette(darkgray, 7), dark: palette(darkgray, 7),
), ),
faint: ( ),
subtle: (
"Border color with low contrast against window background",
(
light: palette(lightgray, 4), light: palette(lightgray, 4),
dark: palette(darkgray, 6), dark: palette(darkgray, 6),
), ),
focus: (
light: palette(blue, 5),
dark: palette(blue, 5),
), ),
), strong: (
button: ( "Border color with high contrast against window background",
border: ( (
light: palette(lightgray, 6),
dark: palette(darkgray, 6),
),
focus: (
light: palette(cyan, 3),
dark: palette(cyan, 4),
),
pressed: (
shadow: (
light: palette(lightgray, 7),
dark: palette(darkgray, 7),
),
border: (
light: palette(lightgray, 9), light: palette(lightgray, 9),
dark: palette(darkgray, 9), dark: palette(darkgray, 9),
), ),
), ),
focus: (
"Border color of focused input elements",
(
light: palette(blue, 5),
dark: palette(blue, 5),
),
),
),
button: (
disabled: ( disabled: (
"Background color of disabled buttons",
(
light: color.scale(palette(lightgray, 5), $alpha: -50%), light: color.scale(palette(lightgray, 5), $alpha: -50%),
dark: color.scale(palette(darkgray, 3), $alpha: -50%), dark: color.scale(palette(darkgray, 3), $alpha: -50%),
), ),
),
gradient: ( gradient: (
start: ( start: (
"Start value of default button gradient",
(
light: white, light: white,
dark: palette(darkgray, 3), dark: palette(darkgray, 4),
),
), ),
end: ( end: (
light: palette(lightgray, 1), "End value of default button gradient",
dark: palette(darkgray, 4), (
light: palette(lightgray, 3),
dark: palette(darkgray, 5),
),
), ),
), ),
hover: ( hover: (
gradient: ( gradient: (
start: ( start: (
"Start value of default button gradient in hover state",
(
light: palette(lightgray, 1), light: palette(lightgray, 1),
dark: palette(darkgray, 2),
),
end: (
light: palette(lightgray, 4),
dark: palette(darkgray, 3), dark: palette(darkgray, 3),
), ),
), ),
end: (
"End value of default button gradient in hover state",
(
light: palette(lightgray, 2),
dark: palette(darkgray, 4),
),
),
),
border: ( border: (
"Border color of default button in hover state",
(
light: palette(lightgray, 8), light: palette(lightgray, 8),
dark: palette(darkgray, 8), dark: palette(darkgray, 8),
), ),
), ),
),
primary: ( primary: (
gradient: ( gradient: (
start: ( start: (
"Start value of primary button gradient",
(
light: palette(blue, 4), light: palette(blue, 4),
dark: color.scale(palette(blue, 6), $saturation: -10%), dark: color.scale(palette(blue, 6), $saturation: -10%),
), ),
),
end: ( end: (
"End value of primary button gradient",
(
light: palette(blue, 6), light: palette(blue, 6),
dark: color.scale(palette(blue, 8), $saturation: -10%), dark: color.scale(palette(blue, 8), $saturation: -10%),
), ),
), ),
),
hover: ( hover: (
gradient: ( gradient: (
start: ( start: (
"Start value of primary button gradient in hover state",
(
light: palette(blue, 3), light: palette(blue, 3),
dark: color.scale(palette(blue, 5), $saturation: -10%), dark: color.scale(palette(blue, 5), $saturation: -10%),
), ),
),
end: ( end: (
"End value of primary button gradient in hover state",
(
light: palette(blue, 5), light: palette(blue, 5),
dark: color.scale(palette(blue, 7), $saturation: -10%), dark: color.scale(palette(blue, 7), $saturation: -10%),
), ),
), ),
), ),
),
disabled: ( disabled: (
"Background of primary button in disabled state",
(
light: palette(blue, 3), light: palette(blue, 3),
dark: color.scale(palette(blue, 5), $saturation: -10%), dark: color.scale(palette(blue, 5), $saturation: -10%),
), ),
), ),
), ),
),
scrollbar: ( scrollbar: (
bg: ( bg: (
default: ( default: (
"Background of scrollbar in idle state (Win/Lin only)",
(
light: palette(lightgray, 5), light: palette(lightgray, 5),
dark: palette(darkgray, 4), dark: palette(darkgray, 4),
), ),
),
hover: ( hover: (
"Background of scrollbar in hover state (Win/Lin only)",
(
light: palette(lightgray, 6), light: palette(lightgray, 6),
dark: palette(darkgray, 3), dark: palette(darkgray, 3),
), ),
),
active: ( active: (
"Background of scrollbar in pressed state (Win/Lin only)",
(
light: palette(lightgray, 7), light: palette(lightgray, 7),
dark: palette(darkgray, 1), dark: palette(darkgray, 1),
), ),
), ),
), ),
),
shadow: ( shadow: (
default: ( default: (
light: palette(lightgray, 8), "Default box-shadow color",
(
light: palette(lightgray, 6),
dark: palette(darkgray, 7), dark: palette(darkgray, 7),
), ),
),
inset: ( inset: (
light: palette(lightgray, 9), "Inset box-shadow color",
(
light: palette(darkgray, 3),
dark: palette(darkgray, 7), dark: palette(darkgray, 7),
), ),
),
subtle: ( subtle: (
light: palette(lightgray, 5), "Box-shadow color with lower contrast against window background",
dark: palette(darkgray, 6), (
light: palette(darkgray, 0),
dark: palette(darkgray, 4),
),
), ),
focus: ( focus: (
"Box-shadow color for elements in focused state",
(
default: palette(indigo, 5), default: palette(indigo, 5),
), ),
), ),
),
accent: ( accent: (
card: ( card: (
"Accent color for cards",
(
light: palette(blue, 4), light: palette(blue, 4),
dark: palette(blue, 3), dark: palette(blue, 3),
), ),
),
note: ( note: (
"Accent color for notes",
(
light: palette(green, 5), light: palette(green, 5),
dark: palette(green, 4), dark: palette(green, 4),
), ),
link: (
light: palette(blue, 7),
dark: palette(blue, 2),
), ),
danger: ( danger: (
"Saturated accent color to grab attention",
(
light: palette(red, 5), light: palette(red, 5),
dark: palette(red, 4), dark: palette(red, 4),
), ),
), ),
),
flag: ( flag: (
1: ( 1: (
"Flag 1 (red)",
(
light: palette(red, 5), light: palette(red, 5),
dark: palette(red, 4), dark: palette(red, 4),
), ),
),
2: ( 2: (
"Flag 2 (orange)",
(
light: palette(orange, 4), light: palette(orange, 4),
dark: palette(orange, 3), dark: palette(orange, 3),
), ),
),
3: ( 3: (
"Flag 3 (green)",
(
light: palette(green, 4), light: palette(green, 4),
dark: palette(green, 3), dark: palette(green, 3),
), ),
),
4: ( 4: (
"Flag 4 (blue)",
(
light: palette(blue, 5), light: palette(blue, 5),
dark: palette(blue, 4), dark: palette(blue, 4),
), ),
),
5: ( 5: (
"Flag 5 (pink)",
(
light: palette(fuchsia, 4), light: palette(fuchsia, 4),
dark: palette(fuchsia, 3), dark: palette(fuchsia, 3),
), ),
),
6: ( 6: (
"Flag 6 (turquoise)",
(
light: palette(teal, 4), light: palette(teal, 4),
dark: palette(teal, 3), dark: palette(teal, 3),
), ),
),
7: ( 7: (
"Flag 7 (purple)",
(
light: palette(purple, 5), light: palette(purple, 5),
dark: palette(purple, 4), dark: palette(purple, 4),
), ),
), ),
),
state: ( state: (
new: ( new: (
"Accent color for new cards",
(
light: palette(blue, 5), light: palette(blue, 5),
dark: palette(blue, 3), dark: palette(blue, 3),
), ),
),
learn: ( learn: (
"Accent color for cards in learning state",
(
light: palette(red, 6), light: palette(red, 6),
dark: palette(red, 4), dark: palette(red, 4),
), ),
),
review: ( review: (
"Accent color for cards in review state",
(
light: palette(green, 6), light: palette(green, 6),
dark: palette(green, 5), dark: palette(green, 5),
), ),
),
buried: ( buried: (
"Accent color for buried cards",
(
light: palette(amber, 5), light: palette(amber, 5),
dark: palette(amber, 8), dark: palette(amber, 8),
), ),
),
suspended: ( suspended: (
"Accent color for suspended cards",
(
light: palette(yellow, 4), light: palette(yellow, 4),
dark: palette(yellow, 1), dark: palette(yellow, 1),
), ),
),
marked: ( marked: (
"Accent color for marked cards",
(
light: palette(indigo, 2), light: palette(indigo, 2),
dark: palette(purple, 5), dark: palette(purple, 5),
), ),
), ),
),
highlight: ( highlight: (
bg: ( bg: (
light: color.scale(palette(blue, 3), $alpha: -33%), "Background color of highlighted items",
dark: color.scale(palette(blue, 4), $alpha: -33%), (
light: color.scale(palette(blue, 6), $alpha: -50%),
dark: color.scale(palette(blue, 3), $alpha: -50%),
),
), ),
fg: ( fg: (
"Foreground color of highlighted items",
(
light: black, light: black,
dark: white, dark: white,
), ),
), ),
selection: ( ),
selected: (
bg: ( bg: (
"Background color of selected text",
(
light: color.scale(palette(lightgray, 5), $alpha: -50%), light: color.scale(palette(lightgray, 5), $alpha: -50%),
dark: color.scale(palette(blue, 3), $alpha: -50%), dark: color.scale(palette(blue, 3), $alpha: -50%),
), ),
),
fg: ( fg: (
"Foreground color of selected text",
(
light: black, light: black,
dark: white, dark: white,
), ),
), ),
), ),
),
); );
@function prop($keyword) {
@return var(--#{$keyword});
}
@function color($keyword) { @function color($keyword) {
@return var(--#{$keyword}); @return var(--#{$keyword});
} }
@ -302,41 +445,3 @@ $vars: (
$colors: map.get($vars, colors); $colors: map.get($vars, colors);
@return get-value-from-map($colors, $keyword, $theme); @return get-value-from-map($colors, $keyword, $theme);
} }
@function prop($keyword) {
@return var(--#{$keyword});
}
/*! colors */
:root {
$colors: map.get($vars, colors);
@each $name, $val in create-vars-from-map($colors, light) {
#{$name}: #{$val};
}
color-scheme: light;
&.night-mode {
@each $name, $val in create-vars-from-map($colors, dark) {
#{$name}: #{$val};
}
color-scheme: dark;
}
}
/*! props */
:root {
$props: map.get($vars, props);
@each $name, $val in create-vars-from-map($props, default) {
#{$name}: #{$val};
}
@each $name, $val in create-vars-from-map($props, light) {
#{$name}: #{$val};
}
&.night-mode {
@each $name, $val in create-vars-from-map($props, default) {
#{$name}: #{$val};
}
@each $name, $val in create-vars-from-map($props, dark) {
#{$name}: #{$val};
}
}
}

View file

@ -1,11 +1,12 @@
@use "vars"; @use "vars" as *;
@use "root-vars";
@use "scrollbar"; @use "scrollbar";
@use "button-mixins" as button; @use "button-mixins" as button;
$body-color: var(--fg); $body-color: color(fg);
$body-bg: var(--canvas); $body-bg: color(canvas);
$link-hover-color: var(--accent-link); $link-hover-color: color(fg-link);
$link-hover-decoration: none; $link-hover-decoration: none;
$utilities: ( $utilities: (
@ -53,6 +54,7 @@ body {
button { button {
/* override transition for instant hover response */ /* override transition for instant hover response */
transition: color 0.15s ease-in-out, box-shadow 0.15s ease-in-out !important; transition: color 0.15s ease-in-out, box-shadow 0.15s ease-in-out !important;
border-radius: prop(border-radius);
} }
pre, pre,
@ -82,12 +84,12 @@ samp {
} }
.form-select:focus { .form-select:focus {
outline: none; outline: none;
border: 1px solid var(--border-focus); border: 1px solid color(border-focus);
box-shadow: none !important; box-shadow: none !important;
} }
.night-mode .form-select:disabled { .night-mode .form-select:disabled {
background-color: var(--fg-disabled); background-color: color(fg-disabled);
} }
.reduced-motion * { .reduced-motion * {
@ -104,10 +106,10 @@ select {
&:focus, &:focus,
&.focus { &.focus {
border: 1px solid var(--border-focus); border: 1px solid color(border-focus);
} }
option { option {
background: var(--canvas-elevated); background: color(canvas-elevated);
color: var(--fg); color: color(fg);
} }
} }

View file

@ -1,5 +1,6 @@
@use "vars"; @use "vars";
@use "button-mixins" as button; @use "button-mixins" as button;
@use "elevation" as *;
:root { :root {
--focus-color: #{vars.palette-of(shadow-focus)}; --focus-color: #{vars.palette-of(shadow-focus)};
@ -22,7 +23,11 @@
} }
button { button {
outline: none !important;
@include button.base; @include button.base;
border-radius: var(--border-radius); @include elevation(1);
padding: 5px 10px; border-radius: var(--border-radius-large);
padding: 8px 10px;
font-weight: 500;
margin: 0 4px;
} }

View file

@ -16,6 +16,6 @@ body {
} }
a { a {
color: var(--accent-link); color: var(--fg-link);
text-decoration: none; text-decoration: none;
} }

View file

@ -1,4 +1,5 @@
// Heavily inspired by https://github.com/material-components/material-components-web/tree/master/packages/mdc-elevation // Heavily inspired by https://github.com/material-components/material-components-web/tree/master/packages/mdc-elevation
@use "sass:color";
@use "sass:map"; @use "sass:map";
@use "sass:list"; @use "sass:list";
@ -64,21 +65,23 @@ $ambient-opacity: 0.12;
$penumbra-z-value: map.get($penumbra-map, $level); $penumbra-z-value: map.get($penumbra-map, $level);
$ambient-z-value: map.get($ambient-map, $level); $ambient-z-value: map.get($ambient-map, $level);
$umbra-color: rgba($color, $umbra-opacity + $opacity-boost); $umbra-color: color.adjust(rgba($color, $umbra-opacity), $alpha: $opacity-boost);
$penumbra-color: rgba($color, $penumbra-opacity + $opacity-boost); $penumbra-color: color.adjust(
$ambient-color: rgba($color, $ambient-opacity + $opacity-boost); rgba($color, $penumbra-opacity),
$alpha: $opacity-boost
);
$ambient-color: color.adjust(
rgba($color, $ambient-opacity),
$alpha: $opacity-boost
);
@return ( @return (
#{"#{$umbra-z-value} #{$umbra-color}"}, #{$umbra-z-value} $umbra-color,
#{"#{$penumbra-z-value} #{$penumbra-color}"}, #{$penumbra-z-value} $penumbra-color,
#{$ambient-z-value} $ambient-color #{$ambient-z-value} $ambient-color
); );
} }
@mixin elevation($level, $opacity-boost: 0, $color: black) { @mixin elevation($level, $opacity-boost: 0, $color: #141414) {
box-shadow: box-shadow($level, $opacity-boost, $color); box-shadow: box-shadow($level, $opacity-boost, $color);
} }
@mixin elevation-transition() {
transition: box-shadow 80ms cubic-bezier(0.33, 1, 0.68, 1);
}

View file

@ -1,4 +1,3 @@
@use "sass/vars";
@use "sass/bootstrap-dark"; @use "sass/bootstrap-dark";
@import "sass/base"; @import "sass/base";

View file

@ -20,10 +20,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</div> </div>
<style lang="scss"> <style lang="scss">
@use "sass/vars"; @use "sass/vars" as *;
.popover { .popover {
border-radius: 5px; border-radius: 5px;
background-color: var(--canvas-elevated); background-color: color(canvas-elevated);
min-width: var(--popover-width, 1rem); min-width: var(--popover-width, 1rem);
max-width: 95vw; max-width: 95vw;
@ -31,20 +31,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
padding: var(--popover-padding-block, 6px) var(--popover-padding-inline, 6px); padding: var(--popover-padding-block, 6px) var(--popover-padding-inline, 6px);
font-size: 1rem; font-size: 1rem;
color: var(--fg); color: color(fg);
/* outer border */ /* outer border */
border: 1px solid vars.palette(lightgray, 6); border: 1px solid palette(lightgray, 6);
&.dark { &.dark {
border-color: vars.palette(darkgray, 9); border-color: palette(darkgray, 9);
} }
/* inner border */ /* inner border */
box-shadow: inset 0 0 0 1px vars.palette(lightgray, 3); box-shadow: inset 0 0 0 1px palette(lightgray, 3);
&.dark { &.dark {
box-shadow: inset 0 0 0 1px vars.palette(darkgray, 2); box-shadow: inset 0 0 0 1px palette(darkgray, 2);
} }
&.scrollable { &.scrollable {
max-height: 80vh; max-height: 80vh;

View file

@ -75,13 +75,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
.chevron { .chevron {
position: absolute; position: absolute;
inset: 0 0 0 auto; inset: 0 0 0 auto;
border-left: 1px solid var(--button-border); border-left: 1px solid var(--border-subtle);
} }
:global([dir="rtl"]) { :global([dir="rtl"]) {
.chevron { .chevron {
inset: 0 auto 0 0; inset: 0 auto 0 0;
border-left: none; border-left: none;
border-right: 1px solid var(--button-border); border-right: 1px solid var(--border-subtle);
} }
} }
</style> </style>

View file

@ -102,6 +102,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
input { input {
width: 100%; width: 100%;
padding: 0.2rem 1.5rem 0.2rem 0.75rem; padding: 0.2rem 1.5rem 0.2rem 0.75rem;
background: var(--canvas-elevated);
color: var(--fg);
border: none; border: none;
outline: none; outline: none;
text-align: center; text-align: center;
@ -120,7 +122,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
button { button {
opacity: 0; opacity: 0;
position: absolute; position: absolute;
@include button.base($border: false); @include button.base($border: false, $elevation: 0);
&.left { &.left {
inset: 0 auto 0 0; inset: 0 auto 0 0;

View file

@ -29,7 +29,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<Container --gutter-block="1rem" --gutter-inline="2px" breakpoint="sm"> <Container --gutter-block="1rem" --gutter-inline="2px" breakpoint="sm">
<Col --col-justify="center"> <Col --col-justify="center">
<div class="congrats"> <div class="congrats">
<h3>{congrats}</h3> <h1>{congrats}</h1>
<p>{nextLearnMsg}</p> <p>{nextLearnMsg}</p>
@ -66,17 +66,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<style lang="scss"> <style lang="scss">
.congrats { .congrats {
margin-top: 2em;
max-width: 30em; max-width: 30em;
font-size: var(--base-font-size); font-size: var(--base-font-size);
:global(a) { :global(a) {
color: var(--accent-link); color: var(--fg-link);
text-decoration: none; text-decoration: none;
} }
h3 {
font-weight: bold;
}
} }
.description { .description {

View file

@ -1,3 +1,4 @@
@use "sass/root-vars";
@import "sass/base"; @import "sass/base";
@import "sass/bootstrap/scss/containers"; @import "sass/bootstrap/scss/containers";

View file

@ -118,7 +118,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
border-radius: 5px; border-radius: 5px;
border: 1px solid var(--border); border: 1px solid var(--border);
@include elevation-transition;
@include elevation(1); @include elevation(1);
&:focus-within { &:focus-within {

View file

@ -1,6 +1,5 @@
/* Copyright: Ankitects Pty Ltd and contributors /* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
@use "sass/vars";
@use "sass:color"; @use "sass:color";
@use "sass/button-mixins" as button; @use "sass/button-mixins" as button;

View file

@ -1,4 +1,3 @@
@use "sass/vars";
@use "sass/bootstrap-dark"; @use "sass/bootstrap-dark";
@import "sass/base"; @import "sass/base";

View file

@ -143,7 +143,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
.search-link:hover { .search-link:hover {
cursor: pointer; cursor: pointer;
color: var(--accent-link); color: var(--fg-link);
text-decoration: underline; text-decoration: underline;
} }
</style> </style>

View file

@ -1,4 +1,4 @@
@use "sass/base"; @use "root-vars";
@use "sass/button-mixins" as button; @use "sass/button-mixins" as button;
label, label,

View file

@ -1,4 +1,3 @@
@use "sass/vars";
@use "sass/bootstrap-dark"; @use "sass/bootstrap-dark";
@import "sass/base"; @import "sass/base";

View file

@ -33,7 +33,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</div> </div>
<style lang="scss"> <style lang="scss">
@use "sass/vars";
@use "sass/button-mixins" as button; @use "sass/button-mixins" as button;
.autocomplete-item { .autocomplete-item {