@madebywild/sanity-link-field 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # @madebywild/sanity-link-field
@@ -0,0 +1,21 @@
1
+ import { type FieldOptions, type InternalLinksQueryResult, type PluginConfig, typeName } from "./types";
2
+ declare function buildLinkPreview({ type, customText, internalUri, internalTitle, externalUrl, email, phone, fileName, }: {
3
+ type?: string;
4
+ customText?: string;
5
+ internalUri?: string;
6
+ internalTitle?: string;
7
+ externalUrl?: string;
8
+ email?: string;
9
+ phone?: string;
10
+ fileName?: string;
11
+ }): {
12
+ title: string;
13
+ subtitle: string | undefined;
14
+ media: () => import("react/jsx-runtime").JSX.Element;
15
+ } | {
16
+ title: string;
17
+ media: () => import("react/jsx-runtime").JSX.Element;
18
+ subtitle?: undefined;
19
+ };
20
+ declare const wildSanityLinkFieldPlugin: import("sanity").Plugin<PluginConfig>;
21
+ export { wildSanityLinkFieldPlugin, buildLinkPreview, typeName, type FieldOptions, type InternalLinksQueryResult, type PluginConfig, };
package/dist/index.js ADDED
@@ -0,0 +1,180 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { requiredIf, visibleIf } from "@madebywild/sanity-utils";
3
+ import { defineField, definePlugin, defineType } from "sanity";
4
+ import { LinkInput } from "./input";
5
+ import { typeName } from "./types";
6
+ const visibleIfType = visibleIf("type");
7
+ const requiredIfType = requiredIf("type");
8
+ function buildLinkPreview({ type, customText, internalUri, internalTitle, externalUrl, email, phone, fileName, }) {
9
+ switch (type) {
10
+ case "internal": {
11
+ return {
12
+ title: customText ?? internalTitle ?? "Internal Link",
13
+ subtitle: internalUri,
14
+ media: () => _jsx(_Fragment, { children: "\uD83D\uDCC4" }),
15
+ };
16
+ }
17
+ case "external":
18
+ return {
19
+ title: customText ?? "External Link",
20
+ subtitle: externalUrl,
21
+ media: () => _jsx(_Fragment, { children: "\uD83C\uDF0D" }),
22
+ };
23
+ case "email":
24
+ return {
25
+ title: customText ?? "Email Link",
26
+ subtitle: email,
27
+ media: () => _jsx(_Fragment, { children: "\uD83D\uDCE7" }),
28
+ };
29
+ case "phone":
30
+ return {
31
+ title: customText ?? "Phone Link",
32
+ subtitle: phone,
33
+ media: () => _jsx(_Fragment, { children: "\u260E\uFE0F" }),
34
+ };
35
+ case "file":
36
+ return {
37
+ title: customText ?? "File Link",
38
+ subtitle: fileName,
39
+ media: () => _jsx(_Fragment, { children: "\uD83D\uDCC3" }),
40
+ };
41
+ default:
42
+ return {
43
+ title: customText ?? "Empty Link",
44
+ media: () => _jsx(_Fragment, { children: "\u26D3\uFE0F\u200D\uD83D\uDCA5" }),
45
+ };
46
+ }
47
+ }
48
+ const wildSanityLinkFieldPlugin = definePlugin((config) => {
49
+ return {
50
+ name: "@madebywild/sanity-link-field",
51
+ schema: {
52
+ types: [
53
+ defineType({
54
+ name: typeName,
55
+ type: "object",
56
+ title: "Link",
57
+ description: "Link to an internal page, external URL and more.",
58
+ icon: () => _jsx(_Fragment, { children: "\uD83D\uDD17" }),
59
+ components: {
60
+ input: (props) => _jsx(LinkInput, { config: config, ...props }),
61
+ },
62
+ fields: [
63
+ defineField({
64
+ name: "type",
65
+ type: "string",
66
+ title: "Link Type",
67
+ options: {
68
+ layout: "dropdown",
69
+ list: [
70
+ { title: "📄 Internal Link", value: "internal" },
71
+ { title: "🌍 External Link", value: "external" },
72
+ { title: "📧 Email", value: "email" },
73
+ { title: "☎️ Phone", value: "phone" },
74
+ { title: "📃 File", value: "file" },
75
+ ],
76
+ },
77
+ }),
78
+ defineField({
79
+ name: "external",
80
+ type: "url",
81
+ title: "External Link",
82
+ ...visibleIfType("external"),
83
+ ...requiredIfType("external"),
84
+ }),
85
+ defineField({
86
+ name: "email",
87
+ type: "string",
88
+ title: "Email",
89
+ ...visibleIfType("email"),
90
+ ...requiredIfType("email"),
91
+ }),
92
+ defineField({
93
+ name: "phone",
94
+ type: "string",
95
+ title: "Phone",
96
+ ...visibleIfType("phone"),
97
+ ...requiredIfType("phone"),
98
+ }),
99
+ defineField({
100
+ name: "file",
101
+ type: "file",
102
+ title: "File",
103
+ ...visibleIfType("file"),
104
+ ...requiredIfType("file"),
105
+ }),
106
+ defineField({
107
+ name: "canDownload",
108
+ type: "boolean",
109
+ title: "Downloadable",
110
+ initialValue: true,
111
+ description: "The file will be downloaded when the link is clicked.",
112
+ options: { layout: "switch" },
113
+ ...visibleIfType("file"),
114
+ ...requiredIfType("file"),
115
+ }),
116
+ defineField({
117
+ name: "internal",
118
+ type: "object",
119
+ title: "Internal Link",
120
+ description: "Select a page and an optional section target.",
121
+ ...visibleIfType("internal"),
122
+ ...requiredIfType("internal"),
123
+ options: {
124
+ collapsed: false,
125
+ collapsible: false,
126
+ },
127
+ fields: [
128
+ defineField({
129
+ type: "reference",
130
+ weak: true,
131
+ name: "link",
132
+ title: "Page",
133
+ // Note: The custom input component will augment this and add more types.
134
+ // We need to keep at least one type here for Sanity to not complain.
135
+ to: config.internalLinkSchemaTypes ?? [{ type: "page" }],
136
+ }),
137
+ defineField({
138
+ type: "string",
139
+ name: "sectionTarget",
140
+ title: "Section Target",
141
+ }),
142
+ ],
143
+ }),
144
+ defineField({
145
+ name: "customText",
146
+ type: "string",
147
+ title: "Custom text",
148
+ description: "Will take precedence over inferred text.",
149
+ }),
150
+ defineField({
151
+ name: "openInNewTab",
152
+ type: "boolean",
153
+ title: "Open in new tab",
154
+ description: "Open the link in a new tab.",
155
+ initialValue: false,
156
+ options: { layout: "switch" },
157
+ ...visibleIfType(["external", "internal"]),
158
+ }),
159
+ ],
160
+ preview: {
161
+ select: {
162
+ type: "type",
163
+ email: "email",
164
+ phone: "phone",
165
+ customText: "customText",
166
+ fileName: "file.asset.originalFilename",
167
+ externalUrl: "external",
168
+ internalUri: "internal.uri.current",
169
+ internalTitle: "internal.title",
170
+ },
171
+ prepare: (props) => {
172
+ return buildLinkPreview(props);
173
+ },
174
+ },
175
+ }),
176
+ ],
177
+ },
178
+ };
179
+ });
180
+ export { wildSanityLinkFieldPlugin, buildLinkPreview, typeName, };
@@ -0,0 +1,6 @@
1
+ import type { ObjectInputProps } from "sanity";
2
+ import type { PluginConfig } from "./types";
3
+ declare function LinkInput(props: ObjectInputProps & {
4
+ config: PluginConfig;
5
+ }): import("react/jsx-runtime").JSX.Element;
6
+ export { LinkInput };
package/dist/input.js ADDED
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AsyncAutocomplete } from "@madebywild/sanity-utils";
3
+ import { Box, Card, Flex, Stack, Text } from "@sanity/ui";
4
+ import * as changeCase from "change-case";
5
+ import * as React from "react";
6
+ import { MemberField, set, unset, useClient } from "sanity";
7
+ function useFieldMember(members, fieldName) {
8
+ return React.useMemo(() => members.find((member) => member.kind === "field" && member.name === fieldName), [members, fieldName]);
9
+ }
10
+ function InternalLinkInput({ config, value, onChange, }) {
11
+ const linkQuery = config.internalLinkQuery;
12
+ const queryTransformer = config.internalLinkTransformer;
13
+ const client = useClient({ apiVersion: "2025-10-21" });
14
+ const [items, setItems] = React.useState([]);
15
+ const selectedItem = items.find((_) => _.value === value?.link?._ref);
16
+ React.useEffect(() => {
17
+ client
18
+ .fetch(linkQuery)
19
+ .then(queryTransformer)
20
+ .then((items) => items.sort((a, b) => a.label.localeCompare(b.label)))
21
+ .then(setItems);
22
+ }, [client, linkQuery, queryTransformer]);
23
+ return (_jsxs(Flex, { direction: "column", gap: 2, children: [_jsx(AsyncAutocomplete, { placeholder: "Select page", defaultValue: value?.link?._ref, listItems: items, onChange: (value) => {
24
+ const next = value ? set({ _ref: value, _type: "reference" }, ["link"]) : unset(["link"]);
25
+ return onChange(next);
26
+ }, renderValue: (value, opt) => {
27
+ return opt?.label ? changeCase.capitalCase(opt.label) : value;
28
+ }, renderOption: ({ label, uri }) => {
29
+ return (_jsx(Card, { as: "button", children: _jsxs(Flex, { flex: 1, padding: 3, gap: 2, direction: "column", children: [_jsx(Text, { size: 2, children: changeCase.capitalCase(label) }), _jsx(Text, { size: 0, muted: true, children: uri })] }) }));
30
+ } }), selectedItem?.sections?.length ? (_jsx(AsyncAutocomplete, { placeholder: "Select section", defaultValue: value?.sectionTarget, listItems: selectedItem?.sections, onChange: (value) => {
31
+ const next = value ? set(value, ["sectionTarget"]) : unset(["sectionTarget"]);
32
+ return onChange(next);
33
+ }, renderValue: (value, opt) => {
34
+ return opt?.label ? changeCase.capitalCase(opt.label) : value;
35
+ }, renderOption: ({ label }) => {
36
+ return (_jsx(Card, { as: "button", children: _jsx(Box, { flex: 1, padding: 3, children: _jsx(Text, { size: 2, children: changeCase.capitalCase(label) }) }) }));
37
+ } })) : null] }));
38
+ }
39
+ function LinkInput(props) {
40
+ const options = props.schemaType.options;
41
+ const typeFieldMember = useFieldMember(props.members, "type");
42
+ const externalFieldMember = useFieldMember(props.members, "external");
43
+ const emailFieldMember = useFieldMember(props.members, "email");
44
+ const phoneFieldMember = useFieldMember(props.members, "phone");
45
+ const fileFieldMember = useFieldMember(props.members, "file");
46
+ const canDownloadFieldMember = useFieldMember(props.members, "canDownload");
47
+ const internalFieldMember = useFieldMember(props.members, "internal");
48
+ const customTextFieldMember = useFieldMember(props.members, "customText");
49
+ const openInNewTabFieldMember = useFieldMember(props.members, "openInNewTab");
50
+ return (_jsxs(Stack, { space: 4, children: [typeFieldMember && _jsx(MemberField, { member: typeFieldMember, ...props }), externalFieldMember && _jsx(MemberField, { member: externalFieldMember, ...props }), emailFieldMember && _jsx(MemberField, { member: emailFieldMember, ...props }), phoneFieldMember && _jsx(MemberField, { member: phoneFieldMember, ...props }), fileFieldMember && _jsx(MemberField, { member: fileFieldMember, ...props }), canDownloadFieldMember && _jsx(MemberField, { member: canDownloadFieldMember, ...props }), internalFieldMember && _jsx(InternalLinkInput, { ...props }), !options?.noCustomText && customTextFieldMember && _jsx(MemberField, { member: customTextFieldMember, ...props }), openInNewTabFieldMember && _jsx(MemberField, { member: openInNewTabFieldMember, ...props })] }));
51
+ }
52
+ export { LinkInput };
@@ -0,0 +1,27 @@
1
+ import type { ObjectDefinition, ObjectOptions, ReferenceTo } from "sanity";
2
+ export type InternalLinksQueryResult = {
3
+ label: string;
4
+ value: string;
5
+ uri?: string;
6
+ sections?: {
7
+ label: string;
8
+ value: string;
9
+ }[];
10
+ }[];
11
+ export type PluginConfig = {
12
+ internalLinkQuery: string;
13
+ internalLinkSchemaTypes?: ReferenceTo;
14
+ internalLinkTransformer: <QueryRes = unknown>(queryRes: QueryRes) => InternalLinksQueryResult;
15
+ };
16
+ export type FieldOptions = ObjectOptions & {
17
+ noCustomText?: boolean;
18
+ };
19
+ export declare const typeName: "wild.link";
20
+ declare module "sanity" {
21
+ interface IntrinsicDefinitions {
22
+ [typeName]: Omit<ObjectDefinition, "type" | "fields"> & {
23
+ type: typeof typeName;
24
+ options?: FieldOptions;
25
+ };
26
+ }
27
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export const typeName = "wild.link";
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@madebywild/sanity-link-field",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "clear": "rm -rf dist",
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "dependencies": {
19
+ "@madebywild/sanity-utils": "*",
20
+ "change-case": "^5.4.4"
21
+ },
22
+ "peerDependencies": {
23
+ "sanity": "^4.17",
24
+ "react": "^19",
25
+ "react-dom": "^19"
26
+ },
27
+ "devDependencies": {
28
+ "sanity": "^4.17",
29
+ "react": "^19",
30
+ "react-dom": "^19",
31
+ "typescript": "^5.9",
32
+ "@types/react": "^19",
33
+ "@types/react-dom": "^19"
34
+ }
35
+ }