diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index b6dafa2ea..7c8573528 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,3 +1,7 @@ View Filters config structure changed View Groups config structure changed -`theme` props removed (use new `useTheme` hook instead) \ No newline at end of file +`theme` props removed (use new `useTheme` hook instead) + + +# Typescript +`StringOrTextField` has been split into `StringField` and `TextField`. \ No newline at end of file diff --git a/packages/core/dev-test/config.yml b/packages/core/dev-test/config.yml index 60a5896ff..e725ba0fd 100644 --- a/packages/core/dev-test/config.yml +++ b/packages/core/dev-test/config.yml @@ -156,6 +156,19 @@ collections: widget: boolean pattern: ['true', 'Must be true'] required: false + - name: prefix + label: With Prefix + widget: boolean + prefix: "I'm a prefix" + - name: suffix + label: With Suffix + widget: boolean + suffix: "I'm a suffix" + - name: prefix_and_suffix + label: With Prefix and Suffix + widget: boolean + prefix: "I'm a prefix" + suffix: "I'm a suffix" - name: code label: Code file: _widgets/code.json @@ -784,6 +797,19 @@ collections: widget: number pattern: ['[0-9]{3,}', 'Must be at least 3 digits'] required: false + - name: prefix + label: With Prefix + widget: number + prefix: "$" + - name: suffix + label: With Suffix + widget: number + suffix: "%" + - name: prefix_and_suffix + label: With Prefix and Suffix + widget: number + prefix: "$" + suffix: "%" - name: object label: Object file: _widgets/object.json @@ -1061,6 +1087,19 @@ collections: widget: string pattern: ['.{12,}', 'Must have at least 12 characters'] required: false + - name: prefix + label: With Prefix + widget: string + prefix: "$" + - name: suffix + label: With Suffix + widget: string + suffix: "%" + - name: prefix_and_suffix + label: With Prefix and Suffix + widget: string + prefix: "$" + suffix: "%" - name: text label: Text file: _widgets/text.json diff --git a/packages/core/dev-test/index.js b/packages/core/dev-test/index.js index 1e7908ddf..9b1d85f5d 100644 --- a/packages/core/dev-test/index.js +++ b/packages/core/dev-test/index.js @@ -11,7 +11,7 @@ const PostPreview = ({ entry, widgetFor }) => { ); }; -const PostPreviewCard = ({ entry, theme, hasLocalBackup, collection }) => { +const PostPreviewCard = ({ entry, hasLocalBackup, collection }) => { const theme = useTheme(); const date = new Date(entry.data.date); @@ -292,7 +292,7 @@ CMS.registerShortcode('youtube', { toArgs: ({ src }) => { return [src]; }, - control: ({ src, onChange, theme }) => { + control: ({ src, onChange }) => { const theme = useTheme(); return h('span', {}, [ diff --git a/packages/core/src/components/entry-editor/editor-control-pane/EditorControlPane.tsx b/packages/core/src/components/entry-editor/editor-control-pane/EditorControlPane.tsx index 06ee58afb..c26a830ac 100644 --- a/packages/core/src/components/entry-editor/editor-control-pane/EditorControlPane.tsx +++ b/packages/core/src/components/entry-editor/editor-control-pane/EditorControlPane.tsx @@ -16,7 +16,7 @@ import type { Field, FieldsErrors, I18nSettings, - StringOrTextField, + StringField, TranslatedProps, } from '@staticcms/core/interface'; @@ -71,7 +71,7 @@ const EditorControlPane = ({ widget: 'string', i18n: 'none', hint: ``, - } as StringOrTextField), + } as StringField), [collection], ); diff --git a/packages/core/src/interface.ts b/packages/core/src/interface.ts index 344209089..7ef7bf1d0 100644 --- a/packages/core/src/interface.ts +++ b/packages/core/src/interface.ts @@ -633,6 +633,9 @@ export interface MediaField extends BaseField { export interface BooleanField extends BaseField { widget: 'boolean'; default?: boolean; + + prefix?: string; + suffix?: string; } export interface CodeField extends BaseField { @@ -777,6 +780,9 @@ export interface NumberField extends BaseField { max?: number; step?: number; + + prefix?: string; + suffix?: string; } export interface SelectField extends BaseField { @@ -809,8 +815,15 @@ export interface HiddenField extends BaseField { default?: ValueOrNestedValue; } -export interface StringOrTextField extends BaseField { - widget: 'string' | 'text'; +export interface StringField extends BaseField { + widget: 'string'; + default?: string; + prefix?: string; + suffix?: string; +} + +export interface TextField extends BaseField { + widget: 'text'; default?: string; } @@ -839,7 +852,8 @@ export type Field = | RelationField | SelectField | HiddenField - | StringOrTextField + | StringField + | TextField | UUIDField | EF; diff --git a/packages/core/src/lib/util/__tests__/field.util.spec.ts b/packages/core/src/lib/util/__tests__/field.util.spec.ts index fc4b19cc0..8f83ce387 100644 --- a/packages/core/src/lib/util/__tests__/field.util.spec.ts +++ b/packages/core/src/lib/util/__tests__/field.util.spec.ts @@ -2,16 +2,16 @@ import { createMockEntry } from '@staticcms/test/data/entry.mock'; import { isHidden } from '../field.util'; import { I18N_FIELD_NONE } from '../../i18n'; -import type { StringOrTextField } from '@staticcms/core/interface'; +import type { StringField, TextField } from '@staticcms/core/interface'; describe('filterEntries', () => { - const mockTitleField: StringOrTextField = { + const mockTitleField: StringField = { label: 'Title', name: 'title', widget: 'string', }; - const mockUrlField: StringOrTextField = { + const mockUrlField: StringField = { label: 'URL', name: 'url', widget: 'string', @@ -22,7 +22,7 @@ describe('filterEntries', () => { }, }; - const mockBodyField: StringOrTextField = { + const mockBodyField: TextField = { label: 'Body', name: 'body', widget: 'text', diff --git a/packages/core/src/reducers/__tests__/entryDraft.spec.ts b/packages/core/src/reducers/__tests__/entryDraft.spec.ts index 5981bee48..1b20cb010 100644 --- a/packages/core/src/reducers/__tests__/entryDraft.spec.ts +++ b/packages/core/src/reducers/__tests__/entryDraft.spec.ts @@ -2,7 +2,7 @@ import { DRAFT_CHANGE_FIELD, DRAFT_CREATE_EMPTY } from '@staticcms/core/constant import entryDraftReducer from '../entryDraft'; import { createMockEntry } from '@staticcms/test/data/entry.mock'; -import type { I18nSettings, StringOrTextField } from '@staticcms/core/interface'; +import type { I18nSettings, StringField } from '@staticcms/core/interface'; import type { EntryDraftState } from '../entryDraft'; describe('entryDraft', () => { @@ -145,7 +145,7 @@ describe('entryDraft', () => { }); it('should duplicate values to other locales for singleton list', () => { - const field: StringOrTextField = { + const field: StringField = { widget: 'string', name: 'stringInput', i18n: 'duplicate', diff --git a/packages/core/src/widgets/boolean/BooleanControl.css b/packages/core/src/widgets/boolean/BooleanControl.css new file mode 100644 index 000000000..80a5118ae --- /dev/null +++ b/packages/core/src/widgets/boolean/BooleanControl.css @@ -0,0 +1,23 @@ +.CMS_WidgetBoolean_content { + @apply flex + gap-2 + items-center; +} + +.CMS_WidgetBoolean_prefix { + color: var(--text-secondary); + + @apply text-sm + whitespace-nowrap; + + line-height: 100%; +} + +.CMS_WidgetBoolean_suffix { + color: var(--text-secondary); + + @apply text-sm + whitespace-nowrap; + + line-height: 100%; +} diff --git a/packages/core/src/widgets/boolean/BooleanControl.tsx b/packages/core/src/widgets/boolean/BooleanControl.tsx index a339c870f..b6f507785 100644 --- a/packages/core/src/widgets/boolean/BooleanControl.tsx +++ b/packages/core/src/widgets/boolean/BooleanControl.tsx @@ -3,11 +3,14 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import Field from '@staticcms/core/components/common/field/Field'; import Switch from '@staticcms/core/components/common/switch/Switch'; import classNames from '@staticcms/core/lib/util/classNames.util'; +import { isNotEmpty } from '@staticcms/core/lib/util/string.util'; import { generateClassNames } from '@staticcms/core/lib/util/theming.util'; import type { BooleanField, WidgetControlProps } from '@staticcms/core/interface'; import type { ChangeEvent, FC } from 'react'; +import './BooleanControl.css'; + const classes = generateClassNames('WidgetBoolean', [ 'root', 'error', @@ -15,6 +18,9 @@ const classes = generateClassNames('WidgetBoolean', [ 'disabled', 'for-single-list', 'input', + 'content', + 'prefix', + 'suffix', ]); const BooleanControl: FC> = ({ @@ -43,6 +49,9 @@ const BooleanControl: FC> = ({ [onChange], ); + const prefix = useMemo(() => field.prefix ?? '', [field.prefix]); + const suffix = useMemo(() => field.suffix ?? '', [field.suffix]); + return ( > = ({ forSingleList && classes['for-single-list'], )} > - +
+ {isNotEmpty(prefix) ?
{prefix}
: null} + + {isNotEmpty(suffix) ?
{suffix}
: null} +
); }; diff --git a/packages/core/src/widgets/boolean/schema.ts b/packages/core/src/widgets/boolean/schema.ts index 858da99cb..13c045318 100644 --- a/packages/core/src/widgets/boolean/schema.ts +++ b/packages/core/src/widgets/boolean/schema.ts @@ -1,5 +1,7 @@ export default { properties: { default: { type: 'boolean' }, + prefix: { type: 'string' }, + suffix: { type: 'string' }, }, }; diff --git a/packages/core/src/widgets/number/NumberControl.css b/packages/core/src/widgets/number/NumberControl.css new file mode 100644 index 000000000..8e1cbe263 --- /dev/null +++ b/packages/core/src/widgets/number/NumberControl.css @@ -0,0 +1,35 @@ +.CMS_WidgetNumber_root { + & .CMS_WidgetNumber_input { + &.CMS_WidgetNumber_with-prefix { + @apply ps-0.5; + } + + &.CMS_WidgetNumber_with-suffix { + @apply pe-0.5; + } + } +} + +.CMS_WidgetNumber_prefix { + color: var(--text-secondary); + + @apply ps-3 + text-sm + flex + items-center + whitespace-nowrap; + + line-height: 100%; +} + +.CMS_WidgetNumber_suffix { + color: var(--text-secondary); + + @apply pe-3 + text-sm + flex + items-center + whitespace-nowrap; + + line-height: 100%; +} diff --git a/packages/core/src/widgets/number/NumberControl.tsx b/packages/core/src/widgets/number/NumberControl.tsx index 4bafe1dc9..f2426b46a 100644 --- a/packages/core/src/widgets/number/NumberControl.tsx +++ b/packages/core/src/widgets/number/NumberControl.tsx @@ -3,18 +3,25 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import Field from '@staticcms/core/components/common/field/Field'; import TextField from '@staticcms/core/components/common/text-field/TextField'; import classNames from '@staticcms/core/lib/util/classNames.util'; +import { isNotEmpty } from '@staticcms/core/lib/util/string.util'; import { generateClassNames } from '@staticcms/core/lib/util/theming.util'; import type { NumberField, WidgetControlProps } from '@staticcms/core/interface'; import type { ChangeEvent, FC } from 'react'; -const classes = generateClassNames('WidgetNumberPreview', [ +import './NumberControl.css'; + +const classes = generateClassNames('WidgetNumber', [ 'root', 'error', 'required', 'disabled', 'for-single-list', 'input', + 'with-prefix', + 'with-suffix', + 'prefix', + 'suffix', ]); const NumberControl: FC> = ({ @@ -68,6 +75,9 @@ const NumberControl: FC> = ({ return 1; }, [field.step, field.value_type]); + const prefix = useMemo(() => field.prefix ?? '', [field.prefix]); + const suffix = useMemo(() => field.suffix ?? '', [field.suffix]); + return ( > = ({ step={step} disabled={disabled} onChange={handleChange} - inputClassName={classes.input} + inputClassName={classNames( + classes.input, + isNotEmpty(prefix) && classes['with-prefix'], + isNotEmpty(suffix) && classes['with-suffix'], + )} + startAdornment={isNotEmpty(prefix) ?
{prefix}
: null} + endAdornment={isNotEmpty(suffix) ?
{suffix}
: null} />
); diff --git a/packages/core/src/widgets/number/schema.ts b/packages/core/src/widgets/number/schema.ts index 9b3291ce4..8916bcad4 100644 --- a/packages/core/src/widgets/number/schema.ts +++ b/packages/core/src/widgets/number/schema.ts @@ -5,5 +5,7 @@ export default { min: { type: 'number' }, max: { type: 'number' }, default: { type: 'number' }, + prefix: { type: 'string' }, + suffix: { type: 'string' }, }, }; diff --git a/packages/core/src/widgets/relation/__tests__/RelationControl.spec.ts b/packages/core/src/widgets/relation/__tests__/RelationControl.spec.ts index 871ec4937..b7da975d0 100644 --- a/packages/core/src/widgets/relation/__tests__/RelationControl.spec.ts +++ b/packages/core/src/widgets/relation/__tests__/RelationControl.spec.ts @@ -24,7 +24,7 @@ import type { ListField, ObjectField, RelationField, - StringOrTextField, + TextField, } from '@staticcms/core/interface'; jest.mock('@staticcms/core/backend'); @@ -69,7 +69,7 @@ const coAuthorsField: ListField = { ], }; -const bodyField: StringOrTextField = { +const bodyField: TextField = { widget: 'text', name: 'body', }; diff --git a/packages/core/src/widgets/string/StringControl.css b/packages/core/src/widgets/string/StringControl.css new file mode 100644 index 000000000..ec1ddaa33 --- /dev/null +++ b/packages/core/src/widgets/string/StringControl.css @@ -0,0 +1,35 @@ +.CMS_WidgetString_root { + & .CMS_WidgetString_input { + &.CMS_WidgetString_with-prefix { + @apply ps-0.5; + } + + &.CMS_WidgetString_with-suffix { + @apply pe-0.5; + } + } +} + +.CMS_WidgetString_prefix { + color: var(--text-secondary); + + @apply ps-3 + text-sm + flex + items-center + whitespace-nowrap; + + line-height: 100%; +} + +.CMS_WidgetString_suffix { + color: var(--text-secondary); + + @apply pe-3 + text-sm + flex + items-center + whitespace-nowrap; + + line-height: 100%; +} diff --git a/packages/core/src/widgets/string/StringControl.tsx b/packages/core/src/widgets/string/StringControl.tsx index 186f70672..ea3c42633 100644 --- a/packages/core/src/widgets/string/StringControl.tsx +++ b/packages/core/src/widgets/string/StringControl.tsx @@ -4,11 +4,14 @@ import Field from '@staticcms/core/components/common/field/Field'; import TextField from '@staticcms/core/components/common/text-field/TextField'; import useDebounce from '@staticcms/core/lib/hooks/useDebounce'; import classNames from '@staticcms/core/lib/util/classNames.util'; +import { isNotEmpty } from '@staticcms/core/lib/util/string.util'; import { generateClassNames } from '@staticcms/core/lib/util/theming.util'; -import type { StringOrTextField, WidgetControlProps } from '@staticcms/core/interface'; +import type { StringField, WidgetControlProps } from '@staticcms/core/interface'; import type { ChangeEvent, FC } from 'react'; +import './StringControl.css'; + const classes = generateClassNames('WidgetString', [ 'root', 'error', @@ -16,9 +19,13 @@ const classes = generateClassNames('WidgetString', [ 'disabled', 'for-single-list', 'input', + 'with-prefix', + 'with-suffix', + 'prefix', + 'suffix', ]); -const StringControl: FC> = ({ +const StringControl: FC> = ({ value, label, errors, @@ -52,6 +59,9 @@ const StringControl: FC> = ({ onChange(debouncedInternalValue); }, [debouncedInternalValue, onChange, rawValue]); + const prefix = useMemo(() => field.prefix ?? '', [field.prefix]); + const suffix = useMemo(() => field.suffix ?? '', [field.suffix]); + return ( > = ({ value={internalValue} disabled={disabled} onChange={handleChange} - inputClassName={classes.input} + inputClassName={classNames( + classes.input, + isNotEmpty(prefix) && classes['with-prefix'], + isNotEmpty(suffix) && classes['with-suffix'], + )} + startAdornment={isNotEmpty(prefix) ?
{prefix}
: null} + endAdornment={isNotEmpty(suffix) ?
{suffix}
: null} />
); diff --git a/packages/core/src/widgets/string/StringPreview.tsx b/packages/core/src/widgets/string/StringPreview.tsx index 5d89e8b2c..ba65766dc 100644 --- a/packages/core/src/widgets/string/StringPreview.tsx +++ b/packages/core/src/widgets/string/StringPreview.tsx @@ -2,12 +2,12 @@ import React from 'react'; import { generateClassNames } from '@staticcms/core/lib/util/theming.util'; -import type { StringOrTextField, WidgetPreviewProps } from '@staticcms/core/interface'; +import type { StringField, WidgetPreviewProps } from '@staticcms/core/interface'; import type { FC } from 'react'; const classes = generateClassNames('WidgetStringPreview', ['root']); -const StringPreview: FC> = ({ value = '' }) => { +const StringPreview: FC> = ({ value = '' }) => { return
{value}
; }; diff --git a/packages/core/src/widgets/string/index.ts b/packages/core/src/widgets/string/index.ts index 8c57ec22a..fe8ac0367 100644 --- a/packages/core/src/widgets/string/index.ts +++ b/packages/core/src/widgets/string/index.ts @@ -2,9 +2,9 @@ import schema from './schema'; import controlComponent from './StringControl'; import previewComponent from './StringPreview'; -import type { StringOrTextField, WidgetParam } from '@staticcms/core/interface'; +import type { StringField, WidgetParam } from '@staticcms/core/interface'; -const StringWidget = (): WidgetParam => { +const StringWidget = (): WidgetParam => { return { name: 'string', controlComponent, diff --git a/packages/core/src/widgets/string/schema.ts b/packages/core/src/widgets/string/schema.ts index c1d7002a6..4786253d1 100644 --- a/packages/core/src/widgets/string/schema.ts +++ b/packages/core/src/widgets/string/schema.ts @@ -1,5 +1,7 @@ export default { properties: { default: { type: 'string' }, + prefix: { type: 'string' }, + suffix: { type: 'string' }, }, }; diff --git a/packages/core/src/widgets/text/TextControl.tsx b/packages/core/src/widgets/text/TextControl.tsx index 789f03358..f6571fd23 100644 --- a/packages/core/src/widgets/text/TextControl.tsx +++ b/packages/core/src/widgets/text/TextControl.tsx @@ -6,7 +6,7 @@ import useDebounce from '@staticcms/core/lib/hooks/useDebounce'; import classNames from '@staticcms/core/lib/util/classNames.util'; import { generateClassNames } from '@staticcms/core/lib/util/theming.util'; -import type { StringOrTextField, WidgetControlProps } from '@staticcms/core/interface'; +import type { TextField, WidgetControlProps } from '@staticcms/core/interface'; import type { ChangeEvent, FC } from 'react'; const classes = generateClassNames('WidgetText', [ @@ -18,7 +18,7 @@ const classes = generateClassNames('WidgetText', [ 'input', ]); -const TextControl: FC> = ({ +const TextControl: FC> = ({ label, value, errors, diff --git a/packages/core/src/widgets/text/TextPreview.tsx b/packages/core/src/widgets/text/TextPreview.tsx index cb52a285d..0b1bbbaa4 100644 --- a/packages/core/src/widgets/text/TextPreview.tsx +++ b/packages/core/src/widgets/text/TextPreview.tsx @@ -2,12 +2,12 @@ import React from 'react'; import { generateClassNames } from '@staticcms/core/lib/util/theming.util'; -import type { StringOrTextField, WidgetPreviewProps } from '@staticcms/core/interface'; +import type { TextField, WidgetPreviewProps } from '@staticcms/core/interface'; import type { FC } from 'react'; const classes = generateClassNames('WidgetTextPreview', ['root']); -const TextPreview: FC> = ({ value }) => { +const TextPreview: FC> = ({ value }) => { return
{value}
; }; diff --git a/packages/core/src/widgets/text/index.ts b/packages/core/src/widgets/text/index.ts index 664637ad8..47a1da948 100644 --- a/packages/core/src/widgets/text/index.ts +++ b/packages/core/src/widgets/text/index.ts @@ -2,9 +2,9 @@ import schema from './schema'; import controlComponent from './TextControl'; import previewComponent from './TextPreview'; -import type { StringOrTextField, WidgetParam } from '@staticcms/core/interface'; +import type { TextField, WidgetParam } from '@staticcms/core/interface'; -const TextWidget = (): WidgetParam => { +const TextWidget = (): WidgetParam => { return { name: 'text', controlComponent, diff --git a/packages/core/test/data/fields.mock.ts b/packages/core/test/data/fields.mock.ts index ce21dcd61..b380c7f73 100644 --- a/packages/core/test/data/fields.mock.ts +++ b/packages/core/test/data/fields.mock.ts @@ -8,7 +8,8 @@ import type { NumberField, RelationField, SelectField, - StringOrTextField, + StringField, + TextField, UUIDField, } from '@staticcms/core'; @@ -92,13 +93,13 @@ export const mockSelectField: SelectField = { options: ['Option 1', 'Option 2', 'Option 3'], }; -export const mockStringField: StringOrTextField = { +export const mockStringField: StringField = { label: 'String', name: 'mock_string', widget: 'string', }; -export const mockTextField: StringOrTextField = { +export const mockTextField: TextField = { label: 'Text', name: 'mock_text', widget: 'text', diff --git a/packages/demo/public/config.yml b/packages/demo/public/config.yml index 9f7bde2fe..0cd34924a 100644 --- a/packages/demo/public/config.yml +++ b/packages/demo/public/config.yml @@ -156,6 +156,19 @@ collections: widget: boolean pattern: ['true', 'Must be true'] required: false + - name: prefix + label: With Prefix + widget: boolean + prefix: "I'm a prefix" + - name: suffix + label: With Suffix + widget: boolean + suffix: "I'm a suffix" + - name: prefix_and_suffix + label: With Prefix and Suffix + widget: boolean + prefix: "I'm a prefix" + suffix: "I'm a suffix" - name: code label: Code file: _widgets/code.json @@ -784,6 +797,19 @@ collections: widget: number pattern: ['[0-9]{3,}', 'Must be at least 3 digits'] required: false + - name: prefix + label: With Prefix + widget: number + prefix: "$" + - name: suffix + label: With Suffix + widget: number + suffix: "%" + - name: prefix_and_suffix + label: With Prefix and Suffix + widget: number + prefix: "$" + suffix: "%" - name: object label: Object file: _widgets/object.json @@ -1061,6 +1087,19 @@ collections: widget: string pattern: ['.{12,}', 'Must have at least 12 characters'] required: false + - name: prefix + label: With Prefix + widget: string + prefix: "$" + - name: suffix + label: With Suffix + widget: string + suffix: "%" + - name: prefix_and_suffix + label: With Prefix and Suffix + widget: string + prefix: "$" + suffix: "%" - name: text label: Text file: _widgets/text.json diff --git a/packages/demo/src/cms.jsx b/packages/demo/src/cms.jsx index d3147da74..68472924c 100644 --- a/packages/demo/src/cms.jsx +++ b/packages/demo/src/cms.jsx @@ -20,7 +20,8 @@ const PostPreview = ({ entry, widgetFor }) => { ); }; -const PostPreviewCard = ({ entry, theme, hasLocalBackup }) => { +const PostPreviewCard = ({ entry, hasLocalBackup }) => { + const theme = useTheme(); const date = new Date(entry.data.date); const month = date.getMonth() + 1; @@ -52,7 +53,7 @@ const PostPreviewCard = ({ entry, theme, hasLocalBackup }) => { justifyContent: "space-between", alignItems: "start", gap: "4px", - color: theme === "dark" ? "white" : "inherit", + color: theme.text.primary, }} >
{ + const theme = useTheme(); + return ( { onChange({ src: event.target.value }); }} + style={{ + width: "100%", + backgroundColor: theme.common.gray, + color: theme.text.primary, + padding: "4px 8px", + }} />