diff --git a/base/pom.xml b/base/pom.xml
index 7da138a..ad5e8a8 100644
--- a/base/pom.xml
+++ b/base/pom.xml
@@ -317,6 +317,55 @@
+
+ dance
+
+
+
+ org.apache.maven.plugins
+ maven-clean-plugin
+
+
+
+ ${project.basedir}
+
+ package.json
+ package-lock.json
+ tsconfig.json
+ tsconfig.json.*
+ types.d.ts
+ types.d.ts.*
+ vite.config.ts
+ vite.generated.ts
+ webpack.config.js
+ webpack.generated.js
+
+
+
+ ${project.basedir}/frontend
+
+ index.html
+
+
+
+ ${project.basedir}/frontend/generated
+
+
+ ${project.basedir}/node_modules
+
+
+ ${project.basedir}/src/main/bundles
+
+
+ ${project.basedir}/src/main/dev-bundle
+
+
+
+
+
+
+
+
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/CommonsDemoIcons.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/CommonsDemoIcons.java
new file mode 100644
index 0000000..af68c25
--- /dev/null
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/CommonsDemoIcons.java
@@ -0,0 +1,78 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2025 Flowing Code
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package com.flowingcode.vaadin.addons.demo;
+
+import com.vaadin.flow.component.dependency.JsModule;
+import com.vaadin.flow.component.icon.IconFactory;
+import java.util.Locale;
+
+/**
+ * CommonsDemo icons.
+ *
+ * @author Javier Godoy / Flowing Code
+ */
+public enum CommonsDemoIcons implements IconFactory {
+ ROTATE, FLIP, HIDE_SOURCE, SHOW_SOURCE;
+
+ /**
+ * The Iconset name, i.e. {@code "fab"}."
+ */
+ public static final String ICONSET = "commons-demo";
+
+ /**
+ * Return the full icon name.
+ *
+ * @return the full icon name, i.e. {@code "commons-demo:name"}..
+ */
+ public String getIconName() {
+ return ICONSET + ':' + getIconPart();
+ }
+
+ /**
+ * Return the icon name within the iconset.
+ *
+ * @return the icon name, i.e. {@code "name"}..
+ */
+ public String getIconPart() {
+ return name().toLowerCase(Locale.ENGLISH).replace('_', '-').replaceFirst("^-", "");
+ }
+
+ /**
+ * Create a new {@link Icon} instance with the icon determined by the name.
+ *
+ * @return a new instance of {@link Icon} component
+ */
+ @Override
+ public Icon create() {
+ return new Icon(getIconPart());
+ }
+
+ /**
+ * Server side component for {@code Brands}
+ */
+ @JsModule("./commons-demo-iconset.ts")
+ @SuppressWarnings("serial")
+ public static final class Icon extends com.vaadin.flow.component.icon.Icon {
+ private Icon(String icon) {
+ super(ICONSET, icon);
+ }
+ }
+
+}
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SourceCodeViewer.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SourceCodeViewer.java
index bfb3e45..7d7c6d8 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SourceCodeViewer.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SourceCodeViewer.java
@@ -26,6 +26,8 @@
import com.vaadin.flow.component.HasElement;
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.UI;
+import com.vaadin.flow.component.button.Button;
+import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.dom.Element;
@@ -51,13 +53,130 @@ public SourceCodeViewer(String sourceUrl, Map properties) {
}
public SourceCodeViewer(String url, String language, Map properties) {
+ addClassName("source-code-viewer");
+ addClassName("has-code-viewer-gutter");
+
codeViewer = new Element("code-viewer");
- getElement().appendChild(codeViewer);
- getElement().getStyle().set("overflow", "auto");
- getElement().getStyle().set("display", "flex");
- codeViewer.getStyle().set("flex-grow", "1");
+
+ Div codeViewerWrapper = new Div();
+ codeViewerWrapper.addClassName("source-code-viewer-codeviewer-wrapper");
+ codeViewerWrapper.getElement().appendChild(codeViewer);
+
+ Button showButton = new Button(CommonsDemoIcons.SHOW_SOURCE.create());
+ showButton.addThemeVariants(ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_TERTIARY_INLINE);
+ showButton.setAriaLabel("Show source code");
+ showButton.addClickListener(ev -> setSourceCollapsed(false));
+ showButton.addClassName("source-code-viewer-button");
+ showButton.addClassName("source-code-viewer-show-button");
+
+ Button hideButton = new Button(CommonsDemoIcons.HIDE_SOURCE.create());
+ hideButton.addThemeVariants(ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_TERTIARY_INLINE);
+ hideButton.setAriaLabel("Hide source code");
+ hideButton.addClickListener(ev -> setSourceCollapsed(true));
+ hideButton.addClassName("source-code-viewer-button");
+ hideButton.addClassName("source-code-viewer-hide-button");
+
+ Button rotateButton = new Button(CommonsDemoIcons.ROTATE.create());
+ rotateButton.addThemeVariants(ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_TERTIARY_INLINE);
+ rotateButton.setAriaLabel("Rotate source code");
+ rotateButton.addClickListener(ev -> rotateSource());
+ rotateButton.addClassName("source-code-viewer-button");
+ rotateButton.addClassName("source-code-viewer-rotate-button");
+
+ Button flipButton = new Button(CommonsDemoIcons.FLIP.create());
+ flipButton.addThemeVariants(ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_TERTIARY_INLINE);
+ flipButton.setAriaLabel("Flip source code");
+ flipButton.addClickListener(ev -> flipSource());
+ flipButton.addClassName("source-code-viewer-button");
+ flipButton.addClassName("source-code-viewer-flip-button");
+
+ Div buttons = new Div(showButton, hideButton, flipButton, rotateButton);
+ buttons.addClassName("source-code-viewer-buttons");
+
+ // Non-scrolling overlay so the buttons stay pinned while the code scrolls
+ Div buttonsWrapper = new Div(buttons);
+ buttonsWrapper.addClassName("source-code-viewer-buttons-wrapper");
+
+ add(codeViewerWrapper, buttonsWrapper);
+
setProperties(properties);
- addAttachListener(ev -> fetchContents(url, language));
+ addAttachListener(ev -> {
+ fetchContents(url, language);
+ observeScrollbar();
+ });
+ }
+
+ /**
+ * Observes the scrollable wrapper. Whenever the vertical scrollbar appears or disappears, sets (or
+ * clears) the {@code --code-viewer-gutter} custom property on the nearest ancestor (or self)
+ * carrying the {@code has-code-viewer-gutter} class. Whenever the wrapper collapses below 24px in
+ * either axis, sets {@code --source-code-viewer-show-button-display} so the show button becomes
+ * visible (and clears it otherwise).
+ */
+ private void observeScrollbar() {
+ getElement().executeJs(
+ """
+ const root = this;
+ const wrapper = root.querySelector('.source-code-viewer-codeviewer-wrapper');
+ if (!wrapper) return;
+ root.__scrollbarObserver?.disconnect();
+ root.__scrollbarMutation?.disconnect();
+ let hasScrollbar = null;
+ const update = () => {
+ if (wrapper.offsetWidth < 24 || wrapper.offsetHeight < 10) {
+ root.style.setProperty('--source-code-viewer-show-button-display', 'block');
+ } else {
+ root.style.removeProperty('--source-code-viewer-show-button-display');
+ }
+ const current = wrapper.scrollHeight > wrapper.clientHeight;
+ if (current === hasScrollbar) return;
+ hasScrollbar = current;
+ let target = root;
+ while (target && !target.classList.contains('has-code-viewer-gutter')) {
+ target = target.parentElement;
+ }
+ if (target) {
+ if (current) {
+ const scrollbarWidth = wrapper.offsetWidth - wrapper.clientWidth;
+ target.style.setProperty('--code-viewer-gutter', scrollbarWidth + 'px');
+ } else {
+ target.style.removeProperty('--code-viewer-gutter');
+ }
+ }
+ };
+ let frame = 0;
+ const scheduleUpdate = () => {
+ if (frame) return;
+ frame = requestAnimationFrame(() => { frame = 0; update(); });
+ };
+ const resizeObserver = new ResizeObserver(scheduleUpdate);
+ resizeObserver.observe(wrapper);
+ root.__scrollbarObserver = resizeObserver;
+ const codeViewer = root.querySelector('code-viewer');
+ if (codeViewer) {
+ const mutationObserver = new MutationObserver(scheduleUpdate);
+ mutationObserver.observe(codeViewer, {childList: true, subtree: true});
+ root.__scrollbarMutation = mutationObserver;
+ }
+ update();
+ """);
+ }
+
+ private void setSourceCollapsed(boolean collapsed) {
+ getElement().executeJs(
+ "this.dispatchEvent(new CustomEvent('source-collapse-changed',"
+ + " {bubbles: true, detail: {collapsed: $0}}))",
+ collapsed);
+ }
+
+ private void rotateSource() {
+ getElement().executeJs(
+ "this.dispatchEvent(new CustomEvent('source-rotate', {bubbles: true}))");
+ }
+
+ private void flipSource() {
+ getElement().executeJs(
+ "this.dispatchEvent(new CustomEvent('source-flip', {bubbles: true}))");
}
public void fetchContents(String url, String language) {
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
index 5d83475..1f7a148 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
@@ -59,7 +59,11 @@ public boolean isEmpty() {
return code.isEmpty();
}
- private void setSourcePosition(SourcePosition position) {
+ public SourcePosition getSourcePosition() {
+ return sourcePosition;
+ }
+
+ public void setSourcePosition(SourcePosition position) {
if (!position.equals(sourcePosition)) {
getContent().removeAll();
switch (position) {
@@ -76,10 +80,6 @@ private void setSourcePosition(SourcePosition position) {
}
}
- public void toggleSourcePosition() {
- setSourcePosition(sourcePosition.toggle());
- }
-
public void setOrientation(Orientation o) {
getContent().setOrientation(o);
getContent()
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
index 6505f93..7a12934 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
@@ -20,6 +20,9 @@
package com.flowingcode.vaadin.addons.demo;
import com.flowingcode.vaadin.addons.GithubBranch;
+import com.flowingcode.vaadin.addons.demo.events.OrientationChangedEvent;
+import com.flowingcode.vaadin.addons.demo.events.SourceCollapseChangedEvent;
+import com.flowingcode.vaadin.addons.demo.events.SourcePositionChangedEvent;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEventListener;
@@ -70,12 +73,10 @@ public class TabbedDemo extends VerticalLayout implements RouterLayout {
private static final int MOBILE_DEVICE_BREAKPOINT_WIDTH = 768;
private boolean autoVisibility;
+ private boolean sourceCollapsed;
private EnhancedRouteTabs tabs;
private HorizontalLayout footer;
private SplitLayoutDemo currentLayout;
- private Checkbox orientationCB;
- private Checkbox codeCB;
- private Checkbox codePositionCB;
private Checkbox themeCB;
private Orientation splitOrientation;
private Button helperButton;
@@ -89,23 +90,14 @@ public TabbedDemo() {
tabs = new EnhancedRouteTabs();
- // Footer
- orientationCB = new Checkbox("Toggle Orientation");
- orientationCB.setValue(true);
- orientationCB.addClassName("smallcheckbox");
- orientationCB.addValueChangeListener(ev -> {
- if (ev.isFromClient()) {
- toggleSplitterOrientation();
- }
+ // The source controls live inside SourceCodeViewer and signal across components
+ // via bubbling DOM events; collapse carries data, so it uses SourceCollapseChangedEvent.
+ addSourceCollapseListener(ev -> {
+ sourceCollapsed = ev.isCollapsed();
+ updateSplitterPosition();
});
- codeCB = new Checkbox("Show Source Code");
- codeCB.setValue(true);
- codeCB.addClassName("smallcheckbox");
- codeCB.addValueChangeListener(ev -> updateSplitterPosition());
- codePositionCB = new Checkbox("Toggle Code Position");
- codePositionCB.setValue(true);
- codePositionCB.addClassName("smallcheckbox");
- codePositionCB.addValueChangeListener(ev -> toggleSourcePosition());
+ getElement().addEventListener("source-flip", ev -> toggleSourcePosition(true));
+ getElement().addEventListener("source-rotate", ev -> toggleSplitterOrientation(true));
themeCB = new Checkbox("Dark Theme");
themeCB.setValue(false);
themeCB.addClassName("smallcheckbox");
@@ -117,7 +109,7 @@ public TabbedDemo() {
footer = new HorizontalLayout();
footer.setWidthFull();
footer.setJustifyContentMode(JustifyContentMode.END);
- footer.add(codeCB, codePositionCB, orientationCB, themeCB);
+ footer.add(themeCB);
footer.setClassName("demo-footer");
Package pkg = this.getClass().getPackage();
@@ -334,13 +326,11 @@ public void removeRouterLayoutContent(HasElement oldContent) {
private void updateSplitterPosition() {
if (currentLayout != null) {
- if (codeCB.getValue()) {
- currentLayout.showSourceCode();
- } else {
+ if (sourceCollapsed) {
currentLayout.hideSourceCode();
+ } else {
+ currentLayout.showSourceCode();
}
- orientationCB.setEnabled(codeCB.getValue());
- codePositionCB.setEnabled(codeCB.getValue());
}
}
@@ -350,20 +340,39 @@ private void updateSplitterPosition() {
* @param visible {@code true} to make the source code visible, {@code false} otherwise
*/
public void setSourceVisible(boolean visible) {
- codeCB.setValue(visible);
- codePositionCB.setVisible(visible);
+ fireSourceCollapseChangedEvent(!visible, false);
}
/**
* Toggles the position of the source code relative to the demo content.
*/
public void toggleSourcePosition() {
+ toggleSourcePosition(false);
+ }
+
+ private void toggleSourcePosition(boolean fromClient) {
+ if (currentLayout != null) {
+ setSourcePosition(currentLayout.getSourcePosition().toggle(), fromClient);
+ }
+ }
+
+ /**
+ * Sets the position of the source code relative to the demo content.
+ *
+ * @param sourcePosition the new source position
+ */
+ public void setSourcePosition(SourcePosition sourcePosition) {
+ setSourcePosition(sourcePosition, false);
+ }
+
+ private void setSourcePosition(SourcePosition sourcePosition, boolean fromClient) {
if (currentLayout != null) {
- currentLayout.toggleSourcePosition();
+ currentLayout.setSourcePosition(sourcePosition);
+ fireSourcePositionChangedEvent(sourcePosition, fromClient);
}
}
- private void toggleSplitterOrientation() {
+ private void toggleSplitterOrientation(boolean fromClient) {
if (currentLayout == null) {
return;
}
@@ -372,7 +381,7 @@ private void toggleSplitterOrientation() {
} else {
splitOrientation = Orientation.HORIZONTAL;
}
- setOrientation(splitOrientation);
+ setOrientation(splitOrientation, fromClient);
}
/**
@@ -390,12 +399,16 @@ public Orientation getOrientation() {
* @param orientation the new orientation
*/
public void setOrientation(Orientation orientation) {
+ setOrientation(orientation, false);
+ }
+
+ private void setOrientation(Orientation orientation, boolean fromClient) {
splitOrientation = orientation;
if (currentLayout != null) {
currentLayout.setOrientation(orientation);
currentLayout.setSplitterPosition(50);
}
- orientationCB.setValue(Orientation.HORIZONTAL.equals(orientation));
+ fireOrientationChangedEvent(orientation, fromClient);
}
/**
@@ -491,9 +504,6 @@ private static Stream collectThemeChangeObservers(Component
private void updateFooterButtonsVisibility() {
boolean hasSourceCode = currentLayout != null;
ComponentUtil.fireEvent(this, new TabbedDemoSourceEvent(this, hasSourceCode));
- orientationCB.setVisible(hasSourceCode);
- codeCB.setVisible(hasSourceCode);
- codePositionCB.setVisible(hasSourceCode);
}
/**
@@ -512,8 +522,7 @@ protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
getUI().ifPresent(ui -> ui.getPage().retrieveExtendedClientDetails(receiver -> {
boolean mobile = receiver.getBodyClientWidth() <= MOBILE_DEVICE_BREAKPOINT_WIDTH;
- codeCB.setValue(codeCB.getValue() && !mobile);
- codePositionCB.setValue(codeCB.getValue() && !mobile);
+ fireSourceCollapseChangedEvent(sourceCollapsed || mobile, false);
boolean portraitOrientation = receiver.getBodyClientHeight() > receiver.getBodyClientWidth();
adjustSplitOrientation(portraitOrientation);
@@ -557,4 +566,46 @@ private void setupDemoHelperButton(Class> helperClass) {
}
}
+ /**
+ * Adds a listener for {@link SourceCollapseChangedEvent}.
+ *
+ * @param listener the listener to add
+ */
+ public void addSourceCollapseListener(
+ ComponentEventListener listener) {
+ ComponentUtil.addListener(this, SourceCollapseChangedEvent.class, listener);
+ }
+
+ /**
+ * Adds a listener for {@link SourcePositionChangedEvent}.
+ *
+ * @param listener the listener to add
+ */
+ public void addSourcePositionChangedListener(
+ ComponentEventListener listener) {
+ ComponentUtil.addListener(this, SourcePositionChangedEvent.class, listener);
+ }
+
+ /**
+ * Adds a listener for {@link OrientationChangedEvent}.
+ *
+ * @param listener the listener to add
+ */
+ public void addOrientationChangedListener(
+ ComponentEventListener listener) {
+ ComponentUtil.addListener(this, OrientationChangedEvent.class, listener);
+ }
+
+ private void fireSourceCollapseChangedEvent(boolean collapsed, boolean fromClient) {
+ fireEvent(new SourceCollapseChangedEvent(this, fromClient, collapsed));
+ }
+
+ private void fireSourcePositionChangedEvent(SourcePosition sourcePosition, boolean fromClient) {
+ fireEvent(new SourcePositionChangedEvent(this, fromClient, sourcePosition));
+ }
+
+ private void fireOrientationChangedEvent(Orientation orientation, boolean fromClient) {
+ fireEvent(new OrientationChangedEvent(this, fromClient, orientation));
+ }
+
}
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/OrientationChangedEvent.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/OrientationChangedEvent.java
new file mode 100644
index 0000000..ba79252
--- /dev/null
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/OrientationChangedEvent.java
@@ -0,0 +1,38 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package com.flowingcode.vaadin.addons.demo.events;
+
+import com.flowingcode.vaadin.addons.demo.TabbedDemo;
+import com.vaadin.flow.component.ComponentEvent;
+import com.vaadin.flow.component.splitlayout.SplitLayout.Orientation;
+import lombok.Getter;
+
+@SuppressWarnings("serial")
+public class OrientationChangedEvent extends ComponentEvent {
+
+ @Getter
+ private Orientation orientation;
+
+ public OrientationChangedEvent(TabbedDemo source, boolean fromClient, Orientation orientation) {
+ super(source, fromClient);
+ this.orientation = orientation;
+ }
+
+}
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourceCollapseChangedEvent.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourceCollapseChangedEvent.java
new file mode 100644
index 0000000..e95daa9
--- /dev/null
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourceCollapseChangedEvent.java
@@ -0,0 +1,41 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2025 Flowing Code
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package com.flowingcode.vaadin.addons.demo.events;
+
+import com.flowingcode.vaadin.addons.demo.TabbedDemo;
+import com.vaadin.flow.component.ComponentEvent;
+import com.vaadin.flow.component.DomEvent;
+import com.vaadin.flow.component.EventData;
+import lombok.Getter;
+
+@SuppressWarnings("serial")
+@DomEvent("source-collapse-changed")
+public class SourceCollapseChangedEvent extends ComponentEvent {
+
+ @Getter
+ private boolean collapsed;
+
+ public SourceCollapseChangedEvent(TabbedDemo source, boolean fromClient,
+ @EventData("event.detail.collapsed") boolean collapsed) {
+ super(source, fromClient);
+ this.collapsed = collapsed;
+ }
+
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourcePositionChangedEvent.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourcePositionChangedEvent.java
new file mode 100644
index 0000000..b4b7d7a
--- /dev/null
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourcePositionChangedEvent.java
@@ -0,0 +1,39 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package com.flowingcode.vaadin.addons.demo.events;
+
+import com.flowingcode.vaadin.addons.demo.SourcePosition;
+import com.flowingcode.vaadin.addons.demo.TabbedDemo;
+import com.vaadin.flow.component.ComponentEvent;
+import lombok.Getter;
+
+@SuppressWarnings("serial")
+public class SourcePositionChangedEvent extends ComponentEvent {
+
+ @Getter
+ private SourcePosition sourcePosition;
+
+ public SourcePositionChangedEvent(TabbedDemo source, boolean fromClient,
+ SourcePosition sourcePosition) {
+ super(source, fromClient);
+ this.sourcePosition = sourcePosition;
+ }
+
+}
diff --git a/base/src/main/resources/META-INF/resources/frontend/commons-demo-iconset.ts b/base/src/main/resources/META-INF/resources/frontend/commons-demo-iconset.ts
new file mode 100644
index 0000000..1405b48
--- /dev/null
+++ b/base/src/main/resources/META-INF/resources/frontend/commons-demo-iconset.ts
@@ -0,0 +1,124 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2025 Flowing Code
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+import '@vaadin/icon/vaadin-icon.js';
+import { Iconset } from '@vaadin/icon/vaadin-iconset.js';
+import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+/**
+ISC License
+
+Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2023 as part of Feather (MIT).
+All other copyright (c) for Lucide are held by Lucide Contributors 2025.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+---
+
+The MIT License (MIT) (for portions derived from Feather)
+
+Copyright (c) 2013-2023 Cole Bemis
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+---
+
+Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
+License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
+Copyright 2024 Fonticons, Inc.
+
+---
+
+All brand icons are trademarks of their respective owners. The use of these
+trademarks does not indicate endorsement of the trademark holder by Font
+Awesome, nor vice versa. **Please do not use brand logos for any purpose except
+to represent the company, product, or service to which they refer.**
+
+*/
+
+registerStyles(
+ 'vaadin-button',
+ css`
+ [part] ::slotted(vaadin-icon[icon^='commons-demo:']), [part] ::slotted(iron-icon[icon^='commons-demo:'])
+ {
+ padding: 0.25em;
+ box-sizing: border-box !important;
+ }`,
+);
+
+const template = document.createElement('template');
+template.innerHTML = `
+`;
+
+customElements.whenDefined('vaadin-iconset').then(Iconset=>{
+ Iconset.register('commons-demo', 4, template);
+});
diff --git a/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/shared-styles.css b/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/shared-styles.css
index 6fef4f7..900482b 100644
--- a/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/shared-styles.css
+++ b/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/shared-styles.css
@@ -51,5 +51,122 @@ code-highlighter code {
cursor: pointer;
}
+.source-code-viewer {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ overflow: hidden;
+}
+
+.source-code-viewer-codeviewer-wrapper {
+ display: flex;
+ flex-grow: 1;
+ overflow: auto;
+}
+
+.source-code-viewer-codeviewer-wrapper > code-viewer {
+ flex-grow: 1;
+}
+
+.source-code-viewer-buttons-wrapper {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+}
+
+.source-code-viewer-buttons {
+ position: absolute;
+ z-index: 1;
+ right: calc(0.25rem + var(--code-viewer-gutter, 0px));
+ top: 0.25rem;
+ display: flex;
+ gap: var(--lumo-space-xs, 0.25rem);
+ container-type: inline-size;
+ width: 100%;
+ justify-content: end;
+ pointer-events: none;
+}
+
+vaadin-button.source-code-viewer-button {
+ color: color-mix(in srgb, #f8f8f2 70%, transparent);
+ pointer-events: auto;
+ background: transparent;
+ display: none;
+ padding: 0;
+}
+
+vaadin-button.source-code-viewer-button vaadin-icon {
+ --vaadin-icon-size: 20px;
+ padding: 0.125em;
+}
+
+@container style(--lumo-size-m) {
+ vaadin-button.source-code-viewer-button vaadin-icon {
+ padding: 0.25em;
+ }
+}
+
+
+vaadin-button.source-code-viewer-show-button {
+ display: var(--source-code-viewer-show-button-display, none);
+ color: var(--lumo-body-text-color, var(--vaadin-text-color));
+ position: fixed;
+}
+
+[orientation="horizontal"] [slot="primary"] vaadin-button.source-code-viewer-show-button {
+ right: 0;
+}
+
+[orientation="horizontal"] [slot="secondary"] vaadin-button.source-code-viewer-show-button {
+ right: 0.5rem;
+}
+
+[orientation="vertical"] [slot="primary"] vaadin-button.source-code-viewer-show-button {
+ top: 0.5rem;
+}
+
+[orientation="vertical"] [slot="secondary"] vaadin-button.source-code-viewer-show-button {
+ top: 0;
+}
+
+/* Rotate the icons to match the layout disposition.*/
+[orientation="horizontal"] [slot="secondary"] vaadin-button.source-code-viewer-button{
+ transform: rotate(0deg);
+ &.source-code-viewer-rotate-button { transform: scalex(-1) rotate(90deg); }
+}
+
+[orientation="horizontal"] [slot="primary"] vaadin-button.source-code-viewer-button {
+ transform: rotate(180deg);
+ &.source-code-viewer-rotate-button { transform: rotate(0deg); }
+}
+
+[orientation="vertical"] [slot="secondary"] vaadin-button.source-code-viewer-button {
+ transform: rotate(90deg);
+ &.source-code-viewer-rotate-button { transform: rotate(0deg); }
+}
+
+[orientation="vertical"] [slot="primary"] vaadin-button.source-code-viewer-button {
+ transform: rotate(270deg);
+ &.source-code-viewer-rotate-button { transform: scalex(-1) rotate(90deg); }
+}
+
+@container (min-width: 82px) {
+ vaadin-button.source-code-viewer-flip-button {
+ display: block;
+ }
+}
+
+@container (min-width: 53px) {
+ vaadin-button.source-code-viewer-rotate-button {
+ display: block;
+ }
+}
+
+@container (min-width: 24px) {
+ vaadin-button.source-code-viewer-hide-button {
+ display: block;
+ }
+}
+
.commons-demo-split-layout { overflow: hidden }
.commons-demo-split-layout > [slot] { overflow: auto; }
\ No newline at end of file