Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions lang/en/fieldtypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
'bard.config.word_count' => 'Show the word count at the bottom of the field.',
'bard.title' => 'Bard',
'button_group.title' => 'Button Group',
'checkboxes.config.appearance' => 'Choose how the checkboxes are displayed.',
'checkboxes.config.inline' => 'Show the checkboxes in a row.',
'checkboxes.config.options' => 'Set the array keys and their optional labels.',
'checkboxes.title' => 'Checkboxes',
Expand Down Expand Up @@ -143,6 +144,7 @@
'picker.category.special.description' => 'These fields are special, each in their own way.',
'picker.category.structured.description' => 'Fields that store structured data. Some can even nest other fields inside themselves.',
'picker.category.text.description' => 'Fields that store strings of text, rich content, or both.',
'radio.config.appearance' => 'Choose how the radio buttons are displayed.',
'radio.config.inline' => 'Show the radio buttons in a row.',
'radio.config.options' => 'Set the array keys and their optional labels.',
'radio.title' => 'Radio',
Expand Down
69 changes: 55 additions & 14 deletions resources/js/components/fieldtypes/ButtonGroupFieldtype.vue
Original file line number Diff line number Diff line change
@@ -1,30 +1,49 @@
<template>
<ButtonGroup overflow="stack" ref="buttonGroup">
<Button
v-for="(option, $index) in options"
ref="button"
:disabled="config.disabled"
:key="$index"
:name="name"
:read-only="isReadOnly"
:text="option.label || option.value"
:value="option.value"
:variant="value == option.value ? 'pressed' : 'default'"
@click="updateSelectedOption(option.value)"
/>
<ButtonGroup :overflow="config.appearance_previews ? 'gap' : 'stack'" ref="buttonGroup">
<template v-for="(option, $index) in options" :key="$index">
<Button
v-if="config.appearance_previews"
ref="button"
:disabled="config.disabled"
:name="name"
:read-only="isReadOnly"
:value="option.value"
:variant="value == option.value ? 'pressed' : 'default'"
:class="appearancePreviewButtonClass(option.value)"
@click="updateSelectedOption(option.value)"
>
<div class="flex w-full flex-col items-start gap-2 py-0.5 text-left">
<span class="text-sm font-medium">{{ option.label || option.value }}</span>
<ControlAppearancePreview :appearance="option.value" :control="config.control || 'radio'" />
</div>
</Button>
<Button
v-else
ref="button"
:disabled="config.disabled"
:name="name"
:read-only="isReadOnly"
:text="option.label || option.value"
:value="option.value"
:variant="value == option.value ? 'pressed' : 'default'"
@click="updateSelectedOption(option.value)"
/>
</template>
</ButtonGroup>
</template>

<script>
import Fieldtype from './Fieldtype.vue';
import HasInputOptions from './HasInputOptions.js';
import ControlAppearancePreview from './ControlAppearancePreview.vue';
import { Button, ButtonGroup } from '@/components/ui';

export default {
mixins: [Fieldtype, HasInputOptions],
components: {
Button,
ButtonGroup
ButtonGroup,
ControlAppearancePreview,
},

computed: {
Expand All @@ -41,6 +60,28 @@ export default {
},

methods: {
appearancePreviewButtonClass(optionValue) {
if (!this.config.appearance_previews) {
return null;
}

const base = 'min-w-34 h-auto items-start justify-start py-1.5';

if (this.value != optionValue) {
return [
base,
'from-white to-white hover:from-white hover:to-gray-50',
'dark:from-gray-850 dark:to-gray-850 dark:hover:from-gray-800 dark:hover:to-gray-850',
];
}

return [
base,
'from-gray-150 to-gray-50 border-gray-300',
'dark:from-gray-925 dark:to-gray-900 dark:border-gray-700/80',
];
},

updateSelectedOption(newValue) {
this.update(this.value == newValue && this.config.clearable ? null : newValue);
},
Expand Down
10 changes: 9 additions & 1 deletion resources/js/components/fieldtypes/CheckboxesFieldtype.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div>
<CheckboxGroup v-model="values" :inline="config.inline" ref="checkboxes">
<CheckboxGroup v-model="values" :appearance="appearance" ref="checkboxes">
<Checkbox
v-for="(option, index) in options"
:disabled="config.disabled"
Expand Down Expand Up @@ -33,6 +33,14 @@ export default {
},

computed: {
appearance() {
if (this.config.appearance) {
return this.config.appearance;
}

return this.config.inline ? 'inline' : 'default';
},

options() {
return this.normalizeInputOptions(this.meta.options || this.config.options);
},
Expand Down
79 changes: 79 additions & 0 deletions resources/js/components/fieldtypes/ControlAppearancePreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<script setup>
const props = defineProps({
appearance: {
type: String,
required: true,
validator: (value) => ['default', 'inline', 'chips'].includes(value),
},
control: {
type: String,
default: 'radio',
validator: (value) => ['radio', 'checkbox'].includes(value),
},
});

const isRadio = props.control === 'radio';
const chipClass = isRadio
? 'rounded-full px-1.5 pe-2 py-1 bg-white dark:bg-gray-850'
: 'items-center rounded-sm px-1.5 pe-2 py-1 bg-white dark:bg-gray-850';
</script>

<template>
<div
class="pointer-events-none select-none"
:class="{
'flex flex-col gap-1.5 pb-0.75': appearance === 'default',
'flex flex-wrap gap-1.5': appearance === 'inline' || appearance === 'chips',
}"
aria-hidden="true"
>
<div
v-for="selected in [true, false]"
:key="selected ? 'on' : 'off'"
class="flex items-center gap-1"
:class="appearance === 'chips' ? `border border-gray-300 shadow-ui-xs dark:border-gray-700 ${chipClass}` : null"
>
<div
v-if="isRadio"
class="relative flex size-2 shrink-0 items-center justify-center rounded-full border shadow-ui-xs"
:class="
selected
? 'border-ui-accent-bg bg-white dark:bg-gray-300'
: 'border-gray-400/75 bg-white dark:border-none dark:bg-gray-500'
"
>
<div v-if="selected" class="size-1 rounded-full bg-ui-accent-bg" />
</div>
<div
v-else
class="flex size-2 shrink-0 items-center justify-center rounded-[2px] border shadow-ui-xs"
:class="
selected
? 'border-ui-accent-bg bg-ui-accent-bg dark:border-none'
: 'border-gray-400/75 bg-white dark:border-none dark:bg-gray-500'
"
>
<svg
v-if="selected"
viewBox="0 0 10 8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="size-1.5 shrink-0 !text-white !opacity-100"
aria-hidden="true"
>
<path
d="M9 1L3.5 6.5L1 4"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<div
class="h-1 rounded-[2px] bg-gray-300 dark:bg-gray-600"
:class="[selected ? 'w-5 dark:bg-gray-500' : 'w-4 dark:bg-gray-700']"
/>
</div>
</div>
</template>
10 changes: 9 additions & 1 deletion resources/js/components/fieldtypes/RadioFieldtype.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<RadioGroup :inline="config.inline" :model-value="value" @update:model-value="update" ref="radio">
<RadioGroup :appearance="appearance" :model-value="value" @update:model-value="update" ref="radio">
<Radio
v-for="(option, index) in options"
:disabled="config.disabled"
Expand All @@ -25,6 +25,14 @@ export default {
},

computed: {
appearance() {
if (this.config.appearance) {
return this.config.appearance;
}

return this.config.inline ? 'inline' : 'default';
},

options() {
return this.normalizeInputOptions(this.meta.options || this.config.options);
},
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/ui/Button/Button.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const buttonClasses = computed(() => {
subtle: 'bg-transparent hover:bg-gray-400/10 text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:bg-white/7 dark:hover:text-gray-200 [&_svg]:opacity-35',
pressed: [
'bg-linear-to-b from-gray-200 to-gray-150 text-gray-900 border border-gray-300 inset-shadow-sm/10',
'dark:from-black dark:to-black dark:text-white dark:border-gray-700/80',
'dark:from-gray-950 dark:to-gray-900 dark:text-white dark:border-gray-700/80',
],
},
size: {
Expand Down
29 changes: 25 additions & 4 deletions resources/js/components/ui/Checkbox/Group.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
<script setup>
import { useId } from 'vue';
import { computed, provide, useId } from 'vue';
import { CheckboxGroupRoot } from 'reka-ui';

defineProps({
/** When `true`, displays checkboxes horizontally */
const props = defineProps({
/** Controls how the checkbox group is displayed. Options: `default`, `inline`, `chips` */
appearance: {
type: String,
default: 'default',
validator: (value) => ['default', 'inline', 'chips'].includes(value),
},
/** @deprecated Use `appearance="inline"` instead. */
inline: { type: Boolean, default: false },
/** The controlled value of the checkbox group */
modelValue: { type: Array, default: () => [] },
Expand All @@ -12,6 +18,16 @@ defineProps({
required: { type: Boolean, default: false },
});

const resolvedAppearance = computed(() => {
if (props.appearance !== 'default') {
return props.appearance;
}

return props.inline ? 'inline' : 'default';
});

provide('checkboxAppearance', resolvedAppearance);

const focus = function () {
console.log('focusing. todo.');
};
Expand All @@ -27,8 +43,13 @@ defineExpose({ focus });
@update:modelValue="$emit('update:modelValue', $event)"
:name="name"
class="relative block w-full space-y-2"
:class="{ 'flex flex-wrap space-y-0 gap-x-4 gap-y-2': inline }"
:class="{
'flex flex-wrap space-y-0 gap-x-4 gap-y-2': resolvedAppearance === 'inline' || resolvedAppearance === 'chips',
'gap-x-2.5!': resolvedAppearance === 'chips',
}"
:data-appearance="resolvedAppearance !== 'default' ? resolvedAppearance : undefined"
data-ui-input
data-ui-checkbox-group
>
<slot />
</CheckboxGroupRoot>
Expand Down
14 changes: 11 additions & 3 deletions resources/js/components/ui/Checkbox/Item.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup>
import { CheckboxIndicator, CheckboxRoot, useId } from 'reka-ui';
import { computed, useAttrs } from 'vue';
import { computed, inject, useAttrs } from 'vue';
import { cva } from 'cva';
import { twMerge } from 'tailwind-merge';

Expand All @@ -9,6 +9,12 @@ defineOptions({ inheritAttrs: false });
const attrs = useAttrs();

const props = defineProps({
/** Additional classes applied when the group appearance is `chips` */
chipsClass: {
type: String,
default:
'items-center gap-2 border border-gray-300 dark:border-gray-700 mb-0 p-2 py-2 pe-3 shadow-ui-xs rounded-xl [&_button]:mt-0',
},
/** Controls the vertical alignment of the checkbox with its label. Options: `start`, `center` */
align: { type: String, default: 'start', validator: (value) => ['start', 'center'].includes(value) },
/** Description text to display below the label */
Expand All @@ -34,6 +40,8 @@ const props = defineProps({

const emit = defineEmits(['update:modelValue', 'keydown']);

const appearance = inject('checkboxAppearance', computed(() => 'default'));

const id = useId();

const handleKeydown = (event) => {
Expand Down Expand Up @@ -76,7 +84,7 @@ const containerClasses = computed(() => {
},
})({ ...props });

return twMerge(classes, attrs.class);
return twMerge(classes, appearance.value === 'chips' ? props.chipsClass : null, attrs.class);
});

const conditionalProps = computed(() => {
Expand Down Expand Up @@ -104,7 +112,7 @@ const conditionalProps = computed(() => {
</script>

<template>
<div :class="containerClasses">
<div :class="containerClasses" data-ui-checkbox-item>
<CheckboxRoot
:disabled="readOnly || disabled"
:id
Expand Down
28 changes: 24 additions & 4 deletions resources/js/components/ui/Radio/Group.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
<script setup>
import { useId } from 'vue';
import { computed, provide, useId } from 'vue';
import { RadioGroupRoot } from 'reka-ui';

defineProps({
/** When `true`, displays radio buttons horizontally */
const props = defineProps({
/** Controls how the radio group is displayed. Options: `default`, `inline`, `chips` */
appearance: {
type: String,
default: 'default',
validator: (value) => ['default', 'inline', 'chips'].includes(value),
},
/** @deprecated Use `appearance="inline"` instead. */
inline: { type: Boolean, default: false },
/** The controlled value of the radio group */
modelValue: { type: String, default: null },
Expand All @@ -12,6 +18,16 @@ defineProps({
required: { type: Boolean, default: false },
});

const resolvedAppearance = computed(() => {
if (props.appearance !== 'default') {
return props.appearance;
}

return props.inline ? 'inline' : 'default';
});

provide('radioAppearance', resolvedAppearance);

const focus = function () {
console.log('focusing. todo.');
};
Expand All @@ -27,7 +43,11 @@ defineExpose({ focus });
@update:modelValue="$emit('update:modelValue', $event)"
:name="name"
class="relative block w-full space-y-2"
:class="{ 'flex flex-wrap space-y-0 gap-x-4 gap-y-2': inline }"
:class="{
'flex flex-wrap space-y-0 gap-x-4 gap-y-2': resolvedAppearance === 'inline' || resolvedAppearance === 'chips',
'gap-x-2.5!': resolvedAppearance === 'chips',
}"
:data-appearance="resolvedAppearance !== 'default' ? resolvedAppearance : undefined"
data-ui-input
data-ui-radio-group
>
Expand Down
Loading
Loading