Improve color palette widget

* add description
* use better color combinations for color blocks
* refactor and pay tech debts
This commit is contained in:
mkpaz 2022-09-01 22:22:45 +04:00
parent fc19ca5a36
commit 8a7204d93c
18 changed files with 505 additions and 217 deletions

@ -1,6 +1,9 @@
/* SPDX-License-Identifier: MIT */ /* SPDX-License-Identifier: MIT */
package atlantafx.sampler; package atlantafx.sampler;
import atlantafx.sampler.event.BrowseEvent;
import atlantafx.sampler.event.DefaultEventBus;
import atlantafx.sampler.event.Listener;
import atlantafx.sampler.layout.ApplicationWindow; import atlantafx.sampler.layout.ApplicationWindow;
import atlantafx.sampler.theme.ThemeManager; import atlantafx.sampler.theme.ThemeManager;
import fr.brouillard.oss.cssfx.CSSFX; import fr.brouillard.oss.cssfx.CSSFX;
@ -63,6 +66,9 @@ public class Launcher extends Application {
stage.setResizable(true); stage.setResizable(true);
stage.setOnCloseRequest(t -> Platform.exit()); stage.setOnCloseRequest(t -> Platform.exit());
// register event listeners
DefaultEventBus.getInstance().subscribe(BrowseEvent.class, this::onBrowseEvent);
Platform.runLater(() -> { Platform.runLater(() -> {
stage.show(); stage.show();
stage.requestFocus(); stage.requestFocus();
@ -109,4 +115,9 @@ public class Launcher extends Application {
); );
CSSFX.start(scene); CSSFX.start(scene);
} }
@Listener
private void onBrowseEvent(BrowseEvent event) {
getHostServices().showDocument(event.getUri().toString());
}
} }

@ -0,0 +1,24 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.event;
import java.net.URI;
public class BrowseEvent extends Event {
private final URI uri;
public BrowseEvent(URI uri) {
this.uri = uri;
}
public URI getUri() {
return uri;
}
@Override
public String toString() {
return "BrowseEvent{" +
"uri=" + uri +
'}';
}
}

@ -0,0 +1,92 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.event;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
/**
* Simple event bus implementation.
* <p>
* Subscribe and publish events. Events are published in channels distinguished by event type.
* Channels can be grouped using an event type hierarchy.
* <p>
* You can use the default event bus instance {@link #getInstance}, which is a singleton
* or you can create one or multiple instances of {@link DefaultEventBus}.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public final class DefaultEventBus implements EventBus {
public DefaultEventBus() {}
private final Map<Class<?>, Set<Consumer>> subscribers = new ConcurrentHashMap<>();
@Override
public <E extends Event> void subscribe(Class<? extends E> eventType, Consumer<E> subscriber) {
Objects.requireNonNull(eventType);
Objects.requireNonNull(subscriber);
Set<Consumer> eventSubscribers = getOrCreateSubscribers(eventType);
eventSubscribers.add(subscriber);
}
private <E> Set<Consumer> getOrCreateSubscribers(Class<E> eventType) {
Set<Consumer> eventSubscribers = subscribers.get(eventType);
if (eventSubscribers == null) {
eventSubscribers = new CopyOnWriteArraySet<>();
subscribers.put(eventType, eventSubscribers);
}
return eventSubscribers;
}
@Override
public <E extends Event> void unsubscribe(Consumer<E> subscriber) {
Objects.requireNonNull(subscriber);
subscribers.values().forEach(eventSubscribers -> eventSubscribers.remove(subscriber));
}
@Override
public <E extends Event> void unsubscribe(Class<? extends E> eventType, Consumer<E> subscriber) {
Objects.requireNonNull(eventType);
Objects.requireNonNull(subscriber);
subscribers.keySet().stream()
.filter(eventType::isAssignableFrom)
.map(subscribers::get)
.forEach(eventSubscribers -> eventSubscribers.remove(subscriber));
}
@Override
public <E extends Event> void publish(E event) {
Objects.requireNonNull(event);
Class<?> eventType = event.getClass();
subscribers.keySet().stream()
.filter(type -> type.isAssignableFrom(eventType))
.flatMap(type -> subscribers.get(type).stream())
.forEach(subscriber -> publish(event, subscriber));
}
private <E extends Event> void publish(E event, Consumer<E> subscriber) {
try {
subscriber.accept(event);
} catch (Exception e) {
Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
}
}
///////////////////////////////////////////////////////////////////////////
private static class InstanceHolder {
private static final DefaultEventBus INSTANCE = new DefaultEventBus();
}
public static DefaultEventBus getInstance() {
return InstanceHolder.INSTANCE;
}
}

@ -0,0 +1,35 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.event;
import java.util.UUID;
public abstract class Event {
protected final UUID id = UUID.randomUUID();
protected Event() { }
public UUID getId() {
return id;
}
@Override
public boolean equals(Object o) {
if (this == o) { return true; }
if (o == null || getClass() != o.getClass()) { return false; }
Event event = (Event) o;
return id.equals(event.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public String toString() {
return "Event{" +
"id=" + id +
'}';
}
}

@ -0,0 +1,43 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.event;
import java.util.function.Consumer;
public interface EventBus {
/**
* Subscribe to an event type
*
* @param eventType the event type, can be a super class of all events to subscribe.
* @param subscriber the subscriber which will consume the events.
* @param <T> the event type class.
*/
<T extends Event> void subscribe(Class<? extends T> eventType, Consumer<T> subscriber);
/**
* Unsubscribe from all event types.
*
* @param subscriber the subscriber to unsubscribe.
*/
<T extends Event> void unsubscribe(Consumer<T> subscriber);
/**
* Unsubscribe from an event type.
*
* @param eventType the event type, can be a super class of all events to unsubscribe.
* @param subscriber the subscriber to unsubscribe.
* @param <T> the event type class.
*/
<T extends Event> void unsubscribe(Class<? extends T> eventType, Consumer<T> subscriber);
/**
* Publish an event to all subscribers.
* <p>
* The event type is the class of <code>event</code>. The event is published to all consumers which subscribed to
* this event type or any super class.
*
* @param event the event.
*/
<T extends Event> void publish(T event);
}

@ -0,0 +1,8 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.event;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
public @interface Listener {}

@ -12,13 +12,18 @@ import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.util.Duration; import javafx.util.Duration;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
import static atlantafx.sampler.util.Controls.hyperlink;
class ColorPalette extends VBox { class ColorPalette extends VBox {
private final List<ColorPaletteBlock> blocks = new ArrayList<>(); private final List<ColorPaletteBlock> blocks = new ArrayList<>();
@ -41,13 +46,30 @@ class ColorPalette extends VBox {
headerBox.setAlignment(Pos.CENTER_LEFT); headerBox.setAlignment(Pos.CENTER_LEFT);
headerBox.getStyleClass().add("header"); headerBox.getStyleClass().add("header");
var noteText = new VBox(6);
noteText.getChildren().setAll(
new TextFlow(
new Text("Color contrast between text and its background must meet "),
hyperlink("required WCAG standards",
URI.create("https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html")
),
new Text(":")
),
new Text(" • 4.5:1 for normal text"),
new Text(" • 3:1 for large text (>24px)"),
new Text(" • 3:1 for UI elements and graphics"),
new Text(" • no contrast requirement for decorative and disabled elements"),
new Text(),
new Text("Click on any color block to observe and modify color combination via built-in contrast checker.")
);
var colorGrid = colorGrid(); var colorGrid = colorGrid();
backgroundProperty().addListener((obs, old, val) -> bgBaseColor.set( backgroundProperty().addListener((obs, old, val) -> bgBaseColor.set(
val != null && !val.getFills().isEmpty() ? (Color) val.getFills().get(0).getFill() : Color.WHITE val != null && !val.getFills().isEmpty() ? (Color) val.getFills().get(0).getFill() : Color.WHITE
)); ));
getChildren().setAll(headerBox, colorGrid); getChildren().setAll(headerBox, noteText, colorGrid);
setId("color-palette"); setId("color-palette");
} }
@ -56,33 +78,34 @@ class ColorPalette extends VBox {
grid.getStyleClass().add("grid"); grid.getStyleClass().add("grid");
grid.add(colorBlock("-color-fg-default", "-color-bg-default", "-color-border-default"), 0, 0); grid.add(colorBlock("-color-fg-default", "-color-bg-default", "-color-border-default"), 0, 0);
grid.add(colorBlock("-color-fg-muted", "-color-bg-default", "-color-border-muted"), 1, 0); grid.add(colorBlock("-color-fg-default", "-color-bg-overlay", "-color-border-default"), 1, 0);
grid.add(colorBlock("-color-fg-subtle", "-color-bg-default", "-color-border-subtle"), 2, 0); grid.add(colorBlock("-color-fg-muted", "-color-bg-default", "-color-border-muted"), 2, 0);
grid.add(colorBlock("-color-fg-subtle", "-color-bg-default", "-color-border-subtle"), 3, 0);
grid.add(colorBlock("-color-fg-emphasis", "-color-accent-emphasis", "-color-accent-emphasis"), 0, 1); grid.add(colorBlock("-color-fg-emphasis", "-color-accent-emphasis", "-color-accent-emphasis"), 0, 1);
grid.add(colorBlock("-color-accent-fg", "-color-bg-default", "-color-accent-emphasis"), 1, 1); grid.add(colorBlock("-color-accent-fg", "-color-bg-default", "-color-accent-emphasis"), 1, 1);
grid.add(colorBlock("-color-accent-fg", "-color-accent-muted", "-color-accent-muted"), 2, 1); grid.add(colorBlock("-color-fg-default", "-color-accent-muted", "-color-accent-emphasis"), 2, 1);
grid.add(colorBlock("-color-accent-fg", "-color-accent-subtle", "-color-accent-subtle"), 3, 1); grid.add(colorBlock("-color-accent-fg", "-color-accent-subtle", "-color-accent-emphasis"), 3, 1);
grid.add(colorBlock("-color-fg-emphasis", "-color-neutral-emphasis-plus", "-color-neutral-emphasis-plus"), 0, 2); grid.add(colorBlock("-color-fg-emphasis", "-color-neutral-emphasis-plus", "-color-neutral-emphasis-plus"), 0, 2);
grid.add(colorBlock("-color-fg-emphasis", "-color-neutral-emphasis", "-color-neutral-emphasis"), 1, 2); grid.add(colorBlock("-color-fg-emphasis", "-color-neutral-emphasis", "-color-neutral-emphasis"), 1, 2);
grid.add(colorBlock("-color-fg-muted", "-color-neutral-muted", "-color-neutral-muted"), 2, 2); grid.add(colorBlock("-color-fg-default", "-color-neutral-muted", "-color-neutral-emphasis"), 2, 2);
grid.add(colorBlock("-color-fg-subtle", "-color-neutral-subtle", "-color-neutral-subtle"), 3, 2); grid.add(colorBlock("-color-fg-default", "-color-neutral-subtle", "-color-neutral-emphasis"), 3, 2);
grid.add(colorBlock("-color-fg-emphasis", "-color-success-emphasis", "-color-success-emphasis"), 0, 3); grid.add(colorBlock("-color-fg-emphasis", "-color-success-emphasis", "-color-success-emphasis"), 0, 3);
grid.add(colorBlock("-color-success-fg", "-color-bg-default", "-color-success-emphasis"), 1, 3); grid.add(colorBlock("-color-success-fg", "-color-bg-default", "-color-success-emphasis"), 1, 3);
grid.add(colorBlock("-color-success-fg", "-color-success-muted", "-color-success-muted"), 2, 3); grid.add(colorBlock("-color-fg-default", "-color-success-muted", "-color-success-emphasis"), 2, 3);
grid.add(colorBlock("-color-success-fg", "-color-success-subtle", "-color-success-subtle"), 3, 3); grid.add(colorBlock("-color-success-fg", "-color-success-subtle", "-color-success-emphasis"), 3, 3);
grid.add(colorBlock("-color-fg-emphasis", "-color-warning-emphasis", "-color-warning-emphasis"), 0, 4); grid.add(colorBlock("-color-fg-emphasis", "-color-warning-emphasis", "-color-warning-emphasis"), 0, 4);
grid.add(colorBlock("-color-warning-fg", "-color-bg-default", "-color-warning-emphasis"), 1, 4); grid.add(colorBlock("-color-warning-fg", "-color-bg-default", "-color-warning-emphasis"), 1, 4);
grid.add(colorBlock("-color-warning-fg", "-color-warning-muted", "-color-warning-muted"), 2, 4); grid.add(colorBlock("-color-fg-default", "-color-warning-muted", "-color-warning-emphasis"), 2, 4);
grid.add(colorBlock("-color-warning-fg", "-color-warning-subtle", "-color-warning-subtle"), 3, 4); grid.add(colorBlock("-color-warning-fg", "-color-warning-subtle", "-color-warning-emphasis"), 3, 4);
grid.add(colorBlock("-color-fg-emphasis", "-color-danger-emphasis", "-color-danger-emphasis"), 0, 5); grid.add(colorBlock("-color-fg-emphasis", "-color-danger-emphasis", "-color-danger-emphasis"), 0, 5);
grid.add(colorBlock("-color-danger-fg", "-color-bg-default", "-color-danger-emphasis"), 1, 5); grid.add(colorBlock("-color-danger-fg", "-color-bg-default", "-color-danger-emphasis"), 1, 5);
grid.add(colorBlock("-color-danger-fg", "-color-danger-muted", "-color-danger-muted"), 2, 5); grid.add(colorBlock("-color-fg-default", "-color-danger-muted", "-color-danger-emphasis"), 2, 5);
grid.add(colorBlock("-color-danger-fg", "-color-danger-subtle", "-color-danger-subtle"), 3, 5); grid.add(colorBlock("-color-danger-fg", "-color-danger-subtle", "-color-danger-emphasis"), 3, 5);
return grid; return grid;
} }

@ -3,6 +3,8 @@ package atlantafx.sampler.page.general;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.sampler.util.Containers; import atlantafx.sampler.util.Containers;
import atlantafx.sampler.util.ContrastLevel;
import atlantafx.sampler.util.NodeUtils;
import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.control.Label; import javafx.scene.control.Label;
@ -10,15 +12,17 @@ import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL; import org.kordamp.ikonli.material2.Material2AL;
import java.util.function.Consumer; import java.util.function.Consumer;
import static atlantafx.sampler.page.general.ContrastChecker.*; import static atlantafx.base.theme.Styles.TITLE_3;
import static atlantafx.sampler.page.general.ContrastChecker.LUMINANCE_THRESHOLD;
import static atlantafx.sampler.page.general.ContrastChecker.PASSED;
import static atlantafx.sampler.util.ContrastLevel.getColorLuminance;
import static atlantafx.sampler.util.ContrastLevel.getContrastRatioOpacityAware;
import static atlantafx.sampler.util.JColorUtils.flattenColor; import static atlantafx.sampler.util.JColorUtils.flattenColor;
import static atlantafx.sampler.util.JColorUtils.getColorLuminance;
class ColorPaletteBlock extends VBox { class ColorPaletteBlock extends VBox {
@ -27,11 +31,11 @@ class ColorPaletteBlock extends VBox {
private final String borderColorName; private final String borderColorName;
private final ReadOnlyObjectProperty<Color> bgBaseColor; private final ReadOnlyObjectProperty<Color> bgBaseColor;
private final AnchorPane colorBox; private final AnchorPane colorRectangle;
private final Text fgText; private final Text contrastRatioText;
private final FontIcon wsagIcon = new FontIcon(); private final FontIcon contrastLevelIcon = new FontIcon();
private final Label wsagLabel = new Label(); private final Label contrastLevelLabel = new Label();
private final FontIcon expandIcon = new FontIcon(Feather.MAXIMIZE_2); private final FontIcon editIcon = new FontIcon(Material2AL.COLORIZE);
private Consumer<ColorPaletteBlock> actionHandler; private Consumer<ColorPaletteBlock> actionHandler;
@ -44,69 +48,54 @@ class ColorPaletteBlock extends VBox {
this.borderColorName = validateColorName(borderColorName); this.borderColorName = validateColorName(borderColorName);
this.bgBaseColor = bgBaseColor; this.bgBaseColor = bgBaseColor;
fgText = new Text(); contrastRatioText = new Text();
fgText.setStyle("-fx-fill:" + fgColorName + ";"); contrastRatioText.setStyle("-fx-fill:" + fgColorName + ";");
fgText.getStyleClass().addAll("text", Styles.TITLE_3); contrastRatioText.getStyleClass().addAll("contrast-ratio-text", TITLE_3);
Containers.setAnchors(fgText, new Insets(5, -1, -1, 5)); Containers.setAnchors(contrastRatioText, new Insets(5, -1, -1, 5));
wsagLabel.setGraphic(wsagIcon); contrastLevelLabel.setGraphic(contrastLevelIcon);
wsagLabel.getStyleClass().add("wsag-label"); contrastLevelLabel.getStyleClass().add("contrast-level-label");
wsagLabel.setVisible(false); contrastLevelLabel.setVisible(false);
Containers.setAnchors(wsagLabel, new Insets(-1, 3, 3, -1)); Containers.setAnchors(contrastLevelLabel, new Insets(-1, 3, 3, -1));
expandIcon.setIconSize(24); editIcon.setIconSize(24);
expandIcon.getStyleClass().add("expand-icon"); editIcon.getStyleClass().add("edit-icon");
expandIcon.setVisible(false); NodeUtils.toggleVisibility(editIcon, false);
expandIcon.setManaged(false); Containers.setAnchors(editIcon, new Insets(3, 3, -1, -1));
Containers.setAnchors(expandIcon, new Insets(3, 3, -1, -1));
colorBox = new AnchorPane(); colorRectangle = new AnchorPane();
colorBox.setStyle("-fx-background-color:" + bgColorName + ";" + "-fx-border-color:" + borderColorName + ";"); colorRectangle.setStyle(
colorBox.getStyleClass().add("box"); String.format("-fx-background-color:%s;-fx-border-color:%s;", bgColorName, borderColorName)
colorBox.getChildren().setAll(fgText, wsagLabel, expandIcon); );
colorBox.setOnMouseEntered(e -> { colorRectangle.getStyleClass().add("rectangle");
colorRectangle.getChildren().setAll(contrastRatioText, contrastLevelLabel, editIcon);
colorRectangle.setOnMouseEntered(e -> {
var bgFill = getBgColor(); var bgFill = getBgColor();
// this happens when css isn't updated yet // this happens when css isn't updated yet
if (bgFill == null) { return; } if (bgFill == null) { return; }
toggleHover(true); toggleHover(true);
expandIcon.setFill(getColorLuminance(flattenColor(bgBaseColor.get(), bgFill)) < LUMINANCE_THRESHOLD ? editIcon.setFill(getColorLuminance(flattenColor(bgBaseColor.get(), bgFill)) < LUMINANCE_THRESHOLD ?
Color.WHITE : Color.BLACK Color.WHITE : Color.BLACK
); );
}); });
colorBox.setOnMouseExited(e -> toggleHover(false)); colorRectangle.setOnMouseExited(e -> toggleHover(false));
colorBox.setOnMouseClicked(e -> { colorRectangle.setOnMouseClicked(e -> {
if (actionHandler != null) { actionHandler.accept(this); } if (actionHandler != null) { actionHandler.accept(this); }
}); });
getChildren().addAll( getChildren().addAll(
colorBox, colorRectangle,
description(fgColorName), colorNameText(fgColorName),
description(bgColorName), colorNameText(bgColorName),
description(borderColorName) colorNameText(borderColorName)
); );
getStyleClass().add("color-block"); getStyleClass().add("block");
} }
static String validateColorName(String colorName) { public void setOnAction(Consumer<ColorPaletteBlock> actionHandler) {
if (colorName == null || !colorName.startsWith("-color")) { this.actionHandler = actionHandler;
throw new IllegalArgumentException("Invalid color name: '" + colorName + "'.");
}
return colorName;
}
private void toggleHover(boolean state) {
expandIcon.setVisible(state);
expandIcon.setManaged(state);
fgText.setOpacity(state ? 0.5 : 1);
wsagLabel.setOpacity(state ? 0.5 : 1);
}
private Text description(String text) {
var t = new Text(text);
t.getStyleClass().addAll("description", Styles.TEXT_SMALL);
return t;
} }
public void update() { public void update() {
@ -114,28 +103,28 @@ class ColorPaletteBlock extends VBox {
var bgFill = getBgColor(); var bgFill = getBgColor();
if (fgFill == null || bgFill == null) { if (fgFill == null || bgFill == null) {
fgText.setText(""); contrastRatioText.setText("");
wsagLabel.setText(""); contrastLevelLabel.setText("");
wsagLabel.setVisible(false); contrastLevelLabel.setVisible(false);
return; return;
} }
double contrastRatio = 1 / getContrastRatioOpacityAware(bgFill, fgFill, bgBaseColor.get()); double contrastRatio = getContrastRatioOpacityAware(bgFill, fgFill, bgBaseColor.get());
colorBox.pseudoClassStateChanged(PASSED, contrastRatio >= 4.5); colorRectangle.pseudoClassStateChanged(PASSED, ContrastLevel.AA_NORMAL.satisfies(contrastRatio));
wsagIcon.setIconCode(contrastRatio >= 4.5 ? Material2AL.CHECK : Material2AL.CLOSE); contrastRatioText.setText(String.format("%.2f", contrastRatio));
wsagLabel.setVisible(true); contrastLevelIcon.setIconCode(ContrastLevel.AA_NORMAL.satisfies(contrastRatio) ? Material2AL.CHECK : Material2AL.CLOSE);
wsagLabel.setText(contrastRatio >= 7 ? "AAA" : "AA"); contrastLevelLabel.setVisible(true);
fgText.setText(String.format("%.2f", contrastRatio)); contrastLevelLabel.setText(ContrastLevel.AAA_NORMAL.satisfies(contrastRatio) ? "AAA" : "AA");
} }
public Color getFgColor() { public Color getFgColor() {
return (Color) fgText.getFill(); return (Color) contrastRatioText.getFill();
} }
public Color getBgColor() { public Color getBgColor() {
return colorBox.getBackground() != null && !colorBox.getBackground().isEmpty() ? return colorRectangle.getBackground() != null && !colorRectangle.getBackground().isEmpty() ?
(Color) colorBox.getBackground().getFills().get(0).getFill() : null; (Color) colorRectangle.getBackground().getFills().get(0).getFill() : null;
} }
public String getFgColorName() { public String getFgColorName() {
@ -150,7 +139,22 @@ class ColorPaletteBlock extends VBox {
return borderColorName; return borderColorName;
} }
public void setOnAction(Consumer<ColorPaletteBlock> actionHandler) { private void toggleHover(boolean state) {
this.actionHandler = actionHandler; NodeUtils.toggleVisibility(editIcon, state);
contrastRatioText.setOpacity(state ? 0.5 : 1);
contrastLevelLabel.setOpacity(state ? 0.5 : 1);
}
private Text colorNameText(String text) {
var t = new Text(text);
t.getStyleClass().addAll("color-name", Styles.TEXT_SMALL);
return t;
}
static String validateColorName(String colorName) {
if (colorName == null || !colorName.startsWith("-color")) {
throw new IllegalArgumentException("Invalid color name: '" + colorName + "'.");
}
return colorName;
} }
} }

@ -11,12 +11,14 @@ import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.util.Duration; import javafx.util.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
public class ColorScale extends VBox { class ColorScale extends VBox {
private final ReadOnlyObjectWrapper<Color> bgBaseColor = new ReadOnlyObjectWrapper<>(Color.WHITE); private final ReadOnlyObjectWrapper<Color> bgBaseColor = new ReadOnlyObjectWrapper<>(Color.WHITE);
private final List<ColorScaleBlock> blocks = Arrays.asList( private final List<ColorScaleBlock> blocks = Arrays.asList(
@ -42,6 +44,10 @@ public class ColorScale extends VBox {
headerBox.setAlignment(Pos.CENTER_LEFT); headerBox.setAlignment(Pos.CENTER_LEFT);
headerBox.getStyleClass().add("header"); headerBox.getStyleClass().add("header");
var noteText = new TextFlow(
new Text("Avoid referencing scale variables directly when building UI that needs to adapt to different color themes. Instead, use the functional variables listed above.")
);
backgroundProperty().addListener((obs, old, val) -> bgBaseColor.set( backgroundProperty().addListener((obs, old, val) -> bgBaseColor.set(
val != null && !val.getFills().isEmpty() ? (Color) val.getFills().get(0).getFill() : Color.WHITE val != null && !val.getFills().isEmpty() ? (Color) val.getFills().get(0).getFill() : Color.WHITE
)); ));
@ -49,6 +55,7 @@ public class ColorScale extends VBox {
setId("color-scale"); setId("color-scale");
getChildren().setAll( getChildren().setAll(
headerBox, headerBox,
noteText,
colorTable() colorTable()
); );
} }

@ -8,10 +8,10 @@ import javafx.scene.control.Label;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import static atlantafx.sampler.util.ContrastLevel.getColorLuminance;
import static atlantafx.sampler.util.JColorUtils.flattenColor; import static atlantafx.sampler.util.JColorUtils.flattenColor;
import static atlantafx.sampler.util.JColorUtils.getColorLuminance;
public class ColorScaleBlock extends VBox { class ColorScaleBlock extends VBox {
private static final double BLOCK_WIDTH = 250; private static final double BLOCK_WIDTH = 250;
private static final double BLOCK_HEIGHT = 50; private static final double BLOCK_HEIGHT = 50;
@ -34,8 +34,8 @@ public class ColorScaleBlock extends VBox {
if (c instanceof Label label) { if (c instanceof Label label) {
String colorName = (String) label.getUserData(); String colorName = (String) label.getUserData();
label.setStyle(String.format("-fx-background-color:%s;-fx-text-fill:%s;", label.setStyle(String.format("-fx-background-color:%s;-fx-text-fill:%s;",
colorName, colorName,
JColorUtils.toHexWithAlpha(getSafeFgColor(label)) JColorUtils.toHexWithAlpha(getSafeFgColor(label))
)); ));
} }
}); });

@ -5,6 +5,7 @@ import atlantafx.base.controls.CustomTextField;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.sampler.theme.ThemeManager; import atlantafx.sampler.theme.ThemeManager;
import atlantafx.sampler.util.ContrastLevel;
import atlantafx.sampler.util.JColor; import atlantafx.sampler.util.JColor;
import atlantafx.sampler.util.JColorUtils; import atlantafx.sampler.util.JColorUtils;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -34,16 +35,19 @@ import org.kordamp.ikonli.material2.Material2AL;
import java.util.Objects; import java.util.Objects;
import static atlantafx.sampler.page.general.ColorPaletteBlock.validateColorName; import static atlantafx.sampler.page.general.ColorPaletteBlock.validateColorName;
import static atlantafx.sampler.util.ContrastLevel.getColorLuminance;
import static atlantafx.sampler.util.ContrastLevel.getContrastRatioOpacityAware;
import static atlantafx.sampler.util.JColorUtils.flattenColor; import static atlantafx.sampler.util.JColorUtils.flattenColor;
import static atlantafx.sampler.util.JColorUtils.getColorLuminance;
// Inspired by the https://colourcontrast.cc/ // Inspired by the https://colourcontrast.cc/
public class ContrastChecker extends GridPane { class ContrastChecker extends GridPane {
public static final double CONTRAST_RATIO_THRESHOLD = 1.5; public static final double CONTRAST_RATIO_THRESHOLD = 1.5;
public static final double LUMINANCE_THRESHOLD = 0.55; public static final double LUMINANCE_THRESHOLD = 0.55;
public static final PseudoClass PASSED = PseudoClass.getPseudoClass("passed"); public static final PseudoClass PASSED = PseudoClass.getPseudoClass("passed");
private static final String STATE_PASS = "PASS";
private static final String STATE_FAIL = "FAIL";
private static final int SLIDER_WIDTH = 300; private static final int SLIDER_WIDTH = 300;
private String bgColorName; private String bgColorName;
@ -70,7 +74,7 @@ public class ContrastChecker extends GridPane {
this.bgBaseColor = bgBaseColor; this.bgBaseColor = bgBaseColor;
this.contrastRatio = Bindings.createDoubleBinding( this.contrastRatio = Bindings.createDoubleBinding(
() -> 1 / getContrastRatioOpacityAware(bgColor.getColor(), fgColor.getColor(), bgBaseColor.get()), () -> getContrastRatioOpacityAware(bgColor.getColor(), fgColor.getColor(), bgBaseColor.get()),
bgColor.colorProperty(), bgColor.colorProperty(),
fgColor.colorProperty(), fgColor.colorProperty(),
bgBaseColor bgBaseColor
@ -112,60 +116,42 @@ public class ContrastChecker extends GridPane {
} }
private void createView() { private void createView() {
var textLabel = new Label("Aa"); var largeFontLabel = new Label("Aa");
textLabel.getStyleClass().add("text"); largeFontLabel.getStyleClass().add("large-font");
var ratioLabel = new Label("0.0"); var contrastRatioLabel = new Label("0.0");
ratioLabel.getStyleClass().add("ratio"); contrastRatioLabel.getStyleClass().add("ratio");
ratioLabel.textProperty().bind(Bindings.createStringBinding( contrastRatioLabel.textProperty().bind(Bindings.createStringBinding(
() -> String.format("%.2f", contrastRatio.get()), contrastRatio () -> String.format("%.2f", contrastRatio.get()), contrastRatio
)); ));
var fontBox = new HBox(20, textLabel, ratioLabel); var contrastRatioBox = new HBox(20, largeFontLabel, contrastRatioLabel);
fontBox.getStyleClass().add("font-box"); contrastRatioBox.getStyleClass().add("contrast-ratio");
fontBox.setAlignment(Pos.BASELINE_LEFT); contrastRatioBox.setAlignment(Pos.BASELINE_LEFT);
// ! // !
var aaNormalLabel = wsagLabel(); var aaNormalLabel = contrastLevelLabel();
var aaNormalBox = wsagBox(aaNormalLabel, "AA Normal"); var aaNormalBox = contrastLevelBox(aaNormalLabel, "AA Normal");
var aaLargeLabel = wsagLabel(); var aaLargeLabel = contrastLevelLabel();
var aaLargeBox = wsagBox(aaLargeLabel, "AA Large"); var aaLargeBox = contrastLevelBox(aaLargeLabel, "AA Large");
var aaaNormalLabel = wsagLabel(); var aaaNormalLabel = contrastLevelLabel();
var aaaNormalBox = wsagBox(aaaNormalLabel, "AAA Normal"); var aaaNormalBox = contrastLevelBox(aaaNormalLabel, "AAA Normal");
var aaaLargeLabel = wsagLabel(); var aaaLargeLabel = contrastLevelLabel();
var aaaLargeBox = wsagBox(aaaLargeLabel, "AAA Large"); var aaaLargeBox = contrastLevelBox(aaaLargeLabel, "AAA Large");
var wsagBox = new HBox(20, aaNormalBox, aaLargeBox, aaaNormalBox, aaaLargeBox); var contrastLevels = new HBox(20, aaNormalBox, aaLargeBox, aaaNormalBox, aaaLargeBox);
wsagBox.getStyleClass().add("wsag-box");
contrastRatio.addListener((obs, old, val) -> { contrastRatio.addListener((obs, old, val) -> {
if (val == null) { return; } if (val == null) { return; }
float ratio = val.floatValue(); float ratio = val.floatValue();
if (ratio >= 7) { updateWsagLabel(aaNormalLabel, ContrastLevel.AA_NORMAL.satisfies(ratio));
updateWsagLabel(aaNormalLabel, true); updateWsagLabel(aaLargeLabel, ContrastLevel.AA_LARGE.satisfies(ratio));
updateWsagLabel(aaLargeLabel, true); updateWsagLabel(aaaNormalLabel, ContrastLevel.AAA_NORMAL.satisfies(ratio));
updateWsagLabel(aaaNormalLabel, true); updateWsagLabel(aaaLargeLabel, ContrastLevel.AAA_LARGE.satisfies(ratio));
updateWsagLabel(aaaLargeLabel, true);
} else if (ratio < 7 && ratio >= 4.5) {
updateWsagLabel(aaNormalLabel, true);
updateWsagLabel(aaLargeLabel, true);
updateWsagLabel(aaaNormalLabel, false);
updateWsagLabel(aaaLargeLabel, true);
} else if (ratio < 4.5 && ratio >= 3) {
updateWsagLabel(aaNormalLabel, false);
updateWsagLabel(aaLargeLabel, true);
updateWsagLabel(aaaNormalLabel, false);
updateWsagLabel(aaaLargeLabel, false);
} else {
updateWsagLabel(aaNormalLabel, false);
updateWsagLabel(aaLargeLabel, false);
updateWsagLabel(aaaNormalLabel, false);
updateWsagLabel(aaaLargeLabel, false);
}
}); });
// ~ // ~
@ -296,7 +282,7 @@ public class ContrastChecker extends GridPane {
getStyleClass().add("contrast-checker"); getStyleClass().add("contrast-checker");
// column 0 // column 0
add(new HBox(fontBox, new Spacer(), wsagBox), 0, 0, REMAINING, 1); add(new HBox(contrastRatioBox, new Spacer(), contrastLevels), 0, 0, REMAINING, 1);
add(new Label("Background Color"), 0, 1); add(new Label("Background Color"), 0, 1);
add(bgColorNameLabel, 0, 2); add(bgColorNameLabel, 0, 2);
add(bgTextField, 0, 3); add(bgTextField, 0, 3);
@ -335,8 +321,8 @@ public class ContrastChecker extends GridPane {
private void updateStyle() { private void updateStyle() {
setStyle(String.format("-color-contrast-checker-bg:%s;-color-contrast-checker-fg:%s;", setStyle(String.format("-color-contrast-checker-bg:%s;-color-contrast-checker-fg:%s;",
JColorUtils.toHexWithAlpha(bgColor.getColor()), JColorUtils.toHexWithAlpha(bgColor.getColor()),
JColorUtils.toHexWithAlpha(getSafeFgColor()) JColorUtils.toHexWithAlpha(getSafeFgColor())
)); ));
} }
@ -359,25 +345,26 @@ public class ContrastChecker extends GridPane {
private void updateWsagLabel(Label label, boolean success) { private void updateWsagLabel(Label label, boolean success) {
FontIcon icon = Objects.requireNonNull((FontIcon) label.getGraphic()); FontIcon icon = Objects.requireNonNull((FontIcon) label.getGraphic());
if (success) { if (success) {
label.setText("PASS"); label.setText(STATE_PASS);
icon.setIconCode(Material2AL.CHECK); icon.setIconCode(Material2AL.CHECK);
} else { } else {
label.setText("FAIL"); label.setText(STATE_FAIL);
icon.setIconCode(Material2AL.CLOSE); icon.setIconCode(Material2AL.CLOSE);
} }
label.pseudoClassStateChanged(PASSED, success); label.pseudoClassStateChanged(PASSED, success);
} }
private Label wsagLabel() { private Label contrastLevelLabel() {
var label = new Label("FAIL"); var label = new Label(STATE_FAIL);
label.getStyleClass().add("wsag-label"); label.getStyleClass().add("state");
label.setContentDisplay(ContentDisplay.RIGHT); label.setContentDisplay(ContentDisplay.RIGHT);
label.setGraphic(new FontIcon(Material2AL.CLOSE)); label.setGraphic(new FontIcon(Material2AL.CLOSE));
return label; return label;
} }
private VBox wsagBox(Label label, String description) { private VBox contrastLevelBox(Label label, String description) {
var box = new VBox(10, label, new Label(description)); var box = new VBox(10, label, new Label(description));
box.getStyleClass().add("contrast-level");
box.setAlignment(Pos.CENTER); box.setAlignment(Pos.CENTER);
return box; return box;
} }
@ -392,12 +379,6 @@ public class ContrastChecker extends GridPane {
return slider; return slider;
} }
static double getContrastRatioOpacityAware(Color bgColor, Color fgColor, Color bgBaseColor) {
double luminance1 = getColorLuminance(flattenColor(bgBaseColor, bgColor));
double luminance2 = getColorLuminance(flattenColor(bgBaseColor, fgColor));
return JColorUtils.getContrastRatio(luminance1, luminance2);
}
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
private static class ObservableHSLAColor { private static class ObservableHSLAColor {

@ -10,12 +10,17 @@ import javafx.geometry.HPos;
import javafx.scene.control.ChoiceBox; import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.util.Duration; import javafx.util.Duration;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import java.net.URI;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
import static atlantafx.sampler.util.Controls.hyperlink;
public class ThemePage extends AbstractPage { public class ThemePage extends AbstractPage {
public static final String NAME = "Theme"; public static final String NAME = "Theme";
@ -23,9 +28,9 @@ public class ThemePage extends AbstractPage {
private final Consumer<ColorPaletteBlock> colorBlockActionHandler = colorBlock -> { private final Consumer<ColorPaletteBlock> colorBlockActionHandler = colorBlock -> {
ContrastCheckerDialog dialog = getOrCreateContrastCheckerDialog(); ContrastCheckerDialog dialog = getOrCreateContrastCheckerDialog();
dialog.getContent().setValues(colorBlock.getFgColorName(), dialog.getContent().setValues(colorBlock.getFgColorName(),
colorBlock.getFgColor(), colorBlock.getFgColor(),
colorBlock.getBgColorName(), colorBlock.getBgColorName(),
colorBlock.getBgColor() colorBlock.getBgColor()
); );
overlay.setContent(dialog, HPos.CENTER); overlay.setContent(dialog, HPos.CENTER);
overlay.toFront(); overlay.toFront();
@ -59,11 +64,21 @@ public class ThemePage extends AbstractPage {
} }
private void createView() { private void createView() {
var noteText = new TextFlow(
new Text("AtlantaFX follows "),
hyperlink("Github Primer interface guidelines",
URI.create("https://primer.style/design/foundations/color")
),
new Text(" and color system.")
);
userContent.getChildren().addAll( userContent.getChildren().addAll(
optionsGrid(), optionsGrid(),
noteText,
colorPalette, colorPalette,
colorScale colorScale
); );
// if you want to enable quick menu don't forget that // if you want to enable quick menu don't forget that
// theme selection choice box have to be updated accordingly // theme selection choice box have to be updated accordingly
NodeUtils.toggleVisibility(quickConfigBtn, false); NodeUtils.toggleVisibility(quickConfigBtn, false);

@ -0,0 +1,74 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.util;
import javafx.scene.paint.Color;
import java.util.Arrays;
import static atlantafx.sampler.util.JColorUtils.flattenColor;
/**
* The WCAG contrast measures the difference in brightness (luminance) between two colours.
* It ranges from 1:1 (white on white) to 21:1 (black on white). WCAG requirements are:
* <ul>
* <li>0.14285 (7.0:1) for small text in AAA-level</li>
* <li>0.22222 (4.5:1) for small text in AA-level, or large text in AAA-level</li>
* <li>0.33333 (3.0:1) for large text in AA-level</li>
* </ul>
* WCAG defines large text as text that is 18pt and larger, or 14pt and larger if it is bold.
* <br/>
* <a href="https://www.w3.org/TR/WCAG20-TECHS/G18.html">More info</a>.
*/
public enum ContrastLevel {
AA_NORMAL,
AA_LARGE,
AAA_NORMAL,
AAA_LARGE;
public boolean satisfies(double ratio) {
switch (this) {
case AA_NORMAL -> { return ratio >= 4.5; }
case AA_LARGE -> { return ratio >= 3; }
case AAA_NORMAL -> { return ratio >= 7; }
case AAA_LARGE -> { return ratio >= 4.5; }
}
return false;
}
public static double getContrastRatio(Color color1, Color color2) {
return getContrastRatio(getColorLuminance(color1), getColorLuminance(color2));
}
public static double getContrastRatio(double luminance1, double luminance2) {
return 1 / (luminance1 > luminance2 ?
(luminance2 + 0.05) / (luminance1 + 0.05) :
(luminance1 + 0.05) / (luminance2 + 0.05)
);
}
public static double getContrastRatioOpacityAware(Color bgColor, Color fgColor, Color bgBaseColor) {
double luminance1 = getColorLuminance(flattenColor(bgBaseColor, bgColor));
double luminance2 = getColorLuminance(flattenColor(bgBaseColor, fgColor));
return getContrastRatio(luminance1, luminance2);
}
/**
* Measures relative color luminance according to the
* <a href="https://www.w3.org/TR/WCAG20-TECHS/G18.html">W3C</a>.
* <br/>
* Note that JavaFX provides {@link Color#getBrightness()} which
* IS NOT the same thing as luminance.
*/
public static double getColorLuminance(double[] rgb) {
double[] tmp = Arrays.stream(rgb)
.map(v -> v <= 0.03928 ? (v / 12.92) : Math.pow((v + 0.055) / 1.055, 2.4))
.toArray();
return (tmp[0] * 0.2126) + (tmp[1] * 0.7152) + (tmp[2] * 0.0722);
}
/** @see ContrastLevel#getColorLuminance(double[]) */
public static double getColorLuminance(Color color) {
return getColorLuminance(new double[] { color.getRed(), color.getGreen(), color.getBlue() });
}
}

@ -1,14 +1,15 @@
/* SPDX-License-Identifier: MIT */ /* SPDX-License-Identifier: MIT */
package atlantafx.sampler.util; package atlantafx.sampler.util;
import javafx.scene.control.Button; import atlantafx.sampler.event.BrowseEvent;
import javafx.scene.control.MenuItem; import atlantafx.sampler.event.DefaultEventBus;
import javafx.scene.control.ToggleButton; import javafx.scene.control.*;
import javafx.scene.control.ToggleGroup;
import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.Ikon; import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.net.URI;
import static atlantafx.base.theme.Styles.BUTTON_ICON; import static atlantafx.base.theme.Styles.BUTTON_ICON;
public final class Controls { public final class Controls {
@ -40,10 +41,10 @@ public final class Controls {
} }
public static ToggleButton toggleButton(String text, public static ToggleButton toggleButton(String text,
Ikon icon, Ikon icon,
ToggleGroup group, ToggleGroup group,
boolean selected, boolean selected,
String... styleClasses) { String... styleClasses) {
var toggleButton = new ToggleButton(text); var toggleButton = new ToggleButton(text);
if (icon != null) { toggleButton.setGraphic(new FontIcon(icon)); } if (icon != null) { toggleButton.setGraphic(new FontIcon(icon)); }
if (group != null) { toggleButton.setToggleGroup(group); } if (group != null) { toggleButton.setToggleGroup(group); }
@ -52,4 +53,12 @@ public final class Controls {
return toggleButton; return toggleButton;
} }
public static Hyperlink hyperlink(String text, URI uri) {
var hyperlink = new Hyperlink(text);
if (uri != null) {
hyperlink.setOnAction(event -> DefaultEventBus.getInstance().publish(new BrowseEvent(uri)));
}
return hyperlink;
}
} }

@ -780,48 +780,6 @@ public class JColorUtils {
// Additional Utils, mkpaz (c) // // Additional Utils, mkpaz (c) //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
/**
* The WCAG contrast measures the difference in brightness (luminance) between two colours.
* It ranges from 1:1 (white on white) to 21:1 (black on white). WCAG requirements are:
* <ul>
* <li>0.14285 (7.0:1) for small text in AAA-level</li>
* <li>0.22222 (4.5:1) for small text in AA-level, or large text in AAA-level</li>
* <li>0.33333 (3.0:1) for large text in AA-level</li>
* </ul>
* WCAG defines large text as text that is 18pt and larger, or 14pt and larger if it is bold.
* <br/>
* <a href="https://www.w3.org/TR/WCAG20-TECHS/G18.html">More info</a>.
*/
public static double getContrastRatio(Color color1, Color color2) {
return getContrastRatio(getColorLuminance(color1), getColorLuminance(color2));
}
/** @see JColorUtils#getContrastRatio(Color, Color) */
public static double getContrastRatio(double luminance1, double luminance2) {
return luminance1 > luminance2 ?
(luminance2 + 0.05) / (luminance1 + 0.05) :
(luminance1 + 0.05) / (luminance2 + 0.05);
}
/**
* Measures relative color luminance according to the
* <a href="https://www.w3.org/TR/WCAG20-TECHS/G18.html">W3C</a>.
* <br/>
* Note that JavaFX provides {@link Color#getBrightness()} which
* IS NOT the same thing as luminance.
*/
public static double getColorLuminance(double[] rgb) {
double[] tmp = Arrays.stream(rgb)
.map(v -> v <= 0.03928 ? (v / 12.92) : Math.pow((v + 0.055) / 1.055, 2.4))
.toArray();
return (tmp[0] * 0.2126) + (tmp[1] * 0.7152) + (tmp[2] * 0.0722);
}
/** @see JColorUtils#getColorLuminance(double[]) */
public static double getColorLuminance(Color color) {
return getColorLuminance(new double[] { color.getRed(), color.getGreen(), color.getBlue() });
}
/** /**
* Removes given color opacity, if present. * Removes given color opacity, if present.
* <br/><br/> * <br/><br/>

@ -20,6 +20,7 @@ module atlantafx.sampler {
exports atlantafx.sampler; exports atlantafx.sampler;
exports atlantafx.sampler.fake; exports atlantafx.sampler.fake;
exports atlantafx.sampler.fake.domain; exports atlantafx.sampler.fake.domain;
exports atlantafx.sampler.event;
exports atlantafx.sampler.layout; exports atlantafx.sampler.layout;
exports atlantafx.sampler.page; exports atlantafx.sampler.page;
exports atlantafx.sampler.page.general; exports atlantafx.sampler.page.general;

@ -13,21 +13,21 @@ $color-wsag-fg: white;
-fx-vgap: 20px; -fx-vgap: 20px;
-fx-hgap: 40px; -fx-hgap: 40px;
>.color-block { >.block {
-fx-spacing: 5px; -fx-spacing: 5px;
>.box { >.rectangle {
-fx-min-width: 12em; -fx-min-width: 12em;
-fx-min-height: 5em; -fx-min-height: 5em;
-fx-max-width: 12em; -fx-max-width: 12em;
-fx-max-height: 5em; -fx-max-height: 5em;
-fx-cursor: hand; -fx-cursor: hand;
&:passed>.wsag-label { &:passed>.contrast-level-label {
-fx-background-color: $color-wsag-bg-passed; -fx-background-color: $color-wsag-bg-passed;
} }
>.wsag-label { >.contrast-level-label {
-fx-text-fill: $color-wsag-fg; -fx-text-fill: $color-wsag-fg;
-fx-background-color: $color-wsag-bg-failed; -fx-background-color: $color-wsag-bg-failed;
-fx-background-radius: 6px; -fx-background-radius: 6px;

@ -2,6 +2,7 @@
@use "color-palette" as palette; @use "color-palette" as palette;
.contrast-checker { .contrast-checker {
-fx-background-color: -color-contrast-checker-bg; -fx-background-color: -color-contrast-checker-bg;
-fx-hgap: 40px; -fx-hgap: 40px;
-fx-vgap: 20px; -fx-vgap: 20px;
@ -47,11 +48,6 @@
} }
} }
.ikonli-font-icon {
-fx-icon-color: -color-contrast-checker-fg;
-fx-fill: -color-contrast-checker-fg;
}
.slider { .slider {
>.thumb { >.thumb {
-fx-background-color: -color-contrast-checker-fg; -fx-background-color: -color-contrast-checker-fg;
@ -63,10 +59,15 @@
} }
} }
.font-box { .ikonli-font-icon {
-fx-icon-color: -color-contrast-checker-fg;
-fx-fill: -color-contrast-checker-fg;
}
.contrast-ratio {
-fx-padding: 0 40px 0 0; -fx-padding: 0 40px 0 0;
>.text { >.large-font {
-fx-font-size: 4em; -fx-font-size: 4em;
} }
@ -75,19 +76,21 @@
} }
} }
.wsag-box>*>.wsag-label { .contrast-level {
-fx-padding: 0.5em 1em 0.5em 1em; >.state {
-fx-background-color: palette.$color-wsag-bg-failed; -fx-padding: 0.5em 1em 0.5em 1em;
-fx-background-radius: 4px; -fx-background-color: palette.$color-wsag-bg-failed;
-fx-text-fill: palette.$color-wsag-fg; -fx-background-radius: 4px;
-fx-text-fill: palette.$color-wsag-fg;
&:passed { &:passed {
-fx-background-color: palette.$color-wsag-bg-passed; -fx-background-color: palette.$color-wsag-bg-passed;
} }
>.ikonli-font-icon { >.ikonli-font-icon {
-fx-fill: palette.$color-wsag-fg; -fx-fill: palette.$color-wsag-fg;
-fx-icon-color: palette.$color-wsag-fg; -fx-icon-color: palette.$color-wsag-fg;
}
} }
} }
} }