mk-sdk-git 0.1.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.
@@ -0,0 +1,210 @@
1
+ // bridge.js — módulo de try-on. Abre o iframe do visualizer, escuta postMessages
2
+ // e emite eventos de tracking. API pública exposta em window.mk.
3
+
4
+ import { getConfig, log } from '../core/config.js';
5
+ import { track } from '../core/transport.js';
6
+ import { addToCart as cartAdd } from '../platforms/index.js';
7
+
8
+ let modal = null;
9
+ let iframe = null;
10
+ let isOpen = false;
11
+ let currentConfig = null;
12
+ let availabilityChecked = {};
13
+ let photoMethod = null; // qrcode | gallery | camera (recebido via interaction)
14
+ let generationStartedAt = 0;
15
+
16
+ const callbacks = {
17
+ onReady: null, onClose: null, onAddToCart: null,
18
+ onProductLoaded: null, onError: null, onInteraction: null, onGenerationComplete: null
19
+ };
20
+
21
+ const STORE_ALIASES = { gregory: '698c7e791d3129430f15dddd' };
22
+ function resolveProjectId(p) {
23
+ if (!p) return null;
24
+ return STORE_ALIASES[String(p).toLowerCase()] || p;
25
+ }
26
+
27
+ // ─── API de disponibilidade ───────────────────────────────────────────────
28
+ export async function getAvailability(projectId, identifier) {
29
+ const cfg = getConfig();
30
+ const resolved = resolveProjectId(projectId);
31
+ identifier = String(identifier || '').trim();
32
+ const url = cfg.apiUrl + '/availability/' + resolved + '?sku=' + encodeURIComponent(identifier);
33
+ const resp = await fetch(url);
34
+ if (!resp.ok) throw new Error('availability HTTP ' + resp.status);
35
+ return resp.json();
36
+ }
37
+
38
+ export async function isAvailable(projectId, identifier) {
39
+ if (!identifier) { identifier = projectId; projectId = '698c7e791d3129430f15dddd'; }
40
+ try {
41
+ const data = await getAvailability(projectId, identifier);
42
+ return data.available === true;
43
+ } catch (_) { return false; }
44
+ }
45
+
46
+ // ─── Abrir / fechar modal ─────────────────────────────────────────────────
47
+ export async function open(options) {
48
+ options = options || {};
49
+ if (isOpen) return;
50
+
51
+ const projectId = resolveProjectId(options.projectId || options.projectid || options.store);
52
+ const identifier = String(options.identifier || options.sku || '').trim();
53
+ if (!projectId || !identifier) {
54
+ console.error('[mk-sdk] open: projectId e identifier obrigatorios');
55
+ return;
56
+ }
57
+
58
+ // valida disponibilidade (pula se init já validou)
59
+ const key = projectId + ':' + identifier;
60
+ if (availabilityChecked[key] !== true) {
61
+ try {
62
+ const a = await getAvailability(projectId, identifier);
63
+ if (!a.available) { alert('Produto nao disponivel para prova virtual.'); return; }
64
+ } catch (_) { alert('Produto nao encontrado ou indisponivel.'); return; }
65
+ availabilityChecked[key] = true;
66
+ }
67
+
68
+ currentConfig = { projectId, identifier, width: options.width || 430, height: options.height || 800 };
69
+ setupMessageListener();
70
+ openModal(currentConfig);
71
+ isOpen = true;
72
+ generationStartedAt = Date.now();
73
+
74
+ track('modal_opened', { projectId, identifier });
75
+ }
76
+
77
+ export function close() {
78
+ if (!isOpen) return;
79
+ hideModal();
80
+ isOpen = false;
81
+ fire('onClose');
82
+ track('modal_closed', { projectId: currentConfig && currentConfig.projectId, identifier: currentConfig && currentConfig.identifier });
83
+ }
84
+
85
+ // ─── Modal DOM ────────────────────────────────────────────────────────────
86
+ function openModal(cfg) {
87
+ modal = document.createElement('div');
88
+ modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.8);display:flex;align-items:center;justify-content:center;z-index:99999;';
89
+
90
+ const container = document.createElement('div');
91
+ const w = typeof cfg.width === 'number' ? cfg.width + 'px' : cfg.width;
92
+ const h = typeof cfg.height === 'number' ? cfg.height + 'px' : cfg.height;
93
+ container.style.cssText = 'position:relative;width:' + w + ';height:' + h + ';max-width:100vw;max-height:100dvh;border-radius:12px;overflow:hidden;background:#fff;';
94
+
95
+ iframe = document.createElement('iframe');
96
+ const encoded = encodeURIComponent(cfg.identifier).replace(/\./g, '%2E');
97
+ iframe.src = getConfig().appUrl + '/' + cfg.projectId + '/' + encoded;
98
+ iframe.style.cssText = 'width:100%;height:100%;border:none;';
99
+ iframe.setAttribute('allow', 'camera *; microphone *');
100
+ iframe.setAttribute('allowfullscreen', 'true');
101
+
102
+ container.appendChild(iframe);
103
+ modal.appendChild(container);
104
+ document.body.appendChild(modal);
105
+
106
+ log('try-on modal aberto', iframe.src);
107
+ }
108
+
109
+ function hideModal() {
110
+ if (modal && modal.parentNode) modal.parentNode.removeChild(modal);
111
+ modal = null;
112
+ iframe = null;
113
+ }
114
+
115
+ // ─── postMessage listener — capta eventos do iframe MK ────────────────────
116
+ let listenerActive = false;
117
+ // Ativa o listener de postMessages do provador no boot (não só ao abrir o modal),
118
+ // pra captar add_to_cart mesmo que o fluxo de abertura varie.
119
+ export function initListener() { setupMessageListener(); }
120
+ function setupMessageListener() {
121
+ if (listenerActive) return;
122
+ listenerActive = true;
123
+ window.addEventListener('message', (event) => {
124
+ const data = event.data;
125
+ if (!data || data.source !== 'mkfashion-app') return;
126
+ const action = data.action;
127
+ const payload = data.data || {};
128
+
129
+ switch (action) {
130
+ case 'close_request':
131
+ close();
132
+ break;
133
+ case 'ready':
134
+ fire('onReady', payload);
135
+ // page_ready é redundante com modal_opened — não emitimos
136
+ break;
137
+ case 'product_loaded':
138
+ fire('onProductLoaded', payload);
139
+ track('product_loaded', stripImg(payload));
140
+ break;
141
+ case 'interaction': {
142
+ fire('onInteraction', payload);
143
+ // captura method quando o iframe avisa
144
+ if (payload && payload.method) photoMethod = payload.method;
145
+ track(interactionName(payload), stripImg(payload));
146
+ break;
147
+ }
148
+ case 'add_to_cart':
149
+ track('add_to_cart', Object.assign({ via: 'tryon' }, stripImg(payload)));
150
+ if (typeof callbacks.onAddToCart === 'function') {
151
+ fire('onAddToCart', payload); // cliente cuida (compat SDK v2)
152
+ } else {
153
+ cartAdd(payload).catch(() => {}); // mk-sdk adiciona sozinho (zero-touch)
154
+ }
155
+ break;
156
+ case 'generation_start':
157
+ generationStartedAt = Date.now();
158
+ break;
159
+ case 'generation_complete':
160
+ fire('onGenerationComplete', payload); // cliente recebe imageUrl completo
161
+ track('generation_complete', {
162
+ method: payload.method || photoMethod || 'unknown',
163
+ durationMs: generationStartedAt ? (Date.now() - generationStartedAt) : null,
164
+ success: payload.success !== false,
165
+ imageUrlPresent: !!payload.imageUrl
166
+ });
167
+ break;
168
+ case 'generation_error':
169
+ fire('onError', payload);
170
+ track('generation_error', { method: photoMethod || 'unknown', error: payload && payload.error });
171
+ break;
172
+ }
173
+ });
174
+ }
175
+
176
+ // Eventos de interaction viram page_<category>_<action> ou page_interaction
177
+ function interactionName(payload) {
178
+ if (payload && payload.action) return 'interaction'; // mantém genérico, action vai no params
179
+ return 'interaction';
180
+ }
181
+
182
+ // Remove imageUrl pesado antes de track (sanitize do core também faz, mas garantimos)
183
+ function stripImg(obj) {
184
+ if (!obj || typeof obj !== 'object') return obj;
185
+ const out = {};
186
+ for (const k in obj) {
187
+ if (k === 'imageUrl' || k === 'image' || k === 'product') {
188
+ if (k === 'product' && obj[k]) { out.productSku = obj[k].sku || null; out.productName = obj[k].name || null; }
189
+ else out[k + 'Present'] = !!obj[k];
190
+ continue;
191
+ }
192
+ out[k] = obj[k];
193
+ }
194
+ return out;
195
+ }
196
+
197
+ function fire(name, data) {
198
+ if (typeof callbacks[name] === 'function') {
199
+ try { callbacks[name](data); } catch (e) { console.error('[mk-sdk]', e); }
200
+ }
201
+ }
202
+
203
+ // ─── Registradores de callback (compat API v2) ────────────────────────────
204
+ export function onReady(cb) { callbacks.onReady = cb; }
205
+ export function onClose(cb) { callbacks.onClose = cb; }
206
+ export function onAddToCart(cb) { callbacks.onAddToCart = cb; }
207
+ export function onProductLoaded(cb) { callbacks.onProductLoaded = cb; }
208
+ export function onError(cb) { callbacks.onError = cb; }
209
+ export function onInteraction(cb) { callbacks.onInteraction = cb; }
210
+ export function onGenerationComplete(cb) { callbacks.onGenerationComplete = cb; }
@@ -0,0 +1,56 @@
1
+ // config.js (buttons) — defaults, template server-driven e normalização.
2
+ // Portado da mkfashion-sdk (_BUTTON_DEFAULTS, _fetchButtonTemplate, _normalizeButtonConfig).
3
+
4
+ import { BUTTON_RADIUS, CARD_RADIUS } from './helpers.js';
5
+
6
+ export const BUTTON_DEFAULTS = {
7
+ style: 'gregory-black',
8
+ text: 'Provar Virtualmente',
9
+ fontSize: 14,
10
+ subtext: '',
11
+ bgColor: '#1E1E1E',
12
+ textColor: '#FFFFFF',
13
+ badgeBgColor: '#C51A1B',
14
+ badgeTextColor: '#FFFFFF',
15
+ borderRadius: 'md',
16
+ borderColor: '#1E1E1E',
17
+ borderWidth: 0,
18
+ cardTitle: '',
19
+ cardDescription: '',
20
+ cardFooter: '',
21
+ cardBgColor: '#F6F6F6',
22
+ cardTitleColor: '#1E1E1E',
23
+ cardTextColor: '#717171',
24
+ cardBorderRadius: 'md'
25
+ };
26
+
27
+ /**
28
+ * Busca o template do botão configurado por projeto (server-driven).
29
+ * GET {apiUrl}/projects/{projectId} → project.template.tryOnButton
30
+ * Retorna null em falha (o normalize cai nos defaults).
31
+ */
32
+ export async function fetchButtonTemplate(apiUrl, projectId) {
33
+ try {
34
+ const r = await fetch(`${apiUrl}/projects/${projectId}`);
35
+ if (!r.ok) throw new Error('HTTP ' + r.status);
36
+ const data = await r.json();
37
+ return (data && data.project && data.project.template && data.project.template.tryOnButton)
38
+ || (data && data.template && data.template.tryOnButton)
39
+ || null;
40
+ } catch (_) {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /** Escolhe config desktop/mobile, força gregory-black em mobile, aplica defaults, pré-resolve px. */
46
+ export function normalizeButtonConfig(template, isMobile) {
47
+ const raw = (isMobile ? (template && template.mobile) : (template && template.desktop))
48
+ || (template && template.desktop) || (template && template.mobile) || {};
49
+ const merged = { ...BUTTON_DEFAULTS, ...raw };
50
+ if (merged.style === 'gregory-card' && isMobile) merged.style = 'gregory-black';
51
+ merged.borderRadiusPx = BUTTON_RADIUS[merged.borderRadius] != null ? BUTTON_RADIUS[merged.borderRadius] : '0';
52
+ merged.cardBorderRadiusPx = CARD_RADIUS[merged.cardBorderRadius] != null ? CARD_RADIUS[merged.cardBorderRadius] : '0';
53
+ merged.fontSize = Number(merged.fontSize) || 14;
54
+ merged.borderWidth = Number(merged.borderWidth) || 0;
55
+ return merged;
56
+ }
@@ -0,0 +1,38 @@
1
+ // gregory-black.js — preset de botão simples (ícone + texto + badge opcional).
2
+ // Portado de _renderSimpleButton / _renderBlackBadge da mkfashion-sdk.
3
+
4
+ import { el, cameraIcon, borderFrom, ensureJakartaFont, FONT_STACK } from './helpers.js';
5
+
6
+ function renderBadge(c) {
7
+ if (!c.subtext) return null;
8
+ return el('span', {
9
+ style: `display:flex;align-items:center;justify-content:center;padding:4px;background:${c.badgeBgColor};font-family:${FONT_STACK};font-weight:700;font-size:16px;text-transform:uppercase;color:${c.badgeTextColor};line-height:1;white-space:nowrap;`,
10
+ text: c.subtext
11
+ });
12
+ }
13
+
14
+ /** render(config, onClick) → HTMLButtonElement */
15
+ export function render(c, onClick) {
16
+ ensureJakartaFont();
17
+ const content = el('span', {
18
+ style: 'display:inline-flex;align-items:center;gap:4px;',
19
+ children: [
20
+ cameraIcon(c.textColor),
21
+ el('span', {
22
+ text: c.text,
23
+ style: `font-family:${FONT_STACK};font-weight:700;font-size:${c.fontSize}px;text-transform:uppercase;letter-spacing:2px;color:${c.textColor};white-space:nowrap;text-align:center;line-height:1;`
24
+ })
25
+ ]
26
+ });
27
+
28
+ return el('button', {
29
+ attrs: { type: 'button' },
30
+ style: `display:flex;align-items:center;justify-content:center;gap:8px;padding:8px;width:100%;max-width:238px;box-sizing:border-box;background:${c.bgColor};border:${borderFrom(c)};border-radius:${c.borderRadiusPx};font-family:${FONT_STACK};cursor:pointer;transition:opacity 0.2s ease;line-height:1;-webkit-appearance:none;appearance:none;`,
31
+ children: [content, renderBadge(c)],
32
+ on: {
33
+ mouseenter: (e) => { e.currentTarget.style.opacity = '0.85'; },
34
+ mouseleave: (e) => { e.currentTarget.style.opacity = '1'; },
35
+ click: () => { try { onClick && onClick(); } catch (_) {} }
36
+ }
37
+ });
38
+ }
@@ -0,0 +1,76 @@
1
+ // gregory-card.js — preset de card (título + descrição + CTA + badge).
2
+ // Portado de _renderCardButton / _renderCardBadge / _renderCardCta da mkfashion-sdk.
3
+
4
+ import { el, cameraIcon, sparkleIcon, borderFrom, ensureJakartaFont, FONT_STACK } from './helpers.js';
5
+
6
+ function renderBadge(c) {
7
+ if (!c.subtext) return null;
8
+ return el('span', {
9
+ style: `display:flex;align-items:center;gap:4px;padding:4px;background:${c.badgeBgColor};font-family:${FONT_STACK};font-weight:600;font-size:10px;text-transform:uppercase;color:${c.badgeTextColor};line-height:1;white-space:nowrap;`,
10
+ children: [sparkleIcon(c.badgeTextColor), el('span', { text: c.subtext })]
11
+ });
12
+ }
13
+
14
+ function renderCta(c, onClick) {
15
+ return el('button', {
16
+ attrs: { type: 'button' },
17
+ style: `display:flex;align-items:center;justify-content:center;gap:8px;padding:8px 12px 8px 8px;width:100%;margin-top:auto;box-sizing:border-box;background:${c.bgColor};border:${borderFrom(c)};border-radius:${c.borderRadiusPx};filter:drop-shadow(0 0 6px rgba(0,0,0,0.08));font-family:${FONT_STACK};cursor:pointer;transition:opacity 0.2s ease;line-height:1;-webkit-appearance:none;appearance:none;`,
18
+ children: [
19
+ cameraIcon(c.textColor),
20
+ el('span', {
21
+ text: c.text,
22
+ style: `font-family:${FONT_STACK};font-weight:700;font-size:${c.fontSize}px;color:${c.textColor};white-space:nowrap;line-height:1;`
23
+ })
24
+ ],
25
+ on: {
26
+ mouseenter: (e) => { e.currentTarget.style.opacity = '0.85'; },
27
+ mouseleave: (e) => { e.currentTarget.style.opacity = '1'; },
28
+ click: () => { try { onClick && onClick(); } catch (_) {} }
29
+ }
30
+ });
31
+ }
32
+
33
+ /** render(config, onClick) → HTMLDivElement (card) */
34
+ export function render(c, onClick) {
35
+ ensureJakartaFont();
36
+
37
+ const titleBlock = el('div', {
38
+ style: 'display:flex;flex-direction:column;gap:4px;',
39
+ children: [
40
+ c.cardTitle && el('p', {
41
+ text: c.cardTitle,
42
+ style: `margin:0;font-family:${FONT_STACK};font-weight:700;font-size:16px;letter-spacing:1px;text-transform:uppercase;color:${c.cardTitleColor};line-height:1.2;`
43
+ }),
44
+ c.cardDescription && el('p', {
45
+ html: c.cardDescription,
46
+ style: `margin:0;font-family:${FONT_STACK};font-weight:400;font-size:12px;max-width:177px;line-height:1.2;color:${c.cardTextColor};`
47
+ })
48
+ ]
49
+ });
50
+
51
+ const details = el('div', {
52
+ style: 'display:flex;flex-direction:column;justify-content:space-between;gap:16px;flex:1 1 0;min-width:0;',
53
+ children: [
54
+ titleBlock,
55
+ c.cardFooter && el('p', {
56
+ text: c.cardFooter,
57
+ style: `margin:0;font-family:${FONT_STACK};font-weight:400;font-size:10px;color:${c.cardTextColor};line-height:1.2;`
58
+ })
59
+ ]
60
+ });
61
+
62
+ const right = el('div', {
63
+ style: 'display:flex;flex-direction:column;align-items:flex-end;flex:1 1 0;min-width:0;',
64
+ children: [renderBadge(c), renderCta(c, onClick)]
65
+ });
66
+
67
+ const row = el('div', {
68
+ style: 'display:flex;gap:16px;align-items:stretch;width:100%;flex:1;box-sizing:border-box;',
69
+ children: [details, right]
70
+ });
71
+
72
+ return el('div', {
73
+ style: `display:flex;flex-direction:column;align-items:flex-start;padding:12px;width:100%;max-width:402px;min-height:140px;box-sizing:border-box;background:${c.cardBgColor};border-radius:${c.cardBorderRadiusPx};font-family:${FONT_STACK};`,
74
+ children: [row]
75
+ });
76
+ }
@@ -0,0 +1,57 @@
1
+ // helpers.js — utilitários compartilhados pelos renderizadores de botão.
2
+ // Portado da mkfashion-sdk (mkfashion.js, seção "BOTÃO TRY-ON").
3
+
4
+ export const FONT_STACK = "'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
5
+ export const BUTTON_RADIUS = { none: '0', sm: '4px', md: '8px', pill: '9999px' };
6
+ export const CARD_RADIUS = { none: '0', sm: '4px', md: '8px', lg: '16px' };
7
+
8
+ const CAMERA_ICON_SVG = '<svg viewBox="0 0 17.5263 20.6862" fill="none" xmlns="http://www.w3.org/2000/svg" style="display:block;width:100%;height:100%"><g><path d="M8.49995 12.1862C10.1568 12.1862 11.5 10.8431 11.5 9.18622C11.5 7.52936 10.1568 6.18622 8.49995 6.18622C6.8431 6.18622 5.49995 7.52936 5.49995 9.18622C5.49995 10.8431 6.8431 12.1862 8.49995 12.1862Z" stroke="currentColor" shape-rendering="crispEdges"/><path d="M0.5 9.11152C0.5 6.56953 0.5 5.29894 1.1099 4.38664C1.375 3.9902 1.71424 3.65088 2.10821 3.38809C2.6945 2.9958 3.42899 2.85564 4.55351 2.80588C5.09013 2.80588 5.55183 2.39949 5.65687 1.87284C5.73715 1.48708 5.94572 1.14138 6.24735 0.894155C6.54898 0.646934 6.92516 0.51336 7.31231 0.516006H9.9734C10.7779 0.516006 11.4709 1.08412 11.6288 1.87284C11.7339 2.39949 12.1956 2.80588 12.7322 2.80588C13.8559 2.85564 12.9129 4.67413 13.5 5.06559C13.8949 5.32932 15.1484 3.80355 15.5 4.02647C16.1099 4.93876 16.7857 6.56953 16.7857 9.11152C16.7857 11.6535 16.7857 12.9233 16.1758 13.8364C15.9107 14.2328 15.5715 14.5722 15.1775 14.835C14.2647 15.4445 12.9936 15.4445 10.4522 15.4445H6.83351C4.29213 15.4445 3.02103 15.4445 2.10821 14.835C1.71446 14.5718 1.3755 14.2322 1.11071 13.8356C0.933802 13.5672 0.803519 13.2698 0.725557 12.9564" stroke="currentColor" stroke-linecap="round"/><path d="M12.676 0.607931C12.9446 -0.178095 14.0307 -0.2019 14.3491 0.536515L14.3761 0.60838L14.7385 1.66839C14.8216 1.91149 14.9559 2.13395 15.1322 2.32076C15.3086 2.50757 15.5229 2.65439 15.7608 2.75131L15.8583 2.78769L16.9183 3.14971C17.7044 3.41831 17.7282 4.50437 16.9902 4.82283L16.9183 4.84978L15.8583 5.21225C15.6151 5.29526 15.3926 5.42947 15.2057 5.60582C15.0188 5.78218 14.8719 5.99657 14.7749 6.23453L14.7385 6.33155L14.3765 7.39201C14.1079 8.17803 13.0219 8.20184 12.7039 7.46387L12.676 7.39201L12.314 6.332C12.231 6.08881 12.0968 5.86627 11.9204 5.67938C11.744 5.49249 11.5297 5.3456 11.2917 5.24863L11.1947 5.21225L10.1347 4.85022C9.34817 4.58163 9.32437 3.49557 10.0628 3.17756L10.1347 3.14971L11.1947 2.78769C11.4378 2.70462 11.6602 2.57039 11.847 2.39404C12.0339 2.21769 12.1807 2.00333 12.2776 1.76541L12.314 1.66839L12.676 0.607931Z" fill="currentColor"/></g></svg>';
9
+
10
+ const SPARKLE_ICON_SVG = '<svg viewBox="0 0 11.6122 12.7248" fill="none" xmlns="http://www.w3.org/2000/svg" style="display:block;width:12px;height:12px"><path d="M4.09055 2.01133C4.43939 0.9905 5.84989 0.959583 6.26347 1.91858L6.29847 2.01192L6.76922 3.38858C6.8771 3.7043 7.05144 3.99321 7.28047 4.23583C7.5095 4.47845 7.7879 4.66912 8.09689 4.795L8.22347 4.84225L9.60014 5.31242C10.621 5.66125 10.6519 7.07175 9.69347 7.48533L9.60014 7.52033L8.22347 7.99108C7.90764 8.09889 7.61862 8.2732 7.3759 8.50223C7.13318 8.73127 6.94241 9.0097 6.81647 9.31875L6.76922 9.44475L6.29905 10.822C5.95022 11.8428 4.53972 11.8737 4.12672 10.9153L4.09055 10.822L3.62039 9.44533C3.51258 9.12951 3.33827 8.84048 3.10924 8.59776C2.8802 8.35504 2.60177 8.16427 2.29272 8.03833L2.16672 7.99108L0.790054 7.52092C-0.231363 7.17208 -0.26228 5.76158 0.69672 5.34858L0.790054 5.31242L2.16672 4.84225C2.48244 4.73437 2.77135 4.56003 3.01397 4.331C3.25658 4.10197 3.44726 3.82357 3.57314 3.51458L3.62039 3.38858L4.09055 2.01133ZM9.86147 0C9.9706 0 10.0775 0.0306123 10.1701 0.0883584C10.2627 0.146104 10.3373 0.228668 10.3853 0.326667L10.4133 0.394917L10.6175 0.993417L11.2166 1.19758C11.3259 1.23474 11.4218 1.30353 11.492 1.39523C11.5623 1.48693 11.6037 1.59741 11.611 1.71268C11.6184 1.82795 11.5914 1.94281 11.5334 2.04271C11.4755 2.14261 11.3891 2.22305 11.2854 2.27383L11.2166 2.30183L10.6181 2.506L10.4139 3.10508C10.3767 3.21442 10.3078 3.31024 10.2161 3.38041C10.1244 3.45058 10.0139 3.49194 9.89859 3.49925C9.78333 3.50656 9.66849 3.47949 9.56863 3.42147C9.46876 3.36345 9.38837 3.27709 9.33764 3.17333L9.30964 3.10508L9.10547 2.50658L8.50639 2.30242C8.39702 2.26526 8.30115 2.19647 8.23092 2.10477C8.16069 2.01307 8.11926 1.90259 8.11189 1.78732C8.10452 1.67205 8.13153 1.55719 8.18951 1.45729C8.24749 1.35739 8.33381 1.27695 8.43755 1.22617L8.50639 1.19817L9.10489 0.994L9.30905 0.394917C9.34839 0.279665 9.42281 0.179613 9.52187 0.108791C9.62094 0.0379686 9.73969 -0.0000730808 9.86147 0Z" fill="currentColor"/></svg>';
11
+
12
+ /** Cria elemento DOM declarativamente: el('tag', { style, text|html, children, on, attrs }) */
13
+ export function el(tag, opts = {}) {
14
+ const node = document.createElement(tag);
15
+ if (opts.style) node.style.cssText = opts.style;
16
+ if (opts.text != null) node.textContent = opts.text;
17
+ if (opts.html != null) node.innerHTML = opts.html;
18
+ if (opts.attrs) for (const k in opts.attrs) node.setAttribute(k, opts.attrs[k]);
19
+ if (opts.on) for (const k in opts.on) node.addEventListener(k, opts.on[k]);
20
+ if (opts.children) for (const c of opts.children) if (c) node.appendChild(c);
21
+ return node;
22
+ }
23
+
24
+ export function cameraIcon(color) {
25
+ return el('span', {
26
+ style: `width:24px;height:24px;padding:2px;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;color:${color};flex-shrink:0;`,
27
+ html: CAMERA_ICON_SVG
28
+ });
29
+ }
30
+
31
+ export function sparkleIcon(color) {
32
+ return el('span', {
33
+ style: `width:12px;height:12px;display:inline-flex;align-items:center;justify-content:center;color:${color};flex-shrink:0;`,
34
+ html: SPARKLE_ICON_SVG
35
+ });
36
+ }
37
+
38
+ export function borderFrom(c) {
39
+ return c.borderWidth > 0 && c.borderColor ? `${c.borderWidth}px solid ${c.borderColor}` : 'none';
40
+ }
41
+
42
+ /**
43
+ * Garante a fonte Jakarta no documento. Carregada no document (não no shadow),
44
+ * pois o carregamento de fontes é global e alcança o shadow tree.
45
+ */
46
+ export function ensureJakartaFont() {
47
+ if (document.getElementById('mk-jakarta-font')) return;
48
+ try {
49
+ document.head.appendChild(el('link', {
50
+ attrs: {
51
+ id: 'mk-jakarta-font',
52
+ rel: 'stylesheet',
53
+ href: 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700&display=swap'
54
+ }
55
+ }));
56
+ } catch (_) {}
57
+ }
@@ -0,0 +1,21 @@
1
+ // registry.js — registro de estilos de botão. Pra adicionar um novo tipo:
2
+ // crie ./meu-estilo.js exportando render(config, onClick) e registre aqui.
3
+
4
+ import * as gregoryBlack from './gregory-black.js';
5
+ import * as gregoryCard from './gregory-card.js';
6
+ import { fetchButtonTemplate, normalizeButtonConfig } from './config.js';
7
+
8
+ const RENDERERS = {
9
+ 'gregory-black': gregoryBlack.render,
10
+ 'gregory-card': gregoryCard.render
11
+ };
12
+
13
+ export const AVAILABLE_STYLES = Object.keys(RENDERERS);
14
+
15
+ /** Renderiza o botão do estilo informado (fallback: gregory-black). */
16
+ export function renderButton(config, onClick) {
17
+ const fn = RENDERERS[config.style] || RENDERERS['gregory-black'];
18
+ return fn(config, onClick);
19
+ }
20
+
21
+ export { fetchButtonTemplate, normalizeButtonConfig };