Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
13a2a47
Add useUiDirection composable
jasonvarga Jul 15, 2026
5240d81
Add useContentDirection composable and ContentDirection component
jasonvarga Jul 15, 2026
e1fd7d7
Use UI direction for fieldtype chrome wrapper in Field.vue
jasonvarga Jul 15, 2026
30d7d02
Opt leaf value fieldtypes into content direction
jasonvarga Jul 15, 2026
c3a13bc
Export useUiDirection, useContentDirection, and ContentDirection via …
jasonvarga Jul 15, 2026
0fbfa9e
Add Vitest coverage for UI/content direction
jasonvarga Jul 15, 2026
6a2a69b
Make Tabs primitive follow UI direction by default
jasonvarga Jul 16, 2026
2e51342
Fix field label/instructions inheriting content direction inside sets
jasonvarga Jul 16, 2026
be188b0
Fix Bard set header chrome inheriting content direction
jasonvarga Jul 16, 2026
928891e
Fix Markdown fieldtype write-mode editor not using content direction
jasonvarga Jul 16, 2026
1b544a7
Make Markdown fieldtype's CodeMirror direction reactive
jasonvarga Jul 16, 2026
92c0793
Fix Markdown editor lines rendering LTR when direction is rtl
jasonvarga Jul 16, 2026
19dc2b2
Decouple content direction from CP direction in publish containers
jasonvarga Jul 16, 2026
ce174a2
Set an explicit dir on the CodeMirror wrapper for LTR content too
jasonvarga Jul 16, 2026
43c845c
Keep markdown editor LTR; make only the preview direction-aware
jasonvarga Jul 16, 2026
3603c09
Address text direction review feedback
jasonvarga Jul 17, 2026
344c281
Make direction tests order-independent
jasonvarga Jul 17, 2026
61d4230
Explain intentional ContentDirection export placement
jasonvarga Jul 17, 2026
bc3ec9a
Extract publish container context; move ContentDirection into Publish
jasonvarga Jul 17, 2026
968c3d8
Export direction composables via @statamic/cms/ui
jasonvarga Jul 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/cms/src/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export const {
Combobox,
CommandPaletteItem,
ConfirmationModal,
ContentDirection,
useContentDirection,
useUiDirection,
Context,
ContextFooter,
ContextHeader,
Expand Down
1 change: 1 addition & 0 deletions resources/css/vendors/codemirror.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
.CodeMirror {
/* Set height, width, borders, and global font properties here */
@apply text-gray-900 dark:text-gray-300 leading-normal min-h-20;
direction: ltr;
}

/* CODEMIRROR / PADDING
Expand Down
3 changes: 3 additions & 0 deletions resources/js/bootstrap/cms/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
Combobox,
CommandPaletteItem,
ConfirmationModal,
ContentDirection,
Context,
ContextFooter,
ContextHeader,
Expand Down Expand Up @@ -124,4 +125,6 @@ export {
Widget,
registerIconSet,
registerIconSetFromStrings,
useContentDirection,
useUiDirection,
} from '../../components/ui/index';
11 changes: 11 additions & 0 deletions resources/js/components/fieldtypes/ArrayFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
:key="element._id"
:id="fieldId + '__' + element.key"
:readonly="isReadOnly"
:input-attrs="{ dir: contentDirection }"
/>
</template>
</ui-input-group>
Expand All @@ -41,6 +42,7 @@
:id="fieldId + '__' + element.key"
v-model="data[index].value"
:readonly="isReadOnly"
:dir="contentDirection"
/>
</td>
</tr>
Expand Down Expand Up @@ -71,12 +73,14 @@
<ui-input
v-model="element.key"
:readonly="isReadOnly"
:input-attrs="{ dir: contentDirection }"
/>
</td>
<td>
<ui-input
v-model="element.value"
:readonly="isReadOnly"
:input-attrs="{ dir: contentDirection }"
/>
</td>
<td class="row-controls" v-if="!isReadOnly">
Expand Down Expand Up @@ -107,6 +111,7 @@
import Fieldtype from './Fieldtype.vue';
import { SortableList, SortableHelpers } from '../sortable/Sortable';
import { Button } from '@/components/ui';
import { useContentDirection } from '@/composables/content-direction';

export default {
mixins: [Fieldtype, SortableHelpers],
Expand All @@ -116,6 +121,12 @@ export default {
Button,
},

setup() {
const { direction: contentDirection } = useContentDirection();

return { contentDirection };
},

data() {
const keys = Object.keys(this.value || {});
const selectedKey = keys.length > 0 ? keys[0] : null;
Expand Down
8 changes: 8 additions & 0 deletions resources/js/components/fieldtypes/ListFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
class="!inset-shadow-none focus:!inset-shadow-none"
v-model="element.value"
:readonly="isReadOnly"
:input-attrs="{ dir: contentDirection }"
@blur="focused = false"
@focus="editItemWithoutFocusing(index)"
@keydown.enter.prevent="nextItem"
Expand Down Expand Up @@ -58,6 +59,7 @@
import Fieldtype from './Fieldtype.vue';
import { SortableList, SortableHelpers } from '../sortable/Sortable';
import { Button } from '@/components/ui';
import { useContentDirection } from '@/composables/content-direction';

export default {
mixins: [Fieldtype, SortableHelpers],
Expand All @@ -67,6 +69,12 @@ export default {
Button,
},

setup() {
const { direction: contentDirection } = useContentDirection();

return { contentDirection };
},

data() {
return {
data: [],
Expand Down
8 changes: 8 additions & 0 deletions resources/js/components/fieldtypes/SlugFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
:read-only="isReadOnly"
:name="slug"
:disabled="config.disabled"
:input-attrs="{ dir: contentDirection }"
@focus="$emit('focus')"
@blur="$emit('blur')"
>
Expand All @@ -40,6 +41,7 @@
import { data_get } from '../../bootstrap/globals';
import Fieldtype from './Fieldtype.vue';
import { Input, Button, Icon } from '@/components/ui';
import { useContentDirection } from '@/composables/content-direction';

export default {
mixins: [Fieldtype],
Expand All @@ -50,6 +52,12 @@ export default {
Icon,
},

setup() {
const { direction: contentDirection } = useContentDirection();

return { contentDirection };
},

data() {
return {
slug: this.value,
Expand Down
8 changes: 8 additions & 0 deletions resources/js/components/fieldtypes/TableFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<ui-input
v-model="row.value.cells[cellIndex]"
:readonly="isReadOnly"
:input-attrs="{ dir: contentDirection }"
/>
</td>
<td class="row-controls" v-if="canDeleteRows">
Expand Down Expand Up @@ -88,6 +89,7 @@
<script>
import Fieldtype from './Fieldtype.vue';
import { SortableList, SortableHelpers } from '../sortable/Sortable';
import { useContentDirection } from '@/composables/content-direction';

export default {
mixins: [Fieldtype, SortableHelpers],
Expand All @@ -96,6 +98,12 @@ export default {
SortableList,
},

setup() {
const { direction: contentDirection } = useContentDirection();

return { contentDirection };
},

data: function () {
return {
data: this.arrayToSortable(this.value || []),
Expand Down
5 changes: 4 additions & 1 deletion resources/js/components/fieldtypes/TextFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
:placeholder="__(config.placeholder)"
:name="name"
:id="id"
:direction="config.direction"
:input-attrs="{ dir: contentDirection }"
@update:model-value="inputUpdated"
@focus="$emit('focus')"
@blur="$emit('blur')"
Expand All @@ -25,6 +25,7 @@
import Fieldtype from '@/components/fieldtypes/fieldtype.js';
import { Input } from '@/components/ui';
import { computed } from 'vue';
import { useContentDirection } from '@/composables/content-direction';

const emit = defineEmits(Fieldtype.emits);
const props = defineProps(Fieldtype.props);
Expand All @@ -36,6 +37,8 @@ const {
expose
} = Fieldtype.use(emit, props);

const { direction: contentDirection } = useContentDirection();

const shouldFocus = computed(() => {
if (props.config.focus === false || props.config.focus === true) {
return props.config.focus;
Expand Down
8 changes: 8 additions & 0 deletions resources/js/components/fieldtypes/TextareaFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
:limit="config.character_limit || null"
:placeholder="__(config.placeholder)"
:model-value="value"
:dir="contentDirection"
@blur="$emit('blur')"
@focus="$emit('focus')"
@update:model-value="updateDebounced"
Expand All @@ -16,9 +17,16 @@
<script>
import Fieldtype from './Fieldtype.vue';
import { Textarea } from '@/components/ui';
import { useContentDirection } from '@/composables/content-direction';

export default {
mixins: [Fieldtype],
components: { Textarea },

setup() {
const { direction: contentDirection } = useContentDirection();

return { contentDirection };
},
};
</script>
9 changes: 8 additions & 1 deletion resources/js/components/fieldtypes/bard/BardFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
</floating-menu>

<div class="bard-error" v-if="initError" v-text="initError"></div>
<editor-content :editor="editor" :id="fieldId" />
<editor-content :editor="editor" :id="fieldId" :dir="contentDirection" />
</div>
<div
class="bard-footer-toolbar"
Expand Down Expand Up @@ -177,6 +177,7 @@ import 'highlight.js/styles/github.css';
import importTiptap from '@/util/tiptap.js';
import { computed } from 'vue';
import { data_get } from "@/bootstrap/globals.js";
import { useContentDirection } from '@/composables/content-direction';

const lowlight = createLowlight(common);
let tiptap = null;
Expand All @@ -198,6 +199,12 @@ export default {
isInBardField: true,
},

setup() {
const { direction: contentDirection } = useContentDirection();

return { contentDirection };
},

data() {
return {
events: new Emitter(),
Expand Down
8 changes: 8 additions & 0 deletions resources/js/components/fieldtypes/bard/Set.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<div
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"
:dir="uiDirection"
: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).
Expand Down Expand Up @@ -117,10 +118,17 @@ import {
import { containerContextKey } from '@/components/ui/Publish/Container.vue';
import { watch } from 'vue';
import { reveal } from '@api';
import { useUiDirection } from '@/composables/ui-direction';

export default {
props: nodeViewProps,

setup() {
return {
uiDirection: useUiDirection().direction,
};
},

components: {
Button,
Dropdown,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
v-show="mode == 'preview'"
v-html="markdownPreviewText"
class="markdown-preview p-3 prose prose-sm @md/markdown:prose-base"
:dir="contentDirection"
></div>
</div>
</div>
Expand Down Expand Up @@ -184,6 +185,7 @@ import AssetSelector from '../../assets/Selector.vue';
import Uploader from '../../assets/Uploader.vue';
import Uploads from '../../assets/Uploads.vue';
import MarkdownToolbar from './MarkdownToolbar.vue';
import { useContentDirection } from '@/composables/content-direction';
// Keymaps
import 'codemirror/keymap/sublime';

Expand Down Expand Up @@ -240,6 +242,12 @@ export default {
Stack,
},

setup() {
const { direction: contentDirection } = useContentDirection();

return { contentDirection };
},

data() {
return {
data: this.value || '',
Expand Down Expand Up @@ -654,7 +662,6 @@ export default {
mode: 'gfm',
dragDrop: false,
keyMap: 'sublime',
direction: document.querySelector('html').getAttribute('dir') ?? 'ltr',
lineWrapping: true,
viewportMargin: Infinity,
tabindex: 0,
Expand Down
4 changes: 3 additions & 1 deletion resources/js/components/ui/Field.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const props = defineProps({
inline: { type: Boolean, default: false },
/** Badge text to display next to the label. */
badge: { type: String, default: '' },
/** The reading direction of the field's label, instructions, and errors. */
dir: { type: String, default: null },
disabled: { type: Boolean, default: false },
/** Error message to display below the field. */
error: { type: String },
Expand Down Expand Up @@ -80,7 +82,7 @@ const hasErrors = computed(() => {
</script>

<template>
<div :class="[rootClasses, $attrs.class]" data-ui-input-group :data-ui-field-has-errors="hasErrors ? '' : null">
<div :class="[rootClasses, $attrs.class]" :dir="dir" data-ui-input-group :data-ui-field-has-errors="hasErrors ? '' : null">
<div
v-if="label || $slots.label || $slots.actions || (instructions && !instructionsBelow)"
class="flex flex-col gap-1.5"
Expand Down
8 changes: 5 additions & 3 deletions resources/js/components/ui/Publish/Container.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script>
import createContext from '@/util/createContext.js';
import { injectContainerContext, provideContainerContext, containerContextKey } from './context.js';

export const [injectContainerContext, provideContainerContext, containerContextKey] = createContext('PublishContainer');
export { injectContainerContext, provideContainerContext, containerContextKey };
</script>

<script setup>
Expand All @@ -11,6 +11,7 @@ import Component from '@/components/Component.js';
import Tabs from './Tabs.vue';
import Values from '@/components/publish/Values.js';
import { data_get } from '@/bootstrap/globals.js';
import { useUiDirection } from '@/composables/ui-direction';

const emit = defineEmits(['update:modelValue', 'update:visibleValues', 'update:modifiedFields', 'update:meta']);

Expand Down Expand Up @@ -104,7 +105,8 @@ const meta = ref(props.meta);
const previews = ref({});
const localizedFields = ref(props.modifiedFields || []);
const components = ref([]);
const direction = computed(() => Statamic.$config.get('sites').find(s => s.handle === props.site)?.direction ?? document.documentElement.dir ?? 'ltr');
const { direction: uiDirection } = useUiDirection();
const direction = computed(() => Statamic.$config.get('sites').find(s => s.handle === props.site)?.direction ?? uiDirection.value);

const visibleValues = computed(() => {
const omittable = Object.keys(hiddenFields.value).filter(
Expand Down
19 changes: 19 additions & 0 deletions resources/js/components/ui/Publish/ContentDirection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup>
import { Primitive } from 'reka-ui';
import { useContentDirection } from '@/composables/content-direction';

defineProps({
/** The element or component to render as */
as: { type: [String, Object], default: 'div' },
/** When `true`, merges props onto the immediate child instead of rendering a wrapper element */
asChild: { type: Boolean, default: false },
});

const { direction } = useContentDirection();
</script>

<template>
<Primitive :as="as" :as-child="asChild" :dir="direction">
<slot />
</Primitive>
</template>
Loading
Loading