@neurodyn/ai-try-on 0.1.2 → 0.2.0
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/dist/ai-try-on.embed.min.js +5 -5
- package/dist/ai-try-on.global.min.js +5 -5
- package/dist/ai-try-on.js +161 -38
- package/dist/contract.d.ts +154 -0
- package/dist/contract.js +195 -0
- package/dist/index.d.ts +8 -8
- package/dist/shopify.embed.min.js +1 -1
- package/package.json +9 -2
package/dist/ai-try-on.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AiTryOnDialog, PRODUCT_TYPES
|
|
1
|
+
import { AiTryOnDialog, PRODUCT_TYPES } from "@neurodyn/react-ai-try-on";
|
|
2
2
|
import { createElement } from "react";
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
|
4
4
|
const DIALOG_ROOT_ID_PREFIX = 'neurodyn-ai-try-on-dialog-root';
|
|
@@ -43,54 +43,177 @@ class AiTryOnDialogRenderer {
|
|
|
43
43
|
}));
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
function
|
|
46
|
+
function contract_attribute(name, parser, details) {
|
|
47
|
+
return {
|
|
48
|
+
kind: 'attribute',
|
|
49
|
+
name,
|
|
50
|
+
parse: parser.parse,
|
|
51
|
+
values: parser.values,
|
|
52
|
+
...details
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function optionOnly(reason) {
|
|
56
56
|
return {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
productName: void 0 !== props.productName ? props.productName : readRequiredAttribute(trigger, AI_TRY_ON_PRODUCT_NAME_ATTRIBUTE),
|
|
60
|
-
productPrice: void 0 !== props.productPrice ? normalizeOptionalValue(props.productPrice) : readOptionalAttribute(trigger, AI_TRY_ON_PRODUCT_PRICE_ATTRIBUTE),
|
|
61
|
-
productUrl: void 0 !== props.productUrl ? props.productUrl : readOptionalAttribute(trigger, AI_TRY_ON_PRODUCT_URL_ATTRIBUTE),
|
|
62
|
-
productType: readProductType(void 0 !== props.productType ? props.productType : trigger.getAttribute(AI_TRY_ON_PRODUCT_TYPE_ATTRIBUTE)),
|
|
63
|
-
apiKey: normalizeApiKey(void 0 !== props.apiKey ? props.apiKey : trigger.getAttribute(AI_TRY_ON_API_KEY_ATTRIBUTE))
|
|
57
|
+
kind: 'option',
|
|
58
|
+
reason
|
|
64
59
|
};
|
|
65
60
|
}
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
function defineTriggerContract() {
|
|
62
|
+
return (contract)=>contract;
|
|
63
|
+
}
|
|
64
|
+
function contract_attributeEntries(contract) {
|
|
65
|
+
return Object.entries(contract.fields).filter((entry)=>'attribute' === entry[1].kind);
|
|
66
|
+
}
|
|
67
|
+
function attributeName(contract, key) {
|
|
68
|
+
const field = contract.fields[key];
|
|
69
|
+
return `${contract.prefix}${field.name}`;
|
|
70
|
+
}
|
|
71
|
+
function triggerSelector(contract) {
|
|
72
|
+
return `[${contract.markers[0].name}]`;
|
|
73
|
+
}
|
|
74
|
+
function readRawAttribute(trigger, name, legacyNames = []) {
|
|
75
|
+
const value = trigger.getAttribute(name);
|
|
76
|
+
if (0 === legacyNames.length || value?.trim()) return value;
|
|
77
|
+
for (const legacyName of legacyNames){
|
|
78
|
+
const legacyValue = trigger.getAttribute(legacyName);
|
|
79
|
+
if (legacyValue?.trim()) return legacyValue;
|
|
70
80
|
}
|
|
71
81
|
return value;
|
|
72
82
|
}
|
|
73
|
-
function
|
|
74
|
-
const
|
|
75
|
-
|
|
83
|
+
function readTriggerAttributes(trigger, contract, options = {}) {
|
|
84
|
+
const values = {};
|
|
85
|
+
for (const [key, field] of contract_attributeEntries(contract)){
|
|
86
|
+
const name = `${contract.prefix}${field.name}`;
|
|
87
|
+
const option = options[key];
|
|
88
|
+
if (void 0 !== option) {
|
|
89
|
+
values[key] = field.normalizesOptions ? field.parse(option, {
|
|
90
|
+
attribute: name
|
|
91
|
+
}) : option;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
values[key] = field.parse(readRawAttribute(trigger, name, field.legacyNames), {
|
|
95
|
+
attribute: name
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return values;
|
|
76
99
|
}
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
100
|
+
function trimmedText() {
|
|
101
|
+
return {
|
|
102
|
+
parse: (raw)=>raw?.trim() || void 0
|
|
103
|
+
};
|
|
80
104
|
}
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
105
|
+
function requiredText(missing) {
|
|
106
|
+
return {
|
|
107
|
+
parse: (raw, { attribute })=>{
|
|
108
|
+
const value = raw?.trim();
|
|
109
|
+
if (!value) throw missing(attribute);
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
84
113
|
}
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
114
|
+
function oneOf(values) {
|
|
115
|
+
return {
|
|
116
|
+
parse: (raw)=>values.includes(raw ?? '') ? raw : void 0,
|
|
117
|
+
values
|
|
118
|
+
};
|
|
88
119
|
}
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
120
|
+
function custom(parse, values) {
|
|
121
|
+
return {
|
|
122
|
+
parse,
|
|
123
|
+
values
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function listOf() {
|
|
127
|
+
return (values)=>values;
|
|
128
|
+
}
|
|
129
|
+
const AI_TRY_ON_PRODUCT_TYPES = listOf()([
|
|
130
|
+
'full-body',
|
|
131
|
+
'upper-body',
|
|
132
|
+
'lower-body',
|
|
133
|
+
'footwear',
|
|
134
|
+
'headwear',
|
|
135
|
+
'eyewear',
|
|
136
|
+
'neck',
|
|
137
|
+
'wrist',
|
|
138
|
+
'bag',
|
|
139
|
+
'waist'
|
|
140
|
+
]);
|
|
141
|
+
const AI_TRY_ON_LANGUAGES = listOf()([
|
|
142
|
+
'en',
|
|
143
|
+
'ru'
|
|
144
|
+
]);
|
|
145
|
+
function isProductType(value) {
|
|
146
|
+
return 'string' == typeof value && AI_TRY_ON_PRODUCT_TYPES.includes(value);
|
|
147
|
+
}
|
|
148
|
+
const EXTERNAL_ONLY = 'External integrations only, such as the Shopify block.';
|
|
149
|
+
const aiTryOnContract = defineTriggerContract()({
|
|
150
|
+
prefix: 'data-neurodyn-ai-try-on-',
|
|
151
|
+
markers: [
|
|
152
|
+
{
|
|
153
|
+
name: 'data-neurodyn-ai-try-on-dialog',
|
|
154
|
+
description: 'Marks a dialog trigger for the embed build.'
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
fields: {
|
|
158
|
+
productType: contract_attribute('product-type', custom((value, { attribute })=>{
|
|
159
|
+
if (!isProductType(value)) {
|
|
160
|
+
const received = 'string' == typeof value && value.length > 0 ? ` "${value}"` : '';
|
|
161
|
+
throw new Error(`AI try-on product type from ${attribute} must be one of: ${AI_TRY_ON_PRODUCT_TYPES.join(', ')}; received${received || ' no value'}`);
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}, AI_TRY_ON_PRODUCT_TYPES), {
|
|
165
|
+
required: true,
|
|
166
|
+
normalizesOptions: true,
|
|
167
|
+
description: 'Product category.'
|
|
168
|
+
}),
|
|
169
|
+
productName: contract_attribute('product-name', requiredText((name)=>new Error(`AI try-on dialog trigger is missing ${name}`)), {
|
|
170
|
+
required: true,
|
|
171
|
+
description: 'Product name shown in the dialog.'
|
|
172
|
+
}),
|
|
173
|
+
productImageSrc: contract_attribute('product-image-src', requiredText((name)=>new Error(`AI try-on dialog trigger is missing ${name}`)), {
|
|
174
|
+
required: true,
|
|
175
|
+
description: 'Product image URL.'
|
|
176
|
+
}),
|
|
177
|
+
productPrice: contract_attribute('product-price', trimmedText(), {
|
|
178
|
+
required: false,
|
|
179
|
+
normalizesOptions: true,
|
|
180
|
+
description: 'Already-formatted product price.'
|
|
181
|
+
}),
|
|
182
|
+
productUrl: contract_attribute('product-url', trimmedText(), {
|
|
183
|
+
required: false,
|
|
184
|
+
description: 'Product page URL opened from the result.'
|
|
185
|
+
}),
|
|
186
|
+
language: contract_attribute('language', oneOf(AI_TRY_ON_LANGUAGES), {
|
|
187
|
+
required: false,
|
|
188
|
+
defaultValue: 'Browser language',
|
|
189
|
+
description: 'UI language override.'
|
|
190
|
+
}),
|
|
191
|
+
apiKey: contract_attribute('api-key', trimmedText(), {
|
|
192
|
+
required: true,
|
|
193
|
+
normalizesOptions: true,
|
|
194
|
+
description: 'Public API key.'
|
|
195
|
+
}),
|
|
196
|
+
open: optionOnly('The embed build owns the open state.'),
|
|
197
|
+
defaultOpen: optionOnly('The embed build owns the open state.'),
|
|
198
|
+
onOpenChange: optionOnly('The embed build owns the open state.'),
|
|
199
|
+
externalGeneration: optionOnly(EXTERNAL_ONLY),
|
|
200
|
+
externalActivation: optionOnly(EXTERNAL_ONLY),
|
|
201
|
+
externalClient: optionOnly(EXTERNAL_ONLY),
|
|
202
|
+
externalAddToCart: optionOnly(EXTERNAL_ONLY)
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
function readTriggerProps(trigger, props = {}) {
|
|
206
|
+
return readTriggerAttributes(trigger, aiTryOnContract, props);
|
|
93
207
|
}
|
|
208
|
+
const AI_TRY_ON_TRIGGER_ATTRIBUTE = aiTryOnContract.markers["0"].name;
|
|
209
|
+
const AI_TRY_ON_TRIGGER_SELECTOR = triggerSelector(aiTryOnContract);
|
|
210
|
+
const AI_TRY_ON_LANGUAGE_ATTRIBUTE = attributeName(aiTryOnContract, 'language');
|
|
211
|
+
const AI_TRY_ON_PRODUCT_IMAGE_SRC_ATTRIBUTE = attributeName(aiTryOnContract, 'productImageSrc');
|
|
212
|
+
const AI_TRY_ON_PRODUCT_NAME_ATTRIBUTE = attributeName(aiTryOnContract, 'productName');
|
|
213
|
+
const AI_TRY_ON_PRODUCT_PRICE_ATTRIBUTE = attributeName(aiTryOnContract, 'productPrice');
|
|
214
|
+
const AI_TRY_ON_PRODUCT_URL_ATTRIBUTE = attributeName(aiTryOnContract, 'productUrl');
|
|
215
|
+
const AI_TRY_ON_PRODUCT_TYPE_ATTRIBUTE = attributeName(aiTryOnContract, 'productType');
|
|
216
|
+
attributeName(aiTryOnContract, 'apiKey');
|
|
94
217
|
class src_AiTryOnDialog {
|
|
95
218
|
destroyed = false;
|
|
96
219
|
props;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { AiTryOnDialog } from '@neurodyn/react-ai-try-on';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
|
|
4
|
+
export declare const AI_TRY_ON_EMBED_SCRIPT_URL = "https://cdn.neurodyn.ai/@neurodyn/ai-try-on/dist/ai-try-on.embed.min.js";
|
|
5
|
+
|
|
6
|
+
export declare const AI_TRY_ON_LANGUAGES: readonly ["en", "ru"];
|
|
7
|
+
|
|
8
|
+
export declare const AI_TRY_ON_PRODUCT_TYPES: readonly ["full-body", "upper-body", "lower-body", "footwear", "headwear", "eyewear", "neck", "wrist", "bag", "waist"];
|
|
9
|
+
|
|
10
|
+
/** The eyewear trigger the docs show as an example. */
|
|
11
|
+
export declare const AI_TRY_ON_TRIGGER_EXAMPLE: {
|
|
12
|
+
readonly productType: "eyewear";
|
|
13
|
+
readonly productName: "Sample Glasses";
|
|
14
|
+
readonly productImageSrc: "https://example.com/products/glasses.jpg";
|
|
15
|
+
readonly apiKey: "...";
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export declare const aiTryOnContract: TriggerContract<AiTryOnTriggerDialogProps, {
|
|
19
|
+
readonly productType: TriggerAttribute<"full-body" | "upper-body" | "lower-body" | "footwear" | "headwear" | "eyewear" | "neck" | "wrist" | "bag" | "waist">;
|
|
20
|
+
readonly productName: TriggerAttribute<string>;
|
|
21
|
+
readonly productImageSrc: TriggerAttribute<string>;
|
|
22
|
+
readonly productPrice: TriggerAttribute<string | undefined>;
|
|
23
|
+
readonly productUrl: TriggerAttribute<string | undefined>;
|
|
24
|
+
readonly language: TriggerAttribute<"en" | "ru" | undefined>;
|
|
25
|
+
readonly apiKey: TriggerAttribute<string | undefined>;
|
|
26
|
+
readonly open: OptionOnly;
|
|
27
|
+
readonly defaultOpen: OptionOnly;
|
|
28
|
+
readonly onOpenChange: OptionOnly;
|
|
29
|
+
readonly externalGeneration: OptionOnly;
|
|
30
|
+
readonly externalActivation: OptionOnly;
|
|
31
|
+
readonly externalClient: OptionOnly;
|
|
32
|
+
readonly externalAddToCart: OptionOnly;
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The React dialog's props in ordinary mode, which is what the trigger
|
|
37
|
+
* renders. External mode (the Shopify block) has a contract of its own.
|
|
38
|
+
*/
|
|
39
|
+
export declare interface AiTryOnTriggerDialogProps extends Extract<ComponentProps<typeof AiTryOnDialog>, {
|
|
40
|
+
externalGeneration?: never;
|
|
41
|
+
}> {
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export declare type AiTryOnTriggerRow = TriggerSnippetRow<keyof AiTryOnTriggerValues>;
|
|
45
|
+
|
|
46
|
+
export declare type AiTryOnTriggerValues = TriggerSnippetValues<typeof aiTryOnContract.fields>;
|
|
47
|
+
|
|
48
|
+
/** What the reader hands a parser besides the raw value. */
|
|
49
|
+
declare interface AttributeParseContext {
|
|
50
|
+
/** Full attribute name, e.g. `data-neurodyn-room-viewer-api-key`. */
|
|
51
|
+
attribute: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether an attribute must be set: always, never, or only while another
|
|
56
|
+
* attribute holds one of the listed values (`src` while `type` is `wallart`).
|
|
57
|
+
* `when` names a field of the same contract.
|
|
58
|
+
*/
|
|
59
|
+
declare type AttributeRequirement
|
|
60
|
+
= | boolean
|
|
61
|
+
| { readonly when: string, readonly is: readonly string[] }
|
|
62
|
+
|
|
63
|
+
/** A copy-paste trigger button with the given attributes, in contract order and escaped. */
|
|
64
|
+
export declare function buildAiTryOnTrigger(values: AiTryOnTriggerValues, options: TriggerSnippetOptions): string;
|
|
65
|
+
|
|
66
|
+
/** The tag that loads the self-initializing embed build from `src`. */
|
|
67
|
+
export declare function buildEmbedScript(src: string): string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The rows describing a trigger built from the same values, for the table
|
|
71
|
+
* beside the snippet; `extra` adds attributes the snippet leaves out.
|
|
72
|
+
*/
|
|
73
|
+
export declare function describeAiTryOnTrigger(values: AiTryOnTriggerValues, extra?: readonly (keyof AiTryOnTriggerValues)[]): AiTryOnTriggerRow[];
|
|
74
|
+
|
|
75
|
+
/** A widget option that has no attribute: callbacks, controlled state, sessions. */
|
|
76
|
+
declare interface OptionOnly {
|
|
77
|
+
readonly kind: 'option'
|
|
78
|
+
/** Why the option is programmatic only. */
|
|
79
|
+
readonly reason: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What an attribute's parser produces. */
|
|
83
|
+
declare type ParsedAttributeValue<Field> = Field extends TriggerAttribute<infer Value> ? Value : never
|
|
84
|
+
|
|
85
|
+
/** A widget option a merchant can set with a `data-*` attribute on the trigger. */
|
|
86
|
+
declare interface TriggerAttribute<Value> {
|
|
87
|
+
readonly kind: 'attribute'
|
|
88
|
+
/** Attribute name without the widget prefix, e.g. `api-key`. */
|
|
89
|
+
readonly name: string
|
|
90
|
+
readonly parse: (raw: string | null, context: AttributeParseContext) => Value
|
|
91
|
+
/** The accepted values, listed in the docs. */
|
|
92
|
+
readonly values?: readonly string[]
|
|
93
|
+
/** What the docs and the cabinets' snippets ask for; the reader does not enforce it. */
|
|
94
|
+
readonly required: AttributeRequirement
|
|
95
|
+
/** The default as Markdown for the docs, e.g. `` `cm` `` or `User locale`. */
|
|
96
|
+
readonly defaultValue?: string
|
|
97
|
+
/** One or two sentences for the docs; accepted values are appended from `values`. */
|
|
98
|
+
readonly description: string
|
|
99
|
+
/** Full attribute names read, in order, while the primary one is absent or blank. */
|
|
100
|
+
readonly legacyNames?: readonly string[]
|
|
101
|
+
/** Runs an explicitly passed programmatic option through `parse` as well. */
|
|
102
|
+
readonly normalizesOptions?: boolean
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Keys of the fields that are attributes. */
|
|
106
|
+
declare type TriggerAttributeKey<Fields> = {
|
|
107
|
+
[Key in keyof Fields]: Fields[Key] extends TriggerAttribute<any> ? Key : never
|
|
108
|
+
}[keyof Fields] & string
|
|
109
|
+
|
|
110
|
+
declare interface TriggerContract<Options, Fields extends TriggerFields<Options> = TriggerFields<Options>> {
|
|
111
|
+
/** Prefix shared by the widget's option attributes, e.g. `data-neurodyn-room-viewer-`. */
|
|
112
|
+
readonly prefix: string
|
|
113
|
+
/** The first marker is the one snippets use. */
|
|
114
|
+
readonly markers: readonly [TriggerMarker, ...TriggerMarker[]]
|
|
115
|
+
readonly fields: Fields
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* One entry per widget option, none optional: an option added to the widget's
|
|
120
|
+
* props without an entry here fails type-checking until it is given an
|
|
121
|
+
* attribute or marked option-only.
|
|
122
|
+
*/
|
|
123
|
+
declare type TriggerFields<Options> = {
|
|
124
|
+
readonly [Key in keyof Options]-?: TriggerAttribute<Options[Key] | undefined> | OptionOnly
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A marker attribute that selects trigger elements, e.g. `data-neurodyn-room-viewer-dialog`. */
|
|
128
|
+
declare interface TriggerMarker {
|
|
129
|
+
readonly name: string
|
|
130
|
+
readonly description: string
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export declare interface TriggerSnippetOptions {
|
|
134
|
+
/** The button text. */
|
|
135
|
+
label: string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** One attribute of a snippet, for the table next to it. */
|
|
139
|
+
declare interface TriggerSnippetRow<Key extends string> {
|
|
140
|
+
/** The field key, or `null` for the marker attribute. */
|
|
141
|
+
key: Key | null
|
|
142
|
+
attribute: string
|
|
143
|
+
required: boolean
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Values for a copy-paste trigger: only the contract's attributes, each typed
|
|
148
|
+
* as its option. Placeholders such as `PUBLIC_WIDGET_KEY` fit string options.
|
|
149
|
+
*/
|
|
150
|
+
declare type TriggerSnippetValues<Fields> = {
|
|
151
|
+
[Key in TriggerAttributeKey<Fields>]?: Exclude<ParsedAttributeValue<Fields[Key]>, undefined | null>
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export { }
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
function contract_attribute(name, parser, details) {
|
|
2
|
+
return {
|
|
3
|
+
kind: 'attribute',
|
|
4
|
+
name,
|
|
5
|
+
parse: parser.parse,
|
|
6
|
+
values: parser.values,
|
|
7
|
+
...details
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function optionOnly(reason) {
|
|
11
|
+
return {
|
|
12
|
+
kind: 'option',
|
|
13
|
+
reason
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function defineTriggerContract() {
|
|
17
|
+
return (contract)=>contract;
|
|
18
|
+
}
|
|
19
|
+
function contract_attributeEntries(contract) {
|
|
20
|
+
return Object.entries(contract.fields).filter((entry)=>'attribute' === entry[1].kind);
|
|
21
|
+
}
|
|
22
|
+
function escapeHtml(value) {
|
|
23
|
+
return value.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
|
|
24
|
+
}
|
|
25
|
+
function isAttributeRequired(requirement, values) {
|
|
26
|
+
if ('boolean' == typeof requirement) return requirement;
|
|
27
|
+
const current = values[requirement.when];
|
|
28
|
+
return null != current && requirement.is.includes(String(current));
|
|
29
|
+
}
|
|
30
|
+
function snippetEntries(contract, values) {
|
|
31
|
+
return contract_attributeEntries(contract).flatMap(([key, field])=>{
|
|
32
|
+
const value = values[key];
|
|
33
|
+
return null == value ? [] : [
|
|
34
|
+
{
|
|
35
|
+
key,
|
|
36
|
+
field,
|
|
37
|
+
value
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function snippet_buildTriggerSnippet(contract, values, { label }) {
|
|
43
|
+
const lines = [
|
|
44
|
+
'<button',
|
|
45
|
+
' type="button"',
|
|
46
|
+
` ${contract.markers[0].name}`
|
|
47
|
+
];
|
|
48
|
+
for (const { field, value } of snippetEntries(contract, values))lines.push(` ${contract.prefix}${field.name}="${escapeHtml(String(value))}"`);
|
|
49
|
+
lines.push('>', ` ${escapeHtml(label)}`, '</button>');
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
function describeTriggerSnippet(contract, values, extra = []) {
|
|
53
|
+
const present = snippetEntries(contract, values);
|
|
54
|
+
const presentKeys = new Set(present.map(({ key })=>key));
|
|
55
|
+
const fields = contract.fields;
|
|
56
|
+
const row = (key, field)=>({
|
|
57
|
+
key,
|
|
58
|
+
attribute: `${contract.prefix}${field.name}`,
|
|
59
|
+
required: isAttributeRequired(field.required, values)
|
|
60
|
+
});
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
key: null,
|
|
64
|
+
attribute: contract.markers[0].name,
|
|
65
|
+
required: true
|
|
66
|
+
},
|
|
67
|
+
...present.map(({ key, field })=>row(key, field)),
|
|
68
|
+
...extra.filter((key)=>!presentKeys.has(key)).map((key)=>row(key, fields[key]))
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
function snippet_buildEmbedScript(src) {
|
|
72
|
+
return `<script defer src="${escapeHtml(src)}"></script>`;
|
|
73
|
+
}
|
|
74
|
+
function trimmedText() {
|
|
75
|
+
return {
|
|
76
|
+
parse: (raw)=>raw?.trim() || void 0
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function requiredText(missing) {
|
|
80
|
+
return {
|
|
81
|
+
parse: (raw, { attribute })=>{
|
|
82
|
+
const value = raw?.trim();
|
|
83
|
+
if (!value) throw missing(attribute);
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function oneOf(values) {
|
|
89
|
+
return {
|
|
90
|
+
parse: (raw)=>values.includes(raw ?? '') ? raw : void 0,
|
|
91
|
+
values
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function custom(parse, values) {
|
|
95
|
+
return {
|
|
96
|
+
parse,
|
|
97
|
+
values
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function listOf() {
|
|
101
|
+
return (values)=>values;
|
|
102
|
+
}
|
|
103
|
+
const AI_TRY_ON_PRODUCT_TYPES = listOf()([
|
|
104
|
+
'full-body',
|
|
105
|
+
'upper-body',
|
|
106
|
+
'lower-body',
|
|
107
|
+
'footwear',
|
|
108
|
+
'headwear',
|
|
109
|
+
'eyewear',
|
|
110
|
+
'neck',
|
|
111
|
+
'wrist',
|
|
112
|
+
'bag',
|
|
113
|
+
'waist'
|
|
114
|
+
]);
|
|
115
|
+
const AI_TRY_ON_LANGUAGES = listOf()([
|
|
116
|
+
'en',
|
|
117
|
+
'ru'
|
|
118
|
+
]);
|
|
119
|
+
const AI_TRY_ON_EMBED_SCRIPT_URL = 'https://cdn.neurodyn.ai/@neurodyn/ai-try-on/dist/ai-try-on.embed.min.js';
|
|
120
|
+
function isProductType(value) {
|
|
121
|
+
return 'string' == typeof value && AI_TRY_ON_PRODUCT_TYPES.includes(value);
|
|
122
|
+
}
|
|
123
|
+
const EXTERNAL_ONLY = 'External integrations only, such as the Shopify block.';
|
|
124
|
+
const aiTryOnContract = defineTriggerContract()({
|
|
125
|
+
prefix: 'data-neurodyn-ai-try-on-',
|
|
126
|
+
markers: [
|
|
127
|
+
{
|
|
128
|
+
name: 'data-neurodyn-ai-try-on-dialog',
|
|
129
|
+
description: 'Marks a dialog trigger for the embed build.'
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
fields: {
|
|
133
|
+
productType: contract_attribute('product-type', custom((value, { attribute })=>{
|
|
134
|
+
if (!isProductType(value)) {
|
|
135
|
+
const received = 'string' == typeof value && value.length > 0 ? ` "${value}"` : '';
|
|
136
|
+
throw new Error(`AI try-on product type from ${attribute} must be one of: ${AI_TRY_ON_PRODUCT_TYPES.join(', ')}; received${received || ' no value'}`);
|
|
137
|
+
}
|
|
138
|
+
return value;
|
|
139
|
+
}, AI_TRY_ON_PRODUCT_TYPES), {
|
|
140
|
+
required: true,
|
|
141
|
+
normalizesOptions: true,
|
|
142
|
+
description: 'Product category.'
|
|
143
|
+
}),
|
|
144
|
+
productName: contract_attribute('product-name', requiredText((name)=>new Error(`AI try-on dialog trigger is missing ${name}`)), {
|
|
145
|
+
required: true,
|
|
146
|
+
description: 'Product name shown in the dialog.'
|
|
147
|
+
}),
|
|
148
|
+
productImageSrc: contract_attribute('product-image-src', requiredText((name)=>new Error(`AI try-on dialog trigger is missing ${name}`)), {
|
|
149
|
+
required: true,
|
|
150
|
+
description: 'Product image URL.'
|
|
151
|
+
}),
|
|
152
|
+
productPrice: contract_attribute('product-price', trimmedText(), {
|
|
153
|
+
required: false,
|
|
154
|
+
normalizesOptions: true,
|
|
155
|
+
description: 'Already-formatted product price.'
|
|
156
|
+
}),
|
|
157
|
+
productUrl: contract_attribute('product-url', trimmedText(), {
|
|
158
|
+
required: false,
|
|
159
|
+
description: 'Product page URL opened from the result.'
|
|
160
|
+
}),
|
|
161
|
+
language: contract_attribute('language', oneOf(AI_TRY_ON_LANGUAGES), {
|
|
162
|
+
required: false,
|
|
163
|
+
defaultValue: 'Browser language',
|
|
164
|
+
description: 'UI language override.'
|
|
165
|
+
}),
|
|
166
|
+
apiKey: contract_attribute('api-key', trimmedText(), {
|
|
167
|
+
required: true,
|
|
168
|
+
normalizesOptions: true,
|
|
169
|
+
description: 'Public API key.'
|
|
170
|
+
}),
|
|
171
|
+
open: optionOnly('The embed build owns the open state.'),
|
|
172
|
+
defaultOpen: optionOnly('The embed build owns the open state.'),
|
|
173
|
+
onOpenChange: optionOnly('The embed build owns the open state.'),
|
|
174
|
+
externalGeneration: optionOnly(EXTERNAL_ONLY),
|
|
175
|
+
externalActivation: optionOnly(EXTERNAL_ONLY),
|
|
176
|
+
externalClient: optionOnly(EXTERNAL_ONLY),
|
|
177
|
+
externalAddToCart: optionOnly(EXTERNAL_ONLY)
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
const AI_TRY_ON_TRIGGER_EXAMPLE = {
|
|
181
|
+
productType: 'eyewear',
|
|
182
|
+
productName: 'Sample Glasses',
|
|
183
|
+
productImageSrc: 'https://example.com/products/glasses.jpg',
|
|
184
|
+
apiKey: '...'
|
|
185
|
+
};
|
|
186
|
+
function buildAiTryOnTrigger(values, options) {
|
|
187
|
+
return snippet_buildTriggerSnippet(aiTryOnContract, values, options);
|
|
188
|
+
}
|
|
189
|
+
function describeAiTryOnTrigger(values, extra = []) {
|
|
190
|
+
return describeTriggerSnippet(aiTryOnContract, values, extra);
|
|
191
|
+
}
|
|
192
|
+
function contract_buildEmbedScript(src) {
|
|
193
|
+
return snippet_buildEmbedScript(src);
|
|
194
|
+
}
|
|
195
|
+
export { AI_TRY_ON_EMBED_SCRIPT_URL, AI_TRY_ON_LANGUAGES, AI_TRY_ON_PRODUCT_TYPES, AI_TRY_ON_TRIGGER_EXAMPLE, aiTryOnContract, buildAiTryOnTrigger, contract_buildEmbedScript as buildEmbedScript, describeAiTryOnTrigger };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { PRODUCT_TYPES } from '@neurodyn/react-ai-try-on';
|
|
2
2
|
import { ProductType } from '@neurodyn/react-ai-try-on';
|
|
3
3
|
|
|
4
|
-
export declare const AI_TRY_ON_LANGUAGE_ATTRIBUTE
|
|
4
|
+
export declare const AI_TRY_ON_LANGUAGE_ATTRIBUTE: string;
|
|
5
5
|
|
|
6
|
-
export declare const AI_TRY_ON_PRODUCT_IMAGE_SRC_ATTRIBUTE
|
|
6
|
+
export declare const AI_TRY_ON_PRODUCT_IMAGE_SRC_ATTRIBUTE: string;
|
|
7
7
|
|
|
8
|
-
export declare const AI_TRY_ON_PRODUCT_NAME_ATTRIBUTE
|
|
8
|
+
export declare const AI_TRY_ON_PRODUCT_NAME_ATTRIBUTE: string;
|
|
9
9
|
|
|
10
|
-
export declare const AI_TRY_ON_PRODUCT_PRICE_ATTRIBUTE
|
|
10
|
+
export declare const AI_TRY_ON_PRODUCT_PRICE_ATTRIBUTE: string;
|
|
11
11
|
|
|
12
12
|
/** Canonical Product Type driving per-type generation. */
|
|
13
|
-
export declare const AI_TRY_ON_PRODUCT_TYPE_ATTRIBUTE
|
|
13
|
+
export declare const AI_TRY_ON_PRODUCT_TYPE_ATTRIBUTE: string;
|
|
14
14
|
|
|
15
|
-
export declare const AI_TRY_ON_PRODUCT_URL_ATTRIBUTE
|
|
15
|
+
export declare const AI_TRY_ON_PRODUCT_URL_ATTRIBUTE: string;
|
|
16
16
|
|
|
17
|
-
export declare const AI_TRY_ON_TRIGGER_ATTRIBUTE
|
|
17
|
+
export declare const AI_TRY_ON_TRIGGER_ATTRIBUTE: string;
|
|
18
18
|
|
|
19
|
-
export declare const AI_TRY_ON_TRIGGER_SELECTOR
|
|
19
|
+
export declare const AI_TRY_ON_TRIGGER_SELECTOR: string;
|
|
20
20
|
|
|
21
21
|
export declare class AiTryOnDialog {
|
|
22
22
|
private destroyed;
|