Merge pull request #220 from directus/room-cleaning

Cleaned up all interfaces and displays.
This commit is contained in:
Rijk van Zanten
2020-09-10 11:21:06 -04:00
committed by GitHub
76 changed files with 1085 additions and 306 deletions

View File

@@ -24,6 +24,7 @@ You can add any custom (text) prefix/suffix to the value in the input using the
| `slug` | Force the value to be URL safe | `false` |
| `slug-separator` | What character to use as separator in slugs | `-` |
| `active` | Force the focus state | `false` |
| `trim` | Trim the start and end whitespace | `false` |
Note: all other attached attributes are bound to the input HTMLELement in the component. This allows you to attach any of the standard HTML attributes like `min`, `length`, or `pattern`.

View File

@@ -121,6 +121,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
trim: {
type: Boolean,
default: true,
},
},
setup(props, { emit, listeners }) {
const input = ref<HTMLInputElement | null>(null);
@@ -177,20 +181,28 @@ export default defineComponent({
function emitValue(event: InputEvent) {
let value = (event.target as HTMLInputElement).value;
if (props.slug === true) {
const endsWithSpace = value.endsWith(' ');
value = slugify(value, { separator: props.slugSeparator });
if (endsWithSpace) value += props.slugSeparator;
}
if (props.type === 'number') {
emit('input', Number(value));
} else {
if (props.trim === true) {
value = value.trim();
}
if (props.dbSafe === true) {
value = value.toLowerCase();
value = value.replace(/\s/g, '_');
// Replace é -> e etc
value = value.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
if (props.slug === true) {
const endsWithSpace = value.endsWith(' ');
value = slugify(value, { separator: props.slugSeparator });
if (endsWithSpace) value += props.slugSeparator;
}
emit('input', value);
if (props.dbSafe === true) {
value = value.toLowerCase();
value = value.replace(/\s/g, '_');
// Replace é -> e etc
value = value.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
emit('input', value);
}
}
function stepUp() {
@@ -200,8 +212,8 @@ export default defineComponent({
input.value.stepUp();
if (input.value.value) {
return emit('input', input.value.value);
if (input.value.value != null) {
return emit('input', Number(input.value.value));
}
}
@@ -213,7 +225,7 @@ export default defineComponent({
input.value.stepDown();
if (input.value.value) {
return emit('input', input.value.value);
return emit('input', Number(input.value.value));
} else {
return emit('input', props.min || 0);
}

View File

@@ -3,7 +3,8 @@ import DisplayCollection from './collection.vue';
export default defineDisplay(({ i18n }) => ({
id: 'collection',
name: i18n.t('collection'),
name: i18n.t('displays.collection.collection'),
description: i18n.t('displays.collection.description'),
types: ['string'],
icon: 'label',
handler: DisplayCollection,
@@ -15,9 +16,12 @@ export default defineDisplay(({ i18n }) => ({
meta: {
interface: 'toggle',
options: {
label: `Show the collection's icon`,
label: i18n.t('displays.collection.icon_label'),
},
},
schema: {
default_value: false,
},
},
],
}));

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="color-dot">
<value-null v-if="value === null" />
<div class="dot" :style="styles" v-tooltip="displayValue"></div>
</div>
@@ -8,6 +8,7 @@
<script lang="ts">
import { defineComponent, computed, PropType } from '@vue/composition-api';
import formatTitle from '@directus/format-title';
import { isHex } from '@/utils/color';
type Choice = {
value: string;
@@ -42,24 +43,32 @@ export default defineComponent({
});
const styles = computed(() => {
if (isHex(props.value)) {
return { backgroundColor: props.value || props.defaultColor };
}
return {
backgroundColor: currentChoice.value?.color || props.defaultColor,
};
});
return { displayValue, styles };
return { displayValue, styles, isHex };
},
});
</script>
<style lang="scss" scoped>
.dot {
display: inline-block;
flex-shrink: 0;
width: 12px;
height: 12px;
margin: 0 4px;
vertical-align: middle;
border-radius: 6px;
.color-dot {
display: flex;
align-items: center;
.dot {
display: inline-block;
flex-shrink: 0;
width: 12px;
height: 12px;
margin: 0 4px;
border-radius: 6px;
}
}
</style>

View File

@@ -3,14 +3,15 @@ import DisplayColorDot from './color-dot.vue';
export default defineDisplay(({ i18n }) => ({
id: 'color-dot',
name: i18n.t('color_dot'),
name: i18n.t('displays.color-dot.color-dot'),
description: i18n.t('displays.color-dot.description'),
types: ['string'],
icon: 'flag',
handler: DisplayColorDot,
options: [
{
field: 'defaultColor',
name: i18n.t('default_color'),
name: i18n.t('displays.color-dot.default_color'),
type: 'string',
meta: {
interface: 'color',
@@ -25,6 +26,7 @@ export default defineDisplay(({ i18n }) => ({
name: i18n.t('choices'),
type: 'json',
meta: {
note: i18n.t('displays.color-dot.choices_note'),
interface: 'repeater',
options: {
template: '{{text}}',

View File

@@ -16,9 +16,9 @@ export default defineComponent({
required: true,
},
type: {
type: String as PropType<'datetime' | 'time' | 'date'>,
type: String as PropType<'dateTime' | 'time' | 'date' | 'timestamp'>,
required: true,
validator: (val: string) => ['datetime', 'date', 'time', 'timestamp'].includes(val),
validator: (val: string) => ['dateTime', 'date', 'time', 'timestamp'].includes(val),
},
relative: {
type: Boolean,

View File

@@ -3,20 +3,24 @@ import DisplayDateTime from './datetime.vue';
export default defineDisplay(({ i18n }) => ({
id: 'datetime',
name: i18n.t('datetime'),
name: i18n.t('displays.datetime.datetime'),
description: i18n.t('displays.datetime.description'),
icon: 'query_builder',
handler: DisplayDateTime,
options: [
{
field: 'relative',
name: i18n.t('relative'),
name: i18n.t('displays.datetime.relative'),
type: 'boolean',
meta: {
interface: 'toggle',
options: {
label: 'Show relative time, eg: 5 minutes ago',
label: i18n.t('displays.datetime.relative_label'),
},
},
schema: {
default_value: false,
},
},
],
types: ['dateTime', 'date', 'time', 'timestamp'],

View File

@@ -3,8 +3,9 @@ import DisplayFile from './file.vue';
export default defineDisplay(({ i18n }) => ({
id: 'file',
name: i18n.t('file'),
icon: 'insert_photo',
name: i18n.t('displays.file.file'),
description: i18n.t('displays.file.description'),
icon: 'insert_drive_file',
handler: DisplayFile,
types: ['uuid'],
options: [],

View File

@@ -3,9 +3,10 @@ import handler from './handler';
export default defineDisplay(({ i18n }) => ({
id: 'filesize',
name: i18n.t('filesize'),
name: i18n.t('displays.filesize.filesize'),
description: i18n.t('displays.filesize.description'),
icon: 'description',
handler: handler,
options: null,
options: [],
types: ['integer'],
}));

View File

@@ -3,19 +3,21 @@ import DisplayFormattedValue from './formatted-value.vue';
export default defineDisplay(({ i18n }) => ({
id: 'formatted-value',
name: i18n.t('formatted_value'),
name: i18n.t('displays.formatted-value.formatted-value'),
description: i18n.t('displays.formatted-value.description'),
types: ['string', 'text'],
icon: 'text_format',
handler: DisplayFormattedValue,
options: [
{
field: 'formatTitle',
name: i18n.t('format_title'),
name: i18n.t('displays.formatted-value.format_title'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('auto_format_casing'),
label: i18n.t('displays.formatted-value.format_title_label'),
},
},
schema: {
@@ -25,13 +27,17 @@ export default defineDisplay(({ i18n }) => ({
{
field: 'bold',
name: i18n.t('bold'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('use_bold_style'),
label: i18n.t('displays.formatted-value.bold_label'),
},
},
schema: {
default_value: false,
},
},
{
field: 'color',
@@ -55,6 +61,9 @@ export default defineDisplay(({ i18n }) => ({
],
},
},
schema: {
default_value: 'sans-serif',
},
},
],
}));

View File

@@ -1,16 +1,34 @@
<template functional>
<v-icon small :name="props.value" />
<template>
<v-icon small :name="value" :style="style" :filled="filled" />
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { computed, defineComponent } from '@vue/composition-api';
import { isHex } from '@/utils/color';
export default defineComponent({
name: 'display-icon',
props: {
value: {
type: String,
default: null,
},
color: {
type: String,
default: null,
},
filled: {
type: Boolean,
default: false,
},
},
setup(props) {
const style = computed(() => {
if (isHex(props.color)) return { '--v-icon-color': props.color };
else return {};
});
return { style };
},
});
</script>

View File

@@ -3,21 +3,34 @@ import DisplayIcon from './icon.vue';
export default defineDisplay(({ i18n }) => ({
id: 'icon',
name: i18n.t('icon'),
name: i18n.t('displays.icon.icon'),
description: i18n.t('displays.icon.description'),
icon: 'thumb_up',
handler: DisplayIcon,
options: [
{
field: 'outline',
name: i18n.t('outline'),
field: 'filled',
name: i18n.t('displays.icon.filled'),
type: 'boolean',
meta: {
interface: 'toggle',
width: 'half',
options: {
label: i18n.t('use_outline_variant'),
label: i18n.t('displays.icon.filled_label'),
},
},
schema: {
default_value: false,
},
},
{
field: 'color',
name: i18n.t('color'),
type: 'string',
meta: {
interface: 'color',
width: 'half',
},
},
],
types: ['string'],

View File

@@ -3,18 +3,24 @@ import DisplayImage from './image.vue';
export default defineDisplay(({ i18n }) => ({
id: 'image',
name: i18n.t('image'),
name: i18n.t('displays.image.image'),
description: i18n.t('displays.image.description'),
types: ['uuid'],
icon: 'insert_photo',
handler: DisplayImage,
options: [
{
field: 'circle',
name: i18n.t('circle'),
name: i18n.t('displays.image.circle'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('displays.image.circle_label'),
},
},
schema: {
default_value: false,
},
},

View File

@@ -3,14 +3,15 @@ import DisplayLabels from './labels.vue';
export default defineDisplay(({ i18n }) => ({
id: 'labels',
name: i18n.t('labels'),
name: i18n.t('displays.labels.labels'),
description: i18n.t('displays.labels.description'),
types: ['string', 'json'],
icon: 'flag',
handler: DisplayLabels,
options: [
{
field: 'defaultForeground',
name: i18n.t('default_foreground'),
name: i18n.t('displays.labels.default_foreground'),
type: 'string',
meta: {
interface: 'color',
@@ -22,7 +23,7 @@ export default defineDisplay(({ i18n }) => ({
},
{
field: 'defaultBackground',
name: i18n.t('default_background'),
name: i18n.t('displays.labels.default_background'),
type: 'string',
meta: {
interface: 'color',
@@ -39,6 +40,9 @@ export default defineDisplay(({ i18n }) => ({
meta: {
width: 'half-left',
interface: 'toggle',
options: {
label: i18n.t('displays.labels.format_label'),
},
},
schema: {
default_value: true,

View File

@@ -4,18 +4,21 @@ import { defineDisplay } from '@/displays/define';
export default defineDisplay(({ i18n }) => ({
id: 'mime-type',
name: i18n.t('mime_type'),
name: i18n.t('displays.mime-type.mime-type'),
description: i18n.t('displays.mime-type.description'),
icon: 'picture_as_pdf',
options: [
{
field: 'showAsExtension',
name: i18n.t('extension_only'),
name: i18n.t('displays.mime-type.extension_only'),
type: 'boolean',
meta: {
interface: 'toggle',
options: {
label: i18n.t('only_show_the_file_extension'),
label: i18n.t('displays.mime-type.extension_only_label'),
},
},
schema: {
default_value: false,
},
},

View File

@@ -4,8 +4,25 @@ import DisplayRating from './rating.vue';
export default defineDisplay(({ i18n }) => ({
id: 'rating',
name: i18n.t('displays.rating.rating'),
description: i18n.t('displays.rating.description'),
icon: 'star',
handler: DisplayRating,
options: null,
options: [
{
field: 'simple',
name: i18n.t('displays.rating.simple'),
type: 'boolean',
meta: {
interface: 'toggle',
width: 'half',
options: {
label: i18n.t('displays.rating.simple_label'),
},
},
schema: {
default_value: false,
},
},
],
types: ['integer', 'decimal', 'float'],
}));

View File

@@ -1,5 +1,5 @@
<template>
<span v-if="false" class="rating simple">
<span v-if="simple" class="rating simple">
<v-icon small name="star" />
{{ value }}
</span>
@@ -17,7 +17,9 @@
import { defineComponent, computed, PropType } from '@vue/composition-api';
type InterfaceOptions = {
maxStars: number;
minValue: number;
maxValue: number;
stepInterval: number;
};
export default defineComponent({
@@ -26,6 +28,10 @@ export default defineComponent({
type: Number,
default: null,
},
simple: {
type: Boolean,
default: false,
},
interfaceOptions: {
type: Object as PropType<InterfaceOptions>,
default: null,
@@ -35,7 +41,7 @@ export default defineComponent({
const starCount = computed(() => {
if (props.interfaceOptions === null) return 5;
return props.interfaceOptions.maxStars;
return Math.ceil(props.interfaceOptions.maxValue);
});
const ratingPercentage = computed(() => ({

View File

@@ -12,8 +12,9 @@ type Options = {
export default defineDisplay(({ i18n }) => ({
id: 'related-values',
name: i18n.t('related_values'),
icon: 'text_fields',
name: i18n.t('displays.related-values.related-values'),
description: i18n.t('displays.related-values.description'),
icon: 'settings_ethernet',
handler: DisplayRelatedValues,
options: [
/** @todo make this a component so we have dynamic collection for display template component */

View File

@@ -3,7 +3,8 @@ import DisplayUser from './user.vue';
export default defineDisplay(({ i18n }) => ({
id: 'user',
name: i18n.t('user'),
name: i18n.t('displays.user.user'),
description: i18n.t('displays.user.description'),
types: ['uuid'],
icon: 'person',
handler: DisplayUser,
@@ -13,21 +14,24 @@ export default defineDisplay(({ i18n }) => ({
name: i18n.t('display'),
type: 'string',
meta: {
width: 'half',
interface: 'dropdown',
options: [
{
text: i18n.t('avatar'),
value: 'avatar',
},
{
text: i18n.t('name'),
value: 'name',
},
{
text: i18n.t('both'),
value: 'both',
},
],
options: {
choices: [
{
text: i18n.t('displays.user.avatar'),
value: 'avatar',
},
{
text: i18n.t('displays.user.name'),
value: 'name',
},
{
text: i18n.t('displays.user.both'),
value: 'both',
},
],
},
},
schema: {
default_value: 'both',
@@ -40,6 +44,11 @@ export default defineDisplay(({ i18n }) => ({
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('displays.user.circle_label'),
},
},
schema: {
default_value: false,
},
},

View File

@@ -3,9 +3,10 @@ import InterfaceCheckboxes from './checkboxes.vue';
export default defineInterface(({ i18n }) => ({
id: 'checkboxes',
name: i18n.t('checkboxes'),
name: i18n.t('interfaces.checkboxes.checkboxes'),
icon: 'check_box',
component: InterfaceCheckboxes,
description: i18n.t('interfaces.checkboxes.description'),
types: ['json'],
options: [
{
@@ -43,7 +44,7 @@ export default defineInterface(({ i18n }) => ({
},
{
field: 'allowOther',
name: i18n.t('allow_other'),
name: i18n.t('interfaces.checkboxes.allow_other'),
type: 'boolean',
meta: {
width: 'half',
@@ -77,8 +78,8 @@ export default defineInterface(({ i18n }) => ({
interface: 'icon',
},
schema: {
default_value: 'check_box',
},
default_value: 'check_box'
}
},
{
field: 'iconOff',
@@ -89,8 +90,8 @@ export default defineInterface(({ i18n }) => ({
interface: 'icon',
},
schema: {
default_value: 'check_box_outline_blank',
},
default_value: 'check_box_outline_blank'
}
},
],
recommendedDisplays: ['tags'],

View File

@@ -19,6 +19,7 @@ import 'codemirror/addon/search/matchesonscrollbar.js';
import 'codemirror/addon/scroll/annotatescrollbar.js';
import 'codemirror/addon/lint/lint.js';
import 'codemirror/addon/search/search.js';
import 'codemirror/addon/display/placeholder.js';
import 'codemirror/addon/comment/comment.js';
import 'codemirror/addon/dialog/dialog.js';
@@ -48,6 +49,10 @@ export default defineComponent({
type: Boolean,
default: true,
},
placeholder: {
type: String,
default: null,
},
language: {
type: String,
default: 'text/plain',
@@ -235,6 +240,7 @@ export default defineComponent({
lineNumbers: props.lineNumber,
readOnly: props.disabled ? 'nocursor' : false,
mode: props.language,
placeholder: props.placeholder,
},
props.altOptions ? props.altOptions : {}
);
@@ -268,7 +274,13 @@ export default defineComponent({
};
function fillTemplate() {
emit('input', props.template);
if(props.type === 'json') {
try {
emit('input', JSON.parse(props.template));
} catch {}
} else {
emit('input', props.template);
}
}
},
});

View File

@@ -12,7 +12,8 @@ choices.push({ text: 'JSON', value: 'JSON' });
export default defineInterface(({ i18n }) => ({
id: 'code',
name: i18n.t('code'),
name: i18n.t('interfaces.code.code'),
description: i18n.t('interfaces.code.description'),
icon: 'code',
component: InterfaceCode,
types: ['string', 'json', 'text'],
@@ -29,7 +30,7 @@ export default defineInterface(({ i18n }) => ({
},
{
field: 'lineNumber',
name: i18n.t('line_number'),
name: i18n.t('interfaces.code.line_number'),
type: 'boolean',
meta: {
width: 'half',
@@ -47,6 +48,7 @@ export default defineInterface(({ i18n }) => ({
width: 'full',
interface: 'code',
options: {
placeholder: i18n.t('interfaces.code.placeholder'),
language: 'text/plain',
},
},

View File

@@ -16,7 +16,7 @@ export default defineComponent({
type: Boolean,
default: false,
},
includeMeta: {
includeSystem: {
type: Boolean,
default: false,
},
@@ -25,7 +25,7 @@ export default defineComponent({
const collectionsStore = useCollectionsStore();
const collections = computed(() => {
if (props.includeMeta) return collectionsStore.state.collections;
if (props.includeSystem) return collectionsStore.state.collections;
return collectionsStore.state.collections.filter(
(collection) => collection.collection.startsWith('directus_') === false

View File

@@ -3,7 +3,8 @@ import InterfaceCollections from './collections.vue';
export default defineInterface(({ i18n }) => ({
id: 'collections',
name: i18n.t('collections'),
name: i18n.t('interfaces.collections.collections'),
description: i18n.t('interfaces.collections.description'),
icon: 'featured_play_list',
component: InterfaceCollections,
types: ['string'],
@@ -16,12 +17,13 @@ export default defineInterface(({ i18n }) => ({
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('include_system_collections'),
label: i18n.t('interfaces.collections.include_system_collections'),
},
},
schema: {
default_value: false,
}
},
},
],
recommendedDisplays: ['collection'],
}));

View File

@@ -3,7 +3,7 @@
<template #activator>
<v-input
:disabled="disabled"
:placeholder="$t('choose_a_color')"
:placeholder="$t('interfaces.color.placeholder')"
v-model="hexValue"
:pattern="/#([a-f\d]{2}){3}/i"
class="color-input"

View File

@@ -3,19 +3,22 @@ import InterfaceColor from './color.vue';
export default defineInterface(({ i18n }) => ({
id: 'color',
name: i18n.t('color'),
name: i18n.t('interfaces.color.color'),
description: i18n.t('interfaces.color.description'),
icon: 'palette',
component: InterfaceColor,
types: ['string'],
recommendedDisplays: ['color-dot'],
options: [
{
field: 'presets',
name: i18n.t('preset_colors'),
name: i18n.t('interfaces.color.preset_colors'),
type: 'string',
meta: {
width: 'full',
interface: 'repeater',
options: {
placeholder: i18n.t('interfaces.color.preset_colors_placeholder'),
template: '{{ name }} - {{ color }}',
fields: [
{
@@ -25,7 +28,7 @@ export default defineInterface(({ i18n }) => ({
meta: {
interface: 'text-input',
width: 'half',
}
},
},
{
field: 'color',
@@ -33,12 +36,12 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('color'),
meta: {
interface: 'color',
width: 'half'
}
}
]
}
}
width: 'half',
},
},
],
},
},
},
],
}));

View File

@@ -50,7 +50,7 @@
<v-divider />
<button class="to-now" @click="setToNow">{{ $t('set_to_now') }}</button>
<button class="to-now" @click="setToNow">{{ $t('interfaces.datetime.set_to_now') }}</button>
</v-menu>
</template>

View File

@@ -3,14 +3,16 @@ import InterfaceDateTime from './datetime.vue';
export default defineInterface(({ i18n }) => ({
id: 'datetime',
name: i18n.t('datetime'),
name: i18n.t('interfaces.datetime.datetime'),
description: i18n.t('interfaces.datetime.description'),
icon: 'today',
component: InterfaceDateTime,
types: ['dateTime', 'date', 'time', 'timestamp'],
options: [
{
field: 'includeSeconds',
name: i18n.t('include_seconds'),
name: i18n.t('interfaces.datetime.include_seconds'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',

View File

@@ -1,9 +1,9 @@
<template>
<v-notice v-if="!collectionField" type="warning">
{{ $t('collection_field_not_setup') }}
{{ $t('interfaces.display-template.collection_field_not_setup') }}
</v-notice>
<v-notice v-else-if="collection === null" type="warning">
{{ $t('select_a_collection') }}
{{ $t('interfaces.display-template.select_a_collection') }}
</v-notice>
<v-field-template v-else :collection="collection" @input="$listeners.input" :value="value" :disabled="disabled" />
</template>

View File

@@ -3,10 +3,24 @@ import InterfaceDisplayTemplate from './display-template.vue';
export default defineInterface(({ i18n }) => ({
id: 'display-template',
name: i18n.t('display-template'),
name: i18n.t('interfaces.display-template.display-template'),
description: i18n.t('interfaces.display-template.description'),
icon: 'arrow_drop_down_circle',
component: InterfaceDisplayTemplate,
types: ['string'],
system: true,
options: [],
options: [
{
field: 'collectionField',
name: i18n.t('interfaces.display-template.collection_field'),
type: 'string',
meta: {
width: 'full',
interface: 'text-input'
},
schema: {
default_value: null,
},
},
],
}));

View File

@@ -1,12 +1,12 @@
<template>
<v-divider
:inline-title="false"
:class="{ margin: icon || title }"
:style="{
'--v-divider-color': color,
'--v-divider-label-color': color,
}"
large
:inline-title="inlineTitle"
>
<template v-if="icon" #icon><v-icon :name="icon" /></template>
<template v-if="title" #default>{{ title }}</template>
@@ -30,6 +30,10 @@ export default defineComponent({
type: String,
default: null,
},
inlineTitle: {
type: Boolean,
default: false,
},
},
});
</script>

View File

@@ -3,7 +3,8 @@ import InterfaceDivider from './divider.vue';
export default defineInterface(({ i18n }) => ({
id: 'divider',
name: i18n.t('divider'),
name: i18n.t('interfaces.divider.divider'),
description: i18n.t('interfaces.divider.description'),
icon: 'remove',
component: InterfaceDivider,
hideLabel: true,
@@ -17,7 +18,7 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'half',
interface: 'color',
}
},
},
{
field: 'icon',
@@ -26,7 +27,7 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'half',
interface: 'icon',
}
},
},
{
field: 'title',
@@ -35,7 +36,25 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'half',
interface: 'text-input',
}
options: {
placeholder: i18n.t('interfaces.divider.title_placeholder'),
},
},
},
{
field: 'inlineTitle',
name: i18n.t('interfaces.divider.inline_title'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('interfaces.divider.inline_title_label'),
},
},
schema: {
default_value: false,
},
},
],
}));

View File

@@ -12,6 +12,7 @@
:show-deselect="allowNone"
:placeholder="placeholder"
:allow-other="allowOther"
:close-on-content-click="false"
>
<template #prepend v-if="icon">
<v-icon :name="icon" />

View File

@@ -3,7 +3,8 @@ import InterfaceDropdownMultiselect from './dropdown-multiselect.vue';
export default defineInterface(({ i18n }) => ({
id: 'dropdown-multiselect',
name: i18n.t('dropdown_multiple'),
name: i18n.t('interfaces.dropdown-multiselect.dropdown-multiselect'),
description: i18n.t('interfaces.dropdown-multiselect.description'),
icon: 'arrow_drop_down_circle',
component: InterfaceDropdownMultiselect,
types: ['json'],
@@ -16,6 +17,7 @@ export default defineInterface(({ i18n }) => ({
width: 'full',
interface: 'repeater',
options: {
placeholder: i18n.t('interfaces.dropdown.choices_placeholder'),
template: '{{ text }}',
fields: [
{
@@ -23,6 +25,7 @@ export default defineInterface(({ i18n }) => ({
type: 'string',
name: i18n.t('text'),
meta: {
width: 'half',
interface: 'text-input',
},
},
@@ -31,6 +34,7 @@ export default defineInterface(({ i18n }) => ({
type: 'string',
name: i18n.t('value'),
meta: {
width: 'half',
interface: 'text-input',
options: {
font: 'monospace',
@@ -43,24 +47,46 @@ export default defineInterface(({ i18n }) => ({
},
{
field: 'allowOther',
name: i18n.t('allow_other'),
name: i18n.t('interfaces.dropdown.allow_other'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('interfaces.dropdown.allow_other_label'),
},
},
schema: {
default_value: false,
},
},
{
field: 'allowNone',
name: i18n.t('allow_none'),
name: i18n.t('interfaces.dropdown.allow_none'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('interfaces.dropdown.allow_none_label'),
},
},
schema: {
default_value: false,
},
},
{
field: 'placeholder',
name: i18n.t('placeholder'),
type: 'string',
meta: {
width: 'half',
interface: 'text-input',
options: {
placeholder: i18n.t('enter_a_placeholder'),
},
},
},
{
field: 'icon',
name: i18n.t('icon'),

View File

@@ -4,6 +4,7 @@ import InterfaceDropdown from './dropdown.vue';
export default defineInterface(({ i18n }) => ({
id: 'dropdown',
name: i18n.t('dropdown'),
description: i18n.t('interfaces.dropdown.description'),
icon: 'arrow_drop_down_circle',
component: InterfaceDropdown,
types: ['string'],
@@ -16,6 +17,7 @@ export default defineInterface(({ i18n }) => ({
width: 'full',
interface: 'repeater',
options: {
placeholder: i18n.t('interfaces.dropdown.choices_placeholder'),
template: '{{ text }}',
fields: [
{
@@ -45,21 +47,31 @@ export default defineInterface(({ i18n }) => ({
},
{
field: 'allowOther',
name: i18n.t('allow_other'),
name: i18n.t('interfaces.dropdown.allow_other'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('interfaces.dropdown.allow_other_label'),
},
},
schema: {
default_value: false,
},
},
{
field: 'allowNone',
name: i18n.t('allow_none'),
name: i18n.t('interfaces.dropdown.allow_none'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('interfaces.dropdown.allow_none_label'),
},
},
schema: {
default_value: false,
},
},
@@ -79,6 +91,9 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'half',
interface: 'text-input',
options: {
placeholder: i18n.t('enter_a_placeholder'),
},
},
},
],

View File

@@ -3,7 +3,8 @@ import InterfaceFile from './file.vue';
export default defineInterface(({ i18n }) => ({
id: 'file',
name: i18n.t('file'),
name: i18n.t('interfaces.file.file'),
description: i18n.t('interfaces.file.description'),
icon: 'note_add',
component: InterfaceFile,
types: ['uuid'],

View File

@@ -3,10 +3,12 @@ import InterfaceFiles from './files.vue';
export default defineInterface(({ i18n }) => ({
id: 'files',
name: i18n.t('files'),
name: i18n.t('interfaces.files.files'),
description: i18n.t('interfaces.files.description'),
icon: 'note_add',
component: InterfaceFiles,
types: ['alias'],
relationship: 'm2m',
options: [],
recommendedDisplays: ['files'],
}));

View File

@@ -3,8 +3,9 @@ import { defineInterface } from '@/interfaces/define';
export default defineInterface(({ i18n }) => ({
id: 'hash',
name: i18n.t('hash'),
icon: 'text_fields',
name: i18n.t('interfaces.hash.hash'),
description: i18n.t('interfaces.hash.description'),
icon: 'fingerprint',
component: InterfaceHash,
types: ['string'],
options: [
@@ -15,16 +16,25 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'half',
interface: 'text-input',
}
options: {
placeholder: i18n.t('enter_a_placeholder'),
},
},
},
{
field: 'masked',
name: i18n.t('masked'),
name: i18n.t('interfaces.hash.masked'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
}
options: {
label: i18n.t('interfaces.hash.masked_label'),
},
},
schema: {
default_value: false,
},
},
],
}));

View File

@@ -3,10 +3,10 @@
<template #activator="{ active, activate }">
<v-input
:disabled="disabled"
:placeholder="value ? formatTitle(value) : $t('search_for_icon')"
:placeholder="value ? formatTitle(value) : $t('interfaces.icon.search_for_icon')"
v-model="searchQuery"
@focus="activate"
:class="{ 'has-value' : value}"
:class="{ 'has-value': value }"
>
<template #prepend>
<v-icon v-if="value" @click="activate" :name="value" :class="{ active: value }" />

View File

@@ -3,7 +3,8 @@ import InterfaceIcon from './icon.vue';
export default defineInterface(({ i18n }) => ({
id: 'icon',
name: i18n.t('icon'),
name: i18n.t('interfaces.icon.icon'),
description: i18n.t('interfaces.icon.description'),
icon: 'insert_emoticon',
component: InterfaceIcon,
types: ['string'],

View File

@@ -3,7 +3,8 @@ import InterfaceImage from './image.vue';
export default defineInterface(({ i18n }) => ({
id: 'image',
name: i18n.t('image'),
name: i18n.t('interfaces.image.image'),
description: i18n.t('interfaces.image.description'),
icon: 'insert_photo',
component: InterfaceImage,
types: ['uuid'],

View File

@@ -3,7 +3,8 @@ import InterfaceOptions from './interface-options.vue';
export default defineInterface(({ i18n }) => ({
id: 'interface-options',
name: 'Interface Options',
name: i18n.t('interfaces.interface-options.interface-options'),
description: i18n.t('interfaces.interface-options.description'),
icon: 'box',
component: InterfaceOptions,
types: ['string'],

View File

@@ -3,7 +3,8 @@ import InterfaceInterface from './interface.vue';
export default defineInterface(({ i18n }) => ({
id: 'interface',
name: 'Interface',
name: i18n.t('interfaces.interface.interface'),
description: i18n.t('interfaces.interface.description'),
icon: 'box',
component: InterfaceInterface,
types: ['string'],

View File

@@ -3,7 +3,8 @@ import InterfaceManyToMany from './many-to-many.vue';
export default defineInterface(({ i18n }) => ({
id: 'many-to-many',
name: i18n.t('many_to_many'),
name: i18n.t('interfaces.many-to-many.many-to-many'),
description: i18n.t('interfaces.many-to-many.description'),
icon: 'note_add',
component: InterfaceManyToMany,
relationship: 'm2m',

View File

@@ -3,7 +3,8 @@ import InterfaceManyToOne from './many-to-one.vue';
export default defineInterface(({ i18n }) => ({
id: 'many-to-one',
name: i18n.t('many_to_one'),
name: i18n.t('interfaces.many-to-one.many-to-one'),
description: i18n.t('interfaces.many-to-one.description'),
icon: 'arrow_right_alt',
component: InterfaceManyToOne,
types: ['uuid', 'string', 'text', 'integer', 'bigInteger'],
@@ -11,7 +12,7 @@ export default defineInterface(({ i18n }) => ({
options: [
{
field: 'template',
name: i18n.t('display_template'),
name: i18n.t('interfaces.many-to-one.display_template'),
type: 'string',
meta: {
width: 'half',

View File

@@ -3,8 +3,9 @@ import { defineInterface } from '@/interfaces/define';
export default defineInterface(({ i18n }) => ({
id: 'markdown',
name: i18n.t('markdown'),
icon: 'text_fields',
name: i18n.t('interfaces.markdown.markdown'),
description: i18n.t('interfaces.markdown.description'),
icon: 'functions',
component: InterfaceMarkdown,
types: ['text'],
options: [
@@ -15,15 +16,24 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'half',
interface: 'text-input',
options: {
placeholder: i18n.t('enter_a_placeholder'),
},
},
},
{
field: 'tabbed',
name: i18n.t('tabbed'),
name: i18n.t('interfaces.markdown.tabbed'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('interfaces.markdown.tabbed_label'),
},
},
schema: {
default_value: false,
},
},
],

View File

@@ -4,11 +4,11 @@
<v-tabs v-model="currentTab">
<v-tab>
<v-icon name="code" left />
{{ $t('edit') }}
{{ $t('interfaces.markdown.edit') }}
</v-tab>
<v-tab>
<v-icon name="visibility" outline left />
{{ $t('preview') }}
{{ $t('interfaces.markdown.preview') }}
</v-tab>
</v-tabs>
</div>
@@ -64,7 +64,6 @@ export default defineComponent({
.interface-markdown {
--v-textarea-min-height: var(--input-height-tall);
--v-textarea-max-height: 400px;
--v-tab-background-color: var(--background-subdued);
--v-tab-background-color-active: var(--background-subdued);
@@ -74,9 +73,9 @@ export default defineComponent({
.toolbar {
width: 100%;
height: 42px;
background-color: var(--background-subdued);
border: var(--border-width) solid var(--border-normal);
border-radius: var(--border-radius) var(--border-radius) 0 0;
background-color: var(--background-subdued);
}
.v-textarea {

View File

@@ -3,8 +3,9 @@ import InterfaceNotice from './notice.vue';
export default defineInterface(({ i18n }) => ({
id: 'notice',
name: i18n.t('notice'),
icon: 'remove',
name: i18n.t('interfaces.notice.notice'),
description: i18n.t('interfaces.notice.description'),
icon: 'info',
component: InterfaceNotice,
hideLabel: true,
hideLoader: true,
@@ -19,15 +20,18 @@ export default defineInterface(({ i18n }) => ({
interface: 'dropdown',
default_value: 'normal',
options: {
items: [
{ itemText: i18n.t('normal'), itemValue: 'normal' },
{ itemText: i18n.t('info'), itemValue: 'info' },
{ itemText: i18n.t('success'), itemValue: 'success' },
{ itemText: i18n.t('warning'), itemValue: 'warning' },
{ itemText: i18n.t('danger'), itemValue: 'danger' },
choices: [
{ text: i18n.t('normal'), value: 'normal' },
{ text: i18n.t('info'), value: 'info' },
{ text: i18n.t('success'), value: 'success' },
{ text: i18n.t('warning'), value: 'warning' },
{ text: i18n.t('danger'), value: 'danger' },
],
},
}
},
schema: {
default_value: 'normal',
},
},
{
field: 'icon',
@@ -36,7 +40,7 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'half',
interface: 'icon',
}
},
},
{
field: 'text',
@@ -45,7 +49,10 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'full',
interface: 'textarea',
}
options: {
placeholder: i18n.t('interfaces.notice.text'),
},
},
},
],
}));

View File

@@ -3,14 +3,15 @@ import { defineInterface } from '@/interfaces/define';
export default defineInterface(({ i18n }) => ({
id: 'numeric',
name: i18n.t('numeric'),
name: i18n.t('interfaces.numeric.numeric'),
description: i18n.t('interfaces.numeric.description'),
icon: 'dialpad',
component: InterfaceNumeric,
types: ['integer', 'decimal', 'float', 'bigInteger'],
options: [
{
field: 'min',
name: i18n.t('minimum_value'),
name: i18n.t('interfaces.numeric.minimum_value'),
type: 'integer',
meta: {
width: 'half',
@@ -19,7 +20,7 @@ export default defineInterface(({ i18n }) => ({
},
{
field: 'max',
name: i18n.t('maximum_value'),
name: i18n.t('interfaces.numeric.maximum_value'),
type: 'integer',
meta: {
width: 'half',
@@ -28,12 +29,15 @@ export default defineInterface(({ i18n }) => ({
},
{
field: 'step',
name: i18n.t('step_interval'),
name: i18n.t('interfaces.numeric.step_interval'),
type: 'integer',
meta: {
width: 'half',
interface: 'numeric',
},
schema: {
default_value: 1,
},
},
{
field: 'placeholder',
@@ -42,6 +46,9 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'half',
interface: 'text-input',
options: {
placeholder: i18n.t('enter_a_placeholder'),
},
},
},
{
@@ -77,6 +84,9 @@ export default defineInterface(({ i18n }) => ({
],
},
},
schema: {
default_value: 'sans-serif',
},
},
],
}));

View File

@@ -25,7 +25,7 @@ import { defineComponent, PropType } from '@vue/composition-api';
export default defineComponent({
props: {
value: {
type: [Number, String],
type: Number,
default: null,
},
disabled: {

View File

@@ -3,7 +3,8 @@ import InterfaceOneToMany from './one-to-many.vue';
export default defineInterface(({ i18n }) => ({
id: 'one-to-many',
name: i18n.t('one_to_many'),
name: i18n.t('interfaces.one-to-many.one-to-many'),
description: i18n.t('interfaces.one-to-many.description'),
icon: 'arrow_right_alt',
component: InterfaceOneToMany,
types: ['alias'],
@@ -17,7 +18,7 @@ export default defineInterface(({ i18n }) => ({
interface: 'tags',
width: 'full',
options: {
placeholder: i18n.t('readable_fields_copy'),
placeholder: i18n.t('interfaces.one-to-many.readable_fields_copy'),
},
},
},

View File

@@ -3,10 +3,12 @@ import InterfaceRadioButtons from './radio-buttons.vue';
export default defineInterface(({ i18n }) => ({
id: 'radio-buttons',
name: i18n.t('radio_buttons'),
name: i18n.t('interfaces.radio-buttons.radio-buttons'),
description: i18n.t('interfaces.radio-buttons.description'),
icon: 'radio_button_checked',
component: InterfaceRadioButtons,
types: ['string'],
recommendedDisplays: ['badge'],
options: [
{
field: 'choices',
@@ -23,6 +25,7 @@ export default defineInterface(({ i18n }) => ({
type: 'string',
name: i18n.t('text'),
meta: {
width: 'half',
interface: 'text-input',
},
},
@@ -31,6 +34,7 @@ export default defineInterface(({ i18n }) => ({
type: 'string',
name: i18n.t('value'),
meta: {
width: 'half',
interface: 'text-input',
options: {
font: 'monospace',
@@ -50,8 +54,8 @@ export default defineInterface(({ i18n }) => ({
interface: 'icon',
},
schema: {
default_value: 'radio_button_checked',
},
default_value: 'radio_button_checked'
}
},
{
field: 'iconOff',
@@ -62,8 +66,8 @@ export default defineInterface(({ i18n }) => ({
interface: 'icon',
},
schema: {
default_value: 'radio_button_unchecked',
},
default_value: 'radio_button_unchecked'
}
},
{
field: 'color',
@@ -73,19 +77,16 @@ export default defineInterface(({ i18n }) => ({
width: 'half',
interface: 'color',
},
schema: {
default_value: '#2f80ed',
},
},
{
field: 'allowOther',
name: i18n.t('allow_other'),
name: i18n.t('interfaces.dropdown.allow_other'),
type: 'string',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('enable_custom_values'),
label: i18n.t('interfaces.dropdown.allow_other_label'),
},
},
schema: {

View File

@@ -4,7 +4,8 @@ import RepeaterOptions from './options.vue';
export default defineInterface(({ i18n }) => ({
id: 'repeater',
name: i18n.t('repeater'),
name: i18n.t('interfaces.repeater.repeater'),
description: i18n.t('interfaces.repeater.description'),
icon: 'replay',
component: InterfaceRepeater,
types: ['json'],

View File

@@ -1,10 +1,25 @@
<template>
<div>
<p class="type-label">Template</p>
<v-input class="input" v-model="template" :placeholder="`{{ field }}`" />
<div class="grid">
<div class="grid-element full">
<p class="type-label">{{ $t('template') }}</p>
<v-input class="input" v-model="template" :placeholder="`{{ field }}`" />
</div>
<p class="type-label">Fields</p>
<repeater v-model="repeaterValue" :template="`{{ field }} — {{ interface }}`" :fields="repeaterFields" />
<div class="grid-element full">
<p class="type-label">{{ $t('interfaces.repeater.edit_fields') }}</p>
<repeater
v-model="repeaterValue"
:template="`{{ field }} — {{ interface }}`"
:fields="repeaterFields"
/>
</div>
<div class="grid-element full">
<p class="type-label">{{ $t('interfaces.repeater.add_label') }}</p>
<v-input class="input" v-model="addLabel" :placeholder="$t('add_a_new_item')" />
</div>
</div>
</div>
</template>
@@ -127,17 +142,37 @@ export default defineComponent({
},
});
return { repeaterValue, repeaterFields, template };
const addLabel = computed({
get() {
return props.value?.addLabel;
},
set(newAddLabel: string) {
emit('input', {
...(props.value || {}),
addLabel: newAddLabel,
});
},
});
return { repeaterValue, repeaterFields, template, addLabel };
},
});
</script>
<style lang="scss" scoped>
.grid {
display: grid;
grid-template-columns: [start] minmax(0, 1fr) [half] minmax(0, 1fr) [full];
gap: var(--form-vertical-gap) var(--form-horizontal-gap);
&-element {
&.full {
grid-column: start/full;
}
}
}
.type-label {
margin-bottom: 4px;
}
.input {
margin-bottom: 24px;
}
</style>

View File

@@ -14,7 +14,7 @@
</draggable>
<button @click="addNew" class="add-new" v-if="showAddNew">
<v-icon name="add" />
{{ createItemText }}
{{ addLabel }}
</button>
</v-item-group>
</template>
@@ -42,7 +42,7 @@ export default defineComponent({
type: String,
required: true,
},
createItemText: {
addLabel: {
type: String,
default: i18n.t('add_a_new_item'),
},
@@ -60,7 +60,7 @@ export default defineComponent({
if (props.disabled) return false;
if (props.value === null) return true;
if (props.limit === null) return true;
if (Array.isArray(props.value) && props.value.length <= props.limit) return true;
if (Array.isArray(props.value) && props.value.length < props.limit) return true;
return false;
});

View File

@@ -3,37 +3,38 @@ import { defineInterface } from '@/interfaces/define';
export default defineInterface(({ i18n }) => ({
id: 'slider',
name: i18n.t('slider'),
name: i18n.t('interfaces.slider.slider'),
description: i18n.t('interfaces.slider.description'),
icon: 'linear_scale',
component: InterfaceSlider,
types: ['integer', 'decimal', 'float', 'bigInteger'],
options: [
{
field: 'minValue',
name: i18n.t('minimum_value'),
name: i18n.t('interfaces.numeric.minimum_value'),
type: 'integer',
meta: {
width: 'half',
interface: 'numeric',
}
},
},
{
field: 'maxValue',
name: i18n.t('maximum_value'),
name: i18n.t('interfaces.numeric.maximum_value'),
type: 'integer',
meta: {
width: 'half',
interface: 'numeric',
}
},
},
{
field: 'stepInterval',
name: i18n.t('step_interval'),
name: i18n.t('interfaces.numeric.step_interval'),
type: 'integer',
meta: {
width: 'half',
interface: 'numeric',
}
},
},
],
}));

View File

@@ -3,9 +3,40 @@ import InterfaceSlug from './slug.vue';
export default defineInterface(({ i18n }) => ({
id: 'slug',
name: i18n.t('slug'),
name: i18n.t('interfaces.slug.slug'),
description: i18n.t('interfaces.slug.description'),
icon: 'link',
component: InterfaceSlug,
types: ['string'],
options: [],
options: [
{
field: 'placeholder',
name: i18n.t('placeholder'),
meta: {
width: 'full',
interface: 'text-input',
options: {
placeholder: i18n.t('enter_a_placeholder'),
},
},
},
{
field: 'iconLeft',
name: i18n.t('icon_left'),
type: 'string',
meta: {
width: 'half',
interface: 'icon',
},
},
{
field: 'iconRight',
name: i18n.t('icon_right'),
type: 'string',
meta: {
width: 'half',
interface: 'icon',
},
},
],
}));

View File

@@ -1,5 +1,16 @@
<template>
<v-input :value="value" :disabled="disabled" @input="$emit('input', $event)" slug />
<v-input
:value="value"
:disabled="disabled"
:placeholder="placeholder"
:iconLeft="iconLeft"
:iconRight="iconRight"
@input="$emit('input', $event)"
slug
>
<template v-if="iconLeft" #prepend><v-icon :name="iconLeft" /></template>
<template v-if="iconRight" #append><v-icon :name="iconRight" /></template>
</v-input>
</template>
<script lang="ts">
@@ -15,6 +26,18 @@ export default defineComponent({
type: Boolean,
default: null,
},
iconLeft: {
type: String,
default: null,
},
iconRight: {
type: String,
default: null,
},
placeholder: {
type: String,
default: null,
},
},
});
</script>

View File

@@ -3,7 +3,8 @@ import { defineInterface } from '@/interfaces/define';
export default defineInterface(({ i18n }) => ({
id: 'tags',
name: i18n.t('tags'),
name: i18n.t('interfaces.tags.tags'),
description: i18n.t('interfaces.tags.description'),
icon: 'local_offer',
component: InterfaceTags,
types: ['json'],
@@ -19,27 +20,33 @@ export default defineInterface(({ i18n }) => ({
},
{
field: 'alphabetize',
name: i18n.t('alphabetize'),
name: i18n.t('interfaces.tags.alphabetize'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('force_alphabetical_order'),
label: i18n.t('interfaces.tags.alphabetize_label'),
},
},
schema: {
default_value: false,
},
},
{
field: 'lowercase',
name: i18n.t('lowercase'),
name: i18n.t('interfaces.tags.lowercase'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('force_lowercase'),
label: i18n.t('interfaces.tags.lowercase_label'),
},
},
schema: {
default_value: false,
},
},
{
field: 'placeholder',
@@ -48,19 +55,25 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'half',
interface: 'text-input',
options: {
placeholder: i18n.t('enter_a_placeholder'),
},
},
},
{
field: 'allowCustom',
name: i18n.t('allow_other'),
name: i18n.t('interfaces.dropdown.allow_other'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('enable_custom_values'),
label: i18n.t('interfaces.dropdown.allow_other_label'),
},
},
schema: {
default_value: false,
},
},
{
field: 'iconLeft',

View File

@@ -1,7 +1,7 @@
<template>
<div class="interface-tags">
<v-input
:placeholder="placeholder || $t('add_tags')"
:placeholder="placeholder || $t('interfaces.tags.add_tags')"
@keydown="onInput"
:disabled="disabled"
v-if="allowCustom"

View File

@@ -3,7 +3,8 @@ import { defineInterface } from '@/interfaces/define';
export default defineInterface(({ i18n }) => ({
id: 'text-input',
name: i18n.t('text_input'),
name: i18n.t('interfaces.text-input.text-input'),
description: i18n.t('interfaces.text-input.description'),
icon: 'text_fields',
component: InterfaceTextInput,
types: ['string', 'uuid'],
@@ -15,17 +16,17 @@ export default defineInterface(({ i18n }) => ({
width: 'half',
interface: 'text-input',
options: {
placeholder: i18n.t('text_shown_when_no_value'),
placeholder: i18n.t('enter_a_placeholder'),
},
},
},
{
field: 'font',
name: i18n.t('font'),
type: 'string',
meta: {
width: 'half',
interface: 'dropdown',
default: 'sans-serif',
options: {
choices: [
{ text: i18n.t('sans_serif'), value: 'sans-serif' },
@@ -34,10 +35,14 @@ export default defineInterface(({ i18n }) => ({
],
},
},
schema: {
default_value: 'sans-serif',
},
},
{
field: 'iconLeft',
name: i18n.t('icon_left'),
type: 'string',
meta: {
width: 'half',
interface: 'icon',
@@ -46,10 +51,41 @@ export default defineInterface(({ i18n }) => ({
{
field: 'iconRight',
name: i18n.t('icon_right'),
type: 'string',
meta: {
width: 'half',
interface: 'icon',
},
},
{
field: 'trim',
name: i18n.t('interfaces.text-input.trim'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('interfaces.text-input.trim_label'),
},
},
schema: {
default_value: false,
},
},
{
field: 'masked',
name: i18n.t('interfaces.text-input.mask'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('interfaces.text-input.mask_label'),
},
},
schema: {
default_value: false,
},
},
],
}));

View File

@@ -3,9 +3,9 @@
:value="value"
:placeholder="placeholder"
:disabled="disabled"
:trim="trim"
:type="masked ? 'password' : 'text'"
:class="font"
:maxlength="length"
@input="$listeners.input"
>
<template v-if="iconLeft" #prepend><v-icon :name="iconLeft" /></template>
@@ -67,7 +67,7 @@ export default defineComponent({
default: 'sans-serif',
},
length: {
type: [Number, String],
type: Number,
default: null,
},
},

View File

@@ -3,7 +3,8 @@ import { defineInterface } from '@/interfaces/define';
export default defineInterface(({ i18n }) => ({
id: 'textarea',
name: i18n.t('textarea'),
name: i18n.t('interfaces.textarea.textarea'),
description: i18n.t('interfaces.textarea.description'),
icon: 'text_fields',
component: InterfaceTextarea,
types: ['text'],
@@ -11,27 +12,47 @@ export default defineInterface(({ i18n }) => ({
{
field: 'placeholder',
name: i18n.t('placeholder'),
width: 'half',
interface: 'text-input',
type: 'string',
meta: {
width: 'half',
interface: 'text-input',
options: {
placeholder: i18n.t('enter_a_placeholder'),
},
},
},
{
field: 'trim',
name: i18n.t('trim'),
width: 'half',
interface: 'switch',
name: i18n.t('interfaces.text-input.trim'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
options: {
label: i18n.t('interfaces.text-input.trim_label'),
},
},
schema: {
default_value: false,
},
},
{
field: 'font',
name: i18n.t('font'),
width: 'half',
interface: 'dropdown',
default: 'sans-serif',
options: {
choices: [
{ text: i18n.t('sans_serif'), value: 'sans-serif' },
{ text: i18n.t('monospace'), value: 'monospace' },
{ text: i18n.t('serif'), value: 'serif' },
],
type: 'string',
meta: {
width: 'half',
interface: 'dropdown',
options: {
choices: [
{ text: i18n.t('sans_serif'), value: 'sans-serif' },
{ text: i18n.t('monospace'), value: 'monospace' },
{ text: i18n.t('serif'), value: 'serif' },
],
},
},
schema: {
default_value: 'sans-serif',
},
},
],

View File

@@ -40,15 +40,15 @@ export default defineComponent({
<style lang="scss" scoped>
.v-textarea {
&.monospace {
--v-input-font-family: var(--family-monospace);
--v-textarea-font-family: var(--family-monospace);
}
&.serif {
--v-input-font-family: var(--family-serif);
--v-textarea-font-family: var(--family-serif);
}
&.sans-serif {
--v-input-font-family: var(--family-sans-serif);
--v-textarea-font-family: var(--family-sans-serif);
}
}
</style>

View File

@@ -3,7 +3,8 @@ import { defineInterface } from '@/interfaces/define';
export default defineInterface(({ i18n }) => ({
id: 'toggle',
name: i18n.t('toggle'),
name: i18n.t('interfaces.toggle.toggle'),
description: i18n.t('interfaces.toggle.description'),
icon: 'check_box',
component: InterfaceToggle,
types: ['boolean'],
@@ -15,8 +16,7 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'half',
interface: 'icon',
default_value: 'check_box_outline_blank',
}
},
},
{
field: 'iconOn',
@@ -25,8 +25,7 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'half',
interface: 'icon',
default_value: 'check_box',
}
},
},
{
field: 'label',
@@ -35,8 +34,13 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'half',
interface: 'text-input',
options: {
placeholder: i18n.t('interfaces.toggle.label_placeholder'),
},
},
schema: {
default_value: i18n.t('active'),
}
},
},
{
field: 'color',
@@ -45,8 +49,7 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'half',
interface: 'color',
default_value: 'var(--primary)',
}
},
},
],
}));

View File

@@ -3,9 +3,31 @@ import { defineInterface } from '@/interfaces/define';
export default defineInterface(({ i18n }) => ({
id: 'user',
name: i18n.t('user'),
name: i18n.t('interfaces.user.user'),
description: i18n.t('interfaces.user.description'),
icon: 'person',
component: InterfaceUser,
types: ['uuid'],
options: [],
options: [
{
field: 'selectMode',
name: i18n.t('interfaces.user.select_mode'),
type: 'string',
meta: {
width: 'full',
interface: 'dropdown',
options: {
choices: [
{ text: i18n.t('interfaces.user.modes.auto'), value: 'auto' },
{ text: i18n.t('interfaces.user.modes.dropdown'), value: 'dropdown' },
{ text: i18n.t('interfaces.user.modes.modal'), value: 'modal' },
],
},
},
schema: {
default_value: 'auto',
},
},
],
recommendedDisplays: ['user'],
}));

View File

@@ -102,7 +102,7 @@ export default defineComponent({
components: { ModalDetail, ModalBrowse },
props: {
value: {
type: [Number, Object],
type: String,
default: null,
},
template: {
@@ -162,7 +162,7 @@ export default defineComponent({
(newValue) => {
// When the newly configured value is a primitive, assume it's the primary key
// of the item and fetch it from the API to render the preview
if (newValue !== null && newValue !== currentUser.value?.id && typeof newValue === 'number') {
if (newValue !== null && newValue !== currentUser.value?.id && typeof newValue === 'string') {
fetchCurrent();
}
@@ -178,16 +178,8 @@ export default defineComponent({
const currentPrimaryKey = computed<string | number>(() => {
if (!currentUser.value) return '+';
if (!props.value) return '+';
if (typeof props.value === 'number' || typeof props.value === 'string') {
return props.value;
}
if (typeof props.value === 'object' && props.value.hasOwnProperty('id')) {
return props.value.id;
}
return '+';
return props.value;
});
return { setCurrent, currentUser, loading, currentPrimaryKey };
@@ -312,15 +304,7 @@ export default defineComponent({
const selection = computed<(number | string)[]>(() => {
if (!props.value) return [];
if (typeof props.value === 'object' && props.value.hasOwnProperty('id')) {
return [props.value.id];
}
if (typeof props.value === 'string' || typeof props.value === 'number') {
return [props.value];
}
return [];
return [props.value];
});
return { selection, stageSelection, selectModalActive };

View File

@@ -3,14 +3,15 @@ import { defineInterface } from '@/interfaces/define';
export default defineInterface(({ i18n }) => ({
id: 'wysiwyg',
name: i18n.t('wysiwyg'),
name: i18n.t('interfaces.wysiwyg.wysiwyg'),
description: i18n.t('interfaces.wysiwyg.description'),
icon: 'format_quote',
component: InterfaceWYSIWYG,
types: ['text'],
options: [
{
field: 'toolbar',
name: i18n.t('toolbar'),
name: i18n.t('interfaces.wysiwyg.toolbar'),
type: 'json',
schema: {
default_value: [
@@ -224,7 +225,6 @@ export default defineInterface(({ i18n }) => ({
meta: {
width: 'half',
interface: 'dropdown',
default: 'sans-serif',
options: {
choices: [
{ text: i18n.t('sans_serif'), value: 'sans-serif' },
@@ -233,10 +233,13 @@ export default defineInterface(({ i18n }) => ({
],
},
},
schema: {
default_value: 'sans-serif',
},
},
{
field: 'customFormats',
name: i18n.t('custom_formats'),
name: i18n.t('interfaces.wysiwyg.custom_formats'),
type: 'json',
meta: {
interface: 'code',
@@ -258,7 +261,7 @@ export default defineInterface(({ i18n }) => ({
},
{
field: 'tinymceOverrides',
name: i18n.t('options_override'),
name: i18n.t('interfaces.wysiwyg.options_override'),
type: 'json',
meta: {
interface: 'code',

View File

@@ -1,16 +1,83 @@
{
"displays": {
"icon": {
"icon": "Icon"
"collection": {
"collection": "Collection",
"description": "Display a collection.",
"icon_label": "Show the collection's icon"
},
"format-title": {
"format-title": "Format Title"
"color-dot": {
"color-dot": "Color Dot",
"description": "Display a colored dot.",
"default_color": "Default Color",
"choices_note": "Set colors relative to the text."
},
"datetime": {
"datetime": "Datetime",
"description": "Display values related to time.",
"relative": "Relative",
"relative_label": "Show relative time, eg: 5 minutes ago"
},
"file": {
"file": "File",
"description": "Display files."
},
"filesize": {
"filesize": "File Size",
"description": "Display the size of a file."
},
"formatted-value": {
"formatted-value": "Formatted Value",
"description": "Display a formatted version of the text.",
"format_title": "Format Title",
"format_title_label": "Auto-format casing",
"bold_label": "Use bold style"
},
"icon": {
"icon": "Icon",
"description": "Display an icon.",
"filled": "Filled",
"filled_label": "Use the filled variant"
},
"image": {
"image": "Image",
"description": "Display a tiny image preview.",
"circle": "Circle",
"circle_label": "Display as a circle"
},
"labels": {
"labels": "Labels",
"description": "Display either a single or a list of labels.",
"default_foreground": "Default Foreground",
"default_background": "Default Background",
"format_label": "Format each label"
},
"mime-type": {
"mime-type": "MIME Type",
"description": "Show the MIME-Type of a file.",
"extension_only": "Extension Only",
"extension_only_label": "Only show the file extension"
},
"rating": {
"rating": "Rating"
"rating": "Rating",
"description": "Visualize a number as stars relative to the max-value.",
"simple": "Simple",
"simple_label": "Show stars in a simple format."
},
"raw": {
"raw": "Raw Value"
},
"related-values": {
"related-values": "Related Values",
"description": "Display relative values."
},
"user": {
"user": "User",
"description": "Display a directus user.",
"avatar": "Avatar",
"name": "Name",
"both": "Both",
"circle_label": "Show user in a circle"
}
}
}

View File

@@ -223,6 +223,7 @@
"translations": "Translations",
"note": "Note",
"enter_a_value": "Enter a value...",
"enter_a_placeholder": "Enter a placeholder...",
"length": "Length",
"required": "Required",
"readonly": "Readonly",
@@ -344,6 +345,8 @@
"notice": "Notice",
"slug": "Slug",
"repeater": "Repeater",
"max-amount": "Max Amount",
"select_mode": "Select Mode",
"months": {
"january": "January",
@@ -404,6 +407,8 @@
"primary_key_field": "Primary Key Field",
"type": "Type",
"number": "Number",
"true": "True",
"false": "False",
"creating_new_collection": "Creating New Collection",
"status": "Status",
"sort": "Sort",
@@ -520,6 +525,7 @@
"formatted_value": "Formatted Value",
"format_title": "Format Title",
"inline_title": "Inline Title",
"auto_format_casing": "Auto-format casing",
"errors": {
@@ -623,6 +629,7 @@
"step_interval": "Step Interval",
"icon_left": "Icon Left",
"icon_right": "Icon Right",
"trimed": "Trimed",
"font_family": "Font Family",
"font": "Font",
"numeric": "Numeric",
@@ -734,6 +741,7 @@
"role_only": "Role Only",
"update": "Update",
"edit_fields": "Edit Fields",
"select_fields": "Select Fields",
"readable_fields": "Readable Fields",
"writable_fields": "Writable Fields",
@@ -745,9 +753,6 @@
"tags": "Tags",
"format_text": "Format Text",
"outline": "Outline",
"use_outline_variant": "Use the outline variant",
"bold": "Bold",
"subdued": "Subdued",

View File

@@ -1,5 +1,189 @@
{
"interfaces": {
"checkboxes": {
"checkboxes": "Checkboxes",
"description": "Choose between multiple options via checkboxes.",
"allow_other": "Allow Other",
"enable_custom_values": "Enable custom values"
},
"code": {
"code": "Code",
"description": "Write or share code snippets.",
"line_number": "Line Number",
"placeholder": "Enter code here..."
},
"collections": {
"collections": "Collections",
"description": "Select between existing collections.",
"include_system_collections": "Include System Collections"
},
"color": {
"color": "Color",
"description": "Enter color values.",
"placeholder": "Choose a color...",
"preset_colors": "Preset Colors",
"preset_colors_placeholder": "Add a new color..."
},
"datetime": {
"datetime": "Datetime",
"description": "Enter dates and times.",
"include_seconds": "Include Seconds",
"set_to_now": "Set to Now"
},
"display-template": {
"display-template": "Display Template",
"description": "Mix text with field values.",
"collection_field": "Collection field",
"collection_field_not_setup": "The collection field option is misconfigured",
"select_a_collection": "Select a Collection"
},
"divider": {
"divider": "Divider",
"description": "Divide fields into seperate sections.",
"title_placeholder": "Enter a title...",
"inline_title": "Inline Title",
"inline_title_label": "Show title inside line."
},
"dropdown": {
"dropdown": "Dropdown",
"description": "Select a value from a dropdown.",
"choices_placeholder": "Add a new choice",
"allow_other": "Allow Other",
"allow_other_label": "Allow other values",
"allow_none": "Allow None",
"allow_none_label": "Allow no selection"
},
"dropdown-multiselect": {
"dropdown-multiselect": "Dropdown (Multiple)",
"description": "Select multiple values of a dropdown."
},
"file": {
"file": "File",
"description": "Select or upload a file."
},
"files": {
"files": "Files",
"description": "Select or upload multiple files."
},
"hash": {
"hash": "Hash",
"description": "Enter a hash.",
"masked": "Masked",
"masked_label": "Hide the true values"
},
"icon": {
"icon": "Icon",
"description": "Select an icon from a dropdown.",
"search_for_icon": "Search for icon..."
},
"image": {
"image": "Image",
"description": "Select or upload an image."
},
"interface": {
"interface": "Interface",
"description": "Select an existing interface."
},
"interface-options": {
"interface-options": "Interface Options",
"description": "A modal for selecting options of an interface."
},
"many-to-many": {
"many-to-many": "Many to Many",
"description": "Select related items."
},
"many-to-one": {
"many-to-one": "Many to One",
"description": "Select a single related item.",
"display_template": "Display Template"
},
"markdown": {
"markdown": "Markdown",
"description": "Enter and preview markdown.",
"tabbed": "Tabbed",
"tabbed_label": "Show preview in separate tab",
"edit": "Edit",
"preview": "Preview"
},
"notice": {
"notice": "Notice",
"description": "Display a short notice.",
"text": "Enter notice content here..."
},
"numeric": {
"numeric": "Numeric",
"description": "Enter a number.",
"minimum_value": "Minimum Value",
"maximum_value": "Maximum Value",
"step_interval": "Step Interval"
},
"one-to-many": {
"one-to-many": "One to Many",
"description": "Select related items.",
"readable_fields_copy": "Select the fields that the user can view"
},
"radio-buttons": {
"radio-buttons": "Radio Buttons",
"description": "Select one of multiple choices."
},
"repeater": {
"repeater": "Repeater",
"description": "Have multiple entires of the same structure.",
"max_amount": "Maximum Amount",
"max_amount_placeholder": "Maximum amount of items...",
"edit_fields": "Edit Fields",
"add_label": "\"Add New Row\" Label"
},
"slider": {
"slider": "Slider",
"description": "Select a number using a slider."
},
"slug": {
"slug": "Slug",
"description": "Enter a word connected with hyphens."
},
"tags": {
"tags": "Tags",
"description": "Select or add tags.",
"lowercase": "Lowercase",
"lowercase_label": "Force Lowercase",
"alphabetize": "Alphabetize",
"alphabetize_label": "Force Alphabetical Order",
"add_tags": "Add tags..."
},
"text-input": {
"text-input": "Text Input",
"description": "Enter a single line text.",
"trim": "Trimed",
"trim_label": "Trim the start and end",
"mask": "Masked",
"mask_label": "Hide the real value"
},
"textarea": {
"textarea": "Textarea",
"description": "Enter multiline text."
},
"toggle": {
"toggle": "Toggle",
"description": "Switch between an on and off state.",
"label_placeholder": "Enter a label..."
},
"user": {
"user": "User",
"description": "Select an existing directus user.",
"select_mode": "Select Mode",
"modes": {
"auto": "Auto",
"dropdown": "Dropdown",
"modal": "Modal"
}
},
"wysiwyg": {
"wysiwyg": "WYSIWYG",
"description": "A text editor for writing complex text.",
"toolbar": "Toolbar",
"custom_formats": "Custom Formats",
"options_override": "Options Override"
}
}
}

View File

@@ -52,9 +52,10 @@ export default defineComponent({
});
const selectItems = computed(() => {
const recommended = clone(selectedInterface.value?.recommendedDisplays) || [];
let recommended = clone(selectedInterface.value?.recommendedDisplays) || [];
recommended.push('raw', 'formatted-value');
recommended = [...new Set(recommended)];
const displayItems: FancySelectItem[] = availableDisplays.value.map((display) => {
const item: FancySelectItem = {
@@ -71,15 +72,23 @@ export default defineComponent({
return item;
});
if (displayItems.length >= 5 && recommended.length > 0) {
return [
...recommended.map((key) => displayItems.find((item) => item.value === key)),
{ divider: true },
...displayItems.filter((item) => recommended.includes(item.value as string) === false),
].filter((i) => i);
} else {
return displayItems;
const recommendedItems: (FancySelectItem | { divider: boolean } | undefined)[] = [];
const recommendedList = recommended.map((key) => displayItems.find((item) => item.value === key));
if (recommendedList !== undefined) {
recommendedItems.push(...recommendedList.filter((i) => i));
}
if (displayItems.length >= 5 && recommended.length > 0) {
recommendedItems.push({ divider: true });
}
const displayList = displayItems.filter((item) => recommended.includes(item.value as string) === false);
if (displayList !== undefined) {
recommendedItems.push(...displayList.filter((i) => i));
}
return recommendedItems;
});
const selectedDisplay = computed(() => {

View File

@@ -64,7 +64,7 @@ export default defineComponent({
datetime: ['datetime'],
date: ['datetime'],
time: ['datetime'],
json: ['code'],
json: ['checkboxes', 'tags'],
uuid: ['text-input'],
};
@@ -85,15 +85,23 @@ export default defineComponent({
return item;
});
if (interfaceItems.length >= 5 && recommended.length > 0) {
return [
...recommended.map((key) => interfaceItems.find((item) => item.value === key)),
{ divider: true },
...interfaceItems.filter((item) => recommended.includes(item.value as string) === false),
].filter((i) => i);
} else {
return interfaceItems;
const recommendedItems: (FancySelectItem | { divider: boolean } | undefined)[] = [];
const recommendedList = recommended.map((key) => interfaceItems.find((item) => item.value === key));
if (recommendedList !== undefined) {
recommendedItems.push(...recommendedList.filter((i) => i));
}
if (interfaceItems.length >= 5 && recommended.length > 0) {
recommendedItems.push({ divider: true });
}
const interfaceList = interfaceItems.filter((item) => recommended.includes(item.value as string) === false);
if (interfaceList !== undefined) {
recommendedItems.push(...interfaceList.filter((i) => i));
}
return recommendedItems;
});
const selectedInterface = computed(() => {

View File

@@ -30,7 +30,7 @@
:value="fieldData.type"
:items="typesWithLabels"
:placeholder="typePlaceholder"
@input="setType"
@input="fieldData.type = $event"
/>
</div>
@@ -40,11 +40,45 @@
</div>
<!-- @todo base default value field type on selected type -->
<div class="field" v-if="fieldData.schema">
<div class="field" v-if="fieldData.schema" :class="{ full: ['text', 'json'].includes(fieldData.type) }">
<div class="label type-label">{{ $t('default_value') }}</div>
<v-input
v-if="['string', 'uuid'].includes(fieldData.type)"
class="monospace"
v-model="fieldData.schema.default_value"
v-model="defaultValue"
:placeholder="$t('add_a_default_value')"
/>
<v-textarea
v-else-if="['text', 'json'].includes(fieldData.type)"
class="monospace"
v-model="defaultValue"
:placeholder="$t('add_a_default_value')"
/>
<v-input
v-else-if="['integer', 'bigInteger', 'float', 'decimal'].includes(fieldData.type)"
type="number"
class="monospace"
v-model="defaultValue"
:placeholder="$t('add_a_default_value')"
/>
<v-input
v-else-if="['timestamp', 'datetime', 'date', 'time'].includes(fieldData.type)"
class="monospace"
v-model="defaultValue"
:placeholder="$t('add_a_default_value')"
/>
<v-checkbox
v-else-if="fieldData.type === 'boolean'"
class="monospace"
v-model="defaultValue"
:label="defaultValue ? $t('true') : $t('false')"
block
/>
<v-input
v-else
class="monospace"
v-model="defaultValue"
disabled
:placeholder="$t('add_a_default_value')"
/>
</div>
@@ -201,23 +235,22 @@ export default defineComponent({
return i18n.t('choose_a_type');
});
return { fieldData: state.fieldData, typesWithLabels, setType, typeDisabled, typePlaceholder };
const defaultValue = computed({
get() {
return state.fieldData.schema.default_value;
},
set(newVal: any) {
state.fieldData.schema.default_value = newVal;
},
});
function setType(value: typeof types[number]) {
if (value === 'uuid') {
state.fieldData.meta.special = 'uuid';
} else {
state.fieldData.meta.special = null;
}
// We'll reset the interface/display as they most likely won't work for the newly selected
// type
state.fieldData.meta.interface = null;
state.fieldData.meta.options = null;
state.fieldData.meta.display = null;
state.fieldData.meta.display_options = null;
state.fieldData.type = value;
}
return {
fieldData: state.fieldData,
typesWithLabels,
typeDisabled,
typePlaceholder,
defaultValue,
};
},
});
</script>

View File

@@ -291,6 +291,34 @@ function initLocalStore(
delete state.fieldData.type;
state.fieldData.meta.special = 'alias';
}
if (type === 'standard') {
watch(
() => state.fieldData.type,
() => {
state.fieldData.meta.interface = null;
state.fieldData.meta.options = null;
state.fieldData.meta.display = null;
state.fieldData.meta.display_options = null;
state.fieldData.meta.special = null;
state.fieldData.schema.default_value = undefined;
switch (state.fieldData.type) {
case 'uuid':
state.fieldData.meta.special = 'uuid';
break;
case 'json':
state.fieldData.meta.special = 'json';
break;
case 'boolean':
state.fieldData.meta.special = 'boolean';
state.fieldData.schema.is_nullable = false;
state.fieldData.schema.default_value = false;
break;
}
}
);
}
}
function clearLocalStore() {

View File

@@ -18,6 +18,10 @@
transition: all var(--fast) var(--transition);
}
.CodeMirror .CodeMirror-placeholder{
color: var(--foreground-subdued);
}
.CodeMirror:hover {
border-color: var(--border-normal-alt);
}