diff --git a/.changeset/six-lies-laugh.md b/.changeset/six-lies-laugh.md
new file mode 100644
index 00000000..95f765f8
--- /dev/null
+++ b/.changeset/six-lies-laugh.md
@@ -0,0 +1,5 @@
+---
+"@tinloof/sanity-studio": minor
+---
+
+Input with characters count component
diff --git a/packages/sanity-studio/README.md b/packages/sanity-studio/README.md
index 88edec73..23334f76 100644
--- a/packages/sanity-studio/README.md
+++ b/packages/sanity-studio/README.md
@@ -27,6 +27,7 @@ npm install @tinloof/sanity-studio
- [`localizedItem`](#localizedItem)
- [`defineIcon`](#defineIcon)
- [Disable creation plugin](#disable-creation-plugin)
+- [Input with characters count](#input-with-characters-count)
## Pages
@@ -528,7 +529,7 @@ export default defineConfig({
### Important notice
-When using this plugin, make sure you placing it after the `stucutureTool()`.
+When using this plugin, make sure you are placing it after the `stucutureTool()`.
```tsx
plugins: [
@@ -540,6 +541,39 @@ plugins: [
],
```
+## Input with characters count
+
+This is a custom component which shows a characters count below a string input. Requires you to specify the minimum and maximum length of the string in order to render and also show tone states.
+
+### Basic usage
+
+Add as a `input` under `components` and include the options `minLength` and `maxLength`.
+
+```tsx
+{
+ type: 'object',
+ name: 'seo',
+ fields: [
+ {
+ type: 'string',
+ name: 'title',
+ components: {
+ input: InputWithCharacterCount,
+ },
+ options: {
+ maxLength: 100,
+ minLength: 10,
+ },
+ },
+ ],
+},
+```
+
+### Parameters
+
+- `minLength`: Number, minimum length of string
+- `maxLength`: Number, maximum length of string
+
## Examples
Check the `/examples` folder.
diff --git a/packages/sanity-studio/src/components/index.ts b/packages/sanity-studio/src/components/index.ts
index fb6affcf..f71854d4 100644
--- a/packages/sanity-studio/src/components/index.ts
+++ b/packages/sanity-studio/src/components/index.ts
@@ -1,2 +1,3 @@
export { IconSelectComponent } from "./IconSelectComponent";
+export { InputWithCharacterCount } from "./input-with-characters-count";
export { PathnameFieldComponent } from "./PathnameFieldComponent";
diff --git a/packages/sanity-studio/src/components/input-with-characters-count.tsx b/packages/sanity-studio/src/components/input-with-characters-count.tsx
new file mode 100644
index 00000000..74ade506
--- /dev/null
+++ b/packages/sanity-studio/src/components/input-with-characters-count.tsx
@@ -0,0 +1,70 @@
+import type { BadgeTone } from "@sanity/ui";
+import { Badge, Flex, Stack } from "@sanity/ui";
+import type { TextInputProps, TextOptions } from "sanity";
+import { useFormValue } from "sanity";
+
+type CountedTextOptions = {
+ maxLength?: number;
+ minLength?: number;
+} & TextOptions;
+
+function CharacterCount(props: { value?: string } & CountedTextOptions) {
+ if (!props.maxLength && !props.minLength) {
+ return null;
+ }
+
+ const { value = "" } = props;
+
+ const maxPercentage =
+ props.maxLength && (value.length / props.maxLength) * 100;
+
+ let tone: BadgeTone = "primary";
+
+ if (maxPercentage && maxPercentage > 100) {
+ tone = "critical";
+ } else if (maxPercentage && maxPercentage > 75) {
+ tone = "caution";
+ }
+
+ if (props.minLength && value.length < props.minLength) {
+ tone = "caution";
+ }
+ return (
+
+ {value.length} / {props.maxLength}
+
+ );
+}
+
+export function InputWithCharacterCount(props: TextInputProps): JSX.Element {
+ const document = useFormValue([]);
+
+ if (!document) {
+ return props.renderDefault(props);
+ }
+
+ const { name, title } = document as {
+ name?: string;
+ title?: string;
+ };
+
+ let defaultTitle: string | undefined = undefined;
+
+ if (props.id === "seo.title") {
+ defaultTitle = title ?? name ?? undefined;
+ }
+
+ props.elementProps.placeholder = defaultTitle;
+
+ return (
+
+ {props.renderDefault(props)}
+
+
+
+
+ );
+}