diff --git a/base/pom.xml b/base/pom.xml
index 7da138a..d2e3426 100644
--- a/base/pom.xml
+++ b/base/pom.xml
@@ -5,7 +5,7 @@
com.flowingcode.vaadin.addons.democommons-demo
- 5.3.2-SNAPSHOT
+ 5.4.0-SNAPSHOTCommons DemoCommon classes for add-ons demo
@@ -317,6 +317,56 @@
+
+ dance
+
+
+
+ org.apache.maven.plugins
+ maven-clean-plugin
+ 3.3.2
+
+
+
+ ${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..49277fc
--- /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 CommonsDemo icons.
+ */
+ @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/MultiSourceCodeViewer.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java
index 1fccdc2..11baff5 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java
@@ -76,6 +76,20 @@ public MultiSourceCodeViewer(List sourceCodeTabs, 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);
+
+ // Non-scrolling overlay so the buttons stay pinned while the code scrolls
+ buttonsWrapper = new Div();
+ buttonsWrapper.addClassName("source-code-viewer-buttons-wrapper");
+
+ add(codeViewerWrapper, buttonsWrapper);
+
setProperties(properties);
- addAttachListener(ev -> fetchContents(url, language));
+ addAttachListener(ev -> {
+ fetchContents(url, language);
+ observeScrollbar();
+ });
+ }
+
+ /**
+ * Adds the overlay controls (show, hide, flip and rotate) pinned over the source code.
+ *
+ * The buttons do not change this viewer directly. Instead, each one dispatches a bubbling DOM
+ * event that is expected to be handled by an enclosing layout, which is what actually collapses,
+ * repositions or reorients the source panel:
+ *
+ * The buttons are therefore only useful when this viewer is placed inside a layout that listens
+ * for those events and coordinates the response (see {@link TabbedDemo}); otherwise they emit
+ * events that nothing consumes.
+ *
+ * The controls are rendered by the {@code source-code-viewer-buttons} client-side web component,
+ * which dispatches the events locally on click; the enclosing layout (see {@link TabbedDemo})
+ * handles them through Vaadin server-side listeners. This method is idempotent: the component is
+ * added only once.
+ */
+ public void withButtons() {
+ if (buttonsWrapper.getElement().getChildCount() == 0) {
+ buttonsWrapper.getElement().appendChild(new Element("source-code-viewer-buttons"));
+ }
+ }
+
+ /**
+ * 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 dispatchEvent(ComponentEvent extends Component> ev) {
+ for (Component c = this; c != null; c = c.getParent().orElse(null)) {
+ ComponentUtil.fireEvent(c, ev);
+ }
}
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..d093d54 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
@@ -48,7 +48,7 @@ public SplitLayoutDemo(Component demo, List tabs) {
properties.put("vaadin", VaadinVersion.getVaadinVersion());
properties.put("flow", Version.getFullVersion());
- code = new MultiSourceCodeViewer(tabs, properties);
+ code = new MultiSourceCodeViewer(tabs, properties).withButtons();
this.demo = demo;
setSourcePosition(code.getSourcePosition());
@@ -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()
@@ -93,26 +93,22 @@ public Orientation getOrientation() {
return getContent().getOrientation();
}
- public void setSplitterPosition(int pos) {
- getContent().setSplitterPosition(pos);
- }
-
public void setSizeFull() {
getContent().setSizeFull();
}
- public void showSourceCode() {
- getContent().setSplitterPosition(50);
- }
-
- public void hideSourceCode() {
- switch (sourcePosition) {
- case PRIMARY:
- getContent().setSplitterPosition(0);
- break;
- case SECONDARY:
- getContent().setSplitterPosition(100);
- break;
+ public void setSourceCollapsed(boolean collapsed) {
+ if (!collapsed) {
+ getContent().setSplitterPosition(50);
+ } else {
+ switch (sourcePosition) {
+ case PRIMARY:
+ getContent().setSplitterPosition(0);
+ break;
+ case SECONDARY:
+ getContent().setSplitterPosition(100);
+ break;
+ }
}
}
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..267791b 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,14 +73,11 @@ 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;
private DemoHelperViewer demoHelperViewer;
@@ -89,23 +89,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 +108,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();
@@ -231,6 +222,11 @@ public void showRouterLayoutContent(HasElement content) {
createSourceCodeTab(demo.getClass(), demoSource).ifPresent(sourceTabs::add);
}
+ Orientation splitOrientation = null;
+ if (currentLayout != null) {
+ splitOrientation = currentLayout.getOrientation();
+ }
+
if (!sourceTabs.isEmpty()) {
currentLayout = new SplitLayoutDemo(demo, sourceTabs);
if (currentLayout.isEmpty()) {
@@ -334,13 +330,7 @@ public void removeRouterLayoutContent(HasElement oldContent) {
private void updateSplitterPosition() {
if (currentLayout != null) {
- if (codeCB.getValue()) {
- currentLayout.showSourceCode();
- } else {
- currentLayout.hideSourceCode();
- }
- orientationCB.setEnabled(codeCB.getValue());
- codePositionCB.setEnabled(codeCB.getValue());
+ currentLayout.setSourceCollapsed(sourceCollapsed);
}
}
@@ -350,29 +340,50 @@ 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) {
- currentLayout.toggleSourcePosition();
+ setSourcePosition(currentLayout.getSourcePosition().toggle(), fromClient);
}
}
- private void toggleSplitterOrientation() {
+ /**
+ * 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.setSourcePosition(sourcePosition);
+ fireSourcePositionChangedEvent(sourcePosition, fromClient);
+ }
+ }
+
+ private void toggleSplitterOrientation(boolean fromClient) {
if (currentLayout == null) {
return;
}
+
+ Orientation splitOrientation = getOrientation();
if (Orientation.HORIZONTAL.equals(splitOrientation)) {
splitOrientation = Orientation.VERTICAL;
} else {
splitOrientation = Orientation.HORIZONTAL;
}
- setOrientation(splitOrientation);
+ setOrientation(splitOrientation, fromClient);
}
/**
@@ -390,12 +401,15 @@ public Orientation getOrientation() {
* @param orientation the new orientation
*/
public void setOrientation(Orientation orientation) {
- splitOrientation = orientation;
- if (currentLayout != null) {
- currentLayout.setOrientation(orientation);
- currentLayout.setSplitterPosition(50);
+ setOrientation(orientation, false);
+ }
+
+ private void setOrientation(Orientation orientation, boolean fromClient) {
+ if (currentLayout != null && orientation != getOrientation()) {
+ currentLayout.setOrientation(orientation);
+ currentLayout.setSourceCollapsed(false);
+ fireOrientationChangedEvent(orientation, fromClient);
}
- orientationCB.setValue(Orientation.HORIZONTAL.equals(orientation));
}
/**
@@ -491,9 +505,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 +523,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);
@@ -522,11 +532,10 @@ protected void onAttach(AttachEvent attachEvent) {
private void adjustSplitOrientation(boolean portraitOrientation) {
if (portraitOrientation) {
- splitOrientation = Orientation.VERTICAL;
+ setOrientation(Orientation.VERTICAL);
} else {
- splitOrientation = Orientation.HORIZONTAL;
+ setOrientation(Orientation.HORIZONTAL);
}
- setOrientation(splitOrientation);
}
/**
@@ -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..ba77586
--- /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', 24, template);
+});
diff --git a/base/src/main/resources/META-INF/resources/frontend/source-code-viewer-buttons.ts b/base/src/main/resources/META-INF/resources/frontend/source-code-viewer-buttons.ts
new file mode 100644
index 0000000..f83182f
--- /dev/null
+++ b/base/src/main/resources/META-INF/resources/frontend/source-code-viewer-buttons.ts
@@ -0,0 +1,70 @@
+/*-
+ * #%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%
+ */
+import {
+ html,
+ LitElement
+} from "lit-element";
+
+import {customElement} from 'lit/decorators/custom-element.js';
+import '@vaadin/button/vaadin-button.js';
+import './commons-demo-iconset.js';
+
+/**
+ * Overlay controls pinned over the source code. Each button dispatches a bubbling DOM event that is
+ * handled by an enclosing layout (see TabbedDemo), which is what actually collapses, repositions or
+ * reorients the source panel. Firing the events on the client avoids a server roundtrip on click.
+ */
+@customElement("source-code-viewer-buttons")
+export class SourceCodeViewerButtons extends LitElement {
+
+ // Render in light DOM so the shared stylesheet (shared-styles.css) styles the buttons.
+ createRenderRoot() {
+ return this;
+ }
+
+ private fire(type: string, detail?: any) {
+ this.dispatchEvent(new CustomEvent(type, {bubbles: true, detail}));
+ }
+
+ render() {
+ return html`
+ this.fire('source-collapse-changed', {collapsed: false})}>
+
+
+ this.fire('source-collapse-changed', {collapsed: true})}>
+
+
+ this.fire('source-flip')}>
+
+
+ this.fire('source-rotate')}>
+
+
+ `;
+ }
+}
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..be7f6fb 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
diff --git a/pom.xml b/pom.xml
index 9e9e1af..362867d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
com.flowingcode.vaadin.addons.democommons-demo-aggregator
- 5.3.2-SNAPSHOT
+ 5.4.0-SNAPSHOTpomCommons Demo Aggregator
diff --git a/processor/pom.xml b/processor/pom.xml
index fbd8a0c..48f9113 100644
--- a/processor/pom.xml
+++ b/processor/pom.xml
@@ -5,7 +5,7 @@
com.flowingcode.vaadin.addons.democommons-demo-processor
- 5.3.2-SNAPSHOT
+ 5.4.0-SNAPSHOTCommons Demo ProcessorAnnotation processor for Commons Demo: copies @DemoSource-referenced files into the class output
@@ -23,7 +23,6 @@
UTF-81717
- 5.3.2-SNAPSHOT
@@ -50,7 +49,7 @@
com.flowingcode.vaadin.addons.democommons-demo
- ${commons-demo.version}
+ ${project.version}provided