Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3f9bc3a
Bard - match the "read only" decoration of the other fields
jaygeorge May 12, 2026
3f44e4d
Button group field - add read only state
jaygeorge May 13, 2026
9ac00aa
Bard field - fix tables not showing in readonly mode by loading TipTa…
jaygeorge May 13, 2026
712b407
Code field - improve read only state
jaygeorge May 13, 2026
4912df6
Checkbox field - add read only state
jaygeorge May 13, 2026
ff06fcc
Checkbox field - add read only state
jaygeorge May 13, 2026
30ecbcc
Range field - add read only state
jaygeorge May 13, 2026
c538ac0
Revealer field - add read only state
jaygeorge May 13, 2026
af7b3de
Width field - add read only state
jaygeorge May 13, 2026
5bfc1a1
Video field - add read only state
jaygeorge May 13, 2026
80f9643
Relationship field types - add an empty state for read only
jaygeorge May 13, 2026
009c07f
Array/date/list/time/grid field types - add an empty state for read only
jaygeorge May 13, 2026
145d014
Relationship field types - add read only state when an entry is selected
jaygeorge May 13, 2026
2edd593
Relationship field types - add dashed border
jaygeorge May 13, 2026
d6bbbec
Form field type - improve read only state
jaygeorge May 13, 2026
a884957
Color field type - add read only state
jaygeorge May 13, 2026
4079e1d
Replicator type - add read only state
jaygeorge May 13, 2026
42a650d
radios/checkboxes - adjust dark mode for read only
jaygeorge May 13, 2026
3593c58
Read only / dark mode - fix set backgrounds
jaygeorge May 13, 2026
938cac5
Read only / dark mode - make bg colors consistent
jaygeorge May 13, 2026
a80d70c
Read only / dark mode - fix the button group looking weird
jaygeorge May 13, 2026
70e6369
Read only / dark mode - fix the range field type
jaygeorge May 13, 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
8 changes: 7 additions & 1 deletion resources/css/components/fieldtypes/bard.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
.bard-fieldtype:not(.form-group, .grid-cell, [data-ui-input-group]) {
@apply relative rounded-lg border outline-hidden dark:border-gray-700 with-contrast:border-gray-500;
}

/* Match ui Input / Textarea read-only: dashed outer border (same :not scope as default border). */
.bard-fieldtype:not(.form-group, .grid-cell, [data-ui-input-group]):has(.bard-editor.mode\:read-only) {
@apply border-dashed;
}
}
/* BARD / EDITOR
=================================================== */
Expand Down Expand Up @@ -51,8 +56,9 @@
/* BARD / MODES
=================================================== */
@layer ui {
/* Match ui Input / Textarea read-only: neutral surface (dashed border on .bard-fieldtype above). */
.bard-editor.mode\:read-only .ProseMirror {
@apply bg-gray-300 text-gray-700 dark:bg-gray-600 dark:text-gray-100;
@apply bg-white text-gray-925 dark:bg-gray-900 dark:text-gray-300;
}

.bard-editor.mode\:minimal .ProseMirror {
Expand Down
43 changes: 38 additions & 5 deletions resources/js/components/fields/WidthSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const props = defineProps({
initialWidths: Array,
size: { type: String, default: 'base' },
variant: { type: String, default: 'default' },
/** When `true`, the value cannot be changed (read-only, same pattern as other CP controls). */
readOnly: { type: Boolean, default: false },
})

const emit = defineEmits(['update:model-value'])
Expand All @@ -16,14 +18,35 @@ const hoveringOver = ref(null)
const widths = ref(props.initialWidths ?? [25, 33, 50, 66, 75, 100])

const selected = computed(() => {
if (props.readOnly) {
return props.modelValue
}
if (isHovering.value) {
return hoveringOver.value
}
return props.modelValue
})

const readOnlyChromeClass =
'data-readonly:border-dashed! data-readonly:border-gray-300 data-readonly:with-contrast:border-gray-100 data-readonly:dark:border! data-readonly:dark:border-dashed! data-readonly:dark:border-gray-600!'

function handleMouseEnter() {
if (props.readOnly) return
isHovering.value = true
}

function handleMouseLeave() {
isHovering.value = false
hoveringOver.value = null
}

function handleSegmentClick(width) {
if (props.readOnly) return
emit('update:model-value', width)
}

const wrapperClasses = cva({
base: 'relative text-gray-600 dark:text-gray-400 font-mono antialiased bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 with-contrast:border-gray-500 overflow-hidden flex cursor-pointer',
base: 'relative text-gray-600 dark:text-gray-400 font-mono antialiased bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 with-contrast:border-gray-500 overflow-hidden flex cursor-pointer data-readonly:cursor-default',
variants: {
size: {
base: 'h-6 w-14 text-xs rounded-md',
Expand Down Expand Up @@ -54,18 +77,28 @@ const sizerClasses = cva({
</script>

<template>
<div :class="wrapperClasses" @mouseenter="isHovering = true" @mouseleave="isHovering = false">
<div class="flex w-full">
<div
:class="[wrapperClasses, readOnly ? readOnlyChromeClass : '']"
:data-readonly="readOnly ? true : undefined"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<div class="flex w-full" :class="{ 'pointer-events-none': readOnly }">
<div
v-for="width in widths"
:key="width"
@mouseenter.stop="hoveringOver = width"
@click="$emit('update:model-value', width)"
@click="handleSegmentClick(width)"
:class="sizerClasses"
:data-state="selected >= width ? 'selected' : 'unselected'"
:data-last="selected === width && width !== 100"
/>
</div>
<div class="pointer-events-none absolute inset-0 z-10 flex w-full items-center justify-center text-center font-medium text-gray-900 dark:text-gray-300">{{ selected }}%</div>
<div
class="pointer-events-none absolute inset-0 z-10 flex w-full items-center justify-center text-center font-medium text-gray-900 transition-opacity dark:text-gray-300"
:class="{ 'opacity-60': readOnly }"
>
{{ selected }}%
</div>
</div>
</template>
13 changes: 13 additions & 0 deletions resources/js/components/fieldtypes/ArrayFieldtype.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
<template>
<div>
<div
v-if="showReadOnlyEmpty"
class="rounded-lg border border-dashed border-gray-300 bg-gray-50 px-3 py-3 text-center text-sm text-gray-400 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-400"
data-array-readonly-empty
>
{{ __('None') }}
</div>
<template v-else>
<ui-input-group v-if="isSingle">
<ui-input-group-prepend>
<select
Expand Down Expand Up @@ -90,6 +98,7 @@
<div class="flex gap-2">
<ui-button @click="addValue" :disabled="atMax" v-if="!isReadOnly && !isSingle && !isKeyed" :text="addButton" size="sm" />
</div>
</template>

<confirmation-modal
:open="deleting !== false"
Expand Down Expand Up @@ -195,6 +204,10 @@ export default {
.filter(Boolean)
.join(', ');
},

showReadOnlyEmpty() {
return this.isReadOnly && this.valueCount === 0 && !this.isSingle;
},
},

methods: {
Expand Down
4 changes: 2 additions & 2 deletions resources/js/components/fieldtypes/ButtonGroupFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
<Button
v-for="(option, $index) in options"
ref="button"
:disabled="config.disabled"
:disabled="config.disabled || isReadOnly"
:key="$index"
:name="name"
:read-only="isReadOnly"
:text="option.label || option.value"
:value="option.value"
:variant="value == option.value ? 'pressed' : 'default'"
Expand Down Expand Up @@ -42,6 +41,7 @@ export default {

methods: {
updateSelectedOption(newValue) {
if (this.isReadOnly) return;
this.update(this.value == newValue && this.config.clearable ? null : newValue);
},

Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/fieldtypes/CodeFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
:color-mode="config.color_mode"
:rulers="config.rulers"
:disabled="config.disabled"
:read-only="config.read_only"
:read-only="isReadOnly"
:key-map="config.key_map"
:tab-size="config.indent_size"
:indent-type="config.indent_type"
Expand Down
49 changes: 42 additions & 7 deletions resources/js/components/fieldtypes/ColorFieldtype.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<template>
<div class="flex items-center">
<!-- <div class="input-group w-auto" :class="{ 'max-w-[130px]': config.allow_any }"> -->
<div class="flex items-center rounded-full relative border shadow-ui-sm with-contrast:border-gray-500">
<div class="flex w-full min-w-0 items-center">
<div
class="flex items-center rounded-full relative border shadow-ui-sm with-contrast:border-gray-500"
:class="{ 'border-dashed border-gray-300 dark:border-gray-700 dark:with-contrast:border-gray-600': isReadOnly }"
>
<ui-popover
v-if="!isReadOnly"
ref="colorPopover"
name="swatches"
direction="bottom"
Expand All @@ -12,10 +15,13 @@
@update:open="popoverOpen = $event"
>
<template #trigger>
<button type="button" class="cursor-pointer size-9 border rounded-full flex items-center justify-center with-contrast:border-gray-500" :aria-label="__('Pick Color')">
<button
type="button"
class="cursor-pointer size-9 border rounded-full flex items-center justify-center with-contrast:border-gray-500"
:aria-label="__('Pick Color')"
>
<div
class="size-8 rounded-full"
:class="{ 'cursor-not-allowed': isReadOnly }"
:style="{ 'background-color': customColor || value }"
/>
</button>
Expand Down Expand Up @@ -57,8 +63,29 @@
</template>
</ui-popover>

<div
v-else-if="hasColorSelected"
class="pointer-events-none flex size-9 shrink-0 items-center justify-center rounded-full border border-transparent"
role="img"
:aria-label="__('Color')"
>
<div
class="size-8 rounded-full"
:style="{ 'background-color': customColor || value }"
/>
</div>

<div
v-else
class="pointer-events-none flex h-9 min-h-9 shrink-0 items-center justify-center rounded-full bg-gray-50 px-4 text-sm text-gray-400 dark:bg-gray-900 dark:text-gray-400"
role="status"
data-color-readonly-empty
>
{{ __('None') }}
</div>

<input
v-if="config.allow_any"
v-if="config.allow_any && (!isReadOnly || hasColorSelected)"
class="font-mono text-sm px-2 w-24 outline-none"
maxlength="7"
type="text"
Expand All @@ -69,7 +96,7 @@
/>
</div>

<ui-button v-if="value" icon="x" :aria-label="__('Reset')" @click="resetColor" round inset size="sm" variant="ghost" class="ms-1" />
<ui-button v-if="value && !isReadOnly" icon="x" :aria-label="__('Reset')" @click="resetColor" round inset size="sm" variant="ghost" class="ms-1" />
</div>
</template>

Expand Down Expand Up @@ -98,6 +125,14 @@ export default {
},

computed: {
hasColorSelected() {
const v = this.value;
if (v == null || v === false) return false;
if (typeof v === 'string' && v.trim() === '') return false;

return true;
},

replicatorPreview() {
if (!this.showFieldPreviews) return;

Expand Down
8 changes: 8 additions & 0 deletions resources/js/components/fieldtypes/DateFieldtype.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
<template>
<div class="datetime min-w-[145px]">
<div
v-if="isReadOnly && !hasDate && !isInline"
class="rounded-lg border border-dashed border-gray-300 bg-gray-50 px-3 py-3 text-center text-sm text-gray-400 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-400"
data-date-readonly-empty
>
{{ __('None') }}
</div>

<Button :text="__('Add Date')" icon="calendar" v-if="!isReadOnly && !isInline && !hasDate" @click="addDate" />

<Component
Expand Down
1 change: 1 addition & 0 deletions resources/js/components/fieldtypes/Fieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default {
this.readOnly ||
this.config.visibility === 'read_only' ||
this.config.visibility === 'computed' ||
this.config.read_only === true ||
false
);
},
Expand Down
25 changes: 15 additions & 10 deletions resources/js/components/fieldtypes/LinkFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="flex gap-2 sm:gap-3">
<!-- Link type selector -->
<div class="w-fit">
<Select :options v-model="option" />
<Select :options v-model="option" :read-only="isReadOnly" />
</div>

<div class="flex-1 flex">
Expand All @@ -14,6 +14,7 @@
:config="meta.entry.config"
:meta="meta.entry.meta"
:value="selectedEntries"
:read-only="isReadOnly"
@update:meta="meta.entry.meta = $event"
@update:value="entriesSelected"
button-size="base"
Expand All @@ -28,6 +29,7 @@
ref="assets"
handle="asset"
:value="selectedAssets"
:read-only="isReadOnly"
:config="meta.asset.config"
:meta="meta.asset.meta"
@update:value="assetsSelected"
Expand Down Expand Up @@ -102,22 +104,22 @@ export default {
if (this.metaChanging) return;

if (option === null) {
this.update(null);
if (!this.isReadOnly) this.update(null);
} else if (option === 'url') {
this.updateDebounced(this.urlValue);
if (!this.isReadOnly) this.updateDebounced(this.urlValue);
} else if (option === 'first-child') {
this.update('@child');
if (!this.isReadOnly) this.update('@child');
} else if (option === 'entry') {
if (this.entryValue) {
this.update(this.entryValue);
} else {
setTimeout(() => this.$refs.entries.linkExistingItem(), 0);
if (!this.isReadOnly) this.update(this.entryValue);
} else if (!this.isReadOnly) {
setTimeout(() => this.$refs.entries?.linkExistingItem(), 0);
}
} else if (option === 'asset') {
if (this.assetValue) {
this.update(this.assetValue);
} else {
setTimeout(() => this.$refs.assets.openSelector(), 0);
if (!this.isReadOnly) this.update(this.assetValue);
} else if (!this.isReadOnly) {
setTimeout(() => this.$refs.assets?.openSelector(), 0);
}
}

Expand All @@ -126,6 +128,7 @@ export default {

urlValue(url) {
if (this.metaChanging) return;
if (this.isReadOnly) return;
this.syncUrlDebounced(url);
},

Expand Down Expand Up @@ -158,12 +161,14 @@ export default {
},

entriesSelected(entries) {
if (this.isReadOnly) return;
this.selectedEntries = entries;
this.update(this.entryValue);
this.updateMeta({ ...this.meta, initialSelectedEntries: entries });
},

assetsSelected(assets) {
if (this.isReadOnly) return;
this.selectedAssets = assets;
this.update(this.assetValue);
this.updateMeta({ ...this.meta, initialSelectedAssets: assets });
Expand Down
7 changes: 7 additions & 0 deletions resources/js/components/fieldtypes/ListFieldtype.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
<template>
<div>
<div
v-if="isReadOnly && data.length === 0"
class="rounded-lg border border-dashed border-gray-300 bg-gray-50 px-3 py-3 text-center text-sm text-gray-400 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-400"
data-list-readonly-empty
>
{{ __('None') }}
</div>
<table class="table-contained" v-if="data.length > 0">
<sortable-list
v-model="data"
Expand Down
Loading
Loading