diff --git a/base/src/main/java/atlantafx/base/controls/Card.java b/base/src/main/java/atlantafx/base/controls/Card.java index b8cc05b..d3d7ff6 100644 --- a/base/src/main/java/atlantafx/base/controls/Card.java +++ b/base/src/main/java/atlantafx/base/controls/Card.java @@ -20,6 +20,7 @@ public class Card extends Control { // Default constructor public Card() { super(); + getStyleClass().add("card"); } @Override diff --git a/base/src/main/java/atlantafx/base/controls/CardSkin.java b/base/src/main/java/atlantafx/base/controls/CardSkin.java index e7b679c..435b644 100644 --- a/base/src/main/java/atlantafx/base/controls/CardSkin.java +++ b/base/src/main/java/atlantafx/base/controls/CardSkin.java @@ -3,14 +3,22 @@ package atlantafx.base.controls; import javafx.beans.value.ChangeListener; +import javafx.css.PseudoClass; import javafx.scene.Node; import javafx.scene.control.Skin; +import javafx.scene.image.ImageView; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; public class CardSkin implements Skin { + protected static final PseudoClass HAS_HEADER = PseudoClass.getPseudoClass("has-header"); + protected static final PseudoClass HAS_SUBHEADER = PseudoClass.getPseudoClass("has-subheader"); + protected static final PseudoClass HAS_BODY = PseudoClass.getPseudoClass("has-body"); + protected static final PseudoClass HAS_FOOTER = PseudoClass.getPseudoClass("has-footer"); + protected static final PseudoClass HAS_IMAGE = PseudoClass.getPseudoClass("has-image"); + protected final Card control; protected final VBox root = new VBox(); @@ -31,30 +39,42 @@ public class CardSkin implements Skin { headerSlot = new StackPane(); headerSlot.getStyleClass().add("header"); - headerSlotListener = new SlotListener(headerSlot); + headerSlotListener = new SlotListener( + headerSlot, (n, active) -> getSkinnable().pseudoClassStateChanged(HAS_HEADER, active) + ); control.headerProperty().addListener(headerSlotListener); headerSlotListener.changed(control.headerProperty(), null, control.getHeader()); subHeaderSlot = new StackPane(); subHeaderSlot.getStyleClass().add("sub-header"); - subHeaderSlotListener = new SlotListener(subHeaderSlot); + subHeaderSlotListener = new SlotListener( + subHeaderSlot, + (n, active) -> { + getSkinnable().pseudoClassStateChanged(HAS_SUBHEADER, active); + getSkinnable().pseudoClassStateChanged(HAS_IMAGE, n instanceof ImageView); + } + ); control.subHeaderProperty().addListener(subHeaderSlotListener); subHeaderSlotListener.changed(control.subHeaderProperty(), null, control.getSubHeader()); bodySlot = new StackPane(); bodySlot.getStyleClass().add("body"); VBox.setVgrow(bodySlot, Priority.ALWAYS); - bodySlotListener = new SlotListener(bodySlot); + bodySlotListener = new SlotListener( + bodySlot, (n, active) -> getSkinnable().pseudoClassStateChanged(HAS_BODY, active) + ); control.bodyProperty().addListener(bodySlotListener); bodySlotListener.changed(control.bodyProperty(), null, control.getBody()); footerSlot = new StackPane(); footerSlot.getStyleClass().add("footer"); - footerSlotListener = new SlotListener(footerSlot); + footerSlotListener = new SlotListener( + footerSlot, (n, active) -> getSkinnable().pseudoClassStateChanged(HAS_FOOTER, active) + ); control.footerProperty().addListener(footerSlotListener); footerSlotListener.changed(control.footerProperty(), null, control.getFooter()); - root.getStyleClass().add("card"); + root.getStyleClass().add("container"); root.getChildren().setAll(headerSlot, subHeaderSlot, bodySlot, footerSlot); } diff --git a/base/src/main/java/atlantafx/base/controls/MessageSkin.java b/base/src/main/java/atlantafx/base/controls/MessageSkin.java index cd93d54..186f47d 100644 --- a/base/src/main/java/atlantafx/base/controls/MessageSkin.java +++ b/base/src/main/java/atlantafx/base/controls/MessageSkin.java @@ -11,7 +11,7 @@ import javafx.scene.layout.StackPane; public class MessageSkin extends TileSkinBase { - private static final PseudoClass CLOSEABLE = PseudoClass.getPseudoClass("closeable"); + protected static final PseudoClass CLOSEABLE = PseudoClass.getPseudoClass("closeable"); protected final StackPane closeButton = new StackPane(); protected final StackPane closeButtonIcon = new StackPane(); diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/CardPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/CardPage.java index cbb820e..98609f9 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/components/CardPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/components/CardPage.java @@ -1,10 +1,12 @@ package atlantafx.sampler.page.components; import atlantafx.base.controls.Card; +import atlantafx.base.controls.CustomTextField; import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Tile; import atlantafx.base.theme.Styles; import atlantafx.base.util.BBCodeParser; +import atlantafx.sampler.Resources; import atlantafx.sampler.page.ExampleBox; import atlantafx.sampler.page.OutlinePage; import atlantafx.sampler.page.Snippet; @@ -16,10 +18,16 @@ import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.image.WritableImage; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; +import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.material2.Material2AL; import org.kordamp.ikonli.material2.Material2MZ; @@ -45,13 +53,14 @@ public class CardPage extends OutlinePage { addFormattedText(""" The [i]Card[/i] is a versatile container that can used in various contexts \ such as headings, text, dialogs and more. It includes a header to provide a \ - brief overview or context of the information. The sub-header and body sections \ + brief overview or context of the information. The subheader and body sections \ provide more detailed content, while the footer may include additional actions \ or information.""" ); addNode(skeleton()); addSection("Usage", usageExample()); addSection("Elevation", elevationExample()); + addSection("Subheader", subHeaderExample()); } private Node skeleton() { @@ -76,7 +85,7 @@ public class CardPage extends OutlinePage { box.setMaxWidth(500); box.getChildren().setAll( cellBuilder.apply("header"), - cellBuilder.apply("sub-header"), + cellBuilder.apply("subheader"), body, cellBuilder.apply("footer") ); @@ -133,7 +142,7 @@ public class CardPage extends OutlinePage { var box = new HBox(HGAP_20, tweetCard, dialogCard); var description = BBCodeParser.createFormattedText(""" The [i]Card[/i] pairs well with the [i]Tile[/i] component. \ - You can use the [i]Tile[/i] as either a header or body for the [i]Card[/i]. \ + You can use the [i]Tiles[/i] as either a header or body for the [i]Card[/i]. \ It’s also suitable for building more complex dialogs as well.""" ); @@ -160,17 +169,95 @@ public class CardPage extends OutlinePage { "This is a title", "This is a description" )); - card2.setBody(new Label("This is content")); + card2.setBody(new Label("This is a content")); //snippet_2:end var box = new HBox(HGAP_20, card1, card2); box.setPadding(new Insets(0, 0, 10, 0)); var description = BBCodeParser.createFormattedText(""" - With [code]Styles.ELEVATED_N[/code] or [code]Styles.INTERACTIVE[/code] styles classes \ - you can add raised shadow effect to the [i]Card[/i].""" + To add the raised effect to the [i]Card[/i], use the [code]Styles.ELEVATED_N[/code] \ + or [code]Styles.INTERACTIVE[/code] style classes.""" ); return new ExampleBox(box, new Snippet(getClass(), 2), description); } + + private ExampleBox subHeaderExample() { + //snippet_3:start + var card1 = new Card(); + card1.getStyleClass().add(Styles.ELEVATED_1); + card1.setMinWidth(300); + card1.setMaxWidth(300); + + var avatar1 = new Image( + Resources.getResourceAsStream("images/avatars/avatar3.png") + ); + var btn1 = new Button(null, new FontIcon(Feather.MORE_VERTICAL)); + btn1.getStyleClass().addAll(Styles.BUTTON_CIRCLE, Styles.FLAT); + var header1 = new Tile( + "Title", + "This is a description", + new ImageView(avatar1) + ); + header1.setAction(btn1); + card1.setHeader(header1); + + var image1 = new WritableImage( + new Image( + Resources.getResourceAsStream("images/pattern.jpg") + ).getPixelReader(), 0, 0, 298, 150 + ); + card1.setSubHeader(new ImageView(image1)); + + var text1 = new TextFlow(new Text(FAKER.lorem().sentence(15))); + text1.setMaxWidth(260); + card1.setBody(text1); + + // ~ + + var card2 = new Card(); + card2.getStyleClass().add(Styles.ELEVATED_1); + card2.setMinWidth(300); + card2.setMaxWidth(300); + + var header2 = new Tile( + "Reviewers", + "Request up to 10 reviewers" + ); + card2.setHeader(header2); + + var tf2 = new CustomTextField(); + tf2.setPromptText("Search people"); + tf2.setLeft(new FontIcon(Material2MZ.SEARCH)); + card2.setSubHeader(tf2); + + var body2 = new VBox(10); + card2.setBody(body2); + for (int i = 0; i < 5; i++) { + var cb = new CheckBox(); + var lbl = new Label(FAKER.name().fullName()); + var circle = new Circle( + 8, Color.web(FAKER.color().hex(true)) + ); + + var row = new HBox(10, circle, cb, lbl); + row.setAlignment(Pos.CENTER_LEFT); + body2.getChildren().add(row); + } + + //snippet_3:end + + var box = new HBox(HGAP_20, card1, card2); + box.setPadding(new Insets(0, 0, 10, 0)); + + var description = BBCodeParser.createFormattedText(""" + The subheader slot is an optional space for interactive controls. Use \ + it to display a search field, filter menu, or local navigation component. \ + It also has special support for the [i]ImageView[/i]. If you place an image \ + inside the subheader, it will remove its horizontal padding.""" + ); + + return new ExampleBox(box, new Snippet(getClass(), 3), description); + } } diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ModalPanePage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ModalPanePage.java index 2f6469c..41439b6 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/components/ModalPanePage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/components/ModalPanePage.java @@ -339,11 +339,11 @@ public final class ModalPanePage extends OutlinePage { VBox.setVgrow(ta, Priority.ALWAYS); var content = new VBox( - 20, + 16, new Tile("Example Dialog", FAKER.lorem().sentence(10)), ta ); - content.setPadding(new Insets(20)); + content.setPadding(new Insets(16)); dialog.addContent(content); AnchorPane.setTopAnchor(content, 0d); AnchorPane.setRightAnchor(content, 0d); diff --git a/styles/src/components/_card.scss b/styles/src/components/_card.scss index 0eb1394..dd1b942 100644 --- a/styles/src/components/_card.scss +++ b/styles/src/components/_card.scss @@ -7,55 +7,74 @@ $color-bg: -color-bg-default !default; $color-border: -color-border-default !default; $padding-x: 0.75em !default; $padding-y: 1em !default; -$spacing: 10px !default; +$spacing: 1em !default; + +$title-font-size: cfg.$font-title-4; .card { - -fx-background-color: $color-bg; - -fx-alignment: TOP_LEFT; - -fx-padding: $padding-y $padding-x $padding-y $padding-x; - -fx-spacing: $spacing; - - -fx-border-color: $color-border; - -fx-border-width: cfg.$border-width; - -fx-border-radius: cfg.$border-radius; - - >.header { + >.container { + -fx-background-color: $color-bg; -fx-alignment: TOP_LEFT; - } + -fx-padding: $padding-y 0 $padding-y 0; + -fx-spacing: $spacing; - >.sub-header { - -fx-alignment: TOP_LEFT; - } + -fx-border-color: $color-border; + -fx-border-width: cfg.$border-width; + -fx-border-radius: cfg.$border-radius; - >.body { - // double spacing for body - -fx-padding: $spacing 0 $spacing 0; - -fx-alignment: TOP_LEFT; - } + >.header { + -fx-alignment: TOP_LEFT; + -fx-padding: 0 $padding-x 0 $padding-x 0; + } - >.footer { - -fx-alignment: TOP_LEFT; - } + >.sub-header { + -fx-alignment: TOP_LEFT; + -fx-padding: 0 $padding-x 0 $padding-x 0; + } - @each $level, $radius in cfg.$elevation { - &.elevated-#{$level} { - @include effects.shadow(cfg.$elevation-color, $radius); + >.body { + // double vertical spacing for body + -fx-padding: 0 $padding-x 0 $padding-x; + -fx-alignment: TOP_LEFT; + } + + >.footer { + -fx-alignment: TOP_LEFT; + -fx-padding: 0 $padding-x 0 $padding-x 0; + } + + @each $level, $radius in cfg.$elevation { + &.elevated-#{$level} { + @include effects.shadow(cfg.$elevation-color, $radius); + } + } + + &.interactive:hover { + @include effects.shadow(cfg.$elevation-color, cfg.$elevation-interactive); } } - &.interactive:hover { - @include effects.shadow(cfg.$elevation-color, cfg.$elevation-interactive); + &:has-image { + >.container >.sub-header { + -fx-padding: 0; + } + } + + &.edge-to-edge>.container { + -fx-border-width: 0; + -fx-border-radius: 0; + -fx-effect: none; } .tile { - // prevent double indentation - -fx-padding: 0; - -fx-background-radius: 0; + >.container { + // prevent double indentation + -fx-padding: 0; + -fx-background-radius: 0; + + >.header >.title { + -fx-font-size: $title-font-size; + } + } } } - -.edge-to-edge>.card { - -fx-border-width: 0; - -fx-border-radius: 0; - -fx-effect: none; -} diff --git a/styles/src/components/_modal-pane.scss b/styles/src/components/_modal-pane.scss index fbfd5cb..172f2c2 100644 --- a/styles/src/components/_modal-pane.scss +++ b/styles/src/components/_modal-pane.scss @@ -46,5 +46,11 @@ $close-button-icon-size: 0.3em !default; // prevent double indentation inside dialog -fx-padding: 0; -fx-background-radius: 0; + + >.container { + // prevent double indentation + -fx-padding: 0; + -fx-background-radius: 0; + } } }