From 1413c8a08cd357fb95701a215f760d9f854da96a Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 10 Mar 2026 10:54:03 +0000 Subject: [PATCH 01/15] Only remove Bard set when backspace key is hit --- .../js/components/fieldtypes/bard/Set.js | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/resources/js/components/fieldtypes/bard/Set.js b/resources/js/components/fieldtypes/bard/Set.js index bd5225b064d..9b9b988aa96 100644 --- a/resources/js/components/fieldtypes/bard/Set.js +++ b/resources/js/components/fieldtypes/bard/Set.js @@ -1,5 +1,5 @@ import { Node } from '@tiptap/core'; -import { Plugin, PluginKey } from '@tiptap/pm/state'; +import { Plugin, PluginKey, NodeSelection } from '@tiptap/pm/state'; import { Slice, Fragment } from '@tiptap/pm/model'; import { Decoration, DecorationSet } from '@tiptap/pm/view'; import { VueNodeViewRenderer } from '@tiptap/vue-3'; @@ -75,6 +75,35 @@ export const Set = Node.create({ }; }, + addKeyboardShortcuts() { + const shortcuts = {}; + const type = this.type; + + const isSetSelected = (state) => { + const { selection } = state; + return selection instanceof NodeSelection && selection.node.type === type; + }; + + const blockCharacterKey = () => isSetSelected(this.editor.state); + + // Letters a-z + for (let i = 97; i <= 122; i++) { + shortcuts[String.fromCharCode(i)] = blockCharacterKey; + } + + // Numbers 0-9 + for (let i = 0; i <= 9; i++) { + shortcuts[String(i)] = blockCharacterKey; + } + + // Common punctuation/symbols + [' ', '-', '=', '[', ']', '\\', ';', "'", ',', '.', '/', '`'].forEach( + (key) => (shortcuts[key] = blockCharacterKey), + ); + + return shortcuts; + }, + addProseMirrorPlugins() { const bard = this.options.bard; const type = this.type; From fc5d0c897a0e7e008acded6eb55384c1f84fa3d3 Mon Sep 17 00:00:00 2001 From: Jay George Date: Tue, 10 Mar 2026 12:42:49 +0000 Subject: [PATCH 02/15] Selection state - Simplify CSS by converting some long Tailwind classes --- resources/css/components/fieldtypes/bard.css | 18 ++++++++++++++++++ resources/css/elements/base.css | 2 +- .../js/components/fieldtypes/bard/Set.vue | 5 +---- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/resources/css/components/fieldtypes/bard.css b/resources/css/components/fieldtypes/bard.css index 89cdf7e52f5..9b767f50f28 100644 --- a/resources/css/components/fieldtypes/bard.css +++ b/resources/css/components/fieldtypes/bard.css @@ -38,6 +38,24 @@ } } } +/* BARD / SELECTION STATE +=================================================== */ +@layer ui { + /* We’re styling a Set so that it shows a “selection outline” when selected with the mouse or keyboard. */ + /* The extra `&:not(:has(:focus-within))` rule turns that outline off if any element inside the Set has focus (e.g. when editing inside a Bard field). */ + /* This prevents the outer selection outline from showing while the user is actively working inside the Set. */ + .st-set-is-selected:not(:has(:focus-within)) { + @apply border-blue-400! dark:border-blue-400!; + + &::before { + content: ''; + position: absolute; + inset: -1px; + pointer-events: none; + @apply border-2 border-blue-400 dark:border-blue-400 rounded-lg; + } + } +} /* BARD / FOOTER TOOLBAR =================================================== */ @layer ui { diff --git a/resources/css/elements/base.css b/resources/css/elements/base.css index 465bf07665f..b6d33381399 100644 --- a/resources/css/elements/base.css +++ b/resources/css/elements/base.css @@ -92,7 +92,7 @@ - s*/ + */ .show-focus-within:has(.show-focus-within_target:focus-visible) { /* Stop the transition ruining the focus outline quickly appearing. */ transition: none; diff --git a/resources/js/components/fieldtypes/bard/Set.vue b/resources/js/components/fieldtypes/bard/Set.vue index 848ce6923e4..6a1d9fd34f6 100644 --- a/resources/js/components/fieldtypes/bard/Set.vue +++ b/resources/js/components/fieldtypes/bard/Set.vue @@ -4,10 +4,7 @@ ref="container" class="shadow-ui-sm relative w-full rounded-lg border border-gray-300 bg-white text-base dark:border-white/10 dark:bg-gray-900 dark:inset-shadow-2xs dark:inset-shadow-black" :class="{ - // We’re styling a Set so that it shows a “selection outline” when selected with the mouse or keyboard. - // The extra `&:not(:has(:focus-within))` rule turns that outline off if any element inside the Set has focus (e.g. when editing inside a Bard field). - // This prevents the outer selection outline from showing while the user is actively working inside the Set. - 'st-set-is-selected [&:not(:has(:focus-within))]:border-blue-400! [&:not(:has(:focus-within))]:dark:border-blue-400! [&:not(:has(:focus-within))]:before:content-[\'\'] [&:not(:has(:focus-within))]:before:absolute [&:not(:has(:focus-within))]:before:inset-[-1px] [&:not(:has(:focus-within))]:before:pointer-events-none [&:not(:has(:focus-within))]:before:border-2 [&:not(:has(:focus-within))]:before:border-blue-400 [&:not(:has(:focus-within))]:dark:before:border-blue-400 [&:not(:has(:focus-within))]:before:rounded-lg': showSelectionHighlight, + 'st-set-is-selected': showSelectionHighlight, 'border-red-500': hasError, }" :data-type="config.handle" From 08a5245be8b0045f9d819b271e002aac74107c4e Mon Sep 17 00:00:00 2001 From: Jay George Date: Tue, 10 Mar 2026 12:45:35 +0000 Subject: [PATCH 03/15] Selection state - Remove extra border not needed because it's already handled with the pseudo content --- resources/css/components/fieldtypes/bard.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/css/components/fieldtypes/bard.css b/resources/css/components/fieldtypes/bard.css index 9b767f50f28..5161b189e55 100644 --- a/resources/css/components/fieldtypes/bard.css +++ b/resources/css/components/fieldtypes/bard.css @@ -45,8 +45,6 @@ /* The extra `&:not(:has(:focus-within))` rule turns that outline off if any element inside the Set has focus (e.g. when editing inside a Bard field). */ /* This prevents the outer selection outline from showing while the user is actively working inside the Set. */ .st-set-is-selected:not(:has(:focus-within)) { - @apply border-blue-400! dark:border-blue-400!; - &::before { content: ''; position: absolute; From 59aeb643bc1ee39d8157564058b3f8c98676c3fb Mon Sep 17 00:00:00 2001 From: Jay George Date: Tue, 10 Mar 2026 13:55:18 +0000 Subject: [PATCH 04/15] Selection state - Handle the selection state better when the dropdown is open --- resources/css/components/fieldtypes/bard.css | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/resources/css/components/fieldtypes/bard.css b/resources/css/components/fieldtypes/bard.css index 5161b189e55..47aca86da21 100644 --- a/resources/css/components/fieldtypes/bard.css +++ b/resources/css/components/fieldtypes/bard.css @@ -42,9 +42,13 @@ =================================================== */ @layer ui { /* We’re styling a Set so that it shows a “selection outline” when selected with the mouse or keyboard. */ - /* The extra `&:not(:has(:focus-within))` rule turns that outline off if any element inside the Set has focus (e.g. when editing inside a Bard field). */ - /* This prevents the outer selection outline from showing while the user is actively working inside the Set. */ - .st-set-is-selected:not(:has(:focus-within)) { + .st-set-is-selected:not(:has( + /* Turn the outline off if any element inside the Set has focus (e.g. when editing inside a Bard field). This prevents the outer selection outline from showing while the user is actively working inside the Set. */ + :focus-within, + /* When the dropdown is open e.g. with options to collapse the set, the outer selection outline should be hidden. Instead we'll show a focus outline for the replicator set that's selected. */ + [data-ui-dropdown-trigger][data-state="open"] + )), + [data-replicator-set]:has([data-ui-dropdown-trigger][data-state="open"]) { &::before { content: ''; position: absolute; From 5af85a08b0f630349a0e3baab055d39f2d548fa5 Mon Sep 17 00:00:00 2001 From: Jay George Date: Tue, 10 Mar 2026 14:37:00 +0000 Subject: [PATCH 05/15] =?UTF-8?q?When=20the=20dropdown=20has=20just=20clos?= =?UTF-8?q?ed=20(open=E2=86=92closed),=20keep=20the=20outline=20hidden=20b?= =?UTF-8?q?riefly=20so=20it=20doesn't=20flicker=20back=20to=20the=20parent?= =?UTF-8?q?=20while=20focus=20settles.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/css/components/fieldtypes/bard.css | 4 +++- .../js/components/fieldtypes/bard/Set.vue | 18 +++++++++++++++++- .../components/fieldtypes/replicator/Set.vue | 17 +++++++++++++++-- .../js/components/ui/Dropdown/Dropdown.vue | 10 +++++++++- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/resources/css/components/fieldtypes/bard.css b/resources/css/components/fieldtypes/bard.css index 47aca86da21..11f4fc9e08e 100644 --- a/resources/css/components/fieldtypes/bard.css +++ b/resources/css/components/fieldtypes/bard.css @@ -46,7 +46,9 @@ /* Turn the outline off if any element inside the Set has focus (e.g. when editing inside a Bard field). This prevents the outer selection outline from showing while the user is actively working inside the Set. */ :focus-within, /* When the dropdown is open e.g. with options to collapse the set, the outer selection outline should be hidden. Instead we'll show a focus outline for the replicator set that's selected. */ - [data-ui-dropdown-trigger][data-state="open"] + [data-ui-dropdown-trigger][data-state="open"], + /* When the dropdown has just closed (open→closed), keep the outline hidden briefly so it doesn't flicker back to the parent while focus settles. */ + .st-dropdown-just-closed )), [data-replicator-set]:has([data-ui-dropdown-trigger][data-state="open"]) { &::before { diff --git a/resources/js/components/fieldtypes/bard/Set.vue b/resources/js/components/fieldtypes/bard/Set.vue index 6a1d9fd34f6..ef48f2c517e 100644 --- a/resources/js/components/fieldtypes/bard/Set.vue +++ b/resources/js/components/fieldtypes/bard/Set.vue @@ -6,6 +6,7 @@ :class="{ 'st-set-is-selected': showSelectionHighlight, 'border-red-500': hasError, + 'st-dropdown-just-closed': dropdownJustClosed, }" :data-type="config.handle" contenteditable="false" @@ -44,7 +45,7 @@
- + @@ -127,6 +128,12 @@ export default { mixins: [ManagesPreviewText, HasFieldActions], + data() { + return { + dropdownJustClosed: false, + }; + }, + inject: { bard: {}, bardSets: {}, @@ -313,6 +320,15 @@ export default { this.getPos, ); }, + + onSetDropdownClosed() { + this.dropdownJustClosed = true; + if (this._dropdownJustClosedTimeout) clearTimeout(this._dropdownJustClosedTimeout); + this._dropdownJustClosedTimeout = setTimeout(() => { + this.dropdownJustClosed = false; + this._dropdownJustClosedTimeout = null; + }, 100); + }, }, mounted() { diff --git a/resources/js/components/fieldtypes/replicator/Set.vue b/resources/js/components/fieldtypes/replicator/Set.vue index bace108bb31..bf6d0a4a8aa 100644 --- a/resources/js/components/fieldtypes/replicator/Set.vue +++ b/resources/js/components/fieldtypes/replicator/Set.vue @@ -121,6 +121,18 @@ function destroy() { const rootEl = ref(); reveal.use(rootEl, () => emit('expanded')); + +const dropdownJustClosed = ref(false); +let dropdownJustClosedTimeout = null; + +function onSetDropdownClosed() { + dropdownJustClosed.value = true; + if (dropdownJustClosedTimeout) clearTimeout(dropdownJustClosedTimeout); + dropdownJustClosedTimeout = setTimeout(() => { + dropdownJustClosed.value = false; + dropdownJustClosedTimeout = null; + }, 150); +}