@akinon/pz-theme 2.0.71-beta.0 → 2.0.71

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.
@@ -0,0 +1,172 @@
1
+ import type React from 'react';
2
+ import type { CustomPartsDeclaration } from './parts';
3
+ import type { PropertyType } from './types';
4
+
5
+ /**
6
+ * A developer component takes NO props. Everything it needs — the block, the
7
+ * breakpoint, the designer flag — arrives through `CustomBlockContext`, which
8
+ * is what lets a `<ThemeEditable>` five levels down work without threading.
9
+ * Typing the props as empty is deliberate: a component that declares required
10
+ * props fails to compile here rather than rendering with `undefined` for all
11
+ * of them, because nothing on the render path will ever supply one.
12
+ */
13
+ export type CustomBlockComponent = React.ComponentType<Record<string, never>>;
14
+
15
+ export interface CustomSettingOption {
16
+ label?: string;
17
+ value: unknown;
18
+ }
19
+
20
+ /**
21
+ * What a developer writes. Looser than `CustomSettingDescriptor` in exactly two
22
+ * places — `label` may be omitted, and an option may be a bare string/number —
23
+ * because those are the two shapes people write by hand. Registration
24
+ * normalizes both into the descriptor the editor receives.
25
+ */
26
+ export interface CustomSettingDeclaration {
27
+ key: string;
28
+ type: PropertyType;
29
+ label?: string;
30
+ defaultValue?: unknown;
31
+ options?: Array<CustomSettingOption | string | number>;
32
+ min?: number;
33
+ max?: number;
34
+ step?: number;
35
+ unit?: string;
36
+ maxLength?: number;
37
+ helpText?: string;
38
+ group?: string;
39
+ showWhen?: { key: string; value: unknown };
40
+ /** Content field rather than a behaviour knob — see `CustomSettingDescriptor.editable`. */
41
+ editable?: boolean;
42
+ }
43
+
44
+ /**
45
+ * One merchant-fillable region. Each declared slot materializes as one
46
+ * reserved-type CHILD NODE of the block, so a block declaring three slots gets
47
+ * three tree rows the merchant fills independently.
48
+ *
49
+ * `name` stays optional, and omitting it stays meaningful: a block that
50
+ * declares a single slot needs no name at all, which is exactly what every
51
+ * block written against the original single-slot API already does.
52
+ */
53
+ export interface CustomSlotDeclaration {
54
+ name?: string;
55
+ label?: string;
56
+ /** Carried to the editor; pz-theme does not enforce acceptance yet. */
57
+ accepts?: string[];
58
+ max?: number;
59
+ }
60
+
61
+ export interface CustomBlockOptions {
62
+ /** Shown in the editor's block picker. Defaults to the registration name. */
63
+ label?: string;
64
+ icon?: string;
65
+ group?: string;
66
+ /**
67
+ * Optional. An undeclared block still works: the runtime collector in
68
+ * `collect.ts` fills the descriptor from the first render, and the
69
+ * build-time extractor will supersede both. Declaring is what buys a
70
+ * complete picker entry BEFORE the block has ever been placed once.
71
+ */
72
+ settings?: CustomSettingDeclaration[];
73
+ slots?: CustomSlotDeclaration[];
74
+ /**
75
+ * Elements the developer stamped with `partAttrs()` that the merchant may
76
+ * restyle from the properties panel.
77
+ *
78
+ * `['row', 'tile']` for parts with one appearance, `{ row: { states:
79
+ * ['hover', 'open'] } }` for the ones whose appearance depends on what the
80
+ * shopper is doing. The field-derived `field-<key>` parts are added for free
81
+ * and must not be declared here.
82
+ */
83
+ parts?: CustomPartsDeclaration;
84
+ }
85
+
86
+ /**
87
+ * Tag value, not a `Symbol`.
88
+ *
89
+ * A brand can end up with two physical copies of pz-theme resolved at once — a
90
+ * hoisted one beside a nested one, or the installed package beside the stale
91
+ * copy webpack keeps serving out of `.next`. Each copy would own a distinct
92
+ * `Symbol()`, so a definition made by one copy would read as an unwrapped
93
+ * component to the other, silently losing every option the developer declared.
94
+ * A string tag compares equal across the copy boundary, and it is greppable.
95
+ */
96
+ export const CUSTOM_BLOCK_MARKER = 'pz-theme.custom-block';
97
+
98
+ export interface CustomBlockDefinition {
99
+ readonly $$pzCustomBlock: typeof CUSTOM_BLOCK_MARKER;
100
+ component: CustomBlockComponent;
101
+ options: CustomBlockOptions;
102
+ }
103
+
104
+ /** What `registerCustomBlocks` accepts per entry. */
105
+ export type CustomBlockInput = CustomBlockDefinition | CustomBlockComponent;
106
+
107
+ // React.memo() and forwardRef() return objects rather than functions. A brand
108
+ // wrapping its block in either is ordinary React, so both have to pass.
109
+ const isRenderable = (value: unknown): value is CustomBlockComponent =>
110
+ typeof value === 'function' ||
111
+ (typeof value === 'object' && value !== null && '$$typeof' in value);
112
+
113
+ export const isCustomBlockDefinition = (
114
+ value: unknown
115
+ ): value is CustomBlockDefinition =>
116
+ typeof value === 'object' &&
117
+ value !== null &&
118
+ (value as { $$pzCustomBlock?: unknown }).$$pzCustomBlock ===
119
+ CUSTOM_BLOCK_MARKER;
120
+
121
+ /**
122
+ * Wraps a component with the options the editor needs, and nothing else.
123
+ *
124
+ * It deliberately does NOT know its own name. `Component.name` is the obvious
125
+ * source and it is a trap: every production build minifies it, so a block
126
+ * authored as `PromoStrip` would register as `t` on the storefront and as
127
+ * `PromoStrip` in dev — the same composition would then render in one and be
128
+ * an unknown type in the other. The name lives at registration instead, keyed
129
+ * by the record, where minification cannot reach it.
130
+ *
131
+ * The documented shape is `export default defineCustomBlock(PromoStrip)`.
132
+ */
133
+ export const defineCustomBlock = (
134
+ component: CustomBlockComponent,
135
+ options: CustomBlockOptions = {}
136
+ ): CustomBlockDefinition => {
137
+ if (!isRenderable(component)) {
138
+ throw new Error(
139
+ `[pz-theme] defineCustomBlock() expects a React component, received ${typeof component}. ` +
140
+ `Pass the component itself, not the result of calling it.`
141
+ );
142
+ }
143
+
144
+ return Object.freeze({
145
+ $$pzCustomBlock: CUSTOM_BLOCK_MARKER,
146
+ component,
147
+ options
148
+ });
149
+ };
150
+
151
+ /**
152
+ * Accepts the wrapper or a bare component.
153
+ *
154
+ * Someone will forget `defineCustomBlock` — the barrel entry looks identical
155
+ * either way — and a bare component is a complete block with no declared
156
+ * options, which is a shape the runtime collector already supports. Refusing
157
+ * it would buy nothing but a stack trace.
158
+ */
159
+ export const normalizeCustomBlock = (
160
+ input: CustomBlockInput,
161
+ name: string
162
+ ): CustomBlockDefinition => {
163
+ if (isCustomBlockDefinition(input)) return input;
164
+
165
+ if (isRenderable(input)) return defineCustomBlock(input);
166
+
167
+ throw new Error(
168
+ `[pz-theme] Custom block "${name}" is neither a React component nor a defineCustomBlock() result ` +
169
+ `(received ${input === null ? 'null' : typeof input}). ` +
170
+ `Check the barrel entry — a missing default export lands here as undefined.`
171
+ );
172
+ };
@@ -0,0 +1,120 @@
1
+ import type { PropertyType } from './property-types';
2
+
3
+ /**
4
+ * What a `ThemeEditable` does to the element it wraps.
5
+ *
6
+ * Deliberately NOT `PropertyType`. A kind answers "which prop carries the
7
+ * value", which is a render question; a `PropertyType` answers "which control
8
+ * does the panel draw", which is an editor question. They are one-to-one for
9
+ * three of the four members and one-to-TWO for `link` — a link is a label and
10
+ * a destination, and no single control edits both — so collapsing the two
11
+ * vocabularies would make `link` unrepresentable.
12
+ */
13
+ export type EditableKind = 'text' | 'image' | 'link' | 'file';
14
+
15
+ /**
16
+ * Tag -> kind. The wrapped JSX is the only place a developer states intent, so
17
+ * this map is what lets `<ThemeEditable name="headline"><h2>…</h2></…>` need no
18
+ * `type` prop at all.
19
+ *
20
+ * Only tags whose value target is unambiguous are listed. A `<div>` is absent
21
+ * on purpose: it may hold text, or a background image, or nothing the author
22
+ * meant to be edited, and cloning it with the wrong prop would silently blank
23
+ * the element. An explicit `type` is the escape hatch for those.
24
+ */
25
+ export const EDITABLE_TAG_KINDS: Readonly<Record<string, EditableKind>> = {
26
+ img: 'image',
27
+
28
+ h1: 'text',
29
+ h2: 'text',
30
+ h3: 'text',
31
+ h4: 'text',
32
+ h5: 'text',
33
+ h6: 'text',
34
+ p: 'text',
35
+ span: 'text',
36
+ strong: 'text',
37
+ em: 'text',
38
+ small: 'text',
39
+ label: 'text',
40
+ li: 'text',
41
+ blockquote: 'text',
42
+
43
+ a: 'link',
44
+
45
+ video: 'file',
46
+ source: 'file'
47
+ };
48
+
49
+ export const EDITABLE_KINDS: readonly EditableKind[] = [
50
+ 'text',
51
+ 'image',
52
+ 'link',
53
+ 'file'
54
+ ];
55
+
56
+ export const isEditableKind = (value: unknown): value is EditableKind =>
57
+ typeof value === 'string' &&
58
+ (EDITABLE_KINDS as readonly string[]).includes(value);
59
+
60
+ /** Returns null for a tag with no unambiguous value target — see the map above. */
61
+ export const inferEditableKind = (tag: string): EditableKind | null =>
62
+ EDITABLE_TAG_KINDS[tag] ?? null;
63
+
64
+ /**
65
+ * The prop the resolved value is cloned onto.
66
+ *
67
+ * `link` writes `children` here and its href separately, because the href
68
+ * comes from a second setting with its own key.
69
+ */
70
+ export const EDITABLE_KIND_TARGET: Readonly<
71
+ Record<EditableKind, 'children' | 'src'>
72
+ > = {
73
+ text: 'children',
74
+ image: 'src',
75
+ file: 'src',
76
+ link: 'children'
77
+ };
78
+
79
+ /**
80
+ * The control the editor draws for each kind.
81
+ *
82
+ * Every member resolves to something in `SUPPORTED_PROPERTY_TYPES` — a kind
83
+ * that mapped to `richtext`, `multiselect` or `localetext` would render a
84
+ * control whose value can never reach the element (see property-types.ts).
85
+ */
86
+ export const EDITABLE_KIND_PROPERTY_TYPES: Readonly<
87
+ Record<EditableKind, PropertyType>
88
+ > = {
89
+ text: 'text',
90
+ image: 'image',
91
+ file: 'file',
92
+ // The label half. The destination half is a separate `url` setting.
93
+ link: 'text'
94
+ };
95
+
96
+ /**
97
+ * Suffix for a link's destination key: `cta` -> `ctaHref`.
98
+ *
99
+ * Capitalized so the derived key still satisfies `SETTING_KEY_PATTERN`, which
100
+ * allows camelCase but no separators.
101
+ */
102
+ export const LINK_HREF_SUFFIX = 'Href';
103
+
104
+ export const hrefKeyFor = (key: string): string => `${key}${LINK_HREF_SUFFIX}`;
105
+
106
+ /**
107
+ * Prefix for the `data-part` a field element carries.
108
+ *
109
+ * Namespaced so a content field can never collide with a styling part the
110
+ * developer chose (`title`, `icon`, `panel` — all already published in live
111
+ * compositions), and so the editor can tell the two apart from the part name
112
+ * alone when it decides whether to offer the content control on the part.
113
+ *
114
+ * `sanitizePartName` (utils/part-styles.ts) keeps `[A-Za-z0-9_-]`, so both the
115
+ * hyphen here and a camelCase key survive into the generated selector intact.
116
+ */
117
+ export const FIELD_PART_PREFIX = 'field-';
118
+
119
+ export const fieldPartName = (key: string): string =>
120
+ `${FIELD_PART_PREFIX}${key}`;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The custom block authoring API — everything a brand developer imports.
3
+ *
4
+ * THE BRAND BARREL IS IMPORTED FROM THE CLIENT ROOT ONLY — never from the
5
+ * server layout as well. `register.ts` carries `'use client'` because it must:
6
+ * it imports `blockRendererRegistry`, which pulls in all ~99 block modules, and
7
+ * most of those call hooks without a directive of their own. Reaching them from
8
+ * a server graph fails the brand's build at `accordion-block.tsx:1` with
9
+ * "You're importing a module that depends on `useState` into a React Server
10
+ * Component module". This was found by building a brand, not by a test — jest
11
+ * has no RSC graph, so the whole suite was green over it.
12
+ *
13
+ * Client-only registration loses nothing. `theme-block.tsx` is the single
14
+ * production reader of the renderer registry and is itself a client module, the
15
+ * server-side CSS generator never consults it, and SSR still resolves custom
16
+ * renderers because Next renders client components on the server through the
17
+ * client module graph.
18
+ *
19
+ * Use this deep path, not the package root. `@akinon/pz-theme` re-exports the
20
+ * same symbols but also re-exports `ThemePlaceholder`, which imports
21
+ * `server-only`, so importing `registerCustomBlocks` from the root fails a
22
+ * client module with "cannot be imported from a Client Component module".
23
+ *
24
+ * The shape a brand writes:
25
+ *
26
+ * // blocks/promo-strip.tsx
27
+ * import { defineCustomBlock, ThemeEditable, ThemeSlot, useThemeSetting }
28
+ * from '@akinon/pz-theme/src/custom';
29
+ * export default defineCustomBlock(PromoStrip);
30
+ *
31
+ * // blocks/index.ts — imported from the CLIENT ROOT only
32
+ * registerCustomBlocks('acme', { 'promo-strip': PromoStrip });
33
+ *
34
+ * The block NAME lives in that record, never in `Component.name`: every
35
+ * production build minifies the function name, so a block authored as
36
+ * `PromoStrip` would register as `t` on the storefront and as `PromoStrip` in
37
+ * dev, and one composition would render in one build and be an unknown type in
38
+ * the other.
39
+ */
40
+
41
+ export { defineCustomBlock } from './define';
42
+ export type {
43
+ CustomBlockComponent,
44
+ CustomBlockDefinition,
45
+ CustomBlockInput,
46
+ CustomBlockOptions,
47
+ CustomSettingDeclaration,
48
+ CustomSettingOption,
49
+ CustomSlotDeclaration
50
+ } from './define';
51
+
52
+ export type { CustomPartDeclaration, CustomPartsDeclaration } from './parts';
53
+
54
+ export { registerCustomBlocks } from './register';
55
+
56
+ export { ThemeEditable } from './theme-editable';
57
+ export type { ThemeEditableProps } from './theme-editable';
58
+ export type { EditableKind } from './editable-tags';
59
+
60
+ export { ThemeSlot } from './theme-slot';
61
+ export type { ThemeSlotProps } from './theme-slot';
62
+
63
+ export { useThemeSetting } from './use-theme-setting';
64
+ export type {
65
+ ThemeSettingChoice,
66
+ ThemeSettingOptions
67
+ } from './use-theme-setting';
68
+
69
+ // What the editor bridge announces. Exported here rather than kept internal
70
+ // because a brand that mounts its own announce path needs the same payload the
71
+ // package would have sent, not a second construction of it.
72
+ export { getCustomCatalog } from './catalog';
73
+ export type {
74
+ CustomBlockDescriptor,
75
+ CustomCatalog,
76
+ CustomSettingDescriptor,
77
+ CustomSlotDescriptor,
78
+ PropertyType
79
+ } from './types';
80
+
81
+ // The predicate, not the pattern. Every consumer that has to treat a
82
+ // developer-authored type differently tests this one function, so the grammar
83
+ // stays in types.ts and cannot be re-derived slightly wrong somewhere else.
84
+ export { CUSTOM_TYPE_PREFIX, isCustomThemeType } from './types';
85
+
86
+ /**
87
+ * The slot-node grammar, exported for the same reason the type predicate is:
88
+ * the editor and the generator each hold a mirror of it, and a mirror can be
89
+ * diffed against one exported constant instead of a literal re-typed slightly
90
+ * wrong. `isSlotNode` is the one spelling of the check.
91
+ */
92
+ export { CUSTOM_SLOT_BLOCK_TYPE, SLOT_NAME_PROPERTY } from './types';
93
+ export { isSlotNode, slotNodeName } from './slot-nodes';
94
+
95
+ /**
96
+ * Re-exported so a custom block is written against ONE module.
97
+ *
98
+ * `partAttrs` is what makes a developer's own markup addressable from the
99
+ * styling panel, so it is needed by essentially every block that has more than
100
+ * one element — and `ThemeEditable` already stamps its fields with it. Sending
101
+ * a developer to `@akinon/pz-theme/src/utils/part-styles` for the other half of
102
+ * the same job is the kind of split that ends in a hand-written
103
+ * `data-part="…"`, which `sanitizePartName` then has no chance to normalize.
104
+ */
105
+ export { partAttrs } from '../utils/part-styles';
@@ -0,0 +1,108 @@
1
+ import { PART_STYLE_STATES } from '../utils/part-styles';
2
+ import { FIELD_PART_PREFIX } from './editable-tags';
3
+ import { PART_NAME_PATTERN } from './types';
4
+
5
+ /**
6
+ * Developer-declared styling parts.
7
+ *
8
+ * Until now `descriptor.parts` was filled ONLY from `ThemeEditable` fields, so
9
+ * an element a developer stamped with `partAttrs('menuRow')` never reached the
10
+ * properties panel — a brand writing a menu could style its rows in its own
11
+ * stylesheet and nowhere else. The editor side already knows what to do with a
12
+ * declared part (it appends the advanced CSS families, the geometry family and
13
+ * the per-part custom-CSS hatch to any part group it is given); what was
14
+ * missing was the declaration reaching it.
15
+ *
16
+ * States are the reason this is not just a list of names. An active menu row,
17
+ * an open accordion header, a hovered tile — those are the whole point of
18
+ * styling a part, and a part with no states can express none of them.
19
+ */
20
+
21
+ /** What a part declares beyond existing. */
22
+ export interface CustomPartDeclaration {
23
+ /**
24
+ * Must be states the RENDERER can match — `PART_STYLE_STATES`. A state with
25
+ * no selector expression behind it produces a control that writes CSS which
26
+ * can never apply, which reads as "the editor does not save".
27
+ */
28
+ states?: readonly string[];
29
+ }
30
+
31
+ /**
32
+ * Both spellings a developer writes.
33
+ *
34
+ * The bare array is the cheap one and it is genuinely enough for a part that
35
+ * only ever has one appearance. The record is the one that carries states. They
36
+ * cost one normalizer between them, so both are accepted rather than making
37
+ * every block pay the record's syntax for a part that needs nothing from it.
38
+ */
39
+ export type CustomPartsDeclaration =
40
+ | readonly string[]
41
+ | Readonly<Record<string, CustomPartDeclaration | true>>;
42
+
43
+ export interface NormalizedPart {
44
+ name: string;
45
+ states: string[];
46
+ }
47
+
48
+ /**
49
+ * One shape out of the two, in the order the developer wrote them.
50
+ *
51
+ * Object key order is insertion order for non-numeric string keys, so a record
52
+ * is as deterministic as an array here — which matters because this order is
53
+ * the order the part groups appear in the properties panel.
54
+ */
55
+ export const normalizePartsDeclaration = (
56
+ declaration: CustomPartsDeclaration | undefined
57
+ ): NormalizedPart[] => {
58
+ if (!declaration) return [];
59
+
60
+ if (Array.isArray(declaration)) {
61
+ return (declaration as readonly unknown[]).map((name) => ({
62
+ name: String(name),
63
+ states: []
64
+ }));
65
+ }
66
+
67
+ return Object.entries(
68
+ declaration as Record<string, CustomPartDeclaration | true>
69
+ ).map(([name, value]) => ({
70
+ name,
71
+ states:
72
+ value === true || !value?.states ? [] : Array.from(new Set(value.states))
73
+ }));
74
+ };
75
+
76
+ /** The reason a declared part name is refused, or null. */
77
+ export const partNameError = (name: unknown): string | null => {
78
+ if (typeof name !== 'string' || !PART_NAME_PATTERN.test(name)) {
79
+ return (
80
+ `part name ${JSON.stringify(name)} could not survive into a CSS selector. ` +
81
+ `Use letters, digits, hyphens and underscores (max 64 characters) — sanitizePartName ` +
82
+ `strips everything else, so the part the catalog advertises and the part the stylesheet ` +
83
+ `matches would be different strings`
84
+ );
85
+ }
86
+
87
+ if (name.startsWith(FIELD_PART_PREFIX)) {
88
+ return (
89
+ `part name "${name}" uses the "${FIELD_PART_PREFIX}" prefix, which is reserved for the parts ` +
90
+ `ThemeEditable derives from a field key. Declaring one collides with the auto-derived part ` +
91
+ `for that field — today if the field exists, and the moment it is added if it does not. Rename it`
92
+ );
93
+ }
94
+
95
+ return null;
96
+ };
97
+
98
+ /** The reason a declared state is refused, or null. */
99
+ export const partStateError = (state: unknown): string | null => {
100
+ if (typeof state === 'string' && PART_STYLE_STATES.includes(state)) {
101
+ return null;
102
+ }
103
+
104
+ return (
105
+ `state ${JSON.stringify(state)} has no selector behind it, so a control written into it ` +
106
+ `would emit CSS that can never match. Available: ${PART_STYLE_STATES.join(', ')}`
107
+ );
108
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The control vocabulary a custom block may declare, mirroring the editor's
3
+ * `PropertyType` union (`pz-dynamic-widget/src/config/base-config.ts:3-20`).
4
+ *
5
+ * Closed on purpose, and closed in the same way every comparable product
6
+ * closes it (Shopify 31, Webflow 9, Puck 11): a brand cannot introduce a
7
+ * control type, because the form is rendered by an editor bundle the brand
8
+ * does not ship and cannot extend. Two of the editor's members are absent
9
+ * here rather than merely unsupported:
10
+ *
11
+ * - `custom` needs a `customRender` FUNCTION, which cannot cross postMessage.
12
+ * - `group` is a container the panel derives, not a leaf a block declares.
13
+ */
14
+ export const CUSTOM_PROPERTY_TYPES = [
15
+ 'text',
16
+ 'toggle',
17
+ 'select',
18
+ 'multiselect',
19
+ 'slider',
20
+ 'url',
21
+ 'color',
22
+ 'image',
23
+ 'file',
24
+ 'richtext',
25
+ 'localetext',
26
+ 'typography',
27
+ 'icon'
28
+ ] as const;
29
+
30
+ export type PropertyType = (typeof CUSTOM_PROPERTY_TYPES)[number];
31
+
32
+ /**
33
+ * The subset whose value provably survives the full round trip — panel write,
34
+ * persistence, and resolution back into a prop.
35
+ *
36
+ * The three excluded members are excluded for reasons that were verified, not
37
+ * guessed:
38
+ *
39
+ * - `multiselect` and `localetext` are stored UNBUCKETED by the editor
40
+ * (`theme-basic-properties.tsx:1547-1548` passes arrays and locale objects
41
+ * through without the per-breakpoint wrapper). `getResponsiveValue` then
42
+ * rejects an array with a devWarn and returns the fallback, and a locale
43
+ * object has no `desktop` key so it does too. They can never deliver a
44
+ * value until a per-type resolver exists.
45
+ * - `richtext` has no safe renderer here. The allowlist sanitizer this repo
46
+ * already owns parses by assigning to `innerHTML` on a live element, so the
47
+ * sink is its first act — and pz-theme renders server-side, where there is
48
+ * no `document` at all.
49
+ *
50
+ * Declaring one of these throws at define time rather than shipping a control
51
+ * that silently shows the default forever.
52
+ */
53
+ export const SUPPORTED_PROPERTY_TYPES: readonly PropertyType[] = [
54
+ 'text',
55
+ 'toggle',
56
+ 'select',
57
+ 'slider',
58
+ 'url',
59
+ 'color',
60
+ 'image',
61
+ 'file',
62
+ 'typography',
63
+ 'icon'
64
+ ];
65
+
66
+ export const isSupportedPropertyType = (
67
+ type: unknown
68
+ ): type is PropertyType =>
69
+ typeof type === 'string' &&
70
+ (SUPPORTED_PROPERTY_TYPES as readonly string[]).includes(type);