@indesad/feedback-widget 0.0.0-stage → 1.1.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.
@@ -0,0 +1,7 @@
1
+ /** Devuelve el SHA-256 en hexadecimal en mayúsculas (igual que Convert.ToHexString en el servidor). */
2
+ export declare function sha256Hex(text: string): string;
3
+ /**
4
+ * Busca la prueba de trabajo: un número cuyo SHA-256(token:número) empiece con `difficulty` ceros.
5
+ * Cede el hilo cada lote para no congelar la página anfitriona.
6
+ */
7
+ export declare function solveChallenge(token: string, difficulty?: number, maxAttempts?: number): Promise<string>;
@@ -0,0 +1 @@
1
+ export declare const STYLES = "\n:host {\n all: initial;\n --iff-accent: var(--if-brand, #FD0055);\n --iff-accent-hover: var(--if-brand-hover, #E0004B);\n --iff-accent-soft: var(--if-brand-soft, rgba(253, 0, 85, 0.12));\n --iff-accent-contrast: #FFFFFF;\n --iff-bg: var(--if-bg-elevated, #FFFFFF);\n --iff-surface: var(--if-bg-surface, #F8F9FA);\n --iff-hover: var(--if-bg-hover, #F1F3F5);\n --iff-text: var(--if-text-primary, #000002);\n --iff-muted: var(--if-text-tertiary, #64748B);\n --iff-border: var(--if-border-subtle, #E2E8F0);\n --iff-border-strong: var(--if-border-default, #CBD5E1);\n --iff-danger: var(--if-danger, #EF4444);\n --iff-danger-soft: var(--if-danger-soft, rgba(239, 68, 68, 0.12));\n --iff-success: var(--if-success, #10B981);\n --iff-success-soft: var(--if-success-soft, rgba(16, 185, 129, 0.12));\n --iff-shadow: var(--if-shadow-overlay, 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.1));\n --iff-radius: var(--if-radius-lg, 14px);\n --iff-radius-sm: var(--if-radius-md, 10px);\n --iff-font: var(--if-font-sans, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);\n --iff-font-heading: var(--if-font-heading, 'Oxanium', 'Inter', sans-serif);\n position: fixed; z-index: var(--iff-z, 2147483000); font-family: var(--iff-font); color: var(--iff-text);\n -webkit-font-smoothing: antialiased; line-height: 1.4; font-size: 14px;\n}\n:host([data-theme=\"dark\"]) {\n --iff-bg: var(--if-bg-elevated, #262626);\n --iff-surface: var(--if-bg-surface, #181818);\n --iff-hover: var(--if-bg-hover, #2A2A2A);\n --iff-text: var(--if-text-primary, #E2E8F0);\n --iff-muted: var(--if-text-tertiary, #94A3B8);\n --iff-border: var(--if-border-subtle, rgba(255, 255, 255, 0.08));\n --iff-border-strong: var(--if-border-default, rgba(255, 255, 255, 0.14));\n --iff-shadow: var(--if-shadow-overlay, 0 20px 30px -5px rgba(0, 0, 0, 0.9));\n}\n* { box-sizing: border-box; }\nbutton, input, textarea { font: inherit; color: inherit; }\n[hidden] { display: none !important; }\n.launcher {\n display: inline-flex; align-items: center; gap: 8px; height: 44px; padding: 0 16px 0 12px; border: none; border-radius: 9999px;\n background: var(--iff-accent); color: var(--iff-accent-contrast); font-weight: 600; font-size: 14px; cursor: pointer;\n box-shadow: 0 8px 20px -6px rgba(253, 0, 85, 0.45), 0 2px 6px rgba(0, 0, 0, 0.12); transition: transform .15s ease, background .15s ease;\n}\n.launcher:hover { background: var(--iff-accent-hover); transform: translateY(-1px); }\n.launcher.icon-only { width: 44px; padding: 0; justify-content: center; }\n.launcher.icon-only .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n.launcher:focus-visible, button:focus-visible, a:focus-visible, input:focus-visible, textarea:focus-visible, label.file:focus-within {\n outline: 2px solid var(--iff-accent); outline-offset: 2px;\n}\n.launcher svg, .launcher img { width: 20px; height: 20px; flex: none; }\n.launcher .emoji { font-size: 18px; line-height: 1; }\n.panel {\n position: fixed; width: 380px; max-width: calc(100vw - 24px); max-height: min(640px, calc(100vh - 96px)); display: flex; flex-direction: column;\n background: var(--iff-bg); color: var(--iff-text); border: 1px solid var(--iff-border); border-radius: var(--iff-radius); box-shadow: var(--iff-shadow);\n overflow: hidden; animation: iff-in .18s ease-out;\n}\n@keyframes iff-in { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: none; } }\n@media (prefers-reduced-motion: reduce) { .panel { animation: none; } .launcher { transition: none; } }\n.head { display: flex; align-items: flex-start; gap: 10px; padding: 16px 16px 12px; border-bottom: 1px solid var(--iff-border); }\n.head .titles { flex: 1; min-width: 0; }\n.brand { font-family: var(--iff-font-heading); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--iff-accent); font-weight: 700; }\n.title { margin: 2px 0 0; font-size: 17px; font-weight: 700; line-height: 1.25; }\n.subtitle { margin: 4px 0 0; font-size: 12.5px; color: var(--iff-muted); }\n.icon-btn { flex: none; width: 32px; height: 32px; display: inline-grid; place-items: center; border: none; border-radius: 8px; background: transparent; cursor: pointer; color: var(--iff-muted); }\n.icon-btn:hover { background: var(--iff-hover); color: var(--iff-text); }\n.icon-btn svg { width: 18px; height: 18px; }\n.body { padding: 14px 16px 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }\n.kinds { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }\n.kind {\n display: flex; flex-direction: column; align-items: flex-start; gap: 6px; padding: 12px; text-align: left; cursor: pointer;\n background: var(--iff-surface); border: 1px solid var(--iff-border); border-radius: var(--iff-radius-sm); transition: border-color .15s ease, background .15s ease;\n}\n.kind:hover { border-color: var(--iff-accent); background: var(--iff-accent-soft); }\n.kind .k-icon { width: 30px; height: 30px; border-radius: 8px; display: inline-grid; place-items: center; background: var(--iff-accent-soft); color: var(--iff-accent); }\n.kind .k-icon svg { width: 18px; height: 18px; }\n.kind strong { font-size: 13.5px; font-weight: 600; }\n.kind span { font-size: 12px; color: var(--iff-muted); }\n.help { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--iff-muted); text-decoration: none; padding: 8px 10px; border-radius: 8px; border: 1px dashed var(--iff-border-strong); }\n.help:hover { color: var(--iff-text); border-color: var(--iff-accent); }\n.help svg { width: 16px; height: 16px; flex: none; }\n.chip { align-self: flex-start; display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--iff-accent); background: var(--iff-accent-soft); padding: 4px 10px; border-radius: 9999px; }\n.chip svg { width: 14px; height: 14px; }\nlabel.field { display: flex; flex-direction: column; gap: 6px; font-size: 12.5px; font-weight: 600; }\nlabel.field .hint { font-weight: 400; color: var(--iff-muted); }\ninput[type=\"text\"], input[type=\"email\"], textarea {\n width: 100%; padding: 10px 12px; border-radius: var(--iff-radius-sm); border: 1px solid var(--iff-border-strong); background: var(--iff-surface);\n font-size: 14px; font-weight: 400;\n}\ntextarea { min-height: 120px; resize: vertical; }\ninput::placeholder, textarea::placeholder { color: var(--iff-muted); opacity: .9; }\n.row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }\n.meta { display: flex; justify-content: space-between; gap: 8px; font-size: 11.5px; color: var(--iff-muted); }\n.as { font-size: 12px; color: var(--iff-muted); background: var(--iff-surface); border: 1px solid var(--iff-border); border-radius: 8px; padding: 8px 10px; overflow-wrap: anywhere; }\nlabel.file { display: flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; padding: 9px 12px; border: 1px dashed var(--iff-border-strong); border-radius: var(--iff-radius-sm); color: var(--iff-muted); }\nlabel.file:hover { border-color: var(--iff-accent); color: var(--iff-text); }\nlabel.file svg { width: 16px; height: 16px; }\nlabel.file input { position: absolute; width: 1px; height: 1px; opacity: 0; }\n.preview { display: flex; align-items: center; gap: 10px; padding: 8px; border: 1px solid var(--iff-border); border-radius: var(--iff-radius-sm); background: var(--iff-surface); }\n.preview + .preview { margin-top: -6px; }\n.preview img { width: 56px; height: 40px; object-fit: cover; border-radius: 6px; }\n.preview .name { flex: 1; min-width: 0; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.link-btn { border: none; background: none; color: var(--iff-accent); cursor: pointer; font-size: 12.5px; font-weight: 600; padding: 4px; }\n.privacy { font-size: 11.5px; color: var(--iff-muted); margin: 0; }\n.error { font-size: 12.5px; color: var(--iff-danger); background: var(--iff-danger-soft); border-radius: 8px; padding: 8px 10px; margin: 0; }\n.actions { display: flex; justify-content: flex-end; gap: 8px; }\n.primary {\n display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-width: 120px; height: 40px; padding: 0 18px; border: none; border-radius: var(--iff-radius-sm);\n background: var(--iff-accent); color: var(--iff-accent-contrast); font-weight: 600; cursor: pointer;\n}\n.primary:hover { background: var(--iff-accent-hover); }\n.primary[disabled] { opacity: .65; cursor: progress; }\n.secondary { height: 40px; padding: 0 14px; border-radius: var(--iff-radius-sm); border: 1px solid var(--iff-border-strong); background: transparent; cursor: pointer; font-weight: 600; }\n.secondary:hover { background: var(--iff-hover); }\n.spinner { width: 16px; height: 16px; border-radius: 50%; border: 2px solid rgba(255,255,255,.45); border-top-color: #fff; animation: iff-spin .8s linear infinite; }\n@keyframes iff-spin { to { transform: rotate(360deg); } }\n.done { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px; padding: 8px 4px 4px; }\n.done .ok { width: 52px; height: 52px; border-radius: 50%; display: grid; place-items: center; background: var(--iff-success-soft); color: var(--iff-success); }\n.done .ok svg { width: 26px; height: 26px; }\n.done h3 { margin: 0; font-size: 17px; }\n.done p { margin: 0; font-size: 13px; color: var(--iff-muted); }\n.done .folio { font-family: var(--if-font-mono, ui-monospace, monospace); font-weight: 700; color: var(--iff-text); }\n.done .buttons { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin-top: 6px; }\n.done a.primary { text-decoration: none; }\n.foot { padding: 8px 16px; border-top: 1px solid var(--iff-border); font-size: 11px; color: var(--iff-muted); text-align: center; }\n@media (max-width: 480px) {\n .panel { inset: 0 !important; width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; border-radius: 0; border: none;\n padding: var(--if-safe-top, env(safe-area-inset-top, 0px)) var(--if-safe-right, env(safe-area-inset-right, 0px)) var(--if-safe-bottom, env(safe-area-inset-bottom, 0px)) var(--if-safe-left, env(safe-area-inset-left, 0px)); }\n .kinds { grid-template-columns: 1fr; }\n .row { grid-template-columns: 1fr; }\n .launcher .label { display: none; }\n .launcher { width: 48px; height: 48px; padding: 0; justify-content: center; }\n}\n";
@@ -0,0 +1,97 @@
1
+ export type FeedbackKind = 'bug' | 'improvement' | 'question' | 'praise';
2
+ export type FeedbackLocale = 'es-419' | 'en' | 'pt-BR';
3
+ /** Identidad que declara la app anfitriona para la persona que ya inició sesión en ella. IndeFlow no la verifica. */
4
+ export interface FeedbackUser {
5
+ name?: string;
6
+ email?: string;
7
+ company?: string;
8
+ externalUserId?: string;
9
+ }
10
+ /** Colores del widget. Se aplican como variables CSS --iff-*; por defecto heredan los tokens --if-* de la app si existen. */
11
+ export interface FeedbackTheme {
12
+ accent: string;
13
+ accentContrast: string;
14
+ background: string;
15
+ surface: string;
16
+ text: string;
17
+ muted: string;
18
+ border: string;
19
+ danger: string;
20
+ success: string;
21
+ radius: string;
22
+ font: string;
23
+ }
24
+ export interface IndeFlowFeedbackConfig {
25
+ /** Base de la API de IndeFlow, por ejemplo https://api.indeflow.app/api */
26
+ apiUrl: string;
27
+ /** Token de la fuente (if_obs_...) con opiniones activas. Es público por naturaleza: solo permite enviar opiniones. */
28
+ sourceToken: string;
29
+ /** Persona con sesión en la app. Sin user, el formulario pide nombre y correo (si la fuente permite anónimos). */
30
+ user?: FeedbackUser | null;
31
+ /** Idioma del widget y de los correos al reportante. 'auto' usa el del navegador. */
32
+ locale?: FeedbackLocale | 'auto' | string;
33
+ /** Nombre que se muestra arriba (por defecto, el que configuró la fuente). */
34
+ brandLabel?: string;
35
+ title?: string;
36
+ position?: 'bottom-right' | 'bottom-left';
37
+ /** Separación del borde en píxeles, para no tapar otros botones flotantes. */
38
+ offset?: {
39
+ x?: number;
40
+ y?: number;
41
+ };
42
+ /** 'bubble' muestra el botón flotante; 'none' lo oculta y la app abre el panel con open(). */
43
+ launcher?: 'bubble' | 'none';
44
+ /** 'pill' muestra ícono y texto; 'icon' un botón redondo compacto (el texto queda para lectores de pantalla). */
45
+ launcherStyle?: 'pill' | 'icon';
46
+ launcherLabel?: string;
47
+ launcherIcon?: string;
48
+ launcherIconType?: 'default' | 'emoji' | 'image' | 'svg';
49
+ /** 'auto' sigue a la app (html.dark, data-theme o themeSelector) y, si no hay pista, a prefers-color-scheme. */
50
+ theme?: 'light' | 'dark' | 'auto';
51
+ themeSelector?: string;
52
+ colors?: Partial<FeedbackTheme>;
53
+ /** Tipos que ofrece. Si falta, usa los de la fuente. */
54
+ kinds?: FeedbackKind[];
55
+ /** Enlace «Necesito ayuda» hacia el soporte de la app (portal, WhatsApp…). Una urgencia no es una opinión. */
56
+ supportUrl?: string;
57
+ supportLabel?: string;
58
+ /** Ofrece adjuntar capturas (hasta 5 imágenes de 5 MB; el servidor fija los límites). Por defecto true. */
59
+ screenshot?: boolean;
60
+ /** Datos fijos que acompañan cada envío (módulo, plan…). Máximo 20 claves. No pongas secretos. */
61
+ context?: Record<string, string>;
62
+ appVersion?: string;
63
+ /** Adjunta la ruta actual (sin parámetros). Por defecto true. */
64
+ collectRoute?: boolean;
65
+ zIndex?: number;
66
+ showPoweredBy?: boolean;
67
+ onSubmitted?: (result: {
68
+ folio: string;
69
+ trackingUrl: string;
70
+ }) => void;
71
+ onError?: (error: {
72
+ code: string;
73
+ message: string;
74
+ }) => void;
75
+ }
76
+ export interface RemoteConfig {
77
+ appName: string;
78
+ kinds: FeedbackKind[];
79
+ allowAnonymous: boolean;
80
+ requireEmail: boolean;
81
+ defaultLocale: string;
82
+ maxAttachmentBytes: number;
83
+ /** FB-000005: imágenes por opinión. Un servidor anterior no lo manda: el widget asume 5. */
84
+ maxAttachments?: number;
85
+ attachmentTypes: string[];
86
+ maxMessage: number;
87
+ }
88
+ export interface IndeFlowFeedbackController {
89
+ open(kind?: FeedbackKind): void;
90
+ close(): void;
91
+ toggle(): void;
92
+ setUser(user: FeedbackUser | null): void;
93
+ setContext(context: Record<string, string>): void;
94
+ update(config: Partial<IndeFlowFeedbackConfig>): void;
95
+ destroy(): void;
96
+ readonly ready: Promise<boolean>;
97
+ }
@@ -0,0 +1,67 @@
1
+ import type { FeedbackKind, FeedbackUser, IndeFlowFeedbackConfig, IndeFlowFeedbackController } from './types';
2
+ export declare const VERSION = "1.1.1";
3
+ export declare class IndeFlowFeedbackWidget implements IndeFlowFeedbackController {
4
+ private config;
5
+ private remote;
6
+ private host;
7
+ private root;
8
+ private launcherButton;
9
+ private panel;
10
+ private step;
11
+ private kind;
12
+ private draft;
13
+ private attachments;
14
+ private submissionId;
15
+ private sending;
16
+ private error;
17
+ private result;
18
+ private extraContext;
19
+ private themeObserver;
20
+ private media;
21
+ private readonly onMedia;
22
+ private readonly onKey;
23
+ private destroyed;
24
+ readonly ready: Promise<boolean>;
25
+ constructor(config: IndeFlowFeedbackConfig);
26
+ private get locale();
27
+ private get t();
28
+ private get endpoint();
29
+ private get appName();
30
+ private get user();
31
+ private get anonymous();
32
+ private get kinds();
33
+ /** Sin identidad de la app y con anónimos apagados en la fuente, no hay a quién pedirle nada: no se muestra. */
34
+ private get available();
35
+ private headers;
36
+ private start;
37
+ private mount;
38
+ private applyColors;
39
+ private isDark;
40
+ private applyTheme;
41
+ private watchTheme;
42
+ private place;
43
+ private render;
44
+ private renderLauncher;
45
+ private renderPanel;
46
+ private renderKinds;
47
+ private renderForm;
48
+ private renderDone;
49
+ private choose;
50
+ private reset;
51
+ private maxAttachments;
52
+ private maxMegabytes;
53
+ /** Adds each valid image up to the limit; the ones that fail leave their message and the rest still get attached. */
54
+ private attach;
55
+ private validate;
56
+ private context;
57
+ private submit;
58
+ private handleKey;
59
+ open(kind?: FeedbackKind): void;
60
+ close(): void;
61
+ toggle(): void;
62
+ setUser(user: FeedbackUser | null): void;
63
+ setContext(context: Record<string, string>): void;
64
+ update(config: Partial<IndeFlowFeedbackConfig>): void;
65
+ destroy(): void;
66
+ }
67
+ export declare function createIndeFlowFeedback(config: IndeFlowFeedbackConfig): IndeFlowFeedbackController;
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "@indesad/feedback-widget",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.1",
4
+ "description": "Widget de opiniones de producto de IndeFlow: reportar problemas, sugerir mejoras, preguntar y felicitar desde cualquier app web. Sin dependencias, Shadow DOM, claro/oscuro, es/en/pt.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "sideEffects": false,
21
+ "author": "INDESAD",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/INDESAD/IndeFlow.git",
26
+ "directory": "packages/web/feedback-widget"
27
+ },
28
+ "homepage": "https://indeflow.app",
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "registry": "https://registry.npmjs.org/"
32
+ }
33
+ }