@innspel/web 0.0.0 → 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,52 @@
1
+ import { type InnspelClient, type InnspelOptions } from '@innspel/core';
2
+ import { type InnspelProviderProps } from './InnspelProvider.js';
3
+ import type { ÅpneOpsjoner } from './widget/Widget.js';
4
+ export interface MountOptions extends Omit<InnspelProviderProps, 'client' | 'children'> {
5
+ /** En ferdig klient fra `init()` — eller opsjonene under, så lager `mount` den. */
6
+ client?: InnspelClient;
7
+ baseUrl?: string;
8
+ tenantKey?: string;
9
+ identityProvider?: InnspelOptions['identityProvider'];
10
+ /** Standard er offline-køen i `localStorage` (DD-41). */
11
+ storage?: InnspelOptions['storage'];
12
+ /** Vertens bygg — legges på konteksten. Plattform, nettleser og språk settes av pakken. */
13
+ appVersion?: string;
14
+ build?: string;
15
+ textMaxLength?: InnspelOptions['textMaxLength'];
16
+ minTextLength?: number;
17
+ screenshotsEnabled?: boolean;
18
+ /**
19
+ * Standard er plattformens eget mottak for målemodellen (M4.2): anonymt, aldri tekst,
20
+ * aldri en tredjepart. `false` skrur det av; et eget objekt med `emit(e)` erstatter det.
21
+ */
22
+ telemetry?: InnspelOptions['telemetry'] | false;
23
+ /** Injiseres i test. Standard er `fetch` i nettleseren. */
24
+ fetch?: InnspelOptions['fetch'];
25
+ }
26
+ export interface Mounted {
27
+ client: InnspelClient;
28
+ open: (options?: ÅpneOpsjoner) => Promise<void>;
29
+ close: () => void;
30
+ /**
31
+ * Gjør vertens egen knapp til inngangen (DD-21): klikk åpner, knappen får
32
+ * `aria-haspopup="dialog"` og en beskrivelse (DD-47), og fokus kommer tilbake
33
+ * til den når dialogen lukkes. Returnerer en funksjon som løsner koblingen.
34
+ */
35
+ bindTrigger: (knapp: HTMLElement, options?: ÅpneOpsjoner) => () => void;
36
+ unmount: () => void;
37
+ }
38
+ /**
39
+ * Vanilla-inngangen: monter dialogen i et element, og bind vertens knapp.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const innspel = mount(document.getElementById('innspel')!, {
44
+ * baseUrl: 'https://api.innspel.example',
45
+ * tenantKey: 'ik_pub_xxxxxxxx',
46
+ * identityProvider: hentFeedbackToken, // eller null = anonym modus
47
+ * appVersion: '2.4.1',
48
+ * });
49
+ * innspel.bindTrigger(document.getElementById('send-innspill')!);
50
+ * ```
51
+ */
52
+ export declare function mount(el: HTMLElement, options: MountOptions): Mounted;
@@ -0,0 +1,14 @@
1
+ import type { Fangst } from './typer.js';
2
+ /**
3
+ * «Last opp bilde»: PNG eller JPEG fra fil. Bildet dekodes til en bitmap her —
4
+ * det er dekodingen som gjør at EXIF (posisjon, kamera, tidspunkt) er borte når
5
+ * bildet re-enkodes ved komposisjon (K4). Plattformen stripper igjen (K9).
6
+ *
7
+ * Grensene sjekkes før dekoding, med feltnavn, slik kjernen gjør for vedlegget:
8
+ * type mot PNG/JPEG og størrelse mot taket. Ingen private felter — bildet er
9
+ * ikke fra siden, så DD-39 har ingenting å måle.
10
+ */
11
+ export declare function lesBildefil(fil: Blob & {
12
+ type: string;
13
+ name?: string;
14
+ }): Promise<Fangst>;
@@ -0,0 +1,28 @@
1
+ import { type AttachmentInput, type MaskRect } from '@innspel/core';
2
+ import type { Fangst } from './typer.js';
3
+ export interface Maskering {
4
+ /** Private felter — kan ikke fjernes (DD-39). */
5
+ automasker: MaskRect[];
6
+ /** Innsenderens egne: «Masker felt» og «Tegn over» (DD-40). */
7
+ egne: MaskRect[];
8
+ /** «Masker all tekst» (DD-40). */
9
+ allTekst: boolean;
10
+ }
11
+ /**
12
+ * Komposisjonen er det som lastes opp (K4): bitmapen med alle maskene tegnet
13
+ * INN i pikslene, re-enkodet som JPEG. Rå-bitmapen brukes bare til
14
+ * forhåndsvisningen og forlater aldri widgeten.
15
+ *
16
+ * Fargene er `screenRules` fra temaet (theme/theme.ts) — det ene stedet
17
+ * paletten ikke gjelder, fordi maskene skal være synlige uansett hva bildet
18
+ * inneholder. De leses fra CSS-variablene så tallet bor ett sted.
19
+ */
20
+ export declare function komponer(fangst: Fangst, maskering: Maskering, farger?: {
21
+ auto: string;
22
+ egen: string;
23
+ }): Promise<AttachmentInput>;
24
+ /** Maskefargene fra tokens.css. Reserven er de samme tallene som i theme.ts, for et dokument uten stilarket. */
25
+ export declare function lesMaskefarger(): {
26
+ auto: string;
27
+ egen: string;
28
+ };
@@ -0,0 +1,57 @@
1
+ import type { MaskRect } from '@innspel/core';
2
+ import { type Fangst, type ScreenshotAdapter } from './typer.js';
3
+ /**
4
+ * Pakkens egen klientside-renderer: «Ta bilde av siden» uten at én byte av DOM-en
5
+ * forlater nettleseren (K4), og uten en tredjepartsavhengighet (K19). Valget og
6
+ * de forkastede alternativene står i tech-decisions.md, «M1.5».
7
+ *
8
+ * Mekanikken er SVG `foreignObject`: DOM-en klones, beregnede stiler skrives
9
+ * inn som inline-stil på hver node, klonen legges i en `<foreignObject>` i en
10
+ * SVG med viewportens mål, SVG-en lastes som et bilde og tegnes til en canvas.
11
+ * Tre ting skjer FØR serialisering, og det er de som gjør rendreren trygg:
12
+ *
13
+ * - Widgeten selv (`data-innspel-widget`) fjernes fra klonen.
14
+ * - Private felter (`data-innspel-private`, DD-39) tømmes i klonen OG måles i
15
+ * originalen, så de er maskert i bitmapen selv om geometrien skulle sprike.
16
+ * - Bilder erstattes: same-origin tegnes inn som data-URL, alt annet blir en
17
+ * nøytral flate. Et bilde fra en annen origin forgifter canvasen — og er
18
+ * uansett ikke vertens eget innhold.
19
+ *
20
+ * Webfonter lastes ikke inn i SVG-bildet; teksten faller tilbake på systemfont.
21
+ * Layouten er den samme, og det er layouten som er verdien i et skjermbilde.
22
+ */
23
+ export interface Klon {
24
+ klon: HTMLElement;
25
+ /** Private felter i CSS-piksler relativt til viewporten. */
26
+ automasker: MaskRect[];
27
+ /** Tekstlinjer i CSS-piksler relativt til viewporten. */
28
+ tekstRekter: MaskRect[];
29
+ }
30
+ /**
31
+ * Lager klonen som skal rendres. Ren DOM, ingen canvas — så den kan testes i
32
+ * jsdom: widgeten er borte, private felter er tomme, og geometrien deres er målt.
33
+ */
34
+ export declare function klonForBilde(rot: HTMLElement, viewport: {
35
+ width: number;
36
+ height: number;
37
+ }): Klon;
38
+ /** SVG-markupen med klonen i en foreignObject, forskjøvet så viewporten er det som vises. */
39
+ export declare function serialiserSvg(klon: HTMLElement, viewport: {
40
+ width: number;
41
+ height: number;
42
+ }, scroll: {
43
+ x: number;
44
+ y: number;
45
+ }, bakgrunn: string): string;
46
+ /** Største skala som holder bildet innenfor 4096² (K9) — og aldri over enhetens egen. */
47
+ export declare function velgSkala(viewport: {
48
+ width: number;
49
+ height: number;
50
+ }, dpr: number, tak?: number): number;
51
+ /**
52
+ * «Ta bilde av siden»: viewporten slik innsenderen ser den, med widgeten
53
+ * utelatt og private felter maskert (K4, DD-39).
54
+ */
55
+ export declare function renderPage(rot?: HTMLElement): Promise<Fangst>;
56
+ /** Adapteren pakken bruker når verten ikke gir sin egen. */
57
+ export declare const standardAdapter: ScreenshotAdapter;
@@ -0,0 +1,37 @@
1
+ import type { MaskRect } from '@innspel/core';
2
+ /** Attributtet verten setter på det som aldri skal med på et bilde (DD-39). */
3
+ export declare const PRIVAT_ATTRIBUTT = "data-innspel-private";
4
+ /** Attributtet widgeten setter på seg selv, så den aldri kommer med på bildet. */
5
+ export declare const WIDGET_ATTRIBUTT = "data-innspel-widget";
6
+ /**
7
+ * Et bilde klart til forhåndsvisning: en bitmap og de private feltenes
8
+ * rektangler i bildets piksler. Rå-bitmapen forlater aldri widgeten — det som
9
+ * lastes opp er komposittet med maskene (K4).
10
+ */
11
+ export interface Fangst {
12
+ bilde: CanvasImageSource;
13
+ width: number;
14
+ height: number;
15
+ /** Private felter (DD-39), alt i bildets piksler. Tomt for en opplastet fil. */
16
+ automasker: MaskRect[];
17
+ /**
18
+ * Tekstlinjene på siden i bildets piksler — det «Masker all tekst» dekker
19
+ * (DD-40). Utelatt for en opplastet fil; da pikseleres hele bildet i stedet.
20
+ */
21
+ tekstRekter?: MaskRect[];
22
+ kilde: 'side' | 'fil';
23
+ }
24
+ /**
25
+ * Fangst av siden. Pakken har sin egen (`renderPage` i renderer.ts, SVG
26
+ * foreignObject, null avhengigheter — K19), og verten kan bytte den ut med
27
+ * f.eks. html-to-image gjennom denne adapteren (tech-decisions.md, «M1.5»),
28
+ * samme mønster som `createScreenshotAdapter` i React Native.
29
+ */
30
+ export interface ScreenshotAdapter {
31
+ /**
32
+ * Fanger `rot` (typisk `document.body`) uten elementer merket
33
+ * `data-innspel-widget`. Private felter returneres som rektangler i BILDETS
34
+ * piksler, så widgeten kan maskere dem uansett hvem som rendret.
35
+ */
36
+ capture(rot: HTMLElement): Promise<Fangst>;
37
+ }
@@ -0,0 +1,2 @@
1
+ :root{--innspel-color-bg: #F6F7F9;--innspel-color-surface: #FFFFFF;--innspel-color-surface-raised: #EDEFF3;--innspel-color-border: #D2D7DE;--innspel-color-border-strong: #B9C0C9;--innspel-color-border-interactive: #6F7883;--innspel-color-text-primary: #14181D;--innspel-color-text-secondary: #454D57;--innspel-color-text-muted: #5C6470;--innspel-color-text-disabled: #9AA2AD;--innspel-color-action-primary: #0A6673;--innspel-color-on-action-primary: #FFFFFF;--innspel-color-action-secondary-border: #6F7883;--innspel-color-action-secondary-text: #14181D;--innspel-color-brand: #0A6673;--innspel-color-on-brand: #FFFFFF;--innspel-color-brand-fill: #0A6673;--innspel-color-on-brand-fill: #FFFFFF;--innspel-color-brand-bg: #E0F0F2;--innspel-color-danger: #B3001B;--innspel-color-danger-fill: #B3001B;--innspel-color-on-danger-fill: #FFFFFF;--innspel-color-danger-bg: #FCE8EA;--innspel-color-success: #04693C;--innspel-color-on-success: #FFFFFF;--innspel-color-success-bg: #E4F4EC;--innspel-color-warning: #7A5200;--innspel-color-on-warning: #FFFFFF;--innspel-color-warning-bg: #FBF1DE;--innspel-color-info: #00588F;--innspel-color-on-info: #FFFFFF;--innspel-color-info-bg: #E4F0F8;--innspel-color-neutral: #5C6470;--innspel-color-on-neutral: #FFFFFF;--innspel-color-neutral-bg: #EEF0F3;--innspel-color-scrim: rgba(10, 14, 18, .45);--innspel-space-xs: 4px;--innspel-space-sm: 8px;--innspel-space-md: 16px;--innspel-space-lg: 24px;--innspel-space-xl: 32px;--innspel-space-xxl: 48px;--innspel-radius-sm: 6px;--innspel-radius-md: 8px;--innspel-radius-lg: 12px;--innspel-radius-pill: 999px;--innspel-font-display: 40px;--innspel-font-h1: 24px;--innspel-font-h2: 21px;--innspel-font-h3: 18px;--innspel-font-body: 16px;--innspel-font-small: 13px;--innspel-font-tiny: 11px;--innspel-line-height-display: 48px;--innspel-line-height-h1: 30px;--innspel-line-height-h2: 27px;--innspel-line-height-h3: 24px;--innspel-line-height-body: 24px;--innspel-line-height-small: 18px;--innspel-line-height-tiny: 16px;--innspel-weight-regular: 400;--innspel-weight-medium: 500;--innspel-weight-semibold: 600;--innspel-weight-bold: 700;--innspel-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;--innspel-motion-fast: .12s;--innspel-motion-base: .2s;--innspel-motion-slow: .32s;--innspel-icon-inline: 20px;--innspel-icon-minste: 16px;--innspel-layout-touch: 48px;--innspel-layout-screen-padding: 24px;--innspel-layout-hairline: 1px;--innspel-layout-breakpoint: 768px;--innspel-layout-breakpoint-smal: 900px;--innspel-layout-content-width: 1120px;--innspel-layout-reading-width: 720px;--innspel-screen-brush-fill: #E5007D;--innspel-screen-auto-mask-fill: #101114;--innspel-screen-auto-mask-stroke: #E5007D;--innspel-screen-preview-backdrop: #000000;--innspel-screen-brush-width: 28;--innspel-screen-text-blur-radius: 24}[data-innspel-theme=dark]{--innspel-color-bg: #111417;--innspel-color-surface: #1A1E22;--innspel-color-surface-raised: #232830;--innspel-color-border: #2E343B;--innspel-color-border-strong: #3D444D;--innspel-color-border-interactive: #7C8894;--innspel-color-text-primary: #F2F4F6;--innspel-color-text-secondary: #B4BCC5;--innspel-color-text-muted: #8D96A1;--innspel-color-text-disabled: #5A626B;--innspel-color-action-primary: #5AD3E2;--innspel-color-on-action-primary: #04252A;--innspel-color-action-secondary-border: #7C8894;--innspel-color-action-secondary-text: #F2F4F6;--innspel-color-brand: #5AD3E2;--innspel-color-on-brand: #04252A;--innspel-color-brand-fill: #0E7C8C;--innspel-color-on-brand-fill: #FFFFFF;--innspel-color-brand-bg: #0C2A30;--innspel-color-danger: #FF7A8C;--innspel-color-danger-fill: #C4001F;--innspel-color-on-danger-fill: #FFFFFF;--innspel-color-danger-bg: #2C1116;--innspel-color-success: #3ED88A;--innspel-color-on-success: #062015;--innspel-color-success-bg: #0E2A1E;--innspel-color-warning: #FFC24D;--innspel-color-on-warning: #2A1C02;--innspel-color-warning-bg: #2C2107;--innspel-color-info: #5CC0FF;--innspel-color-on-info: #04222F;--innspel-color-info-bg: #0E2531;--innspel-color-neutral: #A3ACB6;--innspel-color-on-neutral: #111417;--innspel-color-neutral-bg: #242A31;--innspel-color-scrim: rgba(0, 0, 0, .72)}
2
+ .innspel-rot{font-family:var(--innspel-family-base);font-size:var(--innspel-font-body);line-height:var(--innspel-line-height-body);color:var(--innspel-color-text-primary);box-sizing:border-box}.innspel-rot *,.innspel-rot *:before,.innspel-rot *:after{box-sizing:inherit}.innspel-rot button,.innspel-rot textarea,.innspel-rot input{font:inherit;color:inherit}.innspel-rot :focus-visible{outline:calc(2 * var(--innspel-layout-hairline)) solid var(--innspel-color-text-primary);outline-offset:var(--innspel-space-xs)}.innspel-scrim{position:fixed;inset:0;background:var(--innspel-color-scrim);z-index:2147483000;display:flex;align-items:flex-start;justify-content:center;padding:var(--innspel-space-xl) var(--innspel-space-md)}.innspel-scrim[data-form=bunnark]{align-items:flex-end;padding:0}.innspel-dialog{background:var(--innspel-color-bg);color:var(--innspel-color-text-primary);border:var(--innspel-layout-hairline) solid var(--innspel-color-border-strong);border-radius:var(--innspel-radius-lg);width:100%;max-width:calc(9 * var(--innspel-layout-touch) + 2 * var(--innspel-space-md));max-height:100%;display:flex;flex-direction:column;overflow:hidden}.innspel-scrim[data-form=bunnark] .innspel-dialog{max-width:none;max-height:88%;border-radius:calc(2 * var(--innspel-radius-lg)) calc(2 * var(--innspel-radius-lg)) 0 0;border-bottom:none}.innspel-dialog-hode{display:flex;align-items:center;justify-content:space-between;padding:var(--innspel-space-sm) var(--innspel-space-sm) var(--innspel-space-sm) var(--innspel-space-md);border-bottom:var(--innspel-layout-hairline) solid var(--innspel-color-border);min-height:var(--innspel-layout-touch)}.innspel-dialog-tittel{font-size:var(--innspel-font-small);color:var(--innspel-color-text-secondary)}.innspel-handtak{width:var(--innspel-layout-touch);height:var(--innspel-space-xs);border-radius:var(--innspel-radius-pill);background:var(--innspel-color-border-interactive);margin:0 auto}.innspel-dialog-kropp{flex:1;overflow-y:auto;padding:var(--innspel-space-md);display:flex;flex-direction:column;gap:var(--innspel-space-md);min-height:0}.innspel-dialog-fot{padding:var(--innspel-space-md);border-top:var(--innspel-layout-hairline) solid var(--innspel-color-border);background:var(--innspel-color-surface);display:flex;flex-direction:column;gap:var(--innspel-space-sm)}.innspel-knapp-ramme{display:flex;flex-direction:column;gap:var(--innspel-space-xs)}.innspel-knapp-ramme[data-grow=true]{flex:1}.innspel-knapp{width:100%;min-height:var(--innspel-layout-touch);padding:0 var(--innspel-space-md);border-radius:var(--innspel-radius-md);border:var(--innspel-layout-hairline) solid transparent;font-weight:var(--innspel-weight-semibold);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:var(--innspel-space-sm);background:transparent;text-align:center}.innspel-knapp[data-variant=primary]{background:var(--innspel-color-action-primary);color:var(--innspel-color-on-action-primary);border-color:var(--innspel-color-action-primary)}.innspel-knapp[data-variant=secondary]{color:var(--innspel-color-action-secondary-text);border-color:var(--innspel-color-action-secondary-border)}.innspel-knapp[data-variant=ghost]{color:var(--innspel-color-text-secondary)}.innspel-knapp[data-variant=danger]{color:var(--innspel-color-danger);border-color:var(--innspel-color-danger)}.innspel-knapp[data-tall=true]{min-height:calc(1.5 * var(--innspel-layout-touch))}.innspel-knapp:disabled{background:var(--innspel-color-surface-raised);color:var(--innspel-color-text-disabled);border-color:var(--innspel-color-border);cursor:not-allowed}.innspel-ikonknapp{min-width:var(--innspel-layout-touch);min-height:var(--innspel-layout-touch);background:transparent;border:none;color:var(--innspel-color-text-secondary);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:var(--innspel-space-xs);font-size:var(--innspel-font-small);border-radius:var(--innspel-radius-md)}.innspel-h{display:flex;flex-direction:column;gap:var(--innspel-space-xs)}.innspel-h>h2,.innspel-h>h1{margin:0;font-size:var(--innspel-font-h2);line-height:var(--innspel-line-height-h2);font-weight:var(--innspel-weight-semibold);color:var(--innspel-color-text-primary)}.innspel-hint{margin:0;font-size:var(--innspel-font-small);line-height:var(--innspel-line-height-small);color:var(--innspel-color-text-muted)}.innspel-p{margin:0;font-size:var(--innspel-font-body);line-height:var(--innspel-line-height-body);color:var(--innspel-color-text-primary)}.innspel-kort{background:var(--innspel-color-surface);border:var(--innspel-layout-hairline) solid var(--innspel-color-border);border-radius:var(--innspel-radius-lg);padding:var(--innspel-space-md);display:flex;flex-direction:column;gap:var(--innspel-space-sm)}.innspel-kort[data-stiplet=true]{border-style:dashed}.innspel-pille{display:inline-flex;align-items:center;align-self:flex-start;gap:var(--innspel-space-xs);padding:var(--innspel-space-xs) var(--innspel-space-sm);border-radius:var(--innspel-radius-pill);font-size:var(--innspel-font-small);font-weight:var(--innspel-weight-medium)}.innspel-pille[data-farge=info]{color:var(--innspel-color-info);background:var(--innspel-color-info-bg)}.innspel-pille[data-farge=success]{color:var(--innspel-color-success);background:var(--innspel-color-success-bg)}.innspel-pille[data-farge=warning]{color:var(--innspel-color-warning);background:var(--innspel-color-warning-bg)}.innspel-pille[data-farge=neutral]{color:var(--innspel-color-neutral);background:var(--innspel-color-neutral-bg)}.innspel-pille[data-farge=danger]{color:var(--innspel-color-danger);background:var(--innspel-color-danger-bg)}.innspel-banner{display:flex;gap:var(--innspel-space-sm);align-items:flex-start;border-radius:var(--innspel-radius-md);padding:var(--innspel-space-sm) var(--innspel-space-md);font-size:var(--innspel-font-small);line-height:var(--innspel-line-height-small);font-weight:var(--innspel-weight-medium)}.innspel-banner>p{margin:0;flex:1}.innspel-banner[data-tone=info]{color:var(--innspel-color-info);background:var(--innspel-color-info-bg)}.innspel-banner[data-tone=success]{color:var(--innspel-color-success);background:var(--innspel-color-success-bg)}.innspel-banner[data-tone=warning]{color:var(--innspel-color-warning);background:var(--innspel-color-warning-bg)}.innspel-banner[data-tone=danger]{color:var(--innspel-color-danger);background:var(--innspel-color-danger-bg)}.innspel-skjelett{height:var(--innspel-space-lg);width:100%;border-radius:var(--innspel-radius-sm);background:var(--innspel-color-surface-raised)}.innspel-skjelett[data-hoy=true]{height:calc(2 * var(--innspel-layout-touch))}.innspel-rad{display:flex;gap:var(--innspel-space-sm)}.innspel-rad[data-gap=md]{gap:var(--innspel-space-md)}.innspel-stabel{display:flex;flex-direction:column;gap:var(--innspel-space-md)}.innspel-stabel[data-gap=sm]{gap:var(--innspel-space-sm)}.innspel-liste{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--innspel-space-sm)}.innspel-flate{flex:1;min-height:calc(2.5 * var(--innspel-layout-touch));background:var(--innspel-color-surface);border:var(--innspel-layout-hairline) solid var(--innspel-color-border-strong);border-radius:var(--innspel-radius-lg);color:var(--innspel-color-text-primary);cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--innspel-space-sm);padding:var(--innspel-space-md)}.innspel-flate-ikon{font-size:var(--innspel-font-h1)}.innspel-flate-tittel{font-size:var(--innspel-font-h3);font-weight:var(--innspel-weight-semibold)}.innspel-flate-under{font-size:var(--innspel-font-small);color:var(--innspel-color-text-muted);text-align:center}.innspel-kjent-tittel{margin:0;font-weight:var(--innspel-weight-medium)}.innspel-kjent-omvei{margin:0;font-size:var(--innspel-font-small);line-height:var(--innspel-line-height-small);color:var(--innspel-color-text-secondary)}.innspel-kjent-svar{display:flex;gap:var(--innspel-space-sm);flex-wrap:wrap}.innspel-felt{display:flex;flex-direction:column;gap:var(--innspel-space-sm)}.innspel-felt-etikett{font-size:var(--innspel-font-h3);font-weight:var(--innspel-weight-semibold)}.innspel-tekstfelt{width:100%;min-height:calc(2 * var(--innspel-layout-touch));background:var(--innspel-color-surface-raised);border:var(--innspel-layout-hairline) solid var(--innspel-color-border-strong);border-radius:var(--innspel-radius-md);padding:var(--innspel-space-md);color:var(--innspel-color-text-primary);resize:vertical}.innspel-tekstfelt::placeholder{color:var(--innspel-color-text-muted)}.innspel-tekstfelt[data-full=true]{border-color:var(--innspel-color-warning)}.innspel-utfall{flex:1;min-height:var(--innspel-layout-touch);padding:var(--innspel-space-sm);font:inherit;font-size:var(--innspel-font-small);color:var(--innspel-color-text-primary);background:var(--innspel-color-surface-raised);border:var(--innspel-layout-hairline) solid var(--innspel-color-border-strong);border-radius:var(--innspel-radius-md);cursor:pointer}.innspel-utfall[data-aktiv=true]{font-weight:var(--innspel-weight-semibold);background:var(--innspel-color-brand-bg);border-color:var(--innspel-color-brand)}.innspel-teller{align-self:flex-end;font-size:var(--innspel-font-small);color:var(--innspel-color-text-muted)}.innspel-teller[data-full=true]{color:var(--innspel-color-warning)}.innspel-chip{display:flex;align-items:center;gap:var(--innspel-space-sm);min-height:var(--innspel-layout-touch);border:var(--innspel-layout-hairline) solid var(--innspel-color-border-interactive);border-radius:var(--innspel-radius-pill);padding:0 var(--innspel-space-xs) 0 var(--innspel-space-md);font-size:var(--innspel-font-small)}.innspel-chip>span{flex:1}.innspel-kontekst{border-top:var(--innspel-layout-hairline) solid var(--innspel-color-border);padding-top:var(--innspel-space-sm)}.innspel-kontekst-bryter{width:100%;min-height:calc(var(--innspel-layout-touch) - var(--innspel-space-sm));background:transparent;border:none;color:var(--innspel-color-text-secondary);font-size:var(--innspel-font-small);display:flex;align-items:center;gap:var(--innspel-space-sm);cursor:pointer;padding:0;text-align:left;border-radius:var(--innspel-radius-sm)}.innspel-kontekst-linje{display:flex;align-items:center;gap:var(--innspel-space-sm);min-height:calc(var(--innspel-layout-touch) - var(--innspel-space-sm));border-bottom:var(--innspel-layout-hairline) solid var(--innspel-color-border);font-size:var(--innspel-font-small)}.innspel-kontekst-nokkel{color:var(--innspel-color-text-muted);min-width:calc(2 * var(--innspel-layout-touch))}.innspel-kontekst-verdi{flex:1;color:var(--innspel-color-text-primary);overflow-wrap:anywhere}.innspel-varsel{display:flex;flex-direction:column;gap:var(--innspel-space-sm);background:var(--innspel-color-warning-bg);color:var(--innspel-color-warning);border-radius:var(--innspel-radius-md);padding:var(--innspel-space-sm) var(--innspel-space-md);font-size:var(--innspel-font-small);line-height:var(--innspel-line-height-small);font-weight:var(--innspel-weight-medium)}.innspel-varsel>p{margin:0}.innspel-verktoy{flex:1;min-height:var(--innspel-layout-touch);background:transparent;border:var(--innspel-layout-hairline) solid var(--innspel-color-action-secondary-border);border-bottom-width:calc(2 * var(--innspel-layout-hairline));border-radius:var(--innspel-radius-md);color:var(--innspel-color-text-primary);font-size:var(--innspel-font-small);cursor:pointer;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--innspel-space-xs);padding:var(--innspel-space-xs)}.innspel-verktoy[aria-pressed=true]{background:var(--innspel-color-surface-raised);border-bottom-color:var(--innspel-color-text-primary);font-weight:var(--innspel-weight-semibold)}.innspel-forhandsvisning-ramme{background:var(--innspel-screen-preview-backdrop);border-radius:var(--innspel-radius-md);overflow:hidden;display:flex;align-items:center;justify-content:center;min-height:calc(4 * var(--innspel-layout-touch))}.innspel-forhandsvisning{position:relative;display:block;max-width:100%;touch-action:none;cursor:crosshair}.innspel-forhandsvisning>canvas{display:block;max-width:100%;height:auto}.innspel-maske{position:absolute;pointer-events:none}.innspel-maske[data-art=auto]{background:var(--innspel-screen-auto-mask-fill);border:calc(2 * var(--innspel-layout-hairline)) solid var(--innspel-screen-auto-mask-stroke);color:var(--innspel-color-text-secondary);font-size:var(--innspel-font-small);display:flex;align-items:center;justify-content:center;overflow:hidden;white-space:nowrap}.innspel-maske[data-art=egen]{background:var(--innspel-screen-brush-fill)}.innspel-skjult-fil{position:absolute;width:var(--innspel-layout-hairline);height:var(--innspel-layout-hairline);opacity:0;overflow:hidden}.innspel-kvittering{margin:auto 0;text-align:center;display:flex;flex-direction:column;gap:var(--innspel-space-md);align-items:center}.innspel-kvittering-ikon{font-size:calc(2 * var(--innspel-font-h1));color:var(--innspel-color-success)}.innspel-kvittering-ikon[data-tone=neutral]{color:var(--innspel-color-neutral)}.innspel-svar{border-left:calc(2 * var(--innspel-layout-hairline)) solid var(--innspel-color-brand);padding-left:var(--innspel-space-sm);display:flex;flex-direction:column;gap:var(--innspel-space-xs)}.innspel-svar-fra{font-size:var(--innspel-font-small);color:var(--innspel-color-text-secondary)}.innspel-svar-tekst{margin:0;font-size:var(--innspel-font-small);line-height:var(--innspel-line-height-small);color:var(--innspel-color-text-primary)}.innspel-sr{position:absolute;width:var(--innspel-layout-hairline);height:var(--innspel-layout-hairline);padding:0;margin:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}@media(prefers-reduced-motion:no-preference){.innspel-dialog{animation:innspel-inn var(--innspel-motion-base) ease-out}@keyframes innspel-inn{0%{opacity:0;transform:translateY(var(--innspel-space-md))}to{opacity:1;transform:none}}}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Legger tokens.css og pakkens stilark inn i `<head>` én gang. Verter med en
3
+ * `style-src` uten `'unsafe-inline'` importerer CSS-en selv i stedet og setter
4
+ * `injectStyles: false`. Tokens er GENERERT fra theme/theme.ts (M0.4) — dette
5
+ * er den ene lovlige veien til en farge (CLAUDE.md, «Styling»).
6
+ */
7
+ export declare function ensureStyles(doc?: Document): void;
8
+ /** Stilarket som tekst, for verter som vil legge det inn selv. */
9
+ export declare const css: string;
@@ -0,0 +1,23 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * DD-47 — «Widgeten er en dialog og oppfører seg som en.» `role="dialog"`,
4
+ * `aria-modal`, fokus inn ved åpning og FANGET der, ESC og ✕ lukker, fokus
5
+ * tilbake til vertens knapp ved lukking. Vertens innhold får `inert` bak
6
+ * dialogen, så skjermlesere ikke ser det.
7
+ *
8
+ * Form: modal på desktop, bunnark på smal skjerm — målt med `matchMedia` mot
9
+ * `--innspel-layout-breakpoint` fra tokens, aldri user-agent. Samme flyt, annen
10
+ * form (wireframe 2b, mobilraden).
11
+ *
12
+ * `data-innspel-widget` på rota gjør at skjermbilde-rendreren utelater
13
+ * dialogen fra bildet (K4).
14
+ */
15
+ export declare function Dialog({ open, onClose, children, footer, colorScheme, returnFocusTo, }: {
16
+ open: boolean;
17
+ onClose: () => void;
18
+ children: ReactNode;
19
+ footer?: ReactNode;
20
+ colorScheme?: 'dark' | 'light';
21
+ /** Elementet fokus skal tilbake til ved lukking — vertens knapp (DD-47). */
22
+ returnFocusTo?: HTMLElement | null;
23
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,32 @@
1
+ import type { ContextInput, KnownIssueCard, PatternFinding } from '@innspel/core';
2
+ /**
3
+ * 2d + 2e — feltet og kontekstlinja. Ett felt, ingenting annet.
4
+ *
5
+ * DD-44: send er inaktiv under `minTextLength`, og det er det ENESTE kravet.
6
+ * DD-45: placeholderen stiller to spørsmål i feilsporet. DD-46: mønstervarselet
7
+ * vises FØR sending og fjerner aldri noe selv. DD-05: konteksten er synlig og
8
+ * fjernbar linje for linje — fem linjer på web (K5; tech-decisions.md, «M1.5»).
9
+ * Ctrl+Enter sender (wireframe 2d).
10
+ */
11
+ export declare function Felt({ spor, verdi, onEndre, minTextLength, textMaxLength, kontekst, fjernedeFelter, onFjernKontekst, lenketTil, onFjernLenke, funn, onSend, sender, onTaBilde, onLastOppBilde, harVedlegg, onFjernVedlegg, }: {
12
+ spor: 'feil' | 'onske';
13
+ verdi: string;
14
+ onEndre: (v: string) => void;
15
+ minTextLength: number;
16
+ textMaxLength: number;
17
+ kontekst: ContextInput & {
18
+ sdkVersion: string;
19
+ };
20
+ fjernedeFelter: string[];
21
+ onFjernKontekst: (felt: string) => void;
22
+ lenketTil?: KnownIssueCard;
23
+ onFjernLenke?: () => void;
24
+ funn: PatternFinding[];
25
+ onSend: () => void;
26
+ sender?: boolean;
27
+ /** Skjermbilde-steget (2f, K4). Utelatt = steget finnes ikke (DD-26). Aldri en deaktivert knapp. */
28
+ onTaBilde?: () => void;
29
+ onLastOppBilde?: (fil: File) => void;
30
+ harVedlegg?: boolean;
31
+ onFjernVedlegg?: () => void;
32
+ }): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import type { KnownIssueCard } from '@innspel/core';
2
+ /**
3
+ * 2c — kjente feil. Vises FØR feltet i feilsporet, ETTER i ønskesporet (DD-02).
4
+ * Tre svar per kort (DD-29): «Ja, jeg også» · «Ligner, men ikke helt» · «Nei,
5
+ * det er noe annet». Kortet er en `group`, ikke én knapp: tre svar må være tre
6
+ * fokusstopp, og hver knapp bærer kortets tittel i etiketten.
7
+ *
8
+ * Treffene er et tilbud, aldri en sperre: tom liste og feilet oppslag hoppes
9
+ * over uten melding (håndteres i Widget).
10
+ */
11
+ export declare function KjenteFeil({ kort, spor, laster, onJaOgsa, onLigner, onNei, }: {
12
+ kort: KnownIssueCard[];
13
+ spor: 'feil' | 'onske';
14
+ laster?: boolean;
15
+ onJaOgsa: (k: KnownIssueCard) => void;
16
+ onLigner: (k: KnownIssueCard) => void;
17
+ onNei: () => void;
18
+ }): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ export type KvitteringArt = 'feil' | 'onske' | 'anonym' | 'jaOgsa' | 'kø';
2
+ /**
3
+ * 2g — kvitteringen. Den LOVER noe, per spor (DD-07), og saksnummeret er der så
4
+ * innsenderen kan vise til det hos support. Anonym modus lover ikke svar, men
5
+ * saken teller. Ingen «vurder appen», ingen oppfordring til å dele.
6
+ *
7
+ * DD-42: feilet vedlegget, er saken likevel sendt. DD-47: live-region, og fokus
8
+ * lander på «Lukk» — Enter lukker.
9
+ */
10
+ export declare function Kvittering({ art, saksnummer, vedlegg, onLukk, }: {
11
+ art: KvitteringArt;
12
+ saksnummer?: string;
13
+ /** `feilet`: teksten er lagret, bildet kom ikke med. `forkastet`: køet offline uten bilde. */
14
+ vedlegg?: 'feilet' | 'forkastet';
15
+ onLukk: () => void;
16
+ }): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import type { MySubmission } from '@innspel/core';
2
+ /**
3
+ * 2j — «Mine innspill» (DD-27). Offline-køen er synlig (DD-41): raden som
4
+ * venter står øverst, uten saksnummer. Svaret står under saken, ordrett som i
5
+ * panelet. Samme komponent i dialogen og i portalens fullskjerm — det er det
6
+ * snapshot-testen beviser.
7
+ *
8
+ * Tilstander: laster (skjelett i radenes form) · tom (invitasjon, DD-19) ·
9
+ * rader · feil (håndteres av den som kaller, med teksten beholdt).
10
+ */
11
+ export declare function MineInnspill({ rader, laster, tenantName, onNytt, overskriftNivaa, }: {
12
+ rader: MySubmission[];
13
+ laster?: boolean;
14
+ tenantName?: string;
15
+ onNytt: () => void;
16
+ overskriftNivaa?: 1 | 2;
17
+ }): import("react").JSX.Element;
@@ -0,0 +1,19 @@
1
+ import type { Maskering } from '../skjermbilde/komposisjon.js';
2
+ import type { Fangst } from '../skjermbilde/typer.js';
3
+ /**
4
+ * 2f — skjermbildet, forhåndsvist FØR noe sendes (K4). Tre likestilte
5
+ * maskeringsveier (DD-40): «Masker felt» (ett klikk legger en boks), «Tegn
6
+ * over» (dra) og «Masker all tekst» (tekstlinjene dekkes; for en fil pikseleres
7
+ * bildet). Private felter (DD-39) ligger der før innsenderen ser bildet, merket
8
+ * «Maskert av appen», og kan ikke fjernes. Egne masker angres med ett klikk.
9
+ *
10
+ * Alle masker holdes i BILDETS piksler; lagene tegnes som prosent av flaten,
11
+ * så de følger bildet uansett hvor stort det vises. Komposisjonen skjer i
12
+ * `komponer` — det er komposittet som lastes opp, aldri rå-bitmapen.
13
+ */
14
+ export declare function Skjermbilde({ fangst, komponerer, onFjern, onLeggVed }: {
15
+ fangst: Fangst;
16
+ komponerer: boolean;
17
+ onFjern: () => void;
18
+ onLeggVed: (m: Maskering) => void;
19
+ }): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 2b — første valg. To flater, ikon og tekst. DD-02: ALDRI et tredje valg, og
3
+ * aldri spørsmål om kategori eller alvorlighet. Fokus 3 i wireframen er venstre
4
+ * flate, deretter høyre.
5
+ */
6
+ export declare function Valg({ onVelg }: {
7
+ onVelg: (spor: 'feil' | 'onske') => void;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,48 @@
1
+ import { type InnspelClient, type OpenFeedbackOptions } from '@innspel/core';
2
+ import type { ScreenshotAdapter } from '../skjermbilde/typer.js';
3
+ export type Steg = 'valg' | 'kjenteFeil' | 'felt' | 'skjermbilde' | 'kvittering' | 'mine';
4
+ /** Det innsenderen har skrevet — det som skal overleve en ny innlogging (tech-decisions.md, «Portalen»). */
5
+ export interface Utkast {
6
+ track: 'feil' | 'onske';
7
+ text: string;
8
+ }
9
+ export interface ÅpneOpsjoner extends OpenFeedbackOptions {
10
+ /** Gjenopprettet utkast, typisk etter «Logg inn på nytt». */
11
+ draft?: Utkast;
12
+ }
13
+ export interface WidgetProps {
14
+ client: InnspelClient;
15
+ open: boolean;
16
+ onClose: () => void;
17
+ apneOpsjoner?: ÅpneOpsjoner;
18
+ /** Leietakerens navn — på svaret i «Mine innspill» og i «Logg inn på nytt hos …». */
19
+ tenantName?: string;
20
+ /** Fangst av siden. `null` = «Ta bilde av siden» finnes ikke; «Last opp bilde» finnes likevel når leietakeren har skjermbilde på. */
21
+ screenshot: ScreenshotAdapter | null;
22
+ colorScheme?: 'dark' | 'light';
23
+ returnFocusTo?: HTMLElement | null;
24
+ /**
25
+ * Tokenet utløp mens innsenderen skrev. Verten får utkastet og tar innsenderen
26
+ * til ny innlogging; teksten går ikke tapt. Uten denne vises bare feilen.
27
+ */
28
+ onIdentityExpired?: (utkast: Utkast) => void;
29
+ /**
30
+ * «Send anonymt» etter utløpt token: verten gjør klienten anonym (portalen:
31
+ * logger ut), og widgeten sender på nytt når `client.isAnonymous` er sann.
32
+ */
33
+ onSendAnonymously?: () => void;
34
+ }
35
+ /**
36
+ * Widgeten — samme flyt som `@innspel/react-native`, i en dialog (DD-47).
37
+ *
38
+ * Flyten er ikke symmetrisk: feilsporet møter kjente feil FØR feltet,
39
+ * ønskesporet ETTER. Skjermbildet lages først ved trykk, aldri ved åpning (K4),
40
+ * og finnes bare når leietakeren har skjermbilde på (DD-26). Teksten sendes
41
+ * først; bildet kan feile alene (DD-42). Køen tømmes når dialogen ÅPNES, aldri
42
+ * ved oppstart (DD-41).
43
+ *
44
+ * Web-spesifikt: `screen` er URL-stien uten query og hash når verten ikke gir
45
+ * et rutenavn; kvitteringen sier «Sendt anonymt» når klienten er anonym; og et
46
+ * utløpt token under skriving beholder utkastet.
47
+ */
48
+ export declare function Widget({ client, open, onClose, apneOpsjoner, tenantName, screenshot, colorScheme, returnFocusTo, onIdentityExpired, onSendAnonymously }: WidgetProps): import("react").JSX.Element;
@@ -0,0 +1,63 @@
1
+ import type { ReactNode } from 'react';
2
+ /**
3
+ * Primitivene widgeten og portalen er bygget av. Alt visuelt ligger i
4
+ * styles.css og kommer fra tokens — ingen hex, ingen px, ingen fontstørrelse
5
+ * her (CLAUDE.md, «Styling»). Minste trykkflate er `--innspel-layout-touch`;
6
+ * den gjelder også mus og tastatur på web.
7
+ */
8
+ type Variant = 'primary' | 'secondary' | 'ghost' | 'danger';
9
+ export declare function Btn({ label, onClick, variant, ikon, hint, disabled, ariaLabel, grow, tall, type, testId, autoFocus, }: {
10
+ label: string;
11
+ onClick?: () => void;
12
+ variant?: Variant;
13
+ ikon?: string;
14
+ hint?: string;
15
+ disabled?: boolean;
16
+ ariaLabel?: string;
17
+ grow?: boolean;
18
+ tall?: boolean;
19
+ type?: 'button' | 'submit';
20
+ testId?: string;
21
+ autoFocus?: boolean;
22
+ }): import("react").JSX.Element;
23
+ export declare function H({ children, sub, id, nivaa }: {
24
+ children: string;
25
+ sub?: string;
26
+ id?: string;
27
+ nivaa?: 1 | 2;
28
+ }): import("react").JSX.Element;
29
+ export declare function Hint({ children, id }: {
30
+ children: ReactNode;
31
+ id?: string;
32
+ }): import("react").JSX.Element;
33
+ export declare function P({ children }: {
34
+ children: ReactNode;
35
+ }): import("react").JSX.Element;
36
+ export declare function Card({ children, dashed, testId, role, ariaLabel, }: {
37
+ children: ReactNode;
38
+ dashed?: boolean;
39
+ testId?: string;
40
+ role?: string;
41
+ ariaLabel?: string;
42
+ }): import("react").JSX.Element;
43
+ /**
44
+ * Status som ikon + etikett. Fargen er et TILLEGG, aldri bæreren: to av
45
+ * palettens statusfarger er målt til under 1,5 i innbyrdes kontrast (DD-08).
46
+ */
47
+ export declare function StatusPill({ ikon, etikett, farge, }: {
48
+ ikon: string;
49
+ etikett: string;
50
+ farge: 'info' | 'success' | 'warning' | 'neutral' | 'danger';
51
+ }): import("react").JSX.Element;
52
+ /** Varsel. `live` gjør den til en live-region; kvitteringen og feil bruker det (DD-47). */
53
+ export declare function Banner({ ikon, tone, children, live, testId, }: {
54
+ ikon: string;
55
+ tone: 'info' | 'warning' | 'danger' | 'success';
56
+ children: ReactNode;
57
+ live?: boolean;
58
+ testId?: string;
59
+ }): import("react").JSX.Element;
60
+ export declare function Skeleton({ hoy }: {
61
+ hoy?: boolean;
62
+ }): import("react").JSX.Element;
63
+ export {};
@@ -0,0 +1,24 @@
1
+ import type { SubmissionStatus, SubmissionStatusView, SubmissionType } from '@innspel/core';
2
+ /**
3
+ * DD-22: `levert` heter «Rettet» i feilsporet og «Levert» i ønskesporet —
4
+ * samme maskinstatus, to navn. Klienten velger navnet; serveren gjør det ikke.
5
+ *
6
+ * DD-08 / «Styling» i CLAUDE.md: **farge bærer aldri mening alene.** Hver status
7
+ * har ikon OG tekstetikett. `brand ↔ info` og `brand ↔ danger` er målt til under
8
+ * 1,5 i kontrast mot hverandre av tema-generatoren, så etiketten er det som
9
+ * faktisk bærer meningen. Speiler `@innspel/react-native`, med vilje ordrett.
10
+ */
11
+ export interface StatusVisning {
12
+ ikon: string;
13
+ etikett: string;
14
+ /** Tokennavn i den aktive paletten, ikke en farge. */
15
+ farge: 'info' | 'success' | 'warning' | 'neutral' | 'danger';
16
+ lofte: string;
17
+ }
18
+ /** Norsk dato uten klokkeslett — fristen er en dato, ikke et tidspunkt (DD-28). */
19
+ export declare function formaterFrist(iso: string): string;
20
+ export declare function statusVisning(status: SubmissionStatus, type: SubmissionType, deadlineAt?: string): StatusVisning;
21
+ /** Raden som venter på nett (DD-41). Den har ingen saksnummer og intet løfte om svar. */
22
+ export declare const venterVisning: StatusVisning;
23
+ /** Etikett for en hel rad, som skjermleserens sammendrag. Ikonet er `aria-hidden`, så etiketten bærer alt. */
24
+ export declare function radEtikett(rad: SubmissionStatusView): string;