From 6b7ff7a442d0a47ea7f3bb36b1dca51ad47c8a5c Mon Sep 17 00:00:00 2001 From: Jay George Date: Mon, 8 Jun 2026 11:27:33 +0100 Subject: [PATCH 01/79] Tree view - start prototype --- resources/js/pages/forms/Logic.vue | 40 +++++++++++++++++++------- resources/js/pages/forms/LogicTree.vue | 14 +++++++++ 2 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 resources/js/pages/forms/LogicTree.vue diff --git a/resources/js/pages/forms/Logic.vue b/resources/js/pages/forms/Logic.vue index dc3b579b4e..60fc297c38 100644 --- a/resources/js/pages/forms/Logic.vue +++ b/resources/js/pages/forms/Logic.vue @@ -2,9 +2,10 @@ import Layout from '@/pages/layout/Layout.vue'; import PanelLayout from '@/pages/layout/PanelLayout.vue'; import FormsLayout from './Layout.vue'; -import { Button, Header, Icon, StatusIndicator } from '@ui'; +import { Button, Header, Icon, StatusIndicator, ToggleGroup, ToggleItem } from '@ui'; import FieldLogic from '@/components/forms/logic/FieldLogic.vue'; import PageLogic from '@/components/forms/logic/PageLogic.vue'; +import LogicTree from './LogicTree.vue'; import Head from '@/pages/layout/Head.vue'; import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; import { keys } from '@api'; @@ -22,6 +23,7 @@ const props = defineProps({ const pages = ref(props.pages); const fields = ref(props.fields); +const logicView = ref('list'); const saving = ref(false); const saveBinding = ref(null); const errors = ref({}); @@ -110,18 +112,34 @@ onUnmounted(() => { {{ form.title }} + - - - + + + + + + diff --git a/resources/js/pages/forms/LogicTree.vue b/resources/js/pages/forms/LogicTree.vue new file mode 100644 index 0000000000..fc038655c3 --- /dev/null +++ b/resources/js/pages/forms/LogicTree.vue @@ -0,0 +1,14 @@ + + + From 5472a6e34c220588ce91ec64c9e764632f72414d Mon Sep 17 00:00:00 2001 From: Jay George Date: Mon, 8 Jun 2026 12:07:41 +0100 Subject: [PATCH 02/79] Temp unhide Pro --- src/Statamic.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Statamic.php b/src/Statamic.php index 7b0ce2bded..03afe4708b 100644 --- a/src/Statamic.php +++ b/src/Statamic.php @@ -57,7 +57,8 @@ public static function enablePro() public static function formsProInstalled(): bool { - return Composer::isInstalled('statamic/forms-pro'); + // return Composer::isInstalled('statamic/forms-pro'); + return true; } public static function availableScripts(Request $request) From 5ed3e361d627a48ee50f1ad4b528890ad23ea35b Mon Sep 17 00:00:00 2001 From: Jay George Date: Mon, 8 Jun 2026 12:07:56 +0100 Subject: [PATCH 03/79] Tree view - look through fields --- resources/js/pages/forms/LogicTree.vue | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/resources/js/pages/forms/LogicTree.vue b/resources/js/pages/forms/LogicTree.vue index fc038655c3..6cdafd887e 100644 --- a/resources/js/pages/forms/LogicTree.vue +++ b/resources/js/pages/forms/LogicTree.vue @@ -8,7 +8,13 @@ defineProps({ From 79ec46446e5a1c435cfeb8e87debe40f570f3c10 Mon Sep 17 00:00:00 2001 From: Jay George Date: Mon, 8 Jun 2026 16:58:32 +0100 Subject: [PATCH 04/79] Tree view - break into pages --- resources/js/pages/forms/LogicTree.vue | 42 ++++++++++++++++++++------ 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/resources/js/pages/forms/LogicTree.vue b/resources/js/pages/forms/LogicTree.vue index 6cdafd887e..4719d2d336 100644 --- a/resources/js/pages/forms/LogicTree.vue +++ b/resources/js/pages/forms/LogicTree.vue @@ -1,20 +1,44 @@ From b92302537c61a942013c052b31e065f1c5dfc1ec Mon Sep 17 00:00:00 2001 From: Jay George Date: Mon, 8 Jun 2026 17:51:41 +0100 Subject: [PATCH 05/79] Tree view - connector experiments - demo 1 --- resources/js/pages/forms/Logic.vue | 8 +- resources/js/pages/forms/LogicTree.vue | 255 +++++++++++++++++++++---- 2 files changed, 218 insertions(+), 45 deletions(-) diff --git a/resources/js/pages/forms/Logic.vue b/resources/js/pages/forms/Logic.vue index 60fc297c38..e3cd359f8a 100644 --- a/resources/js/pages/forms/Logic.vue +++ b/resources/js/pages/forms/Logic.vue @@ -136,12 +136,6 @@ onUnmounted(() => { /> - + diff --git a/resources/js/pages/forms/LogicTree.vue b/resources/js/pages/forms/LogicTree.vue index 4719d2d336..a8ce8a0da3 100644 --- a/resources/js/pages/forms/LogicTree.vue +++ b/resources/js/pages/forms/LogicTree.vue @@ -1,44 +1,223 @@ + + From 1953f716590ab8fe18323220b76b78f78a74d954 Mon Sep 17 00:00:00 2001 From: Jay George Date: Tue, 9 Jun 2026 09:16:58 +0100 Subject: [PATCH 06/79] Put logic tree CSS in separate file --- resources/css/components/logic-tree.css | 119 +++++++++++++++++++++++ resources/css/cp.css | 1 + resources/js/pages/forms/LogicTree.vue | 122 ------------------------ 3 files changed, 120 insertions(+), 122 deletions(-) create mode 100644 resources/css/components/logic-tree.css diff --git a/resources/css/components/logic-tree.css b/resources/css/components/logic-tree.css new file mode 100644 index 0000000000..8fd451c515 --- /dev/null +++ b/resources/css/components/logic-tree.css @@ -0,0 +1,119 @@ +/* https://codepen.io/cbolson/pen/emzegWP — flat CSS equivalent (Vue SFC can't use nested @container) */ +.linked-list { + --checkbox-checked-border-color: dodgerblue; + --radio-checked-border-color: dodgerblue; + --join-stroke: 1px; + --join-line: var(--join-stroke) dashed dodgerblue; + --join-radius: 20px; + --gap: 10vw; + --clr-lines: rgba(0 0 0 / 0.25); + + position: relative; + display: grid; + align-items: start; + grid-template-columns: 1fr 1fr; + width: min(100%, 800px); + gap: var(--gap); +} + +:where(.dark) .linked-list { + --clr-lines: rgba(255 255 255 / 0.25); +} + +.linked-list .list { + margin: 0; + padding: 0; + list-style: none; + display: grid; + gap: 0.5rem; +} + +.linked-list .list > li { + border: 1px solid var(--clr-lines); + display: flex; + align-items: center; +} + +.linked-list .list > li > label { + flex: 1; + padding: 0.5rem; + cursor: pointer; +} + +.linked-list .list.items li:has(:checked) label { + anchor-name: --checked-option; + anchor-scope: --checked-option; + border-color: var(--checkbox-checked-border-color); +} + +.linked-list .list.items li:has(:checked) label::before, +.linked-list .list.items li:has(:checked) label::after { + content: ''; + position: absolute; + pointer-events: none; + border: var(--join-line); + right: calc(anchor(left --radio-option) + var(--gap) / 2); + left: anchor(right --checked-option); +} + +.linked-list .list.items li:has(:checked) label::after { + right: anchor(left --radio-option); + left: calc(anchor(left --radio-option) - var(--gap) / 2 - var(--join-stroke)); +} + +/* source above target */ +.linked-list .list.items li[data-relation="-1"]:has(:checked) label::before { + border-left-color: transparent; + border-bottom-color: transparent; + border-radius: 0 var(--join-radius) 0 0; + top: anchor(center --checked-option); + bottom: anchor(top --radio-option); +} + +.linked-list .list.items li[data-relation="-1"]:has(:checked) label::after { + border-right-color: transparent; + border-top-color: transparent; + border-radius: 0 0 0 var(--join-radius); + top: calc(anchor(top --radio-option) - var(--join-stroke)); + height: var(--join-radius); + bottom: auto; +} + +/* source below target */ +.linked-list .list.items li[data-relation="1"]:has(:checked) label::before { + border-left-color: transparent; + border-top-color: transparent; + border-radius: 0 0 var(--join-radius) 0; + top: anchor(bottom --radio-option); + bottom: anchor(center --checked-option); +} + +.linked-list .list.items li[data-relation="1"]:has(:checked) label::after { + border-right-color: transparent; + border-bottom-color: transparent; + border-radius: var(--join-radius) 0 0 0; + top: auto; + bottom: calc(anchor(bottom --radio-option) - var(--join-stroke)); + height: var(--join-radius); +} + +/* source level with target */ +.linked-list .list.items li[data-relation="0"]:has(:checked) label::before { + border-top-color: transparent; + border-right-color: transparent; + border-left-color: transparent; + border-radius: 0; + top: calc(anchor(center --radio-option) - var(--join-stroke)); + bottom: calc(anchor(center --checked-option) + var(--join-stroke)); + right: anchor(left --radio-option); + left: anchor(right --checked-option); +} + +.linked-list .list.items li[data-relation="0"]:has(:checked) label::after { + display: none; +} + +.linked-list .list.options li:has(:checked) { + anchor-name: --radio-option; + border-color: var(--radio-checked-border-color); +} diff --git a/resources/css/cp.css b/resources/css/cp.css index cb32123428..c8a08ded27 100644 --- a/resources/css/cp.css +++ b/resources/css/cp.css @@ -18,6 +18,7 @@ @import './components/notifications.css'; @import './components/page-tree.css'; @import './components/forms.css'; +@import './components/logic-tree.css'; @import './components/pagination.css'; @import './components/popover.css'; @import './components/preview.css'; diff --git a/resources/js/pages/forms/LogicTree.vue b/resources/js/pages/forms/LogicTree.vue index a8ce8a0da3..b4802afe93 100644 --- a/resources/js/pages/forms/LogicTree.vue +++ b/resources/js/pages/forms/LogicTree.vue @@ -99,125 +99,3 @@ onMounted(updateRelations); - - From 3ea076b4cf1eabfd3604814833eb66def7970a24 Mon Sep 17 00:00:00 2001 From: Jay George Date: Tue, 9 Jun 2026 09:20:23 +0100 Subject: [PATCH 07/79] Remember Logic view preference --- resources/js/pages/forms/Logic.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/js/pages/forms/Logic.vue b/resources/js/pages/forms/Logic.vue index e3cd359f8a..eae947e5f9 100644 --- a/resources/js/pages/forms/Logic.vue +++ b/resources/js/pages/forms/Logic.vue @@ -8,7 +8,7 @@ import PageLogic from '@/components/forms/logic/PageLogic.vue'; import LogicTree from './LogicTree.vue'; import Head from '@/pages/layout/Head.vue'; import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; -import { keys } from '@api'; +import { keys, preferences } from '@api'; import axios from 'axios'; defineOptions({ layout: [Layout, PanelLayout, FormsLayout] }); @@ -23,7 +23,9 @@ const props = defineProps({ const pages = ref(props.pages); const fields = ref(props.fields); -const logicView = ref('list'); +const logicViewPreferenceKey = 'forms.logic.view'; +const logicView = ref(preferences.get(logicViewPreferenceKey, 'list')); +watch(logicView, (view) => preferences.set(logicViewPreferenceKey, view)); const saving = ref(false); const saveBinding = ref(null); const errors = ref({}); From c799c2f62e01c6e6d16519daa86a5eb04a436606 Mon Sep 17 00:00:00 2001 From: Jay George Date: Tue, 9 Jun 2026 09:46:19 +0100 Subject: [PATCH 08/79] Nest CSS --- resources/css/components/forms/logic-tree.css | 125 ++++++++++++++++++ resources/css/components/logic-tree.css | 119 ----------------- resources/css/cp.css | 2 +- 3 files changed, 126 insertions(+), 120 deletions(-) create mode 100644 resources/css/components/forms/logic-tree.css delete mode 100644 resources/css/components/logic-tree.css diff --git a/resources/css/components/forms/logic-tree.css b/resources/css/components/forms/logic-tree.css new file mode 100644 index 0000000000..bef42439aa --- /dev/null +++ b/resources/css/components/forms/logic-tree.css @@ -0,0 +1,125 @@ +/* https://codepen.io/cbolson/pen/emzegWP */ +.linked-list { + --checkbox-checked-border-color: var(--color-blue-400); + --radio-checked-border-color: var(--color-blue-400); + --join-stroke: 1px; + --join-line: var(--join-stroke) dashed var(--color-blue-400); + --join-radius: 25px; + --gap: 10vw; + --clr-lines: rgba(0 0 0 / 0.25); + + position: relative; + display: grid; + align-items: start; + grid-template-columns: 1fr 1fr; + width: min(100%, 800px); + gap: var(--gap); + + :where(.dark) & { + --clr-lines: rgba(255 255 255 / 0.25); + } + + .list { + margin: 0; + padding: 0; + list-style: none; + display: grid; + gap: 0.5rem; + + & > li { + border: 1px solid var(--clr-lines); + display: flex; + align-items: center; + + & > label { + flex: 1; + padding: 0.5rem; + cursor: pointer; + } + } + + &.items li:has(:checked) label { + anchor-name: --checked-option; + anchor-scope: --checked-option; + border-color: var(--checkbox-checked-border-color); + + &::before, + &::after { + content: ''; + position: absolute; + pointer-events: none; + border: var(--join-line); + right: calc(anchor(left --radio-option) + var(--gap) / 2); + left: anchor(right --checked-option); + } + + &::after { + right: anchor(left --radio-option); + left: calc(anchor(left --radio-option) - var(--gap) / 2 - var(--join-stroke)); + } + } + + /* source above target */ + &.items li[data-relation="-1"]:has(:checked) label { + &::before { + border-left-color: transparent; + border-bottom-color: transparent; + border-radius: 0 var(--join-radius) 0 0; + top: anchor(center --checked-option); + bottom: anchor(top --radio-option); + } + + &::after { + border-right-color: transparent; + border-top-color: transparent; + border-radius: 0 0 0 var(--join-radius); + top: calc(anchor(top --radio-option) - var(--join-stroke)); + height: var(--join-radius); + bottom: auto; + } + } + + /* source below target */ + &.items li[data-relation="1"]:has(:checked) label { + &::before { + border-left-color: transparent; + border-top-color: transparent; + border-radius: 0 0 var(--join-radius) 0; + top: anchor(bottom --radio-option); + bottom: anchor(center --checked-option); + } + + &::after { + border-right-color: transparent; + border-bottom-color: transparent; + border-radius: var(--join-radius) 0 0 0; + top: auto; + bottom: calc(anchor(bottom --radio-option) - var(--join-stroke)); + height: var(--join-radius); + } + } + + /* source level with target */ + &.items li[data-relation="0"]:has(:checked) label { + &::before { + border-top-color: transparent; + border-right-color: transparent; + border-left-color: transparent; + border-radius: 0; + top: calc(anchor(center --radio-option) - var(--join-stroke)); + bottom: calc(anchor(center --checked-option) + var(--join-stroke)); + right: anchor(left --radio-option); + left: anchor(right --checked-option); + } + + &::after { + display: none; + } + } + + &.options li:has(:checked) { + anchor-name: --radio-option; + border-color: var(--radio-checked-border-color); + } + } +} diff --git a/resources/css/components/logic-tree.css b/resources/css/components/logic-tree.css deleted file mode 100644 index 8fd451c515..0000000000 --- a/resources/css/components/logic-tree.css +++ /dev/null @@ -1,119 +0,0 @@ -/* https://codepen.io/cbolson/pen/emzegWP — flat CSS equivalent (Vue SFC can't use nested @container) */ -.linked-list { - --checkbox-checked-border-color: dodgerblue; - --radio-checked-border-color: dodgerblue; - --join-stroke: 1px; - --join-line: var(--join-stroke) dashed dodgerblue; - --join-radius: 20px; - --gap: 10vw; - --clr-lines: rgba(0 0 0 / 0.25); - - position: relative; - display: grid; - align-items: start; - grid-template-columns: 1fr 1fr; - width: min(100%, 800px); - gap: var(--gap); -} - -:where(.dark) .linked-list { - --clr-lines: rgba(255 255 255 / 0.25); -} - -.linked-list .list { - margin: 0; - padding: 0; - list-style: none; - display: grid; - gap: 0.5rem; -} - -.linked-list .list > li { - border: 1px solid var(--clr-lines); - display: flex; - align-items: center; -} - -.linked-list .list > li > label { - flex: 1; - padding: 0.5rem; - cursor: pointer; -} - -.linked-list .list.items li:has(:checked) label { - anchor-name: --checked-option; - anchor-scope: --checked-option; - border-color: var(--checkbox-checked-border-color); -} - -.linked-list .list.items li:has(:checked) label::before, -.linked-list .list.items li:has(:checked) label::after { - content: ''; - position: absolute; - pointer-events: none; - border: var(--join-line); - right: calc(anchor(left --radio-option) + var(--gap) / 2); - left: anchor(right --checked-option); -} - -.linked-list .list.items li:has(:checked) label::after { - right: anchor(left --radio-option); - left: calc(anchor(left --radio-option) - var(--gap) / 2 - var(--join-stroke)); -} - -/* source above target */ -.linked-list .list.items li[data-relation="-1"]:has(:checked) label::before { - border-left-color: transparent; - border-bottom-color: transparent; - border-radius: 0 var(--join-radius) 0 0; - top: anchor(center --checked-option); - bottom: anchor(top --radio-option); -} - -.linked-list .list.items li[data-relation="-1"]:has(:checked) label::after { - border-right-color: transparent; - border-top-color: transparent; - border-radius: 0 0 0 var(--join-radius); - top: calc(anchor(top --radio-option) - var(--join-stroke)); - height: var(--join-radius); - bottom: auto; -} - -/* source below target */ -.linked-list .list.items li[data-relation="1"]:has(:checked) label::before { - border-left-color: transparent; - border-top-color: transparent; - border-radius: 0 0 var(--join-radius) 0; - top: anchor(bottom --radio-option); - bottom: anchor(center --checked-option); -} - -.linked-list .list.items li[data-relation="1"]:has(:checked) label::after { - border-right-color: transparent; - border-bottom-color: transparent; - border-radius: var(--join-radius) 0 0 0; - top: auto; - bottom: calc(anchor(bottom --radio-option) - var(--join-stroke)); - height: var(--join-radius); -} - -/* source level with target */ -.linked-list .list.items li[data-relation="0"]:has(:checked) label::before { - border-top-color: transparent; - border-right-color: transparent; - border-left-color: transparent; - border-radius: 0; - top: calc(anchor(center --radio-option) - var(--join-stroke)); - bottom: calc(anchor(center --checked-option) + var(--join-stroke)); - right: anchor(left --radio-option); - left: anchor(right --checked-option); -} - -.linked-list .list.items li[data-relation="0"]:has(:checked) label::after { - display: none; -} - -.linked-list .list.options li:has(:checked) { - anchor-name: --radio-option; - border-color: var(--radio-checked-border-color); -} diff --git a/resources/css/cp.css b/resources/css/cp.css index c8a08ded27..d3aef6130c 100644 --- a/resources/css/cp.css +++ b/resources/css/cp.css @@ -18,7 +18,7 @@ @import './components/notifications.css'; @import './components/page-tree.css'; @import './components/forms.css'; -@import './components/logic-tree.css'; +@import './components/forms/logic-tree.css'; @import './components/pagination.css'; @import './components/popover.css'; @import './components/preview.css'; From 837d886f2f24308578dac20bfc376261fe68dd18 Mon Sep 17 00:00:00 2001 From: Jay George Date: Tue, 9 Jun 2026 10:08:37 +0100 Subject: [PATCH 09/79] Tree view CSS - tidy and explain everything --- resources/css/components/forms/logic-tree.css | 259 +++++++++++------- 1 file changed, 155 insertions(+), 104 deletions(-) diff --git a/resources/css/components/forms/logic-tree.css b/resources/css/components/forms/logic-tree.css index bef42439aa..e72ff0fc25 100644 --- a/resources/css/components/forms/logic-tree.css +++ b/resources/css/components/forms/logic-tree.css @@ -1,125 +1,176 @@ -/* https://codepen.io/cbolson/pen/emzegWP */ -.linked-list { - --checkbox-checked-border-color: var(--color-blue-400); - --radio-checked-border-color: var(--color-blue-400); - --join-stroke: 1px; - --join-line: var(--join-stroke) dashed var(--color-blue-400); - --join-radius: 25px; - --gap: 10vw; - --clr-lines: rgba(0 0 0 / 0.25); - - position: relative; - display: grid; - align-items: start; - grid-template-columns: 1fr 1fr; - width: min(100%, 800px); - gap: var(--gap); - - :where(.dark) & { - --clr-lines: rgba(255 255 255 / 0.25); - } +/* GROUP LOGIC TREE CONNECTORS +=================================================== */ +/* + URL: /cp/forms/{handle}/logic (Tree view) - .list { - margin: 0; - padding: 0; - list-style: none; - display: grid; - gap: 0.5rem; + Connects checked source fields (left column) to the selected destination + (right column) using CSS anchor positioning. Inspired by: + https://codepen.io/cbolson/pen/emzegWP - & > li { - border: 1px solid var(--clr-lines); - display: flex; - align-items: center; + Anatomy (source above target, data-relation="-1"): - & > label { - flex: 1; - padding: 0.5rem; - cursor: pointer; - } - } + [source]────┐ + │ ::before — source leg (horizontal out + vertical down) + │ + └──────┐ ::after — destination leg (horizontal in + corner) + │ + [destination] - &.items li:has(:checked) label { - anchor-name: --checked-option; - anchor-scope: --checked-option; - border-color: var(--checkbox-checked-border-color); - - &::before, - &::after { - content: ''; - position: absolute; - pointer-events: none; - border: var(--join-line); - right: calc(anchor(left --radio-option) + var(--gap) / 2); - left: anchor(right --checked-option); - } + Each connector is two pseudo-elements on the source label: - &::after { - right: anchor(left --radio-option); - left: calc(anchor(left --radio-option) - var(--gap) / 2 - var(--join-stroke)); - } + ::before — left half of the gap + vertical leg from the source + ::after — right half of the gap + rounded corner at the destination + + JS (LogicTree.vue) sets data-relation on each source
  • by comparing + list index to the checked destination: + + -1 source above destination + 0 source level with destination (straight line, no ::after) + 1 source below destination +*/ +@layer components { + /* Inspired by https://codepen.io/cbolson/pen/emzegWP */ + .linked-list { + --checkbox-checked-border-color: var(--color-blue-400); + --radio-checked-border-color: var(--color-blue-400); + --join-stroke: 1px; + --join-line: var(--join-stroke) dashed var(--color-blue-400); + --join-radius: 15px; + --gap: 10vw; + --clr-lines: rgba(0 0 0 / 0.25); + + /* GROUP LOGIC TREE CONNECTORS / LAYOUT + =================================================== */ + position: relative; + display: grid; + align-items: start; + grid-template-columns: 1fr 1fr; + width: min(100%, 800px); + gap: var(--gap); + + :where(.dark) & { + --clr-lines: rgba(255 255 255 / 0.25); } - /* source above target */ - &.items li[data-relation="-1"]:has(:checked) label { - &::before { - border-left-color: transparent; - border-bottom-color: transparent; - border-radius: 0 var(--join-radius) 0 0; - top: anchor(center --checked-option); - bottom: anchor(top --radio-option); - } + .list { + margin: 0; + padding: 0; + list-style: none; + display: grid; + gap: 0.5rem; + + & > li { + border: 1px solid var(--clr-lines); + display: flex; + align-items: center; - &::after { - border-right-color: transparent; - border-top-color: transparent; - border-radius: 0 0 0 var(--join-radius); - top: calc(anchor(top --radio-option) - var(--join-stroke)); - height: var(--join-radius); - bottom: auto; + & > label { + flex: 1; + padding: 0.5rem; + cursor: pointer; + } } - } - /* source below target */ - &.items li[data-relation="1"]:has(:checked) label { - &::before { - border-left-color: transparent; - border-top-color: transparent; - border-radius: 0 0 var(--join-radius) 0; - top: anchor(bottom --radio-option); - bottom: anchor(center --checked-option); + /* GROUP LOGIC TREE CONNECTORS / SOURCE ANCHOR + =================================================== */ + &.items li:has(:checked) label { + anchor-name: --checked-option; + anchor-scope: --checked-option; /* one anchor per checked source */ + border-color: var(--checkbox-checked-border-color); + + /* GROUP LOGIC TREE CONNECTORS / CONNECTOR BASE + =================================================== */ + /* Shared by all relations — sets the horizontal width of both legs. + ::before spans source right edge → gap midpoint. + ::after spans gap midpoint → destination left edge. */ + &::before, + &::after { + content: ''; + position: absolute; + pointer-events: none; + border: var(--join-line); + right: calc(anchor(left --radio-option) + var(--gap) / 2); + left: anchor(right --checked-option); + } + + &::after { + right: anchor(left --radio-option); + left: calc(anchor(left --radio-option) - var(--gap) / 2 - var(--join-stroke)); + } } - &::after { - border-right-color: transparent; - border-bottom-color: transparent; - border-radius: var(--join-radius) 0 0 0; - top: auto; - bottom: calc(anchor(bottom --radio-option) - var(--join-stroke)); - height: var(--join-radius); + /* GROUP LOGIC TREE CONNECTORS / RELATION -1 (SOURCE ABOVE) + =================================================== */ + &.items li[data-relation="-1"]:has(:checked) label { + /* ::before — source leg: out from source centre, down to destination top */ + &::before { + border-left-color: transparent; + border-bottom-color: transparent; + border-radius: 0 var(--join-radius) 0 0; + top: anchor(center --checked-option); + bottom: anchor(top --radio-option); + } + + /* ::after — destination corner: short segment + curve at destination top */ + &::after { + border-right-color: transparent; + border-top-color: transparent; + border-radius: 0 0 0 var(--join-radius); + top: calc(anchor(top --radio-option) - var(--join-stroke)); + height: var(--join-radius); + bottom: auto; + } } - } - /* source level with target */ - &.items li[data-relation="0"]:has(:checked) label { - &::before { - border-top-color: transparent; - border-right-color: transparent; - border-left-color: transparent; - border-radius: 0; - top: calc(anchor(center --radio-option) - var(--join-stroke)); - bottom: calc(anchor(center --checked-option) + var(--join-stroke)); - right: anchor(left --radio-option); - left: anchor(right --checked-option); + /* GROUP LOGIC TREE CONNECTORS / RELATION 1 (SOURCE BELOW) + =================================================== */ + &.items li[data-relation="1"]:has(:checked) label { + /* ::before — source leg: up from destination bottom, to source centre */ + &::before { + border-left-color: transparent; + border-top-color: transparent; + border-radius: 0 0 var(--join-radius) 0; + top: anchor(bottom --radio-option); + bottom: anchor(center --checked-option); + } + + /* ::after — destination corner: short segment + curve at destination bottom */ + &::after { + border-right-color: transparent; + border-bottom-color: transparent; + border-radius: var(--join-radius) 0 0 0; + top: auto; + bottom: calc(anchor(bottom --radio-option) - var(--join-stroke)); + height: var(--join-radius); + } } - &::after { - display: none; + /* GROUP LOGIC TREE CONNECTORS / RELATION 0 (SOURCE LEVEL) + =================================================== */ + &.items li[data-relation="0"]:has(:checked) label { + /* ::before only — straight horizontal line between row centres */ + &::before { + border-top-color: transparent; + border-right-color: transparent; + border-left-color: transparent; + border-radius: 0; + top: calc(anchor(center --radio-option) - var(--join-stroke)); + bottom: calc(anchor(center --checked-option) + var(--join-stroke)); + right: anchor(left --radio-option); + left: anchor(right --checked-option); + } + + &::after { + display: none; + } } - } - &.options li:has(:checked) { - anchor-name: --radio-option; - border-color: var(--radio-checked-border-color); + /* GROUP LOGIC TREE CONNECTORS / DESTINATION ANCHOR + =================================================== */ + &.options li:has(:checked) { + anchor-name: --radio-option; + border-color: var(--radio-checked-border-color); + } } } } From eccae060a3274b43a91c467ede2732a82a3435b0 Mon Sep 17 00:00:00 2001 From: Jay George Date: Tue, 9 Jun 2026 11:01:40 +0100 Subject: [PATCH 10/79] Tree view CSS - experiment with radios at the top --- resources/css/components/forms/logic-tree.css | 15 +- resources/js/pages/forms/LogicTree.vue | 160 +++++++++--------- 2 files changed, 96 insertions(+), 79 deletions(-) diff --git a/resources/css/components/forms/logic-tree.css b/resources/css/components/forms/logic-tree.css index e72ff0fc25..565b9904ca 100644 --- a/resources/css/components/forms/logic-tree.css +++ b/resources/css/components/forms/logic-tree.css @@ -71,6 +71,17 @@ } } + & > li.linked-list__page-label { + font-size: 0.875rem; + font-weight: 500; + color: light-dark(var(--color-gray-700, #374151), var(--color-gray-200, #e5e7eb)); + background: light-dark(var(--color-gray-50, #f9fafb), var(--color-gray-900, #111827)); + + &:not(:has(> label)) { + padding: 0.5rem; + } + } + /* GROUP LOGIC TREE CONNECTORS / SOURCE ANCHOR =================================================== */ &.items li:has(:checked) label { @@ -165,9 +176,9 @@ } } - /* GROUP LOGIC TREE CONNECTORS / DESTINATION ANCHOR + /* GROUP LOGIC TREE CONNECTORS / DESTINATION ANCHOR (PAGE RADIO) =================================================== */ - &.options li:has(:checked) { + &.options li.linked-list__page-label:has(input[type=radio]:checked) { anchor-name: --radio-option; border-color: var(--radio-checked-border-color); } diff --git a/resources/js/pages/forms/LogicTree.vue b/resources/js/pages/forms/LogicTree.vue index b4802afe93..bad4b2b692 100644 --- a/resources/js/pages/forms/LogicTree.vue +++ b/resources/js/pages/forms/LogicTree.vue @@ -1,7 +1,7 @@ From c1027ef7fbf910401af72cadd220fcc39ccbdf31 Mon Sep 17 00:00:00 2001 From: Jay George Date: Tue, 9 Jun 2026 11:05:06 +0100 Subject: [PATCH 11/79] Tree view CSS - remove confusing island class --- resources/css/components/forms/logic-tree.css | 2 +- resources/js/pages/forms/LogicTree.vue | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/css/components/forms/logic-tree.css b/resources/css/components/forms/logic-tree.css index 565b9904ca..4e659729dd 100644 --- a/resources/css/components/forms/logic-tree.css +++ b/resources/css/components/forms/logic-tree.css @@ -52,7 +52,7 @@ --clr-lines: rgba(255 255 255 / 0.25); } - .list { + ul { margin: 0; padding: 0; list-style: none; diff --git a/resources/js/pages/forms/LogicTree.vue b/resources/js/pages/forms/LogicTree.vue index bad4b2b692..145499df14 100644 --- a/resources/js/pages/forms/LogicTree.vue +++ b/resources/js/pages/forms/LogicTree.vue @@ -25,7 +25,7 @@ onMounted(updateRelations); From 7bb40f2ada29bed5aadc4f5470ec6254209347b5 Mon Sep 17 00:00:00 2001 From: Jay George Date: Tue, 9 Jun 2026 12:46:22 +0100 Subject: [PATCH 13/79] Tree view CSS - Simplify CSS and remove JS since our paths will always be "above" the destination --- resources/css/components/forms/logic-tree.css | 80 ++----------------- resources/js/pages/forms/LogicTree.vue | 25 ------ 2 files changed, 7 insertions(+), 98 deletions(-) diff --git a/resources/css/components/forms/logic-tree.css b/resources/css/components/forms/logic-tree.css index 295ebde707..66d6cc62b3 100644 --- a/resources/css/components/forms/logic-tree.css +++ b/resources/css/components/forms/logic-tree.css @@ -21,13 +21,6 @@ ::before — left half of the gap + vertical leg from the source ::after — right half of the gap + rounded corner at the destination - JS (LogicTree.vue) sets data-relation on each source
  • by comparing - list index to the checked destination: - - -1 source above destination - 0 source level with destination (straight line, no ::after) - 1 source below destination - @@ -120,84 +113,25 @@ left: anchor(right --checked-option); } - &::after { - right: anchor(left var(--destination)); - left: calc(anchor(left var(--destination)) - var(--gap) / 2 - var(--join-stroke)); - } - } - - /* GROUP LOGIC TREE CONNECTORS / RELATION -1 (SOURCE ABOVE) - =================================================== */ - &.items li[data-relation="-1"]:has(:checked) label { - /* ::before — source leg: out from source centre, down to destination top */ - &::before { - border-left-color: transparent; - border-bottom-color: transparent; - border-radius: 0 var(--join-radius) 0 0; - top: anchor(center --checked-option); - bottom: anchor(top var(--destination)); - } - - /* ::after — destination corner: short segment + curve at destination top */ - &::after { - border-right-color: transparent; - border-top-color: transparent; - border-radius: 0 0 0 var(--join-radius); - top: calc(anchor(top var(--destination)) - var(--join-stroke)); - height: var(--join-radius); - bottom: auto; - } - } - - /* GROUP LOGIC TREE CONNECTORS / RELATION 1 (SOURCE BELOW) - =================================================== */ - &.items li[data-relation="1"]:has(:checked) label { - /* ::before — source leg: up from destination bottom, to source centre */ &::before { + top: anchor(bottom var(--destination)); + bottom: anchor(center --checked-option); border-left-color: transparent; border-top-color: transparent; border-radius: 0 0 var(--join-radius) 0; - top: anchor(bottom var(--destination)); - bottom: anchor(center --checked-option); } - /* ::after — destination corner: short segment + curve at destination bottom */ &::after { - border-right-color: transparent; - border-bottom-color: transparent; - border-radius: var(--join-radius) 0 0 0; top: auto; + right: anchor(left var(--destination)); bottom: calc(anchor(bottom var(--destination)) - var(--join-stroke)); + left: calc(anchor(left var(--destination)) - var(--gap) / 2 - var(--join-stroke)); height: var(--join-radius); - } - } - - /* GROUP LOGIC TREE CONNECTORS / RELATION 0 (SOURCE LEVEL) - =================================================== */ - &.items li[data-relation="0"]:has(:checked) label { - /* ::before only — straight horizontal line between row centres */ - &::before { - border-top-color: transparent; border-right-color: transparent; - border-left-color: transparent; - border-radius: 0; - top: calc(anchor(center var(--destination)) - var(--join-stroke)); - bottom: calc(anchor(center --checked-option) + var(--join-stroke)); - right: anchor(left var(--destination)); - left: anchor(right --checked-option); - } - - &::after { - display: none; + border-bottom-color: transparent; + border-radius: var(--join-radius) 0 0 0; } } - - /* GROUP LOGIC TREE CONNECTORS / DESTINATION ANCHOR (PAGE RADIO) - =================================================== */ - &.options li.linked-list__page-label:has(input[type=radio]:checked) { - anchor-name: var(--destination); - border-color: var(--radio-checked-border-color); - } } } -} \ No newline at end of file +} diff --git a/resources/js/pages/forms/LogicTree.vue b/resources/js/pages/forms/LogicTree.vue index f2c12d4386..8d9671e4e9 100644 --- a/resources/js/pages/forms/LogicTree.vue +++ b/resources/js/pages/forms/LogicTree.vue @@ -1,28 +1,3 @@ - - diff --git a/resources/js/pages/forms/LogicTree.vue b/resources/js/pages/forms/LogicTree.vue index 2543f4775a..37ebfb9f92 100644 --- a/resources/js/pages/forms/LogicTree.vue +++ b/resources/js/pages/forms/LogicTree.vue @@ -1,76 +1,77 @@ + +